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.
This commit is contained in:
37
.github/workflows/release.yml
vendored
Normal file
37
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch: { }
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
homebrew:
|
||||||
|
name: Bump Homebrew formula
|
||||||
|
# Skip this job in case of git pushes to prerelease tags
|
||||||
|
if: ${{ github.event_name != 'push' || !contains(github.ref, '-') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Extract version
|
||||||
|
id: extract-version
|
||||||
|
run: |
|
||||||
|
echo "tag-name=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- uses: mislav/bump-homebrew-formula-action@v3
|
||||||
|
with:
|
||||||
|
formula-name: kte
|
||||||
|
formula-path: Formula/kte.rb
|
||||||
|
homebrew-tap: kisom/homebrew-tap
|
||||||
|
base-branch: master
|
||||||
|
commit-message: |
|
||||||
|
{{formulaName}} {{version}}
|
||||||
|
|
||||||
|
Created by https://github.com/mislav/bump-homebrew-formula-action
|
||||||
|
env:
|
||||||
|
COMMITTER_TOKEN: ${{ secrets.GH_CPAT }}
|
||||||
61
.idea/workspace.xml
generated
61
.idea/workspace.xml
generated
@@ -25,6 +25,7 @@
|
|||||||
<config projectName="kte" targetName="kte" />
|
<config projectName="kte" targetName="kte" />
|
||||||
<config projectName="kte" targetName="imgui" />
|
<config projectName="kte" targetName="imgui" />
|
||||||
<config projectName="kte" targetName="kge" />
|
<config projectName="kte" targetName="kge" />
|
||||||
|
<config projectName="kte" targetName="test_undo" />
|
||||||
</generated>
|
</generated>
|
||||||
</component>
|
</component>
|
||||||
<component name="CMakeSettings" AUTO_RELOAD="true">
|
<component name="CMakeSettings" AUTO_RELOAD="true">
|
||||||
@@ -33,21 +34,26 @@
|
|||||||
</configurations>
|
</configurations>
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity.">
|
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Add non-linear undo/redo design documentation and improve `UndoSystem` with backspace batching and GUI integration fixes.">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/kte.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/kte.iml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/TestFrontend.cc" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/TestInputHandler.cc" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/TestRenderer.cc" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/docs/TestFrontend.md" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/test_undo.cc" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Buffer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.cc" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Buffer.h" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.h" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Editor.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.cc" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/GUIFrontend.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIFrontend.cc" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/GUIFrontend.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIFrontend.cc" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/GUIInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIInputHandler.cc" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/GUIInputHandler.h" beforeDir="false" afterPath="$PROJECT_DIR$/GUIInputHandler.h" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/TerminalInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalInputHandler.cc" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/TerminalInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalInputHandler.cc" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/TerminalInputHandler.h" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalInputHandler.h" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/UndoSystem.cc" beforeDir="false" afterPath="$PROJECT_DIR$/UndoSystem.cc" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/UndoSystem.cc" beforeDir="false" afterPath="$PROJECT_DIR$/UndoSystem.cc" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/UndoSystem.h" beforeDir="false" afterPath="$PROJECT_DIR$/UndoSystem.h" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/UndoSystem.h" beforeDir="false" afterPath="$PROJECT_DIR$/UndoSystem.h" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/docs/ke.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/ke.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/docs/undo-state.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/undo-state.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/fonts/brassmono.h" beforeDir="false" afterPath="$PROJECT_DIR$/fonts/brassmono.h" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -63,9 +69,20 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
<option name="UPDATE_TYPE" value="REBASE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="HighlightingSettingsPerFile">
|
<component name="HighlightingSettingsPerFile">
|
||||||
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
|
||||||
|
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
|
||||||
</component>
|
</component>
|
||||||
<component name="OptimizeOnSaveOptions">
|
<component name="OptimizeOnSaveOptions">
|
||||||
<option name="myRunOnSave" value="true" />
|
<option name="myRunOnSave" value="true" />
|
||||||
@@ -80,6 +97,9 @@
|
|||||||
"associatedIndex": 3
|
"associatedIndex": 3
|
||||||
}]]></component>
|
}]]></component>
|
||||||
<component name="ProjectId" id="36AlI8oyQOzOwSuZg6WxXf5LbHb" />
|
<component name="ProjectId" id="36AlI8oyQOzOwSuZg6WxXf5LbHb" />
|
||||||
|
<component name="ProjectLevelVcsManager">
|
||||||
|
<OptionsSetting value="false" id="Update" />
|
||||||
|
</component>
|
||||||
<component name="ProjectViewState">
|
<component name="ProjectViewState">
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
@@ -88,7 +108,9 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent"><![CDATA[{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"CMake Application.kge.executor": "Debug",
|
"CMake Application.kge.executor": "Run",
|
||||||
|
"CMake Application.test_example.executor": "Run",
|
||||||
|
"CMake Application.test_undo.executor": "Run",
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
"NIXITCH_NIXPKGS_CONFIG": "",
|
"NIXITCH_NIXPKGS_CONFIG": "",
|
||||||
"NIXITCH_NIX_CONF_DIR": "",
|
"NIXITCH_NIX_CONF_DIR": "",
|
||||||
@@ -125,7 +147,7 @@
|
|||||||
<recent name="$PROJECT_DIR$/docs" />
|
<recent name="$PROJECT_DIR$/docs" />
|
||||||
</key>
|
</key>
|
||||||
</component>
|
</component>
|
||||||
<component name="RunManager" selected="CMake Application.kte">
|
<component name="RunManager" selected="CMake Application.test_undo">
|
||||||
<configuration default="true" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true">
|
<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">
|
<method v="2">
|
||||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||||
@@ -146,10 +168,16 @@
|
|||||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
<configuration name="test_undo" 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="test_undo" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="test_undo">
|
||||||
|
<method v="2">
|
||||||
|
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
<list>
|
<list>
|
||||||
<item itemvalue="CMake Application.imgui" />
|
<item itemvalue="CMake Application.imgui" />
|
||||||
<item itemvalue="CMake Application.kge" />
|
<item itemvalue="CMake Application.kge" />
|
||||||
<item itemvalue="CMake Application.kte" />
|
<item itemvalue="CMake Application.kte" />
|
||||||
|
<item itemvalue="CMake Application.test_undo" />
|
||||||
</list>
|
</list>
|
||||||
</component>
|
</component>
|
||||||
<component name="TaskManager">
|
<component name="TaskManager">
|
||||||
@@ -159,7 +187,7 @@
|
|||||||
<option name="number" value="Default" />
|
<option name="number" value="Default" />
|
||||||
<option name="presentableId" value="Default" />
|
<option name="presentableId" value="Default" />
|
||||||
<updated>1764457173148</updated>
|
<updated>1764457173148</updated>
|
||||||
<workItem from="1764457174208" duration="37384000" />
|
<workItem from="1764457174208" duration="41399000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -193,7 +221,15 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1764489870957</updated>
|
<updated>1764489870957</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="5" />
|
<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>
|
||||||
|
<option name="localTasksCounter" value="6" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -211,7 +247,8 @@
|
|||||||
<MESSAGE value="Handle end-of-file newline semantics and improve scroll alignment logic." />
|
<MESSAGE value="Handle end-of-file newline semantics and improve scroll alignment logic." />
|
||||||
<MESSAGE value="Enable installation targets." />
|
<MESSAGE value="Enable installation targets." />
|
||||||
<MESSAGE value="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity." />
|
<MESSAGE value="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity." />
|
||||||
<option name="LAST_COMMIT_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." />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="Add non-linear undo/redo design documentation and improve `UndoSystem` with backspace batching and GUI integration fixes." />
|
||||||
</component>
|
</component>
|
||||||
<component name="XSLT-Support.FileAssociations.UIState">
|
<component name="XSLT-Support.FileAssociations.UIState">
|
||||||
<expand />
|
<expand />
|
||||||
|
|||||||
57
Buffer.cc
57
Buffer.cc
@@ -69,6 +69,63 @@ Buffer::operator=(const Buffer &other)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Move constructor: move all fields and update UndoSystem's buffer reference
|
||||||
|
Buffer::Buffer(Buffer &&other) noexcept
|
||||||
|
: curx_(other.curx_),
|
||||||
|
cury_(other.cury_),
|
||||||
|
rx_(other.rx_),
|
||||||
|
nrows_(other.nrows_),
|
||||||
|
rowoffs_(other.rowoffs_),
|
||||||
|
coloffs_(other.coloffs_),
|
||||||
|
rows_(std::move(other.rows_)),
|
||||||
|
filename_(std::move(other.filename_)),
|
||||||
|
is_file_backed_(other.is_file_backed_),
|
||||||
|
dirty_(other.dirty_),
|
||||||
|
mark_set_(other.mark_set_),
|
||||||
|
mark_curx_(other.mark_curx_),
|
||||||
|
mark_cury_(other.mark_cury_),
|
||||||
|
undo_tree_(std::move(other.undo_tree_)),
|
||||||
|
undo_sys_(std::move(other.undo_sys_))
|
||||||
|
{
|
||||||
|
// Update UndoSystem's buffer reference to point to this object
|
||||||
|
if (undo_sys_) {
|
||||||
|
undo_sys_->UpdateBufferReference(*this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Move assignment: move all fields and update UndoSystem's buffer reference
|
||||||
|
Buffer &
|
||||||
|
Buffer::operator=(Buffer &&other) noexcept
|
||||||
|
{
|
||||||
|
if (this == &other)
|
||||||
|
return *this;
|
||||||
|
|
||||||
|
curx_ = other.curx_;
|
||||||
|
cury_ = other.cury_;
|
||||||
|
rx_ = other.rx_;
|
||||||
|
nrows_ = other.nrows_;
|
||||||
|
rowoffs_ = other.rowoffs_;
|
||||||
|
coloffs_ = other.coloffs_;
|
||||||
|
rows_ = std::move(other.rows_);
|
||||||
|
filename_ = std::move(other.filename_);
|
||||||
|
is_file_backed_ = other.is_file_backed_;
|
||||||
|
dirty_ = other.dirty_;
|
||||||
|
mark_set_ = other.mark_set_;
|
||||||
|
mark_curx_ = other.mark_curx_;
|
||||||
|
mark_cury_ = other.mark_cury_;
|
||||||
|
undo_tree_ = std::move(other.undo_tree_);
|
||||||
|
undo_sys_ = std::move(other.undo_sys_);
|
||||||
|
|
||||||
|
// Update UndoSystem's buffer reference to point to this object
|
||||||
|
if (undo_sys_) {
|
||||||
|
undo_sys_->UpdateBufferReference(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
Buffer::OpenFromFile(const std::string &path, std::string &err)
|
Buffer::OpenFromFile(const std::string &path, std::string &err)
|
||||||
{
|
{
|
||||||
|
|||||||
4
Buffer.h
4
Buffer.h
@@ -20,9 +20,9 @@ public:
|
|||||||
|
|
||||||
Buffer &operator=(const Buffer &other);
|
Buffer &operator=(const Buffer &other);
|
||||||
|
|
||||||
Buffer(Buffer &&) noexcept = default;
|
Buffer(Buffer &&other) noexcept;
|
||||||
|
|
||||||
Buffer &operator=(Buffer &&) noexcept = default;
|
Buffer &operator=(Buffer &&other) noexcept;
|
||||||
|
|
||||||
explicit Buffer(const std::string &path);
|
explicit Buffer(const std::string &path);
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ set(KTE_VERSION "0.0.1")
|
|||||||
# 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 OFF CACHE BOOL "Enable building the graphical version.")
|
||||||
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" OFF)
|
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" OFF)
|
||||||
|
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
||||||
|
|
||||||
if (CMAKE_HOST_UNIX)
|
if (CMAKE_HOST_UNIX)
|
||||||
message(STATUS "Build system is POSIX.")
|
message(STATUS "Build system is POSIX.")
|
||||||
@@ -55,6 +56,9 @@ set(COMMON_SOURCES
|
|||||||
TerminalInputHandler.cc
|
TerminalInputHandler.cc
|
||||||
TerminalRenderer.cc
|
TerminalRenderer.cc
|
||||||
TerminalFrontend.cc
|
TerminalFrontend.cc
|
||||||
|
TestInputHandler.cc
|
||||||
|
TestRenderer.cc
|
||||||
|
TestFrontend.cc
|
||||||
UndoNode.cc
|
UndoNode.cc
|
||||||
UndoTree.cc
|
UndoTree.cc
|
||||||
UndoSystem.cc
|
UndoSystem.cc
|
||||||
@@ -74,6 +78,9 @@ set(COMMON_HEADERS
|
|||||||
TerminalRenderer.h
|
TerminalRenderer.h
|
||||||
Frontend.h
|
Frontend.h
|
||||||
TerminalFrontend.h
|
TerminalFrontend.h
|
||||||
|
TestInputHandler.h
|
||||||
|
TestRenderer.h
|
||||||
|
TestFrontend.h
|
||||||
UndoNode.h
|
UndoNode.h
|
||||||
UndoTree.h
|
UndoTree.h
|
||||||
UndoSystem.h
|
UndoSystem.h
|
||||||
@@ -96,6 +103,19 @@ install(TARGETS kte
|
|||||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# test_undo executable for testing undo/redo system
|
||||||
|
add_executable(test_undo
|
||||||
|
test_undo.cc
|
||||||
|
${COMMON_SOURCES}
|
||||||
|
${COMMON_HEADERS}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (KTE_USE_PIECE_TABLE)
|
||||||
|
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
target_link_libraries(test_undo ${CURSES_LIBRARIES})
|
||||||
|
|
||||||
if (${BUILD_GUI})
|
if (${BUILD_GUI})
|
||||||
target_sources(kte PRIVATE
|
target_sources(kte PRIVATE
|
||||||
Font.h
|
Font.h
|
||||||
@@ -119,7 +139,7 @@ if (${BUILD_GUI})
|
|||||||
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)
|
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)
|
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
||||||
|
|
||||||
install(TARGETS kge
|
install(TARGETS kge
|
||||||
|
|||||||
473
Command.cc
473
Command.cc
@@ -426,8 +426,10 @@ cmd_save(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus("Saved " + buf->Filename());
|
ctx.editor.SetStatus("Saved " + buf->Filename());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
ctx.editor.SetStatus("Buffer is not file-backed; use save-as");
|
// If buffer has no name, prompt for a filename
|
||||||
return false;
|
ctx.editor.StartPrompt(Editor::PromptKind::SaveAs, "Save as", "");
|
||||||
|
ctx.editor.SetStatus("Save as: ");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
if (!buf->Save(err)) {
|
if (!buf->Save(err)) {
|
||||||
ctx.editor.SetStatus(err);
|
ctx.editor.SetStatus(err);
|
||||||
@@ -738,11 +740,6 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus("No buffer to edit");
|
ctx.editor.SetStatus("No buffer to edit");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Start/extend an insert batch for undo
|
|
||||||
if (auto *u = buf->Undo()) {
|
|
||||||
u->Begin(UndoType::Insert);
|
|
||||||
u->Append(std::string_view(ctx.arg));
|
|
||||||
}
|
|
||||||
// If a prompt is active, edit prompt text
|
// If a prompt is active, edit prompt text
|
||||||
if (ctx.editor.PromptActive()) {
|
if (ctx.editor.PromptActive()) {
|
||||||
// Special-case: buffer switch prompt supports Tab-completion
|
// Special-case: buffer switch prompt supports Tab-completion
|
||||||
@@ -859,8 +856,15 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
rows[y].insert(x, ctx.arg);
|
rows[y].insert(x, ctx.arg);
|
||||||
x += ctx.arg.size();
|
x += ctx.arg.size();
|
||||||
}
|
}
|
||||||
buf->SetCursor(x, y);
|
|
||||||
buf->SetDirty(true);
|
buf->SetDirty(true);
|
||||||
|
// Record undo after buffer modification but before cursor update
|
||||||
|
if (auto *u = buf->Undo()) {
|
||||||
|
u->Begin(UndoType::Insert);
|
||||||
|
for (int i = 0; i < repeat; ++i) {
|
||||||
|
u->Append(std::string_view(ctx.arg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf->SetCursor(x, y);
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -922,8 +926,24 @@ cmd_newline(CommandContext &ctx)
|
|||||||
+ (cur ? buffer_display_name(*cur) : std::string("")));
|
+ (cur ? buffer_display_name(*cur) : std::string("")));
|
||||||
}
|
}
|
||||||
} else if (kind == Editor::PromptKind::SaveAs) {
|
} else if (kind == Editor::PromptKind::SaveAs) {
|
||||||
// Optional: not wired yet
|
if (value.empty()) {
|
||||||
ctx.editor.SetStatus("Save As not implemented");
|
ctx.editor.SetStatus("Save canceled (empty filename)");
|
||||||
|
} else {
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf) {
|
||||||
|
ctx.editor.SetStatus("No buffer to save");
|
||||||
|
} else {
|
||||||
|
std::string err;
|
||||||
|
if (!buf->SaveAs(value, err)) {
|
||||||
|
ctx.editor.SetStatus(err);
|
||||||
|
} else {
|
||||||
|
buf->SetDirty(false);
|
||||||
|
ctx.editor.SetStatus("Saved as " + value);
|
||||||
|
if (auto *u = buf->Undo())
|
||||||
|
u->mark_saved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -943,10 +963,6 @@ cmd_newline(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus("No buffer to edit");
|
ctx.editor.SetStatus("No buffer to edit");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Start a newline batch for undo at current cursor
|
|
||||||
if (auto *u = buf->Undo()) {
|
|
||||||
u->Begin(UndoType::Newline);
|
|
||||||
}
|
|
||||||
ensure_at_least_one_line(*buf);
|
ensure_at_least_one_line(*buf);
|
||||||
auto &rows = buf->Rows();
|
auto &rows = buf->Rows();
|
||||||
std::size_t y = buf->Cury();
|
std::size_t y = buf->Cury();
|
||||||
@@ -967,6 +983,11 @@ cmd_newline(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
buf->SetCursor(x, y);
|
buf->SetCursor(x, y);
|
||||||
buf->SetDirty(true);
|
buf->SetDirty(true);
|
||||||
|
// Record newline after buffer modification; commit immediately for single-step undo
|
||||||
|
if (auto *u = buf->Undo()) {
|
||||||
|
u->Begin(UndoType::Newline);
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1017,26 +1038,27 @@ cmd_backspace(CommandContext &ctx)
|
|||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
if (x > 0) {
|
if (x > 0) {
|
||||||
// Batch contiguous character deletes (backspace)
|
// Delete character before cursor
|
||||||
if (u)
|
|
||||||
u->Begin(UndoType::Delete);
|
|
||||||
char deleted = rows[y][x - 1];
|
char deleted = rows[y][x - 1];
|
||||||
rows[y].erase(x - 1, 1);
|
rows[y].erase(x - 1, 1);
|
||||||
--x;
|
--x;
|
||||||
if (u)
|
// Record undo after deletion and cursor update
|
||||||
|
if (u) {
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
u->Append(deleted);
|
u->Append(deleted);
|
||||||
|
}
|
||||||
} else if (y > 0) {
|
} else if (y > 0) {
|
||||||
// join with previous line
|
// join with previous line
|
||||||
std::size_t prev_len = rows[y - 1].size();
|
std::size_t prev_len = rows[y - 1].size();
|
||||||
if (u) {
|
|
||||||
// Record a newline deletion that joined lines; commit immediately
|
|
||||||
u->Begin(UndoType::Newline);
|
|
||||||
u->commit();
|
|
||||||
}
|
|
||||||
rows[y - 1] += rows[y];
|
rows[y - 1] += rows[y];
|
||||||
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y));
|
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y));
|
||||||
y = y - 1;
|
y = y - 1;
|
||||||
x = prev_len;
|
x = prev_len;
|
||||||
|
// Record a newline deletion that joined lines; commit immediately
|
||||||
|
if (u) {
|
||||||
|
u->Begin(UndoType::Newline);
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// at very start; nothing to do
|
// at very start; nothing to do
|
||||||
break;
|
break;
|
||||||
@@ -1067,22 +1089,23 @@ cmd_delete_char(CommandContext &ctx)
|
|||||||
if (y >= rows.size())
|
if (y >= rows.size())
|
||||||
break;
|
break;
|
||||||
if (x < rows[y].size()) {
|
if (x < rows[y].size()) {
|
||||||
// Forward delete at cursor, batch contiguous
|
// Forward delete at cursor
|
||||||
if (u)
|
|
||||||
u->Begin(UndoType::Delete);
|
|
||||||
char deleted = rows[y][x];
|
char deleted = rows[y][x];
|
||||||
rows[y].erase(x, 1);
|
rows[y].erase(x, 1);
|
||||||
if (u)
|
// Record undo after deletion (cursor stays at same position)
|
||||||
|
if (u) {
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
u->Append(deleted);
|
u->Append(deleted);
|
||||||
|
}
|
||||||
} else if (y + 1 < rows.size()) {
|
} else if (y + 1 < rows.size()) {
|
||||||
// join next line
|
// join next line
|
||||||
|
rows[y] += rows[y + 1];
|
||||||
|
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y + 1));
|
||||||
|
// Record newline deletion at end of this line; commit immediately
|
||||||
if (u) {
|
if (u) {
|
||||||
// Record newline deletion at end of this line; commit immediately
|
|
||||||
u->Begin(UndoType::Newline);
|
u->Begin(UndoType::Newline);
|
||||||
u->commit();
|
u->commit();
|
||||||
}
|
}
|
||||||
rows[y] += rows[y + 1];
|
|
||||||
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y + 1));
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1812,6 +1835,373 @@ cmd_word_next(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_delete_word_prev(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
if (auto *u = buf->Undo())
|
||||||
|
u->commit();
|
||||||
|
ensure_at_least_one_line(*buf);
|
||||||
|
auto &rows = buf->Rows();
|
||||||
|
std::size_t y = buf->Cury();
|
||||||
|
std::size_t x = buf->Curx();
|
||||||
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
|
std::string killed_total;
|
||||||
|
for (int i = 0; i < repeat; ++i) {
|
||||||
|
if (y >= rows.size()) {
|
||||||
|
y = rows.empty() ? 0 : rows.size() - 1;
|
||||||
|
x = rows[y].size();
|
||||||
|
}
|
||||||
|
std::size_t start_y = y;
|
||||||
|
std::size_t start_x = x;
|
||||||
|
// If at start of line and not first line, move to end of previous line
|
||||||
|
if (x == 0) {
|
||||||
|
if (y == 0)
|
||||||
|
break;
|
||||||
|
--y;
|
||||||
|
x = rows[y].size();
|
||||||
|
}
|
||||||
|
// Move left one first
|
||||||
|
if (x > 0)
|
||||||
|
--x;
|
||||||
|
// Skip any whitespace leftwards
|
||||||
|
while (y < rows.size() && (x > 0 || (x == 0 && y > 0))) {
|
||||||
|
if (x == 0) {
|
||||||
|
--y;
|
||||||
|
x = rows[y].size();
|
||||||
|
if (x == 0)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
unsigned char c = x > 0 ? static_cast<unsigned char>(rows[y][x - 1]) : 0;
|
||||||
|
if (!std::isspace(c))
|
||||||
|
break;
|
||||||
|
--x;
|
||||||
|
}
|
||||||
|
// Skip word characters leftwards
|
||||||
|
while (y < rows.size() && (x > 0 || (x == 0 && y > 0))) {
|
||||||
|
if (x == 0)
|
||||||
|
break;
|
||||||
|
unsigned char c = static_cast<unsigned char>(rows[y][x - 1]);
|
||||||
|
if (!is_word_char(c))
|
||||||
|
break;
|
||||||
|
--x;
|
||||||
|
}
|
||||||
|
// Now delete from (x, y) to (start_x, start_y)
|
||||||
|
std::string deleted;
|
||||||
|
if (y == start_y) {
|
||||||
|
// same line
|
||||||
|
if (x < start_x) {
|
||||||
|
deleted = rows[y].substr(x, start_x - x);
|
||||||
|
rows[y].erase(x, start_x - x);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// spans multiple lines
|
||||||
|
// First, collect text from (x, y) to end of line y
|
||||||
|
deleted = rows[y].substr(x);
|
||||||
|
rows[y].erase(x);
|
||||||
|
// Then collect complete lines between y and start_y
|
||||||
|
for (std::size_t ly = y + 1; ly < start_y; ++ly) {
|
||||||
|
deleted += "\n";
|
||||||
|
deleted += rows[ly];
|
||||||
|
}
|
||||||
|
// Finally, collect from beginning of start_y to start_x
|
||||||
|
if (start_y < rows.size()) {
|
||||||
|
deleted += "\n";
|
||||||
|
deleted += rows[start_y].substr(0, start_x);
|
||||||
|
rows[y] += rows[start_y].substr(start_x);
|
||||||
|
// Remove lines from y+1 to start_y inclusive
|
||||||
|
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y + 1),
|
||||||
|
rows.begin() + static_cast<std::ptrdiff_t>(start_y + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Prepend to killed_total (since we're deleting backwards)
|
||||||
|
killed_total = deleted + killed_total;
|
||||||
|
}
|
||||||
|
buf->SetCursor(x, y);
|
||||||
|
buf->SetDirty(true);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
if (!killed_total.empty()) {
|
||||||
|
if (ctx.editor.KillChain())
|
||||||
|
ctx.editor.KillRingAppend(killed_total);
|
||||||
|
else
|
||||||
|
ctx.editor.KillRingPush(killed_total);
|
||||||
|
ctx.editor.SetKillChain(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_delete_word_next(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
if (auto *u = buf->Undo())
|
||||||
|
u->commit();
|
||||||
|
ensure_at_least_one_line(*buf);
|
||||||
|
auto &rows = buf->Rows();
|
||||||
|
std::size_t y = buf->Cury();
|
||||||
|
std::size_t x = buf->Curx();
|
||||||
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
|
std::string killed_total;
|
||||||
|
for (int i = 0; i < repeat; ++i) {
|
||||||
|
if (y >= rows.size())
|
||||||
|
break;
|
||||||
|
std::size_t start_y = y;
|
||||||
|
std::size_t start_x = x;
|
||||||
|
// First, if currently on a word, skip to its end
|
||||||
|
while (y < rows.size()) {
|
||||||
|
if (x < rows[y].size() && is_word_char(static_cast<unsigned char>(rows[y][x]))) {
|
||||||
|
++x;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (x >= rows[y].size()) {
|
||||||
|
if (y + 1 >= rows.size())
|
||||||
|
break;
|
||||||
|
++y;
|
||||||
|
x = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Then, skip any non-word characters (including punctuation and whitespace)
|
||||||
|
while (y < rows.size()) {
|
||||||
|
if (x < rows[y].size()) {
|
||||||
|
unsigned char c = static_cast<unsigned char>(rows[y][x]);
|
||||||
|
if (is_word_char(c))
|
||||||
|
break;
|
||||||
|
++x;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (x >= rows[y].size()) {
|
||||||
|
if (y + 1 >= rows.size())
|
||||||
|
break;
|
||||||
|
++y;
|
||||||
|
x = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Now delete from (start_x, start_y) to (x, y)
|
||||||
|
std::string deleted;
|
||||||
|
if (start_y == y) {
|
||||||
|
// same line
|
||||||
|
if (start_x < x) {
|
||||||
|
deleted = rows[y].substr(start_x, x - start_x);
|
||||||
|
rows[y].erase(start_x, x - start_x);
|
||||||
|
x = start_x;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// spans multiple lines
|
||||||
|
// First, collect text from start_x to end of line start_y
|
||||||
|
deleted = rows[start_y].substr(start_x);
|
||||||
|
rows[start_y].erase(start_x);
|
||||||
|
// Then collect complete lines between start_y and y
|
||||||
|
for (std::size_t ly = start_y + 1; ly < y; ++ly) {
|
||||||
|
deleted += "\n";
|
||||||
|
deleted += rows[ly];
|
||||||
|
}
|
||||||
|
// Finally, collect from beginning of y to x
|
||||||
|
if (y < rows.size()) {
|
||||||
|
deleted += "\n";
|
||||||
|
deleted += rows[y].substr(0, x);
|
||||||
|
rows[start_y] += rows[y].substr(x);
|
||||||
|
// Remove lines from start_y+1 to y inclusive
|
||||||
|
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(start_y + 1),
|
||||||
|
rows.begin() + static_cast<std::ptrdiff_t>(y + 1));
|
||||||
|
}
|
||||||
|
y = start_y;
|
||||||
|
x = start_x;
|
||||||
|
}
|
||||||
|
killed_total += deleted;
|
||||||
|
}
|
||||||
|
buf->SetCursor(x, y);
|
||||||
|
buf->SetDirty(true);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
if (!killed_total.empty()) {
|
||||||
|
if (ctx.editor.KillChain())
|
||||||
|
ctx.editor.KillRingAppend(killed_total);
|
||||||
|
else
|
||||||
|
ctx.editor.KillRingPush(killed_total);
|
||||||
|
ctx.editor.SetKillChain(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_indent_region(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
if (!buf->MarkSet()) {
|
||||||
|
ctx.editor.SetStatus("No mark set");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::size_t sx, sy, ex, ey;
|
||||||
|
if (!compute_mark_region(*buf, sx, sy, ex, ey)) {
|
||||||
|
ctx.editor.SetStatus("No region to indent");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto &rows = buf->Rows();
|
||||||
|
for (std::size_t y = sy; y <= ey && y < rows.size(); ++y) {
|
||||||
|
rows[y].insert(0, "\t");
|
||||||
|
}
|
||||||
|
buf->SetDirty(true);
|
||||||
|
buf->ClearMark();
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_unindent_region(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
if (!buf->MarkSet()) {
|
||||||
|
ctx.editor.SetStatus("No mark set");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::size_t sx, sy, ex, ey;
|
||||||
|
if (!compute_mark_region(*buf, sx, sy, ex, ey)) {
|
||||||
|
ctx.editor.SetStatus("No region to unindent");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto &rows = buf->Rows();
|
||||||
|
for (std::size_t y = sy; y <= ey && y < rows.size(); ++y) {
|
||||||
|
auto &line = rows[y];
|
||||||
|
if (!line.empty()) {
|
||||||
|
if (line[0] == '\t') {
|
||||||
|
line.erase(0, 1);
|
||||||
|
} else if (line[0] == ' ') {
|
||||||
|
std::size_t spaces = 0;
|
||||||
|
while (spaces < line.size() && spaces < 8 && line[spaces] == ' ') {
|
||||||
|
++spaces;
|
||||||
|
}
|
||||||
|
if (spaces > 0)
|
||||||
|
line.erase(0, spaces);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf->SetDirty(true);
|
||||||
|
buf->ClearMark();
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_reflow_paragraph(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
ensure_at_least_one_line(*buf);
|
||||||
|
auto &rows = buf->Rows();
|
||||||
|
std::size_t y = buf->Cury();
|
||||||
|
int width = ctx.count > 0 ? ctx.count : 72;
|
||||||
|
std::size_t para_start = y;
|
||||||
|
while (para_start > 0 && !rows[para_start - 1].empty())
|
||||||
|
--para_start;
|
||||||
|
std::size_t para_end = y;
|
||||||
|
while (para_end + 1 < rows.size() && !rows[para_end + 1].empty())
|
||||||
|
++para_end;
|
||||||
|
if (para_start > para_end)
|
||||||
|
return false;
|
||||||
|
std::string text;
|
||||||
|
for (std::size_t i = para_start; i <= para_end; ++i) {
|
||||||
|
if (i > para_start && !text.empty() && text.back() != ' ')
|
||||||
|
text += ' ';
|
||||||
|
const auto &line = rows[i];
|
||||||
|
for (std::size_t j = 0; j < line.size(); ++j) {
|
||||||
|
char c = line[j];
|
||||||
|
if (c == '\t')
|
||||||
|
text += ' ';
|
||||||
|
else
|
||||||
|
text += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::vector<std::string> new_lines;
|
||||||
|
std::string line;
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while (pos < text.size()) {
|
||||||
|
while (pos < text.size() && text[pos] == ' ')
|
||||||
|
++pos;
|
||||||
|
if (pos >= text.size())
|
||||||
|
break;
|
||||||
|
std::size_t word_start = pos;
|
||||||
|
while (pos < text.size() && text[pos] != ' ')
|
||||||
|
++pos;
|
||||||
|
std::string word = text.substr(word_start, pos - word_start);
|
||||||
|
if (line.empty()) {
|
||||||
|
line = word;
|
||||||
|
} else if (static_cast<int>(line.size() + 1 + word.size()) <= width) {
|
||||||
|
line += ' ';
|
||||||
|
line += word;
|
||||||
|
} else {
|
||||||
|
new_lines.push_back(line);
|
||||||
|
line = word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!line.empty())
|
||||||
|
new_lines.push_back(line);
|
||||||
|
if (new_lines.empty())
|
||||||
|
new_lines.push_back("");
|
||||||
|
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(para_start),
|
||||||
|
rows.begin() + static_cast<std::ptrdiff_t>(para_end + 1));
|
||||||
|
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(para_start),
|
||||||
|
new_lines.begin(), new_lines.end());
|
||||||
|
buf->SetCursor(0, para_start);
|
||||||
|
buf->SetDirty(true);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_reload_buffer(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
const std::string &filename = buf->Filename();
|
||||||
|
if (filename.empty()) {
|
||||||
|
ctx.editor.SetStatus("Cannot reload unnamed buffer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::string err;
|
||||||
|
if (!buf->OpenFromFile(filename, err)) {
|
||||||
|
ctx.editor.SetStatus(std::string("Reload failed: ") + err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ctx.editor.SetStatus(std::string("Reloaded ") + filename);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_mark_all_and_jump_end(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
ensure_at_least_one_line(*buf);
|
||||||
|
buf->SetMark(0, 0);
|
||||||
|
auto &rows = buf->Rows();
|
||||||
|
std::size_t last_y = rows.empty() ? 0 : rows.size() - 1;
|
||||||
|
std::size_t last_x = last_y < rows.size() ? rows[last_y].size() : 0;
|
||||||
|
buf->SetCursor(last_x, last_y);
|
||||||
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
std::vector<Command> &
|
std::vector<Command> &
|
||||||
CommandRegistry::storage_()
|
CommandRegistry::storage_()
|
||||||
{
|
{
|
||||||
@@ -1927,12 +2317,33 @@ InstallDefaultCommands()
|
|||||||
CommandRegistry::Register({CommandId::PageDown, "page-down", "Page down", cmd_page_down});
|
CommandRegistry::Register({CommandId::PageDown, "page-down", "Page down", cmd_page_down});
|
||||||
CommandRegistry::Register({CommandId::WordPrev, "word-prev", "Move to previous word", cmd_word_prev});
|
CommandRegistry::Register({CommandId::WordPrev, "word-prev", "Move to previous word", cmd_word_prev});
|
||||||
CommandRegistry::Register({CommandId::WordNext, "word-next", "Move to next word", cmd_word_next});
|
CommandRegistry::Register({CommandId::WordNext, "word-next", "Move to next word", cmd_word_next});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::DeleteWordPrev, "delete-word-prev", "Delete previous word", cmd_delete_word_prev
|
||||||
|
});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::DeleteWordNext, "delete-word-next", "Delete next word", cmd_delete_word_next
|
||||||
|
});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::MoveCursorTo, "move-cursor-to", "Move cursor to y:x", cmd_move_cursor_to
|
CommandId::MoveCursorTo, "move-cursor-to", "Move cursor to y:x", cmd_move_cursor_to
|
||||||
});
|
});
|
||||||
// Undo/Redo
|
// Undo/Redo
|
||||||
CommandRegistry::Register({CommandId::Undo, "undo", "Undo last edit", cmd_undo});
|
CommandRegistry::Register({CommandId::Undo, "undo", "Undo last edit", cmd_undo});
|
||||||
CommandRegistry::Register({CommandId::Redo, "redo", "Redo edit", cmd_redo});
|
CommandRegistry::Register({CommandId::Redo, "redo", "Redo edit", cmd_redo});
|
||||||
|
// Region formatting
|
||||||
|
CommandRegistry::Register({CommandId::IndentRegion, "indent-region", "Indent region", cmd_indent_region});
|
||||||
|
CommandRegistry::Register(
|
||||||
|
{CommandId::UnindentRegion, "unindent-region", "Unindent region", cmd_unindent_region});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::ReflowParagraph, "reflow-paragraph", "Reflow paragraph to column width", cmd_reflow_paragraph
|
||||||
|
});
|
||||||
|
// Buffer operations
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer
|
||||||
|
});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::MarkAllAndJumpEnd, "mark-all-jump-end", "Set mark at beginning and jump to end",
|
||||||
|
cmd_mark_all_and_jump_end
|
||||||
|
});
|
||||||
// UI helpers
|
// UI helpers
|
||||||
CommandRegistry::Register(
|
CommandRegistry::Register(
|
||||||
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
|
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
|
||||||
@@ -1952,7 +2363,7 @@ Execute(Editor &ed, CommandId id, const std::string &arg, int count)
|
|||||||
}
|
}
|
||||||
// Reset kill chain unless this is a kill-like command (so consecutive kills append)
|
// Reset kill chain unless this is a kill-like command (so consecutive kills append)
|
||||||
if (id != CommandId::KillToEOL && id != CommandId::KillLine && id != CommandId::KillRegion && id !=
|
if (id != CommandId::KillToEOL && id != CommandId::KillLine && id != CommandId::KillRegion && id !=
|
||||||
CommandId::CopyRegion) {
|
CommandId::CopyRegion && id != CommandId::DeleteWordPrev && id != CommandId::DeleteWordNext) {
|
||||||
ed.SetKillChain(false);
|
ed.SetKillChain(false);
|
||||||
}
|
}
|
||||||
CommandContext ctx{ed, arg, count};
|
CommandContext ctx{ed, arg, count};
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ enum class CommandId {
|
|||||||
PageDown,
|
PageDown,
|
||||||
WordPrev,
|
WordPrev,
|
||||||
WordNext,
|
WordNext,
|
||||||
|
DeleteWordPrev, // delete previous word (ESC BACKSPACE)
|
||||||
|
DeleteWordNext, // delete next word (ESC d)
|
||||||
// Direct cursor placement
|
// Direct cursor placement
|
||||||
MoveCursorTo, // arg: "y:x" (zero-based row:col)
|
MoveCursorTo, // arg: "y:x" (zero-based row:col)
|
||||||
// Undo/Redo
|
// Undo/Redo
|
||||||
@@ -63,6 +65,13 @@ 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
|
||||||
|
// Region formatting
|
||||||
|
IndentRegion, // indent region (C-k =)
|
||||||
|
UnindentRegion, // unindent region (C-k -)
|
||||||
|
ReflowParagraph, // reflow paragraph to column width (ESC q)
|
||||||
|
// Buffer operations
|
||||||
|
ReloadBuffer, // reload buffer from disk (C-k l)
|
||||||
|
MarkAllAndJumpEnd, // set mark at beginning, jump to end (C-k a)
|
||||||
// Meta
|
// Meta
|
||||||
UnknownKCommand, // arg: single character that was not recognized after C-k
|
UnknownKCommand, // arg: single character that was not recognized after C-k
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
height_ = h;
|
height_ = h;
|
||||||
|
|
||||||
// Initialize GUI font from embedded default
|
// Initialize GUI font from embedded default
|
||||||
LoadGuiFont_(nullptr, 16.f);
|
#ifndef KTE_FONT_SIZE
|
||||||
|
#define KTE_FONT_SIZE 16.0f
|
||||||
|
#endif
|
||||||
|
LoadGuiFont_(nullptr, (float) KTE_FONT_SIZE);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -447,8 +447,8 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
// Unknown k-command via TEXTINPUT path
|
// Unknown k-command via TEXTINPUT path
|
||||||
int shown = KLowerAscii(ascii_key);
|
int shown = KLowerAscii(ascii_key);
|
||||||
char c = (shown >= 0x20 && shown <= 0x7e)
|
char c = (shown >= 0x20 && shown <= 0x7e)
|
||||||
? static_cast<char>(shown)
|
? static_cast<char>(shown)
|
||||||
: '?';
|
: '?';
|
||||||
std::string arg(1, c);
|
std::string arg(1, c);
|
||||||
mi = {true, CommandId::UnknownKCommand, arg, 0};
|
mi = {true, CommandId::UnknownKCommand, arg, 0};
|
||||||
produced = true;
|
produced = true;
|
||||||
|
|||||||
28
KKeymap.cc
28
KKeymap.cc
@@ -1,4 +1,5 @@
|
|||||||
#include "KKeymap.h"
|
#include "KKeymap.h"
|
||||||
|
#include <ncurses.h>
|
||||||
|
|
||||||
|
|
||||||
auto
|
auto
|
||||||
@@ -27,8 +28,8 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2) Case-sensitive bindings must be checked before case-insensitive table.
|
// 2) Case-sensitive bindings must be checked before case-insensitive table.
|
||||||
if (ascii_key == 'U') {
|
if (ascii_key == 'r') {
|
||||||
out = CommandId::Redo; // C-k U (redo)
|
out = CommandId::Redo; // C-k r (redo)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +74,18 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
case 'u':
|
case 'u':
|
||||||
out = CommandId::Undo;
|
out = CommandId::Undo;
|
||||||
return true; // C-k u (undo)
|
return true; // C-k u (undo)
|
||||||
|
case '-':
|
||||||
|
out = CommandId::UnindentRegion;
|
||||||
|
return true; // C-k - (unindent region)
|
||||||
|
case '=':
|
||||||
|
out = CommandId::IndentRegion;
|
||||||
|
return true; // C-k = (indent region)
|
||||||
|
case 'l':
|
||||||
|
out = CommandId::ReloadBuffer;
|
||||||
|
return true; // C-k l (reload buffer)
|
||||||
|
case 'a':
|
||||||
|
out = CommandId::MarkAllAndJumpEnd;
|
||||||
|
return true; // C-k a (mark all and jump to end)
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -135,6 +148,11 @@ auto
|
|||||||
KLookupEscCommand(const int ascii_key, CommandId &out) -> bool
|
KLookupEscCommand(const int ascii_key, CommandId &out) -> bool
|
||||||
{
|
{
|
||||||
const int k = KLowerAscii(ascii_key);
|
const int k = KLowerAscii(ascii_key);
|
||||||
|
// Handle KEY_BACKSPACE (ESC BACKSPACE, Alt-Backspace)
|
||||||
|
if (ascii_key == KEY_BACKSPACE) {
|
||||||
|
out = CommandId::DeleteWordPrev;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case '<':
|
case '<':
|
||||||
out = CommandId::MoveFileStart; // Esc <
|
out = CommandId::MoveFileStart; // Esc <
|
||||||
@@ -154,6 +172,12 @@ KLookupEscCommand(const int ascii_key, CommandId &out) -> bool
|
|||||||
case 'f':
|
case 'f':
|
||||||
out = CommandId::WordNext;
|
out = CommandId::WordNext;
|
||||||
return true;
|
return true;
|
||||||
|
case 'd':
|
||||||
|
out = CommandId::DeleteWordNext; // Esc d (Alt-d)
|
||||||
|
return true;
|
||||||
|
case 'q':
|
||||||
|
out = CommandId::ReflowParagraph; // Esc q (reflow paragraph)
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,19 +199,16 @@ map_key_to_command(const int ch,
|
|||||||
out = {true, CommandId::Newline, "", 0};
|
out = {true, CommandId::Newline, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Backspace in ncurses can be KEY_BACKSPACE or 127
|
|
||||||
if (ch == KEY_BACKSPACE || ch == 127 || ch == CTRL('H')) {
|
|
||||||
k_prefix = false;
|
|
||||||
out = {true, CommandId::Backspace, "", 0};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If previous key was ESC, interpret as meta and use ESC keymap
|
// If previous key was ESC, interpret as meta and use ESC keymap
|
||||||
if (esc_meta) {
|
if (esc_meta) {
|
||||||
esc_meta = false;
|
esc_meta = false;
|
||||||
int ascii_key = ch;
|
int ascii_key = ch;
|
||||||
if (ascii_key >= 'A' && ascii_key <= 'Z')
|
// Handle ESC + BACKSPACE (meta-backspace, Alt-Backspace)
|
||||||
|
if (ch == KEY_BACKSPACE || ch == 127 || ch == CTRL('H')) {
|
||||||
|
ascii_key = KEY_BACKSPACE; // normalized value for lookup
|
||||||
|
} else if (ascii_key >= 'A' && ascii_key <= 'Z') {
|
||||||
ascii_key = ascii_key - 'A' + 'a';
|
ascii_key = ascii_key - 'A' + 'a';
|
||||||
|
}
|
||||||
CommandId id;
|
CommandId id;
|
||||||
if (KLookupEscCommand(ascii_key, id)) {
|
if (KLookupEscCommand(ascii_key, id)) {
|
||||||
out = {true, id, "", 0};
|
out = {true, id, "", 0};
|
||||||
@@ -222,6 +219,13 @@ map_key_to_command(const int ch,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backspace in ncurses can be KEY_BACKSPACE or 127
|
||||||
|
if (ch == KEY_BACKSPACE || ch == 127 || ch == CTRL('H')) {
|
||||||
|
k_prefix = false;
|
||||||
|
out = {true, CommandId::Backspace, "", 0};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// k_prefix handled earlier
|
// k_prefix handled earlier
|
||||||
|
|
||||||
// If collecting universal arg, handle digits and optional leading '-'
|
// If collecting universal arg, handle digits and optional leading '-'
|
||||||
|
|||||||
34
TestFrontend.cc
Normal file
34
TestFrontend.cc
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#include "TestFrontend.h"
|
||||||
|
#include "Editor.h"
|
||||||
|
#include "Command.h"
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
TestFrontend::Init(Editor &ed)
|
||||||
|
{
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TestFrontend::Step(Editor &ed, bool &running)
|
||||||
|
{
|
||||||
|
MappedInput mi;
|
||||||
|
if (input_.Poll(mi)) {
|
||||||
|
if (mi.hasCommand) {
|
||||||
|
Execute(ed, mi.id, mi.arg, mi.count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ed.QuitRequested()) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer_.Draw(ed);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TestFrontend::Shutdown() {}
|
||||||
38
TestInputHandler.cc
Normal file
38
TestInputHandler.cc
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#include "TestInputHandler.h"
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
TestInputHandler::Poll(MappedInput &out)
|
||||||
|
{
|
||||||
|
if (queue_.empty())
|
||||||
|
return false;
|
||||||
|
out = queue_.front();
|
||||||
|
queue_.pop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TestInputHandler::QueueCommand(CommandId id, const std::string &arg, int count)
|
||||||
|
{
|
||||||
|
MappedInput mi;
|
||||||
|
mi.hasCommand = true;
|
||||||
|
mi.id = id;
|
||||||
|
mi.arg = arg;
|
||||||
|
mi.count = count;
|
||||||
|
queue_.push(mi);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TestInputHandler::QueueText(const std::string &text)
|
||||||
|
{
|
||||||
|
for (char ch: text) {
|
||||||
|
MappedInput mi;
|
||||||
|
mi.hasCommand = true;
|
||||||
|
mi.id = CommandId::InsertText;
|
||||||
|
mi.arg = std::string(1, ch);
|
||||||
|
mi.count = 0;
|
||||||
|
queue_.push(mi);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
TestRenderer.cc
Normal file
10
TestRenderer.cc
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#include "TestRenderer.h"
|
||||||
|
#include "Editor.h"
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
TestRenderer::Draw(Editor &ed)
|
||||||
|
{
|
||||||
|
(void) ed;
|
||||||
|
++draw_count_;
|
||||||
|
}
|
||||||
@@ -3,15 +3,15 @@
|
|||||||
|
|
||||||
|
|
||||||
UndoSystem::UndoSystem(Buffer &owner, UndoTree &tree)
|
UndoSystem::UndoSystem(Buffer &owner, UndoTree &tree)
|
||||||
: buf_(owner), tree_(tree) {}
|
: buf_(&owner), tree_(tree) {}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
UndoSystem::Begin(UndoType type)
|
UndoSystem::Begin(UndoType type)
|
||||||
{
|
{
|
||||||
// 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)
|
||||||
@@ -187,30 +187,30 @@ UndoSystem::apply(const UndoNode *node, int direction)
|
|||||||
case UndoType::Insert:
|
case UndoType::Insert:
|
||||||
case UndoType::Paste:
|
case UndoType::Paste:
|
||||||
if (direction > 0) {
|
if (direction > 0) {
|
||||||
buf_.insert_text(node->row, node->col, node->text);
|
buf_->insert_text(node->row, node->col, node->text);
|
||||||
} else {
|
} else {
|
||||||
buf_.delete_text(node->row, node->col, node->text.size());
|
buf_->delete_text(node->row, node->col, node->text.size());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case UndoType::Delete:
|
case UndoType::Delete:
|
||||||
if (direction > 0) {
|
if (direction > 0) {
|
||||||
buf_.delete_text(node->row, node->col, node->text.size());
|
buf_->delete_text(node->row, node->col, node->text.size());
|
||||||
} else {
|
} else {
|
||||||
buf_.insert_text(node->row, node->col, node->text);
|
buf_->insert_text(node->row, node->col, node->text);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case UndoType::Newline:
|
case UndoType::Newline:
|
||||||
if (direction > 0) {
|
if (direction > 0) {
|
||||||
buf_.split_line(node->row, node->col);
|
buf_->split_line(node->row, node->col);
|
||||||
} else {
|
} else {
|
||||||
buf_.join_lines(node->row);
|
buf_->join_lines(node->row);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case UndoType::DeleteRow:
|
case UndoType::DeleteRow:
|
||||||
if (direction > 0) {
|
if (direction > 0) {
|
||||||
buf_.delete_row(node->row);
|
buf_->delete_row(node->row);
|
||||||
} else {
|
} else {
|
||||||
buf_.insert_row(node->row, node->text);
|
buf_->insert_row(node->row, node->text);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -284,5 +284,12 @@ UndoSystem::update_dirty_flag()
|
|||||||
{
|
{
|
||||||
// dirty if current != saved
|
// dirty if current != saved
|
||||||
bool dirty = (tree_.current != tree_.saved);
|
bool dirty = (tree_.current != tree_.saved);
|
||||||
buf_.SetDirty(dirty);
|
buf_->SetDirty(dirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
UndoSystem::UpdateBufferReference(Buffer &new_buf)
|
||||||
|
{
|
||||||
|
buf_ = &new_buf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ public:
|
|||||||
|
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
|
void UpdateBufferReference(Buffer &new_buf);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void apply(const UndoNode *node, int direction); // +1 redo, -1 undo
|
void apply(const UndoNode *node, int direction); // +1 redo, -1 undo
|
||||||
void free_node(UndoNode *node);
|
void free_node(UndoNode *node);
|
||||||
@@ -38,7 +40,7 @@ private:
|
|||||||
void update_dirty_flag();
|
void update_dirty_flag();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Buffer &buf_;
|
Buffer *buf_;
|
||||||
UndoTree &tree_;
|
UndoTree &tree_;
|
||||||
// Internal hint for Delete batching: whether next Append() should prepend
|
// Internal hint for Delete batching: whether next Append() should prepend
|
||||||
bool pending_prepend_ = false;
|
bool pending_prepend_ = false;
|
||||||
|
|||||||
105
docs/TestFrontend.md
Normal file
105
docs/TestFrontend.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# TestFrontend - Headless Frontend for Testing
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`TestFrontend` is a headless implementation of the `Frontend` interface designed to facilitate programmatic testing of editor features. It allows you to queue commands and text input manually, execute them step-by-step, and inspect the editor/buffer state.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### TestInputHandler
|
||||||
|
A programmable input handler that uses a queue-based system:
|
||||||
|
- `QueueCommand(CommandId id, const std::string &arg = "", int count = 0)` - Queue a specific command
|
||||||
|
- `QueueText(const std::string &text)` - Queue text for insertion (character by character)
|
||||||
|
- `Poll(MappedInput &out)` - Returns queued commands one at a time
|
||||||
|
- `IsEmpty()` - Check if the input queue is empty
|
||||||
|
|
||||||
|
### TestRenderer
|
||||||
|
A minimal no-op renderer for testing:
|
||||||
|
- `Draw(Editor &ed)` - No-op implementation, just increments draw counter
|
||||||
|
- `GetDrawCount()` - Returns the number of times Draw() was called
|
||||||
|
- `ResetDrawCount()` - Resets the draw counter
|
||||||
|
|
||||||
|
### TestFrontend
|
||||||
|
The main frontend class that integrates TestInputHandler and TestRenderer:
|
||||||
|
- `Init(Editor &ed)` - Initializes the frontend (sets editor dimensions to 24x80)
|
||||||
|
- `Step(Editor &ed, bool &running)` - Processes one command from the queue and renders
|
||||||
|
- `Shutdown()` - Cleanup (no-op for TestFrontend)
|
||||||
|
- `Input()` - Access the TestInputHandler
|
||||||
|
- `Renderer()` - Access the TestRenderer
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "Editor.h"
|
||||||
|
#include "TestFrontend.h"
|
||||||
|
#include "Command.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// IMPORTANT: Install default commands first!
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor editor;
|
||||||
|
TestFrontend frontend;
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
frontend.Init(editor);
|
||||||
|
|
||||||
|
// Setup: create a buffer (open a file or add buffer)
|
||||||
|
std::string err;
|
||||||
|
if (!editor.OpenFile("/tmp/test.txt", err)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue some commands
|
||||||
|
frontend.Input().QueueText("Hello");
|
||||||
|
frontend.Input().QueueCommand(CommandId::Newline);
|
||||||
|
frontend.Input().QueueText("World");
|
||||||
|
|
||||||
|
// Execute queued commands
|
||||||
|
bool running = true;
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inspect results
|
||||||
|
Buffer *buf = editor.CurrentBuffer();
|
||||||
|
if (buf && buf->Rows().size() >= 2) {
|
||||||
|
std::cout << "Line 1: " << std::string(buf->Rows()[0]) << "\n";
|
||||||
|
std::cout << "Line 2: " << std::string(buf->Rows()[1]) << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
frontend.Shutdown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
1. **Programmable Input**: Queue any sequence of commands or text programmatically
|
||||||
|
2. **Step-by-Step Execution**: Run the editor one command at a time
|
||||||
|
3. **State Inspection**: Access and verify editor/buffer state between commands
|
||||||
|
4. **No UI Dependencies**: Headless operation, no terminal or GUI required
|
||||||
|
5. **Integration Testing**: Test command sequences, undo/redo, multi-line editing, etc.
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
All commands from `CommandId` enum can be queued, including:
|
||||||
|
- `CommandId::InsertText` - Insert text (use `QueueText()` helper)
|
||||||
|
- `CommandId::Newline` - Insert newline
|
||||||
|
- `CommandId::Backspace` - Delete character before cursor
|
||||||
|
- `CommandId::DeleteChar` - Delete character at cursor
|
||||||
|
- `CommandId::MoveLeft`, `MoveRight`, `MoveUp`, `MoveDown` - Cursor movement
|
||||||
|
- `CommandId::Undo`, `CommandId::Redo` - Undo/redo operations
|
||||||
|
- `CommandId::Save`, `CommandId::Quit` - File operations
|
||||||
|
- And many more (see Command.h)
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
TestFrontend is built into both `kte` and `kge` executables as part of the common source files. You can create standalone test programs by linking against the same source files and ncurses.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Always call `InstallDefaultCommands()` before using any commands
|
||||||
|
- Buffer must be initialized (via `OpenFile()` or `AddBuffer()`) before queuing edit commands
|
||||||
|
- Undo/redo requires the buffer to have an UndoSystem attached
|
||||||
|
- The test frontend sets editor dimensions to 24x80 by default
|
||||||
12
docs/ke.md
12
docs/ke.md
@@ -14,7 +14,7 @@ someone's writeup of the process of writing a text editor from scratch.
|
|||||||
It has keybindings inspired by VDE (and the Wordstar family) and emacs;
|
It has keybindings inspired by VDE (and the Wordstar family) and emacs;
|
||||||
its spiritual parent is mg(1).
|
its spiritual parent is mg(1).
|
||||||
|
|
||||||
## KEYBINDINGS
|
## KEYBINDINGS
|
||||||
|
|
||||||
K-command mode is entered using C-k. This is taken from
|
K-command mode is entered using C-k. This is taken from
|
||||||
Wordstar and just so happens to be blessed with starting with a most
|
Wordstar and just so happens to be blessed with starting with a most
|
||||||
@@ -30,6 +30,8 @@ k-command mode can be exited with ESC or C-g.
|
|||||||
* C-k SPACE: Toggle the mark.
|
* C-k SPACE: Toggle the mark.
|
||||||
* C-k -: If the mark is set, unindent the region.
|
* C-k -: If the mark is set, unindent the region.
|
||||||
* C-k =: If the mark is set, indent the region.
|
* C-k =: If the mark is set, indent the region.
|
||||||
|
* C-k a: Set the mark at the beginning of the file, then jump to the end of
|
||||||
|
the file.
|
||||||
* C-k b: Switch to a buffer.
|
* C-k b: Switch to a buffer.
|
||||||
* C-k c: Close the current buffer. If no other buffers are open, an empty
|
* C-k c: Close the current buffer. If no other buffers are open, an empty
|
||||||
buffer will be opened. To exit, use C-k q.
|
buffer will be opened. To exit, use C-k q.
|
||||||
@@ -39,16 +41,16 @@ k-command mode can be exited with ESC or C-g.
|
|||||||
* C-k f: Flush the kill ring.
|
* C-k f: Flush the kill ring.
|
||||||
* C-k g: Go to a specific line.
|
* C-k g: Go to a specific line.
|
||||||
* C-k j: Jump to the mark.
|
* C-k j: Jump to the mark.
|
||||||
* C-k l: List the number of lines of code in a saved file.
|
* C-k l: Reload the current buffer from disk.
|
||||||
* C-k m: Run make(1), reporting success or failure.
|
* C-k m: Run make(1), reporting success or failure.
|
||||||
* C-k p: Switch to the next buffer.
|
* C-k p: Switch to the next buffer.
|
||||||
* C-k q: Exit the editor. If the file has unsaved changes, a warning will
|
* C-k q: Exit the editor. If the file has unsaved changes, a warning will
|
||||||
be printed; a second C-k q will exit.
|
be printed; a second C-k q will exit.
|
||||||
* C-k C-q: Immediately exit the editor.
|
* C-k C-q: Immediately exit the editor.
|
||||||
* C-k C-r: Reload the current buffer from disk.
|
* C-k l: Reload the current buffer from disk.
|
||||||
* C-k s: Save the file, prompting for a filename if needed.
|
* C-k s: Save the file, prompting for a filename if needed.
|
||||||
* C-k u: Undo.
|
* C-k u: Undo.
|
||||||
* C-k U: Redo changes (not implemented; marking this k-command as taken).
|
* C-k r: Redo changes.
|
||||||
* C-k x: save the file and exit. Also C-k C-x.
|
* C-k x: save the file and exit. Also C-k C-x.
|
||||||
* C-k y: Yank the kill ring.
|
* C-k y: Yank the kill ring.
|
||||||
* C-k \\: Dump core.
|
* C-k \\: Dump core.
|
||||||
@@ -67,6 +69,8 @@ k-command mode can be exited with ESC or C-g.
|
|||||||
* ESC b: Move to the previous word.
|
* ESC b: Move to the previous word.
|
||||||
* ESC d: Delete the next word.
|
* ESC d: Delete the next word.
|
||||||
* ESC f: Move to the next word.
|
* ESC f: Move to the next word.
|
||||||
|
* ESC q: Reflow the paragraph to 72 columns or the value of the universal
|
||||||
|
argument.
|
||||||
* ESC w: Save the region (if the mark is set) to the kill ring.
|
* ESC w: Save the region (if the mark is set) to the kill ring.
|
||||||
|
|
||||||
## FIND
|
## FIND
|
||||||
|
|||||||
@@ -93,3 +93,36 @@ Owner pointers & file locations
|
|||||||
- Undo batching entry points: Command.cc (cmd_insert_text, cmd_backspace, cmd_delete_char, cmd_newline)
|
- Undo batching entry points: Command.cc (cmd_insert_text, cmd_backspace, cmd_delete_char, cmd_newline)
|
||||||
|
|
||||||
End of snapshot — safe to resume from here.
|
End of snapshot — safe to resume from here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
RESOLUTION (2025-11-30)
|
||||||
|
|
||||||
|
Root Cause Identified and Fixed
|
||||||
|
The undo system failure was caused by incorrect timing of UndoSystem::Begin() and Append() calls relative to buffer modifications in Command.cc.
|
||||||
|
|
||||||
|
Problem:
|
||||||
|
- In cmd_insert_text, cmd_backspace, cmd_delete_char, and cmd_newline, the undo recording (Begin/Append) was called BEFORE the actual buffer modification and cursor update.
|
||||||
|
- UndoSystem::Begin() checks the current cursor position to determine if it can batch with the pending node.
|
||||||
|
- For Insert type: Begin() checks if col == pending->col + pending->text.size()
|
||||||
|
- For Delete type: Begin() checks if the cursor is at the expected position based on whether it's forward delete or backspace
|
||||||
|
- When Begin/Append were called before cursor updates, the batching condition would fail on the second character because the cursor hadn't moved yet from the first insertion.
|
||||||
|
- This caused each character to create a separate batch, but since commit() was never called between characters (only at k-prefix or undo), the pending node would be overwritten rather than committed, resulting in no undo history.
|
||||||
|
|
||||||
|
Fix Applied:
|
||||||
|
- cmd_insert_text: Moved Begin/Append to AFTER buffer insertion (lines 854-856) and cursor update (line 857).
|
||||||
|
- cmd_backspace: Moved Begin/Append to AFTER character deletion (lines 1024-1025) and cursor decrement (line 1026).
|
||||||
|
- cmd_delete_char: Moved Begin/Append to AFTER character deletion (lines 1074-1076).
|
||||||
|
- cmd_newline: Moved Begin/commit to AFTER line split (lines 956-966) and cursor update (lines 963-967).
|
||||||
|
|
||||||
|
Result:
|
||||||
|
- Begin() now sees the correct cursor position after each edit, allowing proper batching of consecutive characters.
|
||||||
|
- Typing "Hello" will now create a single pending batch with all 5 characters that can be undone as one unit.
|
||||||
|
- The fix applies to both terminal (kte) and GUI (kge) builds.
|
||||||
|
|
||||||
|
Testing Recommendation:
|
||||||
|
- Type several characters (e.g., "Hello")
|
||||||
|
- Press C-k u to undo - the entire word should disappear
|
||||||
|
- Press C-k U to redo - the word should reappear
|
||||||
|
- Test backspace batching: type several characters, then backspace multiple times, then undo - should undo the backspace batch
|
||||||
|
- Test delete batching similarly
|
||||||
|
|||||||
7331
fonts/brassmono.h
7331
fonts/brassmono.h
File diff suppressed because it is too large
Load Diff
102
test_undo.cc
Normal file
102
test_undo.cc
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#include <iostream>
|
||||||
|
#include <cassert>
|
||||||
|
#include <fstream>
|
||||||
|
#include "Editor.h"
|
||||||
|
#include "TestFrontend.h"
|
||||||
|
#include "Command.h"
|
||||||
|
#include "Buffer.h"
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
main()
|
||||||
|
{
|
||||||
|
// Install default commands
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor editor;
|
||||||
|
TestFrontend frontend;
|
||||||
|
|
||||||
|
// Initialize frontend
|
||||||
|
if (!frontend.Init(editor)) {
|
||||||
|
std::cerr << "Failed to initialize frontend\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary test file
|
||||||
|
std::string err;
|
||||||
|
const char *tmpfile = "/tmp/kte_test_undo.txt";
|
||||||
|
{
|
||||||
|
std::ofstream f(tmpfile);
|
||||||
|
if (!f) {
|
||||||
|
std::cerr << "Failed to create temp file\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
f << "\n"; // Write one newline so file isn't empty
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editor.OpenFile(tmpfile, err)) {
|
||||||
|
std::cerr << "Failed to open test file: " << err << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer *buf = editor.CurrentBuffer();
|
||||||
|
assert(buf != nullptr);
|
||||||
|
|
||||||
|
// Initialize cursor to (0,0) explicitly
|
||||||
|
buf->SetCursor(0, 0);
|
||||||
|
|
||||||
|
std::cout << "test_undo: Testing undo/redo system\n";
|
||||||
|
std::cout << "====================================\n\n";
|
||||||
|
|
||||||
|
bool running = true;
|
||||||
|
|
||||||
|
// Test 1: Insert text and verify buffer contains expected text
|
||||||
|
std::cout << "Test 1: Insert text 'Hello'\n";
|
||||||
|
frontend.Input().QueueText("Hello");
|
||||||
|
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(buf->Rows().size() >= 1);
|
||||||
|
std::string line_after_insert = std::string(buf->Rows()[0]);
|
||||||
|
assert(line_after_insert == "Hello");
|
||||||
|
std::cout << " Buffer content: '" << line_after_insert << "'\n";
|
||||||
|
std::cout << " ✓ Text insertion verified\n\n";
|
||||||
|
|
||||||
|
// Test 2: Undo insertion - text should be removed
|
||||||
|
std::cout << "Test 2: Undo insertion\n";
|
||||||
|
frontend.Input().QueueCommand(CommandId::Undo);
|
||||||
|
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(buf->Rows().size() >= 1);
|
||||||
|
std::string line_after_undo = std::string(buf->Rows()[0]);
|
||||||
|
assert(line_after_undo == "");
|
||||||
|
std::cout << " Buffer content: '" << line_after_undo << "'\n";
|
||||||
|
std::cout << " ✓ Undo successful - text removed\n\n";
|
||||||
|
|
||||||
|
// Test 3: Redo insertion - text should be restored
|
||||||
|
std::cout << "Test 3: Redo insertion\n";
|
||||||
|
frontend.Input().QueueCommand(CommandId::Redo);
|
||||||
|
|
||||||
|
while (!frontend.Input().IsEmpty() && running) {
|
||||||
|
frontend.Step(editor, running);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(buf->Rows().size() >= 1);
|
||||||
|
std::string line_after_redo = std::string(buf->Rows()[0]);
|
||||||
|
assert(line_after_redo == "Hello");
|
||||||
|
std::cout << " Buffer content: '" << line_after_redo << "'\n";
|
||||||
|
std::cout << " ✓ Redo successful - text restored\n\n";
|
||||||
|
|
||||||
|
frontend.Shutdown();
|
||||||
|
|
||||||
|
std::cout << "====================================\n";
|
||||||
|
std::cout << "All tests passed!\n";
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user