Compare commits
18 Commits
v1.0.4
...
kyle/check
| Author | SHA1 | Date | |
|---|---|---|---|
| 051106a233 | |||
| 33bbb5b98f | |||
| e089c6e4d1 | |||
| ceef6af3ae | |||
| e62cf3ee28 | |||
| 1a77f28ce4 | |||
| 4d84b352eb | |||
| 1892075d82 | |||
| 719862c842 | |||
| 655cc40162 | |||
| d98785e825 | |||
| 970a31e0d9 | |||
| 464ad8d1ae | |||
| 0cb7d36f2a | |||
| 09a6df0c33 | |||
| 69457c424c | |||
| 24c8040d8a | |||
| e869249a7c |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
|||||||
!.idea
|
!.idea
|
||||||
cmake-build*
|
cmake-build*
|
||||||
build
|
build
|
||||||
|
build-*
|
||||||
|
/imgui.ini
|
||||||
|
result
|
||||||
|
|||||||
7
.idea/codeStyles/Project.xml
generated
7
.idea/codeStyles/Project.xml
generated
@@ -141,6 +141,13 @@
|
|||||||
<pair source="c++m" header="" fileNamingConvention="NONE" />
|
<pair source="c++m" header="" fileNamingConvention="NONE" />
|
||||||
</extensions>
|
</extensions>
|
||||||
</files>
|
</files>
|
||||||
|
<codeStyleSettings language="CMake">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="8" />
|
||||||
|
<option name="TAB_SIZE" value="8" />
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
<codeStyleSettings language="ObjectiveC">
|
<codeStyleSettings language="ObjectiveC">
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
<option name="INDENT_SIZE" value="8" />
|
<option name="INDENT_SIZE" value="8" />
|
||||||
|
|||||||
321
.idea/workspace.xml
generated
321
.idea/workspace.xml
generated
@@ -1,321 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AutoImportSettings">
|
|
||||||
<option name="autoReloadType" value="SELECTIVE" />
|
|
||||||
</component>
|
|
||||||
<component name="BackendCodeEditorMiscSettings">
|
|
||||||
<option name="/Default/Environment/Hierarchy/GeneratedFilesCacheKey/Timestamp/@EntryValue" value="3" type="long" />
|
|
||||||
<option name="/Default/Housekeeping/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=SwitchToGoToActionSuggester/@EntryIndexedValue" value="true" type="bool" />
|
|
||||||
<option name="/Default/Housekeeping/GlobalSettingsUpgraded/IsUpgraded/@EntryValue" value="true" type="bool" />
|
|
||||||
<option name="/Default/Housekeeping/OptionsDialog/SelectedPageId/@EntryValue" value="CppFormatterOtherPage" type="string" />
|
|
||||||
<option name="/Default/Housekeeping/RefactoringsMru/RenameRefactoring/DoSearchForTextInStrings/@EntryValue" value="true" type="bool" />
|
|
||||||
<option name="/Default/RiderDebugger/RiderRestoreDecompile/RestoreDecompileSetting/@EntryValue" value="false" type="bool" />
|
|
||||||
</component>
|
|
||||||
<component name="CMakePresetLoader">{
|
|
||||||
"useNewFormat": true
|
|
||||||
}</component>
|
|
||||||
<component name="CMakeProjectFlavorService">
|
|
||||||
<option name="flavorId" value="CMakePlainProjectFlavor" />
|
|
||||||
</component>
|
|
||||||
<component name="CMakeReloadState">
|
|
||||||
<option name="reloaded" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="CMakeRunConfigurationManager">
|
|
||||||
<generated>
|
|
||||||
<config projectName="kte" targetName="kte" />
|
|
||||||
<config projectName="kte" targetName="imgui" />
|
|
||||||
<config projectName="kte" targetName="kge" />
|
|
||||||
</generated>
|
|
||||||
</component>
|
|
||||||
<component name="CMakeSettings" AUTO_RELOAD="true">
|
|
||||||
<configurations>
|
|
||||||
<configuration PROFILE_NAME="Debug" ENABLED="true" CONFIG_NAME="Debug" GENERATION_OPTIONS="-G "Unix Makefiles" -DKTE_USE_PIECE_TABLE:BOOL=ON -DBUILD_GUI:BOOL=ON" />
|
|
||||||
</configurations>
|
|
||||||
</component>
|
|
||||||
<component name="ChangeListManager">
|
|
||||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Actually add the screenshot.">
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" />
|
|
||||||
</list>
|
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
||||||
</component>
|
|
||||||
<component name="ClangdSettings">
|
|
||||||
<option name="formatViaClangd" value="false" />
|
|
||||||
</component>
|
|
||||||
<component name="ExecutionTargetManager" SELECTED_TARGET="CMakeBuildProfile:Debug" />
|
|
||||||
<component name="FormatOnSaveOptions">
|
|
||||||
<option name="myRunOnSave" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="Git.Settings">
|
|
||||||
<option name="PUSH_TAGS">
|
|
||||||
<GitPushTagMode>
|
|
||||||
<option name="argument" value="--tags" />
|
|
||||||
<option name="title" value="All" />
|
|
||||||
</GitPushTagMode>
|
|
||||||
</option>
|
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
||||||
<option name="UPDATE_TYPE" value="REBASE" />
|
|
||||||
</component>
|
|
||||||
<component name="HighlightingSettingsPerFile">
|
|
||||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
|
||||||
</component>
|
|
||||||
<component name="OptimizeOnSaveOptions">
|
|
||||||
<option name="myRunOnSave" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectApplicationVersion">
|
|
||||||
<option name="ide" value="CLion" />
|
|
||||||
<option name="majorVersion" value="2025" />
|
|
||||||
<option name="minorVersion" value="2.5" />
|
|
||||||
<option name="productBranch" value="Classic" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectColorInfo">{
|
|
||||||
"associatedIndex": 3
|
|
||||||
}</component>
|
|
||||||
<component name="ProjectId" id="36AlI8oyQOzOwSuZg6WxXf5LbHb" />
|
|
||||||
<component name="ProjectLevelVcsManager">
|
|
||||||
<OptionsSetting value="false" id="Update" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectViewState">
|
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
|
||||||
<option name="showLibraryContents" value="true" />
|
|
||||||
<option name="sortByType" value="true" />
|
|
||||||
<option name="sortKey" value="BY_TYPE" />
|
|
||||||
</component>
|
|
||||||
<component name="PropertiesComponent">{
|
|
||||||
"keyToString": {
|
|
||||||
"CMake Application.kge.executor": "Run",
|
|
||||||
"CMake Application.test_example.executor": "Run",
|
|
||||||
"CMake Application.test_undo.executor": "Run",
|
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
||||||
"NIXITCH_NIXPKGS_CONFIG": "",
|
|
||||||
"NIXITCH_NIX_CONF_DIR": "",
|
|
||||||
"NIXITCH_NIX_OTHER_STORES": "",
|
|
||||||
"NIXITCH_NIX_PATH": "",
|
|
||||||
"NIXITCH_NIX_PROFILES": "",
|
|
||||||
"NIXITCH_NIX_REMOTE": "",
|
|
||||||
"NIXITCH_NIX_USER_PROFILE_DIR": "",
|
|
||||||
"RunOnceActivity.RadMigrateCodeStyle": "true",
|
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
||||||
"RunOnceActivity.cidr.known.project.marker": "true",
|
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
|
||||||
"RunOnceActivity.readMode.enableVisualFormatting": "true",
|
|
||||||
"RunOnceActivity.west.config.association.type.startup.service": "true",
|
|
||||||
"cf.first.check.clang-format": "false",
|
|
||||||
"cidr.known.project.marker": "true",
|
|
||||||
"code.cleanup.on.save": "true",
|
|
||||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
|
||||||
"git-widget-placeholder": "master",
|
|
||||||
"junie.onboarding.icon.badge.shown": "true",
|
|
||||||
"node.js.detected.package.eslint": "true",
|
|
||||||
"node.js.detected.package.tslint": "true",
|
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
|
||||||
"nodejs_package_manager_path": "npm",
|
|
||||||
"onboarding.tips.debug.path": "/Users/kyle/src/kte/main.cpp",
|
|
||||||
"rearrange.code.on.save": "true",
|
|
||||||
"settings.editor.selected.configurable": "editor.preferences.fonts.default",
|
|
||||||
"to.speed.mode.migration.done": "true",
|
|
||||||
"vue.rearranger.settings.migration": "true"
|
|
||||||
}
|
|
||||||
}</component>
|
|
||||||
<component name="RecentsManager">
|
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
|
||||||
<recent name="$PROJECT_DIR$/docs" />
|
|
||||||
</key>
|
|
||||||
</component>
|
|
||||||
<component name="RunManager" selected="CMake Application.kge">
|
|
||||||
<configuration default="true" type="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" 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="12773000" />
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764485311566</created>
|
|
||||||
<option name="number" value="00001" />
|
|
||||||
<option name="presentableId" value="LOCAL-00001" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764485311566</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00002" summary="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations. This uses either a GapBuffer or PieceTable depending on the compilation.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764486011231</created>
|
|
||||||
<option name="number" value="00002" />
|
|
||||||
<option name="presentableId" value="LOCAL-00002" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764486011231</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00003" summary="Handle end-of-file newline semantics and improve scroll alignment logic.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764486876984</created>
|
|
||||||
<option name="number" value="00003" />
|
|
||||||
<option name="presentableId" value="LOCAL-00003" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764486876984</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00004" summary="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764489870957</created>
|
|
||||||
<option name="number" value="00004" />
|
|
||||||
<option name="presentableId" value="LOCAL-00004" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764489870957</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00005" summary="Add non-linear undo/redo design documentation and improve `UndoSystem` with backspace batching and GUI integration fixes.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764496151303</created>
|
|
||||||
<option name="number" value="00005" />
|
|
||||||
<option name="presentableId" value="LOCAL-00005" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764496151303</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00006" summary="Add `TestFrontend` documentation and `UndoSystem` buffer reference update. - Document `TestFrontend` for programmatic testing, including examples and usage details. - Add `UpdateBufferReference` to `UndoSystem` to support updating buffer associations.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764500200942</created>
|
|
||||||
<option name="number" value="00006" />
|
|
||||||
<option name="presentableId" value="LOCAL-00006" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764500200942</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00007" summary="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure. - Delete `packaging.cmake` to streamline build system. - Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`. - Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing. - Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation. - Enhance kill ring operations and new prompt workflows in `Editor`.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764501532446</created>
|
|
||||||
<option name="number" value="00007" />
|
|
||||||
<option name="presentableId" value="LOCAL-00007" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764501532446</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00008" summary="Add man pages for `kge` and `kte` with installation targets in CMake. - Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples. - Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`. - Ensure `kge` man page installation is conditional on GUI being built.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764502480274</created>
|
|
||||||
<option name="number" value="00008" />
|
|
||||||
<option name="presentableId" value="LOCAL-00008" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764502480274</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00009" summary="Add GUI initialization updates and improve navigation commands. - Implement terminal detachment for GUI mode to enable terminal closure post-launch. - Add `+N` support for opening files at specific line numbers and refine cursor positioning. - Introduce `JumpToLine` command for direct navigation by line number. - Enhance mouse wheel handling for line-wise scrolling.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764505723411</created>
|
|
||||||
<option name="number" value="00009" />
|
|
||||||
<option name="presentableId" value="LOCAL-00009" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764505723411</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00010" summary="Refactor code for consistency and enhanced functionality. - Normalize path handling for buffer operations, supporting tilde expansion and absolute paths. - Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes. - Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`. - Refine keybindings and enhance existing commands for improved command flow. - Adjust GUI and terminal renderers to display total line counts alongside filenames. - Update coding style to align with project guidelines.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764550164829</created>
|
|
||||||
<option name="number" value="00010" />
|
|
||||||
<option name="presentableId" value="LOCAL-00010" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764550164829</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00011" summary="Add horizontal scrolling support and refactor mouse click handling in GUI. - Introduce horizontal scrolling with column offset synchronization in GUI. - Refactor mouse click handling for improved accuracy and viewport alignment. - Enhance tab expansion and cursor rendering logic for better user experience. - Replace redundant variable declarations in `Buffer` for cleaner code.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764551986561</created>
|
|
||||||
<option name="number" value="00011" />
|
|
||||||
<option name="presentableId" value="LOCAL-00011" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764551986561</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00012" summary="Introduce file picker and GUI configuration with enhancements. - Add visual file picker for GUI with toggle support. - Introduce `GUIConfig` class for loading GUI settings from configuration file. - Refactor window initialization to support dynamic sizing based on configuration. - Add macOS-specific handling for fullscreen behavior. - Improve header inclusion order and minor code cleanup.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764556512864</created>
|
|
||||||
<option name="number" value="00012" />
|
|
||||||
<option name="presentableId" value="LOCAL-00012" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764556512864</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00013" summary="Add buffer position display and documentation improvements. - Display buffer position prefix "[x/N]" in GUI and terminal renderers. - Improve `kte` and `kge` man pages with frontend usage details and project homepage. - Update README with GUI invocation instructions. - Bump version to 1.0.0.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764556854788</created>
|
|
||||||
<option name="number" value="00013" />
|
|
||||||
<option name="presentableId" value="LOCAL-00013" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764556854788</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00014" summary="Actually add the screenshot.">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1764557759844</created>
|
|
||||||
<option name="number" value="00014" />
|
|
||||||
<option name="presentableId" value="LOCAL-00014" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1764557759844</updated>
|
|
||||||
</task>
|
|
||||||
<option name="localTasksCounter" value="15" />
|
|
||||||
<servers />
|
|
||||||
</component>
|
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
|
||||||
<option name="version" value="3" />
|
|
||||||
</component>
|
|
||||||
<component name="VCPKGProject">
|
|
||||||
<isAutomaticCheckingOnLaunch value="false" />
|
|
||||||
<isAutomaticFoundErrors value="true" />
|
|
||||||
<isAutomaticReloadCMake value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="VcsManagerConfiguration">
|
|
||||||
<MESSAGE value="Refactoring" />
|
|
||||||
<MESSAGE value="Add undo/redo infrastructure and buffer management additions." />
|
|
||||||
<MESSAGE value="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations. This uses either a GapBuffer or PieceTable depending on the compilation." />
|
|
||||||
<MESSAGE value="Handle end-of-file newline semantics and improve scroll alignment logic." />
|
|
||||||
<MESSAGE value="Enable installation targets." />
|
|
||||||
<MESSAGE value="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity." />
|
|
||||||
<MESSAGE value="Add non-linear undo/redo design documentation and improve `UndoSystem` with backspace batching and GUI integration fixes." />
|
|
||||||
<MESSAGE value="Add `TestFrontend` documentation and `UndoSystem` buffer reference update. - Document `TestFrontend` for programmatic testing, including examples and usage details. - Add `UpdateBufferReference` to `UndoSystem` to support updating buffer associations." />
|
|
||||||
<MESSAGE value="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure. - Delete `packaging.cmake` to streamline build system. - Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`. - Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing. - Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation. - Enhance kill ring operations and new prompt workflows in `Editor`." />
|
|
||||||
<MESSAGE value="Add man pages for `kge` and `kte` with installation targets in CMake. - Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples. - Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`. - Ensure `kge` man page installation is conditional on GUI being built." />
|
|
||||||
<MESSAGE value="Add GUI initialization updates and improve navigation commands. - Implement terminal detachment for GUI mode to enable terminal closure post-launch. - Add `+N` support for opening files at specific line numbers and refine cursor positioning. - Introduce `JumpToLine` command for direct navigation by line number. - Enhance mouse wheel handling for line-wise scrolling." />
|
|
||||||
<MESSAGE value="Refactor code for consistency and enhanced functionality. - Normalize path handling for buffer operations, supporting tilde expansion and absolute paths. - Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes. - Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`. - Refine keybindings and enhance existing commands for improved command flow. - Adjust GUI and terminal renderers to display total line counts alongside filenames. - Update coding style to align with project guidelines." />
|
|
||||||
<MESSAGE value="Add horizontal scrolling support and refactor mouse click handling in GUI. - Introduce horizontal scrolling with column offset synchronization in GUI. - Refactor mouse click handling for improved accuracy and viewport alignment. - Enhance tab expansion and cursor rendering logic for better user experience. - Replace redundant variable declarations in `Buffer` for cleaner code." />
|
|
||||||
<MESSAGE value="Introduce file picker and GUI configuration with enhancements. - Add visual file picker for GUI with toggle support. - Introduce `GUIConfig` class for loading GUI settings from configuration file. - Refactor window initialization to support dynamic sizing based on configuration. - Add macOS-specific handling for fullscreen behavior. - Improve header inclusion order and minor code cleanup." />
|
|
||||||
<MESSAGE value="Add buffer position display and documentation improvements. - Display buffer position prefix "[x/N]" in GUI and terminal renderers. - Improve `kte` and `kge` man pages with frontend usage details and project homepage. - Update README with GUI invocation instructions. - Bump version to 1.0.0." />
|
|
||||||
<MESSAGE value="Actually add the screenshot." />
|
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="Actually add the screenshot." />
|
|
||||||
</component>
|
|
||||||
<component name="XSLT-Support.FileAssociations.UIState">
|
|
||||||
<expand />
|
|
||||||
<select />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
114
Buffer.cc
114
Buffer.cc
@@ -6,6 +6,10 @@
|
|||||||
#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"
|
||||||
|
#include "lsp/BufferChangeTracker.h"
|
||||||
|
|
||||||
|
|
||||||
Buffer::Buffer()
|
Buffer::Buffer()
|
||||||
@@ -16,6 +20,9 @@ Buffer::Buffer()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Buffer::~Buffer() = default;
|
||||||
|
|
||||||
|
|
||||||
Buffer::Buffer(const std::string &path)
|
Buffer::Buffer(const std::string &path)
|
||||||
{
|
{
|
||||||
std::string err;
|
std::string err;
|
||||||
@@ -36,12 +43,36 @@ Buffer::Buffer(const Buffer &other)
|
|||||||
filename_ = other.filename_;
|
filename_ = other.filename_;
|
||||||
is_file_backed_ = other.is_file_backed_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
dirty_ = other.dirty_;
|
dirty_ = other.dirty_;
|
||||||
|
read_only_ = other.read_only_;
|
||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -60,12 +91,32 @@ Buffer::operator=(const Buffer &other)
|
|||||||
filename_ = other.filename_;
|
filename_ = other.filename_;
|
||||||
is_file_backed_ = other.is_file_backed_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
dirty_ = other.dirty_;
|
dirty_ = other.dirty_;
|
||||||
|
read_only_ = other.read_only_;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,12 +133,18 @@ Buffer::Buffer(Buffer &&other) noexcept
|
|||||||
filename_(std::move(other.filename_)),
|
filename_(std::move(other.filename_)),
|
||||||
is_file_backed_(other.is_file_backed_),
|
is_file_backed_(other.is_file_backed_),
|
||||||
dirty_(other.dirty_),
|
dirty_(other.dirty_),
|
||||||
|
read_only_(other.read_only_),
|
||||||
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_),
|
||||||
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);
|
||||||
@@ -112,12 +169,19 @@ Buffer::operator=(Buffer &&other) noexcept
|
|||||||
filename_ = std::move(other.filename_);
|
filename_ = std::move(other.filename_);
|
||||||
is_file_backed_ = other.is_file_backed_;
|
is_file_backed_ = other.is_file_backed_;
|
||||||
dirty_ = other.dirty_;
|
dirty_ = other.dirty_;
|
||||||
|
read_only_ = other.read_only_;
|
||||||
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_;
|
||||||
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);
|
||||||
@@ -334,6 +398,30 @@ Buffer::AsString() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
Buffer::FullText() const
|
||||||
|
{
|
||||||
|
std::string out;
|
||||||
|
// Precompute size for fewer reallocations
|
||||||
|
std::size_t total = 0;
|
||||||
|
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
||||||
|
total += rows_[i].Size();
|
||||||
|
if (i + 1 < rows_.size())
|
||||||
|
total += 1; // for '\n'
|
||||||
|
}
|
||||||
|
out.reserve(total);
|
||||||
|
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
||||||
|
const char *d = rows_[i].Data();
|
||||||
|
std::size_t n = rows_[i].Size();
|
||||||
|
if (d && n)
|
||||||
|
out.append(d, n);
|
||||||
|
if (i + 1 < rows_.size())
|
||||||
|
out.push_back('\n');
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Raw editing APIs (no undo recording, cursor untouched) ---
|
// --- Raw editing APIs (no undo recording, cursor untouched) ---
|
||||||
void
|
void
|
||||||
Buffer::insert_text(int row, int col, std::string_view text)
|
Buffer::insert_text(int row, int col, std::string_view text)
|
||||||
@@ -366,12 +454,15 @@ Buffer::insert_text(int row, int col, std::string_view text)
|
|||||||
// Split line at x
|
// Split line at x
|
||||||
std::string tail = rows_[y].substr(x);
|
std::string tail = rows_[y].substr(x);
|
||||||
rows_[y].erase(x);
|
rows_[y].erase(x);
|
||||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
|
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
||||||
y += 1;
|
y += 1;
|
||||||
x = 0;
|
x = 0;
|
||||||
remain.erase(0, pos + 1);
|
remain.erase(0, pos + 1);
|
||||||
}
|
}
|
||||||
// Do not set dirty here; UndoSystem will manage state/dirty externally
|
// Do not set dirty here; UndoSystem will manage state/dirty externally
|
||||||
|
if (change_tracker_) {
|
||||||
|
change_tracker_->recordInsertion(row, col, std::string(text));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -410,6 +501,9 @@ Buffer::delete_text(int row, int col, std::size_t len)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (change_tracker_) {
|
||||||
|
change_tracker_->recordDeletion(row, col, len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -427,7 +521,7 @@ Buffer::split_line(int row, const int col)
|
|||||||
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
|
||||||
const auto tail = rows_[y].substr(x);
|
const auto tail = rows_[y].substr(x);
|
||||||
rows_[y].erase(x);
|
rows_[y].erase(x);
|
||||||
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
|
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -455,7 +549,7 @@ Buffer::insert_row(int row, const std::string_view text)
|
|||||||
row = 0;
|
row = 0;
|
||||||
if (static_cast<std::size_t>(row) > rows_.size())
|
if (static_cast<std::size_t>(row) > rows_.size())
|
||||||
row = static_cast<int>(rows_.size());
|
row = static_cast<int>(rows_.size());
|
||||||
rows_.insert(rows_.begin() + row, std::string(text));
|
rows_.insert(rows_.begin() + row, Line(std::string(text)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -483,3 +577,17 @@ Buffer::Undo() const
|
|||||||
{
|
{
|
||||||
return undo_sys_.get();
|
return undo_sys_.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
Buffer::SetChangeTracker(std::unique_ptr<kte::lsp::BufferChangeTracker> tracker)
|
||||||
|
{
|
||||||
|
change_tracker_ = std::move(tracker);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
kte::lsp::BufferChangeTracker *
|
||||||
|
Buffer::GetChangeTracker()
|
||||||
|
{
|
||||||
|
return change_tracker_.get();
|
||||||
|
}
|
||||||
154
Buffer.h
154
Buffer.h
@@ -12,12 +12,25 @@
|
|||||||
|
|
||||||
#include "AppendBuffer.h"
|
#include "AppendBuffer.h"
|
||||||
#include "UndoSystem.h"
|
#include "UndoSystem.h"
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include "syntax/HighlighterEngine.h"
|
||||||
|
#include "Highlight.h"
|
||||||
|
|
||||||
|
// Forward declarations to avoid heavy includes
|
||||||
|
namespace kte {
|
||||||
|
namespace lsp {
|
||||||
|
class BufferChangeTracker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Buffer {
|
class Buffer {
|
||||||
public:
|
public:
|
||||||
Buffer();
|
Buffer();
|
||||||
|
|
||||||
|
~Buffer();
|
||||||
|
|
||||||
Buffer(const Buffer &other);
|
Buffer(const Buffer &other);
|
||||||
|
|
||||||
Buffer &operator=(const Buffer &other);
|
Buffer &operator=(const Buffer &other);
|
||||||
@@ -77,13 +90,13 @@ public:
|
|||||||
Line() = default;
|
Line() = default;
|
||||||
|
|
||||||
|
|
||||||
Line(const char *s)
|
explicit Line(const char *s)
|
||||||
{
|
{
|
||||||
assign_from(s ? std::string(s) : std::string());
|
assign_from(s ? std::string(s) : std::string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Line(const std::string &s)
|
explicit Line(const std::string &s)
|
||||||
{
|
{
|
||||||
assign_from(s);
|
assign_from(s);
|
||||||
}
|
}
|
||||||
@@ -139,29 +152,38 @@ public:
|
|||||||
|
|
||||||
|
|
||||||
// conversions
|
// conversions
|
||||||
operator std::string() const
|
explicit operator std::string() const
|
||||||
{
|
{
|
||||||
return std::string(buf_.Data() ? buf_.Data() : "", buf_.Size());
|
return {buf_.Data() ? buf_.Data() : "", buf_.Size()};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// string-like API used by command/renderer layers (implemented via materialization for now)
|
// string-like API used by command/renderer layers (implemented via materialization for now)
|
||||||
std::string substr(std::size_t pos) const
|
[[nodiscard]] std::string substr(std::size_t pos) const
|
||||||
{
|
{
|
||||||
const std::size_t n = buf_.Size();
|
const std::size_t n = buf_.Size();
|
||||||
if (pos >= n)
|
if (pos >= n)
|
||||||
return std::string();
|
return {};
|
||||||
return std::string(buf_.Data() + pos, n - pos);
|
return {buf_.Data() + pos, n - pos};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::string substr(std::size_t pos, std::size_t len) const
|
[[nodiscard]] std::string substr(std::size_t pos, std::size_t len) const
|
||||||
{
|
{
|
||||||
const std::size_t n = buf_.Size();
|
const std::size_t n = buf_.Size();
|
||||||
if (pos >= n)
|
if (pos >= n)
|
||||||
return std::string();
|
return {};
|
||||||
const std::size_t take = (pos + len > n) ? (n - pos) : len;
|
const std::size_t take = (pos + len > n) ? (n - pos) : len;
|
||||||
return std::string(buf_.Data() + pos, take);
|
return {buf_.Data() + pos, take};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// minimal find() to support search within a line
|
||||||
|
[[nodiscard]] std::size_t find(const std::string &needle, const std::size_t pos = 0) const
|
||||||
|
{
|
||||||
|
// Materialize to std::string for now; Line is backed by AppendBuffer
|
||||||
|
const auto s = static_cast<std::string>(*this);
|
||||||
|
return s.find(needle, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -254,6 +276,15 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
|
||||||
|
// This does not mark the buffer as file-backed.
|
||||||
|
void SetVirtualName(const std::string &name)
|
||||||
|
{
|
||||||
|
filename_ = name;
|
||||||
|
is_file_backed_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] bool IsFileBacked() const
|
[[nodiscard]] bool IsFileBacked() const
|
||||||
{
|
{
|
||||||
return is_file_backed_;
|
return is_file_backed_;
|
||||||
@@ -266,20 +297,39 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void SetCursor(std::size_t x, std::size_t y)
|
// Read-only flag
|
||||||
|
[[nodiscard]] bool IsReadOnly() const
|
||||||
|
{
|
||||||
|
return read_only_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SetReadOnly(bool ro)
|
||||||
|
{
|
||||||
|
read_only_ = ro;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ToggleReadOnly()
|
||||||
|
{
|
||||||
|
read_only_ = !read_only_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SetCursor(const std::size_t x, const std::size_t y)
|
||||||
{
|
{
|
||||||
curx_ = x;
|
curx_ = x;
|
||||||
cury_ = y;
|
cury_ = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void SetRenderX(std::size_t rx)
|
void SetRenderX(const std::size_t rx)
|
||||||
{
|
{
|
||||||
rx_ = rx;
|
rx_ = rx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void SetOffsets(std::size_t row, std::size_t col)
|
void SetOffsets(const std::size_t row, const std::size_t col)
|
||||||
{
|
{
|
||||||
rowoffs_ = row;
|
rowoffs_ = row;
|
||||||
coloffs_ = col;
|
coloffs_ = col;
|
||||||
@@ -289,6 +339,12 @@ public:
|
|||||||
void SetDirty(bool d)
|
void SetDirty(bool d)
|
||||||
{
|
{
|
||||||
dirty_ = d;
|
dirty_ = d;
|
||||||
|
if (d) {
|
||||||
|
++version_;
|
||||||
|
if (highlighter_) {
|
||||||
|
highlighter_->InvalidateFrom(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -299,7 +355,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void SetMark(std::size_t x, std::size_t y)
|
void SetMark(const std::size_t x, const std::size_t y)
|
||||||
{
|
{
|
||||||
mark_set_ = true;
|
mark_set_ = true;
|
||||||
mark_curx_ = x;
|
mark_curx_ = x;
|
||||||
@@ -327,6 +383,59 @@ public:
|
|||||||
|
|
||||||
[[nodiscard]] std::string AsString() const;
|
[[nodiscard]] std::string AsString() const;
|
||||||
|
|
||||||
|
// Compose full text of this buffer with newlines between rows
|
||||||
|
[[nodiscard]] std::string FullText() 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);
|
||||||
@@ -344,7 +453,12 @@ public:
|
|||||||
// Undo system accessors (created per-buffer)
|
// Undo system accessors (created per-buffer)
|
||||||
UndoSystem *Undo();
|
UndoSystem *Undo();
|
||||||
|
|
||||||
const UndoSystem *Undo() const;
|
[[nodiscard]] const UndoSystem *Undo() const;
|
||||||
|
|
||||||
|
// LSP integration: optional change tracker
|
||||||
|
void SetChangeTracker(std::unique_ptr<kte::lsp::BufferChangeTracker> tracker);
|
||||||
|
|
||||||
|
kte::lsp::BufferChangeTracker *GetChangeTracker();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// State mirroring original C struct (without undo_tree)
|
// State mirroring original C struct (without undo_tree)
|
||||||
@@ -356,12 +470,22 @@ private:
|
|||||||
std::string filename_;
|
std::string filename_;
|
||||||
bool is_file_backed_ = false;
|
bool is_file_backed_ = false;
|
||||||
bool dirty_ = false;
|
bool dirty_ = false;
|
||||||
|
bool read_only_ = false;
|
||||||
bool mark_set_ = false;
|
bool mark_set_ = false;
|
||||||
std::size_t mark_curx_ = 0, mark_cury_ = 0;
|
std::size_t mark_curx_ = 0, mark_cury_ = 0;
|
||||||
|
|
||||||
// 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_;
|
||||||
|
|
||||||
|
// Optional LSP change tracker (absent by default)
|
||||||
|
std::unique_ptr<kte::lsp::BufferChangeTracker> change_tracker_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KTE_BUFFER_H
|
#endif // KTE_BUFFER_H
|
||||||
486
CMakeLists.txt
486
CMakeLists.txt
@@ -4,43 +4,48 @@ project(kte)
|
|||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(KTE_VERSION "1.0.4")
|
set(KTE_VERSION "1.2.0")
|
||||||
|
|
||||||
# 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.
|
||||||
set(BUILD_GUI OFF CACHE BOOL "Enable building the graphical version.")
|
set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.")
|
||||||
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
|
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_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.")
|
||||||
else ()
|
else ()
|
||||||
message(STATUS "Build system is NOT POSIX.")
|
message(STATUS "Build system is NOT POSIX.")
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
if (MSVC)
|
if (MSVC)
|
||||||
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
|
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
|
||||||
else ()
|
else ()
|
||||||
add_compile_options(
|
add_compile_options(
|
||||||
"-Wall"
|
"-Wall"
|
||||||
"-Wextra"
|
"-Wextra"
|
||||||
"-Werror"
|
"-Werror"
|
||||||
"$<$<CONFIG:DEBUG>:-g>"
|
"$<$<CONFIG:DEBUG>:-g>"
|
||||||
"$<$<CONFIG:RELEASE>:-O2>")
|
"$<$<CONFIG:RELEASE>:-O2>")
|
||||||
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
||||||
add_compile_options("-stdlib=libc++")
|
add_compile_options("-stdlib=libc++")
|
||||||
else ()
|
else ()
|
||||||
# nothing special for gcc at the moment
|
# nothing special for gcc at the moment
|
||||||
endif ()
|
endif ()
|
||||||
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}")
|
||||||
|
|
||||||
if (${BUILD_GUI})
|
if (${BUILD_GUI})
|
||||||
include(cmake/imgui.cmake)
|
include(cmake/imgui.cmake)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
# NCurses for terminal mode
|
# NCurses for terminal mode
|
||||||
@@ -49,157 +54,368 @@ set(CURSES_NEED_WIDE)
|
|||||||
find_package(Curses REQUIRED)
|
find_package(Curses REQUIRED)
|
||||||
include_directories(${CURSES_INCLUDE_DIR})
|
include_directories(${CURSES_INCLUDE_DIR})
|
||||||
|
|
||||||
set(COMMON_SOURCES
|
# Detect availability of get_wch (wide-char input) in the curses headers
|
||||||
GapBuffer.cc
|
include(CheckSymbolExists)
|
||||||
PieceTable.cc
|
set(CMAKE_REQUIRED_INCLUDES ${CURSES_INCLUDE_DIR})
|
||||||
Buffer.cc
|
check_symbol_exists(get_wch "ncurses.h" KTE_HAVE_GET_WCH_IN_NCURSES)
|
||||||
Editor.cc
|
if (NOT KTE_HAVE_GET_WCH_IN_NCURSES)
|
||||||
Command.cc
|
# Some systems expose curses headers as <curses.h>
|
||||||
KKeymap.cc
|
check_symbol_exists(get_wch "curses.h" KTE_HAVE_GET_WCH_IN_CURSES)
|
||||||
TerminalInputHandler.cc
|
endif ()
|
||||||
TerminalRenderer.cc
|
if (KTE_HAVE_GET_WCH_IN_NCURSES OR KTE_HAVE_GET_WCH_IN_CURSES)
|
||||||
TerminalFrontend.cc
|
add_compile_definitions(KTE_HAVE_GET_WCH)
|
||||||
TestInputHandler.cc
|
endif ()
|
||||||
TestRenderer.cc
|
|
||||||
TestFrontend.cc
|
set(SYNTAX_SOURCES
|
||||||
UndoNode.cc
|
syntax/HighlighterEngine.cc
|
||||||
UndoTree.cc
|
syntax/CppHighlighter.cc
|
||||||
UndoSystem.cc
|
syntax/HighlighterRegistry.cc
|
||||||
|
syntax/NullHighlighter.cc
|
||||||
|
syntax/JsonHighlighter.cc
|
||||||
|
syntax/MarkdownHighlighter.cc
|
||||||
|
syntax/ShellHighlighter.cc
|
||||||
|
syntax/GoHighlighter.cc
|
||||||
|
syntax/PythonHighlighter.cc
|
||||||
|
syntax/RustHighlighter.cc
|
||||||
|
syntax/LispHighlighter.cc
|
||||||
|
syntax/SqlHighlighter.cc
|
||||||
|
syntax/ErlangHighlighter.cc
|
||||||
|
syntax/ForthHighlighter.cc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set(COMMON_SOURCES
|
||||||
|
GapBuffer.cc
|
||||||
|
PieceTable.cc
|
||||||
|
Buffer.cc
|
||||||
|
Editor.cc
|
||||||
|
Command.cc
|
||||||
|
HelpText.cc
|
||||||
|
KKeymap.cc
|
||||||
|
TerminalInputHandler.cc
|
||||||
|
TerminalRenderer.cc
|
||||||
|
TerminalFrontend.cc
|
||||||
|
TestInputHandler.cc
|
||||||
|
TestRenderer.cc
|
||||||
|
TestFrontend.cc
|
||||||
|
UndoNode.cc
|
||||||
|
UndoTree.cc
|
||||||
|
UndoSystem.cc
|
||||||
|
lsp/UtfCodec.cc
|
||||||
|
lsp/BufferChangeTracker.cc
|
||||||
|
lsp/JsonRpcTransport.cc
|
||||||
|
lsp/LspProcessClient.cc
|
||||||
|
lsp/DiagnosticStore.cc
|
||||||
|
lsp/TerminalDiagnosticDisplay.cc
|
||||||
|
lsp/LspManager.cc
|
||||||
|
|
||||||
|
${SYNTAX_SOURCES}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (KTE_ENABLE_TREESITTER)
|
||||||
|
list(APPEND SYNTAX_SOURCES
|
||||||
|
syntax/TreeSitterHighlighter.cc)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(THEME_HEADERS
|
||||||
|
themes/EInk.h
|
||||||
|
themes/Gruvbox.h
|
||||||
|
themes/Nord.h
|
||||||
|
themes/Plan9.h
|
||||||
|
themes/Solarized.h
|
||||||
|
themes/ThemeHelpers.h
|
||||||
|
)
|
||||||
|
|
||||||
|
set(SYNTAX_HEADERS
|
||||||
|
syntax/LanguageHighlighter.h
|
||||||
|
syntax/HighlighterEngine.h
|
||||||
|
syntax/CppHighlighter.h
|
||||||
|
syntax/HighlighterRegistry.h
|
||||||
|
syntax/NullHighlighter.h
|
||||||
|
syntax/JsonHighlighter.h
|
||||||
|
syntax/MarkdownHighlighter.h
|
||||||
|
syntax/ShellHighlighter.h
|
||||||
|
syntax/GoHighlighter.h
|
||||||
|
syntax/PythonHighlighter.h
|
||||||
|
syntax/RustHighlighter.h
|
||||||
|
syntax/LispHighlighter.h
|
||||||
|
)
|
||||||
|
|
||||||
|
if (KTE_ENABLE_TREESITTER)
|
||||||
|
list(APPEND SYNTAX_HEADERS
|
||||||
|
syntax/TreeSitterHighlighter.h)
|
||||||
|
endif ()
|
||||||
|
|
||||||
set(COMMON_HEADERS
|
set(COMMON_HEADERS
|
||||||
GapBuffer.h
|
GapBuffer.h
|
||||||
PieceTable.h
|
PieceTable.h
|
||||||
Buffer.h
|
Buffer.h
|
||||||
Editor.h
|
Editor.h
|
||||||
AppendBuffer.h
|
AppendBuffer.h
|
||||||
Command.h
|
Command.h
|
||||||
KKeymap.h
|
HelpText.h
|
||||||
InputHandler.h
|
KKeymap.h
|
||||||
TerminalInputHandler.h
|
InputHandler.h
|
||||||
Renderer.h
|
TerminalInputHandler.h
|
||||||
TerminalRenderer.h
|
Renderer.h
|
||||||
Frontend.h
|
TerminalRenderer.h
|
||||||
TerminalFrontend.h
|
Frontend.h
|
||||||
TestInputHandler.h
|
TerminalFrontend.h
|
||||||
TestRenderer.h
|
TestInputHandler.h
|
||||||
TestFrontend.h
|
TestRenderer.h
|
||||||
UndoNode.h
|
TestFrontend.h
|
||||||
UndoTree.h
|
UndoNode.h
|
||||||
UndoSystem.h
|
UndoTree.h
|
||||||
|
UndoSystem.h
|
||||||
|
Highlight.h
|
||||||
|
lsp/UtfCodec.h
|
||||||
|
lsp/LspTypes.h
|
||||||
|
lsp/BufferChangeTracker.h
|
||||||
|
lsp/JsonRpcTransport.h
|
||||||
|
lsp/LspClient.h
|
||||||
|
lsp/LspProcessClient.h
|
||||||
|
lsp/Diagnostic.h
|
||||||
|
lsp/DiagnosticStore.h
|
||||||
|
lsp/DiagnosticDisplay.h
|
||||||
|
lsp/TerminalDiagnosticDisplay.h
|
||||||
|
lsp/LspManager.h
|
||||||
|
lsp/LspServerConfig.h
|
||||||
|
ext/json.h
|
||||||
|
ext/json_fwd.h
|
||||||
|
|
||||||
|
${THEME_HEADERS}
|
||||||
|
${SYNTAX_HEADERS}
|
||||||
)
|
)
|
||||||
|
|
||||||
# kte (terminal-first) executable
|
# kte (terminal-first) executable
|
||||||
add_executable(kte
|
add_executable(kte
|
||||||
main.cc
|
main.cc
|
||||||
${COMMON_SOURCES}
|
${COMMON_SOURCES}
|
||||||
${COMMON_HEADERS}
|
${COMMON_HEADERS}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (KTE_USE_PIECE_TABLE)
|
if (KTE_USE_PIECE_TABLE)
|
||||||
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
|
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||||
|
endif ()
|
||||||
|
if (KTE_UNDO_DEBUG)
|
||||||
|
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
target_link_libraries(kte ${CURSES_LIBRARIES})
|
target_link_libraries(kte ${CURSES_LIBRARIES})
|
||||||
|
# Ensure vendored headers (e.g., ext/json.h) are on the include path
|
||||||
|
target_include_directories(kte PRIVATE ${CMAKE_SOURCE_DIR}/ext)
|
||||||
|
|
||||||
|
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}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Man pages
|
# Man pages
|
||||||
install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
||||||
|
|
||||||
if (BUILD_TESTS)
|
if (BUILD_TESTS)
|
||||||
# test_undo executable for testing undo/redo system
|
# test_undo executable for testing undo/redo system
|
||||||
add_executable(test_undo
|
add_executable(test_undo
|
||||||
test_undo.cc
|
test_undo.cc
|
||||||
${COMMON_SOURCES}
|
${COMMON_SOURCES}
|
||||||
${COMMON_HEADERS}
|
${COMMON_HEADERS}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (KTE_USE_PIECE_TABLE)
|
if (KTE_USE_PIECE_TABLE)
|
||||||
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
|
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
if (KTE_UNDO_DEBUG)
|
||||||
|
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
|
||||||
target_link_libraries(test_undo ${CURSES_LIBRARIES})
|
target_link_libraries(test_undo ${CURSES_LIBRARIES})
|
||||||
|
# Ensure vendored headers (e.g., ext/json.h) are on the include path for tests as well
|
||||||
|
target_include_directories(test_undo PRIVATE ${CMAKE_SOURCE_DIR}/ext)
|
||||||
|
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 ()
|
||||||
|
|
||||||
|
# test_utfcodec executable for UTF conversion helpers
|
||||||
|
add_executable(test_utfcodec
|
||||||
|
test_utfcodec.cc
|
||||||
|
${COMMON_SOURCES}
|
||||||
|
${COMMON_HEADERS}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (KTE_USE_PIECE_TABLE)
|
||||||
|
target_compile_definitions(test_utfcodec PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (KTE_UNDO_DEBUG)
|
||||||
|
target_compile_definitions(test_utfcodec PRIVATE KTE_UNDO_DEBUG=1)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
|
||||||
|
target_link_libraries(test_utfcodec ${CURSES_LIBRARIES})
|
||||||
|
# Ensure vendored headers (e.g., ext/json.h) are on the include path for tests as well
|
||||||
|
target_include_directories(test_utfcodec PRIVATE ${CMAKE_SOURCE_DIR}/ext)
|
||||||
|
if (KTE_ENABLE_TREESITTER)
|
||||||
|
if (TREESITTER_INCLUDE_DIR)
|
||||||
|
target_include_directories(test_utfcodec PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||||
|
endif ()
|
||||||
|
if (TREESITTER_LIBRARY)
|
||||||
|
target_link_libraries(test_utfcodec ${TREESITTER_LIBRARY})
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
# test_transport executable for JSON-RPC framing
|
||||||
|
add_executable(test_transport
|
||||||
|
test_transport.cc
|
||||||
|
${COMMON_SOURCES}
|
||||||
|
${COMMON_HEADERS}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (KTE_USE_PIECE_TABLE)
|
||||||
|
target_compile_definitions(test_transport PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (KTE_UNDO_DEBUG)
|
||||||
|
target_compile_definitions(test_transport PRIVATE KTE_UNDO_DEBUG=1)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
|
||||||
|
target_link_libraries(test_transport ${CURSES_LIBRARIES})
|
||||||
|
# Ensure vendored headers (e.g., ext/json.h) are on the include path for tests as well
|
||||||
|
target_include_directories(test_transport PRIVATE ${CMAKE_SOURCE_DIR}/ext)
|
||||||
|
if (KTE_ENABLE_TREESITTER)
|
||||||
|
if (TREESITTER_INCLUDE_DIR)
|
||||||
|
target_include_directories(test_transport PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||||
|
endif ()
|
||||||
|
if (TREESITTER_LIBRARY)
|
||||||
|
target_link_libraries(test_transport ${TREESITTER_LIBRARY})
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
# test_lsp_decode executable for dispatcher decoding
|
||||||
|
add_executable(test_lsp_decode
|
||||||
|
test_lsp_decode.cc
|
||||||
|
${COMMON_SOURCES}
|
||||||
|
${COMMON_HEADERS}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (KTE_USE_PIECE_TABLE)
|
||||||
|
target_compile_definitions(test_lsp_decode PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (KTE_UNDO_DEBUG)
|
||||||
|
target_compile_definitions(test_lsp_decode PRIVATE KTE_UNDO_DEBUG=1)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
|
||||||
|
target_link_libraries(test_lsp_decode ${CURSES_LIBRARIES})
|
||||||
|
# Ensure vendored headers (e.g., ext/json.h) are on the include path for tests as well
|
||||||
|
target_include_directories(test_lsp_decode PRIVATE ${CMAKE_SOURCE_DIR}/ext)
|
||||||
|
if (KTE_ENABLE_TREESITTER)
|
||||||
|
if (TREESITTER_INCLUDE_DIR)
|
||||||
|
target_include_directories(test_lsp_decode PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||||
|
endif ()
|
||||||
|
if (TREESITTER_LIBRARY)
|
||||||
|
target_link_libraries(test_lsp_decode ${TREESITTER_LIBRARY})
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
if (${BUILD_GUI})
|
if (${BUILD_GUI})
|
||||||
target_sources(kte PRIVATE
|
target_sources(kte PRIVATE
|
||||||
Font.h
|
Font.h
|
||||||
GUIConfig.cc
|
GUIConfig.cc
|
||||||
GUIConfig.h
|
GUIConfig.h
|
||||||
GUIRenderer.cc
|
GUIRenderer.cc
|
||||||
GUIRenderer.h
|
GUIRenderer.h
|
||||||
GUIInputHandler.cc
|
GUIInputHandler.cc
|
||||||
GUIInputHandler.h
|
GUIInputHandler.h
|
||||||
GUIFrontend.cc
|
GUIFrontend.cc
|
||||||
GUIFrontend.h)
|
GUIFrontend.h)
|
||||||
target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
|
target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
|
||||||
target_link_libraries(kte imgui)
|
target_link_libraries(kte imgui)
|
||||||
|
|
||||||
# kge (GUI-first) executable
|
# kge (GUI-first) executable
|
||||||
add_executable(kge
|
add_executable(kge
|
||||||
main.cc
|
main.cc
|
||||||
${COMMON_SOURCES}
|
${COMMON_SOURCES}
|
||||||
${COMMON_HEADERS}
|
${COMMON_HEADERS}
|
||||||
GUIConfig.cc
|
GUIConfig.cc
|
||||||
GUIConfig.h
|
GUIConfig.h
|
||||||
GUIRenderer.cc
|
GUIRenderer.cc
|
||||||
GUIRenderer.h
|
GUIRenderer.h
|
||||||
GUIInputHandler.cc
|
GUIInputHandler.cc
|
||||||
GUIInputHandler.h
|
GUIInputHandler.h
|
||||||
GUIFrontend.cc
|
GUIFrontend.cc
|
||||||
GUIFrontend.h)
|
GUIFrontend.h)
|
||||||
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
|
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
|
||||||
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
if (KTE_UNDO_DEBUG)
|
||||||
|
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
||||||
|
endif ()
|
||||||
|
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
||||||
|
target_include_directories(kge PRIVATE ${CMAKE_SOURCE_DIR}/ext)
|
||||||
|
|
||||||
# On macOS, build kge as a proper .app bundle
|
# On macOS, build kge as a proper .app bundle
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
# Define the icon file
|
# Define the icon file
|
||||||
set(MACOSX_BUNDLE_ICON_FILE kge.icns)
|
set(MACOSX_BUNDLE_ICON_FILE kge.icns)
|
||||||
set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
|
set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
|
||||||
|
|
||||||
# Add icon to the target sources and mark it as a resource
|
# Add icon to the target sources and mark it as a resource
|
||||||
target_sources(kge PRIVATE ${kge_ICON})
|
target_sources(kge PRIVATE ${kge_ICON})
|
||||||
set_source_files_properties(${kge_ICON} PROPERTIES
|
set_source_files_properties(${kge_ICON} PROPERTIES
|
||||||
MACOSX_PACKAGE_LOCATION Resources)
|
MACOSX_PACKAGE_LOCATION Resources)
|
||||||
|
|
||||||
# Configure Info.plist with version and identifiers
|
# Configure Info.plist with version and identifiers
|
||||||
set(KGE_BUNDLE_ID "dev.wntrmute.kge")
|
set(KGE_BUNDLE_ID "dev.wntrmute.kge")
|
||||||
configure_file(
|
configure_file(
|
||||||
${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in
|
${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
|
${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
|
||||||
@ONLY)
|
@ONLY)
|
||||||
|
|
||||||
set_target_properties(kge PROPERTIES
|
set_target_properties(kge PROPERTIES
|
||||||
MACOSX_BUNDLE TRUE
|
MACOSX_BUNDLE TRUE
|
||||||
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
|
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
|
||||||
MACOSX_BUNDLE_BUNDLE_NAME "kge"
|
MACOSX_BUNDLE_BUNDLE_NAME "kge"
|
||||||
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
|
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
|
||||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
|
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
|
||||||
|
|
||||||
add_dependencies(kge kte)
|
add_dependencies(kge kte)
|
||||||
add_custom_command(TARGET kge POST_BUILD
|
add_custom_command(TARGET kge POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy
|
COMMAND ${CMAKE_COMMAND} -E copy
|
||||||
$<TARGET_FILE:kte>
|
$<TARGET_FILE:kte>
|
||||||
$<TARGET_FILE_DIR:kge>/kte
|
$<TARGET_FILE_DIR:kge>/kte
|
||||||
COMMENT "Copying kte binary into kge.app bundle")
|
COMMENT "Copying kte binary into kge.app bundle")
|
||||||
|
|
||||||
install(TARGETS kge
|
install(TARGETS kge
|
||||||
BUNDLE DESTINATION .
|
BUNDLE DESTINATION .
|
||||||
)
|
)
|
||||||
|
|
||||||
install(TARGETS kte
|
install(TARGETS kte
|
||||||
RUNTIME DESTINATION kge.app/Contents/MacOS
|
RUNTIME DESTINATION kge.app/Contents/MacOS
|
||||||
)
|
)
|
||||||
else ()
|
else ()
|
||||||
install(TARGETS kge
|
install(TARGETS kge
|
||||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
)
|
)
|
||||||
endif ()
|
endif ()
|
||||||
# Install kge man page only when GUI is built
|
# Install kge man page only when GUI is built
|
||||||
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
||||||
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
|
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
|
||||||
endif ()
|
endif ()
|
||||||
|
|||||||
1220
Command.cc
1220
Command.cc
File diff suppressed because it is too large
Load Diff
25
Command.h
25
Command.h
@@ -23,8 +23,10 @@ enum class CommandId {
|
|||||||
Refresh, // force redraw
|
Refresh, // force redraw
|
||||||
KPrefix, // show "C-k _" prompt in status when entering k-command
|
KPrefix, // show "C-k _" prompt in status when entering k-command
|
||||||
FindStart, // begin incremental search (placeholder)
|
FindStart, // begin incremental search (placeholder)
|
||||||
|
RegexFindStart, // begin regex search (C-r)
|
||||||
|
RegexpReplace, // begin regex search & replace (C-t)
|
||||||
|
SearchReplace, // begin search & replace (two-step prompt)
|
||||||
OpenFileStart, // begin open-file prompt
|
OpenFileStart, // begin open-file prompt
|
||||||
// GUI: visual file picker
|
|
||||||
VisualFilePickerToggle,
|
VisualFilePickerToggle,
|
||||||
// Buffers
|
// Buffers
|
||||||
BufferSwitchStart, // begin buffer switch prompt
|
BufferSwitchStart, // begin buffer switch prompt
|
||||||
@@ -67,10 +69,15 @@ 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 -)
|
||||||
ReflowParagraph, // reflow paragraph to column width (ESC q)
|
ReflowParagraph, // reflow paragraph to column width (ESC q)
|
||||||
|
// Read-only buffers
|
||||||
|
ToggleReadOnly, // toggle current buffer read-only (C-k ')
|
||||||
// Buffer operations
|
// Buffer operations
|
||||||
ReloadBuffer, // reload buffer from disk (C-k l)
|
ReloadBuffer, // reload buffer from disk (C-k l)
|
||||||
MarkAllAndJumpEnd, // set mark at beginning, jump to end (C-k a)
|
MarkAllAndJumpEnd, // set mark at beginning, jump to end (C-k a)
|
||||||
@@ -78,8 +85,22 @@ enum class CommandId {
|
|||||||
JumpToLine, // prompt for line and jump (C-k g)
|
JumpToLine, // prompt for line and jump (C-k g)
|
||||||
ShowWorkingDirectory, // Display the current working directory in the editor message.
|
ShowWorkingDirectory, // Display the current working directory in the editor message.
|
||||||
ChangeWorkingDirectory, // Change the editor's current directory.
|
ChangeWorkingDirectory, // Change the editor's current directory.
|
||||||
|
// Help
|
||||||
|
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>)
|
||||||
|
// LSP
|
||||||
|
LspHover,
|
||||||
|
LspGotoDefinition,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -103,6 +124,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
95
Editor.cc
95
Editor.cc
@@ -1,8 +1,14 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include "syntax/HighlighterRegistry.h"
|
||||||
|
#include "syntax/NullHighlighter.h"
|
||||||
|
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
|
#include "lsp/LspManager.h"
|
||||||
|
#include "syntax/HighlighterRegistry.h"
|
||||||
|
#include "syntax/CppHighlighter.h"
|
||||||
|
#include "syntax/NullHighlighter.h"
|
||||||
|
|
||||||
|
|
||||||
Editor::Editor() = default;
|
Editor::Editor() = default;
|
||||||
@@ -24,6 +30,15 @@ Editor::SetStatus(const std::string &message)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
Editor::NotifyBufferSaved(Buffer *buf)
|
||||||
|
{
|
||||||
|
if (lsp_manager_ && buf) {
|
||||||
|
lsp_manager_->onBufferSaved(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Buffer *
|
Buffer *
|
||||||
Editor::CurrentBuffer()
|
Editor::CurrentBuffer()
|
||||||
{
|
{
|
||||||
@@ -151,7 +166,36 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Notify LSP (if wired) for current buffer open
|
||||||
|
if (lsp_manager_) {
|
||||||
|
lsp_manager_->onBufferOpened(&cur);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,9 +203,37 @@ 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);
|
||||||
|
// Notify LSP (if wired) for current buffer open
|
||||||
|
if (lsp_manager_) {
|
||||||
|
lsp_manager_->onBufferOpened(&buffers_[curbuf_]);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +245,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
72
Editor.h
72
Editor.h
@@ -11,6 +11,13 @@
|
|||||||
|
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
|
|
||||||
|
// fwd decl for LSP wiring
|
||||||
|
namespace kte {
|
||||||
|
namespace lsp {
|
||||||
|
class LspManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Editor {
|
class Editor {
|
||||||
public:
|
public:
|
||||||
@@ -302,7 +309,22 @@ public:
|
|||||||
|
|
||||||
|
|
||||||
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
|
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
|
||||||
enum class PromptKind { None = 0, Search, OpenFile, SaveAs, Confirm, BufferSwitch, GotoLine, Chdir };
|
enum class PromptKind {
|
||||||
|
None = 0,
|
||||||
|
Search,
|
||||||
|
RegexSearch,
|
||||||
|
RegexReplaceFind, // step 1 of Regex Search & Replace: find pattern
|
||||||
|
RegexReplaceWith, // step 2 of Regex Search & Replace: replacement text
|
||||||
|
OpenFile,
|
||||||
|
SaveAs,
|
||||||
|
Confirm,
|
||||||
|
BufferSwitch,
|
||||||
|
GotoLine,
|
||||||
|
Chdir,
|
||||||
|
ReplaceFind, // step 1 of Search & Replace: find what
|
||||||
|
ReplaceWith, // step 2 of Search & Replace: replace with
|
||||||
|
Command // generic command prompt (": ")
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
|
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
|
||||||
@@ -421,6 +443,22 @@ public:
|
|||||||
|
|
||||||
bool OpenFile(const std::string &path, std::string &err);
|
bool OpenFile(const std::string &path, std::string &err);
|
||||||
|
|
||||||
|
// LSP: attach/detach manager
|
||||||
|
void SetLspManager(kte::lsp::LspManager *mgr)
|
||||||
|
{
|
||||||
|
lsp_manager_ = mgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// LSP helpers: trigger hover/definition at current cursor in current buffer
|
||||||
|
bool LspHoverAtCursor();
|
||||||
|
|
||||||
|
bool LspGotoDefinitionAtCursor();
|
||||||
|
|
||||||
|
|
||||||
|
// LSP: notify buffer saved (used by commands)
|
||||||
|
void NotifyBufferSaved(Buffer *buf);
|
||||||
|
|
||||||
// Buffer switching/closing
|
// Buffer switching/closing
|
||||||
bool SwitchTo(std::size_t index);
|
bool SwitchTo(std::size_t index);
|
||||||
|
|
||||||
@@ -507,6 +545,38 @@ private:
|
|||||||
// GUI-only state (safe no-op in terminal builds)
|
// GUI-only state (safe no-op in terminal builds)
|
||||||
bool file_picker_visible_ = false;
|
bool file_picker_visible_ = false;
|
||||||
std::string file_picker_dir_;
|
std::string file_picker_dir_;
|
||||||
|
|
||||||
|
// Temporary state for Search & Replace flow
|
||||||
|
public:
|
||||||
|
void SetReplaceFindTmp(const std::string &s)
|
||||||
|
{
|
||||||
|
replace_find_tmp_ = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SetReplaceWithTmp(const std::string &s)
|
||||||
|
{
|
||||||
|
replace_with_tmp_ = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] const std::string &ReplaceFindTmp() const
|
||||||
|
{
|
||||||
|
return replace_find_tmp_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] const std::string &ReplaceWithTmp() const
|
||||||
|
{
|
||||||
|
return replace_with_tmp_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string replace_find_tmp_;
|
||||||
|
std::string replace_with_tmp_;
|
||||||
|
|
||||||
|
// Non-owning pointer to LSP manager (if provided)
|
||||||
|
kte::lsp::LspManager *lsp_manager_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KTE_EDITOR_H
|
#endif // KTE_EDITOR_H
|
||||||
27
GUIConfig.cc
27
GUIConfig.cc
@@ -23,7 +23,7 @@ default_config_path()
|
|||||||
{
|
{
|
||||||
const char *home = std::getenv("HOME");
|
const char *home = std::getenv("HOME");
|
||||||
if (!home || !*home)
|
if (!home || !*home)
|
||||||
return std::string();
|
return {};
|
||||||
std::string path(home);
|
std::string path(home);
|
||||||
path += "/.config/kte/kge.ini";
|
path += "/.config/kte/kge.ini";
|
||||||
return path;
|
return path;
|
||||||
@@ -34,7 +34,8 @@ GUIConfig
|
|||||||
GUIConfig::Load()
|
GUIConfig::Load()
|
||||||
{
|
{
|
||||||
GUIConfig cfg; // defaults already set
|
GUIConfig cfg; // defaults already set
|
||||||
std::string path = default_config_path();
|
const std::string path = default_config_path();
|
||||||
|
|
||||||
if (!path.empty()) {
|
if (!path.empty()) {
|
||||||
cfg.LoadFromFile(path);
|
cfg.LoadFromFile(path);
|
||||||
}
|
}
|
||||||
@@ -98,8 +99,28 @@ GUIConfig::LoadFromFile(const std::string &path)
|
|||||||
try {
|
try {
|
||||||
v = std::stof(val);
|
v = std::stof(val);
|
||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
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
GUIConfig.h
16
GUIConfig.h
@@ -12,10 +12,18 @@
|
|||||||
|
|
||||||
class GUIConfig {
|
class GUIConfig {
|
||||||
public:
|
public:
|
||||||
bool fullscreen = false;
|
bool fullscreen = false;
|
||||||
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();
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
#include "GUIFrontend.h"
|
#include "GUIFrontend.h"
|
||||||
#include "Font.h" // embedded default font (DefaultFontRegular)
|
#include "Font.h" // embedded default font (DefaultFontRegular)
|
||||||
#include "GUIConfig.h"
|
#include "GUIConfig.h"
|
||||||
|
#include "GUITheme.h"
|
||||||
|
#include "syntax/HighlighterRegistry.h"
|
||||||
|
#include "syntax/NullHighlighter.h"
|
||||||
|
|
||||||
|
|
||||||
#ifndef KTE_FONT_SIZE
|
#ifndef KTE_FONT_SIZE
|
||||||
@@ -31,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);
|
||||||
@@ -46,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{};
|
||||||
@@ -60,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{};
|
||||||
@@ -74,7 +77,7 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
}
|
}
|
||||||
|
|
||||||
window_ = SDL_CreateWindow(
|
window_ = SDL_CreateWindow(
|
||||||
"kge - kyle's text editor " KTE_VERSION_STR,
|
"kge - kyle's graphical editor " KTE_VERSION_STR,
|
||||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||||
width_, height_,
|
width_, height_,
|
||||||
win_flags);
|
win_flags);
|
||||||
@@ -85,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,6 +108,45 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
(void) io;
|
(void) io;
|
||||||
ImGui::StyleColorsDark();
|
ImGui::StyleColorsDark();
|
||||||
|
|
||||||
|
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
||||||
|
if (cfg.background == "light")
|
||||||
|
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||||
|
else
|
||||||
|
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||||
|
kte::ApplyThemeByName(cfg.theme);
|
||||||
|
|
||||||
|
// Apply default syntax highlighting preference from GUI config to the current buffer
|
||||||
|
if (Buffer *b = ed.CurrentBuffer()) {
|
||||||
|
if (cfg.syntax) {
|
||||||
|
b->SetSyntaxEnabled(true);
|
||||||
|
// Ensure a highlighter is available if possible
|
||||||
|
b->EnsureHighlighter();
|
||||||
|
if (auto *eng = b->Highlighter()) {
|
||||||
|
if (!eng->HasHighlighter()) {
|
||||||
|
// Try detect from filename and first line; fall back to cpp or existing filetype
|
||||||
|
std::string first_line;
|
||||||
|
const auto &rows = b->Rows();
|
||||||
|
if (!rows.empty())
|
||||||
|
first_line = static_cast<std::string>(rows[0]);
|
||||||
|
std::string ft = kte::HighlighterRegistry::DetectForPath(
|
||||||
|
b->Filename(), first_line);
|
||||||
|
if (!ft.empty()) {
|
||||||
|
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||||
|
b->SetFiletype(ft);
|
||||||
|
eng->InvalidateFrom(0);
|
||||||
|
} else {
|
||||||
|
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
|
||||||
|
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||||
|
b->SetFiletype("");
|
||||||
|
eng->InvalidateFrom(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b->SetSyntaxEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
||||||
return false;
|
return false;
|
||||||
if (!ImGui_ImplOpenGL3_Init(kGlslVersion))
|
if (!ImGui_ImplOpenGL3_Init(kGlslVersion))
|
||||||
@@ -132,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;
|
||||||
}
|
}
|
||||||
@@ -211,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)));
|
||||||
@@ -261,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 *) DefaultFontRegularCompressedData,
|
DefaultFontBoldCompressedData,
|
||||||
(int) DefaultFontRegularCompressedSize,
|
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_{};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <algorithm>
|
||||||
#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"
|
||||||
@@ -91,12 +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:
|
||||||
// Insert a literal tab character
|
// Insert a literal tab character when not interpreting a k-prefix suffix.
|
||||||
out.hasCommand = true;
|
// If k-prefix is active, let the k-prefix handler below consume the key
|
||||||
out.id = CommandId::InsertText;
|
// (so Tab doesn't leave k-prefix stuck).
|
||||||
out.arg = "\t";
|
if (!k_prefix) {
|
||||||
out.count = 0;
|
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};
|
||||||
@@ -281,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
|
||||||
@@ -348,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) {
|
||||||
@@ -520,11 +538,21 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!k_prefix_ && e.text.text[0] != '\0') {
|
if (!k_prefix_ && e.text.text[0] != '\0') {
|
||||||
mi.hasCommand = true;
|
// Ensure InsertText never carries a newline; those must originate from KEYDOWN
|
||||||
mi.id = CommandId::InsertText;
|
std::string text(e.text.text);
|
||||||
mi.arg = std::string(e.text.text);
|
// Strip any CR/LF that might slip through from certain platforms/IME behaviors
|
||||||
mi.count = 0;
|
text.erase(std::remove(text.begin(), text.end(), '\n'), text.end());
|
||||||
produced = true;
|
text.erase(std::remove(text.begin(), text.end(), '\r'), text.end());
|
||||||
|
if (!text.empty()) {
|
||||||
|
mi.hasCommand = true;
|
||||||
|
mi.id = CommandId::InsertText;
|
||||||
|
mi.arg = std::move(text);
|
||||||
|
mi.count = 0;
|
||||||
|
produced = true;
|
||||||
|
} else {
|
||||||
|
// Nothing to insert after filtering; consume the event
|
||||||
|
produced = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
produced = true; // consumed while k-prefix is active
|
produced = true; // consumed while k-prefix is active
|
||||||
}
|
}
|
||||||
|
|||||||
634
GUIRenderer.cc
634
GUIRenderer.cc
@@ -7,8 +7,11 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
#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"
|
||||||
@@ -43,6 +46,7 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
ImGui::SetNextWindowSize(main_sz);
|
ImGui::SetNextWindowSize(main_sz);
|
||||||
|
|
||||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar
|
||||||
|
| ImGuiWindowFlags_NoScrollbar
|
||||||
| ImGuiWindowFlags_NoResize
|
| ImGuiWindowFlags_NoResize
|
||||||
| ImGuiWindowFlags_NoMove
|
| ImGuiWindowFlags_NoMove
|
||||||
| ImGuiWindowFlags_NoCollapse
|
| ImGuiWindowFlags_NoCollapse
|
||||||
@@ -55,7 +59,7 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.f, 6.f));
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.f, 6.f));
|
||||||
|
|
||||||
ImGui::Begin("kte", nullptr, flags);
|
ImGui::Begin("kte", nullptr, flags | ImGuiWindowFlags_NoScrollWithMouse);
|
||||||
|
|
||||||
const Buffer *buf = ed.CurrentBuffer();
|
const Buffer *buf = ed.CurrentBuffer();
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
@@ -64,7 +68,7 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
const auto &lines = buf->Rows();
|
const auto &lines = buf->Rows();
|
||||||
// Reserve space for status bar at bottom
|
// Reserve space for status bar at bottom
|
||||||
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
|
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
|
||||||
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
ImGuiWindowFlags_HorizontalScrollbar);
|
||||||
// Detect click-to-move inside this scroll region
|
// Detect click-to-move inside this scroll region
|
||||||
ImVec2 list_origin = ImGui::GetCursorScreenPos();
|
ImVec2 list_origin = ImGui::GetCursorScreenPos();
|
||||||
float scroll_y = ImGui::GetScrollY();
|
float scroll_y = ImGui::GetScrollY();
|
||||||
@@ -104,13 +108,13 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
// If user scrolled, update buffer offsets accordingly
|
// If user scrolled, update buffer offsets accordingly
|
||||||
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
|
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
|
||||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
if (auto mbuf = const_cast<Buffer *>(buf)) {
|
||||||
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
||||||
mbuf->Coloffs());
|
mbuf->Coloffs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x) {
|
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x) {
|
||||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
if (auto mbuf = const_cast<Buffer *>(buf)) {
|
||||||
mbuf->SetOffsets(mbuf->Rowoffs(),
|
mbuf->SetOffsets(mbuf->Rowoffs(),
|
||||||
static_cast<std::size_t>(std::max(0L, scroll_left)));
|
static_cast<std::size_t>(std::max(0L, scroll_left)));
|
||||||
}
|
}
|
||||||
@@ -150,15 +154,23 @@ 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 dependency on drawn items
|
||||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||||
// Compute viewport-relative row so (0) is top row of the visible area
|
// Compute viewport-relative row so (0) is top row of the visible area
|
||||||
float vy_f = (mp.y - list_origin.y - scroll_y) / row_h;
|
// Note: list_origin is already in the scrolled space of the child window,
|
||||||
long vy = static_cast<long>(vy_f);
|
// so we must NOT subtract scroll_y again (would double-apply).
|
||||||
if (vy < 0)
|
float vy_f = (mp.y - list_origin.y) / row_h;
|
||||||
vy = 0;
|
long vy = static_cast<long>(vy_f);
|
||||||
|
if (vy < 0)
|
||||||
|
vy = 0;
|
||||||
|
|
||||||
// Clamp vy within visible content height to avoid huge jumps
|
// Clamp vy within visible content height to avoid huge jumps
|
||||||
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
||||||
@@ -170,93 +182,160 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
if (vy >= vis_rows)
|
if (vy >= vis_rows)
|
||||||
vy = vis_rows - 1;
|
vy = vis_rows - 1;
|
||||||
|
|
||||||
// Translate viewport row to buffer row using Buffer::Rowoffs
|
// Translate viewport row to buffer row using Buffer::Rowoffs
|
||||||
std::size_t by = buf->Rowoffs() + static_cast<std::size_t>(vy);
|
std::size_t by = buf->Rowoffs() + static_cast<std::size_t>(vy);
|
||||||
if (by >= lines.size()) {
|
if (by >= lines.size()) {
|
||||||
if (!lines.empty())
|
if (!lines.empty())
|
||||||
by = lines.size() - 1;
|
by = lines.size() - 1;
|
||||||
else
|
else
|
||||||
by = 0;
|
by = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute desired pixel X inside the viewport content (subtract horizontal scroll)
|
// Compute desired pixel X inside the viewport content.
|
||||||
float px = (mp.x - list_origin.x - scroll_x);
|
// list_origin is already scrolled; do not subtract scroll_x here.
|
||||||
if (px < 0.0f)
|
float px = (mp.x - list_origin.x);
|
||||||
px = 0.0f;
|
if (px < 0.0f)
|
||||||
|
px = 0.0f;
|
||||||
|
|
||||||
// Empty buffer guard: if there are no lines yet, just move to 0:0
|
// Empty buffer guard: if there are no lines yet, just move to 0:0
|
||||||
if (lines.empty()) {
|
if (lines.empty()) {
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string("0:0"));
|
Execute(ed, CommandId::MoveCursorTo, std::string("0:0"));
|
||||||
} else {
|
} else {
|
||||||
// Convert pixel X to a render-column target including horizontal col offset
|
// Convert pixel X to a render-column target including horizontal col offset
|
||||||
// Use our own tab expansion of width 8 to match command layer logic.
|
// Use our own tab expansion of width 8 to match command layer logic.
|
||||||
const std::string &line_clicked = lines[by];
|
std::string line_clicked = static_cast<std::string>(lines[by]);
|
||||||
const std::size_t tabw = 8;
|
const std::size_t tabw = 8;
|
||||||
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
|
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
|
||||||
// then translate to viewport-space by subtracting Coloffs.
|
// then translate to viewport-space by subtracting Coloffs.
|
||||||
std::size_t coloffs = buf->Coloffs();
|
std::size_t coloffs = buf->Coloffs();
|
||||||
std::size_t rx_abs = 0; // absolute rendered column
|
std::size_t rx_abs = 0; // absolute rendered column
|
||||||
std::size_t i = 0; // source column iterator
|
std::size_t i = 0; // source column iterator
|
||||||
|
|
||||||
// Fast-forward i until rx_abs >= coloffs to align with leftmost visible column
|
// Fast-forward i until rx_abs >= coloffs to align with leftmost visible column
|
||||||
if (!line_clicked.empty() && coloffs > 0) {
|
if (!line_clicked.empty() && coloffs > 0) {
|
||||||
while (i < line_clicked.size() && rx_abs < coloffs) {
|
while (i < line_clicked.size() && rx_abs < coloffs) {
|
||||||
if (line_clicked[i] == '\t') {
|
if (line_clicked[i] == '\t') {
|
||||||
rx_abs += (tabw - (rx_abs % tabw));
|
rx_abs += (tabw - (rx_abs % tabw));
|
||||||
} else {
|
} else {
|
||||||
rx_abs += 1;
|
rx_abs += 1;
|
||||||
}
|
}
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now search for closest source column to clicked px within/after viewport
|
// Now search for closest source column to clicked px within/after viewport
|
||||||
std::size_t best_col = i; // default to first visible column
|
std::size_t best_col = i; // default to first visible column
|
||||||
float best_dist = std::numeric_limits<float>::infinity();
|
float best_dist = std::numeric_limits<float>::infinity();
|
||||||
while (true) {
|
while (true) {
|
||||||
// For i in [current..size], evaluate candidate including the implicit end position
|
// For i in [current..size], evaluate candidate including the implicit end position
|
||||||
std::size_t rx_view = (rx_abs >= coloffs) ? (rx_abs - coloffs) : 0;
|
std::size_t rx_view = (rx_abs >= coloffs) ? (rx_abs - coloffs) : 0;
|
||||||
float rx_px = static_cast<float>(rx_view) * space_w;
|
float rx_px = static_cast<float>(rx_view) * space_w;
|
||||||
float dist = std::fabs(px - rx_px);
|
float dist = std::fabs(px - rx_px);
|
||||||
if (dist <= best_dist) {
|
if (dist <= best_dist) {
|
||||||
best_dist = dist;
|
best_dist = dist;
|
||||||
best_col = i;
|
best_col = i;
|
||||||
}
|
}
|
||||||
if (i == line_clicked.size())
|
if (i == line_clicked.size())
|
||||||
break;
|
break;
|
||||||
// advance to next source column
|
// advance to next source column
|
||||||
if (line_clicked[i] == '\t') {
|
if (line_clicked[i] == '\t') {
|
||||||
rx_abs += (tabw - (rx_abs % tabw));
|
rx_abs += (tabw - (rx_abs % tabw));
|
||||||
} else {
|
} else {
|
||||||
rx_abs += 1;
|
rx_abs += 1;
|
||||||
}
|
}
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch absolute buffer coordinates (row:col)
|
// Dispatch absolute buffer coordinates (row:col)
|
||||||
char tmp[64];
|
char tmp[64];
|
||||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col);
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col);
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Cache current horizontal offset in rendered columns
|
// Cache current horizontal offset in rendered columns
|
||||||
const std::size_t coloffs_now = buf->Coloffs();
|
const std::size_t coloffs_now = buf->Coloffs();
|
||||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
||||||
// Capture the screen position before drawing the line
|
// Capture the screen position before drawing the line
|
||||||
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
||||||
const std::string &line = lines[i];
|
auto line = static_cast<std::string>(lines[i]);
|
||||||
|
|
||||||
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
|
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
|
||||||
const std::size_t tabw = 8;
|
constexpr std::size_t tabw = 8;
|
||||||
std::string expanded;
|
std::string expanded;
|
||||||
expanded.reserve(line.size() + 16);
|
expanded.reserve(line.size() + 16);
|
||||||
std::size_t rx_abs_draw = 0; // rendered column for drawing
|
std::size_t rx_abs_draw = 0; // rendered column for drawing
|
||||||
// Emit entire line (ImGui child scrolling will handle clipping)
|
// Compute search highlight ranges for this line in source indices
|
||||||
|
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||||
|
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
|
||||||
|
if (search_mode) {
|
||||||
|
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring
|
||||||
|
if (ed.PromptActive() && (
|
||||||
|
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
|
||||||
|
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
||||||
|
try {
|
||||||
|
std::regex rx(ed.SearchQuery());
|
||||||
|
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
||||||
|
it != std::sregex_iterator(); ++it) {
|
||||||
|
const auto &m = *it;
|
||||||
|
auto sx = static_cast<std::size_t>(m.position());
|
||||||
|
std::size_t ex = sx + static_cast<std::size_t>(m.length());
|
||||||
|
hl_src_ranges.emplace_back(sx, ex);
|
||||||
|
}
|
||||||
|
} catch (const std::regex_error &) {
|
||||||
|
// ignore invalid patterns here; status line already shows the error
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const std::string &q = ed.SearchQuery();
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
|
||||||
|
hl_src_ranges.emplace_back(pos, pos + q.size());
|
||||||
|
pos += q.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto src_to_rx = [&](std::size_t upto_src_exclusive) -> std::size_t {
|
||||||
|
std::size_t rx = 0;
|
||||||
|
std::size_t s = 0;
|
||||||
|
while (s < upto_src_exclusive && s < line.size()) {
|
||||||
|
if (line[s] == '\t')
|
||||||
|
rx += (tabw - (rx % tabw));
|
||||||
|
else
|
||||||
|
rx += 1;
|
||||||
|
++s;
|
||||||
|
}
|
||||||
|
return rx;
|
||||||
|
};
|
||||||
|
// Draw background highlights (under text)
|
||||||
|
if (search_mode && !hl_src_ranges.empty()) {
|
||||||
|
// Current match emphasis
|
||||||
|
bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i;
|
||||||
|
std::size_t cur_x = has_current ? ed.SearchMatchX() : 0;
|
||||||
|
std::size_t cur_end = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
||||||
|
for (const auto &rg: hl_src_ranges) {
|
||||||
|
std::size_t sx = rg.first, ex = rg.second;
|
||||||
|
std::size_t rx_start = src_to_rx(sx);
|
||||||
|
std::size_t rx_end = src_to_rx(ex);
|
||||||
|
// Apply horizontal scroll offset
|
||||||
|
if (rx_end <= coloffs_now)
|
||||||
|
continue; // fully left of view
|
||||||
|
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
|
||||||
|
std::size_t vx1 = rx_end - coloffs_now;
|
||||||
|
auto p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||||
|
auto p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
||||||
|
line_pos.y + line_h);
|
||||||
|
// Choose color: current match stronger
|
||||||
|
bool is_current = has_current && sx == cur_x && ex == cur_end;
|
||||||
|
ImU32 col = is_current
|
||||||
|
? IM_COL32(255, 220, 120, 140)
|
||||||
|
: IM_COL32(200, 200, 0, 90);
|
||||||
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Emit entire line to an expanded buffer (tabs -> spaces)
|
||||||
for (std::size_t src = 0; src < line.size(); ++src) {
|
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 {
|
||||||
@@ -265,7 +344,45 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TextUnformatted(expanded.c_str());
|
// Draw syntax-colored runs (text above background highlights)
|
||||||
|
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||||
|
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(
|
||||||
|
*buf, static_cast<int>(i), buf->Version());
|
||||||
|
// Helper to convert a src column to expanded rx position
|
||||||
|
auto src_to_rx_full = [&](const 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));
|
||||||
|
auto 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). Advance by the same amount
|
||||||
|
// ImGui uses between lines (line height + spacing) so hit-testing (which
|
||||||
|
// divides by row_h) aligns with drawing.
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
||||||
|
} else {
|
||||||
|
// No syntax: draw as one run
|
||||||
|
ImGui::TextUnformatted(expanded.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
// Draw a visible cursor indicator on the current line
|
// Draw a visible cursor indicator on the current line
|
||||||
if (i == cy) {
|
if (i == cy) {
|
||||||
@@ -287,160 +404,219 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
|
||||||
// Status bar spanning full width
|
// Status bar spanning full width
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
// Compute full content width and draw a filled background rectangle
|
// Compute full content width and draw a filled background rectangle
|
||||||
ImVec2 win_pos = ImGui::GetWindowPos();
|
ImVec2 win_pos = ImGui::GetWindowPos();
|
||||||
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
||||||
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
|
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
|
||||||
float x0 = win_pos.x + cr_min.x;
|
float x0 = win_pos.x + cr_min.x;
|
||||||
float x1 = win_pos.x + cr_max.x;
|
float x1 = win_pos.x + cr_max.x;
|
||||||
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
||||||
float bar_h = ImGui::GetFrameHeight();
|
float bar_h = ImGui::GetFrameHeight();
|
||||||
ImVec2 p0(x0, cursor.y);
|
ImVec2 p0(x0, cursor.y);
|
||||||
ImVec2 p1(x1, cursor.y + bar_h);
|
ImVec2 p1(x1, cursor.y + bar_h);
|
||||||
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
|
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
|
||||||
// If a prompt is active, replace the entire status bar with the prompt text
|
// If a prompt is active, replace the entire status bar with the prompt text
|
||||||
if (ed.PromptActive()) {
|
|
||||||
std::string msg = ed.PromptLabel();
|
|
||||||
if (!msg.empty()) msg += ": ";
|
|
||||||
std::string ptext = ed.PromptText();
|
|
||||||
auto kind = ed.CurrentPromptKind();
|
|
||||||
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
|
||||||
kind == Editor::PromptKind::Chdir) {
|
|
||||||
const char *home_c = std::getenv("HOME");
|
|
||||||
if (home_c && *home_c) {
|
|
||||||
std::string home(home_c);
|
|
||||||
if (ptext.rfind(home, 0) == 0) {
|
|
||||||
std::string rest = ptext.substr(home.size());
|
|
||||||
if (rest.empty())
|
|
||||||
ptext = "~";
|
|
||||||
else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\'))
|
|
||||||
ptext = std::string("~") + rest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
msg += ptext;
|
|
||||||
|
|
||||||
float pad = 6.f;
|
|
||||||
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
|
|
||||||
float left_x = p0.x + pad;
|
|
||||||
ImGui::PushClipRect(ImVec2(p0.x, p0.y), ImVec2(p1.x, p1.y), true);
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
|
||||||
ImGui::TextUnformatted(msg.c_str());
|
|
||||||
ImGui::PopClipRect();
|
|
||||||
// Advance cursor to after the bar to keep layout consistent
|
|
||||||
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
|
||||||
} else {
|
|
||||||
// Build left text
|
|
||||||
std::string left;
|
|
||||||
left.reserve(256);
|
|
||||||
left += "kge"; // GUI app name
|
|
||||||
left += " ";
|
|
||||||
left += KTE_VERSION_STR;
|
|
||||||
std::string fname;
|
|
||||||
try {
|
|
||||||
fname = ed.DisplayNameFor(*buf);
|
|
||||||
} catch (...) {
|
|
||||||
fname = buf->Filename();
|
|
||||||
try {
|
|
||||||
fname = std::filesystem::path(fname).filename().string();
|
|
||||||
} catch (...) {}
|
|
||||||
}
|
|
||||||
left += " ";
|
|
||||||
// Insert buffer position prefix "[x/N] " before filename
|
|
||||||
{
|
|
||||||
std::size_t total = ed.BufferCount();
|
|
||||||
if (total > 0) {
|
|
||||||
std::size_t idx1 = ed.CurrentBufferIndex() + 1; // 1-based for display
|
|
||||||
left += "[";
|
|
||||||
left += std::to_string(static_cast<unsigned long long>(idx1));
|
|
||||||
left += "/";
|
|
||||||
left += std::to_string(static_cast<unsigned long long>(total));
|
|
||||||
left += "] ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
left += fname;
|
|
||||||
if (buf->Dirty())
|
|
||||||
left += " *";
|
|
||||||
// Append total line count as "<n>L"
|
|
||||||
{
|
|
||||||
unsigned long lcount = static_cast<unsigned long>(buf->Rows().size());
|
|
||||||
left += " ";
|
|
||||||
left += std::to_string(lcount);
|
|
||||||
left += "L";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build right text (cursor/mark)
|
|
||||||
int row1 = static_cast<int>(buf->Cury()) + 1;
|
|
||||||
int col1 = static_cast<int>(buf->Curx()) + 1;
|
|
||||||
bool have_mark = buf->MarkSet();
|
|
||||||
int mrow1 = have_mark ? static_cast<int>(buf->MarkCury()) + 1 : 0;
|
|
||||||
int mcol1 = have_mark ? static_cast<int>(buf->MarkCurx()) + 1 : 0;
|
|
||||||
char rbuf[128];
|
|
||||||
if (have_mark)
|
|
||||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
|
|
||||||
else
|
|
||||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
|
||||||
std::string right = rbuf;
|
|
||||||
|
|
||||||
// Middle message: if a prompt is active, show "Label: text"; otherwise show status
|
|
||||||
std::string msg;
|
|
||||||
if (ed.PromptActive()) {
|
if (ed.PromptActive()) {
|
||||||
msg = ed.PromptLabel();
|
const std::string &label = ed.PromptLabel();
|
||||||
if (!msg.empty())
|
std::string ptext = ed.PromptText();
|
||||||
msg += ": ";
|
auto kind = ed.CurrentPromptKind();
|
||||||
msg += ed.PromptText();
|
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
||||||
} else {
|
kind == Editor::PromptKind::Chdir) {
|
||||||
msg = ed.Status();
|
const char *home_c = std::getenv("HOME");
|
||||||
}
|
if (home_c && *home_c) {
|
||||||
|
std::string home(home_c);
|
||||||
|
if (ptext.rfind(home, 0) == 0) {
|
||||||
|
std::string rest = ptext.substr(home.size());
|
||||||
|
if (rest.empty())
|
||||||
|
ptext = "~";
|
||||||
|
else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\'))
|
||||||
|
ptext = std::string("~") + rest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Measurements
|
float pad = 6.f;
|
||||||
ImVec2 left_sz = ImGui::CalcTextSize(left.c_str());
|
float left_x = p0.x + pad;
|
||||||
ImVec2 right_sz = ImGui::CalcTextSize(right.c_str());
|
float right_x = p1.x - pad;
|
||||||
float pad = 6.f;
|
float max_px = std::max(0.0f, right_x - left_x);
|
||||||
float left_x = p0.x + pad;
|
|
||||||
float right_x = p1.x - pad - right_sz.x;
|
std::string prefix;
|
||||||
if (right_x < left_x + left_sz.x + pad) {
|
if (kind == Editor::PromptKind::Command) {
|
||||||
// Not enough room; clip left to fit
|
prefix = ": ";
|
||||||
float max_left = std::max(0.0f, right_x - left_x - pad);
|
} else if (!label.empty()) {
|
||||||
if (max_left < left_sz.x && max_left > 10.0f) {
|
prefix = label + ": ";
|
||||||
// Render a clipped left using a child region
|
}
|
||||||
|
|
||||||
|
// Compose showing right-end of filename portion when too long for space
|
||||||
|
std::string final_msg;
|
||||||
|
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
|
||||||
|
float avail_px = std::max(0.0f, max_px - prefix_sz.x);
|
||||||
|
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
|
||||||
|
Editor::PromptKind::Chdir) && avail_px > 0.0f) {
|
||||||
|
// Trim from left until it fits by pixel width
|
||||||
|
std::string tail = ptext;
|
||||||
|
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
|
||||||
|
if (tail_sz.x > avail_px) {
|
||||||
|
// Remove leading chars until it fits
|
||||||
|
// Use a simple loop; text lengths are small here
|
||||||
|
size_t start = 0;
|
||||||
|
// To avoid O(n^2) worst-case, remove chunks
|
||||||
|
while (start < tail.size()) {
|
||||||
|
// Estimate how many chars to skip based on ratio
|
||||||
|
float ratio = tail_sz.x / avail_px;
|
||||||
|
size_t skip = ratio > 1.5f
|
||||||
|
? std::min(tail.size() - start,
|
||||||
|
static_cast<size_t>(std::max<size_t>(
|
||||||
|
1, tail.size() / 4)))
|
||||||
|
: 1;
|
||||||
|
start += skip;
|
||||||
|
std::string candidate = tail.substr(start);
|
||||||
|
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
|
||||||
|
if (cand_sz.x <= avail_px) {
|
||||||
|
tail = candidate;
|
||||||
|
tail_sz = cand_sz;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ImGui::CalcTextSize(tail.c_str()).x > avail_px && !tail.empty()) {
|
||||||
|
// As a last resort, ensure fit by chopping exactly
|
||||||
|
// binary reduce
|
||||||
|
size_t lo = 0, hi = tail.size();
|
||||||
|
while (lo < hi) {
|
||||||
|
size_t mid = (lo + hi) / 2;
|
||||||
|
std::string cand = tail.substr(mid);
|
||||||
|
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px)
|
||||||
|
hi = mid;
|
||||||
|
else
|
||||||
|
lo = mid + 1;
|
||||||
|
}
|
||||||
|
tail = tail.substr(lo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final_msg = prefix + tail;
|
||||||
|
} else {
|
||||||
|
final_msg = prefix + ptext;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec2 msg_sz = ImGui::CalcTextSize(final_msg.c_str());
|
||||||
|
ImGui::PushClipRect(ImVec2(p0.x, p0.y), ImVec2(p1.x, p1.y), true);
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
||||||
|
ImGui::TextUnformatted(final_msg.c_str());
|
||||||
|
ImGui::PopClipRect();
|
||||||
|
// Advance cursor to after the bar to keep layout consistent
|
||||||
|
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
||||||
|
} else {
|
||||||
|
// Build left text
|
||||||
|
std::string left;
|
||||||
|
left.reserve(256);
|
||||||
|
left += "kge"; // GUI app name
|
||||||
|
left += " ";
|
||||||
|
left += KTE_VERSION_STR;
|
||||||
|
std::string fname;
|
||||||
|
try {
|
||||||
|
fname = ed.DisplayNameFor(*buf);
|
||||||
|
} catch (...) {
|
||||||
|
fname = buf->Filename();
|
||||||
|
try {
|
||||||
|
fname = std::filesystem::path(fname).filename().string();
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
left += " ";
|
||||||
|
// Insert buffer position prefix "[x/N] " before filename
|
||||||
|
{
|
||||||
|
if (std::size_t total = ed.BufferCount(); total > 0) {
|
||||||
|
std::size_t idx1 = ed.CurrentBufferIndex() + 1; // 1-based for display
|
||||||
|
left += "[";
|
||||||
|
left += std::to_string(static_cast<unsigned long long>(idx1));
|
||||||
|
left += "/";
|
||||||
|
left += std::to_string(static_cast<unsigned long long>(total));
|
||||||
|
left += "] ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
left += fname;
|
||||||
|
if (buf->Dirty())
|
||||||
|
left += " *";
|
||||||
|
// Append total line count as "<n>L"
|
||||||
|
{
|
||||||
|
auto lcount = buf->Rows().size();
|
||||||
|
left += " ";
|
||||||
|
left += std::to_string(lcount);
|
||||||
|
left += "L";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build right text (cursor/mark)
|
||||||
|
int row1 = static_cast<int>(buf->Cury()) + 1;
|
||||||
|
int col1 = static_cast<int>(buf->Curx()) + 1;
|
||||||
|
bool have_mark = buf->MarkSet();
|
||||||
|
int mrow1 = have_mark ? static_cast<int>(buf->MarkCury()) + 1 : 0;
|
||||||
|
int mcol1 = have_mark ? static_cast<int>(buf->MarkCurx()) + 1 : 0;
|
||||||
|
char rbuf[128];
|
||||||
|
if (have_mark)
|
||||||
|
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
|
||||||
|
else
|
||||||
|
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
||||||
|
std::string right = rbuf;
|
||||||
|
|
||||||
|
// Middle message: if a prompt is active, show "Label: text"; otherwise show status
|
||||||
|
std::string msg;
|
||||||
|
if (ed.PromptActive()) {
|
||||||
|
msg = ed.PromptLabel();
|
||||||
|
if (!msg.empty())
|
||||||
|
msg += ": ";
|
||||||
|
msg += ed.PromptText();
|
||||||
|
} else {
|
||||||
|
msg = ed.Status();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measurements
|
||||||
|
ImVec2 left_sz = ImGui::CalcTextSize(left.c_str());
|
||||||
|
ImVec2 right_sz = ImGui::CalcTextSize(right.c_str());
|
||||||
|
float pad = 6.f;
|
||||||
|
float left_x = p0.x + pad;
|
||||||
|
float right_x = p1.x - pad - right_sz.x;
|
||||||
|
if (right_x < left_x + left_sz.x + pad) {
|
||||||
|
// Not enough room; clip left to fit
|
||||||
|
float max_left = std::max(0.0f, right_x - left_x - pad);
|
||||||
|
if (max_left < left_sz.x && max_left > 10.0f) {
|
||||||
|
// Render a clipped left using a child region
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
||||||
|
ImGui::PushClipRect(ImVec2(left_x, p0.y), ImVec2(right_x - pad, p1.y), true);
|
||||||
|
ImGui::TextUnformatted(left.c_str());
|
||||||
|
ImGui::PopClipRect();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Draw left normally
|
||||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
||||||
ImGui::PushClipRect(ImVec2(left_x, p0.y), ImVec2(right_x - pad, p1.y), true);
|
|
||||||
ImGui::TextUnformatted(left.c_str());
|
ImGui::TextUnformatted(left.c_str());
|
||||||
ImGui::PopClipRect();
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Draw left normally
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
|
||||||
ImGui::TextUnformatted(left.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw right
|
// 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),
|
||||||
ImGui::TextUnformatted(right.c_str());
|
p0.y + (bar_h - right_sz.y) * 0.5f));
|
||||||
|
ImGui::TextUnformatted(right.c_str());
|
||||||
|
|
||||||
// Draw middle message centered in remaining space
|
// Draw middle message centered in remaining space
|
||||||
if (!msg.empty()) {
|
if (!msg.empty()) {
|
||||||
float mid_left = left_x + left_sz.x + pad;
|
float mid_left = left_x + left_sz.x + pad;
|
||||||
float mid_right = std::max(right_x - pad, mid_left);
|
float mid_right = std::max(right_x - pad, mid_left);
|
||||||
float mid_w = std::max(0.0f, mid_right - mid_left);
|
float mid_w = std::max(0.0f, mid_right - mid_left);
|
||||||
if (mid_w > 1.0f) {
|
if (mid_w > 1.0f) {
|
||||||
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
|
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
|
||||||
float msg_x = mid_left + std::max(0.0f, (mid_w - msg_sz.x) * 0.5f);
|
float msg_x = mid_left + std::max(0.0f, (mid_w - msg_sz.x) * 0.5f);
|
||||||
// Clip to middle region
|
// Clip to middle region
|
||||||
ImGui::PushClipRect(ImVec2(mid_left, p0.y), ImVec2(mid_right, p1.y), true);
|
ImGui::PushClipRect(ImVec2(mid_left, p0.y), ImVec2(mid_right, p1.y), true);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(msg_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
ImGui::SetCursorScreenPos(ImVec2(msg_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
||||||
ImGui::TextUnformatted(msg.c_str());
|
ImGui::TextUnformatted(msg.c_str());
|
||||||
ImGui::PopClipRect();
|
ImGui::PopClipRect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Advance cursor to after the bar to keep layout consistent
|
||||||
|
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
||||||
}
|
}
|
||||||
// Advance cursor to after the bar to keep layout consistent
|
|
||||||
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
@@ -452,9 +628,9 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
ImGuiViewport *vp2 = ImGui::GetMainViewport();
|
ImGuiViewport *vp2 = ImGui::GetMainViewport();
|
||||||
|
|
||||||
// Desired size, min size, and margins
|
// Desired size, min size, and margins
|
||||||
const ImVec2 want(800.0f, 500.0f);
|
constexpr ImVec2 want(800.0f, 500.0f);
|
||||||
const ImVec2 min_sz(240.0f, 160.0f);
|
constexpr ImVec2 min_sz(240.0f, 160.0f);
|
||||||
const float margin = 20.0f; // space from viewport edges
|
constexpr float margin = 20.0f; // space from viewport edges
|
||||||
|
|
||||||
// Compute the maximum allowed size (viewport minus margins) and make sure it's not negative
|
// Compute the maximum allowed size (viewport minus margins) and make sure it's not negative
|
||||||
ImVec2 max_sz(std::max(32.0f, vp2->Size.x - 2.0f * margin),
|
ImVec2 max_sz(std::max(32.0f, vp2->Size.x - 2.0f * margin),
|
||||||
|
|||||||
415
GUITheme.h
Normal file
415
GUITheme.h
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
// GUITheme.h — ImGui theming helpers and background mode
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
#include "themes/ThemeHelpers.h"
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
// Background mode selection for light/dark palettes
|
||||||
|
enum class BackgroundMode { Light, Dark };
|
||||||
|
|
||||||
|
// Global background mode; default to Dark to match prior defaults
|
||||||
|
static inline auto gBackgroundMode = BackgroundMode::Dark;
|
||||||
|
|
||||||
|
// Basic theme identifier (kept minimal; some ids are aliases)
|
||||||
|
enum class ThemeId {
|
||||||
|
EInk = 0,
|
||||||
|
GruvboxDarkMedium = 1,
|
||||||
|
GruvboxLightMedium = 1, // alias to unified gruvbox index
|
||||||
|
Nord = 2,
|
||||||
|
Plan9 = 3,
|
||||||
|
Solarized = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Current theme tracking
|
||||||
|
static inline auto gCurrentTheme = ThemeId::Nord;
|
||||||
|
static inline std::size_t gCurrentThemeIndex = 0;
|
||||||
|
|
||||||
|
// Forward declarations for helpers used below
|
||||||
|
static size_t ThemeIndexFromId(ThemeId id);
|
||||||
|
|
||||||
|
static ThemeId ThemeIdFromIndex(size_t idx);
|
||||||
|
|
||||||
|
// Helpers to set/query background mode
|
||||||
|
static void
|
||||||
|
SetBackgroundMode(const BackgroundMode m)
|
||||||
|
{
|
||||||
|
gBackgroundMode = m;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static BackgroundMode
|
||||||
|
GetBackgroundMode()
|
||||||
|
{
|
||||||
|
return gBackgroundMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline const char *
|
||||||
|
BackgroundModeName()
|
||||||
|
{
|
||||||
|
return gBackgroundMode == BackgroundMode::Light ? "light" : "dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Include individual theme implementations split under ./themes
|
||||||
|
#include "themes/Nord.h"
|
||||||
|
#include "themes/Plan9.h"
|
||||||
|
#include "themes/Solarized.h"
|
||||||
|
#include "themes/Gruvbox.h"
|
||||||
|
#include "themes/EInk.h"
|
||||||
|
|
||||||
|
|
||||||
|
// Theme abstraction and registry (generalized theme system)
|
||||||
|
class Theme {
|
||||||
|
public:
|
||||||
|
virtual ~Theme() = default;
|
||||||
|
|
||||||
|
[[nodiscard]] virtual const char *Name() const = 0; // canonical name (e.g., "nord", "gruvbox-dark")
|
||||||
|
virtual void Apply() const = 0; // apply to current ImGui style
|
||||||
|
virtual ThemeId Id() = 0; // theme identifier
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
struct NordTheme final : Theme {
|
||||||
|
[[nodiscard]] const char *Name() const override
|
||||||
|
{
|
||||||
|
return "nord";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Apply() const override
|
||||||
|
{
|
||||||
|
ApplyNordImGuiTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThemeId Id() override
|
||||||
|
{
|
||||||
|
return ThemeId::Nord;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GruvboxTheme final : Theme {
|
||||||
|
[[nodiscard]] const char *Name() const override
|
||||||
|
{
|
||||||
|
return "gruvbox";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Apply() const override
|
||||||
|
{
|
||||||
|
if (gBackgroundMode == BackgroundMode::Light)
|
||||||
|
ApplyGruvboxLightMediumTheme();
|
||||||
|
else
|
||||||
|
ApplyGruvboxDarkMediumTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThemeId Id() override
|
||||||
|
{
|
||||||
|
// Legacy maps to dark; unified under base id GruvboxDarkMedium
|
||||||
|
return ThemeId::GruvboxDarkMedium;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EInkTheme final : Theme {
|
||||||
|
[[nodiscard]] const char *Name() const override
|
||||||
|
{
|
||||||
|
return "eink";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Apply() const override
|
||||||
|
{
|
||||||
|
if (gBackgroundMode == BackgroundMode::Dark)
|
||||||
|
ApplyEInkDarkImGuiTheme();
|
||||||
|
else
|
||||||
|
ApplyEInkImGuiTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThemeId Id() override
|
||||||
|
{
|
||||||
|
return ThemeId::EInk;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SolarizedTheme final : Theme {
|
||||||
|
[[nodiscard]] const char *Name() const override
|
||||||
|
{
|
||||||
|
return "solarized";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Apply() const override
|
||||||
|
{
|
||||||
|
if (gBackgroundMode == BackgroundMode::Light)
|
||||||
|
ApplySolarizedLightTheme();
|
||||||
|
else
|
||||||
|
ApplySolarizedDarkTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThemeId Id() override
|
||||||
|
{
|
||||||
|
return ThemeId::Solarized;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Plan9Theme final : Theme {
|
||||||
|
[[nodiscard]] const char *Name() const override
|
||||||
|
{
|
||||||
|
return "plan9";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Apply() const override
|
||||||
|
{
|
||||||
|
ApplyPlan9Theme();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThemeId Id() override
|
||||||
|
{
|
||||||
|
return ThemeId::Plan9;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
static const std::vector<std::unique_ptr<Theme> > &
|
||||||
|
ThemeRegistry()
|
||||||
|
{
|
||||||
|
static std::vector<std::unique_ptr<Theme> > reg;
|
||||||
|
if (reg.empty()) {
|
||||||
|
// Alphabetical by canonical name: eink, gruvbox, nord, plan9, solarized
|
||||||
|
reg.emplace_back(std::make_unique<detail::EInkTheme>());
|
||||||
|
reg.emplace_back(std::make_unique<detail::GruvboxTheme>());
|
||||||
|
reg.emplace_back(std::make_unique<detail::NordTheme>());
|
||||||
|
reg.emplace_back(std::make_unique<detail::Plan9Theme>());
|
||||||
|
reg.emplace_back(std::make_unique<detail::SolarizedTheme>());
|
||||||
|
}
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Canonical theme name for a given ThemeId (via registry order)
|
||||||
|
[[maybe_unused]] static const char *
|
||||||
|
ThemeName(const ThemeId id)
|
||||||
|
{
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
const size_t idx = ThemeIndexFromId(id);
|
||||||
|
if (idx < reg.size())
|
||||||
|
return reg[idx]->Name();
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Helper to apply a theme by id and update current theme
|
||||||
|
static void
|
||||||
|
ApplyTheme(const ThemeId id)
|
||||||
|
{
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
const size_t idx = ThemeIndexFromId(id);
|
||||||
|
if (idx < reg.size()) {
|
||||||
|
reg[idx]->Apply();
|
||||||
|
gCurrentTheme = id;
|
||||||
|
gCurrentThemeIndex = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[maybe_unused]] static ThemeId
|
||||||
|
CurrentTheme()
|
||||||
|
{
|
||||||
|
return gCurrentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Cycle helpers
|
||||||
|
[[maybe_unused]] static ThemeId
|
||||||
|
NextTheme()
|
||||||
|
{
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
if (reg.empty()) {
|
||||||
|
return gCurrentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t nxt = (gCurrentThemeIndex + 1) % reg.size();
|
||||||
|
ApplyTheme(ThemeIdFromIndex(nxt));
|
||||||
|
return gCurrentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[maybe_unused]] static ThemeId
|
||||||
|
PrevTheme()
|
||||||
|
{
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
if (reg.empty()) {
|
||||||
|
return gCurrentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t prv = (gCurrentThemeIndex + reg.size() - 1) % reg.size();
|
||||||
|
ApplyTheme(ThemeIdFromIndex(prv));
|
||||||
|
return gCurrentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Name-based API
|
||||||
|
[[maybe_unused]] static const Theme *
|
||||||
|
GetThemeByName(const std::string &name)
|
||||||
|
{
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
for (const auto &t: reg) {
|
||||||
|
if (name == t->Name())
|
||||||
|
return t.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[maybe_unused]] static bool
|
||||||
|
ApplyThemeByName(const std::string &name)
|
||||||
|
{
|
||||||
|
// Handle aliases and background-specific names
|
||||||
|
std::string n = name;
|
||||||
|
// lowercase copy
|
||||||
|
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::tolower(c));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (n == "gruvbox-dark") {
|
||||||
|
SetBackgroundMode(BackgroundMode::Dark);
|
||||||
|
n = "gruvbox";
|
||||||
|
} else if (n == "gruvbox-light") {
|
||||||
|
SetBackgroundMode(BackgroundMode::Light);
|
||||||
|
n = "gruvbox";
|
||||||
|
} else if (n == "solarized-dark") {
|
||||||
|
SetBackgroundMode(BackgroundMode::Dark);
|
||||||
|
n = "solarized";
|
||||||
|
} else if (n == "solarized-light") {
|
||||||
|
SetBackgroundMode(BackgroundMode::Light);
|
||||||
|
n = "solarized";
|
||||||
|
} else if (n == "eink-dark") {
|
||||||
|
SetBackgroundMode(BackgroundMode::Dark);
|
||||||
|
n = "eink";
|
||||||
|
} else if (n == "eink-light") {
|
||||||
|
SetBackgroundMode(BackgroundMode::Light);
|
||||||
|
n = "eink";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
for (size_t i = 0; i < reg.size(); ++i) {
|
||||||
|
if (n == reg[i]->Name()) {
|
||||||
|
reg[i]->Apply();
|
||||||
|
gCurrentThemeIndex = i;
|
||||||
|
gCurrentTheme = ThemeIdFromIndex(i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[maybe_unused]] static const char *
|
||||||
|
CurrentThemeName()
|
||||||
|
{
|
||||||
|
const auto ® = ThemeRegistry();
|
||||||
|
if (gCurrentThemeIndex < reg.size()) {
|
||||||
|
return reg[gCurrentThemeIndex]->Name();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Helpers to map between legacy ThemeId and registry index
|
||||||
|
static size_t
|
||||||
|
ThemeIndexFromId(const ThemeId id)
|
||||||
|
{
|
||||||
|
switch (id) {
|
||||||
|
case ThemeId::EInk:
|
||||||
|
return 0;
|
||||||
|
case ThemeId::GruvboxDarkMedium:
|
||||||
|
return 1;
|
||||||
|
case ThemeId::Nord:
|
||||||
|
return 2;
|
||||||
|
case ThemeId::Plan9:
|
||||||
|
return 3;
|
||||||
|
case ThemeId::Solarized:
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static ThemeId
|
||||||
|
ThemeIdFromIndex(const size_t idx)
|
||||||
|
{
|
||||||
|
switch (idx) {
|
||||||
|
default:
|
||||||
|
case 0:
|
||||||
|
return ThemeId::EInk;
|
||||||
|
case 1:
|
||||||
|
return ThemeId::GruvboxDarkMedium; // unified gruvbox
|
||||||
|
case 2:
|
||||||
|
return ThemeId::Nord;
|
||||||
|
case 3:
|
||||||
|
return ThemeId::Plan9;
|
||||||
|
case 4:
|
||||||
|
return ThemeId::Solarized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Syntax palette (v1): map TokenKind to ink color per current theme/background ---
|
||||||
|
[[maybe_unused]] static ImVec4
|
||||||
|
SyntaxInk(const TokenKind k)
|
||||||
|
{
|
||||||
|
// Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults
|
||||||
|
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
|
||||||
|
// Base text
|
||||||
|
const ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
|
||||||
|
switch (k) {
|
||||||
|
case TokenKind::Keyword:
|
||||||
|
return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC);
|
||||||
|
case TokenKind::Type:
|
||||||
|
return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A);
|
||||||
|
case TokenKind::String:
|
||||||
|
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
|
||||||
|
case TokenKind::Char:
|
||||||
|
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
|
||||||
|
case TokenKind::Comment:
|
||||||
|
return dark ? RGBA(0x616E88) : RGBA(0x7A869A);
|
||||||
|
case TokenKind::Number:
|
||||||
|
return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900);
|
||||||
|
case TokenKind::Preproc:
|
||||||
|
return dark ? RGBA(0xD08770) : RGBA(0xAF3A03);
|
||||||
|
case TokenKind::Constant:
|
||||||
|
return dark ? RGBA(0xB48EAD) : RGBA(0x7B4B7F);
|
||||||
|
case TokenKind::Function:
|
||||||
|
return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4);
|
||||||
|
case TokenKind::Operator:
|
||||||
|
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
|
||||||
|
case TokenKind::Punctuation:
|
||||||
|
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
|
||||||
|
case TokenKind::Identifier:
|
||||||
|
return def;
|
||||||
|
case TokenKind::Whitespace:
|
||||||
|
return def;
|
||||||
|
case TokenKind::Error:
|
||||||
|
return dark ? RGBA(0xBF616A) : RGBA(0xCC0000);
|
||||||
|
case TokenKind::Default: default:
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
81
HelpText.cc
Normal file
81
HelpText.cc
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* HelpText.cc - embedded/customizable help content
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "HelpText.h"
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
HelpText::Text()
|
||||||
|
{
|
||||||
|
// Customize the help text here. This string will be used by C-k h first.
|
||||||
|
// You can keep it empty to fall back to the manpage or built-in defaults.
|
||||||
|
// Note: keep newline characters as-is; the renderer splits lines on '\n'.
|
||||||
|
|
||||||
|
return std::string(
|
||||||
|
"KTE - Kyle's Text Editor\n\n"
|
||||||
|
"About:\n"
|
||||||
|
" kte is Kyle's Text Editor. It keeps a small, fast core and uses a\n"
|
||||||
|
" WordStar/VDE-style command model with some emacs influences.\n"
|
||||||
|
"\n"
|
||||||
|
"K-commands (prefix C-k):\n"
|
||||||
|
" C-k ' Toggle read-only\n"
|
||||||
|
" C-k - Unindent region (mark required)\n"
|
||||||
|
" C-k = Indent region (mark required)\n"
|
||||||
|
" C-k ; Command prompt (:\\ )\n"
|
||||||
|
" C-k C-d Kill entire line\n"
|
||||||
|
" C-k C-q Quit now (no confirm)\n"
|
||||||
|
" C-k C-x Save and quit\n"
|
||||||
|
" C-k a Mark start of file, jump to end\n"
|
||||||
|
" C-k b Switch buffer\n"
|
||||||
|
" C-k c Close current buffer\n"
|
||||||
|
" C-k d Kill to end of line\n"
|
||||||
|
" C-k e Open file (prompt)\n"
|
||||||
|
" C-k f Flush kill ring\n"
|
||||||
|
" C-k g Jump to line\n"
|
||||||
|
" C-k h Show this help\n"
|
||||||
|
" C-k j Jump to mark\n"
|
||||||
|
" C-k l Reload buffer from disk\n"
|
||||||
|
" C-k n Previous buffer\n"
|
||||||
|
" C-k o Change working directory (prompt)\n"
|
||||||
|
" C-k p Next buffer\n"
|
||||||
|
" C-k q Quit (confirm if dirty)\n"
|
||||||
|
" C-k r Redo\n"
|
||||||
|
" C-k s Save buffer\n"
|
||||||
|
" C-k u Undo\n"
|
||||||
|
" C-k v Toggle visual file picker (GUI)\n"
|
||||||
|
" C-k w Show working directory\n"
|
||||||
|
" C-k x Save and quit\n"
|
||||||
|
" C-k y Yank\n"
|
||||||
|
"\n"
|
||||||
|
"ESC/Alt commands:\n"
|
||||||
|
" ESC < Go to beginning of file\n"
|
||||||
|
" ESC > Go to end of file\n"
|
||||||
|
" ESC m Toggle mark\n"
|
||||||
|
" ESC w Copy region to kill ring (Alt-w)\n"
|
||||||
|
" ESC b Previous word\n"
|
||||||
|
" ESC f Next word\n"
|
||||||
|
" ESC d Delete next word (Alt-d)\n"
|
||||||
|
" ESC BACKSPACE Delete previous word (Alt-Backspace)\n"
|
||||||
|
" ESC q Reflow paragraph\n"
|
||||||
|
"\n"
|
||||||
|
"Control keys:\n"
|
||||||
|
" C-a C-e Line start / end\n"
|
||||||
|
" C-b C-f Move left / right\n"
|
||||||
|
" C-n C-p Move down / up\n"
|
||||||
|
" C-d Delete char\n"
|
||||||
|
" C-w / C-y Kill region / Yank\n"
|
||||||
|
" C-s Incremental find\n"
|
||||||
|
" C-r Regex search\n"
|
||||||
|
" C-t Regex search & replace\n"
|
||||||
|
" C-h Search & replace\n"
|
||||||
|
" C-l / C-g Refresh / Cancel\n"
|
||||||
|
" C-u [digits] Universal argument (repeat count)\n"
|
||||||
|
"\n"
|
||||||
|
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
|
||||||
|
"\n"
|
||||||
|
"GUI appearance (command prompt):\n"
|
||||||
|
" : theme NAME Set GUI theme (eink, gruvbox, nord, plan9, solarized)\n"
|
||||||
|
" : background MODE Set background: light | dark (affects eink, gruvbox, solarized)\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
17
HelpText.h
Normal file
17
HelpText.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* HelpText.h - embedded/customizable help content
|
||||||
|
*/
|
||||||
|
#ifndef KTE_HELPTEXT_H
|
||||||
|
#define KTE_HELPTEXT_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class HelpText {
|
||||||
|
public:
|
||||||
|
// Returns the embedded help text as a single string with newlines.
|
||||||
|
// Project maintainers can customize the returned string below
|
||||||
|
// (in HelpText.cc) without touching the help command logic.
|
||||||
|
static std::string Text();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KTE_HELPTEXT_H
|
||||||
37
Highlight.h
Normal file
37
Highlight.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Highlight.h - core syntax highlighting types for kte
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
// Token kinds shared between renderers and highlighters
|
||||||
|
enum class TokenKind {
|
||||||
|
Default,
|
||||||
|
Keyword,
|
||||||
|
Type,
|
||||||
|
String,
|
||||||
|
Char,
|
||||||
|
Comment,
|
||||||
|
Number,
|
||||||
|
Preproc,
|
||||||
|
Constant,
|
||||||
|
Function,
|
||||||
|
Operator,
|
||||||
|
Punctuation,
|
||||||
|
Identifier,
|
||||||
|
Whitespace,
|
||||||
|
Error
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HighlightSpan {
|
||||||
|
int col_start{0}; // inclusive, 0-based columns in buffer indices
|
||||||
|
int col_end{0}; // exclusive
|
||||||
|
TokenKind kind{TokenKind::Default};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LineHighlight {
|
||||||
|
std::vector<HighlightSpan> spans;
|
||||||
|
std::uint64_t version{0}; // buffer version used for this line
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
19
KKeymap.cc
19
KKeymap.cc
@@ -33,6 +33,10 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
out = CommandId::Redo; // C-k r (redo)
|
out = CommandId::Redo; // C-k r (redo)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (ascii_key == '\'') {
|
||||||
|
out = CommandId::ToggleReadOnly; // C-k ' (toggle read-only)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
switch (k_lower) {
|
switch (k_lower) {
|
||||||
case 'a':
|
case 'a':
|
||||||
@@ -59,6 +63,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
case 'g':
|
case 'g':
|
||||||
out = CommandId::JumpToLine;
|
out = CommandId::JumpToLine;
|
||||||
return true;
|
return true;
|
||||||
|
case 'h':
|
||||||
|
out = CommandId::ShowHelp;
|
||||||
|
return true;
|
||||||
case 'j':
|
case 'j':
|
||||||
out = CommandId::JumpToMark;
|
out = CommandId::JumpToMark;
|
||||||
return true;
|
return true;
|
||||||
@@ -101,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;
|
||||||
}
|
}
|
||||||
@@ -145,6 +155,15 @@ KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
|
|||||||
case 's':
|
case 's':
|
||||||
out = CommandId::FindStart;
|
out = CommandId::FindStart;
|
||||||
return true;
|
return true;
|
||||||
|
case 'r':
|
||||||
|
out = CommandId::RegexFindStart; // C-r regex search
|
||||||
|
return true;
|
||||||
|
case 't':
|
||||||
|
out = CommandId::RegexpReplace; // C-t regex search & replace
|
||||||
|
return true;
|
||||||
|
case 'h':
|
||||||
|
out = CommandId::SearchReplace; // C-h: search & replace
|
||||||
|
return true;
|
||||||
case 'l':
|
case 'l':
|
||||||
out = CommandId::Refresh;
|
out = CommandId::Refresh;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
14
ROADMAP.md
14
ROADMAP.md
@@ -1,10 +1,10 @@
|
|||||||
ROADMAP / TODO:
|
ROADMAP / TODO:
|
||||||
|
|
||||||
- [ ] Search + Replace
|
- [x] Search + Replace
|
||||||
- [ ] Regex search + replace
|
- [x] Regex search + replace
|
||||||
- [ ] The undo system should actually work
|
- [ ] The undo system should actually work
|
||||||
- [ ] Able to mark buffers as read-only
|
- [x] Able to mark buffers as read-only
|
||||||
- [ ] Built-in help text
|
- [x] Built-in help text
|
||||||
- [ ] Shorten paths in the homedir with ~
|
- [x] Shorten paths in the homedir with ~
|
||||||
- [ ] 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
|
||||||
to keep it in view
|
keep it in view
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
|
#include <clocale>
|
||||||
#include <termios.h>
|
#include <termios.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#ifdef __APPLE__
|
||||||
|
#include <xlocale.h>
|
||||||
|
#endif
|
||||||
|
#include <langinfo.h>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
#include "TerminalFrontend.h"
|
#include "TerminalFrontend.h"
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
@@ -10,6 +16,35 @@
|
|||||||
bool
|
bool
|
||||||
TerminalFrontend::Init(Editor &ed)
|
TerminalFrontend::Init(Editor &ed)
|
||||||
{
|
{
|
||||||
|
// Enable UTF-8 locale so ncurses and the terminal handle multibyte correctly
|
||||||
|
// This relies on the user's environment (e.g., LANG/LC_ALL) being set to a UTF-8 locale.
|
||||||
|
// If not set, try a couple of common UTF-8 fallbacks.
|
||||||
|
const char *loc = std::setlocale(LC_ALL, "");
|
||||||
|
auto is_utf8_codeset = []() -> bool {
|
||||||
|
const char *cs = nl_langinfo(CODESET);
|
||||||
|
if (!cs)
|
||||||
|
return false;
|
||||||
|
std::string s(cs);
|
||||||
|
for (auto &ch: s)
|
||||||
|
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
|
||||||
|
return (s.find("UTF-8") != std::string::npos) || (s.find("UTF8") != std::string::npos);
|
||||||
|
};
|
||||||
|
bool utf8_ok = (MB_CUR_MAX > 1) && is_utf8_codeset();
|
||||||
|
if (!utf8_ok) {
|
||||||
|
// Try common UTF-8 locales
|
||||||
|
loc = std::setlocale(LC_CTYPE, "C.UTF-8");
|
||||||
|
utf8_ok = (loc != nullptr) && (MB_CUR_MAX > 1) && is_utf8_codeset();
|
||||||
|
if (!utf8_ok) {
|
||||||
|
loc = std::setlocale(LC_CTYPE, "en_US.UTF-8");
|
||||||
|
utf8_ok = (loc != nullptr) && (MB_CUR_MAX > 1) && is_utf8_codeset();
|
||||||
|
}
|
||||||
|
if (!utf8_ok) {
|
||||||
|
// macOS often uses plain "UTF-8" locale identifier
|
||||||
|
loc = std::setlocale(LC_CTYPE, "UTF-8");
|
||||||
|
utf8_ok = (loc != nullptr) && (MB_CUR_MAX > 1) && is_utf8_codeset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure Control keys reach the app: disable XON/XOFF and dsusp/susp bindings (e.g., ^S/^Q, ^Y on macOS)
|
// Ensure Control keys reach the app: disable XON/XOFF and dsusp/susp bindings (e.g., ^S/^Q, ^Y on macOS)
|
||||||
{
|
{
|
||||||
struct termios tio{};
|
struct termios tio{};
|
||||||
@@ -55,6 +90,9 @@ TerminalFrontend::Init(Editor &ed)
|
|||||||
prev_r_ = r;
|
prev_r_ = r;
|
||||||
prev_c_ = c;
|
prev_c_ = c;
|
||||||
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
|
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
|
||||||
|
// Inform renderer of UTF-8 capability so it can choose proper output path
|
||||||
|
renderer_.SetUtf8Enabled(utf8_ok);
|
||||||
|
input_.SetUtf8Enabled(utf8_ok);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cwchar>
|
||||||
|
#include <climits>
|
||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
|
|
||||||
#include "TerminalInputHandler.h"
|
#include "TerminalInputHandler.h"
|
||||||
@@ -36,18 +38,48 @@ map_key_to_command(const int ch,
|
|||||||
MEVENT ev{};
|
MEVENT ev{};
|
||||||
if (getmouse(&ev) == OK) {
|
if (getmouse(&ev) == OK) {
|
||||||
// Mouse wheel → map to MoveUp/MoveDown one line per wheel notch
|
// Mouse wheel → map to MoveUp/MoveDown one line per wheel notch
|
||||||
|
unsigned long wheel_up_mask = 0;
|
||||||
|
unsigned long wheel_dn_mask = 0;
|
||||||
#ifdef BUTTON4_PRESSED
|
#ifdef BUTTON4_PRESSED
|
||||||
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
|
wheel_up_mask |= BUTTON4_PRESSED;
|
||||||
out = {true, CommandId::MoveUp, "", 0};
|
#endif
|
||||||
return true;
|
#ifdef BUTTON4_RELEASED
|
||||||
}
|
wheel_up_mask |= BUTTON4_RELEASED;
|
||||||
|
#endif
|
||||||
|
#ifdef BUTTON4_CLICKED
|
||||||
|
wheel_up_mask |= BUTTON4_CLICKED;
|
||||||
|
#endif
|
||||||
|
#ifdef BUTTON4_DOUBLE_CLICKED
|
||||||
|
wheel_up_mask |= BUTTON4_DOUBLE_CLICKED;
|
||||||
|
#endif
|
||||||
|
#ifdef BUTTON4_TRIPLE_CLICKED
|
||||||
|
wheel_up_mask |= BUTTON4_TRIPLE_CLICKED;
|
||||||
#endif
|
#endif
|
||||||
#ifdef BUTTON5_PRESSED
|
#ifdef BUTTON5_PRESSED
|
||||||
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
|
wheel_dn_mask |= BUTTON5_PRESSED;
|
||||||
out = {true, CommandId::MoveDown, "", 0};
|
#endif
|
||||||
|
#ifdef BUTTON5_RELEASED
|
||||||
|
wheel_dn_mask |= BUTTON5_RELEASED;
|
||||||
|
#endif
|
||||||
|
#ifdef BUTTON5_CLICKED
|
||||||
|
wheel_dn_mask |= BUTTON5_CLICKED;
|
||||||
|
#endif
|
||||||
|
#ifdef BUTTON5_DOUBLE_CLICKED
|
||||||
|
wheel_dn_mask |= BUTTON5_DOUBLE_CLICKED;
|
||||||
|
#endif
|
||||||
|
#ifdef BUTTON5_TRIPLE_CLICKED
|
||||||
|
wheel_dn_mask |= BUTTON5_TRIPLE_CLICKED;
|
||||||
|
#endif
|
||||||
|
if (wheel_up_mask && (ev.bstate & wheel_up_mask)) {
|
||||||
|
// Prefer viewport scrolling for wheel: page up
|
||||||
|
out = {true, CommandId::PageUp, "", 0};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (wheel_dn_mask && (ev.bstate & wheel_dn_mask)) {
|
||||||
|
// Prefer viewport scrolling for wheel: page down
|
||||||
|
out = {true, CommandId::PageDown, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
// React to left button click/press
|
// React to left button click/press
|
||||||
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
|
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
|
||||||
char buf[64];
|
char buf[64];
|
||||||
@@ -281,6 +313,77 @@ map_key_to_command(const int ch,
|
|||||||
bool
|
bool
|
||||||
TerminalInputHandler::decode_(MappedInput &out)
|
TerminalInputHandler::decode_(MappedInput &out)
|
||||||
{
|
{
|
||||||
|
#if defined(KTE_HAVE_GET_WCH)
|
||||||
|
if (utf8_enabled_) {
|
||||||
|
// Prefer wide-character input so we can capture Unicode code points
|
||||||
|
wint_t wch = 0;
|
||||||
|
int rc = get_wch(&wch);
|
||||||
|
if (rc == ERR) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (rc == KEY_CODE_YES) {
|
||||||
|
// Function/special key; pass through existing mapper
|
||||||
|
int sk = static_cast<int>(wch);
|
||||||
|
bool consumed = map_key_to_command(
|
||||||
|
sk,
|
||||||
|
k_prefix_, esc_meta_,
|
||||||
|
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_,
|
||||||
|
uarg_text_,
|
||||||
|
out);
|
||||||
|
if (!consumed)
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// Regular character
|
||||||
|
if (wch <= 0x7F) {
|
||||||
|
// ASCII path -> reuse existing mapping (handles control, ESC, etc.)
|
||||||
|
int ch = static_cast<int>(wch);
|
||||||
|
bool consumed = map_key_to_command(
|
||||||
|
ch,
|
||||||
|
k_prefix_, esc_meta_,
|
||||||
|
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_,
|
||||||
|
uarg_text_,
|
||||||
|
out);
|
||||||
|
if (!consumed)
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// Non-ASCII printable -> insert UTF-8 text directly
|
||||||
|
if (iswcntrl(static_cast<wint_t>(wch))) {
|
||||||
|
out.hasCommand = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
char mb[MB_LEN_MAX];
|
||||||
|
mbstate_t st{};
|
||||||
|
std::size_t n = wcrtomb(mb, static_cast<wchar_t>(wch), &st);
|
||||||
|
if (n == static_cast<std::size_t>(-1)) {
|
||||||
|
// Fallback placeholder if encoding failed
|
||||||
|
out.hasCommand = true;
|
||||||
|
out.id = CommandId::InsertText;
|
||||||
|
out.arg = "?";
|
||||||
|
out.count = 0;
|
||||||
|
} else {
|
||||||
|
out.hasCommand = true;
|
||||||
|
out.id = CommandId::InsertText;
|
||||||
|
out.arg.assign(mb, mb + n);
|
||||||
|
out.count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int ch = getch();
|
||||||
|
if (ch == ERR) {
|
||||||
|
return false; // no input
|
||||||
|
}
|
||||||
|
bool consumed = map_key_to_command(
|
||||||
|
ch,
|
||||||
|
k_prefix_, esc_meta_,
|
||||||
|
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_, uarg_text_,
|
||||||
|
out);
|
||||||
|
if (!consumed)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Wide-character input not available in this curses; fall back to byte-wise getch
|
||||||
|
(void) utf8_enabled_;
|
||||||
int ch = getch();
|
int ch = getch();
|
||||||
if (ch == ERR) {
|
if (ch == ERR) {
|
||||||
return false; // no input
|
return false; // no input
|
||||||
@@ -292,6 +395,7 @@ TerminalInputHandler::decode_(MappedInput &out)
|
|||||||
out);
|
out);
|
||||||
if (!consumed)
|
if (!consumed)
|
||||||
return false;
|
return false;
|
||||||
|
#endif
|
||||||
// If a command was produced and a universal argument is active, attach it and clear state
|
// If a command was produced and a universal argument is active, attach it and clear state
|
||||||
if (out.hasCommand && uarg_active_ && out.id != CommandId::UArgStatus) {
|
if (out.hasCommand && uarg_active_ && out.id != CommandId::UArgStatus) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ public:
|
|||||||
|
|
||||||
bool Poll(MappedInput &out) override;
|
bool Poll(MappedInput &out) override;
|
||||||
|
|
||||||
|
|
||||||
|
void SetUtf8Enabled(bool on)
|
||||||
|
{
|
||||||
|
utf8_enabled_ = on;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool decode_(MappedInput &out);
|
bool decode_(MappedInput &out);
|
||||||
|
|
||||||
@@ -30,6 +36,8 @@ private:
|
|||||||
bool uarg_had_digits_ = false; // whether any digits were supplied
|
bool uarg_had_digits_ = false; // whether any digits were supplied
|
||||||
int uarg_value_ = 0; // current absolute value (>=0)
|
int uarg_value_ = 0; // current absolute value (>=0)
|
||||||
std::string uarg_text_; // raw digits/minus typed for status display
|
std::string uarg_text_; // raw digits/minus typed for status display
|
||||||
|
|
||||||
|
bool utf8_enabled_ = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KTE_TERMINAL_INPUT_HANDLER_H
|
#endif // KTE_TERMINAL_INPUT_HANDLER_H
|
||||||
@@ -3,11 +3,14 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
|
#include <regex>
|
||||||
|
#include <cwchar>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#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
|
||||||
@@ -40,23 +43,118 @@ 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);
|
||||||
std::size_t render_col = 0;
|
std::size_t render_col = 0;
|
||||||
std::size_t src_i = 0;
|
std::size_t src_i = 0;
|
||||||
bool do_hl = ed.SearchActive() && li == ed.SearchMatchY() && ed.SearchMatchLen() > 0;
|
// Compute matches for this line if search highlighting is active
|
||||||
std::size_t mx = do_hl ? ed.SearchMatchX() : 0;
|
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||||
std::size_t mlen = do_hl ? ed.SearchMatchLen() : 0;
|
std::vector<std::pair<std::size_t, std::size_t> > ranges; // [start, end)
|
||||||
bool hl_on = false;
|
if (search_mode && li < lines.size()) {
|
||||||
int written = 0;
|
std::string sline = static_cast<std::string>(lines[li]);
|
||||||
|
// If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges
|
||||||
|
if (ed.PromptActive() && (
|
||||||
|
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
|
||||||
|
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
||||||
|
try {
|
||||||
|
std::regex rx(ed.SearchQuery());
|
||||||
|
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
|
||||||
|
it != std::sregex_iterator(); ++it) {
|
||||||
|
const auto &m = *it;
|
||||||
|
std::size_t sx = static_cast<std::size_t>(m.position());
|
||||||
|
std::size_t ex = sx + static_cast<std::size_t>(m.length());
|
||||||
|
ranges.emplace_back(sx, ex);
|
||||||
|
}
|
||||||
|
} catch (const std::regex_error &) {
|
||||||
|
// ignore invalid patterns here; status shows error
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const std::string &q = ed.SearchQuery();
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while (!q.empty() && (pos = sline.find(q, pos)) != std::string::npos) {
|
||||||
|
ranges.emplace_back(pos, pos + q.size());
|
||||||
|
pos += q.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto is_src_in_hl = [&](std::size_t si) -> bool {
|
||||||
|
if (ranges.empty())
|
||||||
|
return false;
|
||||||
|
// ranges are non-overlapping and ordered by construction
|
||||||
|
// linear scan is fine for now
|
||||||
|
for (const auto &rg: ranges) {
|
||||||
|
if (si < rg.first)
|
||||||
|
break;
|
||||||
|
if (si >= rg.first && si < rg.second)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
// Track current-match to optionally emphasize
|
||||||
|
const bool has_current = ed.SearchActive() && ed.SearchMatchLen() > 0;
|
||||||
|
const std::size_t cur_mx = has_current ? ed.SearchMatchX() : 0;
|
||||||
|
const std::size_t cur_my = has_current ? ed.SearchMatchY() : 0;
|
||||||
|
const std::size_t cur_mend = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
||||||
|
bool hl_on = false;
|
||||||
|
bool cur_on = false;
|
||||||
|
int written = 0;
|
||||||
if (li < lines.size()) {
|
if (li < lines.size()) {
|
||||||
const std::string &line = 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 = ' ';
|
// Default to space when beyond EOL
|
||||||
bool from_src = false;
|
bool from_src = false;
|
||||||
|
int wcw = 1; // display width
|
||||||
|
std::size_t advance_bytes = 0;
|
||||||
if (src_i < line.size()) {
|
if (src_i < line.size()) {
|
||||||
unsigned char c = static_cast<unsigned char>(line[src_i]);
|
unsigned char c = static_cast<unsigned char>(line[src_i]);
|
||||||
if (c == '\t') {
|
if (c == '\t') {
|
||||||
@@ -76,16 +174,36 @@ 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 = do_hl && src_i >= mx && src_i < mx + mlen;
|
bool in_hl = search_mode && is_src_in_hl(src_i);
|
||||||
// highlight by source index
|
bool in_cur =
|
||||||
if (in_hl && !hl_on) {
|
has_current && li == cur_my && src_i >= cur_mx
|
||||||
|
&& src_i < cur_mend;
|
||||||
|
// Toggle highlight attributes
|
||||||
|
int attr = 0;
|
||||||
|
if (in_hl)
|
||||||
|
attr |= A_STANDOUT;
|
||||||
|
if (in_cur)
|
||||||
|
attr |= A_BOLD;
|
||||||
|
if ((attr & A_STANDOUT) && !hl_on) {
|
||||||
attron(A_STANDOUT);
|
attron(A_STANDOUT);
|
||||||
hl_on = true;
|
hl_on = true;
|
||||||
}
|
}
|
||||||
if (!in_hl && hl_on) {
|
if (!(attr & A_STANDOUT) && hl_on) {
|
||||||
attroff(A_STANDOUT);
|
attroff(A_STANDOUT);
|
||||||
hl_on = false;
|
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;
|
||||||
@@ -94,36 +212,85 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
++src_i;
|
++src_i;
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
// normal char
|
if (!Utf8Enabled()) {
|
||||||
if (render_col < coloffs) {
|
// ASCII fallback: treat each byte as single width
|
||||||
++render_col;
|
if (render_col + 1 <= coloffs) {
|
||||||
++src_i;
|
++render_col;
|
||||||
continue;
|
++src_i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
wcw = 1;
|
||||||
|
advance_bytes = 1;
|
||||||
|
from_src = true;
|
||||||
|
} else {
|
||||||
|
// Decode one UTF-8 codepoint
|
||||||
|
mbstate_t st{};
|
||||||
|
const char *p = line.data() + src_i;
|
||||||
|
std::size_t rem = line.size() - src_i;
|
||||||
|
wchar_t tmp_wc = 0;
|
||||||
|
std::size_t n = mbrtowc(&tmp_wc, p, rem, &st);
|
||||||
|
if (n == static_cast<std::size_t>(-1) || n ==
|
||||||
|
static_cast<std::size_t>(-2) || n == 0) {
|
||||||
|
// Invalid/incomplete -> treat as single-byte placeholder
|
||||||
|
tmp_wc = L'?';
|
||||||
|
n = 1;
|
||||||
|
}
|
||||||
|
int w = wcwidth(tmp_wc);
|
||||||
|
if (w < 0)
|
||||||
|
w = 1;
|
||||||
|
// If this codepoint is scrolled off to the left, skip it
|
||||||
|
if (render_col + static_cast<std::size_t>(w) <=
|
||||||
|
coloffs) {
|
||||||
|
render_col += static_cast<std::size_t>(w);
|
||||||
|
src_i += n;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
wcw = w;
|
||||||
|
advance_bytes = n;
|
||||||
|
from_src = true;
|
||||||
}
|
}
|
||||||
ch = static_cast<char>(c);
|
|
||||||
from_src = true;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// beyond EOL, fill spaces
|
// beyond EOL, fill spaces
|
||||||
ch = ' ';
|
|
||||||
from_src = false;
|
from_src = false;
|
||||||
}
|
}
|
||||||
if (do_hl) {
|
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
||||||
bool in_hl = from_src && src_i >= mx && src_i < mx + mlen;
|
bool in_cur =
|
||||||
if (in_hl && !hl_on) {
|
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
|
||||||
attron(A_STANDOUT);
|
cur_mend;
|
||||||
hl_on = true;
|
if (in_hl && !hl_on) {
|
||||||
}
|
attron(A_STANDOUT);
|
||||||
if (!in_hl && hl_on) {
|
hl_on = true;
|
||||||
attroff(A_STANDOUT);
|
|
||||||
hl_on = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
addch(static_cast<unsigned char>(ch));
|
if (!in_hl && hl_on) {
|
||||||
++written;
|
attroff(A_STANDOUT);
|
||||||
++render_col;
|
hl_on = false;
|
||||||
if (from_src)
|
}
|
||||||
++src_i;
|
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));
|
||||||
|
}
|
||||||
|
if (written + wcw > cols) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (from_src) {
|
||||||
|
// Output original bytes for this unit (UTF-8 codepoint or ASCII byte)
|
||||||
|
const char *cp = line.data() + (src_i);
|
||||||
|
int out_n = Utf8Enabled() ? static_cast<int>(advance_bytes) : 1;
|
||||||
|
addnstr(cp, out_n);
|
||||||
|
src_i += static_cast<std::size_t>(out_n);
|
||||||
|
} else {
|
||||||
|
addch(' ');
|
||||||
|
}
|
||||||
|
written += wcw;
|
||||||
|
render_col += wcw;
|
||||||
if (src_i >= line.size() && written >= cols)
|
if (src_i >= line.size() && written >= cols)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -132,6 +299,11 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
attroff(A_STANDOUT);
|
attroff(A_STANDOUT);
|
||||||
hl_on = false;
|
hl_on = false;
|
||||||
}
|
}
|
||||||
|
if (cur_on) {
|
||||||
|
attroff(A_BOLD);
|
||||||
|
cur_on = false;
|
||||||
|
}
|
||||||
|
attrset(A_NORMAL);
|
||||||
clrtoeol();
|
clrtoeol();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,50 +322,74 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
mvaddstr(0, 0, "[no buffer]");
|
mvaddstr(0, 0, "[no buffer]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status line (inverse)
|
// Status line (inverse)
|
||||||
move(rows - 1, 0);
|
move(rows - 1, 0);
|
||||||
attron(A_REVERSE);
|
attron(A_REVERSE);
|
||||||
|
|
||||||
// Fill the status line with spaces first
|
// Fill the status line with spaces first
|
||||||
for (int i = 0; i < cols; ++i)
|
for (int i = 0; i < cols; ++i)
|
||||||
addch(' ');
|
addch(' ');
|
||||||
|
|
||||||
// If a prompt is active, replace the status bar with the full prompt text
|
// If a prompt is active, replace the status bar with the full prompt text
|
||||||
if (ed.PromptActive()) {
|
if (ed.PromptActive()) {
|
||||||
// Build prompt text: "Label: text" and shorten HOME path for file-related prompts
|
// Build prompt text: "Label: text" and shorten HOME path for file-related prompts
|
||||||
std::string msg = ed.PromptLabel();
|
std::string label = ed.PromptLabel();
|
||||||
if (!msg.empty())
|
std::string ptext = ed.PromptText();
|
||||||
msg += ": ";
|
auto kind = ed.CurrentPromptKind();
|
||||||
std::string ptext = ed.PromptText();
|
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
||||||
auto kind = ed.CurrentPromptKind();
|
kind == Editor::PromptKind::Chdir) {
|
||||||
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
const char *home_c = std::getenv("HOME");
|
||||||
kind == Editor::PromptKind::Chdir) {
|
if (home_c && *home_c) {
|
||||||
const char *home_c = std::getenv("HOME");
|
std::string home(home_c);
|
||||||
if (home_c && *home_c) {
|
// Ensure we match only at the start
|
||||||
std::string home(home_c);
|
if (ptext.rfind(home, 0) == 0) {
|
||||||
// Ensure we match only at the start
|
std::string rest = ptext.substr(home.size());
|
||||||
if (ptext.rfind(home, 0) == 0) {
|
if (rest.empty())
|
||||||
std::string rest = ptext.substr(home.size());
|
ptext = "~";
|
||||||
if (rest.empty())
|
else if (rest[0] == '/' || rest[0] == '\\')
|
||||||
ptext = "~";
|
ptext = std::string("~") + rest;
|
||||||
else if (rest[0] == '/' || rest[0] == '\\')
|
}
|
||||||
ptext = std::string("~") + rest;
|
}
|
||||||
}
|
}
|
||||||
}
|
// Prefer keeping the tail of the filename visible when it exceeds the window
|
||||||
}
|
std::string msg;
|
||||||
msg += ptext;
|
if (kind == Editor::PromptKind::Command) {
|
||||||
|
msg = ": ";
|
||||||
|
} else if (!label.empty()) {
|
||||||
|
msg = label + ": ";
|
||||||
|
}
|
||||||
|
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible
|
||||||
|
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
|
||||||
|
Editor::PromptKind::Chdir) && cols > 0) {
|
||||||
|
int avail = cols - static_cast<int>(msg.size());
|
||||||
|
if (avail <= 0) {
|
||||||
|
// No room for label; fall back to showing the rightmost portion of the whole string
|
||||||
|
std::string whole = msg + ptext;
|
||||||
|
if ((int) whole.size() > cols)
|
||||||
|
whole = whole.substr(whole.size() - cols);
|
||||||
|
msg = whole;
|
||||||
|
} else {
|
||||||
|
if ((int) ptext.size() > avail) {
|
||||||
|
ptext = ptext.substr(ptext.size() - avail);
|
||||||
|
}
|
||||||
|
msg += ptext;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-file prompts: simple concatenation and clip by terminal
|
||||||
|
msg += ptext;
|
||||||
|
}
|
||||||
|
|
||||||
// Draw left-aligned, clipped to width
|
// Draw left-aligned, clipped to width
|
||||||
if (!msg.empty())
|
if (!msg.empty())
|
||||||
mvaddnstr(rows - 1, 0, msg.c_str(), std::max(0, cols));
|
mvaddnstr(rows - 1, 0, msg.c_str(), std::max(0, cols));
|
||||||
|
|
||||||
// End status rendering for prompt mode
|
// End status rendering for prompt mode
|
||||||
attroff(A_REVERSE);
|
attroff(A_REVERSE);
|
||||||
// Restore logical cursor position in content area
|
// Restore logical cursor position in content area
|
||||||
if (saved_cur_y >= 0 && saved_cur_x >= 0)
|
if (saved_cur_y >= 0 && saved_cur_x >= 0)
|
||||||
move(saved_cur_y, saved_cur_x);
|
move(saved_cur_y, saved_cur_x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build left segment
|
// Build left segment
|
||||||
std::string left;
|
std::string left;
|
||||||
@@ -233,6 +429,9 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
left += fname;
|
left += fname;
|
||||||
if (b && b->Dirty())
|
if (b && b->Dirty())
|
||||||
left += " *";
|
left += " *";
|
||||||
|
// Append read-only indicator
|
||||||
|
if (b && b->IsReadOnly())
|
||||||
|
left += " [RO]";
|
||||||
// Append total line count as "<n>L"
|
// Append total line count as "<n>L"
|
||||||
if (b) {
|
if (b) {
|
||||||
unsigned long lcount = static_cast<unsigned long>(b->Rows().size());
|
unsigned long lcount = static_cast<unsigned long>(b->Rows().size());
|
||||||
@@ -263,6 +462,10 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
else
|
else
|
||||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
||||||
right = rbuf;
|
right = rbuf;
|
||||||
|
// If UTF-8 is not enabled (ASCII fallback), append a short hint
|
||||||
|
if (!Utf8Enabled()) {
|
||||||
|
right += " | ASCII";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute placements with truncation rules: prioritize left and right; middle gets remaining
|
// Compute placements with truncation rules: prioritize left and right; middle gets remaining
|
||||||
@@ -281,10 +484,10 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
if (llen > 0)
|
if (llen > 0)
|
||||||
mvaddnstr(rows - 1, 0, left.c_str(), llen);
|
mvaddnstr(rows - 1, 0, left.c_str(), llen);
|
||||||
|
|
||||||
// Draw right, flush to end
|
// Draw right, flush to end
|
||||||
int rstart = std::max(0, cols - rlen);
|
int rstart = std::max(0, cols - rlen);
|
||||||
if (rlen > 0)
|
if (rlen > 0)
|
||||||
mvaddnstr(rows - 1, rstart, right.c_str(), rlen);
|
mvaddnstr(rows - 1, rstart, right.c_str(), rlen);
|
||||||
|
|
||||||
// Middle message
|
// Middle message
|
||||||
const std::string &msg = ed.Status();
|
const std::string &msg = ed.Status();
|
||||||
@@ -300,7 +503,7 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attroff(A_REVERSE);
|
attroff(A_REVERSE);
|
||||||
|
|
||||||
// Restore terminal cursor to the content position so a visible caret
|
// Restore terminal cursor to the content position so a visible caret
|
||||||
// remains in the editing area (not on the status line).
|
// remains in the editing area (not on the status line).
|
||||||
|
|||||||
@@ -14,6 +14,21 @@ public:
|
|||||||
~TerminalRenderer() override;
|
~TerminalRenderer() override;
|
||||||
|
|
||||||
void Draw(Editor &ed) override;
|
void Draw(Editor &ed) override;
|
||||||
|
|
||||||
|
// Enable/disable UTF-8 aware rendering (set by TerminalFrontend after locale init)
|
||||||
|
void SetUtf8Enabled(bool on)
|
||||||
|
{
|
||||||
|
utf8_enabled_ = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] bool Utf8Enabled() const
|
||||||
|
{
|
||||||
|
return utf8_enabled_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool utf8_enabled_ = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KTE_TERMINAL_RENDERER_H
|
#endif // KTE_TERMINAL_RENDERER_H
|
||||||
113
UndoSystem.cc
113
UndoSystem.cc
@@ -1,5 +1,7 @@
|
|||||||
#include "UndoSystem.h"
|
#include "UndoSystem.h"
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
|
||||||
UndoSystem::UndoSystem(Buffer &owner, UndoTree &tree)
|
UndoSystem::UndoSystem(Buffer &owner, UndoTree &tree)
|
||||||
@@ -9,21 +11,24 @@ UndoSystem::UndoSystem(Buffer &owner, UndoTree &tree)
|
|||||||
void
|
void
|
||||||
UndoSystem::Begin(UndoType type)
|
UndoSystem::Begin(UndoType type)
|
||||||
{
|
{
|
||||||
|
#ifdef KTE_UNDO_DEBUG
|
||||||
|
debug_log("Begin");
|
||||||
|
#endif
|
||||||
// Reuse pending if batching conditions are met
|
// Reuse pending if batching conditions are met
|
||||||
const int row = static_cast<int>(buf_->Cury());
|
const int row = static_cast<int>(buf_->Cury());
|
||||||
const int col = static_cast<int>(buf_->Curx());
|
const int col = static_cast<int>(buf_->Curx());
|
||||||
if (tree_.pending && tree_.pending->type == type && tree_.pending->row == row) {
|
if (tree_.pending && tree_.pending->type == type && tree_.pending->row == row) {
|
||||||
if (type == UndoType::Delete) {
|
if (type == UndoType::Delete) {
|
||||||
// Support batching both forward deletes (DeleteChar) and backspace (prepend case)
|
// Support batching both forward deletes (DeleteChar) and backspace (prepend case)
|
||||||
// Forward delete: cursor stays at anchor col; expected == col
|
// Forward delete: cursor stays at anchor col; keep batching when col == anchor
|
||||||
const auto anchor = static_cast<std::size_t>(tree_.pending->col);
|
const auto anchor = static_cast<std::size_t>(tree_.pending->col);
|
||||||
if (anchor + tree_.pending->text.size() == static_cast<std::size_t>(col)) {
|
if (anchor == static_cast<std::size_t>(col)) {
|
||||||
pending_prepend_ = false;
|
pending_prepend_ = false;
|
||||||
return; // keep batching forward delete
|
return; // keep batching forward delete
|
||||||
}
|
}
|
||||||
// Backspace: cursor moved left by 1; allow extend if col + text.size() == anchor
|
// Backspace: cursor moved left by exactly one position relative to current anchor.
|
||||||
if (static_cast<std::size_t>(col) + tree_.pending->text.size() == anchor) {
|
// Extend batch by shifting anchor left and prepending the deleted byte.
|
||||||
// Move anchor one left to new cursor column; next Append should prepend
|
if (static_cast<std::size_t>(col) + 1 == anchor) {
|
||||||
tree_.pending->col = col;
|
tree_.pending->col = col;
|
||||||
pending_prepend_ = true;
|
pending_prepend_ = true;
|
||||||
return;
|
return;
|
||||||
@@ -47,6 +52,16 @@ UndoSystem::Begin(UndoType type)
|
|||||||
node->next = nullptr;
|
node->next = nullptr;
|
||||||
tree_.pending = node;
|
tree_.pending = node;
|
||||||
pending_prepend_ = false;
|
pending_prepend_ = false;
|
||||||
|
|
||||||
|
#ifdef KTE_UNDO_DEBUG
|
||||||
|
debug_log("Begin:new");
|
||||||
|
#endif
|
||||||
|
// Assert pending is detached from the tree
|
||||||
|
assert(tree_.pending && "pending must exist after Begin");
|
||||||
|
assert(tree_.pending != tree_.root);
|
||||||
|
assert(tree_.pending != tree_.current);
|
||||||
|
assert(tree_.pending != tree_.saved);
|
||||||
|
assert(!is_descendant(tree_.root, tree_.pending));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -61,6 +76,9 @@ UndoSystem::Append(char ch)
|
|||||||
} else {
|
} else {
|
||||||
tree_.pending->text.push_back(ch);
|
tree_.pending->text.push_back(ch);
|
||||||
}
|
}
|
||||||
|
#ifdef KTE_UNDO_DEBUG
|
||||||
|
debug_log("Append:ch");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -70,12 +88,18 @@ UndoSystem::Append(std::string_view text)
|
|||||||
if (!tree_.pending)
|
if (!tree_.pending)
|
||||||
return;
|
return;
|
||||||
tree_.pending->text.append(text.data(), text.size());
|
tree_.pending->text.append(text.data(), text.size());
|
||||||
|
#ifdef KTE_UNDO_DEBUG
|
||||||
|
debug_log("Append:sv");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
UndoSystem::commit()
|
UndoSystem::commit()
|
||||||
{
|
{
|
||||||
|
#ifdef KTE_UNDO_DEBUG
|
||||||
|
debug_log("commit:enter");
|
||||||
|
#endif
|
||||||
if (!tree_.pending)
|
if (!tree_.pending)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -105,6 +129,11 @@ UndoSystem::commit()
|
|||||||
}
|
}
|
||||||
tree_.pending = nullptr;
|
tree_.pending = nullptr;
|
||||||
update_dirty_flag();
|
update_dirty_flag();
|
||||||
|
#ifdef KTE_UNDO_DEBUG
|
||||||
|
debug_log("commit:done");
|
||||||
|
#endif
|
||||||
|
// post-conditions
|
||||||
|
assert(tree_.pending == nullptr && "pending must be cleared after commit");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -121,6 +150,9 @@ UndoSystem::undo()
|
|||||||
apply(node, -1);
|
apply(node, -1);
|
||||||
tree_.current = parent;
|
tree_.current = parent;
|
||||||
update_dirty_flag();
|
update_dirty_flag();
|
||||||
|
#ifdef KTE_UNDO_DEBUG
|
||||||
|
debug_log("undo");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -143,6 +175,9 @@ UndoSystem::redo()
|
|||||||
apply(next, +1);
|
apply(next, +1);
|
||||||
tree_.current = next;
|
tree_.current = next;
|
||||||
update_dirty_flag();
|
update_dirty_flag();
|
||||||
|
#ifdef KTE_UNDO_DEBUG
|
||||||
|
debug_log("redo");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -151,6 +186,9 @@ UndoSystem::mark_saved()
|
|||||||
{
|
{
|
||||||
tree_.saved = tree_.current;
|
tree_.saved = tree_.current;
|
||||||
update_dirty_flag();
|
update_dirty_flag();
|
||||||
|
#ifdef KTE_UNDO_DEBUG
|
||||||
|
debug_log("mark_saved");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -161,6 +199,9 @@ UndoSystem::discard_pending()
|
|||||||
delete tree_.pending;
|
delete tree_.pending;
|
||||||
tree_.pending = nullptr;
|
tree_.pending = nullptr;
|
||||||
}
|
}
|
||||||
|
#ifdef KTE_UNDO_DEBUG
|
||||||
|
debug_log("discard_pending");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -175,6 +216,9 @@ UndoSystem::clear()
|
|||||||
}
|
}
|
||||||
tree_.root = tree_.current = tree_.saved = tree_.pending = nullptr;
|
tree_.root = tree_.current = tree_.saved = tree_.pending = nullptr;
|
||||||
update_dirty_flag();
|
update_dirty_flag();
|
||||||
|
#ifdef KTE_UNDO_DEBUG
|
||||||
|
debug_log("clear");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -293,3 +337,62 @@ UndoSystem::UpdateBufferReference(Buffer &new_buf)
|
|||||||
{
|
{
|
||||||
buf_ = &new_buf;
|
buf_ = &new_buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- Debug helpers ----
|
||||||
|
const char *
|
||||||
|
UndoSystem::type_str(UndoType t)
|
||||||
|
{
|
||||||
|
switch (t) {
|
||||||
|
case UndoType::Insert:
|
||||||
|
return "Insert";
|
||||||
|
case UndoType::Delete:
|
||||||
|
return "Delete";
|
||||||
|
case UndoType::Paste:
|
||||||
|
return "Paste";
|
||||||
|
case UndoType::Newline:
|
||||||
|
return "Newline";
|
||||||
|
case UndoType::DeleteRow:
|
||||||
|
return "DeleteRow";
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
UndoSystem::is_descendant(UndoNode *root, const UndoNode *target)
|
||||||
|
{
|
||||||
|
if (!root || !target)
|
||||||
|
return false;
|
||||||
|
if (root == target)
|
||||||
|
return true;
|
||||||
|
for (UndoNode *child = root->child; child != nullptr; child = child->next) {
|
||||||
|
if (is_descendant(child, target))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
UndoSystem::debug_log(const char *op) const
|
||||||
|
{
|
||||||
|
#ifdef KTE_UNDO_DEBUG
|
||||||
|
int row = static_cast<int>(buf_->Cury());
|
||||||
|
int col = static_cast<int>(buf_->Curx());
|
||||||
|
const UndoNode *p = tree_.pending;
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[UNDO] %s cur=(%d,%d) pending=%p t=%s r=%d c=%d nlen=%zu current=%p saved=%p\n",
|
||||||
|
op,
|
||||||
|
row, col,
|
||||||
|
(const void *) p,
|
||||||
|
p ? type_str(p->type) : "-",
|
||||||
|
p ? p->row : -1,
|
||||||
|
p ? p->col : -1,
|
||||||
|
p ? p->text.size() : 0,
|
||||||
|
(void *) tree_.current,
|
||||||
|
(void *) tree_.saved);
|
||||||
|
#else
|
||||||
|
(void) op;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
#define KTE_UNDOSYSTEM_H
|
#define KTE_UNDOSYSTEM_H
|
||||||
|
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
#include "UndoTree.h"
|
#include "UndoTree.h"
|
||||||
|
|
||||||
@@ -39,6 +41,13 @@ private:
|
|||||||
void free_branch(UndoNode *node); // frees redo siblings only
|
void free_branch(UndoNode *node); // frees redo siblings only
|
||||||
UndoNode *find_parent(UndoNode *from, UndoNode *target);
|
UndoNode *find_parent(UndoNode *from, UndoNode *target);
|
||||||
|
|
||||||
|
// Debug helpers (compiled only when KTE_UNDO_DEBUG is defined)
|
||||||
|
void debug_log(const char *op) const;
|
||||||
|
|
||||||
|
static const char *type_str(UndoType t);
|
||||||
|
|
||||||
|
static bool is_descendant(UndoNode *root, const UndoNode *target);
|
||||||
|
|
||||||
void update_dirty_flag();
|
void update_dirty_flag();
|
||||||
|
|
||||||
Buffer *buf_;
|
Buffer *buf_;
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
stdenv,
|
|
||||||
cmake,
|
|
||||||
ncurses,
|
|
||||||
SDL2,
|
|
||||||
libGL,
|
|
||||||
xorg,
|
|
||||||
installShellFiles,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cmakeContent = builtins.readFile ./CMakeLists.txt;
|
|
||||||
cmakeLines = lib.splitString "\n" cmakeContent;
|
|
||||||
versionLine = lib.findFirst (l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
|
|
||||||
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
|
|
||||||
in
|
|
||||||
stdenv.mkDerivation {
|
|
||||||
pname = "kte";
|
|
||||||
inherit version;
|
|
||||||
|
|
||||||
src = lib.cleanSource ./.;
|
|
||||||
|
|
||||||
nativeBuildInputs = [
|
|
||||||
cmake
|
|
||||||
ncurses
|
|
||||||
SDL2
|
|
||||||
libGL
|
|
||||||
xorg.libX11
|
|
||||||
installShellFiles
|
|
||||||
];
|
|
||||||
|
|
||||||
cmakeFlags = [
|
|
||||||
"-DBUILD_GUI=ON"
|
|
||||||
"-DCMAKE_BUILD_TYPE=Debug"
|
|
||||||
];
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
runHook preInstall
|
|
||||||
|
|
||||||
mkdir -p $out/bin
|
|
||||||
cp kte $out/bin/
|
|
||||||
cp kge $out/bin/
|
|
||||||
|
|
||||||
installManPage ../docs/kte.1
|
|
||||||
installManPage ../docs/kge.1
|
|
||||||
|
|
||||||
mkdir -p $out/share/icons
|
|
||||||
cp ../kge.png $out/share/icons/
|
|
||||||
|
|
||||||
runHook postInstall
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
stdenv,
|
|
||||||
cmake,
|
|
||||||
ncurses,
|
|
||||||
installShellFiles,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cmakeContent = builtins.readFile ./CMakeLists.txt;
|
|
||||||
cmakeLines = lib.splitString "\n" cmakeContent;
|
|
||||||
versionLine = lib.findFirst (l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
|
|
||||||
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
|
|
||||||
in
|
|
||||||
stdenv.mkDerivation {
|
|
||||||
pname = "kte";
|
|
||||||
inherit version;
|
|
||||||
|
|
||||||
src = lib.cleanSource ./.;
|
|
||||||
|
|
||||||
nativeBuildInputs = [
|
|
||||||
cmake
|
|
||||||
ncurses
|
|
||||||
installShellFiles
|
|
||||||
];
|
|
||||||
|
|
||||||
cmakeFlags = [
|
|
||||||
"-DBUILD_GUI=OFF"
|
|
||||||
"-DCMAKE_BUILD_TYPE=Debug"
|
|
||||||
];
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
runHook preInstall
|
|
||||||
|
|
||||||
mkdir -p $out/bin
|
|
||||||
cp kte $out/bin/
|
|
||||||
|
|
||||||
installManPage ../docs/kte.1
|
|
||||||
|
|
||||||
runHook postInstall
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
21
default.nix
21
default.nix
@@ -7,12 +7,16 @@
|
|||||||
libGL,
|
libGL,
|
||||||
xorg,
|
xorg,
|
||||||
installShellFiles,
|
installShellFiles,
|
||||||
|
|
||||||
|
graphical ? false,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cmakeContent = builtins.readFile ./CMakeLists.txt;
|
cmakeContent = builtins.readFile ./CMakeLists.txt;
|
||||||
cmakeLines = lib.splitString "\n" cmakeContent;
|
cmakeLines = lib.splitString "\n" cmakeContent;
|
||||||
versionLine = lib.findFirst (l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
|
versionLine = lib.findFirst (
|
||||||
|
l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null
|
||||||
|
) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
|
||||||
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
|
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
|
||||||
in
|
in
|
||||||
stdenv.mkDerivation {
|
stdenv.mkDerivation {
|
||||||
@@ -24,14 +28,16 @@ stdenv.mkDerivation {
|
|||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
cmake
|
cmake
|
||||||
ncurses
|
ncurses
|
||||||
|
installShellFiles
|
||||||
|
]
|
||||||
|
++ lib.optionals graphical [
|
||||||
SDL2
|
SDL2
|
||||||
libGL
|
libGL
|
||||||
xorg.libX11
|
xorg.libX11
|
||||||
installShellFiles
|
|
||||||
];
|
];
|
||||||
|
|
||||||
cmakeFlags = [
|
cmakeFlags = [
|
||||||
"-DBUILD_GUI=ON"
|
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
|
||||||
"-DCMAKE_BUILD_TYPE=Debug"
|
"-DCMAKE_BUILD_TYPE=Debug"
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -40,14 +46,17 @@ stdenv.mkDerivation {
|
|||||||
|
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
cp kte $out/bin/
|
cp kte $out/bin/
|
||||||
cp kge $out/bin/
|
|
||||||
|
|
||||||
installManPage ../docs/kte.1
|
installManPage ../docs/kte.1
|
||||||
installManPage ../docs/kge.1
|
|
||||||
|
|
||||||
|
''
|
||||||
|
+ lib.optionalString graphical ''
|
||||||
|
cp kge $out/bin/
|
||||||
|
installManPage ../docs/kge.1
|
||||||
mkdir -p $out/share/icons
|
mkdir -p $out/share/icons
|
||||||
cp ../kge.png $out/share/icons/
|
cp ../kge.png $out/share/icons/
|
||||||
|
''
|
||||||
|
+ ''
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
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.
|
||||||
279
docs/undo-roadmap.md
Normal file
279
docs/undo-roadmap.md
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
Undo System Overhaul Roadmap (emacs-style undo-tree)
|
||||||
|
|
||||||
|
Context: macOS, C++17 project, ncurses terminal and SDL2/ImGui GUI frontends. Date: 2025-12-01.
|
||||||
|
|
||||||
|
Purpose
|
||||||
|
|
||||||
|
- Define a clear, incremental plan to implement a robust, non-linear undo system inspired by emacs' undo-tree.
|
||||||
|
- Align implementation with docs/undo.md and fix gaps observed in docs/undo-state.md.
|
||||||
|
- Provide test cases and acceptance criteria so a junior engineer or agentic coding system can execute the plan safely.
|
||||||
|
|
||||||
|
References
|
||||||
|
|
||||||
|
- Specification: docs/undo.md (API, invariants, batching rules, raw buffer ops)
|
||||||
|
- Current snapshot and recent fix: docs/undo-state.md (GUI mapping notes; Begin/Append ordering fix)
|
||||||
|
- Code: UndoSystem.{h,cc}, UndoTree.{h,cc}, UndoNode.{h,cc}, Buffer.{h,cc}, Command.{h,cc}, GUI/Terminal InputHandlers,
|
||||||
|
KKeymap.
|
||||||
|
|
||||||
|
Instrumentation (KTE_UNDO_DEBUG)
|
||||||
|
|
||||||
|
- How to enable
|
||||||
|
- Build with the CMake option `-DKTE_UNDO_DEBUG=ON` to enable concise instrumentation logs from `UndoSystem`.
|
||||||
|
- The following targets receive the `KTE_UNDO_DEBUG` compile definition when ON:
|
||||||
|
- `kte` (terminal), `kge` (GUI), and `test_undo` (tests).
|
||||||
|
- Examples:
|
||||||
|
```sh
|
||||||
|
# Terminal build with tests and instrumentation ON
|
||||||
|
cmake -S . -B cmake-build-term -DBUILD_TESTS=ON -DBUILD_GUI=OFF -DKTE_UNDO_DEBUG=ON
|
||||||
|
cmake --build cmake-build-term --target test_undo -j
|
||||||
|
./cmake-build-term/test_undo 2> undo.log
|
||||||
|
|
||||||
|
# GUI build (requires SDL2/OpenGL/Freetype toolchain) with instrumentation ON
|
||||||
|
cmake -S . -B cmake-build-gui -DBUILD_GUI=ON -DKTE_UNDO_DEBUG=ON
|
||||||
|
cmake --build cmake-build-gui --target kge -j
|
||||||
|
# Run kge and perform actions; logs go to stderr
|
||||||
|
```
|
||||||
|
|
||||||
|
- What it logs
|
||||||
|
- Each Begin/Append/commit/undo/redo operation prints a single `[UNDO]` line with:
|
||||||
|
- current cursor `(row,col)`, pointer to `pending`, its type/row/col/text-size, and pointers to `current`/`saved`.
|
||||||
|
- Example fields: `[UNDO] Begin cur=(0,0) pending=0x... t=Insert r=0 c=0 nlen=2 current=0x... saved=0x...`
|
||||||
|
|
||||||
|
- Example trace snippets
|
||||||
|
- Typing a contiguous word ("Hello") batches into a single Insert node; one commit occurs before the subsequent undo:
|
||||||
|
```text
|
||||||
|
[UNDO] Begin cur=(0,0) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x0 saved=0x0
|
||||||
|
[UNDO] commit:enter cur=(0,0) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x0 saved=0x0
|
||||||
|
[UNDO] Begin:new cur=(0,0) pending=0x... t=Insert r=0 c=0 nlen=0 current=0x0 saved=0x0
|
||||||
|
[UNDO] Append:sv cur=(0,0) pending=0x... t=Insert r=0 c=0 nlen=1 current=0x0 saved=0x0
|
||||||
|
... (more Append as characters are typed) ...
|
||||||
|
[UNDO] commit:enter cur=(0,5) pending=0x... t=Insert r=0 c=0 nlen=5 current=0x0 saved=0x0
|
||||||
|
[UNDO] commit:done cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x... saved=0x0
|
||||||
|
```
|
||||||
|
|
||||||
|
- Undo then Redo across that batch:
|
||||||
|
```text
|
||||||
|
[UNDO] commit:enter cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x... saved=0x0
|
||||||
|
[UNDO] undo cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x0 saved=0x0
|
||||||
|
[UNDO] commit:enter cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x0 saved=0x0
|
||||||
|
[UNDO] redo cur=(0,5) pending=0x0 t=- r=-1 c=-1 nlen=0 current=0x... saved=0x0
|
||||||
|
```
|
||||||
|
|
||||||
|
- Newline and backspace/delete traces follow the same pattern with `t=Newline` or `t=Delete` and immediate commit for newline.
|
||||||
|
Capture by running `kge`/`kte` with `KTE_UNDO_DEBUG=ON` and performing the actions; append representative 3–6 line snippets to docs.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
|
||||||
|
- Pointer values and exact cursor positions in the logs depend on the runtime and actions; this is expected.
|
||||||
|
- Keep `KTE_UNDO_DEBUG` OFF by default in CI/release to avoid noisy logs and any performance impact.
|
||||||
|
|
||||||
|
̄1) Current State Summary (from docs/undo-state.md)
|
||||||
|
|
||||||
|
- Terminal (kte): Keybindings and UndoSystem integration have been stable.
|
||||||
|
- GUI (kge): Previously, C-k u/U mapping and SDL TEXTINPUT suppression had issues on macOS; these were debugged. The
|
||||||
|
core root cause of “status shows Undone but no change” was fixed by moving UndoSystem::Begin/Append/commit to occur
|
||||||
|
after buffer modifications/cursor updates so batching conditions see the correct cursor.
|
||||||
|
- Undo core exists with tree invariants, saved marker/dirty flag mirroring, batching for Insert/Delete, and Newline as a
|
||||||
|
single-step undo.
|
||||||
|
|
||||||
|
Gaps/Risks
|
||||||
|
|
||||||
|
- Event-path unification between KEYDOWN and TEXTINPUT across platforms (macOS specifics).
|
||||||
|
- Comprehensive tests for branching, GC/limits, multi-line operations, and UTF-8 text input.
|
||||||
|
- Advanced/compound command grouping and future region operations.
|
||||||
|
|
||||||
|
|
||||||
|
2) Design Goals (emacs-like undo-tree)
|
||||||
|
|
||||||
|
- Per-buffer, non-linear undo tree: new edits after undo create a branch; existing redo branches are discarded.
|
||||||
|
- Batching: insert/backspace/paste/newline grouped into sensible units to match user expectations.
|
||||||
|
- Silent apply during undo/redo (no re-recording), using raw Buffer methods only.
|
||||||
|
- Correct saved/dirty tracking and robust pending node lifecycle (detached until commit).
|
||||||
|
- Efficient memory behavior; optional pruning limits similar to emacs (undo-limit, undo-strong-limit).
|
||||||
|
- Deterministic behavior across terminal and GUI frontends.
|
||||||
|
|
||||||
|
|
||||||
|
3) Invariants and API (must align with docs/undo.md)
|
||||||
|
|
||||||
|
- UndoTree holds root/current/saved/pending; pending is detached and only linked on commit.
|
||||||
|
- Begin(type) reuses pending only if: same type, same row, and pending->col + pending->text.size() == current cursor
|
||||||
|
col (or prepend rules for backspace sequences); otherwise it commits and starts a new node.
|
||||||
|
- commit(): frees redo siblings from current, attaches pending as current->child, advances current, clears pending;
|
||||||
|
nullifies saved marker if diverged.
|
||||||
|
- undo()/redo(): move current and apply the node using low-level Buffer APIs that do not trigger undo recording.
|
||||||
|
- mark_saved(): updates saved pointer and dirty flag (dirty ⇔ current != saved).
|
||||||
|
- discard_pending()/clear(): lifecycle for buffer close/reset/new file.
|
||||||
|
|
||||||
|
|
||||||
|
4) Phased Roadmap
|
||||||
|
|
||||||
|
Phase 0 — Baseline & Instrumentation (1 day)
|
||||||
|
|
||||||
|
- Audit UndoSystem against docs/undo.md invariants; ensure apply() uses only raw Buffer ops.
|
||||||
|
- Verify Begin/Append ordering across all edit commands: insert, backspace, delete, newline, paste.
|
||||||
|
- Add a temporary debug toggle (compile-time or editor flag) to log Begin/Append/commit/undo/redo, cursor(row,col), node
|
||||||
|
sizes, and pending state. Include assertions for: pending detached, commit clears pending, redo branch freed on new
|
||||||
|
commit, and correct batching preconditions.
|
||||||
|
- Deliverables: Short log from typing/undo/redo scenarios; instrumentation behind a macro so it can be removed.
|
||||||
|
|
||||||
|
Phase 1 — Input Path Unification & Batching Rules (1–2 days)
|
||||||
|
|
||||||
|
- Ensure all printable text insertion (terminal and GUI) flows through CommandId::InsertText and reaches UndoSystem
|
||||||
|
Begin/Append. On SDL, handle KEYDOWN vs TEXTINPUT consistently; always suppress trailing TEXTINPUT after k-prefix
|
||||||
|
suffix commands.
|
||||||
|
- Commit boundaries: at k-prefix entry, before Undo/Redo, on cursor movement, on focus/file ops, and before any
|
||||||
|
non-editing command that should separate undo units.
|
||||||
|
- Batching heuristics:
|
||||||
|
- Insert: same row, contiguous columns; Append(std::string_view) handles multi-character text (pastes, IME).
|
||||||
|
- Backspace: prepend batching in increasing column order (store deleted text in forward order).
|
||||||
|
- Delete (forward): contiguous at same row/col.
|
||||||
|
- Newline: record as UndoType::Newline and immediately commit (single-step undo for line splits/joins).
|
||||||
|
- Deliverables: Manual tests pass for typing/backspace/delete/newline/paste; GUI C-k u/U work as expected on macOS.
|
||||||
|
|
||||||
|
Phase 2 — Tree Limits & GC (1 day)
|
||||||
|
|
||||||
|
- Add configurable memory/size limits for undo data (soft and strong limits like emacs). Implement pruning of oldest
|
||||||
|
ancestors or deep redo branches while preserving recent edits. Provide stats (node count, bytes in text storage).
|
||||||
|
- Deliverables: Config hooks, tests demonstrating pruning without violating apply/undo invariants.
|
||||||
|
|
||||||
|
Phase 3 — Compound Commands & Region Ops (2–3 days)
|
||||||
|
|
||||||
|
- Introduce an optional RAII-style UndoTransaction to group multi-step commands (indent region, kill region, rectangle
|
||||||
|
ops) into a single undo step. Internally this just sequences Begin/Append and ensures commit even on early returns.
|
||||||
|
- Support row operations (InsertRow/DeleteRow) with proper raw Buffer calls. Ensure join_lines/split_line are handled by
|
||||||
|
Newline nodes or dedicated types if necessary.
|
||||||
|
- Deliverables: Commands updated to use transactions when appropriate; tests for region delete/indent and multi-line
|
||||||
|
paste.
|
||||||
|
|
||||||
|
Phase 4 — Developer UX & Diagnostics (1 day)
|
||||||
|
|
||||||
|
- Add a dev command to dump the undo tree (preorder) with markers for current/saved and pending (detached). For GUI,
|
||||||
|
optionally expose a simple ImGui debug window (behind a compile flag) that visualizes the current branch.
|
||||||
|
- Editor status improvements: show short status codes for undo/redo and when a new branch was created or redo discarded.
|
||||||
|
- Deliverables: Tree dump command; example output in docs.
|
||||||
|
|
||||||
|
Phase 5 — Comprehensive Tests & Property Checks (2–3 days)
|
||||||
|
|
||||||
|
- Unit tests (extend test_undo.cc):
|
||||||
|
- Insert batching: type "Hello" then one undo removes all; redo restores.
|
||||||
|
- Backspace batching: type "Hello", backspace 3×, undo → restores the 3; redo → re-applies deletion.
|
||||||
|
- Delete batching (forward delete) with cursor not moving.
|
||||||
|
- Newline: split a line and undo to join; join a line (via backspace at col 0) and undo to split.
|
||||||
|
- Branching: type "abc", undo twice, type "X" → redo history discarded; ensure redo no longer restores 'b'/'c'.
|
||||||
|
- Saved/dirty: mark_saved after typing; ensure dirty flag toggles correctly after undo/redo; saved marker tracks the
|
||||||
|
node.
|
||||||
|
- discard_pending: create pending by typing, then move cursor or invoke commit boundary; ensure pending is attached;
|
||||||
|
also ensure discard on buffer close clears pending.
|
||||||
|
- clear(): resets state with no leaks; tree pointers null.
|
||||||
|
- UTF-8 input: insert multi-byte characters via InsertText with multi-char std::string; ensure counts/col tracking
|
||||||
|
behave (text stored as bytes; editor col policy consistent within kte).
|
||||||
|
- Integration tests (TestFrontend):
|
||||||
|
- Both TerminalFrontend and GUIFrontend: simulate text input and commands, including k-prefix C-k u/U.
|
||||||
|
- Paste scenarios: multi-character insertions batched as one.
|
||||||
|
- Property tests (optional but recommended):
|
||||||
|
- Generate random sequences of edits; record them; then apply undo until root and redo back to the end → buffer
|
||||||
|
contents match at each step; no crashes; dirty flag transitions consistent. Seed-based to reproduce failures.
|
||||||
|
- Redo-branch discard property: any new edit after undo must eliminate redo path; redoing should be impossible
|
||||||
|
afterward.
|
||||||
|
- Deliverables: Tests merged and passing on CI for both frontends; failures block changes to undo core.
|
||||||
|
|
||||||
|
Phase 6 — Performance & Stress (0.5–1 day)
|
||||||
|
|
||||||
|
- Stress test with large files and long edit sequences. Target: smooth typing at 10k+ ops/minute on commodity hardware;
|
||||||
|
memory growth bounded when GC limits enabled.
|
||||||
|
- Deliverables: Basic perf notes; optional lightweight benchmarks.
|
||||||
|
|
||||||
|
|
||||||
|
5) Acceptance Criteria
|
||||||
|
|
||||||
|
- Conformance to docs/undo.md invariants and API surface (including raw Buffer operations for apply()).
|
||||||
|
- Repro checklist passes:
|
||||||
|
- Type text; single-step undo/redo works and respects batching.
|
||||||
|
- Backspace/delete batching works.
|
||||||
|
- Newline split/join are single-step undo/redo.
|
||||||
|
- Branching works: undo, then type → redo path is discarded; no ghost redo.
|
||||||
|
- Saved/dirty flags accurate across undo/redo and diverge/rejoin paths.
|
||||||
|
- No pending nodes leaked on buffer close/reload; no re-recording during undo/redo.
|
||||||
|
- Behavior identical across terminal and GUI input paths.
|
||||||
|
- Tests added for all above; CI green.
|
||||||
|
|
||||||
|
|
||||||
|
6) Concrete Work Items by File
|
||||||
|
|
||||||
|
- UndoSystem.h/cc:
|
||||||
|
- Re-verify Begin/Append ordering; enforce batching invariants; prepend logic for backspace; immediate commit for
|
||||||
|
newline.
|
||||||
|
- Implement/verify apply() uses only Buffer raw methods: insert_text/delete_text/split_line/join_lines/row ops.
|
||||||
|
- Add limits (configurable) and stats; add discard_pending safety paths.
|
||||||
|
- Buffer.h/cc:
|
||||||
|
- Ensure raw methods exist and do not trigger undo; ensure UpdateBufferReference is correctly used when
|
||||||
|
replacing/renaming the underlying buffer.
|
||||||
|
- Call undo.commit() on cursor movement and non-editing commands (via Command layer integration).
|
||||||
|
- Command.cc:
|
||||||
|
- Ensure all edit commands drive UndoSystem correctly; commit at k-prefix entry and before Undo/Redo.
|
||||||
|
- Introduce UndoTransaction for compound commands when needed.
|
||||||
|
- GUIInputHandler.cc / TerminalInputHandler.cc / KKeymap.cc:
|
||||||
|
- Ensure unified InsertText path; suppress SDL_TEXTINPUT when a k-prefix suffix produced a command; preserve case
|
||||||
|
mapping.
|
||||||
|
- Tests: test_undo.cc (extend) + new tests (e.g., test_undo_branching.cc, test_undo_multiline.cc).
|
||||||
|
|
||||||
|
|
||||||
|
7) Example Test Cases (sketches)
|
||||||
|
|
||||||
|
- Branch discard after undo:
|
||||||
|
1) InsertText("abc"); Undo(); Undo(); InsertText("X"); Redo();
|
||||||
|
Expected: Redo is a no-op (or status indicates no redo), buffer is "aX".
|
||||||
|
|
||||||
|
- Newline split/join:
|
||||||
|
1) InsertText("ab"); Newline(); InsertText("c"); Undo();
|
||||||
|
Expected: single undo joins lines → buffer "abc" on one line at original join point; Redo() splits again.
|
||||||
|
|
||||||
|
- Backspace batching:
|
||||||
|
1) InsertText("hello"); Backspace×3; Undo();
|
||||||
|
Expected: restores "hello".
|
||||||
|
|
||||||
|
- UTF-8 insertion:
|
||||||
|
1) InsertText("😀汉"); Undo(); Redo();
|
||||||
|
Expected: content unchanged across cycles; no crashes.
|
||||||
|
|
||||||
|
- Saved/dirty transitions:
|
||||||
|
1) InsertText("hi"); mark_saved(); InsertText("!"); Undo(); Redo();
|
||||||
|
Expected: dirty false after mark_saved; dirty true after InsertText("!"); dirty returns to false after Undo();
|
||||||
|
true again after Redo().
|
||||||
|
|
||||||
|
|
||||||
|
8) Risks & Mitigations
|
||||||
|
|
||||||
|
- SDL/macOS event ordering (KEYDOWN vs TEXTINPUT, IME): Mitigate by suppressing TEXTINPUT on mapped k-prefix suffixes;
|
||||||
|
optionally temporarily disable SDL text input during k-prefix suffix mapping; add targeted diagnostics.
|
||||||
|
- UTF-8 width vs byte-length: Store bytes in UndoNode::text; keep column logic consistent with existing Buffer
|
||||||
|
semantics.
|
||||||
|
- Memory growth: Add GC/limits and provide a way to clear/reduce history for huge sessions.
|
||||||
|
- Re-entrancy during apply(): Prevent public edit paths from being called; use only raw operations.
|
||||||
|
|
||||||
|
|
||||||
|
9) Nice-to-Have (post-MVP)
|
||||||
|
|
||||||
|
- Visual undo-tree navigation (emacs-like time travel and branch selection), at least as a debug tool initially.
|
||||||
|
- Persistent undo across saves (opt-in; likely out-of-scope initially).
|
||||||
|
- Time-based batching threshold (e.g., break batches after >500ms pause in typing).
|
||||||
|
|
||||||
|
|
||||||
|
10) Execution Notes for a Junior Engineer/Agentic System
|
||||||
|
|
||||||
|
- Start from Phase 0; do not skip instrumentation—assertions will catch subtle batching bugs early.
|
||||||
|
- Change one surface at a time; when adjusting Begin/Append/commit positions, re-run unit tests immediately.
|
||||||
|
- Always ensure commit boundaries before invoking commands that move the cursor/state.
|
||||||
|
- When unsure about apply(), read docs/undo.md and mirror exactly: only raw Buffer methods, never the public editing
|
||||||
|
APIs.
|
||||||
|
- Keep diffs small and localized; add tests alongside behavior changes.
|
||||||
|
|
||||||
|
Appendix A — Minimal Developer Checklist
|
||||||
|
|
||||||
|
- [ ] Begin/Append occur after buffer mutation and cursor updates for all edit commands.
|
||||||
|
- [ ] Pending detached until commit; freed/cleared on commit/discard/clear.
|
||||||
|
- [ ] Redo branches freed on new commit after undo.
|
||||||
|
- [ ] mark_saved updates both saved pointer and dirty flag; dirty mirrors current != saved.
|
||||||
|
- [ ] apply() uses only raw Buffer methods; no recording during apply.
|
||||||
|
- [ ] Terminal and GUI both route printable input to InsertText; k-prefix mapping suppresses trailing TEXTINPUT.
|
||||||
|
- [ ] Unit and integration tests cover batching, branching, newline, saved/dirty, and UTF-8 cases.
|
||||||
@@ -1,128 +1,139 @@
|
|||||||
Undo/Redo + C-k GUI status (macOS) — current state snapshot
|
### Context recap
|
||||||
|
|
||||||
Context
|
- The undo system is now tree‑based with batching rules and `KTE_UNDO_DEBUG` instrumentation hooks already present in
|
||||||
- Platform: macOS (Darwin)
|
`UndoSystem.{h,cc}`.
|
||||||
- Target: GUI build (kge) using SDL2/ImGui path
|
- GUI path uses SDL; printable input now flows exclusively via `SDL_TEXTINPUT` to `CommandId::InsertText`, while
|
||||||
- Date: 2025-11-30 00:30 local (from user)
|
control/meta/movement (incl. Backspace/Delete/Newline and k‑prefix) come from `SDL_KEYDOWN`.
|
||||||
|
- Commit boundaries must be enforced at well‑defined points (movement, non‑editing commands, newline, undo/redo, etc.).
|
||||||
|
|
||||||
What works right now
|
### Status summary (2025‑12‑01)
|
||||||
- Terminal (kte): C-k keymap and UndoSystem integration have been stable in recent builds.
|
|
||||||
- GUI: Most C-k mappings work: C-k d (KillToEOL), C-k x (Save+Quit), C-k q (Quit) — confirmed by user.
|
|
||||||
- UndoSystem core is implemented and integrated for InsertText/Newline/Delete/Backspace. Buffer owns an UndoSystem and raw edit APIs are used by apply().
|
|
||||||
|
|
||||||
What is broken (GUI, macOS)
|
- Input‑path unification: Completed. `GUIInputHandler.cc` routes all printable characters through `SDL_TEXTINPUT → InsertText`.
|
||||||
- C-k u: Status shows "Undone" but buffer content does not change (no visible undo).
|
Newlines originate only from `SDL_KEYDOWN → Newline`. CR/LF are filtered out of `SDL_TEXTINPUT` payloads. Suppression
|
||||||
- C-k U: Inserts a literal 'U' into the buffer; does not execute Redo.
|
rules prevent stray `TEXTINPUT` after meta/prefix/universal‑argument flows. Terminal input path remains consistent.
|
||||||
- C-k C-u / C-k C-U: No effect (expected unmapped), but the k-prefix prompt can remain in some paths.
|
- Tests: `test_undo.cc` expanded to cover branching behavior, UTF‑8 insertion, multi‑line newline/join, and typing batching.
|
||||||
|
All scenarios pass.
|
||||||
|
- Instrumentation: `KTE_UNDO_DEBUG` hooks exist in `UndoSystem.{h,cc}`; a CMake toggle has not yet been added.
|
||||||
|
- Commit boundaries: Undo/Redo commit boundaries are in place; newline path commits immediately by design. A final audit
|
||||||
|
pass across movement/non‑editing commands is still pending.
|
||||||
|
- Docs: This status document updated. Further docs (instrumentation how‑to and example traces) remain pending in
|
||||||
|
`docs/undo.md` / `docs/undo-roadmap.md`.
|
||||||
|
|
||||||
Repro steps (GUI)
|
### Objectives
|
||||||
1) Type "Hello".
|
|
||||||
2) Press C-k then press u → status becomes "Undone", but text remains "Hello".
|
|
||||||
3) Press C-k then press Shift+U → a literal 'U' is inserted (becomes "HelloU").
|
|
||||||
4) Press C-k then hold Ctrl on the suffix and press u → status "Undone", still no change.
|
|
||||||
5) Press C-k then hold Ctrl on the suffix and press Shift+U → status shows the k-prefix prompt again ("C-k _").
|
|
||||||
|
|
||||||
Keymap and input-layer changes we attempted (and kept)
|
- Use the existing instrumentation to capture short traces of typing/backspacing/deleting and undo/redo.
|
||||||
- KKeymap.cc: Case-sensitive 'U' mapping prioritized before the lowercase table. Added ctrl→non-ctrl fall-through so C-k u/U still map even if SDL reports Ctrl held on the suffix.
|
- Unify input paths (SDL `KEYDOWN` vs `TEXTINPUT`) and lock down commit boundaries across commands.
|
||||||
- TerminalInputHandler: already preserved case and mapped correctly.
|
- Extend tests to cover branching behavior, UTF‑8, and multi‑line operations.
|
||||||
- GUIInputHandler:
|
|
||||||
- Preserve case for k-prefix suffix letters (Shift → uppercase). Clear esc_meta before k-suffix mapping.
|
|
||||||
- Strengthened SDL_TEXTINPUT suppression after a k-prefix printable suffix to avoid inserting literal characters.
|
|
||||||
- Added fallback to map the k-prefix suffix in the SDL_TEXTINPUT path (to catch macOS cases where uppercase arrives in TEXTINPUT rather than KEYDOWN).
|
|
||||||
- Fixed malformed switch block introduced during iteration.
|
|
||||||
- Command layer: commit pending undo batch at k-prefix entry and just before Undo/Redo so prior typing can actually be undone/redone.
|
|
||||||
|
|
||||||
Diagnostics added
|
### Plan of action
|
||||||
- GUIInputHandler logs k-prefix u/U suffix attempts to stderr and (previously) /tmp/kge.log. The user’s macOS session showed only KEYDOWN logs for 'u':
|
|
||||||
- "[kge] k-prefix suffix: sym=117 mods=0x0 ascii=117 'u' ctrl2=0 pass_ctrl=0 mapped=1 id=38"
|
|
||||||
- "[kge] k-prefix suffix: sym=117 mods=0x80 ascii=117 'u' ctrl2=1 pass_ctrl=0 mapped=1 id=38"
|
|
||||||
- No logs were produced for 'U' (neither KEYDOWN nor TEXTINPUT). The /tmp log file was not created on the user’s host in the last run (stderr logs were visible earlier from KEYDOWN).
|
|
||||||
|
|
||||||
Hypotheses for current failures
|
1. Enable instrumentation and make it easy to toggle
|
||||||
1) Undo appears to be invoked (status "Undone"), but no state change:
|
- Add a CMake option in `CMakeLists.txt` (root project):
|
||||||
- The most likely cause is that no committed node exists at the time of undo (i.e., typing "Hello" is not being recorded as an undo node), because our current typing path in Command.cc directly edits buffer rows without always driving UndoSystem Begin/Append/commit at the right times for every printable char on GUI.
|
`option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)`.
|
||||||
- Although we call u->Begin(Insert) and u->Append(text) in cmd_insert_text for CommandId::InsertText, the GUI printable input might be arriving through a different path or being short-circuited (e.g., via a prompt or suppression), resulting in actual text insertion but no corresponding UndoSystem pending node content, or pending but never committed.
|
- When ON, add a compile definition `-DKTE_UNDO_DEBUG` to all targets that include the editor core (e.g., `kte`,
|
||||||
- We now commit at k-prefix entry and before undo; if there is still "nothing to undo", it implies the batch never had text appended (Append not called) or is detached from the real buffer edits.
|
`kge`, and test binaries).
|
||||||
|
- Keep the default OFF so normal builds are quiet; ensure both modes compile in CI.
|
||||||
|
|
||||||
2) Redo via C-k U inserts a literal 'U':
|
2. Capture short traces to validate current behavior
|
||||||
- On macOS, uppercase letters often arrive as SDL_TEXTINPUT events. We added TEXTINPUT-based k-prefix mapping, but the user's run still showed a literal insertion and no diagnostic lines for TEXTINPUT, which suggests:
|
- Build with `-DKTE_UNDO_DEBUG=ON` and run the GUI frontend:
|
||||||
a) The TEXTINPUT suppression didn’t trigger for that platform/sequence, or
|
- Scenario A: type a contiguous word, then move cursor (should show `Begin(Insert)` + multiple `Append`, single
|
||||||
b) The k-prefix flag was already cleared by the time TEXTINPUT arrived, so the TEXTINPUT path defaulted to InsertText, or
|
`commit` at a movement boundary).
|
||||||
c) The GUI window’s input focus or SDL event ordering differs from expectations (e.g., IME/text input settings), so our suppression/mapping didn’t see the event.
|
- Scenario B: hold backspace to delete a run, including backspace batching (prepend rule); verify
|
||||||
|
`Begin(Delete)` with prepended `Append` behavior, single `commit`.
|
||||||
|
- Scenario C: forward deletes at a fixed column (anchor batching); expected single `Begin(Delete)` with same
|
||||||
|
column.
|
||||||
|
- Scenario D: insert newline (`Newline` node) and immediately commit; type text on the next line; undo/redo
|
||||||
|
across the boundary.
|
||||||
|
- Scenario E: undo chain and redo chain; then type new text and confirm redo branch gets discarded in logs.
|
||||||
|
- Save representative trace snippets and add to `docs/undo.md` or `docs/undo-roadmap.md` for reference.
|
||||||
|
|
||||||
Relevant code pointers
|
3. Input‑path unification (SDL `KEYDOWN` vs `TEXTINPUT`) — Completed 2025‑12‑01
|
||||||
- Key mapping tables: KKeymap.cc → KLookupKCommand() (C-k suffix), KLookupCtrlCommand(), KLookupEscCommand().
|
- In `GUIInputHandler.cc`:
|
||||||
- Terminal input: TerminalInputHandler.cc → map_key_to_command().
|
- Ensure printable characters are generated exclusively from `SDL_TEXTINPUT` and mapped to
|
||||||
- GUI input: GUIInputHandler.cc → map_key() and GUIInputHandler::ProcessSDLEvent() (KEYDOWN + TEXTINPUT handling, suppression, k_prefix_/esc_meta_ flags).
|
`CommandId::InsertText`.
|
||||||
- Command dispatch: Command.cc → cmd_insert_text(), cmd_newline(), cmd_backspace(), cmd_delete_char(), cmd_undo(), cmd_redo(), cmd_kprefix().
|
- Keep `SDL_KEYDOWN` for control/meta/movement, backspace/delete, newline, and k‑prefix handling.
|
||||||
- Undo core: UndoSystem.{h,cc}, UndoNode.{h,cc}, UndoTree.{h,cc}. Buffer raw methods used by apply().
|
- Maintain suppression of stray `SDL_TEXTINPUT` immediately following meta/prefix or universal‑argument
|
||||||
|
collection so no accidental text is inserted.
|
||||||
|
- Confirm that `InsertText` path never carries `"\n"`; newline must only originate from `KEYDOWN` →
|
||||||
|
`CommandId::Newline`.
|
||||||
|
- If the terminal input path exists, ensure parity: printable insertions go through `InsertText`, control via key
|
||||||
|
events, and the same commit boundaries apply.
|
||||||
|
- Status: Implemented. See `GUIInputHandler.cc` changes; tests confirm parity with terminal path.
|
||||||
|
|
||||||
Immediate next steps (when we return to this)
|
4. Enforce and verify commit boundaries in command execution — In progress
|
||||||
1) Verify that GUI printable insertion always flows through CommandId::InsertText so UndoSystem::Begin/Append gets called. If SDL_TEXTINPUT delivers multi-byte strings, ensure Append() is given the same text inserted into buffer.
|
- Audit `Command.cc` and ensure `u->commit()` is called before executing any non‑editing command that should end a
|
||||||
- Add a one-session debug hook in cmd_insert_text to assert/trace: pending node type/text length and current cursor col before/after.
|
batch:
|
||||||
- If GUI sometimes sends CommandId::InsertTextEmpty or another path, unify.
|
- Movement commands (left/right/up/down/home/end/page).
|
||||||
|
- Prompt accept/cancel transitions and mode switches (search prompts, replace prompts).
|
||||||
|
- Buffer/file operations (open/switch/save/close), and focus changes.
|
||||||
|
- Before running `Undo` or `Redo` (already present).
|
||||||
|
- Ensure immediate commit at the end of atomic edit operations:
|
||||||
|
- `Newline` insertion and line joins (`Delete` of newline when backspacing at column 0) should create
|
||||||
|
`UndoType::Newline` and commit immediately (parts are already implemented; verify all call sites).
|
||||||
|
- Pastes should be a single `Paste`/`Insert` batch per operation (depending on current design).
|
||||||
|
|
||||||
2) Ensure batching rules are satisfied so Begin() reuses pending correctly:
|
5. Extend automated tests (or add them if absent) — Phase 1 completed
|
||||||
- Begin(Insert) must see same row and col == pending->col + pending->text.size() for typing sequences.
|
- Branching behavior ✓
|
||||||
- If GUI accumulates multiple characters per TEXTINPUT (e.g., pasted text), Append(std::string_view) is fine, but row/col expectations remain.
|
- Insert `"abc"`, undo twice (back to `"a"`), insert `"X"`, assert redo list is discarded, and new timeline
|
||||||
|
continues with `aX`.
|
||||||
|
- Navigate undo/redo along the new branch to ensure correctness.
|
||||||
|
- UTF‑8 insertion and deletion ✓
|
||||||
|
- Insert `"é漢"` (multi‑byte characters) via `InsertText`; verify buffer content and that a single Insert batch
|
||||||
|
is created.
|
||||||
|
- Undo/redo restores/removes the full insertion batch.
|
||||||
|
- Backspace after typed UTF‑8 should remove the last inserted codepoint from the batch in a single undo step (
|
||||||
|
current semantics are byte‑oriented in buffer ops; test to current behavior and document).
|
||||||
|
- Multi‑line operations ✓
|
||||||
|
- Newline splits a line: verify an `UndoType::Newline` node is created and committed immediately; undo/redo
|
||||||
|
round‑trip.
|
||||||
|
- Backspace at column 0 joins with previous line: record as `Newline` deletion (via `UndoType::Newline`
|
||||||
|
inverse); undo/redo round‑trip.
|
||||||
|
- Typing and deletion batching ✓ (typing) / Pending (delete batching)
|
||||||
|
- Typing a contiguous word (no cursor moves) yields one `Insert` node with accumulated text.
|
||||||
|
- Forward delete at a fixed anchor column yields one `Delete` batch. (Pending test)
|
||||||
|
- Backspace batching uses the prepend rule when the cursor moves left. (Pending test)
|
||||||
|
- Place tests near existing test suite files (e.g., `tests/test_undo.cc`) or create them if not present. Prefer
|
||||||
|
using `Buffer` + `UndoSystem` directly for tight unit tests; add higher‑level integration tests as needed.
|
||||||
|
|
||||||
3) For C-k U uppercase mapping on macOS:
|
6. Documentation updates — In progress
|
||||||
- Add a temporary status dump when k-prefix suffix mapping falls back to TEXTINPUT path (we added stderr prints; also set Editor status with a short code like "K-TI U" during one session) to confirm path is used and suppression is working.
|
- In `docs/undo.md` and `docs/undo-roadmap.md`:
|
||||||
- If TEXTINPUT never arrives, force suppression: when we detect k-prefix and KEYDOWN of a letter with Shift, preemptively handle via KEYDOWN-derived uppercase ASCII rather than deferring.
|
- Describe how to enable instrumentation (`KTE_UNDO_DEBUG`) and an example of trace logs.
|
||||||
|
- List batching rules and commit boundaries clearly with examples.
|
||||||
|
- Document current UTF‑8 semantics (byte‑wise vs codepoint‑wise) and any known limitations.
|
||||||
|
- Current status: this `undo-state.md` updated; instrumentation how‑to and example traces pending.
|
||||||
|
|
||||||
4) Consolidate k-prefix handling:
|
7. CI and build hygiene — Pending
|
||||||
- After mapping a k-prefix suffix to a command (Undo/Redo/etc.), always set suppress_text_input_once_ = true to avoid any trailing TEXTINPUT.
|
- Default builds: `KTE_UNDO_DEBUG` OFF.
|
||||||
- Clear k_prefix_ reliably on both KEYDOWN and TEXTINPUT paths.
|
- Add a CI job that builds and runs tests with `KTE_UNDO_DEBUG=ON` to ensure the instrumentation path remains
|
||||||
|
healthy.
|
||||||
|
- Ensure no performance regressions or excessive logging in release builds.
|
||||||
|
|
||||||
5) Once mapping is solid, remove all diagnostics and keep the minimal, deterministic logic.
|
8. Stretch goals (optional, time‑boxed) — Pending
|
||||||
|
- IME composition: confirm that `SDL_TEXTINPUT` behavior during IME composition does not produce partial/broken
|
||||||
|
insertions; if needed, buffer composition updates into a single commit.
|
||||||
|
- Ensure paste operations (multi‑line/UTF‑8) remain atomic in undo history.
|
||||||
|
|
||||||
Open questions for future debugging
|
### How to run the tests
|
||||||
- Does SDL on this macOS setup deliver Shift+U as KEYDOWN+TEXTINPUT consistently, or only TEXTINPUT? We need a small on-screen debug to avoid relying on stderr.
|
|
||||||
- Are there any IME/TextInput SDL hints on macOS we should set for raw key handling during k-prefix?
|
|
||||||
- Should we temporarily disable SDL text input (SDL_StopTextInput) during k-prefix suffix processing to eliminate TEXTINPUT races on macOS?
|
|
||||||
|
|
||||||
Notes on UndoSystem correctness (unrelated to the GUI mapping bug)
|
- Configure with `-DBUILD_TESTS=ON` and build the `test_undo` target. Run the produced binary (e.g., `./test_undo`).
|
||||||
- Undo tree invariants are implemented: pending is detached; commit attaches and clears redo branches; undo/redo apply low-level Buffer edits with no public editor paths; saved marker updated via mark_saved().
|
The test prints progress and uses assertions to validate behavior.
|
||||||
- Dirty flag mirrors (current != saved).
|
|
||||||
- Delete batching supports prepend for backspace sequences (stored text is in increasing column order from anchor).
|
|
||||||
- Newline joins/splits are recorded as UndoType::Newline and committed immediately for single-step undo of line joins.
|
|
||||||
|
|
||||||
Owner pointers & file locations
|
### Deliverables
|
||||||
- GUI mapping and suppression: GUIInputHandler.cc
|
|
||||||
- Command layer commit boundaries: Command.cc (cmd_kprefix, cmd_undo, cmd_redo)
|
|
||||||
- Undo batching entry points: Command.cc (cmd_insert_text, cmd_backspace, cmd_delete_char, cmd_newline)
|
|
||||||
|
|
||||||
End of snapshot — safe to resume from here.
|
- CMake toggle for instrumentation and verified logs for core scenarios. (Pending)
|
||||||
|
- Updated `GUIInputHandler.cc` solidifying `KEYDOWN` vs `TEXTINPUT` separation and suppression rules. (Completed)
|
||||||
|
- Verified commit boundaries in `Command.cc` with comments where appropriate. (In progress)
|
||||||
|
- New tests for branching, UTF‑8, and multi‑line operations; all passing. (Completed for listed scenarios)
|
||||||
|
- Docs updated with how‑to and example traces. (Pending)
|
||||||
|
|
||||||
---
|
### Acceptance criteria
|
||||||
|
|
||||||
RESOLUTION (2025-11-30)
|
### Current status (2025‑12‑01) vs acceptance criteria
|
||||||
|
|
||||||
Root Cause Identified and Fixed
|
- Short instrumentation traces match expected batching and commit behavior for typing, backspace/delete, newline, and
|
||||||
The undo system failure was caused by incorrect timing of UndoSystem::Begin() and Append() calls relative to buffer modifications in Command.cc.
|
undo/redo. — Pending (instrumentation toggle + capture not done)
|
||||||
|
- Printable input comes exclusively from `SDL_TEXTINPUT`; no stray inserts after meta/prefix/universal‑argument flows.
|
||||||
Problem:
|
— Satisfied (GUI path updated; terminal path consistent)
|
||||||
- In cmd_insert_text, cmd_backspace, cmd_delete_char, and cmd_newline, the undo recording (Begin/Append) was called BEFORE the actual buffer modification and cursor update.
|
- Undo branching behaves correctly; redo is discarded upon new commits after undo. — Satisfied (tested)
|
||||||
- UndoSystem::Begin() checks the current cursor position to determine if it can batch with the pending node.
|
- UTF‑8 and multi‑line scenarios round‑trip via undo/redo according to the documented semantics. — Satisfied (tested)
|
||||||
- For Insert type: Begin() checks if col == pending->col + pending->text.size()
|
- Tests pass with `KTE_UNDO_DEBUG` both OFF and ON. — Pending (no CMake toggle yet; default OFF passes)
|
||||||
- For Delete type: Begin() checks if the cursor is at the expected position based on whether it's forward delete or backspace
|
|
||||||
- When Begin/Append were called before cursor updates, the batching condition would fail on the second character because the cursor hadn't moved yet from the first insertion.
|
|
||||||
- This caused each character to create a separate batch, but since commit() was never called between characters (only at k-prefix or undo), the pending node would be overwritten rather than committed, resulting in no undo history.
|
|
||||||
|
|
||||||
Fix Applied:
|
|
||||||
- cmd_insert_text: Moved Begin/Append to AFTER buffer insertion (lines 854-856) and cursor update (line 857).
|
|
||||||
- cmd_backspace: Moved Begin/Append to AFTER character deletion (lines 1024-1025) and cursor decrement (line 1026).
|
|
||||||
- cmd_delete_char: Moved Begin/Append to AFTER character deletion (lines 1074-1076).
|
|
||||||
- cmd_newline: Moved Begin/commit to AFTER line split (lines 956-966) and cursor update (lines 963-967).
|
|
||||||
|
|
||||||
Result:
|
|
||||||
- Begin() now sees the correct cursor position after each edit, allowing proper batching of consecutive characters.
|
|
||||||
- Typing "Hello" will now create a single pending batch with all 5 characters that can be undone as one unit.
|
|
||||||
- The fix applies to both terminal (kte) and GUI (kge) builds.
|
|
||||||
|
|
||||||
Testing Recommendation:
|
|
||||||
- Type several characters (e.g., "Hello")
|
|
||||||
- Press C-k u to undo - the entire word should disappear
|
|
||||||
- Press C-k U to redo - the word should reappear
|
|
||||||
- Test backspace batching: type several characters, then backspace multiple times, then undo - should undo the backspace batch
|
|
||||||
- Test delete batching similarly
|
|
||||||
25626
ext/json.h
Normal file
25626
ext/json.h
Normal file
File diff suppressed because it is too large
Load Diff
185
ext/json_fwd.h
Normal file
185
ext/json_fwd.h
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
// __ _____ _____ _____
|
||||||
|
// __| | __| | | | JSON for Modern C++
|
||||||
|
// | | |__ | | | | | | version 3.12.0
|
||||||
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||||
|
//
|
||||||
|
// SPDX-FileCopyrightText: 2013-2025 Niels Lohmann <https://nlohmann.me>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_
|
||||||
|
#define INCLUDE_NLOHMANN_JSON_FWD_HPP_
|
||||||
|
|
||||||
|
#include <cstdint> // int64_t, uint64_t
|
||||||
|
#include <map> // map
|
||||||
|
#include <memory> // allocator
|
||||||
|
#include <string> // string
|
||||||
|
#include <vector> // vector
|
||||||
|
|
||||||
|
// #include <nlohmann/detail/abi_macros.hpp>
|
||||||
|
// __ _____ _____ _____
|
||||||
|
// __| | __| | | | JSON for Modern C++
|
||||||
|
// | | |__ | | | | | | version 3.12.0
|
||||||
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||||
|
//
|
||||||
|
// SPDX-FileCopyrightText: 2013-2025 Niels Lohmann <https://nlohmann.me>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
|
||||||
|
// This file contains all macro definitions affecting or depending on the ABI
|
||||||
|
|
||||||
|
#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK
|
||||||
|
#if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH)
|
||||||
|
#if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0
|
||||||
|
#warning "Already included a different version of the library!"
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum)
|
||||||
|
#define NLOHMANN_JSON_VERSION_MINOR 12 // NOLINT(modernize-macro-to-enum)
|
||||||
|
#define NLOHMANN_JSON_VERSION_PATCH 0 // NOLINT(modernize-macro-to-enum)
|
||||||
|
|
||||||
|
#ifndef JSON_DIAGNOSTICS
|
||||||
|
#define JSON_DIAGNOSTICS 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef JSON_DIAGNOSTIC_POSITIONS
|
||||||
|
#define JSON_DIAGNOSTIC_POSITIONS 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||||
|
#define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if JSON_DIAGNOSTICS
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
|
||||||
|
#else
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if JSON_DIAGNOSTIC_POSITIONS
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp
|
||||||
|
#else
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp
|
||||||
|
#else
|
||||||
|
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
|
||||||
|
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Construct the namespace ABI tags component
|
||||||
|
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c
|
||||||
|
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \
|
||||||
|
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c)
|
||||||
|
|
||||||
|
#define NLOHMANN_JSON_ABI_TAGS \
|
||||||
|
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
|
||||||
|
NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \
|
||||||
|
NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \
|
||||||
|
NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS)
|
||||||
|
|
||||||
|
// Construct the namespace version component
|
||||||
|
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
|
||||||
|
_v ## major ## _ ## minor ## _ ## patch
|
||||||
|
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \
|
||||||
|
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch)
|
||||||
|
|
||||||
|
#if NLOHMANN_JSON_NAMESPACE_NO_VERSION
|
||||||
|
#define NLOHMANN_JSON_NAMESPACE_VERSION
|
||||||
|
#else
|
||||||
|
#define NLOHMANN_JSON_NAMESPACE_VERSION \
|
||||||
|
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \
|
||||||
|
NLOHMANN_JSON_VERSION_MINOR, \
|
||||||
|
NLOHMANN_JSON_VERSION_PATCH)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Combine namespace components
|
||||||
|
#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b
|
||||||
|
#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \
|
||||||
|
NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b)
|
||||||
|
|
||||||
|
#ifndef NLOHMANN_JSON_NAMESPACE
|
||||||
|
#define NLOHMANN_JSON_NAMESPACE \
|
||||||
|
nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \
|
||||||
|
NLOHMANN_JSON_ABI_TAGS, \
|
||||||
|
NLOHMANN_JSON_NAMESPACE_VERSION)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||||
|
#define NLOHMANN_JSON_NAMESPACE_BEGIN \
|
||||||
|
namespace nlohmann \
|
||||||
|
{ \
|
||||||
|
inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \
|
||||||
|
NLOHMANN_JSON_ABI_TAGS, \
|
||||||
|
NLOHMANN_JSON_NAMESPACE_VERSION) \
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef NLOHMANN_JSON_NAMESPACE_END
|
||||||
|
#define NLOHMANN_JSON_NAMESPACE_END \
|
||||||
|
} /* namespace (inline namespace) NOLINT(readability/namespace) */ \
|
||||||
|
} // namespace nlohmann
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief namespace for Niels Lohmann
|
||||||
|
@see https://github.com/nlohmann
|
||||||
|
@since version 1.0.0
|
||||||
|
*/
|
||||||
|
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||||
|
/*!
|
||||||
|
@brief default JSONSerializer template argument
|
||||||
|
|
||||||
|
This serializer ignores the template arguments and uses ADL
|
||||||
|
([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl))
|
||||||
|
for serialization.
|
||||||
|
*/
|
||||||
|
template<typename T = void, typename SFINAE = void>
|
||||||
|
struct adl_serializer;
|
||||||
|
|
||||||
|
/// a class to store JSON values
|
||||||
|
/// @sa https://json.nlohmann.me/api/basic_json/
|
||||||
|
template<template<typename U, typename V, typename... Args> class ObjectType =
|
||||||
|
std::map,
|
||||||
|
template<typename U, typename... Args> class ArrayType = std::vector,
|
||||||
|
class StringType = std::string, class BooleanType = bool,
|
||||||
|
class NumberIntegerType = std::int64_t,
|
||||||
|
class NumberUnsignedType = std::uint64_t,
|
||||||
|
class NumberFloatType = double,
|
||||||
|
template<typename U> class AllocatorType = std::allocator,
|
||||||
|
template<typename T, typename SFINAE = void> class JSONSerializer =
|
||||||
|
adl_serializer,
|
||||||
|
class BinaryType = std::vector<std::uint8_t>, // cppcheck-suppress syntaxError
|
||||||
|
class CustomBaseClass = void>
|
||||||
|
class basic_json;
|
||||||
|
|
||||||
|
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
|
||||||
|
/// @sa https://json.nlohmann.me/api/json_pointer/
|
||||||
|
template<typename RefStringType>
|
||||||
|
class json_pointer;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief default specialization
|
||||||
|
@sa https://json.nlohmann.me/api/json/
|
||||||
|
*/
|
||||||
|
using json = basic_json<>;
|
||||||
|
|
||||||
|
/// @brief a minimal map-like container that preserves insertion order
|
||||||
|
/// @sa https://json.nlohmann.me/api/ordered_map/
|
||||||
|
template<class Key, class T, class IgnoredLess, class Allocator>
|
||||||
|
struct ordered_map;
|
||||||
|
|
||||||
|
/// @brief specialization that maintains the insertion order of object keys
|
||||||
|
/// @sa https://json.nlohmann.me/api/ordered_json/
|
||||||
|
using ordered_json = basic_json<nlohmann::ordered_map>;
|
||||||
|
|
||||||
|
NLOHMANN_JSON_NAMESPACE_END
|
||||||
|
|
||||||
|
#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# flake.nix
|
|
||||||
{
|
|
||||||
description = "kte ImGui/SDL2 text editor";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils }:
|
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
|
||||||
let
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
in {
|
|
||||||
packages.default = pkgs.stdenv.mkDerivation {
|
|
||||||
pname = "kte";
|
|
||||||
version = "0.1.0";
|
|
||||||
src = ./.;
|
|
||||||
|
|
||||||
nativeBuildInputs = [ pkgs.cmake pkgs.pkg-config ];
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
ncurses
|
|
||||||
SDL2
|
|
||||||
libGL
|
|
||||||
xorg.libX11
|
|
||||||
];
|
|
||||||
|
|
||||||
cmakeFlags = [
|
|
||||||
"-DBUILD_GUI=ON"
|
|
||||||
"-DCURSES_NEED_NCURSES=TRUE"
|
|
||||||
"-DCURSES_NEED_WIDE=TRUE"
|
|
||||||
];
|
|
||||||
|
|
||||||
# Alternative (even stronger): completely hide the broken module
|
|
||||||
preConfigure = ''
|
|
||||||
# If the project ships its own FindSDL2.cmake in cmake/, hide it
|
|
||||||
if [ -f cmake/FindSDL2.cmake ]; then
|
|
||||||
mv cmake/FindSDL2.cmake cmake/FindSDL2.cmake.disabled
|
|
||||||
echo "Disabled bundled FindSDL2.cmake"
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
meta = with pkgs.lib; {
|
|
||||||
description = "kte ImGui/SDL2 GUI editor";
|
|
||||||
mainProgram = "kte";
|
|
||||||
platforms = platforms.linux;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
inputsFrom = [ self.packages.${system}.default ];
|
|
||||||
packages = with pkgs; [ gdb clang-tools ];
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
16
flake.nix
16
flake.nix
@@ -3,16 +3,18 @@
|
|||||||
|
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
outputs = inputs @ { self, nixpkgs, ... }:
|
outputs =
|
||||||
|
inputs@{ self, nixpkgs, ... }:
|
||||||
let
|
let
|
||||||
eachSystem = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
|
eachSystem = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
|
||||||
pkgsFor = system: import nixpkgs { inherit system; };
|
pkgsFor = system: import nixpkgs { inherit system; };
|
||||||
in {
|
in
|
||||||
packages = eachSystem (system: {
|
{
|
||||||
default = (pkgsFor system).callPackage ./default-nogui.nix { };
|
packages = eachSystem (system: rec {
|
||||||
kge = (pkgsFor system).callPackage ./default-gui.nix { };
|
default = kte;
|
||||||
kte = (pkgsFor system).callPackage ./default-nogui.nix { };
|
full = kge;
|
||||||
full = (pkgsFor system).callPackage ./default.nix { };
|
kte = (pkgsFor system).callPackage ./default.nix { graphical = false; };
|
||||||
|
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
8
kte-cloc
8
kte-cloc
@@ -1,3 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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 lsp/*.{cc,h} | grep -v '^Font.h$' | xargs cloc ${fmt_args}
|
||||||
|
|||||||
49
lsp/BufferChangeTracker.cc
Normal file
49
lsp/BufferChangeTracker.cc
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* BufferChangeTracker.cc - minimal initial implementation
|
||||||
|
*/
|
||||||
|
#include "BufferChangeTracker.h"
|
||||||
|
#include "../Buffer.h"
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
BufferChangeTracker::BufferChangeTracker(const Buffer *buffer)
|
||||||
|
: buffer_(buffer) {}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
BufferChangeTracker::recordInsertion(int /*row*/, int /*col*/, const std::string &/*text*/)
|
||||||
|
{
|
||||||
|
// For Phase 1–2 bring-up, coalesce to full-document changes
|
||||||
|
fullChangePending_ = true;
|
||||||
|
++version_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
BufferChangeTracker::recordDeletion(int /*row*/, int /*col*/, std::size_t /*len*/)
|
||||||
|
{
|
||||||
|
fullChangePending_ = true;
|
||||||
|
++version_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<TextDocumentContentChangeEvent>
|
||||||
|
BufferChangeTracker::getChanges() const
|
||||||
|
{
|
||||||
|
std::vector<TextDocumentContentChangeEvent> v;
|
||||||
|
if (!buffer_)
|
||||||
|
return v;
|
||||||
|
if (fullChangePending_) {
|
||||||
|
TextDocumentContentChangeEvent ev;
|
||||||
|
ev.text = buffer_->FullText();
|
||||||
|
v.push_back(std::move(ev));
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
BufferChangeTracker::clearChanges()
|
||||||
|
{
|
||||||
|
fullChangePending_ = false;
|
||||||
|
}
|
||||||
|
} // namespace kte::lsp
|
||||||
44
lsp/BufferChangeTracker.h
Normal file
44
lsp/BufferChangeTracker.h
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* BufferChangeTracker.h - integrates with Buffer to accumulate LSP-friendly changes
|
||||||
|
*/
|
||||||
|
#ifndef KTE_BUFFER_CHANGE_TRACKER_H
|
||||||
|
#define KTE_BUFFER_CHANGE_TRACKER_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "LspTypes.h"
|
||||||
|
|
||||||
|
class Buffer; // forward declare from core
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
class BufferChangeTracker {
|
||||||
|
public:
|
||||||
|
explicit BufferChangeTracker(const Buffer *buffer);
|
||||||
|
|
||||||
|
// Called by Buffer on each edit operation
|
||||||
|
void recordInsertion(int row, int col, const std::string &text);
|
||||||
|
|
||||||
|
void recordDeletion(int row, int col, std::size_t len);
|
||||||
|
|
||||||
|
// Get accumulated changes since last sync
|
||||||
|
std::vector<TextDocumentContentChangeEvent> getChanges() const;
|
||||||
|
|
||||||
|
// Clear changes after sending to LSP
|
||||||
|
void clearChanges();
|
||||||
|
|
||||||
|
// Get current document version for LSP
|
||||||
|
int getVersion() const
|
||||||
|
{
|
||||||
|
return version_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const Buffer *buffer_ = nullptr;
|
||||||
|
bool fullChangePending_ = false;
|
||||||
|
int version_ = 0;
|
||||||
|
};
|
||||||
|
} // namespace kte::lsp
|
||||||
|
|
||||||
|
#endif // KTE_BUFFER_CHANGE_TRACKER_H
|
||||||
37
lsp/Diagnostic.h
Normal file
37
lsp/Diagnostic.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Diagnostic.h - LSP diagnostic data types
|
||||||
|
*/
|
||||||
|
#ifndef KTE_LSP_DIAGNOSTIC_H
|
||||||
|
#define KTE_LSP_DIAGNOSTIC_H
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "LspTypes.h"
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
enum class DiagnosticSeverity {
|
||||||
|
Error = 1,
|
||||||
|
Warning = 2,
|
||||||
|
Information = 3,
|
||||||
|
Hint = 4
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DiagnosticRelatedInformation {
|
||||||
|
std::string uri; // related location URI
|
||||||
|
Range range; // related range
|
||||||
|
std::string message;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Diagnostic {
|
||||||
|
Range range{};
|
||||||
|
DiagnosticSeverity severity{DiagnosticSeverity::Information};
|
||||||
|
std::optional<std::string> code;
|
||||||
|
std::optional<std::string> source;
|
||||||
|
std::string message;
|
||||||
|
std::vector<DiagnosticRelatedInformation> relatedInfo;
|
||||||
|
};
|
||||||
|
} // namespace kte::lsp
|
||||||
|
|
||||||
|
#endif // KTE_LSP_DIAGNOSTIC_H
|
||||||
30
lsp/DiagnosticDisplay.h
Normal file
30
lsp/DiagnosticDisplay.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* DiagnosticDisplay.h - Abstract interface for showing diagnostics
|
||||||
|
*/
|
||||||
|
#ifndef KTE_LSP_DIAGNOSTIC_DISPLAY_H
|
||||||
|
#define KTE_LSP_DIAGNOSTIC_DISPLAY_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Diagnostic.h"
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
class DiagnosticDisplay {
|
||||||
|
public:
|
||||||
|
virtual ~DiagnosticDisplay() = default;
|
||||||
|
|
||||||
|
virtual void updateDiagnostics(const std::string &uri,
|
||||||
|
const std::vector<Diagnostic> &diagnostics) = 0;
|
||||||
|
|
||||||
|
virtual void showInlineDiagnostic(const Diagnostic &diagnostic) = 0;
|
||||||
|
|
||||||
|
virtual void showDiagnosticList(const std::vector<Diagnostic> &diagnostics) = 0;
|
||||||
|
|
||||||
|
virtual void hideDiagnosticList() = 0;
|
||||||
|
|
||||||
|
virtual void updateStatusBar(int errorCount, int warningCount) = 0;
|
||||||
|
};
|
||||||
|
} // namespace kte::lsp
|
||||||
|
|
||||||
|
#endif // KTE_LSP_DIAGNOSTIC_DISPLAY_H
|
||||||
123
lsp/DiagnosticStore.cc
Normal file
123
lsp/DiagnosticStore.cc
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* DiagnosticStore.cc - implementation
|
||||||
|
*/
|
||||||
|
#include "DiagnosticStore.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
void
|
||||||
|
DiagnosticStore::setDiagnostics(const std::string &uri, std::vector<Diagnostic> diagnostics)
|
||||||
|
{
|
||||||
|
diagnostics_[uri] = std::move(diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const std::vector<Diagnostic> &
|
||||||
|
DiagnosticStore::getDiagnostics(const std::string &uri) const
|
||||||
|
{
|
||||||
|
auto it = diagnostics_.find(uri);
|
||||||
|
static const std::vector<Diagnostic> kEmpty;
|
||||||
|
if (it == diagnostics_.end())
|
||||||
|
return kEmpty;
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<Diagnostic>
|
||||||
|
DiagnosticStore::getDiagnosticsAtLine(const std::string &uri, int line) const
|
||||||
|
{
|
||||||
|
std::vector<Diagnostic> out;
|
||||||
|
auto it = diagnostics_.find(uri);
|
||||||
|
if (it == diagnostics_.end())
|
||||||
|
return out;
|
||||||
|
out.reserve(it->second.size());
|
||||||
|
for (const auto &d: it->second) {
|
||||||
|
if (containsLine(d.range, line))
|
||||||
|
out.push_back(d);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::optional<Diagnostic>
|
||||||
|
DiagnosticStore::getDiagnosticAtPosition(const std::string &uri, Position pos) const
|
||||||
|
{
|
||||||
|
auto it = diagnostics_.find(uri);
|
||||||
|
if (it == diagnostics_.end())
|
||||||
|
return std::nullopt;
|
||||||
|
for (const auto &d: it->second) {
|
||||||
|
if (containsPosition(d.range, pos))
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
DiagnosticStore::clear(const std::string &uri)
|
||||||
|
{
|
||||||
|
diagnostics_.erase(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
DiagnosticStore::clearAll()
|
||||||
|
{
|
||||||
|
diagnostics_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
DiagnosticStore::getErrorCount(const std::string &uri) const
|
||||||
|
{
|
||||||
|
auto it = diagnostics_.find(uri);
|
||||||
|
if (it == diagnostics_.end())
|
||||||
|
return 0;
|
||||||
|
int count = 0;
|
||||||
|
for (const auto &d: it->second) {
|
||||||
|
if (d.severity == DiagnosticSeverity::Error)
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
DiagnosticStore::getWarningCount(const std::string &uri) const
|
||||||
|
{
|
||||||
|
auto it = diagnostics_.find(uri);
|
||||||
|
if (it == diagnostics_.end())
|
||||||
|
return 0;
|
||||||
|
int count = 0;
|
||||||
|
for (const auto &d: it->second) {
|
||||||
|
if (d.severity == DiagnosticSeverity::Warning)
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
DiagnosticStore::containsLine(const Range &r, int line)
|
||||||
|
{
|
||||||
|
return (line > r.start.line || line == r.start.line) &&
|
||||||
|
(line < r.end.line || line == r.end.line);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
DiagnosticStore::containsPosition(const Range &r, const Position &p)
|
||||||
|
{
|
||||||
|
if (p.line < r.start.line || p.line > r.end.line)
|
||||||
|
return false;
|
||||||
|
if (r.start.line == r.end.line) {
|
||||||
|
return p.line == r.start.line && p.character >= r.start.character && p.character <= r.end.character;
|
||||||
|
}
|
||||||
|
if (p.line == r.start.line)
|
||||||
|
return p.character >= r.start.character;
|
||||||
|
if (p.line == r.end.line)
|
||||||
|
return p.character <= r.end.character;
|
||||||
|
return true; // between start and end lines
|
||||||
|
}
|
||||||
|
} // namespace kte::lsp
|
||||||
42
lsp/DiagnosticStore.h
Normal file
42
lsp/DiagnosticStore.h
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* DiagnosticStore.h - Central storage for diagnostics by document URI
|
||||||
|
*/
|
||||||
|
#ifndef KTE_LSP_DIAGNOSTIC_STORE_H
|
||||||
|
#define KTE_LSP_DIAGNOSTIC_STORE_H
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Diagnostic.h"
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
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();
|
||||||
|
|
||||||
|
int getErrorCount(const std::string &uri) const;
|
||||||
|
|
||||||
|
int getWarningCount(const std::string &uri) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, std::vector<Diagnostic> > diagnostics_;
|
||||||
|
|
||||||
|
static bool containsLine(const Range &r, int line);
|
||||||
|
|
||||||
|
static bool containsPosition(const Range &r, const Position &p);
|
||||||
|
};
|
||||||
|
} // namespace kte::lsp
|
||||||
|
|
||||||
|
#endif // KTE_LSP_DIAGNOSTIC_STORE_H
|
||||||
147
lsp/JsonRpcTransport.cc
Normal file
147
lsp/JsonRpcTransport.cc
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
/*
|
||||||
|
* JsonRpcTransport.cc - minimal stdio JSON-RPC framing (Content-Length)
|
||||||
|
*/
|
||||||
|
#include "JsonRpcTransport.h"
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
void
|
||||||
|
JsonRpcTransport::connect(int inFd, int outFd)
|
||||||
|
{
|
||||||
|
inFd_ = inFd;
|
||||||
|
outFd_ = outFd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
JsonRpcTransport::send(const std::string &/*method*/, const std::string &payload)
|
||||||
|
{
|
||||||
|
if (outFd_ < 0)
|
||||||
|
return;
|
||||||
|
const std::string header = "Content-Length: " + std::to_string(payload.size()) + "\r\n\r\n";
|
||||||
|
std::lock_guard<std::mutex> lk(writeMutex_);
|
||||||
|
// write header
|
||||||
|
const char *hbuf = header.data();
|
||||||
|
size_t hleft = header.size();
|
||||||
|
while (hleft > 0) {
|
||||||
|
ssize_t n = ::write(outFd_, hbuf, hleft);
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hbuf += static_cast<size_t>(n);
|
||||||
|
hleft -= static_cast<size_t>(n);
|
||||||
|
}
|
||||||
|
// write payload
|
||||||
|
const char *pbuf = payload.data();
|
||||||
|
size_t pleft = payload.size();
|
||||||
|
while (pleft > 0) {
|
||||||
|
ssize_t n = ::write(outFd_, pbuf, pleft);
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pbuf += static_cast<size_t>(n);
|
||||||
|
pleft -= static_cast<size_t>(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
readLineCrlf(int fd, std::string &out, size_t maxLen)
|
||||||
|
{
|
||||||
|
out.clear();
|
||||||
|
char ch;
|
||||||
|
while (true) {
|
||||||
|
ssize_t n = ::read(fd, &ch, 1);
|
||||||
|
if (n == 0)
|
||||||
|
return false; // EOF
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.push_back(ch);
|
||||||
|
// Handle CRLF or bare LF as end-of-line
|
||||||
|
if ((out.size() >= 2 && out[out.size() - 2] == '\r' && out[out.size() - 1] == '\n') ||
|
||||||
|
(out.size() >= 1 && out[out.size() - 1] == '\n')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (out.size() > maxLen) {
|
||||||
|
// sanity cap
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::optional<JsonRpcMessage>
|
||||||
|
JsonRpcTransport::read()
|
||||||
|
{
|
||||||
|
if (inFd_ < 0)
|
||||||
|
return std::nullopt;
|
||||||
|
// Parse headers (case-insensitive), accept/ignore extras
|
||||||
|
size_t contentLength = 0;
|
||||||
|
while (true) {
|
||||||
|
std::string line;
|
||||||
|
if (!readLineCrlf(inFd_, line, kMaxHeaderLine))
|
||||||
|
return std::nullopt;
|
||||||
|
// Normalize end-of-line handling: consider blank line as end of headers
|
||||||
|
if (line == "\r\n" || line == "\n" || line == "\r")
|
||||||
|
break;
|
||||||
|
// Trim trailing CRLF
|
||||||
|
if (!line.empty() && (line.back() == '\n' || line.back() == '\r')) {
|
||||||
|
while (!line.empty() && (line.back() == '\n' || line.back() == '\r'))
|
||||||
|
line.pop_back();
|
||||||
|
}
|
||||||
|
// Find colon
|
||||||
|
auto pos = line.find(':');
|
||||||
|
if (pos == std::string::npos)
|
||||||
|
continue;
|
||||||
|
std::string name = line.substr(0, pos);
|
||||||
|
std::string value = line.substr(pos + 1);
|
||||||
|
// trim leading spaces in value
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < value.size() && (value[i] == ' ' || value[i] == '\t'))
|
||||||
|
++i;
|
||||||
|
value.erase(0, i);
|
||||||
|
// lower-case name for comparison
|
||||||
|
for (auto &c: name)
|
||||||
|
c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
|
||||||
|
if (name == "content-length") {
|
||||||
|
size_t len = static_cast<size_t>(std::strtoull(value.c_str(), nullptr, 10));
|
||||||
|
if (len > kMaxBody) {
|
||||||
|
return std::nullopt; // drop too-large message
|
||||||
|
}
|
||||||
|
contentLength = len;
|
||||||
|
}
|
||||||
|
// else: ignore other headers
|
||||||
|
}
|
||||||
|
if (contentLength == 0)
|
||||||
|
return std::nullopt;
|
||||||
|
std::string body;
|
||||||
|
body.resize(contentLength);
|
||||||
|
size_t readTotal = 0;
|
||||||
|
while (readTotal < contentLength) {
|
||||||
|
ssize_t n = ::read(inFd_, &body[readTotal], contentLength - readTotal);
|
||||||
|
if (n == 0)
|
||||||
|
return std::nullopt;
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
readTotal += static_cast<size_t>(n);
|
||||||
|
}
|
||||||
|
return JsonRpcMessage{std::move(body)};
|
||||||
|
}
|
||||||
|
} // namespace kte::lsp
|
||||||
43
lsp/JsonRpcTransport.h
Normal file
43
lsp/JsonRpcTransport.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* JsonRpcTransport.h - minimal JSON-RPC over stdio transport
|
||||||
|
*/
|
||||||
|
#ifndef KTE_JSON_RPC_TRANSPORT_H
|
||||||
|
#define KTE_JSON_RPC_TRANSPORT_H
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
struct JsonRpcMessage {
|
||||||
|
std::string raw; // raw JSON payload (stub)
|
||||||
|
};
|
||||||
|
|
||||||
|
class JsonRpcTransport {
|
||||||
|
public:
|
||||||
|
JsonRpcTransport() = default;
|
||||||
|
|
||||||
|
~JsonRpcTransport() = default;
|
||||||
|
|
||||||
|
// Connect this transport to file descriptors (read from inFd, write to outFd)
|
||||||
|
void connect(int inFd, int outFd);
|
||||||
|
|
||||||
|
// Send a method call (request or notification)
|
||||||
|
// 'payload' should be a complete JSON object string to send as the message body.
|
||||||
|
void send(const std::string &method, const std::string &payload);
|
||||||
|
|
||||||
|
// Blocking read next message; returns nullopt on EOF or error
|
||||||
|
std::optional<JsonRpcMessage> read();
|
||||||
|
|
||||||
|
private:
|
||||||
|
int inFd_ = -1;
|
||||||
|
int outFd_ = -1;
|
||||||
|
std::mutex writeMutex_;
|
||||||
|
|
||||||
|
// Limits to keep the transport resilient
|
||||||
|
static constexpr size_t kMaxHeaderLine = 16 * 1024; // 16 KiB per header line
|
||||||
|
static constexpr size_t kMaxBody = 64ull * 1024ull * 1024ull; // 64 MiB body cap
|
||||||
|
};
|
||||||
|
} // namespace kte::lsp
|
||||||
|
|
||||||
|
#endif // KTE_JSON_RPC_TRANSPORT_H
|
||||||
75
lsp/LspClient.h
Normal file
75
lsp/LspClient.h
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* LspClient.h - Core LSP client abstraction (initial stub)
|
||||||
|
*/
|
||||||
|
#ifndef KTE_LSP_CLIENT_H
|
||||||
|
#define KTE_LSP_CLIENT_H
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "LspTypes.h"
|
||||||
|
#include "Diagnostic.h"
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
// Callback types for initial language features
|
||||||
|
// If error is non-empty, the result may be default-constructed/empty
|
||||||
|
using CompletionCallback = std::function<void(const CompletionList & result, const std::string & error)>;
|
||||||
|
using HoverCallback = std::function<void(const HoverResult & result, const std::string & error)>;
|
||||||
|
using LocationCallback = std::function<void(const std::vector<Location> & result, const std::string & error)>;
|
||||||
|
|
||||||
|
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 (initial)
|
||||||
|
virtual void completion(const std::string &, Position,
|
||||||
|
CompletionCallback) {}
|
||||||
|
|
||||||
|
|
||||||
|
virtual void hover(const std::string &, Position,
|
||||||
|
HoverCallback) {}
|
||||||
|
|
||||||
|
|
||||||
|
virtual void definition(const std::string &, Position,
|
||||||
|
LocationCallback) {}
|
||||||
|
|
||||||
|
|
||||||
|
// Process Management
|
||||||
|
virtual bool isRunning() const = 0;
|
||||||
|
|
||||||
|
virtual std::string getServerName() const = 0;
|
||||||
|
|
||||||
|
// Handlers (optional; set by manager)
|
||||||
|
using DiagnosticsHandler = std::function<void(const std::string & uri,
|
||||||
|
const std::vector<Diagnostic> &diagnostics
|
||||||
|
)
|
||||||
|
>;
|
||||||
|
|
||||||
|
|
||||||
|
virtual void setDiagnosticsHandler(DiagnosticsHandler h)
|
||||||
|
{
|
||||||
|
(void) h;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace kte::lsp
|
||||||
|
|
||||||
|
#endif // KTE_LSP_CLIENT_H
|
||||||
736
lsp/LspManager.cc
Normal file
736
lsp/LspManager.cc
Normal file
@@ -0,0 +1,736 @@
|
|||||||
|
/*
|
||||||
|
* LspManager.cc - central coordination of LSP servers and diagnostics
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "LspManager.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <utility>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <ctime>
|
||||||
|
#include <cstdarg>
|
||||||
|
|
||||||
|
#include "../Buffer.h"
|
||||||
|
#include "../Editor.h"
|
||||||
|
#include "BufferChangeTracker.h"
|
||||||
|
#include "LspProcessClient.h"
|
||||||
|
#include "UtfCodec.h"
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
static void
|
||||||
|
lsp_debug_file(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
FILE *f = std::fopen("/tmp/kte-lsp.log", "a");
|
||||||
|
if (!f)
|
||||||
|
return;
|
||||||
|
// prepend timestamp
|
||||||
|
std::time_t t = std::time(nullptr);
|
||||||
|
char ts[32];
|
||||||
|
std::strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
||||||
|
std::fprintf(f, "[%s] ", ts);
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
std::vfprintf(f, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
std::fputc('\n', f);
|
||||||
|
std::fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LspManager::LspManager(Editor *editor, DiagnosticDisplay *display)
|
||||||
|
: editor_(editor), display_(display)
|
||||||
|
{
|
||||||
|
// Pre-populate with sensible default server configs
|
||||||
|
registerDefaultServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspManager::registerServer(const std::string &languageId, const LspServerConfig &config)
|
||||||
|
{
|
||||||
|
serverConfigs_[languageId] = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
LspManager::startServerForBuffer(Buffer *buffer)
|
||||||
|
{
|
||||||
|
const auto lang = getLanguageId(buffer);
|
||||||
|
if (lang.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (servers_.find(lang) != servers_.end() && servers_[lang]->isRunning()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = serverConfigs_.find(lang);
|
||||||
|
if (it == serverConfigs_.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &cfg = it->second;
|
||||||
|
// Respect autostart for automatic starts on buffer open
|
||||||
|
if (!cfg.autostart) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Allow env override of server path
|
||||||
|
std::string command = cfg.command;
|
||||||
|
if (lang == "cpp") {
|
||||||
|
if (const char *p = std::getenv("KTE_LSP_CLANGD"); p && *p)
|
||||||
|
command = p;
|
||||||
|
} else if (lang == "go") {
|
||||||
|
if (const char *p = std::getenv("KTE_LSP_GOPLS"); p && *p)
|
||||||
|
command = p;
|
||||||
|
} else if (lang == "rust") {
|
||||||
|
if (const char *p = std::getenv("KTE_LSP_RUST_ANALYZER"); p && *p)
|
||||||
|
command = p;
|
||||||
|
}
|
||||||
|
if (debug_) {
|
||||||
|
std::fprintf(stderr, "[kte][lsp] startServerForBuffer: lang=%s cmd=%s args=%zu file=%s\n",
|
||||||
|
lang.c_str(), command.c_str(), cfg.args.size(), buffer->Filename().c_str());
|
||||||
|
lsp_debug_file("startServerForBuffer: lang=%s cmd=%s args=%zu file=%s",
|
||||||
|
lang.c_str(), command.c_str(), cfg.args.size(), buffer->Filename().c_str());
|
||||||
|
}
|
||||||
|
auto client = std::make_unique<LspProcessClient>(command, cfg.args);
|
||||||
|
// Wire diagnostics handler to manager
|
||||||
|
client->setDiagnosticsHandler([this](const std::string &uri, const std::vector<Diagnostic> &diags) {
|
||||||
|
this->handleDiagnostics(uri, diags);
|
||||||
|
});
|
||||||
|
// Determine workspace root using rootPatterns if set; fallback to file's parent
|
||||||
|
std::string rootPath;
|
||||||
|
if (!buffer->Filename().empty()) {
|
||||||
|
rootPath = detectWorkspaceRoot(buffer->Filename(), cfg);
|
||||||
|
if (rootPath.empty()) {
|
||||||
|
fs::path p(buffer->Filename());
|
||||||
|
rootPath = p.has_parent_path() ? p.parent_path().string() : std::string{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (debug_) {
|
||||||
|
const char *pathEnv = std::getenv("PATH");
|
||||||
|
std::fprintf(stderr, "[kte][lsp] initializing server: rootPath=%s PATH=%s\n",
|
||||||
|
rootPath.c_str(), pathEnv ? pathEnv : "<null>");
|
||||||
|
lsp_debug_file("initializing server: rootPath=%s PATH=%s",
|
||||||
|
rootPath.c_str(), pathEnv ? pathEnv : "<null>");
|
||||||
|
}
|
||||||
|
if (!client->initialize(rootPath)) {
|
||||||
|
if (debug_) {
|
||||||
|
std::fprintf(stderr, "[kte][lsp] initialize failed for lang=%s\n", lang.c_str());
|
||||||
|
lsp_debug_file("initialize failed for lang=%s", lang.c_str());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
servers_[lang] = std::move(client);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspManager::stopServer(const std::string &languageId)
|
||||||
|
{
|
||||||
|
auto it = servers_.find(languageId);
|
||||||
|
if (it != servers_.end()) {
|
||||||
|
it->second->shutdown();
|
||||||
|
servers_.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspManager::stopAllServers()
|
||||||
|
{
|
||||||
|
for (auto &kv: servers_) {
|
||||||
|
kv.second->shutdown();
|
||||||
|
}
|
||||||
|
servers_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
LspManager::startServerForLanguage(const std::string &languageId, const std::string &rootPath)
|
||||||
|
{
|
||||||
|
auto cfgIt = serverConfigs_.find(languageId);
|
||||||
|
if (cfgIt == serverConfigs_.end())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// If already running, nothing to do
|
||||||
|
auto it = servers_.find(languageId);
|
||||||
|
if (it != servers_.end() && it->second && it->second->isRunning()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &cfg = cfgIt->second;
|
||||||
|
std::string command = cfg.command;
|
||||||
|
if (languageId == "cpp") {
|
||||||
|
if (const char *p = std::getenv("KTE_LSP_CLANGD"); p && *p)
|
||||||
|
command = p;
|
||||||
|
} else if (languageId == "go") {
|
||||||
|
if (const char *p = std::getenv("KTE_LSP_GOPLS"); p && *p)
|
||||||
|
command = p;
|
||||||
|
} else if (languageId == "rust") {
|
||||||
|
if (const char *p = std::getenv("KTE_LSP_RUST_ANALYZER"); p && *p)
|
||||||
|
command = p;
|
||||||
|
}
|
||||||
|
if (debug_) {
|
||||||
|
std::fprintf(stderr, "[kte][lsp] startServerForLanguage: lang=%s cmd=%s args=%zu root=%s\n",
|
||||||
|
languageId.c_str(), command.c_str(), cfg.args.size(), rootPath.c_str());
|
||||||
|
lsp_debug_file("startServerForLanguage: lang=%s cmd=%s args=%zu root=%s",
|
||||||
|
languageId.c_str(), command.c_str(), cfg.args.size(), rootPath.c_str());
|
||||||
|
}
|
||||||
|
auto client = std::make_unique<LspProcessClient>(command, cfg.args);
|
||||||
|
client->setDiagnosticsHandler([this](const std::string &uri, const std::vector<Diagnostic> &diags) {
|
||||||
|
this->handleDiagnostics(uri, diags);
|
||||||
|
});
|
||||||
|
std::string root = rootPath;
|
||||||
|
if (!root.empty()) {
|
||||||
|
// keep
|
||||||
|
} else {
|
||||||
|
// Try cwd if not provided
|
||||||
|
root = std::string();
|
||||||
|
}
|
||||||
|
if (!client->initialize(root)) {
|
||||||
|
if (debug_) {
|
||||||
|
std::fprintf(stderr, "[kte][lsp] initialize failed for lang=%s\n", languageId.c_str());
|
||||||
|
lsp_debug_file("initialize failed for lang=%s", languageId.c_str());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
servers_[languageId] = std::move(client);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
LspManager::restartServer(const std::string &languageId, const std::string &rootPath)
|
||||||
|
{
|
||||||
|
stopServer(languageId);
|
||||||
|
return startServerForLanguage(languageId, rootPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspManager::onBufferOpened(Buffer *buffer)
|
||||||
|
{
|
||||||
|
if (debug_) {
|
||||||
|
std::fprintf(stderr, "[kte][lsp] onBufferOpened: file=%s lang=%s\n",
|
||||||
|
buffer->Filename().c_str(), getLanguageId(buffer).c_str());
|
||||||
|
lsp_debug_file("onBufferOpened: file=%s lang=%s",
|
||||||
|
buffer->Filename().c_str(), getLanguageId(buffer).c_str());
|
||||||
|
}
|
||||||
|
if (!startServerForBuffer(buffer)) {
|
||||||
|
if (debug_) {
|
||||||
|
std::fprintf(stderr, "[kte][lsp] onBufferOpened: server did not start\n");
|
||||||
|
lsp_debug_file("onBufferOpened: server did not start");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto *client = ensureServerForLanguage(getLanguageId(buffer));
|
||||||
|
if (!client)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto uri = getUri(buffer);
|
||||||
|
const auto lang = getLanguageId(buffer);
|
||||||
|
const int version = static_cast<int>(buffer->Version());
|
||||||
|
const std::string text = buffer->FullText();
|
||||||
|
if (debug_) {
|
||||||
|
std::fprintf(stderr, "[kte][lsp] didOpen: uri=%s lang=%s version=%d bytes=%zu\n",
|
||||||
|
uri.c_str(), lang.c_str(), version, text.size());
|
||||||
|
lsp_debug_file("didOpen: uri=%s lang=%s version=%d bytes=%zu",
|
||||||
|
uri.c_str(), lang.c_str(), version, text.size());
|
||||||
|
}
|
||||||
|
client->didOpen(uri, lang, version, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspManager::onBufferChanged(Buffer *buffer)
|
||||||
|
{
|
||||||
|
auto *client = ensureServerForLanguage(getLanguageId(buffer));
|
||||||
|
if (!client)
|
||||||
|
return;
|
||||||
|
const auto uri = getUri(buffer);
|
||||||
|
int version = static_cast<int>(buffer->Version());
|
||||||
|
|
||||||
|
std::vector<TextDocumentContentChangeEvent> changes;
|
||||||
|
if (auto *tracker = buffer->GetChangeTracker()) {
|
||||||
|
changes = tracker->getChanges();
|
||||||
|
tracker->clearChanges();
|
||||||
|
version = tracker->getVersion();
|
||||||
|
} else {
|
||||||
|
// Fallback: full document change
|
||||||
|
TextDocumentContentChangeEvent ev;
|
||||||
|
ev.range.reset();
|
||||||
|
ev.text = buffer->FullText();
|
||||||
|
changes.push_back(std::move(ev));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option A: convert ranges from UTF-8 (editor coords) -> UTF-16 (LSP wire)
|
||||||
|
std::vector<TextDocumentContentChangeEvent> changes16;
|
||||||
|
changes16.reserve(changes.size());
|
||||||
|
// LineProvider that serves lines from this buffer by URI
|
||||||
|
Buffer *bufForUri = buffer; // changes are for this buffer
|
||||||
|
auto provider = [bufForUri](const std::string &/*u*/, int line) -> std::string_view {
|
||||||
|
if (!bufForUri)
|
||||||
|
return std::string_view();
|
||||||
|
const auto &rows = bufForUri->Rows();
|
||||||
|
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||||
|
return std::string_view();
|
||||||
|
// Materialize one line into a thread_local scratch; return view
|
||||||
|
thread_local std::string scratch;
|
||||||
|
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||||
|
return std::string_view(scratch);
|
||||||
|
};
|
||||||
|
for (const auto &ch: changes) {
|
||||||
|
TextDocumentContentChangeEvent out = ch;
|
||||||
|
if (ch.range.has_value()) {
|
||||||
|
Range r16 = toUtf16(uri, *ch.range, provider);
|
||||||
|
if (debug_) {
|
||||||
|
lsp_debug_file("didChange range convert: L%d C%d-%d -> L%d C%d-%d",
|
||||||
|
ch.range->start.line, ch.range->start.character,
|
||||||
|
ch.range->end.character,
|
||||||
|
r16.start.line, r16.start.character, r16.end.character);
|
||||||
|
}
|
||||||
|
out.range = r16;
|
||||||
|
}
|
||||||
|
changes16.push_back(std::move(out));
|
||||||
|
}
|
||||||
|
client->didChange(uri, version, changes16);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspManager::onBufferClosed(Buffer *buffer)
|
||||||
|
{
|
||||||
|
auto *client = ensureServerForLanguage(getLanguageId(buffer));
|
||||||
|
if (!client)
|
||||||
|
return;
|
||||||
|
client->didClose(getUri(buffer));
|
||||||
|
// Clear diagnostics for this file
|
||||||
|
diagnosticStore_.clear(getUri(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspManager::onBufferSaved(Buffer *buffer)
|
||||||
|
{
|
||||||
|
auto *client = ensureServerForLanguage(getLanguageId(buffer));
|
||||||
|
if (!client)
|
||||||
|
return;
|
||||||
|
client->didSave(getUri(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspManager::requestCompletion(Buffer *buffer, Position pos, CompletionCallback callback)
|
||||||
|
{
|
||||||
|
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
|
||||||
|
const auto uri = getUri(buffer);
|
||||||
|
// Convert position to UTF-16 using Option A provider
|
||||||
|
auto provider = [buffer](const std::string &/*u*/, int line) -> std::string_view {
|
||||||
|
if (!buffer)
|
||||||
|
return std::string_view();
|
||||||
|
const auto &rows = buffer->Rows();
|
||||||
|
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||||
|
return std::string_view();
|
||||||
|
thread_local std::string scratch;
|
||||||
|
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||||
|
return std::string_view(scratch);
|
||||||
|
};
|
||||||
|
Position p16 = toUtf16(uri, pos, provider);
|
||||||
|
if (debug_) {
|
||||||
|
lsp_debug_file("completion pos convert: L%d C%d -> L%d C%d", pos.line, pos.character, p16.line,
|
||||||
|
p16.character);
|
||||||
|
}
|
||||||
|
client->completion(uri, p16, std::move(callback));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspManager::requestHover(Buffer *buffer, Position pos, HoverCallback callback)
|
||||||
|
{
|
||||||
|
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
|
||||||
|
const auto uri = getUri(buffer);
|
||||||
|
auto provider = [buffer](const std::string &/*u*/, int line) -> std::string_view {
|
||||||
|
if (!buffer)
|
||||||
|
return std::string_view();
|
||||||
|
const auto &rows = buffer->Rows();
|
||||||
|
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||||
|
return std::string_view();
|
||||||
|
thread_local std::string scratch;
|
||||||
|
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||||
|
return std::string_view(scratch);
|
||||||
|
};
|
||||||
|
Position p16 = toUtf16(uri, pos, provider);
|
||||||
|
if (debug_) {
|
||||||
|
lsp_debug_file("hover pos convert: L%d C%d -> L%d C%d", pos.line, pos.character, p16.line,
|
||||||
|
p16.character);
|
||||||
|
}
|
||||||
|
// Wrap the callback to convert any returned range from UTF-16 (wire) -> UTF-8 (editor)
|
||||||
|
HoverCallback wrapped = [this, uri, provider, cb = std::move(callback)](const HoverResult &res16,
|
||||||
|
const std::string &err) {
|
||||||
|
if (!cb)
|
||||||
|
return;
|
||||||
|
if (!res16.range.has_value()) {
|
||||||
|
cb(res16, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HoverResult res8 = res16;
|
||||||
|
res8.range = toUtf8(uri, *res16.range, provider);
|
||||||
|
if (debug_) {
|
||||||
|
const auto &r16 = *res16.range;
|
||||||
|
const auto &r8 = *res8.range;
|
||||||
|
lsp_debug_file("hover range convert: L%d %d-%d -> L%d %d-%d",
|
||||||
|
r16.start.line, r16.start.character, r16.end.character,
|
||||||
|
r8.start.line, r8.start.character, r8.end.character);
|
||||||
|
}
|
||||||
|
cb(res8, err);
|
||||||
|
};
|
||||||
|
client->hover(uri, p16, std::move(wrapped));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspManager::requestDefinition(Buffer *buffer, Position pos, LocationCallback callback)
|
||||||
|
{
|
||||||
|
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
|
||||||
|
const auto uri = getUri(buffer);
|
||||||
|
auto provider = [buffer](const std::string &/*u*/, int line) -> std::string_view {
|
||||||
|
if (!buffer)
|
||||||
|
return std::string_view();
|
||||||
|
const auto &rows = buffer->Rows();
|
||||||
|
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||||
|
return std::string_view();
|
||||||
|
thread_local std::string scratch;
|
||||||
|
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||||
|
return std::string_view(scratch);
|
||||||
|
};
|
||||||
|
Position p16 = toUtf16(uri, pos, provider);
|
||||||
|
if (debug_) {
|
||||||
|
lsp_debug_file("definition pos convert: L%d C%d -> L%d C%d", pos.line, pos.character, p16.line,
|
||||||
|
p16.character);
|
||||||
|
}
|
||||||
|
// Wrap callback to convert Location ranges from UTF-16 (wire) -> UTF-8 (editor)
|
||||||
|
LocationCallback wrapped = [this, uri, provider, cb = std::move(callback)](
|
||||||
|
const std::vector<Location> &locs16,
|
||||||
|
const std::string &err) {
|
||||||
|
if (!cb)
|
||||||
|
return;
|
||||||
|
std::vector<Location> locs8;
|
||||||
|
locs8.reserve(locs16.size());
|
||||||
|
for (const auto &l: locs16) {
|
||||||
|
Location x = l;
|
||||||
|
x.range = toUtf8(uri, l.range, provider);
|
||||||
|
if (debug_) {
|
||||||
|
lsp_debug_file("definition range convert: L%d %d-%d -> L%d %d-%d",
|
||||||
|
l.range.start.line, l.range.start.character,
|
||||||
|
l.range.end.character,
|
||||||
|
x.range.start.line, x.range.start.character,
|
||||||
|
x.range.end.character);
|
||||||
|
}
|
||||||
|
locs8.push_back(std::move(x));
|
||||||
|
}
|
||||||
|
cb(locs8, err);
|
||||||
|
};
|
||||||
|
client->definition(uri, p16, std::move(wrapped));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspManager::handleDiagnostics(const std::string &uri, const std::vector<Diagnostic> &diagnostics)
|
||||||
|
{
|
||||||
|
// Convert incoming ranges from UTF-16 (wire) -> UTF-8 (editor)
|
||||||
|
std::vector<Diagnostic> conv = diagnostics;
|
||||||
|
Buffer *buf = findBufferByUri(uri);
|
||||||
|
auto provider = [buf](const std::string &/*u*/, int line) -> std::string_view {
|
||||||
|
if (!buf)
|
||||||
|
return std::string_view();
|
||||||
|
const auto &rows = buf->Rows();
|
||||||
|
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||||
|
return std::string_view();
|
||||||
|
thread_local std::string scratch;
|
||||||
|
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||||
|
return std::string_view(scratch);
|
||||||
|
};
|
||||||
|
for (auto &d: conv) {
|
||||||
|
Range r8 = toUtf8(uri, d.range, provider);
|
||||||
|
if (debug_) {
|
||||||
|
lsp_debug_file("diagnostic range convert: L%d C%d-%d -> L%d C%d-%d",
|
||||||
|
d.range.start.line, d.range.start.character, d.range.end.character,
|
||||||
|
r8.start.line, r8.start.character, r8.end.character);
|
||||||
|
}
|
||||||
|
d.range = r8;
|
||||||
|
}
|
||||||
|
diagnosticStore_.setDiagnostics(uri, conv);
|
||||||
|
if (display_) {
|
||||||
|
display_->updateDiagnostics(uri, conv);
|
||||||
|
display_->updateStatusBar(diagnosticStore_.getErrorCount(uri), diagnosticStore_.getWarningCount(uri));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
LspManager::toggleAutostart(const std::string &languageId)
|
||||||
|
{
|
||||||
|
auto it = serverConfigs_.find(languageId);
|
||||||
|
if (it == serverConfigs_.end())
|
||||||
|
return false;
|
||||||
|
it->second.autostart = !it->second.autostart;
|
||||||
|
return it->second.autostart;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<std::string>
|
||||||
|
LspManager::configuredLanguages() const
|
||||||
|
{
|
||||||
|
std::vector<std::string> out;
|
||||||
|
out.reserve(serverConfigs_.size());
|
||||||
|
for (const auto &kv: serverConfigs_)
|
||||||
|
out.push_back(kv.first);
|
||||||
|
std::sort(out.begin(), out.end());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<std::string>
|
||||||
|
LspManager::runningLanguages() const
|
||||||
|
{
|
||||||
|
std::vector<std::string> out;
|
||||||
|
for (const auto &kv: servers_) {
|
||||||
|
if (kv.second && kv.second->isRunning())
|
||||||
|
out.push_back(kv.first);
|
||||||
|
}
|
||||||
|
std::sort(out.begin(), out.end());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
LspManager::getLanguageId(Buffer *buffer)
|
||||||
|
{
|
||||||
|
// Prefer explicit filetype if set
|
||||||
|
const auto &ft = buffer->Filetype();
|
||||||
|
if (!ft.empty())
|
||||||
|
return ft;
|
||||||
|
// Otherwise map extension
|
||||||
|
fs::path p(buffer->Filename());
|
||||||
|
return extToLanguageId(p.extension().string());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
LspManager::getUri(Buffer *buffer)
|
||||||
|
{
|
||||||
|
const auto &path = buffer->Filename();
|
||||||
|
if (path.empty()) {
|
||||||
|
// Untitled buffer: use a pseudo-URI
|
||||||
|
return std::string("untitled:") + std::to_string(reinterpret_cast<std::uintptr_t>(buffer));
|
||||||
|
}
|
||||||
|
fs::path p(path);
|
||||||
|
p = fs::weakly_canonical(p);
|
||||||
|
#ifdef _WIN32
|
||||||
|
// rudimentary file URI; future: robust encoding
|
||||||
|
return std::string("file:/") + p.string();
|
||||||
|
#else
|
||||||
|
return std::string("file://") + p.string();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Resolve a Buffer* by matching constructed file URI
|
||||||
|
Buffer *
|
||||||
|
LspManager::findBufferByUri(const std::string &uri)
|
||||||
|
{
|
||||||
|
if (!editor_)
|
||||||
|
return nullptr;
|
||||||
|
// Compare against getUri for each buffer
|
||||||
|
auto &bufs = editor_->Buffers();
|
||||||
|
for (auto &b: bufs) {
|
||||||
|
if (getUri(&b) == uri)
|
||||||
|
return &b;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
LspManager::extToLanguageId(const std::string &ext)
|
||||||
|
{
|
||||||
|
std::string e = ext;
|
||||||
|
if (!e.empty() && e[0] == '.')
|
||||||
|
e.erase(0, 1);
|
||||||
|
std::string lower;
|
||||||
|
lower.resize(e.size());
|
||||||
|
std::transform(e.begin(), e.end(), lower.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::tolower(c));
|
||||||
|
});
|
||||||
|
if (lower == "rs")
|
||||||
|
return "rust";
|
||||||
|
if (lower == "c" || lower == "cc" || lower == "cpp" || lower == "h" || lower == "hpp")
|
||||||
|
return "cpp";
|
||||||
|
if (lower == "go")
|
||||||
|
return "go";
|
||||||
|
if (lower == "py")
|
||||||
|
return "python";
|
||||||
|
if (lower == "js")
|
||||||
|
return "javascript";
|
||||||
|
if (lower == "ts")
|
||||||
|
return "typescript";
|
||||||
|
if (lower == "json")
|
||||||
|
return "json";
|
||||||
|
if (lower == "sh" || lower == "bash" || lower == "zsh")
|
||||||
|
return "shell";
|
||||||
|
if (lower == "md")
|
||||||
|
return "markdown";
|
||||||
|
return lower; // best-effort
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LspClient *
|
||||||
|
LspManager::ensureServerForLanguage(const std::string &languageId)
|
||||||
|
{
|
||||||
|
auto it = servers_.find(languageId);
|
||||||
|
if (it != servers_.end() && it->second && it->second->isRunning()) {
|
||||||
|
return it->second.get();
|
||||||
|
}
|
||||||
|
// Attempt to start from config if present
|
||||||
|
auto cfg = serverConfigs_.find(languageId);
|
||||||
|
if (cfg == serverConfigs_.end())
|
||||||
|
return nullptr;
|
||||||
|
auto client = std::make_unique<LspProcessClient>(cfg->second.command, cfg->second.args);
|
||||||
|
client->setDiagnosticsHandler([this](const std::string &uri, const std::vector<Diagnostic> &diags) {
|
||||||
|
this->handleDiagnostics(uri, diags);
|
||||||
|
});
|
||||||
|
// No specific file context here; initialize with empty or current working dir
|
||||||
|
if (!client->initialize(""))
|
||||||
|
return nullptr;
|
||||||
|
auto *ret = client.get();
|
||||||
|
servers_[languageId] = std::move(client);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspManager::registerDefaultServers()
|
||||||
|
{
|
||||||
|
// Import defaults and register by inferred languageId from file patterns
|
||||||
|
for (const auto &cfg: GetDefaultServerConfigs()) {
|
||||||
|
if (cfg.filePatterns.empty()) {
|
||||||
|
// If no patterns, we can't infer; skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const auto &pat: cfg.filePatterns) {
|
||||||
|
const auto lang = patternToLanguageId(pat);
|
||||||
|
if (lang.empty())
|
||||||
|
continue;
|
||||||
|
// Don't overwrite if user already registered a server for this lang
|
||||||
|
if (serverConfigs_.find(lang) == serverConfigs_.end()) {
|
||||||
|
serverConfigs_.emplace(lang, cfg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
LspManager::patternToLanguageId(const std::string &pattern)
|
||||||
|
{
|
||||||
|
// Expect patterns like "*.rs", "*.cpp" etc. Extract extension and reuse extToLanguageId
|
||||||
|
// Find last '.' in the pattern and take substring after it, stripping any trailing wildcards
|
||||||
|
std::string ext;
|
||||||
|
// Common case: starts with *.
|
||||||
|
auto pos = pattern.rfind('.');
|
||||||
|
if (pos != std::string::npos && pos + 1 < pattern.size()) {
|
||||||
|
ext = pattern.substr(pos + 1);
|
||||||
|
// Remove any trailing wildcard characters
|
||||||
|
while (!ext.empty() && (ext.back() == '*' || ext.back() == '?')) {
|
||||||
|
ext.pop_back();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No dot; try to treat whole pattern as extension after trimming leading '*'
|
||||||
|
ext = pattern;
|
||||||
|
while (!ext.empty() && (ext.front() == '*' || ext.front() == '.')) {
|
||||||
|
ext.erase(ext.begin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ext.empty())
|
||||||
|
return {};
|
||||||
|
return extToLanguageId(ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Detect workspace root by walking up from filePath looking for any of the
|
||||||
|
// configured rootPatterns (simple filenames). Supports comma/semicolon-separated
|
||||||
|
// patterns in cfg.rootPatterns.
|
||||||
|
std::string
|
||||||
|
LspManager::detectWorkspaceRoot(const std::string &filePath, const LspServerConfig &cfg)
|
||||||
|
{
|
||||||
|
if (filePath.empty())
|
||||||
|
return {};
|
||||||
|
fs::path start(filePath);
|
||||||
|
fs::path dir = start.has_parent_path() ? start.parent_path() : start;
|
||||||
|
|
||||||
|
// Build cache key
|
||||||
|
const std::string cacheKey = (dir.string() + "|" + cfg.rootPatterns);
|
||||||
|
auto it = rootCache_.find(cacheKey);
|
||||||
|
if (it != rootCache_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split patterns by ',', ';', or ':'
|
||||||
|
std::vector<std::string> pats;
|
||||||
|
{
|
||||||
|
std::string acc;
|
||||||
|
for (char c: cfg.rootPatterns) {
|
||||||
|
if (c == ',' || c == ';' || c == ':') {
|
||||||
|
if (!acc.empty()) {
|
||||||
|
pats.push_back(acc);
|
||||||
|
acc.clear();
|
||||||
|
}
|
||||||
|
} else if (!std::isspace(static_cast<unsigned char>(c))) {
|
||||||
|
acc.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!acc.empty())
|
||||||
|
pats.push_back(acc);
|
||||||
|
}
|
||||||
|
// If no patterns defined, cache empty and return {}
|
||||||
|
if (pats.empty()) {
|
||||||
|
rootCache_[cacheKey] = {};
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::path cur = dir;
|
||||||
|
while (true) {
|
||||||
|
// Check each pattern in this directory
|
||||||
|
for (const auto &pat: pats) {
|
||||||
|
if (pat.empty())
|
||||||
|
continue;
|
||||||
|
fs::path candidate = cur / pat;
|
||||||
|
std::error_code ec;
|
||||||
|
bool exists = fs::exists(candidate, ec);
|
||||||
|
if (!ec && exists) {
|
||||||
|
rootCache_[cacheKey] = cur.string();
|
||||||
|
return rootCache_[cacheKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cur.has_parent_path()) {
|
||||||
|
fs::path parent = cur.parent_path();
|
||||||
|
if (parent == cur)
|
||||||
|
break; // reached root guard
|
||||||
|
cur = parent;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootCache_[cacheKey] = {};
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
} // namespace kte::lsp
|
||||||
108
lsp/LspManager.h
Normal file
108
lsp/LspManager.h
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* LspManager.h - central coordination of LSP servers and diagnostics
|
||||||
|
*/
|
||||||
|
#ifndef KTE_LSP_MANAGER_H
|
||||||
|
#define KTE_LSP_MANAGER_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
class Buffer; // fwd
|
||||||
|
class Editor; // fwd
|
||||||
|
|
||||||
|
#include "DiagnosticDisplay.h"
|
||||||
|
#include "DiagnosticStore.h"
|
||||||
|
#include "LspClient.h"
|
||||||
|
#include "LspServerConfig.h"
|
||||||
|
#include "UtfCodec.h"
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Manual lifecycle controls
|
||||||
|
bool startServerForLanguage(const std::string &languageId, const std::string &rootPath = std::string());
|
||||||
|
|
||||||
|
bool restartServer(const std::string &languageId, const std::string &rootPath = std::string());
|
||||||
|
|
||||||
|
// Document sync (to be called by editor/buffer events)
|
||||||
|
void onBufferOpened(Buffer *buffer);
|
||||||
|
|
||||||
|
void onBufferChanged(Buffer *buffer);
|
||||||
|
|
||||||
|
void onBufferClosed(Buffer *buffer);
|
||||||
|
|
||||||
|
void onBufferSaved(Buffer *buffer);
|
||||||
|
|
||||||
|
// Feature requests (stubs)
|
||||||
|
void requestCompletion(Buffer *buffer, Position pos, CompletionCallback callback);
|
||||||
|
|
||||||
|
void requestHover(Buffer *buffer, Position pos, HoverCallback callback);
|
||||||
|
|
||||||
|
void requestDefinition(Buffer *buffer, Position pos, LocationCallback callback);
|
||||||
|
|
||||||
|
// Diagnostics (public so LspClient impls can forward results here later)
|
||||||
|
void handleDiagnostics(const std::string &uri, const std::vector<Diagnostic> &diagnostics);
|
||||||
|
|
||||||
|
|
||||||
|
void setDebugLogging(bool enabled)
|
||||||
|
{
|
||||||
|
debug_ = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Configuration utilities
|
||||||
|
bool toggleAutostart(const std::string &languageId);
|
||||||
|
|
||||||
|
std::vector<std::string> configuredLanguages() const;
|
||||||
|
|
||||||
|
std::vector<std::string> runningLanguages() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[maybe_unused]] Editor *editor_{}; // non-owning
|
||||||
|
DiagnosticDisplay *display_{}; // non-owning
|
||||||
|
DiagnosticStore diagnosticStore_{};
|
||||||
|
|
||||||
|
// Key: languageId → client
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<LspClient> > servers_;
|
||||||
|
std::unordered_map<std::string, LspServerConfig> serverConfigs_;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
static std::string getLanguageId(Buffer *buffer);
|
||||||
|
|
||||||
|
static std::string getUri(Buffer *buffer);
|
||||||
|
|
||||||
|
static std::string extToLanguageId(const std::string &ext);
|
||||||
|
|
||||||
|
LspClient *ensureServerForLanguage(const std::string &languageId);
|
||||||
|
|
||||||
|
bool debug_ = false;
|
||||||
|
|
||||||
|
// Configuration helpers
|
||||||
|
void registerDefaultServers();
|
||||||
|
|
||||||
|
static std::string patternToLanguageId(const std::string &pattern);
|
||||||
|
|
||||||
|
// Workspace root detection helpers/cache
|
||||||
|
std::string detectWorkspaceRoot(const std::string &filePath, const LspServerConfig &cfg);
|
||||||
|
|
||||||
|
// key = startDir + "|" + cfg.rootPatterns
|
||||||
|
std::unordered_map<std::string, std::string> rootCache_;
|
||||||
|
|
||||||
|
// Resolve a buffer by its file:// (or untitled:) URI
|
||||||
|
Buffer *findBufferByUri(const std::string &uri);
|
||||||
|
};
|
||||||
|
} // namespace kte::lsp
|
||||||
|
|
||||||
|
#endif // KTE_LSP_MANAGER_H
|
||||||
948
lsp/LspProcessClient.cc
Normal file
948
lsp/LspProcessClient.cc
Normal file
@@ -0,0 +1,948 @@
|
|||||||
|
/*
|
||||||
|
* LspProcessClient.cc - process-based LSP client (Phase 1 minimal)
|
||||||
|
*/
|
||||||
|
#include "LspProcessClient.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "json.h"
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
LspProcessClient::LspProcessClient(std::string serverCommand, std::vector<std::string> serverArgs)
|
||||||
|
: command_(std::move(serverCommand)), args_(std::move(serverArgs)), transport_(new JsonRpcTransport())
|
||||||
|
{
|
||||||
|
if (const char *dbg = std::getenv("KTE_LSP_DEBUG"); dbg && *dbg) {
|
||||||
|
debug_ = true;
|
||||||
|
}
|
||||||
|
if (const char *to = std::getenv("KTE_LSP_REQ_TIMEOUT_MS"); to && *to) {
|
||||||
|
char *end = nullptr;
|
||||||
|
long long v = std::strtoll(to, &end, 10);
|
||||||
|
if (end && *end == '\0' && v >= 0) {
|
||||||
|
requestTimeoutMs_ = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (const char *mp = std::getenv("KTE_LSP_MAX_PENDING"); mp && *mp) {
|
||||||
|
char *end = nullptr;
|
||||||
|
long long v = std::strtoll(mp, &end, 10);
|
||||||
|
if (end && *end == '\0' && v >= 0) {
|
||||||
|
maxPending_ = static_cast<size_t>(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
LspProcessClient::~LspProcessClient()
|
||||||
|
{
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
LspProcessClient::spawnServerProcess()
|
||||||
|
{
|
||||||
|
int toChild[2]; // parent writes toChild[1] -> child's stdin
|
||||||
|
int fromChild[2]; // child writes fromChild[1] -> parent's stdout reader
|
||||||
|
if (pipe(toChild) != 0) {
|
||||||
|
if (debug_)
|
||||||
|
std::fprintf(stderr, "[kte][lsp] pipe(toChild) failed: %s\n", std::strerror(errno));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pipe(fromChild) != 0) {
|
||||||
|
::close(toChild[0]);
|
||||||
|
::close(toChild[1]);
|
||||||
|
if (debug_)
|
||||||
|
std::fprintf(stderr, "[kte][lsp] pipe(fromChild) failed: %s\n", std::strerror(errno));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
// fork failed
|
||||||
|
::close(toChild[0]);
|
||||||
|
::close(toChild[1]);
|
||||||
|
::close(fromChild[0]);
|
||||||
|
::close(fromChild[1]);
|
||||||
|
if (debug_)
|
||||||
|
std::fprintf(stderr, "[kte][lsp] fork failed: %s\n", std::strerror(errno));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pid == 0) {
|
||||||
|
// Child: set up stdio
|
||||||
|
::dup2(toChild[0], STDIN_FILENO);
|
||||||
|
::dup2(fromChild[1], STDOUT_FILENO);
|
||||||
|
// Close extra fds
|
||||||
|
::close(toChild[0]);
|
||||||
|
::close(toChild[1]);
|
||||||
|
::close(fromChild[0]);
|
||||||
|
::close(fromChild[1]);
|
||||||
|
// Build argv
|
||||||
|
std::vector<char *> argv;
|
||||||
|
argv.push_back(const_cast<char *>(command_.c_str()));
|
||||||
|
for (auto &s: args_)
|
||||||
|
argv.push_back(const_cast<char *>(s.c_str()));
|
||||||
|
argv.push_back(nullptr);
|
||||||
|
// Exec
|
||||||
|
execvp(command_.c_str(), argv.data());
|
||||||
|
// If exec fails
|
||||||
|
// Note: in child; cannot easily log to parent. Attempt to write to stderr.
|
||||||
|
std::fprintf(stderr, "[kte][lsp] execvp failed for '%s': %s\n", command_.c_str(), std::strerror(errno));
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent: keep ends
|
||||||
|
childPid_ = pid;
|
||||||
|
outFd_ = toChild[1]; // write to child's stdin
|
||||||
|
inFd_ = fromChild[0]; // read from child's stdout
|
||||||
|
// Close the other ends we don't use
|
||||||
|
::close(toChild[0]);
|
||||||
|
::close(fromChild[1]);
|
||||||
|
// Set CLOEXEC on our fds
|
||||||
|
fcntl(outFd_, F_SETFD, FD_CLOEXEC);
|
||||||
|
fcntl(inFd_, F_SETFD, FD_CLOEXEC);
|
||||||
|
if (debug_) {
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << command_;
|
||||||
|
for (const auto &a: args_) {
|
||||||
|
oss << ' ' << a;
|
||||||
|
}
|
||||||
|
const char *pathEnv = std::getenv("PATH");
|
||||||
|
std::fprintf(stderr, "[kte][lsp] spawned pid=%d argv=[%s] inFd=%d outFd=%d PATH=%s\n",
|
||||||
|
static_cast<int>(childPid_), oss.str().c_str(), inFd_, outFd_,
|
||||||
|
pathEnv ? pathEnv : "<null>");
|
||||||
|
}
|
||||||
|
transport_->connect(inFd_, outFd_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::terminateProcess()
|
||||||
|
{
|
||||||
|
if (outFd_ >= 0) {
|
||||||
|
::close(outFd_);
|
||||||
|
outFd_ = -1;
|
||||||
|
}
|
||||||
|
if (inFd_ >= 0) {
|
||||||
|
::close(inFd_);
|
||||||
|
inFd_ = -1;
|
||||||
|
}
|
||||||
|
if (childPid_ > 0) {
|
||||||
|
// Try to wait non-blocking; if still running, send SIGTERM
|
||||||
|
int status = 0;
|
||||||
|
pid_t r = waitpid(childPid_, &status, WNOHANG);
|
||||||
|
if (r == 0) {
|
||||||
|
// still running
|
||||||
|
kill(childPid_, SIGTERM);
|
||||||
|
waitpid(childPid_, &status, 0);
|
||||||
|
}
|
||||||
|
childPid_ = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::sendInitialize(const std::string &rootPath)
|
||||||
|
{
|
||||||
|
int idNum = nextRequestIntId_++;
|
||||||
|
pendingInitializeId_ = std::to_string(idNum);
|
||||||
|
nlohmann::json j;
|
||||||
|
j["jsonrpc"] = "2.0";
|
||||||
|
j["id"] = idNum;
|
||||||
|
j["method"] = "initialize";
|
||||||
|
nlohmann::json params;
|
||||||
|
params["processId"] = static_cast<int>(getpid());
|
||||||
|
params["rootUri"] = toFileUri(rootPath);
|
||||||
|
// Minimal client capabilities for now
|
||||||
|
nlohmann::json caps;
|
||||||
|
caps["textDocument"]["synchronization"]["didSave"] = true;
|
||||||
|
params["capabilities"] = std::move(caps);
|
||||||
|
j["params"] = std::move(params);
|
||||||
|
transport_->send("initialize", j.dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
LspProcessClient::initialize(const std::string &rootPath)
|
||||||
|
{
|
||||||
|
if (running_)
|
||||||
|
return true;
|
||||||
|
if (debug_)
|
||||||
|
std::fprintf(stderr, "[kte][lsp] initialize: rootPath=%s\n", rootPath.c_str());
|
||||||
|
if (!spawnServerProcess())
|
||||||
|
return false;
|
||||||
|
running_ = true;
|
||||||
|
sendInitialize(rootPath);
|
||||||
|
startReader();
|
||||||
|
startTimeoutWatchdog();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::shutdown()
|
||||||
|
{
|
||||||
|
if (!running_)
|
||||||
|
return;
|
||||||
|
if (debug_)
|
||||||
|
std::fprintf(stderr, "[kte][lsp] shutdown\n");
|
||||||
|
// Send shutdown request then exit notification (best-effort)
|
||||||
|
int id = nextRequestIntId_++;
|
||||||
|
{
|
||||||
|
nlohmann::json j;
|
||||||
|
j["jsonrpc"] = "2.0";
|
||||||
|
j["id"] = id;
|
||||||
|
j["method"] = "shutdown";
|
||||||
|
transport_->send("shutdown", j.dump());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
nlohmann::json j;
|
||||||
|
j["jsonrpc"] = "2.0";
|
||||||
|
j["method"] = "exit";
|
||||||
|
transport_->send("exit", j.dump());
|
||||||
|
}
|
||||||
|
// Close pipes to unblock reader, then join thread, then ensure child is gone
|
||||||
|
terminateProcess();
|
||||||
|
stopReader();
|
||||||
|
stopTimeoutWatchdog();
|
||||||
|
// Clear any pending callbacks
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||||
|
pending_.clear();
|
||||||
|
pendingOrder_.clear();
|
||||||
|
}
|
||||||
|
running_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::didOpen(const std::string &uri, const std::string &languageId,
|
||||||
|
int version, const std::string &text)
|
||||||
|
{
|
||||||
|
if (!running_)
|
||||||
|
return;
|
||||||
|
if (debug_)
|
||||||
|
std::fprintf(stderr, "[kte][lsp] -> didOpen uri=%s lang=%s version=%d bytes=%zu\n",
|
||||||
|
uri.c_str(), languageId.c_str(), version, text.size());
|
||||||
|
nlohmann::json j;
|
||||||
|
j["jsonrpc"] = "2.0";
|
||||||
|
j["method"] = "textDocument/didOpen";
|
||||||
|
j["params"]["textDocument"]["uri"] = uri;
|
||||||
|
j["params"]["textDocument"]["languageId"] = languageId;
|
||||||
|
j["params"]["textDocument"]["version"] = version;
|
||||||
|
j["params"]["textDocument"]["text"] = text;
|
||||||
|
transport_->send("textDocument/didOpen", j.dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::didChange(const std::string &uri, int version,
|
||||||
|
const std::vector<TextDocumentContentChangeEvent> &changes)
|
||||||
|
{
|
||||||
|
if (!running_)
|
||||||
|
return;
|
||||||
|
if (debug_)
|
||||||
|
std::fprintf(stderr, "[kte][lsp] -> didChange uri=%s version=%d changes=%zu\n",
|
||||||
|
uri.c_str(), version, changes.size());
|
||||||
|
// Phase 1: send full or ranged changes using proper JSON construction
|
||||||
|
nlohmann::json j;
|
||||||
|
j["jsonrpc"] = "2.0";
|
||||||
|
j["method"] = "textDocument/didChange";
|
||||||
|
j["params"]["textDocument"]["uri"] = uri;
|
||||||
|
j["params"]["textDocument"]["version"] = version;
|
||||||
|
auto &arr = j["params"]["contentChanges"];
|
||||||
|
arr = nlohmann::json::array();
|
||||||
|
for (const auto &ch: changes) {
|
||||||
|
nlohmann::json c;
|
||||||
|
if (ch.range.has_value()) {
|
||||||
|
c["range"]["start"]["line"] = ch.range->start.line;
|
||||||
|
c["range"]["start"]["character"] = ch.range->start.character;
|
||||||
|
c["range"]["end"]["line"] = ch.range->end.line;
|
||||||
|
c["range"]["end"]["character"] = ch.range->end.character;
|
||||||
|
}
|
||||||
|
c["text"] = ch.text;
|
||||||
|
arr.push_back(std::move(c));
|
||||||
|
}
|
||||||
|
transport_->send("textDocument/didChange", j.dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::didClose(const std::string &uri)
|
||||||
|
{
|
||||||
|
if (!running_)
|
||||||
|
return;
|
||||||
|
if (debug_)
|
||||||
|
std::fprintf(stderr, "[kte][lsp] -> didClose uri=%s\n", uri.c_str());
|
||||||
|
nlohmann::json j;
|
||||||
|
j["jsonrpc"] = "2.0";
|
||||||
|
j["method"] = "textDocument/didClose";
|
||||||
|
j["params"]["textDocument"]["uri"] = uri;
|
||||||
|
transport_->send("textDocument/didClose", j.dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::didSave(const std::string &uri)
|
||||||
|
{
|
||||||
|
if (!running_)
|
||||||
|
return;
|
||||||
|
if (debug_)
|
||||||
|
std::fprintf(stderr, "[kte][lsp] -> didSave uri=%s\n", uri.c_str());
|
||||||
|
nlohmann::json j;
|
||||||
|
j["jsonrpc"] = "2.0";
|
||||||
|
j["method"] = "textDocument/didSave";
|
||||||
|
j["params"]["textDocument"]["uri"] = uri;
|
||||||
|
transport_->send("textDocument/didSave", j.dump());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::startReader()
|
||||||
|
{
|
||||||
|
stopReader_ = false;
|
||||||
|
reader_ = std::thread([this] {
|
||||||
|
this->readerLoop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::stopReader()
|
||||||
|
{
|
||||||
|
stopReader_ = true;
|
||||||
|
if (reader_.joinable()) {
|
||||||
|
// Waking up read() by closing inFd_ is handled in terminateProcess(); ensure it’s closed first
|
||||||
|
// Here, best-effort join with small delay
|
||||||
|
reader_.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::readerLoop()
|
||||||
|
{
|
||||||
|
if (debug_)
|
||||||
|
std::fprintf(stderr, "[kte][lsp] readerLoop start\n");
|
||||||
|
while (!stopReader_) {
|
||||||
|
auto msg = transport_->read();
|
||||||
|
if (!msg.has_value()) {
|
||||||
|
// EOF or error
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
handleIncoming(msg->raw);
|
||||||
|
}
|
||||||
|
if (debug_)
|
||||||
|
std::fprintf(stderr, "[kte][lsp] readerLoop end\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::handleIncoming(const std::string &json)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
auto j = nlohmann::json::parse(json, nullptr, false);
|
||||||
|
if (j.is_discarded())
|
||||||
|
return; // malformed JSON
|
||||||
|
// Validate jsonrpc if present
|
||||||
|
if (auto itRpc = j.find("jsonrpc"); itRpc != j.end()) {
|
||||||
|
if (!itRpc->is_string() || *itRpc != "2.0")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto normalizeId = [](const nlohmann::json &idVal) -> std::string {
|
||||||
|
if (idVal.is_string())
|
||||||
|
return idVal.get<std::string>();
|
||||||
|
if (idVal.is_number_integer())
|
||||||
|
return std::to_string(idVal.get<long long>());
|
||||||
|
return std::string();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle responses (have id and no method) or server -> client requests (have id and method)
|
||||||
|
if (auto itId = j.find("id"); itId != j.end() && !itId->is_null()) {
|
||||||
|
const std::string respIdStr = normalizeId(*itId);
|
||||||
|
|
||||||
|
// If it's a request from server, it will also have a method
|
||||||
|
if (auto itMeth = j.find("method"); itMeth != j.end() && itMeth->is_string()) {
|
||||||
|
const std::string method = *itMeth;
|
||||||
|
if (method == "workspace/configuration") {
|
||||||
|
// Respond with default empty settings array matching requested items length
|
||||||
|
size_t n = 0;
|
||||||
|
if (auto itParams = j.find("params");
|
||||||
|
itParams != j.end() && itParams->is_object()) {
|
||||||
|
if (auto itItems = itParams->find("items");
|
||||||
|
itItems != itParams->end() && itItems->is_array()) {
|
||||||
|
n = itItems->size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nlohmann::json resp;
|
||||||
|
resp["jsonrpc"] = "2.0";
|
||||||
|
// echo id type: if original was string, send string; else number
|
||||||
|
if (itId->is_string())
|
||||||
|
resp["id"] = *itId;
|
||||||
|
else if (itId->is_number_integer())
|
||||||
|
resp["id"] = *itId;
|
||||||
|
nlohmann::json arr = nlohmann::json::array();
|
||||||
|
for (size_t i = 0; i < n; ++i)
|
||||||
|
arr.push_back(nlohmann::json::object());
|
||||||
|
resp["result"] = std::move(arr);
|
||||||
|
transport_->send("response", resp.dump());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method == "window/showMessageRequest") {
|
||||||
|
// Best-effort respond with null result (dismiss)
|
||||||
|
nlohmann::json resp;
|
||||||
|
resp["jsonrpc"] = "2.0";
|
||||||
|
if (itId->is_string())
|
||||||
|
resp["id"] = *itId;
|
||||||
|
else if (itId->is_number_integer())
|
||||||
|
resp["id"] = *itId;
|
||||||
|
resp["result"] = nullptr;
|
||||||
|
transport_->send("response", resp.dump());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Unknown server request: respond with MethodNotFound
|
||||||
|
nlohmann::json err;
|
||||||
|
err["code"] = -32601;
|
||||||
|
err["message"] = "Method not found";
|
||||||
|
nlohmann::json resp;
|
||||||
|
resp["jsonrpc"] = "2.0";
|
||||||
|
if (itId->is_string())
|
||||||
|
resp["id"] = *itId;
|
||||||
|
else if (itId->is_number_integer())
|
||||||
|
resp["id"] = *itId;
|
||||||
|
resp["error"] = std::move(err);
|
||||||
|
transport_->send("response", resp.dump());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize handshake special-case
|
||||||
|
if (!pendingInitializeId_.empty() && respIdStr == pendingInitializeId_) {
|
||||||
|
nlohmann::json init;
|
||||||
|
init["jsonrpc"] = "2.0";
|
||||||
|
init["method"] = "initialized";
|
||||||
|
init["params"] = nlohmann::json::object();
|
||||||
|
transport_->send("initialized", init.dump());
|
||||||
|
pendingInitializeId_.clear();
|
||||||
|
}
|
||||||
|
// Dispatcher lookup
|
||||||
|
std::function < void(const nlohmann::json &, const nlohmann::json *) > cb;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||||
|
auto it = pending_.find(respIdStr);
|
||||||
|
if (it != pending_.end()) {
|
||||||
|
cb = it->second.callback;
|
||||||
|
if (it->second.orderIt != pendingOrder_.end()) {
|
||||||
|
pendingOrder_.erase(it->second.orderIt);
|
||||||
|
}
|
||||||
|
pending_.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cb) {
|
||||||
|
const nlohmann::json *errPtr = nullptr;
|
||||||
|
const auto itErr = j.find("error");
|
||||||
|
if (itErr != j.end() && itErr->is_object())
|
||||||
|
errPtr = &(*itErr);
|
||||||
|
nlohmann::json result;
|
||||||
|
const auto itRes = j.find("result");
|
||||||
|
if (itRes != j.end())
|
||||||
|
result = *itRes; // may be null
|
||||||
|
cb(result, errPtr);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto itMethod = j.find("method");
|
||||||
|
if (itMethod == j.end() || !itMethod->is_string())
|
||||||
|
return;
|
||||||
|
const std::string method = *itMethod;
|
||||||
|
if (method == "window/logMessage") {
|
||||||
|
if (debug_) {
|
||||||
|
const auto itParams = j.find("params");
|
||||||
|
if (itParams != j.end()) {
|
||||||
|
const auto itMsg = itParams->find("message");
|
||||||
|
if (itMsg != itParams->end() && itMsg->is_string()) {
|
||||||
|
std::fprintf(stderr, "[kte][lsp] logMessage: %s\n",
|
||||||
|
itMsg->get_ref<const std::string &>().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method == "window/showMessage") {
|
||||||
|
const auto itParams = j.find("params");
|
||||||
|
if (debug_ &&itParams
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
!=
|
||||||
|
j.end() && itParams->is_object()
|
||||||
|
)
|
||||||
|
{
|
||||||
|
int typ = 0;
|
||||||
|
std::string msg;
|
||||||
|
if (auto itm = itParams->find("message"); itm != itParams->end() && itm->is_string())
|
||||||
|
msg = *itm;
|
||||||
|
if (auto ity = itParams->find("type");
|
||||||
|
ity != itParams->end() && ity->is_number_integer())
|
||||||
|
typ = *ity;
|
||||||
|
std::fprintf(stderr, "[kte][lsp] showMessage(type=%d): %s\n", typ, msg.c_str());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (method != "textDocument/publishDiagnostics") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto itParams = j.find("params");
|
||||||
|
if (itParams == j.end() || !itParams->is_object())
|
||||||
|
return;
|
||||||
|
const auto itUri = itParams->find("uri");
|
||||||
|
if (itUri == itParams->end() || !itUri->is_string())
|
||||||
|
return;
|
||||||
|
const std::string uri = *itUri;
|
||||||
|
std::vector<Diagnostic> diags;
|
||||||
|
const auto itDiag = itParams->find("diagnostics");
|
||||||
|
if (itDiag != itParams->end() && itDiag->is_array()) {
|
||||||
|
for (const auto &djson: *itDiag) {
|
||||||
|
if (!djson.is_object())
|
||||||
|
continue;
|
||||||
|
Diagnostic d;
|
||||||
|
// severity
|
||||||
|
int sev = 3;
|
||||||
|
if (auto itS = djson.find("severity"); itS != djson.end() && itS->is_number_integer()) {
|
||||||
|
sev = *itS;
|
||||||
|
}
|
||||||
|
switch (sev) {
|
||||||
|
case 1:
|
||||||
|
d.severity = DiagnosticSeverity::Error;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
d.severity = DiagnosticSeverity::Warning;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
d.severity = DiagnosticSeverity::Information;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
d.severity = DiagnosticSeverity::Hint;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
d.severity = DiagnosticSeverity::Information;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (auto itM = djson.find("message"); itM != djson.end() && itM->is_string()) {
|
||||||
|
d.message = *itM;
|
||||||
|
}
|
||||||
|
if (auto itR = djson.find("range"); itR != djson.end() && itR->is_object()) {
|
||||||
|
if (auto itStart = itR->find("start");
|
||||||
|
itStart != itR->end() && itStart->is_object()) {
|
||||||
|
if (auto itL = itStart->find("line");
|
||||||
|
itL != itStart->end() && itL->is_number_integer()) {
|
||||||
|
d.range.start.line = *itL;
|
||||||
|
}
|
||||||
|
if (auto itC = itStart->find("character");
|
||||||
|
itC != itStart->end() && itC->is_number_integer()) {
|
||||||
|
d.range.start.character = *itC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (auto itEnd = itR->find("end"); itEnd != itR->end() && itEnd->is_object()) {
|
||||||
|
if (auto itL = itEnd->find("line");
|
||||||
|
itL != itEnd->end() && itL->is_number_integer()) {
|
||||||
|
d.range.end.line = *itL;
|
||||||
|
}
|
||||||
|
if (auto itC = itEnd->find("character");
|
||||||
|
itC != itEnd->end() && itC->is_number_integer()) {
|
||||||
|
d.range.end.character = *itC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// optional code/source
|
||||||
|
if (auto itCode = djson.find("code"); itCode != djson.end()) {
|
||||||
|
if (itCode->is_string())
|
||||||
|
d.code = itCode->get<std::string>();
|
||||||
|
else if (itCode->is_number_integer())
|
||||||
|
d.code = std::to_string(itCode->get<int>());
|
||||||
|
}
|
||||||
|
if (auto itSrc = djson.find("source"); itSrc != djson.end() && itSrc->is_string()) {
|
||||||
|
d.source = itSrc->get<std::string>();
|
||||||
|
}
|
||||||
|
diags.push_back(std::move(d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (diagnosticsHandler_) {
|
||||||
|
diagnosticsHandler_(uri, diags);
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
// swallow parse errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
LspProcessClient::sendRequest(const std::string &method, const nlohmann::json ¶ms,
|
||||||
|
std::function<void(const nlohmann::json & result, const nlohmann::json * errorJson)> cb)
|
||||||
|
{
|
||||||
|
if (!running_)
|
||||||
|
return 0;
|
||||||
|
int id = nextRequestIntId_++;
|
||||||
|
nlohmann::json j;
|
||||||
|
j["jsonrpc"] = "2.0";
|
||||||
|
j["id"] = id;
|
||||||
|
j["method"] = method;
|
||||||
|
if (!params.is_null())
|
||||||
|
j["params"] = params;
|
||||||
|
if (debug_)
|
||||||
|
std::fprintf(stderr, "[kte][lsp] -> request method=%s id=%d\n", method.c_str(), id);
|
||||||
|
transport_->send(method, j.dump());
|
||||||
|
if (cb) {
|
||||||
|
std::function < void() > callDropped;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||||
|
if (maxPending_ > 0 && pending_.size() >= maxPending_) {
|
||||||
|
// Evict oldest
|
||||||
|
if (!pendingOrder_.empty()) {
|
||||||
|
std::string oldestId = pendingOrder_.front();
|
||||||
|
auto it = pending_.find(oldestId);
|
||||||
|
if (it != pending_.end()) {
|
||||||
|
auto cbOld = it->second.callback;
|
||||||
|
std::string methOld = it->second.method;
|
||||||
|
if (debug_) {
|
||||||
|
std::fprintf(
|
||||||
|
stderr,
|
||||||
|
"[kte][lsp] dropping oldest pending id=%s method=%s (cap=%zu)\n",
|
||||||
|
oldestId.c_str(), methOld.c_str(), maxPending_);
|
||||||
|
}
|
||||||
|
// Prepare drop callback to run outside lock
|
||||||
|
callDropped = [cbOld] {
|
||||||
|
if (cbOld) {
|
||||||
|
nlohmann::json err;
|
||||||
|
err["code"] = -32001;
|
||||||
|
err["message"] =
|
||||||
|
"Request dropped (max pending exceeded)";
|
||||||
|
cbOld(nlohmann::json(), &err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pending_.erase(it);
|
||||||
|
}
|
||||||
|
pendingOrder_.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingOrder_.push_back(std::to_string(id));
|
||||||
|
auto itOrder = pendingOrder_.end();
|
||||||
|
--itOrder;
|
||||||
|
PendingRequest pr;
|
||||||
|
pr.method = method;
|
||||||
|
pr.callback = std::move(cb);
|
||||||
|
if (requestTimeoutMs_ > 0) {
|
||||||
|
pr.deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(
|
||||||
|
requestTimeoutMs_);
|
||||||
|
}
|
||||||
|
pr.orderIt = itOrder;
|
||||||
|
pending_[std::to_string(id)] = std::move(pr);
|
||||||
|
}
|
||||||
|
if (callDropped)
|
||||||
|
callDropped();
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::completion(const std::string &uri, Position pos, CompletionCallback cb)
|
||||||
|
{
|
||||||
|
nlohmann::json params;
|
||||||
|
params["textDocument"]["uri"] = uri;
|
||||||
|
params["position"]["line"] = pos.line;
|
||||||
|
params["position"]["character"] = pos.character;
|
||||||
|
sendRequest("textDocument/completion", params,
|
||||||
|
[cb = std::move(cb)](const nlohmann::json &result, const nlohmann::json *error) {
|
||||||
|
CompletionList out{};
|
||||||
|
std::string err;
|
||||||
|
if (error) {
|
||||||
|
if (auto itMsg = error->find("message");
|
||||||
|
itMsg != error->end() && itMsg->is_string())
|
||||||
|
err = itMsg->get<std::string>();
|
||||||
|
else
|
||||||
|
err = "LSP error";
|
||||||
|
} else {
|
||||||
|
auto parseItem = [](const nlohmann::json &j) -> CompletionItem {
|
||||||
|
CompletionItem it{};
|
||||||
|
if (auto il = j.find("label"); il != j.end() && il->is_string())
|
||||||
|
it.label = il->get<std::string>();
|
||||||
|
if (auto idt = j.find("detail"); idt != j.end() && idt->is_string())
|
||||||
|
it.detail = idt->get<std::string>();
|
||||||
|
if (auto ins = j.find("insertText"); ins != j.end() && ins->is_string())
|
||||||
|
it.insertText = ins->get<std::string>();
|
||||||
|
return it;
|
||||||
|
};
|
||||||
|
if (result.is_array()) {
|
||||||
|
for (const auto &ji: result) {
|
||||||
|
if (ji.is_object())
|
||||||
|
out.items.push_back(parseItem(ji));
|
||||||
|
}
|
||||||
|
} else if (result.is_object()) {
|
||||||
|
if (auto ii = result.find("isIncomplete");
|
||||||
|
ii != result.end() && ii->is_boolean())
|
||||||
|
out.isIncomplete = ii->get<bool>();
|
||||||
|
if (auto itms = result.find("items");
|
||||||
|
itms != result.end() && itms->is_array()) {
|
||||||
|
for (const auto &ji: *itms) {
|
||||||
|
if (ji.is_object())
|
||||||
|
out.items.push_back(parseItem(ji));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cb)
|
||||||
|
cb(out, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::hover(const std::string &uri, Position pos, HoverCallback cb)
|
||||||
|
{
|
||||||
|
nlohmann::json params;
|
||||||
|
params["textDocument"]["uri"] = uri;
|
||||||
|
params["position"]["line"] = pos.line;
|
||||||
|
params["position"]["character"] = pos.character;
|
||||||
|
sendRequest("textDocument/hover", params,
|
||||||
|
[cb = std::move(cb)](const nlohmann::json &result, const nlohmann::json *error) {
|
||||||
|
HoverResult out{};
|
||||||
|
std::string err;
|
||||||
|
if (error) {
|
||||||
|
if (auto itMsg = error->find("message");
|
||||||
|
itMsg != error->end() && itMsg->is_string())
|
||||||
|
err = itMsg->get<std::string>();
|
||||||
|
else
|
||||||
|
err = "LSP error";
|
||||||
|
} else if (!result.is_null()) {
|
||||||
|
auto appendText = [&](const std::string &s) {
|
||||||
|
if (!out.contents.empty())
|
||||||
|
out.contents.push_back('\n');
|
||||||
|
out.contents += s;
|
||||||
|
};
|
||||||
|
if (result.is_object()) {
|
||||||
|
if (auto itC = result.find("contents"); itC != result.end()) {
|
||||||
|
if (itC->is_string()) {
|
||||||
|
appendText(itC->get<std::string>());
|
||||||
|
} else if (itC->is_object()) {
|
||||||
|
if (auto itV = itC->find("value");
|
||||||
|
itV != itC->end() && itV->is_string())
|
||||||
|
appendText(itV->get<std::string>());
|
||||||
|
} else if (itC->is_array()) {
|
||||||
|
for (const auto &el: *itC) {
|
||||||
|
if (el.is_string())
|
||||||
|
appendText(el.get<std::string>());
|
||||||
|
else if (el.is_object()) {
|
||||||
|
if (auto itV = el.find("value");
|
||||||
|
itV != el.end() && itV->is_string())
|
||||||
|
appendText(itV->get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (auto itR = result.find("range");
|
||||||
|
itR != result.end() && itR->is_object()) {
|
||||||
|
Range r{};
|
||||||
|
if (auto s = itR->find("start");
|
||||||
|
s != itR->end() && s->is_object()) {
|
||||||
|
if (auto il = s->find("line");
|
||||||
|
il != s->end() && il->is_number_integer())
|
||||||
|
r.start.line = *il;
|
||||||
|
if (auto ic = s->find("character");
|
||||||
|
ic != s->end() && ic->is_number_integer())
|
||||||
|
r.start.character = *ic;
|
||||||
|
}
|
||||||
|
if (auto e = itR->find("end"); e != itR->end() && e->is_object()) {
|
||||||
|
if (auto il = e->find("line");
|
||||||
|
il != e->end() && il->is_number_integer())
|
||||||
|
r.end.line = *il;
|
||||||
|
if (auto ic = e->find("character");
|
||||||
|
ic != e->end() && ic->is_number_integer())
|
||||||
|
r.end.character = *ic;
|
||||||
|
}
|
||||||
|
out.range = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cb)
|
||||||
|
cb(out, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::definition(const std::string &uri, Position pos, LocationCallback cb)
|
||||||
|
{
|
||||||
|
nlohmann::json params;
|
||||||
|
params["textDocument"]["uri"] = uri;
|
||||||
|
params["position"]["line"] = pos.line;
|
||||||
|
params["position"]["character"] = pos.character;
|
||||||
|
sendRequest("textDocument/definition", params,
|
||||||
|
[cb = std::move(cb)](const nlohmann::json &result, const nlohmann::json *error) {
|
||||||
|
std::vector<Location> out;
|
||||||
|
std::string err;
|
||||||
|
auto parseRange = [](const nlohmann::json &jr) -> Range {
|
||||||
|
Range r{};
|
||||||
|
if (!jr.is_object())
|
||||||
|
return r;
|
||||||
|
if (auto s = jr.find("start"); s != jr.end() && s->is_object()) {
|
||||||
|
if (auto il = s->find("line"); il != s->end() && il->is_number_integer())
|
||||||
|
r.start.line = *il;
|
||||||
|
if (auto ic = s->find("character");
|
||||||
|
ic != s->end() && ic->is_number_integer())
|
||||||
|
r.start.character = *ic;
|
||||||
|
}
|
||||||
|
if (auto e = jr.find("end"); e != jr.end() && e->is_object()) {
|
||||||
|
if (auto il = e->find("line"); il != e->end() && il->is_number_integer())
|
||||||
|
r.end.line = *il;
|
||||||
|
if (auto ic = e->find("character");
|
||||||
|
ic != e->end() && e->is_number_integer())
|
||||||
|
r.end.character = *ic;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
auto pushLocObj = [&](const nlohmann::json &jo) {
|
||||||
|
Location loc{};
|
||||||
|
if (auto iu = jo.find("uri"); iu != jo.end() && iu->is_string())
|
||||||
|
loc.uri = iu->get<std::string>();
|
||||||
|
if (auto ir = jo.find("range"); ir != jo.end())
|
||||||
|
loc.range = parseRange(*ir);
|
||||||
|
out.push_back(std::move(loc));
|
||||||
|
};
|
||||||
|
if (error) {
|
||||||
|
if (auto itMsg = error->find("message");
|
||||||
|
itMsg != error->end() && itMsg->is_string())
|
||||||
|
err = itMsg->get<std::string>();
|
||||||
|
else
|
||||||
|
err = "LSP error";
|
||||||
|
} else if (!result.is_null()) {
|
||||||
|
if (result.is_object()) {
|
||||||
|
if (result.contains("uri") && result.contains("range")) {
|
||||||
|
pushLocObj(result);
|
||||||
|
} else if (result.contains("targetUri")) {
|
||||||
|
Location loc{};
|
||||||
|
if (auto tu = result.find("targetUri");
|
||||||
|
tu != result.end() && tu->is_string())
|
||||||
|
loc.uri = tu->get<std::string>();
|
||||||
|
if (auto tr = result.find("targetRange"); tr != result.end())
|
||||||
|
loc.range = parseRange(*tr);
|
||||||
|
out.push_back(std::move(loc));
|
||||||
|
}
|
||||||
|
} else if (result.is_array()) {
|
||||||
|
for (const auto &el: result) {
|
||||||
|
if (el.is_object()) {
|
||||||
|
if (el.contains("uri")) {
|
||||||
|
pushLocObj(el);
|
||||||
|
} else if (el.contains("targetUri")) {
|
||||||
|
Location loc{};
|
||||||
|
if (auto tu = el.find("targetUri");
|
||||||
|
tu != el.end() && tu->is_string())
|
||||||
|
loc.uri = tu->get<std::string>();
|
||||||
|
if (auto tr = el.find("targetRange");
|
||||||
|
tr != el.end())
|
||||||
|
loc.range = parseRange(*tr);
|
||||||
|
out.push_back(std::move(loc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cb)
|
||||||
|
cb(out, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
LspProcessClient::isRunning() const
|
||||||
|
{
|
||||||
|
return running_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
LspProcessClient::getServerName() const
|
||||||
|
{
|
||||||
|
return command_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
LspProcessClient::toFileUri(const std::string &path)
|
||||||
|
{
|
||||||
|
if (path.empty())
|
||||||
|
return std::string();
|
||||||
|
#ifdef _WIN32
|
||||||
|
return std::string("file:/") + path;
|
||||||
|
#else
|
||||||
|
return std::string("file://") + path;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::startTimeoutWatchdog()
|
||||||
|
{
|
||||||
|
stopTimeout_ = false;
|
||||||
|
if (requestTimeoutMs_ <= 0)
|
||||||
|
return;
|
||||||
|
timeoutThread_ = std::thread([this] {
|
||||||
|
while (!stopTimeout_) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
struct Expired {
|
||||||
|
std::string id;
|
||||||
|
std::string method;
|
||||||
|
std::function<void(const nlohmann::json &, const nlohmann::json *)> cb;
|
||||||
|
};
|
||||||
|
std::vector<Expired> expired;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||||
|
for (auto it = pending_.begin(); it != pending_.end();) {
|
||||||
|
const auto &pr = it->second;
|
||||||
|
if (pr.deadline.time_since_epoch().count() != 0 && now >= pr.deadline) {
|
||||||
|
expired.push_back(Expired{it->first, pr.method, pr.callback});
|
||||||
|
if (pr.orderIt != pendingOrder_.end())
|
||||||
|
pendingOrder_.erase(pr.orderIt);
|
||||||
|
it = pending_.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (auto &kv: expired) {
|
||||||
|
if (debug_) {
|
||||||
|
std::fprintf(stderr, "[kte][lsp] request timeout id=%s method=%s\n",
|
||||||
|
kv.id.c_str(), kv.method.c_str());
|
||||||
|
}
|
||||||
|
if (kv.cb) {
|
||||||
|
nlohmann::json err;
|
||||||
|
err["code"] = -32000;
|
||||||
|
err["message"] = "Request timed out";
|
||||||
|
kv.cb(nlohmann::json(), &err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
LspProcessClient::stopTimeoutWatchdog()
|
||||||
|
{
|
||||||
|
stopTimeout_ = true;
|
||||||
|
if (timeoutThread_.joinable())
|
||||||
|
timeoutThread_.join();
|
||||||
|
}
|
||||||
|
} // namespace kte::lsp
|
||||||
189
lsp/LspProcessClient.h
Normal file
189
lsp/LspProcessClient.h
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* LspProcessClient.h - process-based LSP client (initial stub)
|
||||||
|
*/
|
||||||
|
#ifndef KTE_LSP_PROCESS_CLIENT_H
|
||||||
|
#define KTE_LSP_PROCESS_CLIENT_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <thread>
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <chrono>
|
||||||
|
#include <list>
|
||||||
|
|
||||||
|
#include "json.h"
|
||||||
|
|
||||||
|
#include "LspClient.h"
|
||||||
|
#include "JsonRpcTransport.h"
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
class LspProcessClient : public LspClient {
|
||||||
|
public:
|
||||||
|
LspProcessClient(std::string serverCommand, std::vector<std::string> serverArgs);
|
||||||
|
|
||||||
|
~LspProcessClient() override;
|
||||||
|
|
||||||
|
bool initialize(const std::string &rootPath) override;
|
||||||
|
|
||||||
|
void shutdown() override;
|
||||||
|
|
||||||
|
void didOpen(const std::string &uri, const std::string &languageId,
|
||||||
|
int version, const std::string &text) override;
|
||||||
|
|
||||||
|
void didChange(const std::string &uri, int version,
|
||||||
|
const std::vector<TextDocumentContentChangeEvent> &changes) override;
|
||||||
|
|
||||||
|
void didClose(const std::string &uri) override;
|
||||||
|
|
||||||
|
void didSave(const std::string &uri) override;
|
||||||
|
|
||||||
|
// Language Features (wire-up via dispatcher; minimal callbacks for now)
|
||||||
|
void completion(const std::string &uri, Position pos,
|
||||||
|
CompletionCallback cb) override;
|
||||||
|
|
||||||
|
void hover(const std::string &uri, Position pos,
|
||||||
|
HoverCallback cb) override;
|
||||||
|
|
||||||
|
void definition(const std::string &uri, Position pos,
|
||||||
|
LocationCallback cb) override;
|
||||||
|
|
||||||
|
bool isRunning() const override;
|
||||||
|
|
||||||
|
std::string getServerName() const override;
|
||||||
|
|
||||||
|
|
||||||
|
void setDiagnosticsHandler(DiagnosticsHandler h) override
|
||||||
|
{
|
||||||
|
diagnosticsHandler_ = std::move(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string command_;
|
||||||
|
std::vector<std::string> args_;
|
||||||
|
std::unique_ptr<JsonRpcTransport> transport_;
|
||||||
|
bool running_ = false;
|
||||||
|
bool debug_ = false;
|
||||||
|
int inFd_ = -1; // read from server (server stdout)
|
||||||
|
int outFd_ = -1; // write to server (server stdin)
|
||||||
|
pid_t childPid_ = -1;
|
||||||
|
int nextRequestIntId_ = 1;
|
||||||
|
std::string pendingInitializeId_{}; // echo exactly as sent (string form)
|
||||||
|
|
||||||
|
// Incoming processing
|
||||||
|
std::thread reader_;
|
||||||
|
std::atomic<bool> stopReader_{false};
|
||||||
|
DiagnosticsHandler diagnosticsHandler_{};
|
||||||
|
|
||||||
|
// Simple request dispatcher: map request id -> callback
|
||||||
|
struct PendingRequest {
|
||||||
|
std::string method;
|
||||||
|
// If error is present, errorJson points to it; otherwise nullptr
|
||||||
|
std::function<void(const nlohmann::json & result, const nlohmann::json * errorJson)> callback;
|
||||||
|
// Optional timeout
|
||||||
|
std::chrono::steady_clock::time_point deadline{}; // epoch if no timeout
|
||||||
|
// Order tracking for LRU eviction
|
||||||
|
std::list<std::string>::iterator orderIt{};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unordered_map<std::string, PendingRequest> pending_;
|
||||||
|
// Maintain insertion order (oldest at front)
|
||||||
|
std::list<std::string> pendingOrder_;
|
||||||
|
std::mutex pendingMutex_;
|
||||||
|
|
||||||
|
// Timeout/watchdog for pending requests
|
||||||
|
std::thread timeoutThread_;
|
||||||
|
std::atomic<bool> stopTimeout_{false};
|
||||||
|
int64_t requestTimeoutMs_ = 0; // 0 = disabled
|
||||||
|
size_t maxPending_ = 0; // 0 = unlimited
|
||||||
|
|
||||||
|
bool spawnServerProcess();
|
||||||
|
|
||||||
|
void terminateProcess();
|
||||||
|
|
||||||
|
static std::string toFileUri(const std::string &path);
|
||||||
|
|
||||||
|
void sendInitialize(const std::string &rootPath);
|
||||||
|
|
||||||
|
void startReader();
|
||||||
|
|
||||||
|
void stopReader();
|
||||||
|
|
||||||
|
void readerLoop();
|
||||||
|
|
||||||
|
void handleIncoming(const std::string &json);
|
||||||
|
|
||||||
|
// Helper to send a request with params and register a response callback
|
||||||
|
int sendRequest(const std::string &method, const nlohmann::json ¶ms,
|
||||||
|
std::function<void(const nlohmann::json & result, const nlohmann::json * errorJson)> cb);
|
||||||
|
|
||||||
|
// Start/stop timeout thread
|
||||||
|
void startTimeoutWatchdog();
|
||||||
|
|
||||||
|
void stopTimeoutWatchdog();
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Test hook: inject a raw JSON message as if received from server
|
||||||
|
void debugInjectMessageForTest(const std::string &raw)
|
||||||
|
{
|
||||||
|
handleIncoming(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Test hook: add a pending request entry manually
|
||||||
|
void debugAddPendingForTest(const std::string &id, const std::string &method,
|
||||||
|
std::function<void(const nlohmann::json & result,
|
||||||
|
const nlohmann::json *errorJson)
|
||||||
|
|
||||||
|
>
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||||
|
pendingOrder_.push_back(id);
|
||||||
|
auto it = pendingOrder_.end();
|
||||||
|
--it;
|
||||||
|
PendingRequest pr{method, std::move(cb), {}, it};
|
||||||
|
pending_[id] = std::move(pr);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Test hook: override timeout
|
||||||
|
void setRequestTimeoutMsForTest(int64_t ms)
|
||||||
|
{
|
||||||
|
requestTimeoutMs_ = ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Test hook: set max pending
|
||||||
|
void setMaxPendingForTest(size_t maxPending)
|
||||||
|
{
|
||||||
|
maxPending_ = maxPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Test hook: set running flag (to allow sendRequest in tests without spawning)
|
||||||
|
void setRunningForTest(bool r)
|
||||||
|
{
|
||||||
|
running_ = r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Test hook: send a raw request using internal machinery
|
||||||
|
int debugSendRequestForTest(const std::string &method, const nlohmann::json ¶ms,
|
||||||
|
std::function<void(const nlohmann::json & result,
|
||||||
|
const nlohmann::json *errorJson)
|
||||||
|
|
||||||
|
>
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return sendRequest(method, params, std::move(cb));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace kte::lsp
|
||||||
|
|
||||||
|
#endif // KTE_LSP_PROCESS_CLIENT_H
|
||||||
47
lsp/LspServerConfig.h
Normal file
47
lsp/LspServerConfig.h
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* LspServerConfig.h - per-language LSP server configuration
|
||||||
|
*/
|
||||||
|
#ifndef KTE_LSP_SERVER_CONFIG_H
|
||||||
|
#define KTE_LSP_SERVER_CONFIG_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
enum class LspSyncMode {
|
||||||
|
None = 0,
|
||||||
|
Full = 1,
|
||||||
|
Incremental = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LspServerConfig {
|
||||||
|
std::string command; // executable name/path
|
||||||
|
std::vector<std::string> args; // CLI args
|
||||||
|
std::vector<std::string> filePatterns; // e.g. {"*.rs"}
|
||||||
|
std::string rootPatterns; // e.g. "Cargo.toml"
|
||||||
|
LspSyncMode preferredSyncMode = LspSyncMode::Incremental;
|
||||||
|
bool autostart = true;
|
||||||
|
std::unordered_map<std::string, std::string> initializationOptions; // placeholder
|
||||||
|
std::unordered_map<std::string, std::string> settings; // placeholder
|
||||||
|
};
|
||||||
|
|
||||||
|
// Provide a small set of defaults; callers may ignore
|
||||||
|
inline std::vector<LspServerConfig>
|
||||||
|
GetDefaultServerConfigs()
|
||||||
|
{
|
||||||
|
return std::vector<LspServerConfig>{
|
||||||
|
LspServerConfig{
|
||||||
|
.command = "rust-analyzer", .args = {}, .filePatterns = {"*.rs"}, .rootPatterns = "Cargo.toml"
|
||||||
|
},
|
||||||
|
LspServerConfig{
|
||||||
|
.command = "clangd", .args = {"--background-index"},
|
||||||
|
.filePatterns = {"*.c", "*.cc", "*.cpp", "*.h", "*.hpp"},
|
||||||
|
.rootPatterns = "compile_commands.json"
|
||||||
|
},
|
||||||
|
LspServerConfig{.command = "gopls", .args = {}, .filePatterns = {"*.go"}, .rootPatterns = "go.mod"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} // namespace kte::lsp
|
||||||
|
|
||||||
|
#endif // KTE_LSP_SERVER_CONFIG_H
|
||||||
55
lsp/LspTypes.h
Normal file
55
lsp/LspTypes.h
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* LspTypes.h - minimal LSP-related data types for initial integration
|
||||||
|
*/
|
||||||
|
#ifndef KTE_LSP_TYPES_H
|
||||||
|
#define KTE_LSP_TYPES_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
// NOTE on coordinates:
|
||||||
|
// - Internal editor coordinates use UTF-8 columns counted by Unicode scalars.
|
||||||
|
// - LSP wire protocol uses UTF-16 code units for the `character` field.
|
||||||
|
// Conversions are performed in higher layers via `lsp/UtfCodec.h` helpers.
|
||||||
|
struct Position {
|
||||||
|
int line = 0;
|
||||||
|
int character = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Range {
|
||||||
|
Position start;
|
||||||
|
Position end;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TextDocumentContentChangeEvent {
|
||||||
|
std::optional<Range> range; // if not set, represents full document change
|
||||||
|
std::string text; // new text for the given range
|
||||||
|
};
|
||||||
|
|
||||||
|
// Minimal feature result types for phase 1
|
||||||
|
struct CompletionItem {
|
||||||
|
std::string label;
|
||||||
|
std::optional<std::string> detail; // optional extra info
|
||||||
|
std::optional<std::string> insertText; // if present, use instead of label
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CompletionList {
|
||||||
|
bool isIncomplete = false;
|
||||||
|
std::vector<CompletionItem> items;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HoverResult {
|
||||||
|
std::string contents; // concatenated plaintext/markdown for now
|
||||||
|
std::optional<Range> range; // optional range
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Location {
|
||||||
|
std::string uri;
|
||||||
|
Range range;
|
||||||
|
};
|
||||||
|
} // namespace kte::lsp
|
||||||
|
|
||||||
|
#endif // KTE_LSP_TYPES_H
|
||||||
53
lsp/TerminalDiagnosticDisplay.cc
Normal file
53
lsp/TerminalDiagnosticDisplay.cc
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* TerminalDiagnosticDisplay.cc - minimal stub implementation
|
||||||
|
*/
|
||||||
|
#include "TerminalDiagnosticDisplay.h"
|
||||||
|
|
||||||
|
#include "../TerminalRenderer.h"
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
TerminalDiagnosticDisplay::TerminalDiagnosticDisplay(TerminalRenderer *renderer)
|
||||||
|
: renderer_(renderer) {}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TerminalDiagnosticDisplay::updateDiagnostics(const std::string &uri,
|
||||||
|
const std::vector<Diagnostic> &diagnostics)
|
||||||
|
{
|
||||||
|
(void) uri;
|
||||||
|
(void) diagnostics;
|
||||||
|
// Stub: no rendering yet. Future: gutter markers, underlines, virtual text.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TerminalDiagnosticDisplay::showInlineDiagnostic(const Diagnostic &diagnostic)
|
||||||
|
{
|
||||||
|
(void) diagnostic;
|
||||||
|
// Stub: show as message line in future.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TerminalDiagnosticDisplay::showDiagnosticList(const std::vector<Diagnostic> &diagnostics)
|
||||||
|
{
|
||||||
|
(void) diagnostics;
|
||||||
|
// Stub: open a panel/list in future.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TerminalDiagnosticDisplay::hideDiagnosticList()
|
||||||
|
{
|
||||||
|
// Stub
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TerminalDiagnosticDisplay::updateStatusBar(int errorCount, int warningCount)
|
||||||
|
{
|
||||||
|
(void) errorCount;
|
||||||
|
(void) warningCount;
|
||||||
|
// Stub: integrate with status bar rendering later.
|
||||||
|
}
|
||||||
|
} // namespace kte::lsp
|
||||||
35
lsp/TerminalDiagnosticDisplay.h
Normal file
35
lsp/TerminalDiagnosticDisplay.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* TerminalDiagnosticDisplay.h - Terminal (ncurses) diagnostics visualization stub
|
||||||
|
*/
|
||||||
|
#ifndef KTE_LSP_TERMINAL_DIAGNOSTIC_DISPLAY_H
|
||||||
|
#define KTE_LSP_TERMINAL_DIAGNOSTIC_DISPLAY_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "DiagnosticDisplay.h"
|
||||||
|
|
||||||
|
class TerminalRenderer; // fwd
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
class TerminalDiagnosticDisplay final : 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:
|
||||||
|
[[maybe_unused]] TerminalRenderer *renderer_{}; // non-owning
|
||||||
|
};
|
||||||
|
} // namespace kte::lsp
|
||||||
|
|
||||||
|
#endif // KTE_LSP_TERMINAL_DIAGNOSTIC_DISPLAY_H
|
||||||
155
lsp/UtfCodec.cc
Normal file
155
lsp/UtfCodec.cc
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
* UtfCodec.cc - UTF-8 <-> UTF-16 code unit position conversions
|
||||||
|
*/
|
||||||
|
#include "UtfCodec.h"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
// Decode next code point from a UTF-8 string.
|
||||||
|
// On invalid input, consumes 1 byte and returns U+FFFD.
|
||||||
|
// Returns: (codepoint, bytesConsumed)
|
||||||
|
static inline std::pair<uint32_t, size_t>
|
||||||
|
decodeUtf8(std::string_view s, size_t i)
|
||||||
|
{
|
||||||
|
if (i >= s.size())
|
||||||
|
return {0, 0};
|
||||||
|
unsigned char c0 = static_cast<unsigned char>(s[i]);
|
||||||
|
if (c0 < 0x80) {
|
||||||
|
return {c0, 1};
|
||||||
|
}
|
||||||
|
// Determine sequence length
|
||||||
|
if ((c0 & 0xE0) == 0xC0) {
|
||||||
|
if (i + 1 >= s.size())
|
||||||
|
return {0xFFFD, 1};
|
||||||
|
unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
|
||||||
|
if ((c1 & 0xC0) != 0x80)
|
||||||
|
return {0xFFFD, 1};
|
||||||
|
uint32_t cp = ((c0 & 0x1F) << 6) | (c1 & 0x3F);
|
||||||
|
// Overlong check: must be >= 0x80
|
||||||
|
if (cp < 0x80)
|
||||||
|
return {0xFFFD, 1};
|
||||||
|
return {cp, 2};
|
||||||
|
}
|
||||||
|
if ((c0 & 0xF0) == 0xE0) {
|
||||||
|
if (i + 2 >= s.size())
|
||||||
|
return {0xFFFD, 1};
|
||||||
|
unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
|
||||||
|
unsigned char c2 = static_cast<unsigned char>(s[i + 2]);
|
||||||
|
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80)
|
||||||
|
return {0xFFFD, 1};
|
||||||
|
uint32_t cp = ((c0 & 0x0F) << 12) | ((c1 & 0x3F) << 6) | (c2 & 0x3F);
|
||||||
|
// Overlong / surrogate range check
|
||||||
|
if (cp < 0x800 || (cp >= 0xD800 && cp <= 0xDFFF))
|
||||||
|
return {0xFFFD, 1};
|
||||||
|
return {cp, 3};
|
||||||
|
}
|
||||||
|
if ((c0 & 0xF8) == 0xF0) {
|
||||||
|
if (i + 3 >= s.size())
|
||||||
|
return {0xFFFD, 1};
|
||||||
|
unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
|
||||||
|
unsigned char c2 = static_cast<unsigned char>(s[i + 2]);
|
||||||
|
unsigned char c3 = static_cast<unsigned char>(s[i + 3]);
|
||||||
|
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80 || (c3 & 0xC0) != 0x80)
|
||||||
|
return {0xFFFD, 1};
|
||||||
|
uint32_t cp = ((c0 & 0x07) << 18) | ((c1 & 0x3F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
|
||||||
|
// Overlong / max range check
|
||||||
|
if (cp < 0x10000 || cp > 0x10FFFF)
|
||||||
|
return {0xFFFD, 1};
|
||||||
|
return {cp, 4};
|
||||||
|
}
|
||||||
|
return {0xFFFD, 1};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline size_t
|
||||||
|
utf16UnitsForCodepoint(uint32_t cp)
|
||||||
|
{
|
||||||
|
return (cp <= 0xFFFF) ? 1 : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
size_t
|
||||||
|
utf8ColToUtf16Units(std::string_view lineUtf8, size_t utf8Col)
|
||||||
|
{
|
||||||
|
// Count by Unicode scalars up to utf8Col; clamp at EOL
|
||||||
|
size_t units = 0;
|
||||||
|
size_t col = 0;
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < lineUtf8.size()) {
|
||||||
|
if (col >= utf8Col)
|
||||||
|
break;
|
||||||
|
auto [cp, n] = decodeUtf8(lineUtf8, i);
|
||||||
|
if (n == 0)
|
||||||
|
break;
|
||||||
|
units += utf16UnitsForCodepoint(cp);
|
||||||
|
i += n;
|
||||||
|
++col;
|
||||||
|
}
|
||||||
|
return units;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
size_t
|
||||||
|
utf16UnitsToUtf8Col(std::string_view lineUtf8, size_t utf16Units)
|
||||||
|
{
|
||||||
|
// Traverse code points until consuming utf16Units (or reaching EOL)
|
||||||
|
size_t units = 0;
|
||||||
|
size_t col = 0;
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < lineUtf8.size()) {
|
||||||
|
auto [cp, n] = decodeUtf8(lineUtf8, i);
|
||||||
|
if (n == 0)
|
||||||
|
break;
|
||||||
|
size_t add = utf16UnitsForCodepoint(cp);
|
||||||
|
if (units + add > utf16Units)
|
||||||
|
break;
|
||||||
|
units += add;
|
||||||
|
i += n;
|
||||||
|
++col;
|
||||||
|
if (units == utf16Units)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Position
|
||||||
|
toUtf16(const std::string &uri, const Position &pUtf8, const LineProvider &provider)
|
||||||
|
{
|
||||||
|
Position out = pUtf8;
|
||||||
|
std::string_view line = provider ? provider(uri, pUtf8.line) : std::string_view();
|
||||||
|
out.character = static_cast<int>(utf8ColToUtf16Units(line, static_cast<size_t>(pUtf8.character)));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Position
|
||||||
|
toUtf8(const std::string &uri, const Position &pUtf16, const LineProvider &provider)
|
||||||
|
{
|
||||||
|
Position out = pUtf16;
|
||||||
|
std::string_view line = provider ? provider(uri, pUtf16.line) : std::string_view();
|
||||||
|
out.character = static_cast<int>(utf16UnitsToUtf8Col(line, static_cast<size_t>(pUtf16.character)));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Range
|
||||||
|
toUtf16(const std::string &uri, const Range &rUtf8, const LineProvider &provider)
|
||||||
|
{
|
||||||
|
Range r;
|
||||||
|
r.start = toUtf16(uri, rUtf8.start, provider);
|
||||||
|
r.end = toUtf16(uri, rUtf8.end, provider);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Range
|
||||||
|
toUtf8(const std::string &uri, const Range &rUtf16, const LineProvider &provider)
|
||||||
|
{
|
||||||
|
Range r;
|
||||||
|
r.start = toUtf8(uri, rUtf16.start, provider);
|
||||||
|
r.end = toUtf8(uri, rUtf16.end, provider);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
} // namespace kte::lsp
|
||||||
37
lsp/UtfCodec.h
Normal file
37
lsp/UtfCodec.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* UtfCodec.h - Helpers for UTF-8 <-> UTF-16 code unit position conversions
|
||||||
|
*/
|
||||||
|
#ifndef KTE_LSP_UTF_CODEC_H
|
||||||
|
#define KTE_LSP_UTF_CODEC_H
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "LspTypes.h"
|
||||||
|
|
||||||
|
namespace kte::lsp {
|
||||||
|
// Map between editor-internal UTF-8 columns (by Unicode scalar count)
|
||||||
|
// and LSP wire UTF-16 code units (per LSP spec).
|
||||||
|
|
||||||
|
// Convert a UTF-8 column index (in Unicode scalars) to UTF-16 code units for a given line.
|
||||||
|
size_t utf8ColToUtf16Units(std::string_view lineUtf8, size_t utf8Col);
|
||||||
|
|
||||||
|
// Convert a UTF-16 code unit count to a UTF-8 column index (in Unicode scalars) for a given line.
|
||||||
|
size_t utf16UnitsToUtf8Col(std::string_view lineUtf8, size_t utf16Units);
|
||||||
|
|
||||||
|
// Line text provider to allow conversions without giving the codec direct buffer access.
|
||||||
|
using LineProvider = std::function<std::string_view(const std::string & uri, int line)>;
|
||||||
|
|
||||||
|
// Convenience helpers for positions and ranges using a line provider.
|
||||||
|
Position toUtf16(const std::string &uri, const Position &pUtf8, const LineProvider &provider);
|
||||||
|
|
||||||
|
Position toUtf8(const std::string &uri, const Position &pUtf16, const LineProvider &provider);
|
||||||
|
|
||||||
|
Range toUtf16(const std::string &uri, const Range &rUtf8, const LineProvider &provider);
|
||||||
|
|
||||||
|
Range toUtf8(const std::string &uri, const Range &rUtf16, const LineProvider &provider);
|
||||||
|
} // namespace kte::lsp
|
||||||
|
|
||||||
|
#endif // KTE_LSP_UTF_CODEC_H
|
||||||
47
main.cc
47
main.cc
@@ -12,6 +12,7 @@
|
|||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
#include "Frontend.h"
|
#include "Frontend.h"
|
||||||
#include "TerminalFrontend.h"
|
#include "TerminalFrontend.h"
|
||||||
|
#include "lsp/LspManager.h"
|
||||||
|
|
||||||
#if defined(KTE_BUILD_GUI)
|
#if defined(KTE_BUILD_GUI)
|
||||||
#include "GUIFrontend.h"
|
#include "GUIFrontend.h"
|
||||||
@@ -28,6 +29,8 @@ PrintUsage(const char *prog)
|
|||||||
{
|
{
|
||||||
std::cerr << "Usage: " << prog << " [OPTIONS] [files]\n"
|
std::cerr << "Usage: " << prog << " [OPTIONS] [files]\n"
|
||||||
<< "Options:\n"
|
<< "Options:\n"
|
||||||
|
<< " -c, --chdir DIR Change working directory before opening files\n"
|
||||||
|
<< " -d, --debug Enable LSP debug logging\n"
|
||||||
<< " -g, --gui Use GUI frontend (if built)\n"
|
<< " -g, --gui Use GUI frontend (if built)\n"
|
||||||
<< " -t, --term Use terminal (ncurses) frontend [default]\n"
|
<< " -t, --term Use terminal (ncurses) frontend [default]\n"
|
||||||
<< " -h, --help Show this help and exit\n"
|
<< " -h, --help Show this help and exit\n"
|
||||||
@@ -36,17 +39,25 @@ PrintUsage(const char *prog)
|
|||||||
|
|
||||||
|
|
||||||
int
|
int
|
||||||
main(int argc, const char *argv[])
|
main(const int argc, const char *argv[])
|
||||||
{
|
{
|
||||||
Editor editor;
|
Editor editor;
|
||||||
|
// Wire up LSP manager (no diagnostic UI yet; frontends may provide later)
|
||||||
|
kte::lsp::LspManager lspMgr(&editor, nullptr);
|
||||||
|
editor.SetLspManager(&lspMgr);
|
||||||
|
|
||||||
// CLI parsing using getopt_long
|
// CLI parsing using getopt_long
|
||||||
bool req_gui = false;
|
bool req_gui = false;
|
||||||
bool req_term = false;
|
bool req_term = false;
|
||||||
bool show_help = false;
|
bool show_help = false;
|
||||||
bool show_version = false;
|
bool show_version = false;
|
||||||
|
bool lsp_debug = false;
|
||||||
|
|
||||||
|
std::string nwd;
|
||||||
|
|
||||||
static struct option long_opts[] = {
|
static struct option long_opts[] = {
|
||||||
|
{"chdir", required_argument, nullptr, 'c'},
|
||||||
|
{"debug", no_argument, nullptr, 'd'},
|
||||||
{"gui", no_argument, nullptr, 'g'},
|
{"gui", no_argument, nullptr, 'g'},
|
||||||
{"term", no_argument, nullptr, 't'},
|
{"term", no_argument, nullptr, 't'},
|
||||||
{"help", no_argument, nullptr, 'h'},
|
{"help", no_argument, nullptr, 'h'},
|
||||||
@@ -56,8 +67,14 @@ main(int argc, const char *argv[])
|
|||||||
|
|
||||||
int opt;
|
int opt;
|
||||||
int long_index = 0;
|
int long_index = 0;
|
||||||
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "gthV", long_opts, &long_index)) != -1) {
|
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "c:dgthV", long_opts, &long_index)) != -1) {
|
||||||
switch (opt) {
|
switch (opt) {
|
||||||
|
case 'c':
|
||||||
|
nwd = optarg;
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
lsp_debug = true;
|
||||||
|
break;
|
||||||
case 'g':
|
case 'g':
|
||||||
req_gui = true;
|
req_gui = true;
|
||||||
break;
|
break;
|
||||||
@@ -90,6 +107,16 @@ main(int argc, const char *argv[])
|
|||||||
(void) req_term; // suppress unused warning when GUI is not compiled in
|
(void) req_term; // suppress unused warning when GUI is not compiled in
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Apply LSP debug setting strictly based on -d flag
|
||||||
|
lspMgr.setDebugLogging(lsp_debug);
|
||||||
|
if (lsp_debug) {
|
||||||
|
// Ensure LSP subprocess client picks up debug via environment
|
||||||
|
::setenv("KTE_LSP_DEBUG", "1", 1);
|
||||||
|
} else {
|
||||||
|
// Prevent environment from enabling debug implicitly
|
||||||
|
::unsetenv("KTE_LSP_DEBUG");
|
||||||
|
}
|
||||||
|
|
||||||
// Determine frontend
|
// Determine frontend
|
||||||
#if !defined(KTE_BUILD_GUI)
|
#if !defined(KTE_BUILD_GUI)
|
||||||
if (req_gui) {
|
if (req_gui) {
|
||||||
@@ -104,11 +131,14 @@ main(int argc, const char *argv[])
|
|||||||
} else if (req_term) {
|
} else if (req_term) {
|
||||||
use_gui = false;
|
use_gui = false;
|
||||||
} else {
|
} else {
|
||||||
// Default depends on build target: kge defaults to GUI, kte to terminal
|
|
||||||
|
|
||||||
|
|
||||||
|
// Default depends on build target: kge defaults to GUI, kte to terminal
|
||||||
#if defined(KTE_DEFAULT_GUI)
|
#if defined(KTE_DEFAULT_GUI)
|
||||||
use_gui = true;
|
use_gui = true;
|
||||||
#else
|
#else
|
||||||
use_gui = false;
|
use_gui = false;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -199,6 +229,13 @@ main(int argc, const char *argv[])
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(KTE_BUILD_GUI)
|
||||||
|
if (!nwd.empty()) {
|
||||||
|
if (chdir(nwd.c_str()) != 0) {
|
||||||
|
std::cerr << "kge: failed to chdir to " << nwd << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (!fe->Init(editor)) {
|
if (!fe->Init(editor)) {
|
||||||
std::cerr << "kte: failed to initialize frontend" << std::endl;
|
std::cerr << "kte: failed to initialize frontend" << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
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
|
||||||
119
test_lsp_decode.cc
Normal file
119
test_lsp_decode.cc
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// test_lsp_decode.cc - tests for LspProcessClient JSON decoding/dispatch
|
||||||
|
#include <cassert>
|
||||||
|
#include <atomic>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "lsp/LspProcessClient.h"
|
||||||
|
#include "lsp/Diagnostic.h"
|
||||||
|
|
||||||
|
using namespace kte::lsp;
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
main()
|
||||||
|
{
|
||||||
|
// Create client (won't start a process for these tests)
|
||||||
|
LspProcessClient client("/bin/echo", {});
|
||||||
|
|
||||||
|
// 1) Numeric id response should match string key "42"
|
||||||
|
{
|
||||||
|
std::atomic<bool> called{false};
|
||||||
|
client.debugAddPendingForTest("42", "dummy",
|
||||||
|
[&](const nlohmann::json &result, const nlohmann::json *err) {
|
||||||
|
(void) result;
|
||||||
|
(void) err;
|
||||||
|
called = true;
|
||||||
|
});
|
||||||
|
std::string resp = "{\"jsonrpc\":\"2.0\",\"id\":42,\"result\":null}";
|
||||||
|
client.debugInjectMessageForTest(resp);
|
||||||
|
assert(called.load());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) String id response should resolve
|
||||||
|
{
|
||||||
|
std::atomic<bool> called{false};
|
||||||
|
client.debugAddPendingForTest("abc123", "dummy",
|
||||||
|
[&](const nlohmann::json &result, const nlohmann::json *err) {
|
||||||
|
(void) result;
|
||||||
|
(void) err;
|
||||||
|
called = true;
|
||||||
|
});
|
||||||
|
std::string resp = "{\"jsonrpc\":\"2.0\",\"id\":\"abc123\",\"result\":{}}";
|
||||||
|
client.debugInjectMessageForTest(resp);
|
||||||
|
assert(called.load());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Diagnostics notification decoding
|
||||||
|
{
|
||||||
|
std::atomic<bool> diagCalled{false};
|
||||||
|
client.setDiagnosticsHandler([&](const std::string &uri, const std::vector<Diagnostic> &d) {
|
||||||
|
assert(uri == "file:///tmp/x.rs");
|
||||||
|
assert(!d.empty());
|
||||||
|
diagCalled = true;
|
||||||
|
});
|
||||||
|
std::string notif = R"({
|
||||||
|
"jsonrpc":"2.0",
|
||||||
|
"method":"textDocument/publishDiagnostics",
|
||||||
|
"params":{
|
||||||
|
"uri":"file:///tmp/x.rs",
|
||||||
|
"diagnostics":[{
|
||||||
|
"range": {"start": {"line":0, "character":1}, "end": {"line":0, "character":2}},
|
||||||
|
"severity": 1,
|
||||||
|
"message": "oops"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})";
|
||||||
|
client.debugInjectMessageForTest(notif);
|
||||||
|
assert(diagCalled.load());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) ShowMessage notification should be safely handled (no crash)
|
||||||
|
{
|
||||||
|
std::string msg =
|
||||||
|
"{\"jsonrpc\":\"2.0\",\"method\":\"window/showMessage\",\"params\":{\"type\":2,\"message\":\"hi\"}}";
|
||||||
|
client.debugInjectMessageForTest(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) workspace/configuration request should be responded to (no crash)
|
||||||
|
{
|
||||||
|
std::string req = R"({
|
||||||
|
"jsonrpc":"2.0",
|
||||||
|
"id": 7,
|
||||||
|
"method":"workspace/configuration",
|
||||||
|
"params": {"items": [{"section":"x"},{"section":"y"}]}
|
||||||
|
})";
|
||||||
|
client.debugInjectMessageForTest(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) Pending cap eviction: oldest request is dropped with -32001
|
||||||
|
{
|
||||||
|
LspProcessClient c2("/bin/echo", {});
|
||||||
|
c2.setRunningForTest(true);
|
||||||
|
c2.setMaxPendingForTest(2);
|
||||||
|
|
||||||
|
std::atomic<int> drops{0};
|
||||||
|
auto make_cb = [&](const char *tag) {
|
||||||
|
return [&, tag](const nlohmann::json &res, const nlohmann::json *err) {
|
||||||
|
(void) res;
|
||||||
|
if (err && err->is_object()) {
|
||||||
|
auto it = err->find("code");
|
||||||
|
if (it != err->end() && it->is_number_integer() && *it == -32001) {
|
||||||
|
// Only the oldest (first) should be dropped
|
||||||
|
if (std::string(tag) == "first")
|
||||||
|
drops.fetch_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
// Enqueue 3 requests; cap=2 -> first should be dropped immediately when third is added
|
||||||
|
c2.debugSendRequestForTest("a", nlohmann::json::object(), make_cb("first"));
|
||||||
|
c2.debugSendRequestForTest("b", nlohmann::json::object(), make_cb("second"));
|
||||||
|
c2.debugSendRequestForTest("c", nlohmann::json::object(), make_cb("third"));
|
||||||
|
// Allow callbacks (none are async here, drop is invoked inline after send)
|
||||||
|
assert(drops.load() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::puts("test_lsp_decode: OK");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
76
test_transport.cc
Normal file
76
test_transport.cc
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// test_transport.cc - transport framing tests
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "lsp/JsonRpcTransport.h"
|
||||||
|
|
||||||
|
using namespace kte::lsp;
|
||||||
|
|
||||||
|
|
||||||
|
static void
|
||||||
|
write_all(int fd, const void *bufv, size_t len)
|
||||||
|
{
|
||||||
|
const char *buf = static_cast<const char *>(bufv);
|
||||||
|
size_t left = len;
|
||||||
|
while (left > 0) {
|
||||||
|
ssize_t n = ::write(fd, buf, left);
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
std::perror("write");
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
buf += static_cast<size_t>(n);
|
||||||
|
left -= static_cast<size_t>(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
main()
|
||||||
|
{
|
||||||
|
int p[2];
|
||||||
|
assert(pipe(p) == 0);
|
||||||
|
int readFd = p[0];
|
||||||
|
int writeFd = p[1];
|
||||||
|
|
||||||
|
JsonRpcTransport t;
|
||||||
|
// We only need inFd for read tests; pass writeFd for completeness
|
||||||
|
t.connect(readFd, writeFd);
|
||||||
|
|
||||||
|
auto sendMsg = [&](const std::string &payload, bool lowerHeader) {
|
||||||
|
std::string header = (lowerHeader ? std::string("content-length: ") : std::string("Content-Length: ")) +
|
||||||
|
std::to_string(payload.size()) + "\r\n\r\n";
|
||||||
|
write_all(writeFd, header.data(), header.size());
|
||||||
|
// Send body in two parts to exercise partial reads
|
||||||
|
size_t mid = payload.size() / 2;
|
||||||
|
write_all(writeFd, payload.data(), mid);
|
||||||
|
write_all(writeFd, payload.data() + mid, payload.size() - mid);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string p1 = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":null}";
|
||||||
|
std::string p2 = "{\"jsonrpc\":\"2.0\",\"method\":\"ping\"}";
|
||||||
|
sendMsg(p1, false);
|
||||||
|
sendMsg(p2, true); // case-insensitive header
|
||||||
|
|
||||||
|
auto m1 = t.read();
|
||||||
|
assert(m1.has_value());
|
||||||
|
assert(m1->raw == p1);
|
||||||
|
|
||||||
|
auto m2 = t.read();
|
||||||
|
assert(m2.has_value());
|
||||||
|
assert(m2->raw == p2);
|
||||||
|
|
||||||
|
// Close write end to signal EOF; next read should return nullopt
|
||||||
|
::close(writeFd);
|
||||||
|
auto m3 = t.read();
|
||||||
|
assert(!m3.has_value());
|
||||||
|
|
||||||
|
::close(readFd);
|
||||||
|
std::puts("test_transport: OK");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
235
test_undo.cc
235
test_undo.cc
@@ -94,6 +94,241 @@ main()
|
|||||||
std::cout << " Buffer content: '" << line_after_redo << "'\n";
|
std::cout << " Buffer content: '" << line_after_redo << "'\n";
|
||||||
std::cout << " ✓ Redo successful - text restored\n\n";
|
std::cout << " ✓ Redo successful - text restored\n\n";
|
||||||
|
|
||||||
|
// Test 4: Branching behavior – redo is discarded after new edits
|
||||||
|
std::cout << "Test 4: Branching behavior (redo discarded after new edits)\n";
|
||||||
|
// Reset to empty by undoing the last redo and the original insert, then reinsert 'abc'
|
||||||
|
// Ensure buffer is empty before starting this scenario
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo); // undo Hello
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "");
|
||||||
|
|
||||||
|
// Type a contiguous word 'abc' (single batch)
|
||||||
|
frontend.Input().QueueText("abc");
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "abc");
|
||||||
|
|
||||||
|
// Undo once – should remove the whole batch and leave empty
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "");
|
||||||
|
|
||||||
|
// Now type new text 'X' – this should create a new branch and discard old redo chain
|
||||||
|
frontend.Input().QueueText("X");
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "X");
|
||||||
|
|
||||||
|
// Attempt Redo – should be a no-op (redo branch was discarded by new edit)
|
||||||
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "X");
|
||||||
|
// Undo and Redo along the new branch should still work
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "X");
|
||||||
|
std::cout << " ✓ Redo discarded after new edit; new branch undo/redo works\n\n";
|
||||||
|
|
||||||
|
// Clear buffer state for next tests: undo to empty if needed
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "");
|
||||||
|
|
||||||
|
// Test 5: UTF-8 insertion and undo/redo round-trip
|
||||||
|
std::cout << "Test 5: UTF-8 insertion 'é漢' and undo/redo\n";
|
||||||
|
const std::string utf8_text = "é漢"; // multi-byte UTF-8 (2 bytes + 3 bytes)
|
||||||
|
frontend.Input().QueueText(utf8_text);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == utf8_text);
|
||||||
|
// Undo should remove the entire contiguous insertion batch
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "");
|
||||||
|
// Redo restores it
|
||||||
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == utf8_text);
|
||||||
|
std::cout << " ✓ UTF-8 insert round-trips with undo/redo\n\n";
|
||||||
|
|
||||||
|
// Clear for next test
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "");
|
||||||
|
|
||||||
|
// Test 6: Multi-line operations (newline split and join via backspace at BOL)
|
||||||
|
std::cout << "Test 6: Newline split and join via backspace at BOL\n";
|
||||||
|
// Insert "ab" then newline then "cd" → expect two lines
|
||||||
|
frontend.Input().QueueText("ab");
|
||||||
|
frontend.Input().QueueCommand(CommandId::Newline);
|
||||||
|
frontend.Input().QueueText("cd");
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(buf->Rows().size() >= 2);
|
||||||
|
assert(std::string(buf->Rows()[0]) == "ab");
|
||||||
|
assert(std::string(buf->Rows()[1]) == "cd");
|
||||||
|
std::cout << " ✓ Split into two lines\n";
|
||||||
|
|
||||||
|
// Undo once – should remove "cd" insertion leaving two lines ["ab", ""] or join depending on commit
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
// Current design batches typing on the second line; after undo, the second line should exist but be empty
|
||||||
|
assert(buf->Rows().size() >= 2);
|
||||||
|
assert(std::string(buf->Rows()[0]) == "ab");
|
||||||
|
assert(std::string(buf->Rows()[1]) == "");
|
||||||
|
|
||||||
|
// Undo the newline – should rejoin to a single line "ab"
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(buf->Rows().size() >= 1);
|
||||||
|
assert(std::string(buf->Rows()[0]) == "ab");
|
||||||
|
|
||||||
|
// Redo twice to get back to ["ab","cd"]
|
||||||
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "ab");
|
||||||
|
assert(std::string(buf->Rows()[1]) == "cd");
|
||||||
|
std::cout << " ✓ Newline undo/redo round-trip\n";
|
||||||
|
|
||||||
|
// Now join via Backspace at beginning of second line
|
||||||
|
frontend.Input().QueueCommand(CommandId::MoveDown); // ensure we're on the second line
|
||||||
|
frontend.Input().QueueCommand(CommandId::MoveHome); // go to BOL on second line
|
||||||
|
frontend.Input().QueueCommand(CommandId::Backspace); // join with previous line
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(buf->Rows().size() >= 1);
|
||||||
|
assert(std::string(buf->Rows()[0]) == "abcd");
|
||||||
|
std::cout << " ✓ Backspace at BOL joins lines\n";
|
||||||
|
|
||||||
|
// Undo/Redo the join
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(buf->Rows().size() >= 1);
|
||||||
|
assert(std::string(buf->Rows()[0]) == "abcd");
|
||||||
|
std::cout << " ✓ Join undo/redo round-trip\n\n";
|
||||||
|
|
||||||
|
// Test 7: Typing batching – a contiguous word undone in one step
|
||||||
|
std::cout << "Test 7: Typing batching (single undo removes whole word)\n";
|
||||||
|
// Clear current line first
|
||||||
|
frontend.Input().QueueCommand(CommandId::MoveHome);
|
||||||
|
frontend.Input().QueueCommand(CommandId::KillToEOL);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]).empty());
|
||||||
|
// Type a word and verify one undo clears it
|
||||||
|
frontend.Input().QueueText("hello");
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "hello");
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]).empty());
|
||||||
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "hello");
|
||||||
|
std::cout << " ✓ Contiguous typing batched into single undo step\n\n";
|
||||||
|
|
||||||
|
// Test 8: Forward delete batching at a fixed anchor column
|
||||||
|
std::cout << "Test 8: Forward delete batching at fixed anchor (DeleteChar)\n";
|
||||||
|
// Prepare line content
|
||||||
|
frontend.Input().QueueCommand(CommandId::MoveHome);
|
||||||
|
frontend.Input().QueueCommand(CommandId::KillToEOL);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
frontend.Input().QueueText("abcdef");
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
// Ensure cursor at anchor column 0
|
||||||
|
frontend.Input().QueueCommand(CommandId::MoveHome);
|
||||||
|
// Delete three chars at cursor; should batch into one Delete node
|
||||||
|
frontend.Input().QueueCommand(CommandId::DeleteChar, "", 3);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "def");
|
||||||
|
// Single undo should restore the entire deleted run
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "abcdef");
|
||||||
|
// Redo should remove the same run again
|
||||||
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "def");
|
||||||
|
std::cout << " ✓ Forward delete batched and undo/redo round-trips\n\n";
|
||||||
|
|
||||||
|
// Test 9: Backspace batching with prepend rule (cursor moves left)
|
||||||
|
std::cout << "Test 9: Backspace batching with prepend rule\n";
|
||||||
|
// Restore to full string then backspace a run
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo); // bring back to "abcdef"
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "abcdef");
|
||||||
|
// Move to end and backspace three characters; should batch into one Delete node
|
||||||
|
frontend.Input().QueueCommand(CommandId::MoveEnd);
|
||||||
|
frontend.Input().QueueCommand(CommandId::Backspace, "", 3);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "abc");
|
||||||
|
// Single undo restores the deleted run
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "abcdef");
|
||||||
|
// Redo removes it again
|
||||||
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
assert(std::string(buf->Rows()[0]) == "abc");
|
||||||
|
std::cout << " ✓ Backspace run batched and undo/redo round-trips\n\n";
|
||||||
|
|
||||||
frontend.Shutdown();
|
frontend.Shutdown();
|
||||||
|
|
||||||
std::cout << "====================================\n";
|
std::cout << "====================================\n";
|
||||||
|
|||||||
101
test_utfcodec.cc
Normal file
101
test_utfcodec.cc
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// test_utfcodec.cc - simple tests for UtfCodec helpers
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "lsp/UtfCodec.h"
|
||||||
|
|
||||||
|
using namespace kte::lsp;
|
||||||
|
|
||||||
|
|
||||||
|
static std::string_view
|
||||||
|
lp(const std::string &, int)
|
||||||
|
{
|
||||||
|
return std::string_view();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
main()
|
||||||
|
{
|
||||||
|
// ASCII: each scalar = 1 UTF-16 unit
|
||||||
|
{
|
||||||
|
std::string s = "hello"; // 5 ASCII
|
||||||
|
assert(utf8ColToUtf16Units(s, 0) == 0);
|
||||||
|
assert(utf8ColToUtf16Units(s, 3) == 3);
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 3) == 3);
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 10) == 5); // clamp to EOL
|
||||||
|
}
|
||||||
|
|
||||||
|
// BMP multibyte (e.g., ü U+00FC, α U+03B1) -> still 1 UTF-16 unit
|
||||||
|
{
|
||||||
|
std::string s = u8"aüαb"; // bytes: a [C3 BC] [CE B1] b
|
||||||
|
// columns by codepoints: a(0), ü(1), α(2), b(3)
|
||||||
|
assert(utf8ColToUtf16Units(s, 0) == 0);
|
||||||
|
assert(utf8ColToUtf16Units(s, 1) == 1);
|
||||||
|
assert(utf8ColToUtf16Units(s, 2) == 2);
|
||||||
|
assert(utf8ColToUtf16Units(s, 4) == 4); // past EOL clamps to 4 units
|
||||||
|
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 0) == 0);
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 2) == 2);
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 4) == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-BMP (emoji) -> 2 UTF-16 units per code point
|
||||||
|
{
|
||||||
|
std::string s = u8"A😀B"; // U+1F600 between A and B
|
||||||
|
// codepoints: A, 😀, B => utf8 columns 0..3
|
||||||
|
// utf16 units: A(1), 😀(2), B(1) cumulative: 0,1,3,4
|
||||||
|
assert(utf8ColToUtf16Units(s, 0) == 0);
|
||||||
|
assert(utf8ColToUtf16Units(s, 1) == 1); // after A
|
||||||
|
assert(utf8ColToUtf16Units(s, 2) == 3); // after 😀 (2 units)
|
||||||
|
assert(utf8ColToUtf16Units(s, 3) == 4); // after B
|
||||||
|
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 0) == 0);
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 1) == 1); // A
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 2) == 1); // mid-surrogate -> stays before 😀
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 3) == 2); // end of 😀
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 4) == 3); // after B
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 10) == 3); // clamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid UTF-8: treat invalid byte as U+FFFD (1 UTF-16 unit), consume 1 byte
|
||||||
|
{
|
||||||
|
std::string s;
|
||||||
|
s.push_back('X');
|
||||||
|
s.push_back(char(0xFF)); // invalid single byte
|
||||||
|
s.push_back('Y');
|
||||||
|
// Columns by codepoints as we decode: 'X', U+FFFD, 'Y'
|
||||||
|
assert(utf8ColToUtf16Units(s, 0) == 0);
|
||||||
|
assert(utf8ColToUtf16Units(s, 1) == 1);
|
||||||
|
assert(utf8ColToUtf16Units(s, 2) == 2);
|
||||||
|
assert(utf8ColToUtf16Units(s, 3) == 3);
|
||||||
|
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 0) == 0);
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 1) == 1);
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 2) == 2);
|
||||||
|
assert(utf16UnitsToUtf8Col(s, 3) == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position/Range helpers with a simple provider
|
||||||
|
{
|
||||||
|
std::string lines[] = {u8"A😀B"};
|
||||||
|
LineProvider provider = [&](const std::string &, int line) -> std::string_view {
|
||||||
|
return (line == 0) ? std::string_view(lines[0]) : std::string_view();
|
||||||
|
};
|
||||||
|
Position p8{0, 2}; // after 😀 in utf8 columns
|
||||||
|
Position p16 = toUtf16("file:///x", p8, provider);
|
||||||
|
assert(p16.line == 0 && p16.character == 3);
|
||||||
|
|
||||||
|
Position back = toUtf8("file:///x", p16, provider);
|
||||||
|
assert(back.line == 0 && back.character == 2);
|
||||||
|
|
||||||
|
Range r8{{0, 1}, {0, 3}}; // A|😀|B end
|
||||||
|
Range r16 = toUtf16("file:///x", r8, provider);
|
||||||
|
assert(r16.start.character == 1 && r16.end.character == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::puts("test_utfcodec: OK");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user