Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 952e1ed3f2 | |||
| 7069943df5 | |||
| ee2c9939d7 | |||
| f5a4625652 | |||
| 37472c71ec | |||
| 5ff4b2ed3e | |||
| ab2f9918f3 | |||
| d2b53601e2 | |||
| 78b9345799 | |||
| 495183ebd2 | |||
| 998b1b9817 | |||
| dc2cf4c0a6 | |||
| f6c4a5ab34 | |||
| 35ef74910d | |||
| b17672d440 | |||
| cb1d16ebbc | |||
| cbbde43dc2 | |||
| 45b2b88623 | |||
| 389dcf9cc7 | |||
| c98d9e717a | |||
| c864af7daa | |||
| 64022766c5 | |||
| d706b6db44 | |||
| bce9b3b33e | |||
| 56dc904432 | |||
| a8197939f8 | |||
| 9f722ec2bb | |||
| 094020dab5 | |||
| 09e4cd7ec6 | |||
| 49fa7ff8a7 | |||
| 38915484ac | |||
| 87b1e6f502 | |||
| ae822083c2 | |||
| 0c93d619c8 | |||
| 483ff18b0d | |||
| cd33e8feb1 | |||
| 0bfe75fbf0 | |||
| d15b241140 |
3
.idea/codeStyles/Project.xml
generated
3
.idea/codeStyles/Project.xml
generated
@@ -141,6 +141,9 @@
|
||||
<pair source="c++m" header="" fileNamingConvention="NONE" />
|
||||
</extensions>
|
||||
</files>
|
||||
<codeStyleSettings language="Markdown">
|
||||
<option name="RIGHT_MARGIN" value="72" />
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="ObjectiveC">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="8" />
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
* AppendBuffer.h - selector header to choose GapBuffer or PieceTable
|
||||
*/
|
||||
#ifndef KTE_APPENDBUFFER_H
|
||||
#define KTE_APPENDBUFFER_H
|
||||
#pragma once
|
||||
|
||||
#ifdef KTE_USE_PIECE_TABLE
|
||||
#include "PieceTable.h"
|
||||
@@ -10,6 +9,4 @@ using AppendBuffer = PieceTable;
|
||||
#else
|
||||
#include "GapBuffer.h"
|
||||
using AppendBuffer = GapBuffer;
|
||||
#endif
|
||||
|
||||
#endif // KTE_APPENDBUFFER_H
|
||||
#endif
|
||||
@@ -7,8 +7,8 @@
|
||||
#include "UndoSystem.h"
|
||||
#include "UndoTree.h"
|
||||
// For reconstructing highlighter state on copies
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "NullHighlighter.h"
|
||||
#include "syntax/HighlighterRegistry.h"
|
||||
#include "syntax/NullHighlighter.h"
|
||||
|
||||
|
||||
Buffer::Buffer()
|
||||
|
||||
74
Buffer.h
74
Buffer.h
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
* Buffer.h - editor buffer representing an open document
|
||||
*/
|
||||
#ifndef KTE_BUFFER_H
|
||||
#define KTE_BUFFER_H
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
@@ -14,9 +13,14 @@
|
||||
#include "UndoSystem.h"
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include "HighlighterEngine.h"
|
||||
#include "syntax/HighlighterEngine.h"
|
||||
#include "Highlight.h"
|
||||
|
||||
// Forward declaration for swap journal integration
|
||||
namespace kte {
|
||||
class SwapRecorder;
|
||||
}
|
||||
|
||||
|
||||
class Buffer {
|
||||
public:
|
||||
@@ -375,22 +379,62 @@ public:
|
||||
[[nodiscard]] std::string AsString() const;
|
||||
|
||||
// Syntax highlighting integration (per-buffer)
|
||||
[[nodiscard]] std::uint64_t Version() const { return version_; }
|
||||
[[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_; }
|
||||
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();
|
||||
}
|
||||
|
||||
kte::HighlighterEngine *Highlighter() { return highlighter_.get(); }
|
||||
const kte::HighlighterEngine *Highlighter() const { return highlighter_.get(); }
|
||||
|
||||
void EnsureHighlighter()
|
||||
{
|
||||
if (!highlighter_) highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
||||
if (!highlighter_)
|
||||
highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
||||
}
|
||||
|
||||
|
||||
// Swap journal integration (set by Editor)
|
||||
void SetSwapRecorder(kte::SwapRecorder *rec)
|
||||
{
|
||||
swap_rec_ = rec;
|
||||
}
|
||||
|
||||
|
||||
// Raw, low-level editing APIs used by UndoSystem apply().
|
||||
// These must NOT trigger undo recording. They also do not move the cursor.
|
||||
void insert_text(int row, int col, std::string_view text);
|
||||
@@ -430,9 +474,9 @@ private:
|
||||
|
||||
// Syntax/highlighting state
|
||||
std::uint64_t version_ = 0; // increment on edits
|
||||
bool syntax_enabled_ = true;
|
||||
bool syntax_enabled_ = true;
|
||||
std::string filetype_;
|
||||
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
||||
};
|
||||
|
||||
#endif // KTE_BUFFER_H
|
||||
// Non-owning pointer to swap recorder managed by Editor/SwapManager
|
||||
kte::SwapRecorder *swap_rec_ = nullptr;
|
||||
};
|
||||
241
CMakeLists.txt
241
CMakeLists.txt
@@ -3,33 +3,53 @@ project(kte)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(KTE_VERSION "1.2.0")
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(KTE_VERSION "1.4.1")
|
||||
|
||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||
set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.")
|
||||
set(KTE_USE_QT OFF CACHE BOOL "Build the QT frontend instead of ImGui.")
|
||||
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
|
||||
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")
|
||||
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
|
||||
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
|
||||
|
||||
# Optionally enable AddressSanitizer (ASan)
|
||||
option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF)
|
||||
|
||||
if (ENABLE_ASAN)
|
||||
message(STATUS "ASan enabled")
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
|
||||
# Ensure the sanitizer is linked too (especially important on some platforms)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
|
||||
endif ()
|
||||
|
||||
if (CMAKE_HOST_UNIX)
|
||||
message(STATUS "Build system is POSIX.")
|
||||
else ()
|
||||
message(STATUS "Build system is NOT POSIX.")
|
||||
endif ()
|
||||
|
||||
add_compile_options(
|
||||
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
|
||||
else ()
|
||||
add_compile_options(
|
||||
"-static"
|
||||
"-Wall"
|
||||
"-Wextra"
|
||||
"-Werror"
|
||||
"-pedantic"
|
||||
"-Wno-unused-function"
|
||||
"-Wno-unused-parameter"
|
||||
"$<$<CONFIG:RELEASE>:-O2>"
|
||||
"$<$<CONFIG:DEBUG>:-g>"
|
||||
"$<$<CONFIG:RELEASE>:-O2>")
|
||||
)
|
||||
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
||||
add_compile_options("-stdlib=libc++")
|
||||
else ()
|
||||
@@ -54,6 +74,59 @@ set(CURSES_NEED_WIDE)
|
||||
find_package(Curses REQUIRED)
|
||||
include_directories(${CURSES_INCLUDE_DIR})
|
||||
|
||||
set(SYNTAX_SOURCES
|
||||
syntax/GoHighlighter.cc
|
||||
syntax/CppHighlighter.cc
|
||||
syntax/JsonHighlighter.cc
|
||||
syntax/ErlangHighlighter.cc
|
||||
syntax/MarkdownHighlighter.cc
|
||||
syntax/TreeSitterHighlighter.cc
|
||||
syntax/LispHighlighter.cc
|
||||
syntax/HighlighterEngine.cc
|
||||
syntax/RustHighlighter.cc
|
||||
syntax/HighlighterRegistry.cc
|
||||
syntax/SqlHighlighter.cc
|
||||
syntax/NullHighlighter.cc
|
||||
syntax/ForthHighlighter.cc
|
||||
syntax/PythonHighlighter.cc
|
||||
syntax/ShellHighlighter.cc
|
||||
)
|
||||
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
list(APPEND SYNTAX_SOURCES
|
||||
TreeSitterHighlighter.cc)
|
||||
endif ()
|
||||
|
||||
set(FONT_SOURCES
|
||||
fonts/Font.cc
|
||||
fonts/FontRegistry.cc
|
||||
)
|
||||
|
||||
if (BUILD_GUI)
|
||||
set(GUI_SOURCES
|
||||
GUIConfig.cc
|
||||
)
|
||||
if (KTE_USE_QT)
|
||||
find_package(Qt6 COMPONENTS Widgets REQUIRED)
|
||||
set(GUI_SOURCES
|
||||
${GUI_SOURCES}
|
||||
QtFrontend.cc
|
||||
QtInputHandler.cc
|
||||
QtRenderer.cc
|
||||
)
|
||||
# Expose preprocessor switch so sources can exclude ImGui-specific code
|
||||
add_compile_definitions(KTE_USE_QT)
|
||||
else ()
|
||||
set(GUI_SOURCES
|
||||
${GUI_SOURCES}
|
||||
${FONT_SOURCES}
|
||||
ImGuiFrontend.cc
|
||||
ImGuiInputHandler.cc
|
||||
ImGuiRenderer.cc
|
||||
)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
set(COMMON_SOURCES
|
||||
GapBuffer.cc
|
||||
PieceTable.cc
|
||||
@@ -62,6 +135,7 @@ set(COMMON_SOURCES
|
||||
Command.cc
|
||||
HelpText.cc
|
||||
KKeymap.cc
|
||||
Swap.cc
|
||||
TerminalInputHandler.cc
|
||||
TerminalRenderer.cc
|
||||
TerminalFrontend.cc
|
||||
@@ -71,24 +145,73 @@ set(COMMON_SOURCES
|
||||
UndoNode.cc
|
||||
UndoTree.cc
|
||||
UndoSystem.cc
|
||||
HighlighterEngine.cc
|
||||
CppHighlighter.cc
|
||||
HighlighterRegistry.cc
|
||||
NullHighlighter.cc
|
||||
JsonHighlighter.cc
|
||||
MarkdownHighlighter.cc
|
||||
ShellHighlighter.cc
|
||||
GoHighlighter.cc
|
||||
PythonHighlighter.cc
|
||||
RustHighlighter.cc
|
||||
LispHighlighter.cc
|
||||
|
||||
${SYNTAX_SOURCES}
|
||||
)
|
||||
|
||||
set(SYNTAX_HEADERS
|
||||
syntax/GoHighlighter.h
|
||||
syntax/HighlighterEngine.h
|
||||
syntax/ShellHighlighter.h
|
||||
syntax/MarkdownHighlighter.h
|
||||
syntax/LispHighlighter.h
|
||||
syntax/SqlHighlighter.h
|
||||
syntax/ForthHighlighter.h
|
||||
syntax/JsonHighlighter.h
|
||||
syntax/TreeSitterHighlighter.h
|
||||
syntax/NullHighlighter.h
|
||||
syntax/CppHighlighter.h
|
||||
syntax/ErlangHighlighter.h
|
||||
syntax/LanguageHighlighter.h
|
||||
syntax/RustHighlighter.h
|
||||
syntax/PythonHighlighter.h
|
||||
)
|
||||
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
list(APPEND COMMON_SOURCES
|
||||
TreeSitterHighlighter.cc)
|
||||
list(APPEND THEME_HEADERS
|
||||
TreeSitterHighlighter.h)
|
||||
endif ()
|
||||
|
||||
set(THEME_HEADERS
|
||||
themes/ThemeHelpers.h
|
||||
themes/EInk.h
|
||||
themes/Gruvbox.h
|
||||
themes/Solarized.h
|
||||
themes/Plan9.h
|
||||
themes/Nord.h
|
||||
themes/Everforest.h
|
||||
themes/KanagawaPaper.h
|
||||
themes/LCARS.h
|
||||
themes/OldBook.h
|
||||
themes/Amber.h
|
||||
themes/Orbital.h
|
||||
themes/WeylandYutani.h
|
||||
themes/Zenburn.h
|
||||
)
|
||||
|
||||
set(FONT_HEADERS
|
||||
fonts/Font.h
|
||||
fonts/FontRegistry.h
|
||||
fonts/FontRegistry.h
|
||||
fonts/FontList.h
|
||||
fonts/B612Mono.h
|
||||
fonts/BrassMono.h
|
||||
fonts/BrassMonoCode.h
|
||||
fonts/FiraCode.h
|
||||
fonts/Go.h
|
||||
fonts/IBMPlexMono.h
|
||||
fonts/Idealist.h
|
||||
fonts/Inconsolata.h
|
||||
fonts/InconsolataExpanded.h
|
||||
fonts/Iosevka.h
|
||||
fonts/IosevkaExtended.h
|
||||
fonts/ShareTech.h
|
||||
fonts/SpaceMono.h
|
||||
fonts/Syne.h
|
||||
fonts/Triplicate.h
|
||||
fonts/Unispace.h
|
||||
)
|
||||
|
||||
set(COMMON_HEADERS
|
||||
GapBuffer.h
|
||||
PieceTable.h
|
||||
@@ -98,6 +221,7 @@ set(COMMON_HEADERS
|
||||
Command.h
|
||||
HelpText.h
|
||||
KKeymap.h
|
||||
Swap.h
|
||||
InputHandler.h
|
||||
TerminalInputHandler.h
|
||||
Renderer.h
|
||||
@@ -111,23 +235,32 @@ set(COMMON_HEADERS
|
||||
UndoTree.h
|
||||
UndoSystem.h
|
||||
Highlight.h
|
||||
LanguageHighlighter.h
|
||||
HighlighterEngine.h
|
||||
CppHighlighter.h
|
||||
HighlighterRegistry.h
|
||||
NullHighlighter.h
|
||||
JsonHighlighter.h
|
||||
MarkdownHighlighter.h
|
||||
ShellHighlighter.h
|
||||
GoHighlighter.h
|
||||
PythonHighlighter.h
|
||||
RustHighlighter.h
|
||||
LispHighlighter.h
|
||||
|
||||
${SYNTAX_HEADERS}
|
||||
)
|
||||
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
list(APPEND COMMON_HEADERS
|
||||
TreeSitterHighlighter.h)
|
||||
if (BUILD_GUI)
|
||||
set(GUI_HEADERS
|
||||
GUIConfig.h
|
||||
)
|
||||
|
||||
if (KTE_USE_QT)
|
||||
set(GUI_HEADERS
|
||||
${GUI_HEADERS}
|
||||
QtFrontend.h
|
||||
QtInputHandler.h
|
||||
QtRenderer.h
|
||||
)
|
||||
else ()
|
||||
set(GUI_HEADERS
|
||||
${GUI_HEADERS}
|
||||
${THEME_HEADERS}
|
||||
${FONT_HEADERS}
|
||||
ImGuiFrontend.h
|
||||
ImGuiInputHandler.h
|
||||
ImGuiRenderer.h
|
||||
)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# kte (terminal-first) executable
|
||||
@@ -194,37 +327,41 @@ if (BUILD_TESTS)
|
||||
endif ()
|
||||
|
||||
if (${BUILD_GUI})
|
||||
target_sources(kte PRIVATE
|
||||
Font.h
|
||||
GUIConfig.cc
|
||||
GUIConfig.h
|
||||
GUIRenderer.cc
|
||||
GUIRenderer.h
|
||||
GUIInputHandler.cc
|
||||
GUIInputHandler.h
|
||||
GUIFrontend.cc
|
||||
GUIFrontend.h)
|
||||
target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
|
||||
target_link_libraries(kte imgui)
|
||||
# ImGui::CreateContext();
|
||||
# ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
# // Set custom ini filename path to ~/.config/kte/imgui.ini
|
||||
# if (const char* home = std::getenv("HOME")) {
|
||||
# static std::string ini_path = std::string(home) + "/.config/kte/imgui.ini";
|
||||
# io.IniFilename = ini_path.c_str();
|
||||
# }
|
||||
|
||||
# io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
|
||||
# io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
|
||||
# Do not enable GUI in the terminal-first 'kte' binary; GUI is built as separate 'kge'.
|
||||
# This avoids referencing GUI classes from kte and keeps dependencies minimal.
|
||||
|
||||
# kge (GUI-first) executable
|
||||
add_executable(kge
|
||||
main.cc
|
||||
${COMMON_SOURCES}
|
||||
${GUI_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
GUIConfig.cc
|
||||
GUIConfig.h
|
||||
GUIRenderer.cc
|
||||
GUIRenderer.h
|
||||
GUIInputHandler.cc
|
||||
GUIInputHandler.h
|
||||
GUIFrontend.cc
|
||||
GUIFrontend.h)
|
||||
${GUI_HEADERS}
|
||||
|
||||
)
|
||||
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
|
||||
if (KTE_USE_QT)
|
||||
target_compile_definitions(kge PRIVATE KTE_USE_QT=1)
|
||||
endif ()
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
||||
if (KTE_USE_QT)
|
||||
target_link_libraries(kge ${CURSES_LIBRARIES} Qt6::Widgets)
|
||||
else ()
|
||||
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
||||
endif ()
|
||||
|
||||
# On macOS, build kge as a proper .app bundle
|
||||
if (APPLE)
|
||||
|
||||
1168
Command.cc
1168
Command.cc
File diff suppressed because it is too large
Load Diff
29
Command.h
29
Command.h
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
* Command.h - command model and registry for editor actions
|
||||
*/
|
||||
#ifndef KTE_COMMAND_H
|
||||
#define KTE_COMMAND_H
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
@@ -28,6 +27,8 @@ enum class CommandId {
|
||||
SearchReplace, // begin search & replace (two-step prompt)
|
||||
OpenFileStart, // begin open-file prompt
|
||||
VisualFilePickerToggle,
|
||||
// GUI-only: toggle/show a visual font selector dialog
|
||||
VisualFontPickerToggle,
|
||||
// Buffers
|
||||
BufferSwitchStart, // begin buffer switch prompt
|
||||
BufferClose,
|
||||
@@ -58,6 +59,8 @@ enum class CommandId {
|
||||
MoveEnd,
|
||||
PageUp,
|
||||
PageDown,
|
||||
ScrollUp, // scroll viewport up (towards beginning) without moving cursor
|
||||
ScrollDown, // scroll viewport down (towards end) without moving cursor
|
||||
WordPrev,
|
||||
WordNext,
|
||||
DeleteWordPrev, // delete previous word (ESC BACKSPACE)
|
||||
@@ -89,15 +92,22 @@ enum class CommandId {
|
||||
ShowHelp, // open +HELP+ buffer with manual text (C-k h)
|
||||
// Meta
|
||||
UnknownKCommand, // arg: single character that was not recognized after C-k
|
||||
UnknownEscCommand, // invalid ESC (meta) command; show status and exit escape mode
|
||||
// Generic command prompt
|
||||
CommandPromptStart, // begin generic command prompt (C-k ;)
|
||||
// Theme by name
|
||||
ThemeSetByName,
|
||||
// Font by name (GUI)
|
||||
FontSetByName,
|
||||
// Font size (GUI)
|
||||
FontSetSize,
|
||||
// Background mode (GUI)
|
||||
BackgroundSet,
|
||||
// Syntax highlighting
|
||||
Syntax, // ":syntax on|off|reload"
|
||||
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
||||
BackgroundSet,
|
||||
// Syntax highlighting
|
||||
Syntax, // ":syntax on|off|reload"
|
||||
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
||||
// Viewport control
|
||||
CenterOnCursor, // center the viewport on the current cursor line (C-k k)
|
||||
};
|
||||
|
||||
|
||||
@@ -123,6 +133,9 @@ struct Command {
|
||||
CommandHandler handler;
|
||||
// Public commands are exposed in the ": " prompt (C-k ;)
|
||||
bool isPublic = false;
|
||||
// Whether this command should consume and honor a universal argument repeat count.
|
||||
// Default true per issue request; authors can turn off per-command.
|
||||
bool repeatable = true;
|
||||
};
|
||||
|
||||
|
||||
@@ -149,6 +162,4 @@ void InstallDefaultCommands();
|
||||
// Returns true if the command executed successfully.
|
||||
bool Execute(Editor &ed, CommandId id, const std::string &arg = std::string(), int count = 0);
|
||||
|
||||
bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0);
|
||||
|
||||
#endif // KTE_COMMAND_H
|
||||
bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0);
|
||||
@@ -1,170 +0,0 @@
|
||||
#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
|
||||
@@ -1,34 +0,0 @@
|
||||
// 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
|
||||
278
Editor.cc
278
Editor.cc
@@ -1,16 +1,17 @@
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <filesystem>
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "NullHighlighter.h"
|
||||
|
||||
#include "Editor.h"
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "CppHighlighter.h"
|
||||
#include "NullHighlighter.h"
|
||||
#include "syntax/HighlighterRegistry.h"
|
||||
#include "syntax/CppHighlighter.h"
|
||||
#include "syntax/NullHighlighter.h"
|
||||
|
||||
|
||||
Editor::Editor() = default;
|
||||
Editor::Editor()
|
||||
{
|
||||
swap_ = std::make_unique<kte::SwapManager>();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
@@ -125,6 +126,11 @@ std::size_t
|
||||
Editor::AddBuffer(const Buffer &buf)
|
||||
{
|
||||
buffers_.push_back(buf);
|
||||
// Attach swap recorder
|
||||
if (swap_) {
|
||||
buffers_.back().SetSwapRecorder(swap_.get());
|
||||
swap_->Attach(&buffers_.back());
|
||||
}
|
||||
if (buffers_.size() == 1) {
|
||||
curbuf_ = 0;
|
||||
}
|
||||
@@ -136,6 +142,10 @@ std::size_t
|
||||
Editor::AddBuffer(Buffer &&buf)
|
||||
{
|
||||
buffers_.push_back(std::move(buf));
|
||||
if (swap_) {
|
||||
buffers_.back().SetSwapRecorder(swap_.get());
|
||||
swap_->Attach(&buffers_.back());
|
||||
}
|
||||
if (buffers_.size() == 1) {
|
||||
curbuf_ = 0;
|
||||
}
|
||||
@@ -148,104 +158,119 @@ Editor::OpenFile(const std::string &path, std::string &err)
|
||||
{
|
||||
// If there is exactly one unnamed, empty, clean buffer, reuse it instead
|
||||
// of creating a new one.
|
||||
if (buffers_.size() == 1) {
|
||||
Buffer &cur = buffers_[curbuf_];
|
||||
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
||||
const bool clean = !cur.Dirty();
|
||||
const auto &rows = cur.Rows();
|
||||
const bool rows_empty = rows.empty();
|
||||
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
|
||||
if (unnamed && clean && (rows_empty || single_empty_line)) {
|
||||
bool ok = cur.OpenFromFile(path, err);
|
||||
if (!ok) return false;
|
||||
// Setup highlighting using registry (extension + shebang)
|
||||
cur.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
const auto &rows = cur.Rows();
|
||||
if (!rows.empty()) first = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
cur.SetFiletype(ft);
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
cur.SetFiletype("");
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (buffers_.size() == 1) {
|
||||
Buffer &cur = buffers_[curbuf_];
|
||||
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
||||
const bool clean = !cur.Dirty();
|
||||
const auto &rows = cur.Rows();
|
||||
const bool rows_empty = rows.empty();
|
||||
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
|
||||
if (unnamed && clean && (rows_empty || single_empty_line)) {
|
||||
bool ok = cur.OpenFromFile(path, err);
|
||||
if (!ok)
|
||||
return false;
|
||||
// Ensure swap recorder is attached for this buffer
|
||||
if (swap_) {
|
||||
cur.SetSwapRecorder(swap_.get());
|
||||
swap_->Attach(&cur);
|
||||
swap_->NotifyFilenameChanged(cur);
|
||||
}
|
||||
// Setup highlighting using registry (extension + shebang)
|
||||
cur.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
const auto &rows = cur.Rows();
|
||||
if (!rows.empty())
|
||||
first = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
cur.SetFiletype(ft);
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
cur.SetFiletype("");
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Buffer b;
|
||||
if (!b.OpenFromFile(path, err)) {
|
||||
return false;
|
||||
}
|
||||
// Initialize syntax highlighting by extension + shebang via registry (v2)
|
||||
b.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
{
|
||||
const auto &rows = b.Rows();
|
||||
if (!rows.empty()) first = static_cast<std::string>(rows[0]);
|
||||
}
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
b.SetFiletype(ft);
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
b.SetFiletype("");
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
// Add as a new buffer and switch to it
|
||||
std::size_t idx = AddBuffer(std::move(b));
|
||||
SwitchTo(idx);
|
||||
return true;
|
||||
Buffer b;
|
||||
if (!b.OpenFromFile(path, err)) {
|
||||
return false;
|
||||
}
|
||||
if (swap_) {
|
||||
b.SetSwapRecorder(swap_.get());
|
||||
// path is known, notify
|
||||
swap_->Attach(&b);
|
||||
swap_->NotifyFilenameChanged(b);
|
||||
}
|
||||
// Initialize syntax highlighting by extension + shebang via registry (v2)
|
||||
b.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
{
|
||||
const auto &rows = b.Rows();
|
||||
if (!rows.empty())
|
||||
first = static_cast<std::string>(rows[0]);
|
||||
}
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
b.SetFiletype(ft);
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
b.SetFiletype("");
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
// Add as a new buffer and switch to it
|
||||
std::size_t idx = AddBuffer(std::move(b));
|
||||
SwitchTo(idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
Editor::SwitchTo(std::size_t index)
|
||||
{
|
||||
if (index >= buffers_.size()) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
if (index >= buffers_.size()) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -277,8 +302,67 @@ Editor::Reset()
|
||||
msgtm_ = 0;
|
||||
uarg_ = 0;
|
||||
ucount_ = 0;
|
||||
repeatable_ = false;
|
||||
quit_requested_ = false;
|
||||
quit_confirm_pending_ = false;
|
||||
// Reset close-confirm/save state
|
||||
close_confirm_pending_ = false;
|
||||
close_after_save_ = false;
|
||||
buffers_.clear();
|
||||
curbuf_ = 0;
|
||||
}
|
||||
|
||||
|
||||
// --- Universal argument helpers ---
|
||||
void
|
||||
Editor::UArgStart()
|
||||
{
|
||||
// If not active, start fresh; else multiply by 4 per ke semantics
|
||||
if (uarg_ == 0) {
|
||||
ucount_ = 0;
|
||||
} else {
|
||||
if (ucount_ == 0) {
|
||||
ucount_ = 1;
|
||||
}
|
||||
ucount_ *= 4;
|
||||
}
|
||||
uarg_ = 1;
|
||||
char buf[64];
|
||||
std::snprintf(buf, sizeof(buf), "C-u %d", ucount_);
|
||||
SetStatus(buf);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Editor::UArgDigit(int d)
|
||||
{
|
||||
if (d < 0)
|
||||
d = 0;
|
||||
if (d > 9)
|
||||
d = 9;
|
||||
if (uarg_ == 0) {
|
||||
uarg_ = 1;
|
||||
ucount_ = 0;
|
||||
}
|
||||
ucount_ = ucount_ * 10 + d;
|
||||
char buf[64];
|
||||
std::snprintf(buf, sizeof(buf), "C-u %d", ucount_);
|
||||
SetStatus(buf);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Editor::UArgClear()
|
||||
{
|
||||
uarg_ = 0;
|
||||
ucount_ = 0;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
Editor::UArgGet()
|
||||
{
|
||||
int n = (ucount_ > 0) ? ucount_ : 1;
|
||||
UArgClear();
|
||||
return n;
|
||||
}
|
||||
88
Editor.h
88
Editor.h
@@ -1,15 +1,14 @@
|
||||
/*
|
||||
* Editor.h - top-level editor state and buffer management
|
||||
*/
|
||||
#ifndef KTE_EDITOR_H
|
||||
#define KTE_EDITOR_H
|
||||
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Buffer.h"
|
||||
#include "Swap.h"
|
||||
|
||||
|
||||
class Editor {
|
||||
@@ -32,6 +31,16 @@ public:
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::size_t ContentRows() const
|
||||
{
|
||||
// Always compute from current rows_ to avoid stale values.
|
||||
// Reserve 1 row for status line.
|
||||
if (rows_ == 0)
|
||||
return 1;
|
||||
return std::max<std::size_t>(1, rows_ - 1);
|
||||
}
|
||||
|
||||
|
||||
// Mode and flags (mirroring legacy fields)
|
||||
void SetMode(int m)
|
||||
{
|
||||
@@ -148,6 +157,33 @@ public:
|
||||
}
|
||||
|
||||
|
||||
// --- Universal argument control (C-u) ---
|
||||
// Begin or extend a universal argument (like ke's uarg_start)
|
||||
void UArgStart();
|
||||
|
||||
// Add a digit 0..9 to the current universal argument (like ke's uarg_digit)
|
||||
void UArgDigit(int d);
|
||||
|
||||
// Clear universal-argument state (like ke's uarg_clear)
|
||||
void UArgClear();
|
||||
|
||||
// Consume the current universal argument, returning count >= 1.
|
||||
// If no universal argument active, returns 1.
|
||||
int UArgGet();
|
||||
|
||||
// Repeatable command flag: input layer can mark the next command as repeatable
|
||||
void SetRepeatable(bool on)
|
||||
{
|
||||
repeatable_ = on;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] bool Repeatable() const
|
||||
{
|
||||
return repeatable_;
|
||||
}
|
||||
|
||||
|
||||
// Status message storage. Rendering is renderer-dependent; the editor
|
||||
// merely stores the current message and its timestamp.
|
||||
void SetStatus(const std::string &message);
|
||||
@@ -184,6 +220,31 @@ public:
|
||||
}
|
||||
|
||||
|
||||
// --- Buffer close/save confirmation state ---
|
||||
void SetCloseConfirmPending(bool on)
|
||||
{
|
||||
close_confirm_pending_ = on;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] bool CloseConfirmPending() const
|
||||
{
|
||||
return close_confirm_pending_;
|
||||
}
|
||||
|
||||
|
||||
void SetCloseAfterSave(bool on)
|
||||
{
|
||||
close_after_save_ = on;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] bool CloseAfterSave() const
|
||||
{
|
||||
return close_after_save_;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::time_t StatusTime() const
|
||||
{
|
||||
return msgtm_;
|
||||
@@ -457,6 +518,13 @@ public:
|
||||
}
|
||||
|
||||
|
||||
// Swap manager access (for advanced integrations/tests)
|
||||
[[nodiscard]] kte::SwapManager *Swap()
|
||||
{
|
||||
return swap_.get();
|
||||
}
|
||||
|
||||
|
||||
// --- GUI: Visual File Picker state ---
|
||||
void SetFilePickerVisible(bool on)
|
||||
{
|
||||
@@ -490,17 +558,23 @@ private:
|
||||
std::string msg_;
|
||||
std::time_t msgtm_ = 0;
|
||||
int uarg_ = 0, ucount_ = 0; // C-u support
|
||||
bool repeatable_ = false; // whether the next command is repeatable
|
||||
|
||||
std::vector<Buffer> buffers_;
|
||||
std::size_t curbuf_ = 0; // index into buffers_
|
||||
|
||||
// Swap journaling manager (lifetime = editor)
|
||||
std::unique_ptr<kte::SwapManager> swap_;
|
||||
|
||||
// Kill ring (Emacs-like)
|
||||
std::vector<std::string> kill_ring_;
|
||||
std::size_t kill_ring_max_ = 60;
|
||||
|
||||
// Quit state
|
||||
bool quit_requested_ = false;
|
||||
bool quit_confirm_pending_ = false;
|
||||
bool quit_requested_ = false;
|
||||
bool quit_confirm_pending_ = false;
|
||||
bool close_confirm_pending_ = false; // awaiting y/N to save-before-close
|
||||
bool close_after_save_ = false; // if true, close buffer after successful Save/SaveAs
|
||||
|
||||
// Search state
|
||||
bool search_active_ = false;
|
||||
@@ -551,6 +625,4 @@ public:
|
||||
private:
|
||||
std::string replace_find_tmp_;
|
||||
std::string replace_with_tmp_;
|
||||
};
|
||||
|
||||
#endif // KTE_EDITOR_H
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* Frontend.h - top-level container that couples Input + Renderer and runs the loop
|
||||
*/
|
||||
#ifndef KTE_FRONTEND_H
|
||||
#define KTE_FRONTEND_H
|
||||
|
||||
#pragma once
|
||||
|
||||
class Editor;
|
||||
class InputHandler;
|
||||
@@ -21,6 +19,4 @@ public:
|
||||
|
||||
// Shutdown/cleanup
|
||||
virtual void Shutdown() = 0;
|
||||
};
|
||||
|
||||
#endif // KTE_FRONTEND_H
|
||||
};
|
||||
48
GUIConfig.cc
48
GUIConfig.cc
@@ -102,27 +102,29 @@ GUIConfig::LoadFromFile(const std::string &path)
|
||||
if (v > 0.0f) {
|
||||
font_size = v;
|
||||
}
|
||||
} else if (key == "theme") {
|
||||
theme = val;
|
||||
} else if (key == "background" || key == "bg") {
|
||||
std::string v = val;
|
||||
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
if (v == "light" || v == "dark")
|
||||
background = v;
|
||||
} else if (key == "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;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (key == "font") {
|
||||
font = val;
|
||||
} else if (key == "theme") {
|
||||
theme = val;
|
||||
} else if (key == "background" || key == "bg") {
|
||||
std::string v = val;
|
||||
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
if (v == "light" || v == "dark")
|
||||
background = v;
|
||||
} else if (key == "syntax") {
|
||||
std::string v = val;
|
||||
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
if (v == "1" || v == "on" || v == "true" || v == "yes") {
|
||||
syntax = true;
|
||||
} else if (v == "0" || v == "off" || v == "false" || v == "no") {
|
||||
syntax = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
30
GUIConfig.h
30
GUIConfig.h
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
* GUIConfig - loads simple GUI configuration from $HOME/.config/kte/kge.ini
|
||||
*/
|
||||
#ifndef KTE_GUI_CONFIG_H
|
||||
#define KTE_GUI_CONFIG_H
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
@@ -12,24 +11,23 @@
|
||||
|
||||
class GUIConfig {
|
||||
public:
|
||||
bool fullscreen = false;
|
||||
int columns = 80;
|
||||
int rows = 42;
|
||||
float font_size = (float) KTE_FONT_SIZE;
|
||||
std::string theme = "nord";
|
||||
// Background mode for themes that support light/dark variants
|
||||
// Values: "dark" (default), "light"
|
||||
std::string background = "dark";
|
||||
bool fullscreen = false;
|
||||
int columns = 80;
|
||||
int rows = 42;
|
||||
float font_size = (float) KTE_FONT_SIZE;
|
||||
std::string font = "default";
|
||||
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
|
||||
// Default syntax highlighting state for GUI (kge): on/off
|
||||
// Accepts: on/off/true/false/yes/no/1/0 in the ini file.
|
||||
bool syntax = true; // default: enabled
|
||||
|
||||
// Load from default path: $HOME/.config/kte/kge.ini
|
||||
static GUIConfig Load();
|
||||
|
||||
// Load from explicit path. Returns true if file existed and was parsed.
|
||||
bool LoadFromFile(const std::string &path);
|
||||
};
|
||||
|
||||
#endif // KTE_GUI_CONFIG_H
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* GUIRenderer - ImGui-based renderer for GUI mode
|
||||
*/
|
||||
#ifndef KTE_GUI_RENDERER_H
|
||||
#define KTE_GUI_RENDERER_H
|
||||
|
||||
#include "Renderer.h"
|
||||
|
||||
class GUIRenderer final : public Renderer {
|
||||
public:
|
||||
GUIRenderer() = default;
|
||||
|
||||
~GUIRenderer() override = default;
|
||||
|
||||
void Draw(Editor &ed) override;
|
||||
};
|
||||
|
||||
#endif // KTE_GUI_RENDERER_H
|
||||
1537
GUITheme.h
1537
GUITheme.h
File diff suppressed because it is too large
Load Diff
14
GapBuffer.cc
14
GapBuffer.cc
@@ -82,7 +82,7 @@ GapBuffer::~GapBuffer()
|
||||
void
|
||||
GapBuffer::Reserve(const std::size_t newCapacity)
|
||||
{
|
||||
if (newCapacity <= capacity_)
|
||||
if (newCapacity <= capacity_) [[likely]]
|
||||
return;
|
||||
// Allocate space for terminator as well
|
||||
char *nb = new char[newCapacity + 1];
|
||||
@@ -108,7 +108,7 @@ GapBuffer::AppendChar(const char c)
|
||||
void
|
||||
GapBuffer::Append(const char *s, const std::size_t len)
|
||||
{
|
||||
if (!s || len == 0)
|
||||
if (!s || len == 0) [[unlikely]]
|
||||
return;
|
||||
ensureCapacityFor(len);
|
||||
std::memcpy(buffer_ + size_, s, len);
|
||||
@@ -131,7 +131,7 @@ GapBuffer::PrependChar(char c)
|
||||
{
|
||||
ensureCapacityFor(1);
|
||||
// shift right by 1
|
||||
if (size_ > 0) {
|
||||
if (size_ > 0) [[likely]] {
|
||||
std::memmove(buffer_ + 1, buffer_, size_);
|
||||
}
|
||||
buffer_[0] = c;
|
||||
@@ -143,10 +143,10 @@ GapBuffer::PrependChar(char c)
|
||||
void
|
||||
GapBuffer::Prepend(const char *s, std::size_t len)
|
||||
{
|
||||
if (!s || len == 0)
|
||||
if (!s || len == 0) [[unlikely]]
|
||||
return;
|
||||
ensureCapacityFor(len);
|
||||
if (size_ > 0) {
|
||||
if (size_ > 0) [[likely]] {
|
||||
std::memmove(buffer_ + len, buffer_, size_);
|
||||
}
|
||||
std::memcpy(buffer_, s, len);
|
||||
@@ -175,7 +175,7 @@ GapBuffer::Clear()
|
||||
void
|
||||
GapBuffer::ensureCapacityFor(std::size_t delta)
|
||||
{
|
||||
if (capacity_ - size_ >= delta)
|
||||
if (capacity_ - size_ >= delta) [[likely]]
|
||||
return;
|
||||
auto required = size_ + delta;
|
||||
Reserve(growCapacity(capacity_, required));
|
||||
@@ -201,4 +201,4 @@ GapBuffer::setTerminator() const
|
||||
}
|
||||
|
||||
buffer_[size_] = '\0';
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* GapBuffer.h - C++ replacement for abuf append/prepend buffer utilities
|
||||
*/
|
||||
#ifndef KTE_GAPBUFFER_H
|
||||
#define KTE_GAPBUFFER_H
|
||||
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
|
||||
|
||||
@@ -75,6 +73,4 @@ private:
|
||||
char *buffer_ = nullptr;
|
||||
std::size_t size_ = 0; // number of valid bytes (excluding terminator)
|
||||
std::size_t capacity_ = 0; // capacity of buffer_ excluding space for terminator
|
||||
};
|
||||
|
||||
#endif // KTE_GAPBUFFER_H
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
#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
|
||||
@@ -1,18 +0,0 @@
|
||||
// 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
|
||||
@@ -75,7 +75,7 @@ HelpText::Text()
|
||||
"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"
|
||||
" : theme NAME Set GUI theme (amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, plan9, solarized, weyland-yutani, zenburn)\n"
|
||||
" : background MODE Set background: light | dark (affects eink, gruvbox, old-book, solarized)\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* HelpText.h - embedded/customizable help content
|
||||
*/
|
||||
#ifndef KTE_HELPTEXT_H
|
||||
#define KTE_HELPTEXT_H
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
class HelpText {
|
||||
@@ -12,6 +10,4 @@ public:
|
||||
// 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
|
||||
};
|
||||
44
Highlight.h
44
Highlight.h
@@ -5,35 +5,33 @@
|
||||
#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
|
||||
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};
|
||||
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
|
||||
std::vector<HighlightSpan> spans;
|
||||
std::uint64_t version{0}; // buffer version used for this line
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -1,181 +0,0 @@
|
||||
#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
|
||||
@@ -1,76 +0,0 @@
|
||||
// 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
|
||||
@@ -1,157 +0,0 @@
|
||||
#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> ®istry() {
|
||||
static std::vector<RegEntry> reg;
|
||||
return reg;
|
||||
}
|
||||
class JSONHighlighter; class MarkdownHighlighter; class ShellHighlighter;
|
||||
class GoHighlighter; class PythonHighlighter; class RustHighlighter; class LispHighlighter;
|
||||
}
|
||||
|
||||
// Headers for the above
|
||||
#include "JsonHighlighter.h"
|
||||
#include "MarkdownHighlighter.h"
|
||||
#include "ShellHighlighter.h"
|
||||
#include "GoHighlighter.h"
|
||||
#include "PythonHighlighter.h"
|
||||
#include "RustHighlighter.h"
|
||||
#include "LispHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
static std::string to_lower(std::string_view s) {
|
||||
std::string r(s);
|
||||
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
||||
return r;
|
||||
}
|
||||
|
||||
std::string HighlighterRegistry::Normalize(std::string_view ft)
|
||||
{
|
||||
std::string f = to_lower(ft);
|
||||
if (f == "c" || f == "c++" || f == "cc" || f == "hpp" || f == "hh" || f == "h" || f == "cxx") return "cpp";
|
||||
if (f == "cpp") return "cpp";
|
||||
if (f == "json") return "json";
|
||||
if (f == "markdown" || f == "md" || f == "mkd" || f == "mdown") return "markdown";
|
||||
if (f == "shell" || f == "sh" || f == "bash" || f == "zsh" || f == "ksh" || f == "fish") return "shell";
|
||||
if (f == "go" || f == "golang") return "go";
|
||||
if (f == "py" || f == "python") return "python";
|
||||
if (f == "rs" || f == "rust") return "rust";
|
||||
if (f == "lisp" || f == "scheme" || f == "scm" || f == "rkt" || f == "el" || f == "clj" || f == "cljc" || f == "cl") return "lisp";
|
||||
return f;
|
||||
}
|
||||
|
||||
std::unique_ptr<LanguageHighlighter> HighlighterRegistry::CreateFor(std::string_view filetype)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
// 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>();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static std::string shebang_to_ft(std::string_view first_line) {
|
||||
if (first_line.size() < 2 || first_line.substr(0,2) != "#!") return "";
|
||||
std::string low = to_lower(first_line);
|
||||
if (low.find("python") != std::string::npos) return "python";
|
||||
if (low.find("bash") != std::string::npos) return "shell";
|
||||
if (low.find("sh") != std::string::npos) return "shell";
|
||||
if (low.find("zsh") != std::string::npos) return "shell";
|
||||
if (low.find("fish") != std::string::npos) return "shell";
|
||||
if (low.find("scheme") != std::string::npos || low.find("racket") != std::string::npos || low.find("guile") != std::string::npos) return "lisp";
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string HighlighterRegistry::DetectForPath(std::string_view path, std::string_view first_line)
|
||||
{
|
||||
// Extension
|
||||
std::string p(path);
|
||||
std::error_code ec;
|
||||
std::string ext = std::filesystem::path(p).extension().string();
|
||||
for (auto &ch: ext) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (!ext.empty()) {
|
||||
if (ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".cxx" || ext == ".h" || ext == ".hpp" || ext == ".hh") return "cpp";
|
||||
if (ext == ".json") return "json";
|
||||
if (ext == ".md" || ext == ".markdown" || ext == ".mkd") return "markdown";
|
||||
if (ext == ".sh" || ext == ".bash" || ext == ".zsh" || ext == ".ksh" || ext == ".fish") return "shell";
|
||||
if (ext == ".go") return "go";
|
||||
if (ext == ".py") return "python";
|
||||
if (ext == ".rs") return "rust";
|
||||
if (ext == ".lisp" || ext == ".scm" || ext == ".rkt" || ext == ".el" || ext == ".clj" || ext == ".cljc" || ext == ".cl") return "lisp";
|
||||
}
|
||||
// Shebang
|
||||
std::string ft = shebang_to_ft(first_line);
|
||||
return ft;
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
|
||||
// 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
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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
|
||||
@@ -1,35 +1,39 @@
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <SDL.h>
|
||||
#include <SDL_opengl.h>
|
||||
#include <imgui.h>
|
||||
#include <backends/imgui_impl_sdl2.h>
|
||||
#include <backends/imgui_impl_opengl3.h>
|
||||
#include <backends/imgui_impl_sdl2.h>
|
||||
|
||||
#include "Editor.h"
|
||||
#include "ImGuiFrontend.h"
|
||||
#include "Command.h"
|
||||
#include "GUIFrontend.h"
|
||||
#include "Font.h" // embedded default font (DefaultFontRegular)
|
||||
#include "Editor.h"
|
||||
#include "GUIConfig.h"
|
||||
#include "GUITheme.h"
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "NullHighlighter.h"
|
||||
#include "fonts/Font.h" // embedded default font (DefaultFont)
|
||||
#include "fonts/FontRegistry.h"
|
||||
#include "syntax/HighlighterRegistry.h"
|
||||
#include "syntax/NullHighlighter.h"
|
||||
|
||||
|
||||
#ifndef KTE_FONT_SIZE
|
||||
#define KTE_FONT_SIZE 16.0f
|
||||
#endif
|
||||
|
||||
static const char *kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
|
||||
static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
|
||||
|
||||
bool
|
||||
GUIFrontend::Init(Editor &ed)
|
||||
{
|
||||
(void) ed; // editor dimensions will be initialized during the first Step() frame
|
||||
// Attach editor to input handler for editor-owned features (e.g., universal argument)
|
||||
input_.Attach(&ed);
|
||||
// editor dimensions will be initialized during the first Step() frame
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -76,13 +80,17 @@ GUIFrontend::Init(Editor &ed)
|
||||
height_ = std::max(200, h);
|
||||
}
|
||||
|
||||
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
||||
window_ = SDL_CreateWindow(
|
||||
"kge - kyle's graphical editor " KTE_VERSION_STR,
|
||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
width_, height_,
|
||||
win_flags);
|
||||
if (!window_)
|
||||
if (!window_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_EnableScreenSaver();
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// macOS: when "fullscreen" is requested, position the window at the
|
||||
@@ -105,45 +113,65 @@ GUIFrontend::Init(Editor &ed)
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
(void) io;
|
||||
|
||||
// Set custom ini filename path to ~/.config/kte/imgui.ini
|
||||
if (const char *home = std::getenv("HOME")) {
|
||||
namespace fs = std::filesystem;
|
||||
fs::path config_dir = fs::path(home) / ".config" / "kte";
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::exists(config_dir)) {
|
||||
fs::create_directories(config_dir, ec);
|
||||
}
|
||||
|
||||
if (fs::exists(config_dir)) {
|
||||
static std::string ini_path = (config_dir / "imgui.ini").string();
|
||||
io.IniFilename = ini_path.c_str();
|
||||
}
|
||||
}
|
||||
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
|
||||
ImGui::StyleColorsDark();
|
||||
|
||||
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
||||
if (cfg.background == "light")
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||
else
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||
kte::ApplyThemeByName(cfg.theme);
|
||||
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
||||
if (cfg.background == "light")
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||
else
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||
kte::ApplyThemeByName(cfg.theme);
|
||||
|
||||
// Apply default syntax highlighting preference from GUI config to the current buffer
|
||||
if (Buffer *b = ed.CurrentBuffer()) {
|
||||
if (cfg.syntax) {
|
||||
b->SetSyntaxEnabled(true);
|
||||
// Ensure a highlighter is available if possible
|
||||
b->EnsureHighlighter();
|
||||
if (auto *eng = b->Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try detect from filename and first line; fall back to cpp or existing filetype
|
||||
std::string first_line;
|
||||
const auto &rows = b->Rows();
|
||||
if (!rows.empty()) first_line = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(b->Filename(), first_line);
|
||||
if (!ft.empty()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
b->SetFiletype(ft);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
b->SetFiletype("");
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b->SetSyntaxEnabled(false);
|
||||
}
|
||||
}
|
||||
// Apply default syntax highlighting preference from GUI config to the current buffer
|
||||
if (Buffer *b = ed.CurrentBuffer()) {
|
||||
if (cfg.syntax) {
|
||||
b->SetSyntaxEnabled(true);
|
||||
// Ensure a highlighter is available if possible
|
||||
b->EnsureHighlighter();
|
||||
if (auto *eng = b->Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try detect from filename and first line; fall back to cpp or existing filetype
|
||||
std::string first_line;
|
||||
const auto &rows = b->Rows();
|
||||
if (!rows.empty())
|
||||
first_line = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(
|
||||
b->Filename(), first_line);
|
||||
if (!ft.empty()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
b->SetFiletype(ft);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
b->SetFiletype("");
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b->SetSyntaxEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
||||
return false;
|
||||
@@ -171,8 +199,19 @@ GUIFrontend::Init(Editor &ed)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Initialize GUI font from embedded default (use configured size or compiled default)
|
||||
LoadGuiFont_(nullptr, (float) cfg.font_size);
|
||||
// Install embedded fonts into registry and load configured font
|
||||
kte::Fonts::InstallDefaultFonts();
|
||||
// Initialize font atlas using configured font name and size; fallback to embedded default helper
|
||||
if (!kte::Fonts::FontRegistry::Instance().LoadFont(cfg.font, (float) cfg.font_size)) {
|
||||
LoadGuiFont_(nullptr, (float) cfg.font_size);
|
||||
// Record defaults in registry so subsequent size changes have a base
|
||||
kte::Fonts::FontRegistry::Instance().RequestLoadFont("default", (float) cfg.font_size);
|
||||
std::string n;
|
||||
float s = 0.0f;
|
||||
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(n, s)) {
|
||||
kte::Fonts::FontRegistry::Instance().LoadFont(n, s);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -201,7 +240,62 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
input_.ProcessSDLEvent(e);
|
||||
}
|
||||
|
||||
// Execute pending mapped inputs (drain queue)
|
||||
// Apply pending font change before starting a new frame
|
||||
{
|
||||
std::string fname;
|
||||
float fsize = 0.0f;
|
||||
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(fname, fsize)) {
|
||||
if (!fname.empty() && fsize > 0.0f) {
|
||||
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
|
||||
// Recreate backend font texture
|
||||
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start a new ImGui frame BEFORE processing commands so dimensions are correct
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame(window_);
|
||||
ImGui::NewFrame();
|
||||
|
||||
// Update editor logical rows/cols using current ImGui metrics and display size
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float line_h = ImGui::GetTextLineHeightWithSpacing();
|
||||
float ch_w = ImGui::CalcTextSize("M").x;
|
||||
if (line_h <= 0.0f)
|
||||
line_h = 16.0f;
|
||||
if (ch_w <= 0.0f)
|
||||
ch_w = 8.0f;
|
||||
// Prefer ImGui IO display size; fall back to cached SDL window size
|
||||
float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast<float>(width_);
|
||||
float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast<float>(height_);
|
||||
|
||||
// Account for the GUI window padding and the status bar height used in ImGuiRenderer.
|
||||
// ImGuiRenderer pushes WindowPadding = (6,6) every frame, so use the same constants here
|
||||
// to avoid mismatches that would cause premature scrolling.
|
||||
const float pad_x = 6.0f;
|
||||
const float pad_y = 6.0f;
|
||||
// Status bar reserves one frame height (with spacing) inside the window
|
||||
float status_h = ImGui::GetFrameHeightWithSpacing();
|
||||
|
||||
float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x);
|
||||
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
|
||||
|
||||
// Visible content rows inside the scroll child
|
||||
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.
|
||||
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)));
|
||||
|
||||
// Only update if changed to avoid churn
|
||||
if (rows != ed.Rows() || cols != ed.Cols()) {
|
||||
ed.SetDimensions(rows, cols);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute pending mapped inputs (drain queue) AFTER dimensions are updated
|
||||
for (;;) {
|
||||
MappedInput mi;
|
||||
if (!input_.Poll(mi))
|
||||
@@ -222,46 +316,6 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
running = false;
|
||||
}
|
||||
|
||||
// Start a new ImGui frame
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame(window_);
|
||||
ImGui::NewFrame();
|
||||
|
||||
// Update editor logical rows/cols using current ImGui metrics and display size
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float line_h = ImGui::GetTextLineHeightWithSpacing();
|
||||
float ch_w = ImGui::CalcTextSize("M").x;
|
||||
if (line_h <= 0.0f)
|
||||
line_h = 16.0f;
|
||||
if (ch_w <= 0.0f)
|
||||
ch_w = 8.0f;
|
||||
// Prefer ImGui IO display size; fall back to cached SDL window size
|
||||
float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast<float>(width_);
|
||||
float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast<float>(height_);
|
||||
|
||||
// Account for the GUI window padding and the status bar height used in GUIRenderer
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
float pad_x = style.WindowPadding.x;
|
||||
float pad_y = style.WindowPadding.y;
|
||||
// Status bar reserves one frame height (with spacing) inside the window
|
||||
float status_h = ImGui::GetFrameHeightWithSpacing();
|
||||
|
||||
float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x);
|
||||
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
|
||||
|
||||
// Visible content rows inside the scroll child
|
||||
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.
|
||||
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)));
|
||||
|
||||
// Only update if changed to avoid churn
|
||||
if (rows != ed.Rows() || cols != ed.Cols()) {
|
||||
ed.SetDimensions(rows, cols);
|
||||
}
|
||||
}
|
||||
|
||||
// No runtime font UI; always use embedded font.
|
||||
|
||||
// Draw editor UI
|
||||
@@ -299,13 +353,13 @@ GUIFrontend::Shutdown()
|
||||
|
||||
|
||||
bool
|
||||
GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
||||
GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px)
|
||||
{
|
||||
const ImGuiIO &io = ImGui::GetIO();
|
||||
io.Fonts->Clear();
|
||||
const ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
|
||||
DefaultFontBoldCompressedData,
|
||||
DefaultFontBoldCompressedSize,
|
||||
kte::Fonts::DefaultFontData,
|
||||
kte::Fonts::DefaultFontSize,
|
||||
size_px);
|
||||
if (!font) {
|
||||
font = io.Fonts->AddFontDefault();
|
||||
@@ -313,7 +367,4 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
||||
(void) font;
|
||||
io.Fonts->Build();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// No runtime font reload or system font resolution in this simplified build.
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
/*
|
||||
* GUIFrontend - couples GUIInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
|
||||
* GUIFrontend - couples ImGuiInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
|
||||
*/
|
||||
#ifndef KTE_GUI_FRONTEND_H
|
||||
#define KTE_GUI_FRONTEND_H
|
||||
|
||||
#pragma once
|
||||
#include "Frontend.h"
|
||||
#include "GUIInputHandler.h"
|
||||
#include "GUIRenderer.h"
|
||||
#include "GUIConfig.h"
|
||||
#include "ImGuiInputHandler.h"
|
||||
#include "ImGuiRenderer.h"
|
||||
|
||||
|
||||
struct SDL_Window;
|
||||
@@ -27,12 +26,11 @@ public:
|
||||
private:
|
||||
static bool LoadGuiFont_(const char *path, float size_px);
|
||||
|
||||
GUIInputHandler input_{};
|
||||
GUIRenderer renderer_{};
|
||||
GUIConfig config_{};
|
||||
ImGuiInputHandler input_{};
|
||||
ImGuiRenderer renderer_{};
|
||||
SDL_Window *window_ = nullptr;
|
||||
SDL_GLContext gl_ctx_ = nullptr;
|
||||
int width_ = 1280;
|
||||
int height_ = 800;
|
||||
};
|
||||
|
||||
#endif // KTE_GUI_FRONTEND_H
|
||||
};
|
||||
@@ -5,8 +5,9 @@
|
||||
#include <SDL.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#include "GUIInputHandler.h"
|
||||
#include "ImGuiInputHandler.h"
|
||||
#include "KKeymap.h"
|
||||
#include "Editor.h"
|
||||
|
||||
|
||||
static bool
|
||||
@@ -14,20 +15,17 @@ map_key(const SDL_Keycode key,
|
||||
const SDL_Keymod mod,
|
||||
bool &k_prefix,
|
||||
bool &esc_meta,
|
||||
// universal-argument state (by ref)
|
||||
bool &uarg_active,
|
||||
bool &uarg_collecting,
|
||||
bool &uarg_negative,
|
||||
bool &uarg_had_digits,
|
||||
int &uarg_value,
|
||||
std::string &uarg_text,
|
||||
MappedInput &out)
|
||||
bool &k_ctrl_pending,
|
||||
Editor *ed,
|
||||
MappedInput &out,
|
||||
bool &suppress_textinput_once)
|
||||
{
|
||||
// Ctrl handling
|
||||
const bool is_ctrl = (mod & KMOD_CTRL) != 0;
|
||||
const bool is_alt = (mod & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
||||
|
||||
// If previous key was ESC, interpret this as Meta via ESC keymap
|
||||
// If previous key was ESC, interpret this as Meta via ESC keymap.
|
||||
// Prefer KEYDOWN when we can derive a printable ASCII; otherwise defer to TEXTINPUT.
|
||||
if (esc_meta) {
|
||||
int ascii_key = 0;
|
||||
if (key == SDLK_BACKSPACE) {
|
||||
@@ -45,52 +43,74 @@ map_key(const SDL_Keycode key,
|
||||
ascii_key = '>';
|
||||
}
|
||||
if (ascii_key != 0) {
|
||||
esc_meta = false; // consume if we can decide on KEYDOWN
|
||||
ascii_key = KLowerAscii(ascii_key);
|
||||
CommandId id;
|
||||
if (KLookupEscCommand(ascii_key, id)) {
|
||||
// Only consume the ESC-meta prefix if we actually mapped a command
|
||||
esc_meta = false;
|
||||
out = {true, id, "", 0};
|
||||
out = {true, id, "", 0};
|
||||
return true;
|
||||
}
|
||||
// Known printable but unmapped ESC sequence: report invalid
|
||||
out = {true, CommandId::UnknownEscCommand, "", 0};
|
||||
return true;
|
||||
}
|
||||
// Unhandled meta chord at KEYDOWN: do not clear esc_meta here.
|
||||
// Leave it set so SDL_TEXTINPUT fallback can translate and suppress insertion.
|
||||
// No usable ASCII from KEYDOWN → keep esc_meta set and let TEXTINPUT handle it
|
||||
out.hasCommand = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Movement and basic keys
|
||||
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
|
||||
switch (key) {
|
||||
case SDLK_LEFT:
|
||||
out = {true, CommandId::MoveLeft, "", 0};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::MoveLeft, "", 0};
|
||||
return true;
|
||||
case SDLK_RIGHT:
|
||||
out = {true, CommandId::MoveRight, "", 0};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::MoveRight, "", 0};
|
||||
return true;
|
||||
case SDLK_UP:
|
||||
out = {true, CommandId::MoveUp, "", 0};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::MoveUp, "", 0};
|
||||
return true;
|
||||
case SDLK_DOWN:
|
||||
out = {true, CommandId::MoveDown, "", 0};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::MoveDown, "", 0};
|
||||
return true;
|
||||
case SDLK_HOME:
|
||||
out = {true, CommandId::MoveHome, "", 0};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::MoveHome, "", 0};
|
||||
return true;
|
||||
case SDLK_END:
|
||||
out = {true, CommandId::MoveEnd, "", 0};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::MoveEnd, "", 0};
|
||||
return true;
|
||||
case SDLK_PAGEUP:
|
||||
out = {true, CommandId::PageUp, "", 0};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::PageUp, "", 0};
|
||||
return true;
|
||||
case SDLK_PAGEDOWN:
|
||||
out = {true, CommandId::PageDown, "", 0};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::PageDown, "", 0};
|
||||
return true;
|
||||
case SDLK_DELETE:
|
||||
out = {true, CommandId::DeleteChar, "", 0};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::DeleteChar, "", 0};
|
||||
return true;
|
||||
case SDLK_BACKSPACE:
|
||||
out = {true, CommandId::Backspace, "", 0};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::Backspace, "", 0};
|
||||
return true;
|
||||
case SDLK_TAB:
|
||||
// Insert a literal tab character when not interpreting a k-prefix suffix.
|
||||
@@ -103,10 +123,13 @@ map_key(const SDL_Keycode key,
|
||||
break; // fall through so k-prefix handler can process
|
||||
case SDLK_RETURN:
|
||||
case SDLK_KP_ENTER:
|
||||
out = {true, CommandId::Newline, "", 0};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::Newline, "", 0};
|
||||
return true;
|
||||
case SDLK_ESCAPE:
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
esc_meta = true; // next key will be treated as Meta
|
||||
out.hasCommand = false; // no immediate command for bare ESC in GUI
|
||||
return true;
|
||||
@@ -116,7 +139,6 @@ map_key(const SDL_Keycode key,
|
||||
|
||||
// If we are in k-prefix, interpret the very next key via the C-k keymap immediately.
|
||||
if (k_prefix) {
|
||||
k_prefix = false;
|
||||
esc_meta = false;
|
||||
// Normalize to ASCII; preserve case for letters using Shift
|
||||
int ascii_key = 0;
|
||||
@@ -136,10 +158,23 @@ map_key(const SDL_Keycode key,
|
||||
ascii_key = static_cast<int>(key);
|
||||
}
|
||||
bool ctrl2 = (mod & KMOD_CTRL) != 0;
|
||||
// If user typed a literal 'C' (or '^') as a control qualifier, keep k-prefix active
|
||||
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
|
||||
k_ctrl_pending = true;
|
||||
// Keep waiting for the next suffix; show status and suppress ensuing TEXTINPUT
|
||||
if (ed)
|
||||
ed->SetStatus("C-k C _");
|
||||
suppress_textinput_once = true;
|
||||
out.hasCommand = false;
|
||||
return true;
|
||||
}
|
||||
// Otherwise, consume the k-prefix now for the actual suffix
|
||||
k_prefix = false;
|
||||
if (ascii_key != 0) {
|
||||
int lower = KLowerAscii(ascii_key);
|
||||
bool ctrl_suffix_supported = (lower == 'd' || lower == 'x' || lower == 'q');
|
||||
bool pass_ctrl = ctrl2 && ctrl_suffix_supported;
|
||||
bool pass_ctrl = (ctrl2 || k_ctrl_pending) && ctrl_suffix_supported;
|
||||
k_ctrl_pending = false;
|
||||
CommandId id;
|
||||
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
||||
// Diagnostics for u/U
|
||||
@@ -156,54 +191,40 @@ map_key(const SDL_Keycode key,
|
||||
}
|
||||
if (mapped) {
|
||||
out = {true, id, "", 0};
|
||||
if (ed)
|
||||
ed->SetStatus(""); // clear "C-k _" hint after suffix
|
||||
return true;
|
||||
}
|
||||
int shown = KLowerAscii(ascii_key);
|
||||
char c = (shown >= 0x20 && shown <= 0x7e) ? static_cast<char>(shown) : '?';
|
||||
std::string arg(1, c);
|
||||
out = {true, CommandId::UnknownKCommand, arg, 0};
|
||||
if (ed)
|
||||
ed->SetStatus(""); // clear hint; handler will set unknown status
|
||||
return true;
|
||||
}
|
||||
out.hasCommand = false;
|
||||
// Non-printable/unmappable key as k-suffix (e.g., F-keys): report unknown and exit k-mode
|
||||
out = {true, CommandId::UnknownKCommand, std::string("?"), 0};
|
||||
if (ed)
|
||||
ed->SetStatus("");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_ctrl) {
|
||||
// Universal argument: C-u
|
||||
if (key == SDLK_u) {
|
||||
if (!uarg_active) {
|
||||
uarg_active = true;
|
||||
uarg_collecting = true;
|
||||
uarg_negative = false;
|
||||
uarg_had_digits = false;
|
||||
uarg_value = 4; // default
|
||||
uarg_text.clear();
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
} else if (uarg_collecting && !uarg_had_digits && !uarg_negative) {
|
||||
if (uarg_value <= 0)
|
||||
uarg_value = 4;
|
||||
else
|
||||
uarg_value *= 4; // repeated C-u multiplies by 4
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
} else {
|
||||
// End collection if already started with digits or '-'
|
||||
uarg_collecting = false;
|
||||
if (!uarg_had_digits && !uarg_negative && uarg_value <= 0)
|
||||
uarg_value = 4;
|
||||
}
|
||||
if (ed)
|
||||
ed->UArgStart();
|
||||
out.hasCommand = false;
|
||||
return true;
|
||||
}
|
||||
// Cancel universal arg on C-g as well (it maps to Refresh via ctrl map)
|
||||
if (key == SDLK_g) {
|
||||
uarg_active = false;
|
||||
uarg_collecting = false;
|
||||
uarg_negative = false;
|
||||
uarg_had_digits = false;
|
||||
uarg_value = 0;
|
||||
uarg_text.clear();
|
||||
if (ed)
|
||||
ed->UArgClear();
|
||||
// Also cancel any pending k-prefix qualifier
|
||||
k_ctrl_pending = false;
|
||||
k_prefix = false; // treat as cancel of prefix
|
||||
}
|
||||
if (key == SDLK_k || key == SDLK_KP_EQUALS) {
|
||||
k_prefix = true;
|
||||
@@ -247,29 +268,17 @@ map_key(const SDL_Keycode key,
|
||||
}
|
||||
}
|
||||
|
||||
// If collecting universal argument, allow digits/minus on KEYDOWN path too
|
||||
if (uarg_active && uarg_collecting) {
|
||||
// If collecting universal argument, allow digits on KEYDOWN path too
|
||||
if (ed && ed->UArg() != 0) {
|
||||
if ((key >= SDLK_0 && key <= SDLK_9) && !(mod & KMOD_SHIFT)) {
|
||||
int d = static_cast<int>(key - SDLK_0);
|
||||
if (!uarg_had_digits) {
|
||||
uarg_value = 0;
|
||||
uarg_had_digits = true;
|
||||
}
|
||||
if (uarg_value < 100000000) {
|
||||
uarg_value = uarg_value * 10 + d;
|
||||
}
|
||||
uarg_text.push_back(static_cast<char>('0' + d));
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
ed->UArgDigit(d);
|
||||
out.hasCommand = false;
|
||||
// We consumed a digit on KEYDOWN; SDL will often also emit TEXTINPUT for it.
|
||||
// Request suppression of the very next TEXTINPUT to avoid double-counting.
|
||||
suppress_textinput_once = true;
|
||||
return true;
|
||||
}
|
||||
if (key == SDLK_MINUS && !uarg_had_digits && !uarg_negative) {
|
||||
uarg_negative = true;
|
||||
uarg_text = "-";
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
}
|
||||
// Any other key will end collection; process it normally
|
||||
uarg_collecting = false;
|
||||
}
|
||||
|
||||
// k_prefix handled earlier
|
||||
@@ -279,21 +288,18 @@ map_key(const SDL_Keycode key,
|
||||
|
||||
|
||||
bool
|
||||
GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
{
|
||||
MappedInput mi;
|
||||
bool produced = false;
|
||||
switch (e.type) {
|
||||
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)
|
||||
// Let ImGui handle mouse wheel when it wants to capture the mouse
|
||||
// (e.g., when hovering the editor child window with scrollbars).
|
||||
// This enables native vertical and horizontal scrolling behavior in GUI.
|
||||
if (ImGui::GetIO().WantCaptureMouse)
|
||||
return false;
|
||||
// Otherwise, fallback to mapping vertical wheel to editor scroll commands.
|
||||
int dy = e.wheel.y;
|
||||
#ifdef SDL_MOUSEWHEEL_FLIPPED
|
||||
if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED)
|
||||
@@ -301,7 +307,7 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
#endif
|
||||
if (dy != 0) {
|
||||
int repeat = dy > 0 ? dy : -dy;
|
||||
CommandId id = dy > 0 ? CommandId::MoveUp : CommandId::MoveDown;
|
||||
CommandId id = dy > 0 ? CommandId::ScrollUp : CommandId::ScrollDown;
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (int i = 0; i < repeat; ++i) {
|
||||
q_.push(MappedInput{true, id, std::string(), 0});
|
||||
@@ -337,7 +343,9 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
segment = std::string_view(text).substr(start);
|
||||
}
|
||||
if (!segment.empty()) {
|
||||
MappedInput ins{true, CommandId::InsertText, std::string(segment), 0};
|
||||
MappedInput ins{
|
||||
true, CommandId::InsertText, std::string(segment), 0
|
||||
};
|
||||
q_.push(ins);
|
||||
}
|
||||
if (has_nl) {
|
||||
@@ -354,29 +362,28 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
}
|
||||
}
|
||||
|
||||
produced = map_key(key, mods,
|
||||
k_prefix_, esc_meta_,
|
||||
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_,
|
||||
uarg_text_,
|
||||
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,
|
||||
// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
|
||||
if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) {
|
||||
// Digits without shift, or a plain '-'
|
||||
const bool is_digit_key = (key >= SDLK_0 && key <= SDLK_9) && !(mods & KMOD_SHIFT);
|
||||
const bool is_minus_key = (key == SDLK_MINUS);
|
||||
if (uarg_active_ && uarg_collecting_ && (is_digit_key || is_minus_key)) {
|
||||
{
|
||||
bool suppress_req = false;
|
||||
produced = map_key(key, mods,
|
||||
k_prefix_, esc_meta_,
|
||||
k_ctrl_pending_,
|
||||
ed_,
|
||||
mi,
|
||||
suppress_req);
|
||||
if (suppress_req) {
|
||||
// Prevent the corresponding TEXTINPUT from delivering the same digit again
|
||||
suppress_text_input_once_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Do NOT suppress SDL_TEXTINPUT after inserting a TAB. Most platforms
|
||||
// do not emit TEXTINPUT for Tab, and suppressing here would incorrectly
|
||||
// eat the next character typed if no TEXTINPUT follows the Tab press.
|
||||
|
||||
// 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.
|
||||
// Additional suppression handled above when KEYDOWN consumed a uarg digit
|
||||
|
||||
// Suppress the immediate following SDL_TEXTINPUT when a printable KEYDOWN was used as a
|
||||
// k-prefix suffix or Meta (Alt/ESC) chord, regardless of whether a command was produced.
|
||||
const bool is_alt = (mods & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
||||
@@ -396,7 +403,8 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
}
|
||||
// Alt/Meta + letter can also generate TEXTINPUT on some platforms
|
||||
const bool is_meta_symbol = (
|
||||
key == SDLK_COMMA || key == SDLK_PERIOD || key == SDLK_LESS || key == SDLK_GREATER);
|
||||
key == SDLK_COMMA || key == SDLK_PERIOD || key == SDLK_LESS || key ==
|
||||
SDLK_GREATER);
|
||||
if (is_alt && ((key >= SDLK_a && key <= SDLK_z) || is_meta_symbol)) {
|
||||
should_suppress = true;
|
||||
}
|
||||
@@ -420,35 +428,26 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
break;
|
||||
}
|
||||
|
||||
// If universal argument collection is active, consume digit/minus TEXTINPUT
|
||||
if (uarg_active_ && uarg_collecting_) {
|
||||
// If editor universal argument is active, consume digit TEXTINPUT
|
||||
if (ed_ &&ed_
|
||||
|
||||
|
||||
|
||||
->
|
||||
UArg() != 0
|
||||
)
|
||||
{
|
||||
const char *txt = e.text.text;
|
||||
if (txt && *txt) {
|
||||
unsigned char c0 = static_cast<unsigned char>(txt[0]);
|
||||
if (c0 >= '0' && c0 <= '9') {
|
||||
int d = c0 - '0';
|
||||
if (!uarg_had_digits_) {
|
||||
uarg_value_ = 0;
|
||||
uarg_had_digits_ = true;
|
||||
}
|
||||
if (uarg_value_ < 100000000) {
|
||||
uarg_value_ = uarg_value_ * 10 + d;
|
||||
}
|
||||
uarg_text_.push_back(static_cast<char>(c0));
|
||||
mi = {true, CommandId::UArgStatus, uarg_text_, 0};
|
||||
produced = true; // consumed and enqueued status update
|
||||
break;
|
||||
}
|
||||
if (c0 == '-' && !uarg_had_digits_ && !uarg_negative_) {
|
||||
uarg_negative_ = true;
|
||||
uarg_text_ = "-";
|
||||
mi = {true, CommandId::UArgStatus, uarg_text_, 0};
|
||||
produced = true;
|
||||
ed_->UArgDigit(d);
|
||||
produced = true; // consumed to update status
|
||||
break;
|
||||
}
|
||||
}
|
||||
// End collection and allow this TEXTINPUT to be processed normally below
|
||||
uarg_collecting_ = false;
|
||||
// Non-digit ends collection; allow processing normally below
|
||||
}
|
||||
|
||||
// If we are still in k-prefix and KEYDOWN path didn't handle the suffix,
|
||||
@@ -464,9 +463,21 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
ascii_key = static_cast<int>(c0);
|
||||
}
|
||||
if (ascii_key != 0) {
|
||||
// Qualifier via TEXTINPUT: 'C' or '^'
|
||||
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
|
||||
k_ctrl_pending_ = true;
|
||||
if (ed_)
|
||||
ed_->SetStatus("C-k C _");
|
||||
// Keep k-prefix active; do not emit a command
|
||||
k_prefix_ = true;
|
||||
produced = true;
|
||||
break;
|
||||
}
|
||||
// Map via k-prefix table; do not pass Ctrl for TEXTINPUT case
|
||||
CommandId id;
|
||||
bool mapped = KLookupKCommand(ascii_key, false, id);
|
||||
bool pass_ctrl = k_ctrl_pending_;
|
||||
k_ctrl_pending_ = false;
|
||||
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
||||
// Diagnostics: log any k-prefix TEXTINPUT suffix mapping
|
||||
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
||||
? static_cast<char>(ascii_key)
|
||||
@@ -477,7 +488,9 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
mapped ? static_cast<int>(id) : -1);
|
||||
std::fflush(stderr);
|
||||
if (mapped) {
|
||||
mi = {true, id, "", 0};
|
||||
mi = {true, id, "", 0};
|
||||
if (ed_)
|
||||
ed_->SetStatus(""); // clear "C-k _" hint after suffix
|
||||
produced = true;
|
||||
break; // handled; do not insert text
|
||||
} else {
|
||||
@@ -487,13 +500,18 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
? static_cast<char>(shown)
|
||||
: '?';
|
||||
std::string arg(1, c);
|
||||
mi = {true, CommandId::UnknownKCommand, arg, 0};
|
||||
mi = {true, CommandId::UnknownKCommand, arg, 0};
|
||||
if (ed_)
|
||||
ed_->SetStatus("");
|
||||
produced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Consume even if no usable ascii was found
|
||||
// If no usable ASCII was found, still report an unknown k-command and exit k-mode
|
||||
mi = {true, CommandId::UnknownKCommand, std::string("?"), 0};
|
||||
if (ed_)
|
||||
ed_->SetStatus("");
|
||||
produced = true;
|
||||
break;
|
||||
}
|
||||
@@ -533,7 +551,8 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
}
|
||||
}
|
||||
}
|
||||
// If we get here, swallow the TEXTINPUT (do not insert stray char)
|
||||
// If we get here, unmapped ESC sequence via TEXTINPUT: report invalid
|
||||
mi = {true, CommandId::UnknownEscCommand, "", 0};
|
||||
produced = true;
|
||||
break;
|
||||
}
|
||||
@@ -563,24 +582,6 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
}
|
||||
|
||||
if (produced && mi.hasCommand) {
|
||||
// Attach universal-argument count if present, then clear the state
|
||||
if (uarg_active_ && mi.id != CommandId::UArgStatus) {
|
||||
int count = 0;
|
||||
if (!uarg_had_digits_ && !uarg_negative_) {
|
||||
count = (uarg_value_ > 0) ? uarg_value_ : 4;
|
||||
} else {
|
||||
count = uarg_value_;
|
||||
if (uarg_negative_)
|
||||
count = -count;
|
||||
}
|
||||
mi.count = count;
|
||||
uarg_active_ = false;
|
||||
uarg_collecting_ = false;
|
||||
uarg_negative_ = false;
|
||||
uarg_had_digits_ = false;
|
||||
uarg_value_ = 0;
|
||||
uarg_text_.clear();
|
||||
}
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(mi);
|
||||
}
|
||||
@@ -589,7 +590,7 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
|
||||
|
||||
bool
|
||||
GUIInputHandler::Poll(MappedInput &out)
|
||||
ImGuiInputHandler::Poll(MappedInput &out)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
if (q_.empty())
|
||||
@@ -597,4 +598,4 @@ GUIInputHandler::Poll(MappedInput &out)
|
||||
out = q_.front();
|
||||
q_.pop();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* GUIInputHandler - ImGui/SDL2-based input mapping for GUI mode
|
||||
* ImGuiInputHandler - ImGui/SDL2-based input mapping for GUI mode
|
||||
*/
|
||||
#ifndef KTE_GUI_INPUT_HANDLER_H
|
||||
#define KTE_GUI_INPUT_HANDLER_H
|
||||
|
||||
#pragma once
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
|
||||
@@ -12,11 +10,18 @@
|
||||
|
||||
union SDL_Event; // fwd decl to avoid including SDL here (SDL defines SDL_Event as a union)
|
||||
|
||||
class GUIInputHandler final : public InputHandler {
|
||||
class ImGuiInputHandler final : public InputHandler {
|
||||
public:
|
||||
GUIInputHandler() = default;
|
||||
ImGuiInputHandler() = default;
|
||||
|
||||
~ImGuiInputHandler() override = default;
|
||||
|
||||
|
||||
void Attach(Editor *ed) override
|
||||
{
|
||||
ed_ = ed;
|
||||
}
|
||||
|
||||
~GUIInputHandler() override = default;
|
||||
|
||||
// Translate an SDL event to editor command and enqueue if applicable.
|
||||
// Returns true if it produced a mapped command or consumed input.
|
||||
@@ -27,20 +32,13 @@ public:
|
||||
private:
|
||||
std::mutex mu_;
|
||||
std::queue<MappedInput> q_;
|
||||
bool k_prefix_ = false;
|
||||
bool k_prefix_ = false;
|
||||
bool k_ctrl_pending_ = false; // if true, next k-suffix is treated as Ctrl- (qualifier via literal 'C' or '^')
|
||||
// Treat ESC as a Meta prefix: next key is looked up via ESC keymap
|
||||
bool esc_meta_ = false;
|
||||
// When a printable keydown generated a non-text command, suppress the very next SDL_TEXTINPUT
|
||||
// event produced by SDL for the same keystroke to avoid inserting stray characters.
|
||||
bool suppress_text_input_once_ = false;
|
||||
|
||||
// Universal argument (C-u) state for GUI
|
||||
bool uarg_active_ = false; // an argument is pending for the next command
|
||||
bool uarg_collecting_ = false; // collecting digits / '-' right now
|
||||
bool uarg_negative_ = false; // whether a leading '-' was supplied
|
||||
bool uarg_had_digits_ = false; // whether any digits were supplied
|
||||
int uarg_value_ = 0; // current absolute value (>=0)
|
||||
std::string uarg_text_; // raw digits/minus typed for status display
|
||||
};
|
||||
|
||||
#endif // KTE_GUI_INPUT_HANDLER_H
|
||||
Editor *ed_ = nullptr; // attached editor for editor-owned uarg handling
|
||||
};
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <imgui.h>
|
||||
#include <regex>
|
||||
|
||||
#include "GUIRenderer.h"
|
||||
#include "ImGuiRenderer.h"
|
||||
#include "Highlight.h"
|
||||
#include "GUITheme.h"
|
||||
#include "Buffer.h"
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
|
||||
void
|
||||
GUIRenderer::Draw(Editor &ed)
|
||||
ImGuiRenderer::Draw(Editor &ed)
|
||||
{
|
||||
// Make the editor window occupy the entire GUI container/viewport
|
||||
ImGuiViewport *vp = ImGui::GetMainViewport();
|
||||
@@ -66,55 +66,66 @@ GUIRenderer::Draw(Editor &ed)
|
||||
if (!buf) {
|
||||
ImGui::TextUnformatted("[no buffer]");
|
||||
} else {
|
||||
const auto &lines = buf->Rows();
|
||||
// Reserve space for status bar at bottom
|
||||
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
|
||||
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
// Detect click-to-move inside this scroll region
|
||||
ImVec2 list_origin = ImGui::GetCursorScreenPos();
|
||||
float scroll_y = ImGui::GetScrollY();
|
||||
float scroll_x = ImGui::GetScrollX();
|
||||
std::size_t rowoffs = 0; // we render from the first line; scrolling is handled by ImGui
|
||||
const auto &lines = buf->Rows();
|
||||
std::size_t cy = buf->Cury();
|
||||
std::size_t cx = buf->Curx();
|
||||
const float line_h = ImGui::GetTextLineHeight();
|
||||
const float row_h = ImGui::GetTextLineHeightWithSpacing();
|
||||
const float space_w = ImGui::CalcTextSize(" ").x;
|
||||
|
||||
// Two-way sync between Buffer::Rowoffs and ImGui scroll position:
|
||||
// - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it.
|
||||
// - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view.
|
||||
// This prevents clicks/wheel from being immediately overridden by stale offsets.
|
||||
static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
|
||||
static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs
|
||||
|
||||
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
||||
const long buf_coloffs = static_cast<long>(buf->Coloffs());
|
||||
|
||||
// Detect programmatic change (e.g., page_down command changed rowoffs)
|
||||
// Use SetNextWindowScroll BEFORE BeginChild to set initial scroll position
|
||||
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
|
||||
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
||||
ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y));
|
||||
}
|
||||
if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
|
||||
float target_x = static_cast<float>(buf_coloffs) * space_w;
|
||||
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
||||
ImGui::SetNextWindowScroll(ImVec2(target_x, target_y));
|
||||
}
|
||||
|
||||
// Reserve space for status bar at bottom
|
||||
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
|
||||
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
|
||||
// Get child window position and scroll for click handling
|
||||
ImVec2 child_window_pos = ImGui::GetWindowPos();
|
||||
float scroll_y = ImGui::GetScrollY();
|
||||
float scroll_x = ImGui::GetScrollX();
|
||||
std::size_t rowoffs = 0; // we render from the first line; scrolling is handled by ImGui
|
||||
|
||||
// Synchronize buffer offsets from ImGui scroll if user scrolled manually
|
||||
bool forced_scroll = false;
|
||||
{
|
||||
static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
|
||||
static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs
|
||||
static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
|
||||
static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels
|
||||
static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
|
||||
static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels
|
||||
|
||||
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
||||
const long buf_coloffs = static_cast<long>(buf->Coloffs());
|
||||
const long scroll_top = static_cast<long>(scroll_y / row_h);
|
||||
const long scroll_left = static_cast<long>(scroll_x / space_w);
|
||||
|
||||
// Detect programmatic change (e.g., keyboard navigation ensured visibility)
|
||||
// Check if rowoffs was programmatically changed this frame
|
||||
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
|
||||
ImGui::SetScrollY(static_cast<float>(buf_rowoffs) * row_h);
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
forced_scroll = true;
|
||||
}
|
||||
if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
|
||||
ImGui::SetScrollX(static_cast<float>(buf_coloffs) * space_w);
|
||||
scroll_x = ImGui::GetScrollX();
|
||||
forced_scroll = true;
|
||||
}
|
||||
// If user scrolled, update buffer offsets accordingly
|
||||
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
|
||||
|
||||
// If user scrolled (not programmatic), update buffer offsets accordingly
|
||||
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y && !forced_scroll) {
|
||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
||||
mbuf->Coloffs());
|
||||
}
|
||||
}
|
||||
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x) {
|
||||
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x && !forced_scroll) {
|
||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||
mbuf->SetOffsets(mbuf->Rowoffs(),
|
||||
static_cast<std::size_t>(std::max(0L, scroll_left)));
|
||||
@@ -122,14 +133,15 @@ GUIRenderer::Draw(Editor &ed)
|
||||
}
|
||||
|
||||
// Update trackers for next frame
|
||||
prev_buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
||||
prev_buf_coloffs = static_cast<long>(buf->Coloffs());
|
||||
prev_scroll_y = ImGui::GetScrollY();
|
||||
prev_scroll_x = ImGui::GetScrollX();
|
||||
prev_scroll_y = scroll_y;
|
||||
prev_scroll_x = scroll_x;
|
||||
}
|
||||
prev_buf_rowoffs = buf_rowoffs;
|
||||
prev_buf_coloffs = buf_coloffs;
|
||||
|
||||
// Synchronize cursor and scrolling.
|
||||
// Ensure the cursor is visible even on the first frame or when it didn't move,
|
||||
// unless we already forced scrolling from Buffer::Rowoffs this frame.
|
||||
// Ensure the cursor is visible, but avoid aggressive centering so that
|
||||
// the same lines remain visible until the cursor actually goes off-screen.
|
||||
{
|
||||
// Compute visible row range using the child window height
|
||||
float child_h = ImGui::GetWindowHeight();
|
||||
@@ -139,50 +151,93 @@ GUIRenderer::Draw(Editor &ed)
|
||||
vis_rows = 1;
|
||||
long last_row = first_row + vis_rows - 1;
|
||||
|
||||
if (!forced_scroll) {
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
|
||||
float max_y = ImGui::GetScrollMaxY();
|
||||
if (target < 0.f)
|
||||
target = 0.f;
|
||||
if (max_y >= 0.f && target > max_y)
|
||||
target = max_y;
|
||||
ImGui::SetScrollY(target);
|
||||
// refresh local variables
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
first_row = static_cast<long>(scroll_y / row_h);
|
||||
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());
|
||||
}
|
||||
}
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row) {
|
||||
// Scroll just enough to bring the cursor line to the top
|
||||
float target = static_cast<float>(cyr) * row_h;
|
||||
if (target < 0.f)
|
||||
target = 0.f;
|
||||
float max_y = ImGui::GetScrollMaxY();
|
||||
if (max_y >= 0.f && target > max_y)
|
||||
target = max_y;
|
||||
ImGui::SetScrollY(target);
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
first_row = static_cast<long>(scroll_y / row_h);
|
||||
last_row = first_row + vis_rows - 1;
|
||||
} else if (cyr > last_row) {
|
||||
// Scroll just enough to bring the cursor line to the bottom
|
||||
long new_first = cyr - vis_rows + 1;
|
||||
if (new_first < 0)
|
||||
new_first = 0;
|
||||
float target = static_cast<float>(new_first) * row_h;
|
||||
float max_y = ImGui::GetScrollMaxY();
|
||||
if (target < 0.f)
|
||||
target = 0.f;
|
||||
if (max_y >= 0.f && target > max_y)
|
||||
target = max_y;
|
||||
ImGui::SetScrollY(target);
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
first_row = static_cast<long>(scroll_y / row_h);
|
||||
last_row = first_row + vis_rows - 1;
|
||||
}
|
||||
|
||||
// Horizontal scroll: ensure cursor column is visible
|
||||
float child_w = ImGui::GetWindowWidth();
|
||||
long vis_cols = static_cast<long>(child_w / space_w);
|
||||
if (vis_cols < 1)
|
||||
vis_cols = 1;
|
||||
long first_col = static_cast<long>(scroll_x / space_w);
|
||||
long last_col = first_col + vis_cols - 1;
|
||||
|
||||
// Compute cursor's rendered X position (accounting for tabs)
|
||||
std::size_t cursor_rx = 0;
|
||||
if (cy < lines.size()) {
|
||||
std::string cur_line = static_cast<std::string>(lines[cy]);
|
||||
const std::size_t tabw = 8;
|
||||
for (std::size_t i = 0; i < cx && i < cur_line.size(); ++i) {
|
||||
if (cur_line[i] == '\t') {
|
||||
cursor_rx += tabw - (cursor_rx % tabw);
|
||||
} else {
|
||||
cursor_rx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
long cxr = static_cast<long>(cursor_rx);
|
||||
if (cxr < first_col || cxr > last_col) {
|
||||
float target_x = static_cast<float>(cxr) * space_w;
|
||||
// Center horizontally if possible
|
||||
target_x -= (child_w / 2.0f);
|
||||
if (target_x < 0.f)
|
||||
target_x = 0.f;
|
||||
float max_x = ImGui::GetScrollMaxX();
|
||||
if (max_x >= 0.f && target_x > max_x)
|
||||
target_x = max_x;
|
||||
ImGui::SetScrollX(target_x);
|
||||
scroll_x = ImGui::GetScrollX();
|
||||
}
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
// Cache current horizontal offset in rendered columns for click handling
|
||||
const std::size_t coloffs_now = buf->Coloffs();
|
||||
|
||||
// Handle mouse click before rendering to avoid dependent on drawn items
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||
// 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;
|
||||
long vy = static_cast<long>(vy_f);
|
||||
if (vy < 0)
|
||||
vy = 0;
|
||||
// Compute content-relative position accounting for scroll
|
||||
// mp.y - child_window_pos.y gives us pixels from top of child window
|
||||
// Adding scroll_y gives us pixels from top of content (buffer row 0)
|
||||
float content_y = (mp.y - child_window_pos.y) + scroll_y;
|
||||
long by_l = static_cast<long>(content_y / row_h);
|
||||
if (by_l < 0)
|
||||
by_l = 0;
|
||||
|
||||
// Clamp vy within visible content height to avoid huge jumps
|
||||
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
||||
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
|
||||
float child_h = (cr_max.y - cr_min.y);
|
||||
long vis_rows = static_cast<long>(child_h / row_h);
|
||||
if (vis_rows < 1)
|
||||
vis_rows = 1;
|
||||
if (vy >= vis_rows)
|
||||
vy = vis_rows - 1;
|
||||
|
||||
// Translate viewport row to buffer row using Buffer::Rowoffs
|
||||
std::size_t by = buf->Rowoffs() + static_cast<std::size_t>(vy);
|
||||
// Convert to buffer row
|
||||
std::size_t by = static_cast<std::size_t>(by_l);
|
||||
if (by >= lines.size()) {
|
||||
if (!lines.empty())
|
||||
by = lines.size() - 1;
|
||||
@@ -190,58 +245,46 @@ GUIRenderer::Draw(Editor &ed)
|
||||
by = 0;
|
||||
}
|
||||
|
||||
// Compute desired pixel X inside the viewport content (subtract horizontal scroll)
|
||||
float px = (mp.x - list_origin.x - scroll_x);
|
||||
if (px < 0.0f)
|
||||
px = 0.0f;
|
||||
// Compute click X position relative to left edge of child window (in pixels)
|
||||
// This gives us the visual offset from the start of displayed content
|
||||
float visual_x = mp.x - child_window_pos.x;
|
||||
if (visual_x < 0.0f)
|
||||
visual_x = 0.0f;
|
||||
|
||||
// Convert visual pixel offset to rendered column, then add coloffs_now
|
||||
// to get the absolute rendered column in the buffer
|
||||
std::size_t clicked_rx = static_cast<std::size_t>(visual_x / space_w) + coloffs_now;
|
||||
|
||||
// Empty buffer guard: if there are no lines yet, just move to 0:0
|
||||
if (lines.empty()) {
|
||||
Execute(ed, CommandId::MoveCursorTo, std::string("0:0"));
|
||||
} else {
|
||||
// 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.
|
||||
// Convert rendered column (clicked_rx) to source column accounting for tabs
|
||||
std::string line_clicked = static_cast<std::string>(lines[by]);
|
||||
const std::size_t tabw = 8;
|
||||
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
|
||||
// then translate to viewport-space by subtracting Coloffs.
|
||||
std::size_t coloffs = buf->Coloffs();
|
||||
std::size_t rx_abs = 0; // absolute rendered column
|
||||
std::size_t i = 0; // source column iterator
|
||||
|
||||
// Fast-forward i until rx_abs >= coloffs to align with leftmost visible column
|
||||
if (!line_clicked.empty() && coloffs > 0) {
|
||||
while (i < line_clicked.size() && rx_abs < coloffs) {
|
||||
if (line_clicked[i] == '\t') {
|
||||
rx_abs += (tabw - (rx_abs % tabw));
|
||||
} else {
|
||||
rx_abs += 1;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
// Now search for closest source column to clicked px within/after viewport
|
||||
std::size_t best_col = i; // default to first visible column
|
||||
// Iterate through source columns, computing rendered position, to find closest match
|
||||
std::size_t rx = 0; // rendered column position
|
||||
std::size_t best_col = 0;
|
||||
float best_dist = std::numeric_limits<float>::infinity();
|
||||
while (true) {
|
||||
// For i in [current..size], evaluate candidate including the implicit end position
|
||||
std::size_t rx_view = (rx_abs >= coloffs) ? (rx_abs - coloffs) : 0;
|
||||
float rx_px = static_cast<float>(rx_view) * space_w;
|
||||
float dist = std::fabs(px - rx_px);
|
||||
if (dist <= best_dist) {
|
||||
float clicked_rx_f = static_cast<float>(clicked_rx);
|
||||
|
||||
for (std::size_t i = 0; i <= line_clicked.size(); ++i) {
|
||||
// Check current position
|
||||
float dist = std::fabs(clicked_rx_f - static_cast<float>(rx));
|
||||
if (dist < best_dist) {
|
||||
best_dist = dist;
|
||||
best_col = i;
|
||||
}
|
||||
if (i == line_clicked.size())
|
||||
break;
|
||||
// advance to next source column
|
||||
if (line_clicked[i] == '\t') {
|
||||
rx_abs += (tabw - (rx_abs % tabw));
|
||||
} else {
|
||||
rx_abs += 1;
|
||||
|
||||
// Advance to next position if not at end
|
||||
if (i < line_clicked.size()) {
|
||||
if (line_clicked[i] == '\t') {
|
||||
rx += (tabw - (rx % tabw));
|
||||
} else {
|
||||
rx += 1;
|
||||
}
|
||||
}
|
||||
++i;
|
||||
}
|
||||
|
||||
// Dispatch absolute buffer coordinates (row:col)
|
||||
@@ -250,8 +293,6 @@ GUIRenderer::Draw(Editor &ed)
|
||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||
}
|
||||
}
|
||||
// Cache current horizontal offset in rendered columns
|
||||
const std::size_t coloffs_now = buf->Coloffs();
|
||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
||||
// Capture the screen position before drawing the line
|
||||
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
||||
@@ -329,50 +370,94 @@ GUIRenderer::Draw(Editor &ed)
|
||||
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) {
|
||||
char c = line[src];
|
||||
if (c == '\t') {
|
||||
std::size_t adv = (tabw - (rx_abs_draw % tabw));
|
||||
expanded.append(adv, ' ');
|
||||
rx_abs_draw += adv;
|
||||
} else {
|
||||
expanded.push_back(c);
|
||||
rx_abs_draw += 1;
|
||||
}
|
||||
}
|
||||
// Emit entire line to an expanded buffer (tabs -> spaces)
|
||||
for (std::size_t src = 0; src < line.size(); ++src) {
|
||||
char c = line[src];
|
||||
if (c == '\t') {
|
||||
std::size_t adv = (tabw - (rx_abs_draw % tabw));
|
||||
expanded.append(adv, ' ');
|
||||
rx_abs_draw += adv;
|
||||
} else {
|
||||
expanded.push_back(c);
|
||||
rx_abs_draw += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw syntax-colored runs (text above background highlights)
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(*buf, static_cast<int>(i), buf->Version());
|
||||
// Helper to convert a src column to expanded rx position
|
||||
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
|
||||
std::size_t rx = 0;
|
||||
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
|
||||
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||
}
|
||||
return rx;
|
||||
};
|
||||
for (const auto &sp: lh.spans) {
|
||||
std::size_t rx_s = src_to_rx_full(static_cast<std::size_t>(std::max(0, sp.col_start)));
|
||||
std::size_t rx_e = src_to_rx_full(static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
|
||||
if (rx_e <= coloffs_now)
|
||||
continue;
|
||||
std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
|
||||
std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
|
||||
if (vx0 >= expanded.size()) continue;
|
||||
vx1 = std::min<std::size_t>(vx1, expanded.size());
|
||||
if (vx1 <= vx0) continue;
|
||||
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
|
||||
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||
ImGui::GetWindowDrawList()->AddText(p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
|
||||
}
|
||||
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
|
||||
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + line_h));
|
||||
} else {
|
||||
// No syntax: draw as one run
|
||||
ImGui::TextUnformatted(expanded.c_str());
|
||||
}
|
||||
// Draw syntax-colored runs (text above background highlights)
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
kte::LineHighlight lh = buf->Highlighter()->GetLine(
|
||||
*buf, static_cast<int>(i), buf->Version());
|
||||
// Sanitize spans defensively: clamp to [0, line.size()], ensure end>=start, drop empties
|
||||
struct SSpan {
|
||||
std::size_t s;
|
||||
std::size_t e;
|
||||
kte::TokenKind k;
|
||||
};
|
||||
std::vector<SSpan> spans;
|
||||
spans.reserve(lh.spans.size());
|
||||
const std::size_t line_len = line.size();
|
||||
for (const auto &sp: lh.spans) {
|
||||
int s_raw = sp.col_start;
|
||||
int e_raw = sp.col_end;
|
||||
if (e_raw < s_raw)
|
||||
std::swap(e_raw, s_raw);
|
||||
std::size_t s = static_cast<std::size_t>(std::max(
|
||||
0, std::min(s_raw, static_cast<int>(line_len))));
|
||||
std::size_t e = static_cast<std::size_t>(std::max(
|
||||
static_cast<int>(s), std::min(e_raw, static_cast<int>(line_len))));
|
||||
if (e <= s)
|
||||
continue;
|
||||
spans.push_back(SSpan{s, e, sp.kind});
|
||||
}
|
||||
std::sort(spans.begin(), spans.end(), [](const SSpan &a, const SSpan &b) {
|
||||
return a.s < b.s;
|
||||
});
|
||||
|
||||
// Helper to convert a src column to expanded rx position
|
||||
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
|
||||
std::size_t rx = 0;
|
||||
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
|
||||
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||
}
|
||||
return rx;
|
||||
};
|
||||
|
||||
for (const auto &sp: spans) {
|
||||
std::size_t rx_s = src_to_rx_full(sp.s);
|
||||
std::size_t rx_e = src_to_rx_full(sp.e);
|
||||
if (rx_e <= coloffs_now)
|
||||
continue; // fully left of viewport
|
||||
// Clamp to visible portion and expanded length
|
||||
std::size_t draw_start = (rx_s > coloffs_now) ? rx_s : coloffs_now;
|
||||
if (draw_start >= expanded.size())
|
||||
continue; // fully right of expanded text
|
||||
std::size_t draw_end = std::min<std::size_t>(rx_e, expanded.size());
|
||||
if (draw_end <= draw_start)
|
||||
continue;
|
||||
// Screen position is relative to coloffs_now
|
||||
std::size_t screen_x = draw_start - coloffs_now;
|
||||
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.k));
|
||||
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(screen_x) * space_w,
|
||||
line_pos.y);
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
p, col, expanded.c_str() + draw_start, expanded.c_str() + draw_end);
|
||||
}
|
||||
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
|
||||
// Use row_h (with spacing) to match click calculation and ensure consistent line positions.
|
||||
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
||||
} else {
|
||||
// No syntax: draw as one run, accounting for horizontal scroll offset
|
||||
if (coloffs_now < expanded.size()) {
|
||||
ImVec2 p = ImVec2(line_pos.x, line_pos.y);
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
p, ImGui::GetColorU32(ImGuiCol_Text),
|
||||
expanded.c_str() + coloffs_now);
|
||||
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
||||
} else {
|
||||
// Line is fully scrolled out of view horizontally
|
||||
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a visible cursor indicator on the current line
|
||||
if (i == cy) {
|
||||
@@ -386,7 +471,19 @@ GUIRenderer::Draw(Editor &ed)
|
||||
}
|
||||
// Convert to viewport x by subtracting horizontal col offset
|
||||
std::size_t rx_viewport = (rx_abs > coloffs_now) ? (rx_abs - coloffs_now) : 0;
|
||||
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(rx_viewport) * space_w, line_pos.y);
|
||||
// For proportional fonts (Linux GUI), avoid accumulating drift by computing
|
||||
// the exact pixel width of the expanded substring up to the cursor.
|
||||
// expanded contains the line with tabs expanded to spaces and is what we draw.
|
||||
float cursor_px = 0.0f;
|
||||
if (rx_viewport > 0 && coloffs_now < expanded.size()) {
|
||||
std::size_t start = coloffs_now;
|
||||
std::size_t end = std::min(expanded.size(), start + rx_viewport);
|
||||
// Measure substring width in pixels
|
||||
ImVec2 sz = ImGui::CalcTextSize(expanded.c_str() + start,
|
||||
expanded.c_str() + end);
|
||||
cursor_px = sz.x;
|
||||
}
|
||||
ImVec2 p0 = ImVec2(line_pos.x + cursor_px, line_pos.y);
|
||||
ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h);
|
||||
ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||
@@ -761,4 +858,4 @@ GUIRenderer::Draw(Editor &ed)
|
||||
ed.SetFilePickerVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
ImGuiRenderer.h
Normal file
14
ImGuiRenderer.h
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* ImGuiRenderer - ImGui-based renderer for GUI mode
|
||||
*/
|
||||
#pragma once
|
||||
#include "Renderer.h"
|
||||
|
||||
class ImGuiRenderer final : public Renderer {
|
||||
public:
|
||||
ImGuiRenderer() = default;
|
||||
|
||||
~ImGuiRenderer() override = default;
|
||||
|
||||
void Draw(Editor &ed) override;
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
* InputHandler.h - input abstraction and mapping to commands
|
||||
*/
|
||||
#ifndef KTE_INPUT_HANDLER_H
|
||||
#define KTE_INPUT_HANDLER_H
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
#include "Command.h"
|
||||
|
||||
class Editor; // fwd decl
|
||||
|
||||
|
||||
// Result of translating raw input into an editor command.
|
||||
struct MappedInput {
|
||||
@@ -21,9 +21,11 @@ class InputHandler {
|
||||
public:
|
||||
virtual ~InputHandler() = default;
|
||||
|
||||
// Optional: attach current Editor so handlers can consult editor state (e.g., universal argument)
|
||||
// Default implementation does nothing.
|
||||
virtual void Attach(Editor *) {}
|
||||
|
||||
// Poll for input and translate it to a command. Non-blocking.
|
||||
// Returns true if a command is available in 'out'. Returns false if no input.
|
||||
virtual bool Poll(MappedInput &out) = 0;
|
||||
};
|
||||
|
||||
#endif // KTE_INPUT_HANDLER_H
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
#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
|
||||
@@ -42,6 +42,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
case 'a':
|
||||
out = CommandId::MarkAllAndJumpEnd;
|
||||
return true;
|
||||
case 'k':
|
||||
out = CommandId::CenterOnCursor; // C-k k center current line
|
||||
return true;
|
||||
case 'b':
|
||||
out = CommandId::BufferSwitchStart;
|
||||
return true;
|
||||
@@ -215,4 +218,4 @@ KLookupEscCommand(const int ascii_key, CommandId &out) -> bool
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* KKeymap.h - mapping for k-command (C-k prefix) keys to CommandId
|
||||
*/
|
||||
#ifndef KTE_KKEYMAP_H
|
||||
#define KTE_KKEYMAP_H
|
||||
|
||||
#pragma once
|
||||
#include "Command.h"
|
||||
|
||||
|
||||
@@ -29,6 +27,4 @@ KLowerAscii(const int key)
|
||||
if (key >= 'A' && key <= 'Z')
|
||||
return key + ('a' - 'A');
|
||||
return key;
|
||||
}
|
||||
|
||||
#endif // KTE_KKEYMAP_H
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// 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
|
||||
@@ -1,41 +0,0 @@
|
||||
#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
|
||||
@@ -1,88 +0,0 @@
|
||||
#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
|
||||
@@ -1,14 +0,0 @@
|
||||
// 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
|
||||
@@ -1,16 +0,0 @@
|
||||
#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
|
||||
79
OptimizedSearch.cc
Normal file
79
OptimizedSearch.cc
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "OptimizedSearch.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
|
||||
void
|
||||
OptimizedSearch::build_bad_char(const std::string &pattern)
|
||||
{
|
||||
if (pattern == last_pat_)
|
||||
return;
|
||||
last_pat_ = pattern;
|
||||
std::fill(bad_char_.begin(), bad_char_.end(), -1);
|
||||
for (std::size_t i = 0; i < pattern.size(); ++i) {
|
||||
bad_char_[static_cast<unsigned char>(pattern[i])] = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::size_t
|
||||
OptimizedSearch::find_first(const std::string &text, const std::string &pattern, std::size_t start)
|
||||
{
|
||||
const std::size_t n = text.size();
|
||||
const std::size_t m = pattern.size();
|
||||
if (m == 0)
|
||||
return start <= n ? start : std::string::npos;
|
||||
if (m > n || start >= n)
|
||||
return std::string::npos;
|
||||
build_bad_char(pattern);
|
||||
std::size_t s = start;
|
||||
while (s <= n - m) {
|
||||
std::size_t j = m;
|
||||
while (j > 0 && pattern[j - 1] == text[s + j - 1]) {
|
||||
--j;
|
||||
}
|
||||
if (j == 0) {
|
||||
return s; // match found
|
||||
}
|
||||
unsigned char badc = static_cast<unsigned char>(text[s + j - 1]);
|
||||
int bcidx = bad_char_[badc];
|
||||
std::size_t shift = (j - 1 > static_cast<std::size_t>(bcidx))
|
||||
? (j - 1 - static_cast<std::size_t>(bcidx))
|
||||
: 1;
|
||||
s += shift;
|
||||
}
|
||||
return std::string::npos;
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::size_t>
|
||||
OptimizedSearch::find_all(const std::string &text, const std::string &pattern, std::size_t start)
|
||||
{
|
||||
std::vector<std::size_t> res;
|
||||
const std::size_t n = text.size();
|
||||
const std::size_t m = pattern.size();
|
||||
if (m == 0)
|
||||
return res;
|
||||
if (m > n || start >= n)
|
||||
return res;
|
||||
build_bad_char(pattern);
|
||||
std::size_t s = start;
|
||||
while (s <= n - m) {
|
||||
std::size_t j = m;
|
||||
while (j > 0 && pattern[j - 1] == text[s + j - 1]) {
|
||||
--j;
|
||||
}
|
||||
if (j == 0) {
|
||||
res.push_back(s);
|
||||
s += m; // non-overlapping
|
||||
continue;
|
||||
}
|
||||
unsigned char badc = static_cast<unsigned char>(text[s + j - 1]);
|
||||
int bcidx = bad_char_[badc];
|
||||
std::size_t shift = (j - 1 > static_cast<std::size_t>(bcidx))
|
||||
? (j - 1 - static_cast<std::size_t>(bcidx))
|
||||
: 1;
|
||||
s += shift;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
23
OptimizedSearch.h
Normal file
23
OptimizedSearch.h
Normal file
@@ -0,0 +1,23 @@
|
||||
// OptimizedSearch.h - Boyer–Moore (bad character) based substring search
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class OptimizedSearch {
|
||||
public:
|
||||
OptimizedSearch() = default;
|
||||
|
||||
// Find first occurrence at or after start. Returns npos if not found.
|
||||
std::size_t find_first(const std::string &text, const std::string &pattern, std::size_t start = 0);
|
||||
|
||||
// Find all non-overlapping matches at or after start. Returns starting indices.
|
||||
std::vector<std::size_t> find_all(const std::string &text, const std::string &pattern, std::size_t start = 0);
|
||||
|
||||
private:
|
||||
std::array<int, 256> bad_char_{};
|
||||
std::string last_pat_;
|
||||
|
||||
void build_bad_char(const std::string &pattern);
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* PieceTable.h - Alternative to GapBuffer using a piece table representation
|
||||
*/
|
||||
#ifndef KTE_PIECETABLE_H
|
||||
#define KTE_PIECETABLE_H
|
||||
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -93,6 +91,4 @@ private:
|
||||
mutable std::string materialized_;
|
||||
mutable bool dirty_ = true;
|
||||
std::size_t total_size_ = 0;
|
||||
};
|
||||
|
||||
#endif // KTE_PIECETABLE_H
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
#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
|
||||
@@ -1,18 +0,0 @@
|
||||
// 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
|
||||
988
QtFrontend.cc
Normal file
988
QtFrontend.cc
Normal file
@@ -0,0 +1,988 @@
|
||||
#include "QtFrontend.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QWidget>
|
||||
#include <QKeyEvent>
|
||||
#include <QTimer>
|
||||
#include <QScreen>
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
#include <QFontDatabase>
|
||||
#include <QFileDialog>
|
||||
#include <QFontDialog>
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
#include <QWheelEvent>
|
||||
#include <regex>
|
||||
|
||||
#include "Editor.h"
|
||||
#include "Command.h"
|
||||
#include "Buffer.h"
|
||||
#include "GUITheme.h"
|
||||
#include "Highlight.h"
|
||||
|
||||
namespace {
|
||||
class MainWindow : public QWidget {
|
||||
public:
|
||||
explicit MainWindow(class QtInputHandler &ih, QWidget *parent = nullptr)
|
||||
: QWidget(parent), input_(ih)
|
||||
{
|
||||
// Match ImGui window title format
|
||||
setWindowTitle(QStringLiteral("kge - kyle's graphical editor ")
|
||||
+ QStringLiteral(KTE_VERSION_STR));
|
||||
resize(1280, 800);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
}
|
||||
|
||||
|
||||
bool WasClosed() const
|
||||
{
|
||||
return closed_;
|
||||
}
|
||||
|
||||
|
||||
void SetEditor(Editor *ed)
|
||||
{
|
||||
ed_ = ed;
|
||||
}
|
||||
|
||||
|
||||
void SetFontFamilyAndSize(QString family, int px)
|
||||
{
|
||||
if (family.isEmpty())
|
||||
family = QStringLiteral("Brass Mono");
|
||||
if (px <= 0)
|
||||
px = 18;
|
||||
font_family_ = std::move(family);
|
||||
font_px_ = px;
|
||||
update();
|
||||
}
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *event) override
|
||||
{
|
||||
// Route to editor keymap; if handled, accept and stop propagation so
|
||||
// Qt doesn't trigger any default widget shortcuts.
|
||||
if (input_.ProcessKeyEvent(*event)) {
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QWidget::keyPressEvent(event);
|
||||
}
|
||||
|
||||
|
||||
void paintEvent(QPaintEvent *event) override
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::TextAntialiasing, true);
|
||||
|
||||
// Colors from GUITheme palette (Qt branch)
|
||||
auto to_qcolor = [](const KteColor &c) -> QColor {
|
||||
int r = int(std::round(c.x * 255.0f));
|
||||
int g = int(std::round(c.y * 255.0f));
|
||||
int b = int(std::round(c.z * 255.0f));
|
||||
int a = int(std::round(c.w * 255.0f));
|
||||
return QColor(r, g, b, a);
|
||||
};
|
||||
const auto pal = kte::GetPalette();
|
||||
const QColor bg = to_qcolor(pal.bg);
|
||||
const QColor fg = to_qcolor(pal.fg);
|
||||
const QColor sel_bg = to_qcolor(pal.sel_bg);
|
||||
const QColor cur_bg = to_qcolor(pal.cur_bg);
|
||||
const QColor status_bg = to_qcolor(pal.status_bg);
|
||||
const QColor status_fg = to_qcolor(pal.status_fg);
|
||||
|
||||
// Background
|
||||
p.fillRect(rect(), bg);
|
||||
|
||||
// Font/metrics (configured or defaults)
|
||||
QFont f(font_family_, font_px_);
|
||||
p.setFont(f);
|
||||
QFontMetrics fm(f);
|
||||
const int line_h = fm.height();
|
||||
const int ch_w = std::max(1, fm.horizontalAdvance(QStringLiteral(" ")));
|
||||
|
||||
// Layout metrics
|
||||
const int pad_l = 8;
|
||||
const int pad_t = 6;
|
||||
const int pad_r = 8;
|
||||
const int pad_b = 6;
|
||||
const int status_h = line_h + 6; // status bar height
|
||||
|
||||
// Content area (text viewport)
|
||||
const QRect content_rect(pad_l,
|
||||
pad_t,
|
||||
width() - pad_l - pad_r,
|
||||
height() - pad_t - pad_b - status_h);
|
||||
|
||||
// Text viewport occupies all content area (no extra title row)
|
||||
QRect viewport(content_rect.x(), content_rect.y(), content_rect.width(), content_rect.height());
|
||||
|
||||
// Draw buffer contents
|
||||
if (ed_ && viewport.height() > 0 && viewport.width() > 0) {
|
||||
const Buffer *buf = ed_->CurrentBuffer();
|
||||
if (buf) {
|
||||
const auto &lines = buf->Rows();
|
||||
const std::size_t nrows = lines.size();
|
||||
const std::size_t rowoffs = buf->Rowoffs();
|
||||
const std::size_t coloffs = buf->Coloffs();
|
||||
const std::size_t cy = buf->Cury();
|
||||
const std::size_t cx = buf->Curx();
|
||||
|
||||
// Visible line count
|
||||
const int max_lines = (line_h > 0) ? (viewport.height() / line_h) : 0;
|
||||
const std::size_t last_row = std::min<std::size_t>(
|
||||
nrows, rowoffs + std::max(0, max_lines));
|
||||
|
||||
// Tab width: follow ImGuiRenderer default of 4
|
||||
const std::size_t tabw = 4;
|
||||
|
||||
// Prepare painter clip to viewport
|
||||
p.save();
|
||||
p.setClipRect(viewport);
|
||||
|
||||
// Iterate visible lines
|
||||
for (std::size_t i = rowoffs, vis_idx = 0; i < last_row; ++i, ++vis_idx) {
|
||||
const auto &line = static_cast<const std::string &>(lines[i]);
|
||||
const int y = viewport.y() + static_cast<int>(vis_idx) * line_h;
|
||||
const int baseline = y + fm.ascent();
|
||||
|
||||
// Helper: convert src col -> rx with tab expansion
|
||||
auto src_to_rx_line = [&](std::size_t src_col) -> std::size_t {
|
||||
std::size_t rx = 0;
|
||||
for (std::size_t k = 0; k < src_col && k < line.size(); ++k) {
|
||||
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||
}
|
||||
return rx;
|
||||
};
|
||||
|
||||
// Search-match background highlights first (under text)
|
||||
if (ed_->SearchActive() && !ed_->SearchQuery().empty()) {
|
||||
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
|
||||
// Compute ranges per line (source indices)
|
||||
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;
|
||||
std::size_t 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 &) {
|
||||
// Invalid regex: ignore, status line already shows errors
|
||||
}
|
||||
} else {
|
||||
const std::string &q = ed_->SearchQuery();
|
||||
if (!q.empty()) {
|
||||
std::size_t pos = 0;
|
||||
while ((pos = line.find(q, pos)) != std::string::npos) {
|
||||
hl_src_ranges.emplace_back(pos, pos + q.size());
|
||||
pos += q.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hl_src_ranges.empty()) {
|
||||
const bool has_current =
|
||||
ed_->SearchMatchLen() > 0 && ed_->SearchMatchY() == i;
|
||||
const std::size_t cur_x = has_current ? ed_->SearchMatchX() : 0;
|
||||
const 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_s = src_to_rx_line(sx);
|
||||
std::size_t rx_e = src_to_rx_line(ex);
|
||||
if (rx_e <= coloffs)
|
||||
continue; // fully left of view
|
||||
int vx0 = viewport.x() + static_cast<int>((
|
||||
(rx_s > coloffs ? rx_s - coloffs : 0)
|
||||
* ch_w));
|
||||
int vx1 = viewport.x() + static_cast<int>((
|
||||
(rx_e - coloffs) * ch_w));
|
||||
QRect r(vx0, y, std::max(0, vx1 - vx0), line_h);
|
||||
if (r.width() <= 0)
|
||||
continue;
|
||||
bool is_current =
|
||||
has_current && sx == cur_x && ex == cur_end;
|
||||
QColor col = is_current
|
||||
? QColor(255, 220, 120, 140)
|
||||
: QColor(200, 200, 0, 90);
|
||||
p.fillRect(r, col);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Selection background (if active on this line)
|
||||
if (buf->MarkSet() && (
|
||||
i == buf->MarkCury() || i == cy || (
|
||||
i > std::min(buf->MarkCury(), cy) && i < std::max(
|
||||
buf->MarkCury(), cy)))) {
|
||||
std::size_t sx = 0, ex = 0;
|
||||
if (buf->MarkCury() == i && cy == i) {
|
||||
sx = std::min(buf->MarkCurx(), cx);
|
||||
ex = std::max(buf->MarkCurx(), cx);
|
||||
} else if (i == buf->MarkCury()) {
|
||||
sx = buf->MarkCurx();
|
||||
ex = line.size();
|
||||
} else if (i == cy) {
|
||||
sx = 0;
|
||||
ex = cx;
|
||||
} else {
|
||||
sx = 0;
|
||||
ex = line.size();
|
||||
}
|
||||
std::size_t rx_s = src_to_rx_line(sx);
|
||||
std::size_t rx_e = src_to_rx_line(ex);
|
||||
if (rx_e > coloffs) {
|
||||
int vx0 = viewport.x() + static_cast<int>((rx_s > coloffs
|
||||
? rx_s - coloffs
|
||||
: 0) * ch_w);
|
||||
int vx1 = viewport.x() + static_cast<int>(
|
||||
(rx_e - coloffs) * ch_w);
|
||||
QRect sel_r(vx0, y, std::max(0, vx1 - vx0), line_h);
|
||||
if (sel_r.width() > 0)
|
||||
p.fillRect(sel_r, sel_bg);
|
||||
}
|
||||
}
|
||||
|
||||
// Build expanded line (tabs -> spaces) for drawing
|
||||
std::string expanded;
|
||||
expanded.reserve(line.size() + 8);
|
||||
std::size_t rx_acc = 0;
|
||||
for (char c: line) {
|
||||
if (c == '\t') {
|
||||
std::size_t adv = (tabw - (rx_acc % tabw));
|
||||
expanded.append(adv, ' ');
|
||||
rx_acc += adv;
|
||||
} else {
|
||||
expanded.push_back(c);
|
||||
rx_acc += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Syntax highlighting spans or plain text
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
|
||||
HasHighlighter()) {
|
||||
kte::LineHighlight lh = buf->Highlighter()->GetLine(
|
||||
*buf, static_cast<int>(i), buf->Version());
|
||||
struct SSpan {
|
||||
std::size_t s;
|
||||
std::size_t e;
|
||||
kte::TokenKind k;
|
||||
};
|
||||
std::vector<SSpan> spans;
|
||||
spans.reserve(lh.spans.size());
|
||||
const std::size_t line_len = line.size();
|
||||
for (const auto &sp: lh.spans) {
|
||||
int s_raw = sp.col_start;
|
||||
int e_raw = sp.col_end;
|
||||
if (e_raw < s_raw)
|
||||
std::swap(e_raw, s_raw);
|
||||
std::size_t s = static_cast<std::size_t>(std::max(
|
||||
0, std::min(s_raw, (int) line_len)));
|
||||
std::size_t e = static_cast<std::size_t>(std::max(
|
||||
(int) s, std::min(e_raw, (int) line_len)));
|
||||
if (s < e)
|
||||
spans.push_back({s, e, sp.kind});
|
||||
}
|
||||
std::sort(spans.begin(), spans.end(),
|
||||
[](const SSpan &a, const SSpan &b) {
|
||||
return a.s < b.s;
|
||||
});
|
||||
|
||||
auto colorFor = [](kte::TokenKind k) -> QColor {
|
||||
// GUITheme provides colors via ImGui vector; avoid direct dependency types
|
||||
const auto v = kte::SyntaxInk(k);
|
||||
return QColor(int(v.x * 255.0f), int(v.y * 255.0f),
|
||||
int(v.z * 255.0f), int(v.w * 255.0f));
|
||||
};
|
||||
|
||||
// Helper to convert src col to expanded rx
|
||||
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
|
||||
std::size_t rx = 0;
|
||||
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
|
||||
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||
}
|
||||
return rx;
|
||||
};
|
||||
|
||||
if (spans.empty()) {
|
||||
// No highlight spans: draw the whole (visible) expanded line in default fg
|
||||
if (coloffs < expanded.size()) {
|
||||
const char *start =
|
||||
expanded.c_str() + static_cast<int>(coloffs);
|
||||
p.setPen(fg);
|
||||
p.drawText(viewport.x(), baseline,
|
||||
QString::fromUtf8(start));
|
||||
}
|
||||
} else {
|
||||
// Draw colored spans
|
||||
for (const auto &sp: spans) {
|
||||
std::size_t rx_s = src_to_rx_full(sp.s);
|
||||
std::size_t rx_e = src_to_rx_full(sp.e);
|
||||
if (rx_e <= coloffs)
|
||||
continue; // left of viewport
|
||||
std::size_t draw_start = (rx_s > coloffs)
|
||||
? rx_s
|
||||
: coloffs;
|
||||
std::size_t draw_end = std::min<std::size_t>(
|
||||
rx_e, expanded.size());
|
||||
if (draw_end <= draw_start)
|
||||
continue;
|
||||
std::size_t screen_x = draw_start - coloffs;
|
||||
int px = viewport.x() + int(screen_x * ch_w);
|
||||
int len = int(draw_end - draw_start);
|
||||
p.setPen(colorFor(sp.k));
|
||||
p.drawText(px, baseline,
|
||||
QString::fromUtf8(
|
||||
expanded.c_str() + draw_start, len));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Draw expanded text clipped by coloffs
|
||||
if (static_cast<std::size_t>(coloffs) < expanded.size()) {
|
||||
const char *start =
|
||||
expanded.c_str() + static_cast<int>(coloffs);
|
||||
p.setPen(fg);
|
||||
p.drawText(viewport.x(), baseline, QString::fromUtf8(start));
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor indicator on current line
|
||||
if (i == cy) {
|
||||
std::size_t rx_cur = src_to_rx_line(cx);
|
||||
if (rx_cur >= coloffs) {
|
||||
// Compute exact pixel x by measuring expanded substring [coloffs, rx_cur)
|
||||
std::size_t start = std::min<std::size_t>(
|
||||
coloffs, expanded.size());
|
||||
std::size_t end = std::min<
|
||||
std::size_t>(rx_cur, expanded.size());
|
||||
int px_advance = 0;
|
||||
if (end > start) {
|
||||
const QString sub = QString::fromUtf8(
|
||||
expanded.c_str() + start,
|
||||
static_cast<int>(end - start));
|
||||
px_advance = fm.horizontalAdvance(sub);
|
||||
}
|
||||
int x0 = viewport.x() + px_advance;
|
||||
QRect r(x0, y, ch_w, line_h);
|
||||
p.fillRect(r, cur_bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// Status bar
|
||||
const int bar_y = height() - status_h;
|
||||
QRect status_rect(0, bar_y, width(), status_h);
|
||||
p.fillRect(status_rect, status_bg);
|
||||
p.setPen(status_fg);
|
||||
if (ed_) {
|
||||
const int pad = 6;
|
||||
const int left_x = status_rect.x() + pad;
|
||||
const int right_x_max = status_rect.x() + status_rect.width() - pad;
|
||||
const int baseline_y = bar_y + (status_h + fm.ascent() - fm.descent()) / 2;
|
||||
|
||||
// If a prompt is active, mirror ImGui/TUI: show only the prompt across the bar
|
||||
if (ed_->PromptActive()) {
|
||||
std::string label = ed_->PromptLabel();
|
||||
std::string text = ed_->PromptText();
|
||||
|
||||
// Map $HOME to ~ for path prompts (Open/Save/Chdir)
|
||||
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 (text.rfind(home, 0) == 0) {
|
||||
std::string rest = text.substr(home.size());
|
||||
if (rest.empty())
|
||||
text = "~";
|
||||
else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\'))
|
||||
text = std::string("~") + rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string prefix;
|
||||
if (kind == Editor::PromptKind::Command)
|
||||
prefix = ": ";
|
||||
else if (!label.empty())
|
||||
prefix = label + ": ";
|
||||
|
||||
// Compose text and elide per behavior:
|
||||
const int max_w = status_rect.width() - 2 * pad;
|
||||
QString qprefix = QString::fromStdString(prefix);
|
||||
QString qtext = QString::fromStdString(text);
|
||||
int avail_w = std::max(0, max_w - fm.horizontalAdvance(qprefix));
|
||||
Qt::TextElideMode mode = Qt::ElideRight;
|
||||
if (kind == Editor::PromptKind::OpenFile ||
|
||||
kind == Editor::PromptKind::SaveAs ||
|
||||
kind == Editor::PromptKind::Chdir) {
|
||||
mode = Qt::ElideLeft;
|
||||
}
|
||||
QString shown = fm.elidedText(qtext, mode, avail_w);
|
||||
p.drawText(left_x, baseline_y, qprefix + shown);
|
||||
} else {
|
||||
// Build left segment: app/version, buffer idx/total, filename [+dirty], line count
|
||||
QString left;
|
||||
left += QStringLiteral("kge ");
|
||||
left += QStringLiteral(KTE_VERSION_STR);
|
||||
|
||||
const Buffer *buf = ed_->CurrentBuffer();
|
||||
if (buf) {
|
||||
// buffer index/total
|
||||
std::size_t total = ed_->BufferCount();
|
||||
if (total > 0) {
|
||||
std::size_t idx1 = ed_->CurrentBufferIndex() + 1; // 1-based
|
||||
left += QStringLiteral(" [");
|
||||
left += QString::number(static_cast<qlonglong>(idx1));
|
||||
left += QStringLiteral("/");
|
||||
left += QString::number(static_cast<qlonglong>(total));
|
||||
left += QStringLiteral("] ");
|
||||
} else {
|
||||
left += QStringLiteral(" ");
|
||||
}
|
||||
|
||||
// buffer display name
|
||||
std::string disp;
|
||||
try {
|
||||
disp = ed_->DisplayNameFor(*buf);
|
||||
} catch (...) {
|
||||
disp = buf->Filename();
|
||||
}
|
||||
if (disp.empty())
|
||||
disp = "[No Name]";
|
||||
left += QString::fromStdString(disp);
|
||||
if (buf->Dirty())
|
||||
left += QStringLiteral(" *");
|
||||
|
||||
// total lines suffix " <n>L"
|
||||
unsigned long lcount = static_cast<unsigned long>(buf->Rows().size());
|
||||
left += QStringLiteral(" ");
|
||||
left += QString::number(static_cast<qlonglong>(lcount));
|
||||
left += QStringLiteral("L");
|
||||
}
|
||||
|
||||
// Build right segment: cursor and mark
|
||||
QString right;
|
||||
if (buf) {
|
||||
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;
|
||||
if (have_mark)
|
||||
right = QString("%1,%2 | M: %3,%4").arg(row1).arg(col1).arg(mrow1).arg(
|
||||
mcol1);
|
||||
else
|
||||
right = QString("%1,%2 | M: not set").arg(row1).arg(col1);
|
||||
}
|
||||
|
||||
// Middle message: status text
|
||||
QString mid = QString::fromStdString(ed_->Status());
|
||||
|
||||
// Measure and layout
|
||||
int left_w = fm.horizontalAdvance(left);
|
||||
int right_w = fm.horizontalAdvance(right);
|
||||
int lx = left_x;
|
||||
int rx = std::max(left_x, right_x_max - right_w);
|
||||
|
||||
// If overlap, elide left to make space for right
|
||||
if (lx + left_w + pad > rx) {
|
||||
int max_left_w = std::max(0, rx - lx - pad);
|
||||
left = fm.elidedText(left, Qt::ElideRight, max_left_w);
|
||||
left_w = fm.horizontalAdvance(left);
|
||||
}
|
||||
|
||||
// Draw left and right
|
||||
p.drawText(lx, baseline_y, left);
|
||||
if (!right.isEmpty())
|
||||
p.drawText(rx, baseline_y, right);
|
||||
|
||||
// Middle message clipped between end of left and start of right
|
||||
int mid_left = lx + left_w + pad;
|
||||
int mid_right = std::max(mid_left, rx - pad);
|
||||
int mid_w = std::max(0, mid_right - mid_left);
|
||||
if (mid_w > 0 && !mid.isEmpty()) {
|
||||
QString mid_show = fm.elidedText(mid, Qt::ElideRight, mid_w);
|
||||
p.save();
|
||||
p.setClipRect(QRect(mid_left, bar_y, mid_w, status_h));
|
||||
p.drawText(mid_left, baseline_y, mid_show);
|
||||
p.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void resizeEvent(QResizeEvent *event) override
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
if (!ed_)
|
||||
return;
|
||||
// Update editor dimensions based on new size
|
||||
QFont f(font_family_, font_px_);
|
||||
QFontMetrics fm(f);
|
||||
const int line_h = std::max(12, fm.height());
|
||||
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral(" ")));
|
||||
const int pad_l = 8, pad_r = 8, pad_t = 6, pad_b = 6;
|
||||
const int status_h = line_h + 6;
|
||||
const int avail_w = std::max(0, width() - pad_l - pad_r);
|
||||
const int avail_h = std::max(0, height() - pad_t - pad_b - status_h);
|
||||
std::size_t rows = std::max<std::size_t>(1, (avail_h / line_h));
|
||||
std::size_t cols = std::max<std::size_t>(1, (avail_w / ch_w));
|
||||
ed_->SetDimensions(rows, cols);
|
||||
}
|
||||
|
||||
|
||||
void wheelEvent(QWheelEvent *event) override
|
||||
{
|
||||
if (!ed_) {
|
||||
QWidget::wheelEvent(event);
|
||||
return;
|
||||
}
|
||||
Buffer *buf = ed_->CurrentBuffer();
|
||||
if (!buf) {
|
||||
QWidget::wheelEvent(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Recompute metrics to map pixel deltas to rows/cols
|
||||
QFont f(font_family_, font_px_);
|
||||
QFontMetrics fm(f);
|
||||
const int line_h = std::max(12, fm.height());
|
||||
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral(" ")));
|
||||
|
||||
// Determine scroll intent: use pixelDelta when available (trackpads), otherwise angleDelta
|
||||
QPoint pixel = event->pixelDelta();
|
||||
QPoint angle = event->angleDelta();
|
||||
|
||||
double v_lines_delta = 0.0;
|
||||
double h_cols_delta = 0.0;
|
||||
|
||||
// Horizontal scroll with Shift or explicit horizontal delta
|
||||
bool horiz_mode = (event->modifiers() & Qt::ShiftModifier) || (!pixel.isNull() && pixel.x() != 0) || (
|
||||
!angle.isNull() && angle.x() != 0);
|
||||
|
||||
if (!pixel.isNull()) {
|
||||
// Trackpad smooth scrolling (pixels)
|
||||
v_lines_delta = -static_cast<double>(pixel.y()) / std::max(1, line_h);
|
||||
h_cols_delta = -static_cast<double>(pixel.x()) / std::max(1, ch_w);
|
||||
} else if (!angle.isNull()) {
|
||||
// Mouse wheel: 120 units per notch; map one notch to 3 lines similar to ImGui UX
|
||||
v_lines_delta = -static_cast<double>(angle.y()) / 120.0 * 3.0;
|
||||
// For horizontal wheels, each notch scrolls 8 columns
|
||||
h_cols_delta = -static_cast<double>(angle.x()) / 120.0 * 8.0;
|
||||
}
|
||||
|
||||
// Accumulate fractional deltas across events
|
||||
v_scroll_accum_ += v_lines_delta;
|
||||
h_scroll_accum_ += h_cols_delta;
|
||||
|
||||
int d_rows = 0;
|
||||
int d_cols = 0;
|
||||
if (std::fabs(v_scroll_accum_) >= 1.0 && (!horiz_mode || std::fabs(v_scroll_accum_) > std::fabs(
|
||||
h_scroll_accum_))) {
|
||||
d_rows = static_cast<int>(v_scroll_accum_);
|
||||
v_scroll_accum_ -= d_rows;
|
||||
}
|
||||
if (std::fabs(h_scroll_accum_) >= 1.0 && (horiz_mode || std::fabs(h_scroll_accum_) >= std::fabs(
|
||||
v_scroll_accum_))) {
|
||||
d_cols = static_cast<int>(h_scroll_accum_);
|
||||
h_scroll_accum_ -= d_cols;
|
||||
}
|
||||
|
||||
if (d_rows != 0 || d_cols != 0) {
|
||||
std::size_t new_rowoffs = buf->Rowoffs();
|
||||
std::size_t new_coloffs = buf->Coloffs();
|
||||
// Clamp vertical between 0 and last row (leaving at least one visible line)
|
||||
if (d_rows != 0) {
|
||||
long nr = static_cast<long>(new_rowoffs) + d_rows;
|
||||
if (nr < 0)
|
||||
nr = 0;
|
||||
const auto nrows = static_cast<long>(buf->Rows().size());
|
||||
if (nr > std::max(0L, nrows - 1))
|
||||
nr = std::max(0L, nrows - 1);
|
||||
new_rowoffs = static_cast<std::size_t>(nr);
|
||||
}
|
||||
if (d_cols != 0) {
|
||||
long nc = static_cast<long>(new_coloffs) + d_cols;
|
||||
if (nc < 0)
|
||||
nc = 0;
|
||||
new_coloffs = static_cast<std::size_t>(nc);
|
||||
}
|
||||
buf->SetOffsets(new_rowoffs, new_coloffs);
|
||||
update();
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
QWidget::wheelEvent(event);
|
||||
}
|
||||
|
||||
|
||||
void closeEvent(QCloseEvent *event) override
|
||||
{
|
||||
closed_ = true;
|
||||
QWidget::closeEvent(event);
|
||||
}
|
||||
|
||||
private:
|
||||
QtInputHandler &input_;
|
||||
bool closed_ = false;
|
||||
Editor *ed_ = nullptr;
|
||||
double v_scroll_accum_ = 0.0;
|
||||
double h_scroll_accum_ = 0.0;
|
||||
QString font_family_ = QStringLiteral("Brass Mono");
|
||||
int font_px_ = 18;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
bool
|
||||
GUIFrontend::Init(Editor &ed)
|
||||
{
|
||||
int argc = 0;
|
||||
char **argv = nullptr;
|
||||
app_ = new QApplication(argc, argv);
|
||||
|
||||
window_ = new MainWindow(input_);
|
||||
window_->show();
|
||||
// Ensure the window becomes the active, focused window so it receives key events
|
||||
window_->activateWindow();
|
||||
window_->raise();
|
||||
window_->setFocus(Qt::OtherFocusReason);
|
||||
|
||||
renderer_.Attach(window_);
|
||||
input_.Attach(&ed);
|
||||
if (auto *mw = dynamic_cast<MainWindow *>(window_))
|
||||
mw->SetEditor(&ed);
|
||||
|
||||
// Load GUI configuration (kge.ini) and configure font for Qt
|
||||
config_ = GUIConfig::Load();
|
||||
|
||||
// Apply background mode from config to match ImGui frontend behavior
|
||||
if (config_.background == "light")
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||
else
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||
|
||||
// Apply theme by name for Qt palette-based theming (maps to named palettes).
|
||||
// If unknown, falls back to the generic light/dark palette.
|
||||
(void) kte::ApplyQtThemeByName(config_.theme);
|
||||
if (window_)
|
||||
window_->update();
|
||||
|
||||
// Map GUIConfig font name to a system family (Qt uses installed fonts)
|
||||
auto choose_family = [](const std::string &name) -> QString {
|
||||
QString fam;
|
||||
std::string n = name;
|
||||
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
if (n.empty() || n == "default" || n == "brassmono" || n == "brassmonocode") {
|
||||
fam = QStringLiteral("Brass Mono");
|
||||
} else if (n == "jetbrains" || n == "jetbrains mono" || n == "jetbrains-mono") {
|
||||
fam = QStringLiteral("JetBrains Mono");
|
||||
} else if (n == "iosevka") {
|
||||
fam = QStringLiteral("Iosevka");
|
||||
} else if (n == "inconsolata" || n == "inconsolataex") {
|
||||
fam = QStringLiteral("Inconsolata");
|
||||
} else if (n == "space" || n == "spacemono" || n == "space mono") {
|
||||
fam = QStringLiteral("Space Mono");
|
||||
} else if (n == "go") {
|
||||
fam = QStringLiteral("Go Mono");
|
||||
} else if (n == "ibm" || n == "ibm plex mono" || n == "ibm-plex-mono") {
|
||||
fam = QStringLiteral("IBM Plex Mono");
|
||||
} else if (n == "fira" || n == "fira code" || n == "fira-code") {
|
||||
fam = QStringLiteral("Fira Code");
|
||||
} else if (!name.empty()) {
|
||||
fam = QString::fromStdString(name);
|
||||
}
|
||||
|
||||
// Validate availability; choose a fallback if needed
|
||||
const auto families = QFontDatabase::families();
|
||||
if (!fam.isEmpty() && families.contains(fam)) {
|
||||
return fam;
|
||||
}
|
||||
// Preferred fallback chain on macOS; otherwise, try common monospace families
|
||||
const QStringList fallbacks = {
|
||||
QStringLiteral("Brass Mono"),
|
||||
QStringLiteral("JetBrains Mono"),
|
||||
QStringLiteral("SF Mono"),
|
||||
QStringLiteral("Menlo"),
|
||||
QStringLiteral("Monaco"),
|
||||
QStringLiteral("Courier New"),
|
||||
QStringLiteral("Courier"),
|
||||
QStringLiteral("Monospace")
|
||||
};
|
||||
for (const auto &fb: fallbacks) {
|
||||
if (families.contains(fb))
|
||||
return fb;
|
||||
}
|
||||
// As a last resort, return the request (Qt will substitute)
|
||||
return fam.isEmpty() ? QStringLiteral("Monospace") : fam;
|
||||
};
|
||||
|
||||
QString family = choose_family(config_.font);
|
||||
int px_size = (config_.font_size > 0.0f) ? (int) std::lround(config_.font_size) : 18;
|
||||
if (auto *mw = dynamic_cast<MainWindow *>(window_)) {
|
||||
mw->SetFontFamilyAndSize(family, px_size);
|
||||
}
|
||||
// Track current font in globals for command/status queries
|
||||
kte::gCurrentFontFamily = family.toStdString();
|
||||
kte::gCurrentFontSize = static_cast<float>(px_size);
|
||||
|
||||
// Set initial dimensions based on font metrics
|
||||
QFont f(family, px_size);
|
||||
QFontMetrics fm(f);
|
||||
const int line_h = std::max(12, fm.height());
|
||||
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral("M")));
|
||||
const int w = window_->width();
|
||||
const int h = window_->height();
|
||||
const int pad = 16;
|
||||
const int status_h = line_h + 4;
|
||||
const int avail_w = std::max(0, w - 2 * pad);
|
||||
const int avail_h = std::max(0, h - 2 * pad - status_h);
|
||||
std::size_t rows = std::max<std::size_t>(1, (avail_h / line_h) + 1); // + status
|
||||
std::size_t cols = std::max<std::size_t>(1, (avail_w / ch_w));
|
||||
ed.SetDimensions(rows, cols);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
GUIFrontend::Step(Editor &ed, bool &running)
|
||||
{
|
||||
// Pump Qt events
|
||||
if (app_)
|
||||
app_->processEvents();
|
||||
|
||||
// Drain input queue
|
||||
for (;;) {
|
||||
MappedInput mi;
|
||||
if (!input_.Poll(mi))
|
||||
break;
|
||||
if (mi.hasCommand) {
|
||||
Execute(ed, mi.id, mi.arg, mi.count);
|
||||
}
|
||||
}
|
||||
|
||||
if (ed.QuitRequested()) {
|
||||
running = false;
|
||||
}
|
||||
|
||||
// --- Visual File Picker (Qt): invoked via CommandId::VisualFilePickerToggle ---
|
||||
if (ed.FilePickerVisible()) {
|
||||
QString startDir;
|
||||
if (!ed.FilePickerDir().empty()) {
|
||||
startDir = QString::fromStdString(ed.FilePickerDir());
|
||||
}
|
||||
QFileDialog dlg(window_, QStringLiteral("Open File"), startDir);
|
||||
dlg.setFileMode(QFileDialog::ExistingFile);
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
const QStringList files = dlg.selectedFiles();
|
||||
if (!files.isEmpty()) {
|
||||
const QString fp = files.front();
|
||||
std::string err;
|
||||
if (ed.OpenFile(fp.toStdString(), err)) {
|
||||
ed.SetStatus(std::string("Opened: ") + fp.toStdString());
|
||||
} else if (!err.empty()) {
|
||||
ed.SetStatus(std::string("Open failed: ") + err);
|
||||
} else {
|
||||
ed.SetStatus("Open failed");
|
||||
}
|
||||
// Update picker dir for next time
|
||||
QFileInfo info(fp);
|
||||
ed.SetFilePickerDir(info.dir().absolutePath().toStdString());
|
||||
}
|
||||
}
|
||||
// Close picker overlay regardless of outcome
|
||||
ed.SetFilePickerVisible(false);
|
||||
if (window_)
|
||||
window_->update();
|
||||
}
|
||||
|
||||
// Apply any queued theme change requests (from command handler)
|
||||
if (kte::gThemeChangePending) {
|
||||
if (!kte::gThemeChangeRequest.empty()) {
|
||||
// Apply Qt palette theme by name; if unknown, keep current palette
|
||||
(void) kte::ApplyQtThemeByName(kte::gThemeChangeRequest);
|
||||
}
|
||||
kte::gThemeChangePending = false;
|
||||
kte::gThemeChangeRequest.clear();
|
||||
if (window_)
|
||||
window_->update();
|
||||
}
|
||||
|
||||
// Visual font picker request (Qt only)
|
||||
if (kte::gFontDialogRequested) {
|
||||
// Seed initial font from current or default
|
||||
QFont seed;
|
||||
if (!kte::gCurrentFontFamily.empty()) {
|
||||
seed = QFont(QString::fromStdString(kte::gCurrentFontFamily),
|
||||
(int) std::lround(kte::gCurrentFontSize > 0 ? kte::gCurrentFontSize : 18));
|
||||
} else {
|
||||
seed = window_ ? window_->font() : QFont();
|
||||
}
|
||||
bool ok = false;
|
||||
const QFont chosen = QFontDialog::getFont(&ok, seed, window_, QStringLiteral("Choose Editor Font"));
|
||||
if (ok) {
|
||||
// Queue font change via existing hooks
|
||||
kte::gFontFamilyRequest = chosen.family().toStdString();
|
||||
// Use pixel size if available, otherwise convert from point size approximately
|
||||
int px = chosen.pixelSize();
|
||||
if (px <= 0) {
|
||||
// Approximate points to pixels (96 DPI assumption); Qt will rasterize appropriately
|
||||
px = (int) std::lround(chosen.pointSizeF() * 96.0 / 72.0);
|
||||
if (px <= 0)
|
||||
px = 18;
|
||||
}
|
||||
kte::gFontSizeRequest = static_cast<float>(px);
|
||||
kte::gFontChangePending = true;
|
||||
}
|
||||
kte::gFontDialogRequested = false;
|
||||
if (window_)
|
||||
window_->update();
|
||||
}
|
||||
|
||||
// Apply any queued font change requests (Qt)
|
||||
if (kte::gFontChangePending) {
|
||||
// Derive target family
|
||||
auto map_family = [](const std::string &name) -> QString {
|
||||
std::string n = name;
|
||||
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
QString fam;
|
||||
if (n == "brass" || n == "brassmono" || n == "brass mono") {
|
||||
fam = QStringLiteral("Brass Mono");
|
||||
} else if (n == "jetbrains" || n == "jetbrains mono" || n == "jetbrains-mono") {
|
||||
fam = QStringLiteral("JetBrains Mono");
|
||||
} else if (n == "iosevka") {
|
||||
fam = QStringLiteral("Iosevka");
|
||||
} else if (n == "inconsolata" || n == "inconsolataex") {
|
||||
fam = QStringLiteral("Inconsolata");
|
||||
} else if (n == "space" || n == "spacemono" || n == "space mono") {
|
||||
fam = QStringLiteral("Space Mono");
|
||||
} else if (n == "go") {
|
||||
fam = QStringLiteral("Go Mono");
|
||||
} else if (n == "ibm" || n == "ibm plex mono" || n == "ibm-plex-mono") {
|
||||
fam = QStringLiteral("IBM Plex Mono");
|
||||
} else if (n == "fira" || n == "fira code" || n == "fira-code") {
|
||||
fam = QStringLiteral("Fira Code");
|
||||
} else if (!name.empty()) {
|
||||
fam = QString::fromStdString(name);
|
||||
}
|
||||
// Validate availability; choose fallback if needed
|
||||
const auto families = QFontDatabase::families();
|
||||
if (!fam.isEmpty() && families.contains(fam)) {
|
||||
return fam;
|
||||
}
|
||||
// Fallback chain
|
||||
const QStringList fallbacks = {
|
||||
QStringLiteral("Brass Mono"),
|
||||
QStringLiteral("JetBrains Mono"),
|
||||
QStringLiteral("SF Mono"),
|
||||
QStringLiteral("Menlo"),
|
||||
QStringLiteral("Monaco"),
|
||||
QStringLiteral("Courier New"),
|
||||
QStringLiteral("Courier"),
|
||||
QStringLiteral("Monospace")
|
||||
};
|
||||
for (const auto &fb: fallbacks) {
|
||||
if (families.contains(fb))
|
||||
return fb;
|
||||
}
|
||||
return fam.isEmpty() ? QStringLiteral("Monospace") : fam;
|
||||
};
|
||||
|
||||
QString target_family;
|
||||
if (!kte::gFontFamilyRequest.empty()) {
|
||||
target_family = map_family(kte::gFontFamilyRequest);
|
||||
} else if (!kte::gCurrentFontFamily.empty()) {
|
||||
target_family = QString::fromStdString(kte::gCurrentFontFamily);
|
||||
}
|
||||
int target_px = 0;
|
||||
if (kte::gFontSizeRequest > 0.0f) {
|
||||
target_px = (int) std::lround(kte::gFontSizeRequest);
|
||||
} else if (kte::gCurrentFontSize > 0.0f) {
|
||||
target_px = (int) std::lround(kte::gCurrentFontSize);
|
||||
}
|
||||
if (target_px <= 0)
|
||||
target_px = 18;
|
||||
if (target_family.isEmpty())
|
||||
target_family = QStringLiteral("Monospace");
|
||||
|
||||
if (auto *mw = dynamic_cast<MainWindow *>(window_)) {
|
||||
mw->SetFontFamilyAndSize(target_family, target_px);
|
||||
}
|
||||
// Update globals
|
||||
kte::gCurrentFontFamily = target_family.toStdString();
|
||||
kte::gCurrentFontSize = static_cast<float>(target_px);
|
||||
// Reset requests
|
||||
kte::gFontChangePending = false;
|
||||
kte::gFontFamilyRequest.clear();
|
||||
kte::gFontSizeRequest = 0.0f;
|
||||
|
||||
// Recompute editor dimensions to match new metrics
|
||||
QFont f(target_family, target_px);
|
||||
QFontMetrics fm(f);
|
||||
const int line_h = std::max(12, fm.height());
|
||||
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral("M")));
|
||||
const int w = window_ ? window_->width() : 0;
|
||||
const int h = window_ ? window_->height() : 0;
|
||||
const int pad = 16;
|
||||
const int status_h = line_h + 4;
|
||||
const int avail_w = std::max(0, w - 2 * pad);
|
||||
const int avail_h = std::max(0, h - 2 * pad - status_h);
|
||||
std::size_t rows = std::max<std::size_t>(1, (avail_h / line_h) + 1); // + status
|
||||
std::size_t cols = std::max<std::size_t>(1, (avail_w / ch_w));
|
||||
ed.SetDimensions(rows, cols);
|
||||
|
||||
if (window_)
|
||||
window_->update();
|
||||
}
|
||||
|
||||
// Draw current frame (request repaint)
|
||||
renderer_.Draw(ed);
|
||||
|
||||
// Detect window close
|
||||
if (auto *mw = dynamic_cast<MainWindow *>(window_)) {
|
||||
if (mw->WasClosed()) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
GUIFrontend::Shutdown()
|
||||
{
|
||||
if (window_) {
|
||||
window_->close();
|
||||
delete window_;
|
||||
window_ = nullptr;
|
||||
}
|
||||
if (app_) {
|
||||
delete app_;
|
||||
app_ = nullptr;
|
||||
}
|
||||
}
|
||||
36
QtFrontend.h
Normal file
36
QtFrontend.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* QtFrontend - couples QtInputHandler + QtRenderer and owns Qt lifecycle
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "Frontend.h"
|
||||
#include "GUIConfig.h"
|
||||
#include "QtInputHandler.h"
|
||||
#include "QtRenderer.h"
|
||||
|
||||
class QApplication;
|
||||
class QWidget;
|
||||
|
||||
// Keep the public class name GUIFrontend to match main.cc selection logic.
|
||||
class GUIFrontend final : public Frontend {
|
||||
public:
|
||||
GUIFrontend() = default;
|
||||
|
||||
~GUIFrontend() override = default;
|
||||
|
||||
bool Init(Editor &ed) override;
|
||||
|
||||
void Step(Editor &ed, bool &running) override;
|
||||
|
||||
void Shutdown() override;
|
||||
|
||||
private:
|
||||
GUIConfig config_{};
|
||||
QtInputHandler input_{};
|
||||
QtRenderer renderer_{};
|
||||
|
||||
QApplication *app_ = nullptr; // owned
|
||||
QWidget *window_ = nullptr; // owned
|
||||
int width_ = 1280;
|
||||
int height_ = 800;
|
||||
};
|
||||
538
QtInputHandler.cc
Normal file
538
QtInputHandler.cc
Normal file
@@ -0,0 +1,538 @@
|
||||
// Refactor: route all key processing through command subsystem, mirroring ImGuiInputHandler
|
||||
|
||||
#include "QtInputHandler.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
|
||||
#include <ncurses.h>
|
||||
|
||||
#include "Editor.h"
|
||||
#include "KKeymap.h"
|
||||
|
||||
// Temporary verbose logging to debug macOS Qt key translation issues
|
||||
// Default to off; enable by defining QT_IH_DEBUG=1 at compile time when needed.
|
||||
#ifndef QT_IH_DEBUG
|
||||
#define QT_IH_DEBUG 0
|
||||
#endif
|
||||
|
||||
#if QT_IH_DEBUG
|
||||
#include <cstdio>
|
||||
static const char *
|
||||
mods_str(Qt::KeyboardModifiers m)
|
||||
{
|
||||
static thread_local char buf[64];
|
||||
buf[0] = '\0';
|
||||
bool first = true;
|
||||
auto add = [&](const char *s) {
|
||||
if (!first)
|
||||
std::snprintf(buf + std::strlen(buf), sizeof(buf) - std::strlen(buf), "|");
|
||||
std::snprintf(buf + std::strlen(buf), sizeof(buf) - std::strlen(buf), "%s", s);
|
||||
first = false;
|
||||
};
|
||||
if (m & Qt::ShiftModifier)
|
||||
add("Shift");
|
||||
if (m & Qt::ControlModifier)
|
||||
add("Ctrl");
|
||||
if (m & Qt::AltModifier)
|
||||
add("Alt");
|
||||
if (m & Qt::MetaModifier)
|
||||
add("Meta");
|
||||
if (first)
|
||||
std::snprintf(buf, sizeof(buf), "none");
|
||||
return buf;
|
||||
}
|
||||
#define LOGF(...) std::fprintf(stderr, __VA_ARGS__)
|
||||
#else
|
||||
#define LOGF(...) ((void)0)
|
||||
#endif
|
||||
|
||||
static bool
|
||||
IsPrintableQt(const QKeyEvent &e)
|
||||
{
|
||||
// Printable if it yields non-empty text and no Ctrl/Meta modifier
|
||||
if (e.modifiers() & (Qt::ControlModifier | Qt::MetaModifier))
|
||||
return false;
|
||||
const QString t = e.text();
|
||||
return !t.isEmpty() && !t.at(0).isNull();
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
ToAsciiKey(const QKeyEvent &e)
|
||||
{
|
||||
const QString t = e.text();
|
||||
if (!t.isEmpty()) {
|
||||
const QChar c = t.at(0);
|
||||
if (!c.isNull())
|
||||
return KLowerAscii(c.unicode());
|
||||
}
|
||||
// When modifiers (like Control) are held, Qt::text() can be empty on macOS.
|
||||
// Fall back to mapping common virtual keys to ASCII.
|
||||
switch (e.key()) {
|
||||
case Qt::Key_A:
|
||||
return 'a';
|
||||
case Qt::Key_B:
|
||||
return 'b';
|
||||
case Qt::Key_C:
|
||||
return 'c';
|
||||
case Qt::Key_D:
|
||||
return 'd';
|
||||
case Qt::Key_E:
|
||||
return 'e';
|
||||
case Qt::Key_F:
|
||||
return 'f';
|
||||
case Qt::Key_G:
|
||||
return 'g';
|
||||
case Qt::Key_H:
|
||||
return 'h';
|
||||
case Qt::Key_I:
|
||||
return 'i';
|
||||
case Qt::Key_J:
|
||||
return 'j';
|
||||
case Qt::Key_K:
|
||||
return 'k';
|
||||
case Qt::Key_L:
|
||||
return 'l';
|
||||
case Qt::Key_M:
|
||||
return 'm';
|
||||
case Qt::Key_N:
|
||||
return 'n';
|
||||
case Qt::Key_O:
|
||||
return 'o';
|
||||
case Qt::Key_P:
|
||||
return 'p';
|
||||
case Qt::Key_Q:
|
||||
return 'q';
|
||||
case Qt::Key_R:
|
||||
return 'r';
|
||||
case Qt::Key_S:
|
||||
return 's';
|
||||
case Qt::Key_T:
|
||||
return 't';
|
||||
case Qt::Key_U:
|
||||
return 'u';
|
||||
case Qt::Key_V:
|
||||
return 'v';
|
||||
case Qt::Key_W:
|
||||
return 'w';
|
||||
case Qt::Key_X:
|
||||
return 'x';
|
||||
case Qt::Key_Y:
|
||||
return 'y';
|
||||
case Qt::Key_Z:
|
||||
return 'z';
|
||||
case Qt::Key_0:
|
||||
return '0';
|
||||
case Qt::Key_1:
|
||||
return '1';
|
||||
case Qt::Key_2:
|
||||
return '2';
|
||||
case Qt::Key_3:
|
||||
return '3';
|
||||
case Qt::Key_4:
|
||||
return '4';
|
||||
case Qt::Key_5:
|
||||
return '5';
|
||||
case Qt::Key_6:
|
||||
return '6';
|
||||
case Qt::Key_7:
|
||||
return '7';
|
||||
case Qt::Key_8:
|
||||
return '8';
|
||||
case Qt::Key_9:
|
||||
return '9';
|
||||
case Qt::Key_Comma:
|
||||
return ',';
|
||||
case Qt::Key_Period:
|
||||
return '.';
|
||||
case Qt::Key_Semicolon:
|
||||
return ';';
|
||||
case Qt::Key_Apostrophe:
|
||||
return '\'';
|
||||
case Qt::Key_Minus:
|
||||
return '-';
|
||||
case Qt::Key_Equal:
|
||||
return '=';
|
||||
case Qt::Key_Slash:
|
||||
return '/';
|
||||
case Qt::Key_Backslash:
|
||||
return '\\';
|
||||
case Qt::Key_BracketLeft:
|
||||
return '[';
|
||||
case Qt::Key_BracketRight:
|
||||
return ']';
|
||||
case Qt::Key_QuoteLeft:
|
||||
return '`';
|
||||
case Qt::Key_Space:
|
||||
return ' ';
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// Case-preserving ASCII derivation for k-prefix handling where we need to
|
||||
// distinguish between 'C' and 'c'. Falls back to virtual-key mapping if
|
||||
// event text is unavailable (common when Control/Meta held on macOS).
|
||||
static int
|
||||
ToAsciiKeyPreserveCase(const QKeyEvent &e)
|
||||
{
|
||||
const QString t = e.text();
|
||||
if (!t.isEmpty()) {
|
||||
const QChar c = t.at(0);
|
||||
if (!c.isNull())
|
||||
return c.unicode();
|
||||
}
|
||||
// Fall back to virtual key mapping (letters as uppercase A..Z)
|
||||
switch (e.key()) {
|
||||
case Qt::Key_A:
|
||||
return 'A';
|
||||
case Qt::Key_B:
|
||||
return 'B';
|
||||
case Qt::Key_C:
|
||||
return 'C';
|
||||
case Qt::Key_D:
|
||||
return 'D';
|
||||
case Qt::Key_E:
|
||||
return 'E';
|
||||
case Qt::Key_F:
|
||||
return 'F';
|
||||
case Qt::Key_G:
|
||||
return 'G';
|
||||
case Qt::Key_H:
|
||||
return 'H';
|
||||
case Qt::Key_I:
|
||||
return 'I';
|
||||
case Qt::Key_J:
|
||||
return 'J';
|
||||
case Qt::Key_K:
|
||||
return 'K';
|
||||
case Qt::Key_L:
|
||||
return 'L';
|
||||
case Qt::Key_M:
|
||||
return 'M';
|
||||
case Qt::Key_N:
|
||||
return 'N';
|
||||
case Qt::Key_O:
|
||||
return 'O';
|
||||
case Qt::Key_P:
|
||||
return 'P';
|
||||
case Qt::Key_Q:
|
||||
return 'Q';
|
||||
case Qt::Key_R:
|
||||
return 'R';
|
||||
case Qt::Key_S:
|
||||
return 'S';
|
||||
case Qt::Key_T:
|
||||
return 'T';
|
||||
case Qt::Key_U:
|
||||
return 'U';
|
||||
case Qt::Key_V:
|
||||
return 'V';
|
||||
case Qt::Key_W:
|
||||
return 'W';
|
||||
case Qt::Key_X:
|
||||
return 'X';
|
||||
case Qt::Key_Y:
|
||||
return 'Y';
|
||||
case Qt::Key_Z:
|
||||
return 'Z';
|
||||
case Qt::Key_Comma:
|
||||
return ',';
|
||||
case Qt::Key_Period:
|
||||
return '.';
|
||||
case Qt::Key_Semicolon:
|
||||
return ';';
|
||||
case Qt::Key_Apostrophe:
|
||||
return '\'';
|
||||
case Qt::Key_Minus:
|
||||
return '-';
|
||||
case Qt::Key_Equal:
|
||||
return '=';
|
||||
case Qt::Key_Slash:
|
||||
return '/';
|
||||
case Qt::Key_Backslash:
|
||||
return '\\';
|
||||
case Qt::Key_BracketLeft:
|
||||
return '[';
|
||||
case Qt::Key_BracketRight:
|
||||
return ']';
|
||||
case Qt::Key_QuoteLeft:
|
||||
return '`';
|
||||
case Qt::Key_Space:
|
||||
return ' ';
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
QtInputHandler::ProcessKeyEvent(const QKeyEvent &e)
|
||||
{
|
||||
const Qt::KeyboardModifiers mods = e.modifiers();
|
||||
LOGF("[QtIH] keyPress key=0x%X mods=%s text='%s' k_prefix=%d k_ctrl_pending=%d esc_meta=%d\n",
|
||||
e.key(), mods_str(mods), e.text().toUtf8().constData(), (int)k_prefix_, (int)k_ctrl_pending_,
|
||||
(int)esc_meta_);
|
||||
|
||||
// Control-chord detection: only treat the physical Control key as control-like.
|
||||
// Do NOT include Meta (Command) here so that ⌘-letter shortcuts do not fall into
|
||||
// the Ctrl map (prevents ⌘-T being mistaken for C-t).
|
||||
const bool ctrl_like = (mods & Qt::ControlModifier);
|
||||
|
||||
// 1) Universal argument digits (when active), consume digits without enqueuing commands
|
||||
if (ed_ &&ed_
|
||||
|
||||
->
|
||||
UArg() != 0
|
||||
)
|
||||
{
|
||||
if (!(mods & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier))) {
|
||||
if (e.key() >= Qt::Key_0 && e.key() <= Qt::Key_9) {
|
||||
int d = e.key() - Qt::Key_0;
|
||||
ed_->UArgDigit(d);
|
||||
// request status refresh
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(MappedInput{true, CommandId::UArgStatus, std::string(), 0});
|
||||
LOGF("[QtIH] UArg digit %d -> enqueue UArgStatus\n", d);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Enter k-prefix on C-k
|
||||
if (ctrl_like && (e.key() == Qt::Key_K)) {
|
||||
k_prefix_ = true;
|
||||
k_ctrl_pending_ = false;
|
||||
LOGF("[QtIH] Enter KPrefix\n");
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(MappedInput{true, CommandId::KPrefix, std::string(), 0});
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3) If currently in k-prefix, resolve next key via KLookupKCommand
|
||||
if (k_prefix_) {
|
||||
// ESC/meta prefix should not interfere with k-suffix resolution
|
||||
esc_meta_ = false;
|
||||
// Support literal 'C' (uppercase) or '^' to indicate the next key is Ctrl-qualified.
|
||||
// Use case-preserving derivation so that 'c' (lowercase) can still be a valid suffix
|
||||
// like C-k c (BufferClose).
|
||||
int ascii_raw = ToAsciiKeyPreserveCase(e);
|
||||
if (ascii_raw == 'C' || ascii_raw == '^') {
|
||||
k_ctrl_pending_ = true;
|
||||
if (ed_)
|
||||
ed_->SetStatus("C-k C _");
|
||||
LOGF("[QtIH] KPrefix: set k_ctrl_pending via '%c'\n", (ascii_raw == 'C') ? 'C' : '^');
|
||||
return true; // consume, wait for next key
|
||||
}
|
||||
int ascii_key = (ascii_raw != 0) ? ascii_raw : ToAsciiKey(e);
|
||||
int lower = KLowerAscii(ascii_key);
|
||||
// Only pass a control suffix for specific supported keys (d/x/q),
|
||||
// matching ImGui behavior so that holding Ctrl during the suffix
|
||||
// doesn't break other mappings like C-k c (BufferClose).
|
||||
bool ctrl_suffix_supported = (lower == 'd' || lower == 'x' || lower == 'q');
|
||||
bool pass_ctrl = (ctrl_like || k_ctrl_pending_) && ctrl_suffix_supported;
|
||||
k_ctrl_pending_ = false; // consume pending qualifier on any suffix
|
||||
LOGF("[QtIH] KPrefix: ascii_key=%d lower=%d pass_ctrl=%d\n", ascii_key, lower, (int)pass_ctrl);
|
||||
if (ascii_key != 0) {
|
||||
CommandId id;
|
||||
if (KLookupKCommand(ascii_key, pass_ctrl, id)) {
|
||||
LOGF("[QtIH] KPrefix: mapped to command id=%d\n", (int)id);
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(MappedInput{true, id, std::string(), 0});
|
||||
} else {
|
||||
// Unknown k-command: notify
|
||||
std::string a;
|
||||
a.push_back(static_cast<char>(ascii_key));
|
||||
LOGF("[QtIH] KPrefix: unknown command for '%c'\n", (char)ascii_key);
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(MappedInput{true, CommandId::UnknownKCommand, a, 0});
|
||||
}
|
||||
k_prefix_ = false;
|
||||
return true;
|
||||
}
|
||||
// If not resolvable, consume and exit k-prefix
|
||||
k_prefix_ = false;
|
||||
LOGF("[QtIH] KPrefix: unresolved key; exiting prefix\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3.5) GUI shortcut: Command/Meta + T opens the visual font picker (Qt only).
|
||||
// Require Meta present and Control NOT present so Ctrl-T never triggers this.
|
||||
if ((mods & Qt::MetaModifier) && !(mods & Qt::ControlModifier) && e.key() == Qt::Key_T) {
|
||||
LOGF("[QtIH] Meta/Super-T -> VisualFontPickerToggle\n");
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(MappedInput{true, CommandId::VisualFontPickerToggle, std::string(), 0});
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4) ESC as Meta prefix (set state). Alt/Meta chord handled below directly.
|
||||
if (e.key() == Qt::Key_Escape) {
|
||||
esc_meta_ = true;
|
||||
LOGF("[QtIH] ESC: set esc_meta\n");
|
||||
return true; // consumed
|
||||
}
|
||||
|
||||
// 5) Alt/Meta bindings (ESC f/b equivalent). Handle either Alt/Meta or pending esc_meta_
|
||||
// ESC/meta chords: on macOS, do NOT treat Meta as ESC; only Alt (Option) should trigger.
|
||||
#if defined(__APPLE__)
|
||||
if (esc_meta_ || (mods & Qt::AltModifier)) {
|
||||
|
||||
|
||||
#else
|
||||
if (esc_meta_ || (mods & (Qt::AltModifier | Qt::MetaModifier))) {
|
||||
#endif
|
||||
int ascii_key = 0;
|
||||
if (e.key() == Qt::Key_Backspace) {
|
||||
ascii_key = KEY_BACKSPACE;
|
||||
} else if (e.key() >= Qt::Key_A && e.key() <= Qt::Key_Z) {
|
||||
ascii_key = 'a' + (e.key() - Qt::Key_A);
|
||||
} else if (e.key() == Qt::Key_Comma) {
|
||||
ascii_key = '<';
|
||||
} else if (e.key() == Qt::Key_Period) {
|
||||
ascii_key = '>';
|
||||
}
|
||||
// If still unknown, try deriving from text (covers digits, punctuation, locale)
|
||||
if (ascii_key == 0) {
|
||||
ascii_key = ToAsciiKey(e);
|
||||
}
|
||||
esc_meta_ = false; // one-shot regardless
|
||||
if (ascii_key != 0) {
|
||||
ascii_key = KLowerAscii(ascii_key);
|
||||
CommandId id;
|
||||
if (KLookupEscCommand(ascii_key, id)) {
|
||||
LOGF("[QtIH] ESC/Meta: mapped '%d' -> id=%d\n", ascii_key, (int)id);
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(MappedInput{true, id, std::string(), 0});
|
||||
return true;
|
||||
} else {
|
||||
// Report invalid ESC sequence just like ImGui path
|
||||
LOGF("[QtIH] ESC/Meta: unknown command for ascii=%d\n", ascii_key);
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(MappedInput{true, CommandId::UnknownEscCommand, std::string(), 0});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Nothing derivable: consume (ESC prefix cleared) and do not insert text
|
||||
return true;
|
||||
}
|
||||
|
||||
// 6) Control-chord direct mappings (e.g., C-n/C-p/C-f/C-b...)
|
||||
if (ctrl_like) {
|
||||
// Universal argument handling: C-u starts collection; C-g cancels
|
||||
if (e.key() == Qt::Key_U) {
|
||||
if (ed_)
|
||||
ed_->UArgStart();
|
||||
LOGF("[QtIH] Ctrl-chord: start universal argument\n");
|
||||
return true;
|
||||
}
|
||||
if (e.key() == Qt::Key_G) {
|
||||
if (ed_)
|
||||
ed_->UArgClear();
|
||||
k_ctrl_pending_ = false;
|
||||
k_prefix_ = false;
|
||||
LOGF("[QtIH] Ctrl-chord: cancel universal argument and k-prefix via C-g\n");
|
||||
// Fall through to map C-g to Refresh via ctrl map
|
||||
}
|
||||
if (e.key() >= Qt::Key_A && e.key() <= Qt::Key_Z) {
|
||||
int ascii_key = 'a' + (e.key() - Qt::Key_A);
|
||||
CommandId id;
|
||||
if (KLookupCtrlCommand(ascii_key, id)) {
|
||||
LOGF("[QtIH] Ctrl-chord: 'C-%c' -> id=%d\n", (char)ascii_key, (int)id);
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(MappedInput{true, id, std::string(), 0});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// If no mapping, continue to allow other keys below
|
||||
}
|
||||
|
||||
// 7) Special navigation/edit keys (match ImGui behavior)
|
||||
{
|
||||
CommandId id;
|
||||
bool has = false;
|
||||
switch (e.key()) {
|
||||
case Qt::Key_Return:
|
||||
case Qt::Key_Enter:
|
||||
id = CommandId::Newline;
|
||||
has = true;
|
||||
break;
|
||||
case Qt::Key_Backspace:
|
||||
id = CommandId::Backspace;
|
||||
has = true;
|
||||
break;
|
||||
case Qt::Key_Delete:
|
||||
id = CommandId::DeleteChar;
|
||||
has = true;
|
||||
break;
|
||||
case Qt::Key_Left:
|
||||
id = CommandId::MoveLeft;
|
||||
has = true;
|
||||
break;
|
||||
case Qt::Key_Right:
|
||||
id = CommandId::MoveRight;
|
||||
has = true;
|
||||
break;
|
||||
case Qt::Key_Up:
|
||||
id = CommandId::MoveUp;
|
||||
has = true;
|
||||
break;
|
||||
case Qt::Key_Down:
|
||||
id = CommandId::MoveDown;
|
||||
has = true;
|
||||
break;
|
||||
case Qt::Key_Home:
|
||||
id = CommandId::MoveHome;
|
||||
has = true;
|
||||
break;
|
||||
case Qt::Key_End:
|
||||
id = CommandId::MoveEnd;
|
||||
has = true;
|
||||
break;
|
||||
case Qt::Key_PageUp:
|
||||
id = CommandId::PageUp;
|
||||
has = true;
|
||||
break;
|
||||
case Qt::Key_PageDown:
|
||||
id = CommandId::PageDown;
|
||||
has = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (has) {
|
||||
LOGF("[QtIH] Special key -> id=%d\n", (int)id);
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(MappedInput{true, id, std::string(), 0});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 8) Insert printable text
|
||||
if (IsPrintableQt(e)) {
|
||||
std::string s = e.text().toStdString();
|
||||
if (!s.empty()) {
|
||||
LOGF("[QtIH] InsertText '%s'\n", s.c_str());
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
q_.push(MappedInput{true, CommandId::InsertText, s, 0});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
LOGF("[QtIH] Unhandled key\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
QtInputHandler::Poll(MappedInput &out)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
if (q_.empty())
|
||||
return false;
|
||||
out = q_.front();
|
||||
q_.pop();
|
||||
return true;
|
||||
}
|
||||
40
QtInputHandler.h
Normal file
40
QtInputHandler.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* QtInputHandler - Qt-based input mapping for GUI mode
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
|
||||
#include "InputHandler.h"
|
||||
|
||||
class QKeyEvent;
|
||||
|
||||
class QtInputHandler final : public InputHandler {
|
||||
public:
|
||||
QtInputHandler() = default;
|
||||
|
||||
~QtInputHandler() override = default;
|
||||
|
||||
|
||||
void Attach(Editor *ed) override
|
||||
{
|
||||
ed_ = ed;
|
||||
}
|
||||
|
||||
|
||||
// Translate a Qt key event to editor command and enqueue if applicable.
|
||||
// Returns true if it produced a mapped command or consumed input.
|
||||
bool ProcessKeyEvent(const QKeyEvent &e);
|
||||
|
||||
bool Poll(MappedInput &out) override;
|
||||
|
||||
private:
|
||||
std::mutex mu_;
|
||||
std::queue<MappedInput> q_;
|
||||
bool k_prefix_ = false;
|
||||
bool k_ctrl_pending_ = false; // C-k C-… qualifier
|
||||
bool esc_meta_ = false; // ESC-prefix for next key
|
||||
bool suppress_text_input_once_ = false; // reserved (Qt sends text in keyPressEvent)
|
||||
Editor *ed_ = nullptr;
|
||||
};
|
||||
76
QtRenderer.cc
Normal file
76
QtRenderer.cc
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "QtRenderer.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
|
||||
#include "Editor.h"
|
||||
|
||||
namespace {
|
||||
class EditorWidget : public QWidget {
|
||||
public:
|
||||
explicit EditorWidget(QWidget *parent = nullptr) : QWidget(parent)
|
||||
{
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
}
|
||||
|
||||
|
||||
void SetEditor(Editor *ed)
|
||||
{
|
||||
ed_ = ed;
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
QPainter p(this);
|
||||
// Background
|
||||
const QColor bg(28, 28, 30);
|
||||
p.fillRect(rect(), bg);
|
||||
|
||||
// Font and metrics
|
||||
QFont f("JetBrains Mono", 13);
|
||||
p.setFont(f);
|
||||
QFontMetrics fm(f);
|
||||
const int line_h = fm.height();
|
||||
|
||||
// Title
|
||||
p.setPen(QColor(220, 220, 220));
|
||||
p.drawText(8, fm.ascent() + 4, QStringLiteral("kte (Qt frontend)"));
|
||||
|
||||
// Status bar at bottom
|
||||
const int bar_h = line_h + 6; // padding
|
||||
const int bar_y = height() - bar_h;
|
||||
QRect status_rect(0, bar_y, width(), bar_h);
|
||||
p.fillRect(status_rect, QColor(40, 40, 44));
|
||||
p.setPen(QColor(180, 180, 140));
|
||||
if (ed_) {
|
||||
const QString status = QString::fromStdString(ed_->Status());
|
||||
// draw at baseline within the bar
|
||||
const int baseline = bar_y + 3 + fm.ascent();
|
||||
p.drawText(8, baseline, status);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Editor *ed_ = nullptr;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void
|
||||
QtRenderer::Draw(Editor &ed)
|
||||
{
|
||||
if (!widget_)
|
||||
return;
|
||||
|
||||
// If our widget is an EditorWidget, pass the editor pointer for painting
|
||||
if (auto *ew = dynamic_cast<EditorWidget *>(widget_)) {
|
||||
ew->SetEditor(&ed);
|
||||
}
|
||||
// Request a repaint
|
||||
widget_->update();
|
||||
}
|
||||
27
QtRenderer.h
Normal file
27
QtRenderer.h
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* QtRenderer - minimal Qt-based renderer
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "Renderer.h"
|
||||
|
||||
class QWidget;
|
||||
|
||||
class QtRenderer final : public Renderer {
|
||||
public:
|
||||
QtRenderer() = default;
|
||||
|
||||
~QtRenderer() override = default;
|
||||
|
||||
|
||||
void Attach(QWidget *widget)
|
||||
{
|
||||
widget_ = widget;
|
||||
}
|
||||
|
||||
|
||||
void Draw(Editor &ed) override;
|
||||
|
||||
private:
|
||||
QWidget *widget_ = nullptr; // not owned
|
||||
};
|
||||
@@ -2,9 +2,12 @@ ROADMAP / TODO:
|
||||
|
||||
- [x] Search + Replace
|
||||
- [x] Regex search + replace
|
||||
- [ ] The undo system should actually work
|
||||
- [x] Able to mark buffers as read-only
|
||||
- [x] Built-in help text
|
||||
- [x] Shorten paths in the homedir with ~
|
||||
- [x] When the filename is longer than the message window, scoot left to
|
||||
keep it in view
|
||||
- [x] Syntax highlighting
|
||||
- [ ] Swap files (crash recovery). See `docs/plans/swap-files.md`
|
||||
- [ ] The undo system should actually work
|
||||
- [ ] LSP integration
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* Renderer.h - rendering abstraction
|
||||
*/
|
||||
#ifndef KTE_RENDERER_H
|
||||
#define KTE_RENDERER_H
|
||||
|
||||
#pragma once
|
||||
|
||||
class Editor;
|
||||
|
||||
@@ -12,6 +10,4 @@ public:
|
||||
virtual ~Renderer() = default;
|
||||
|
||||
virtual void Draw(Editor &ed) = 0;
|
||||
};
|
||||
|
||||
#endif // KTE_RENDERER_H
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
#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
|
||||
@@ -1,18 +0,0 @@
|
||||
// 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
|
||||
@@ -1,43 +0,0 @@
|
||||
#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
|
||||
434
Swap.cc
Normal file
434
Swap.cc
Normal file
@@ -0,0 +1,434 @@
|
||||
#include "Swap.h"
|
||||
#include "Buffer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <cerrno>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace kte {
|
||||
namespace {
|
||||
constexpr std::uint8_t MAGIC[8] = {'K', 'T', 'E', '_', 'S', 'W', 'P', '\0'};
|
||||
constexpr std::uint32_t VERSION = 1;
|
||||
|
||||
// Write all bytes in buf to fd, handling EINTR and partial writes.
|
||||
static bool write_full(int fd, const void *buf, size_t len)
|
||||
{
|
||||
const std::uint8_t *p = static_cast<const std::uint8_t *>(buf);
|
||||
while (len > 0) {
|
||||
ssize_t n = ::write(fd, p, len);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
if (n == 0)
|
||||
return false; // shouldn't happen for regular files; treat as error
|
||||
p += static_cast<size_t>(n);
|
||||
len -= static_cast<size_t>(n);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
SwapManager::SwapManager()
|
||||
{
|
||||
running_.store(true);
|
||||
worker_ = std::thread([this] {
|
||||
this->writer_loop();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
SwapManager::~SwapManager()
|
||||
{
|
||||
running_.store(false);
|
||||
cv_.notify_all();
|
||||
if (worker_.joinable())
|
||||
worker_.join();
|
||||
// Close all journals
|
||||
for (auto &kv: journals_) {
|
||||
close_ctx(kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::Attach(Buffer * /*buf*/)
|
||||
{
|
||||
// Stage 1: lazy-open on first record; nothing to do here.
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::Detach(Buffer * /*buf*/)
|
||||
{
|
||||
// Stage 1: keep files open until manager destruction; future work can close per-buffer.
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::NotifyFilenameChanged(Buffer &buf)
|
||||
{
|
||||
std::lock_guard<std::mutex> lg(mtx_);
|
||||
auto it = journals_.find(&buf);
|
||||
if (it == journals_.end())
|
||||
return;
|
||||
JournalCtx &ctx = it->second;
|
||||
// Close existing file handle, update path; lazily reopen on next write
|
||||
close_ctx(ctx);
|
||||
ctx.path = ComputeSidecarPath(buf);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::SetSuspended(Buffer &buf, bool on)
|
||||
{
|
||||
std::lock_guard<std::mutex> lg(mtx_);
|
||||
auto path = ComputeSidecarPath(buf);
|
||||
// Create/update context for this buffer
|
||||
JournalCtx &ctx = journals_[&buf];
|
||||
ctx.path = path;
|
||||
ctx.suspended = on;
|
||||
}
|
||||
|
||||
|
||||
SwapManager::SuspendGuard::SuspendGuard(SwapManager &m, Buffer *b)
|
||||
: m_(m), buf_(b), prev_(false)
|
||||
{
|
||||
// Suspend recording while guard is alive
|
||||
if (buf_)
|
||||
m_.SetSuspended(*buf_, true);
|
||||
}
|
||||
|
||||
|
||||
SwapManager::SuspendGuard::~SuspendGuard()
|
||||
{
|
||||
if (buf_)
|
||||
m_.SetSuspended(*buf_, false);
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
SwapManager::ComputeSidecarPath(const Buffer &buf)
|
||||
{
|
||||
if (buf.IsFileBacked() || !buf.Filename().empty()) {
|
||||
fs::path p(buf.Filename());
|
||||
fs::path dir = p.parent_path();
|
||||
std::string base = p.filename().string();
|
||||
std::string side = "." + base + ".kte.swp";
|
||||
return (dir / side).string();
|
||||
}
|
||||
// unnamed: $TMPDIR/kte/unnamed-<ptr>.kte.swp (best-effort)
|
||||
const char *tmp = std::getenv("TMPDIR");
|
||||
fs::path t = tmp ? fs::path(tmp) : fs::temp_directory_path();
|
||||
fs::path d = t / "kte";
|
||||
char bufptr[32];
|
||||
std::snprintf(bufptr, sizeof(bufptr), "%p", (const void *) &buf);
|
||||
return (d / (std::string("unnamed-") + bufptr + ".kte.swp")).string();
|
||||
}
|
||||
|
||||
|
||||
std::uint64_t
|
||||
SwapManager::now_ns()
|
||||
{
|
||||
using namespace std::chrono;
|
||||
return duration_cast<nanoseconds>(steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
SwapManager::ensure_parent_dir(const std::string &path)
|
||||
{
|
||||
try {
|
||||
fs::path p(path);
|
||||
fs::path dir = p.parent_path();
|
||||
if (dir.empty())
|
||||
return true;
|
||||
if (!fs::exists(dir))
|
||||
fs::create_directories(dir);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
SwapManager::write_header(JournalCtx &ctx)
|
||||
{
|
||||
if (ctx.fd < 0)
|
||||
return false;
|
||||
// Write a simple 64-byte header
|
||||
std::uint8_t hdr[64];
|
||||
std::memset(hdr, 0, sizeof(hdr));
|
||||
std::memcpy(hdr, MAGIC, 8);
|
||||
std::uint32_t ver = VERSION;
|
||||
std::memcpy(hdr + 8, &ver, sizeof(ver));
|
||||
std::uint64_t ts = static_cast<std::uint64_t>(std::time(nullptr));
|
||||
std::memcpy(hdr + 16, &ts, sizeof(ts));
|
||||
ssize_t w = ::write(ctx.fd, hdr, sizeof(hdr));
|
||||
return (w == (ssize_t) sizeof(hdr));
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
SwapManager::open_ctx(JournalCtx &ctx)
|
||||
{
|
||||
if (ctx.fd >= 0)
|
||||
return true;
|
||||
if (!ensure_parent_dir(ctx.path))
|
||||
return false;
|
||||
// Create or open with 0600 perms
|
||||
int fd = ::open(ctx.path.c_str(), O_CREAT | O_RDWR, 0600);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
// Detect if file is new/empty to write header
|
||||
struct stat st{};
|
||||
if (fstat(fd, &st) != 0) {
|
||||
::close(fd);
|
||||
return false;
|
||||
}
|
||||
ctx.fd = fd;
|
||||
ctx.file = fdopen(fd, "ab");
|
||||
if (!ctx.file) {
|
||||
::close(fd);
|
||||
ctx.fd = -1;
|
||||
return false;
|
||||
}
|
||||
if (st.st_size == 0) {
|
||||
ctx.header_ok = write_header(ctx);
|
||||
} else {
|
||||
ctx.header_ok = true; // trust existing file for stage 1
|
||||
// Seek to end to append
|
||||
::lseek(ctx.fd, 0, SEEK_END);
|
||||
}
|
||||
return ctx.header_ok;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::close_ctx(JournalCtx &ctx)
|
||||
{
|
||||
if (ctx.file) {
|
||||
std::fflush((FILE *) ctx.file);
|
||||
::fsync(ctx.fd);
|
||||
std::fclose((FILE *) ctx.file);
|
||||
ctx.file = nullptr;
|
||||
}
|
||||
if (ctx.fd >= 0) {
|
||||
::close(ctx.fd);
|
||||
ctx.fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::uint32_t
|
||||
SwapManager::crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed)
|
||||
{
|
||||
static std::uint32_t table[256];
|
||||
static bool inited = false;
|
||||
if (!inited) {
|
||||
for (std::uint32_t i = 0; i < 256; ++i) {
|
||||
std::uint32_t c = i;
|
||||
for (int j = 0; j < 8; ++j)
|
||||
c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1);
|
||||
table[i] = c;
|
||||
}
|
||||
inited = true;
|
||||
}
|
||||
std::uint32_t c = ~seed;
|
||||
for (std::size_t i = 0; i < len; ++i)
|
||||
c = table[(c ^ data[i]) & 0xFFu] ^ (c >> 8);
|
||||
return ~c;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::put_varu64(std::vector<std::uint8_t> &out, std::uint64_t v)
|
||||
{
|
||||
while (v >= 0x80) {
|
||||
out.push_back(static_cast<std::uint8_t>(v) | 0x80);
|
||||
v >>= 7;
|
||||
}
|
||||
out.push_back(static_cast<std::uint8_t>(v));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::put_u24(std::uint8_t dst[3], std::uint32_t v)
|
||||
{
|
||||
dst[0] = static_cast<std::uint8_t>((v >> 16) & 0xFF);
|
||||
dst[1] = static_cast<std::uint8_t>((v >> 8) & 0xFF);
|
||||
dst[2] = static_cast<std::uint8_t>(v & 0xFF);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::enqueue(Pending &&p)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lg(mtx_);
|
||||
queue_.emplace_back(std::move(p));
|
||||
}
|
||||
cv_.notify_one();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::RecordInsert(Buffer &buf, int row, int col, std::string_view text)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lg(mtx_);
|
||||
if (journals_[&buf].suspended)
|
||||
return;
|
||||
}
|
||||
Pending p;
|
||||
p.buf = &buf;
|
||||
p.type = SwapRecType::INS;
|
||||
// payload: varint row, varint col, varint len, bytes
|
||||
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, row)));
|
||||
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, col)));
|
||||
put_varu64(p.payload, static_cast<std::uint64_t>(text.size()));
|
||||
p.payload.insert(p.payload.end(), reinterpret_cast<const std::uint8_t *>(text.data()),
|
||||
reinterpret_cast<const std::uint8_t *>(text.data()) + text.size());
|
||||
enqueue(std::move(p));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::RecordDelete(Buffer &buf, int row, int col, std::size_t len)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lg(mtx_);
|
||||
if (journals_[&buf].suspended)
|
||||
return;
|
||||
}
|
||||
Pending p;
|
||||
p.buf = &buf;
|
||||
p.type = SwapRecType::DEL;
|
||||
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, row)));
|
||||
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, col)));
|
||||
put_varu64(p.payload, static_cast<std::uint64_t>(len));
|
||||
enqueue(std::move(p));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::RecordSplit(Buffer &buf, int row, int col)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lg(mtx_);
|
||||
if (journals_[&buf].suspended)
|
||||
return;
|
||||
}
|
||||
Pending p;
|
||||
p.buf = &buf;
|
||||
p.type = SwapRecType::SPLIT;
|
||||
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, row)));
|
||||
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, col)));
|
||||
enqueue(std::move(p));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::RecordJoin(Buffer &buf, int row)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lg(mtx_);
|
||||
if (journals_[&buf].suspended)
|
||||
return;
|
||||
}
|
||||
Pending p;
|
||||
p.buf = &buf;
|
||||
p.type = SwapRecType::JOIN;
|
||||
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, row)));
|
||||
enqueue(std::move(p));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::writer_loop()
|
||||
{
|
||||
while (running_.load()) {
|
||||
std::vector<Pending> batch;
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(mtx_);
|
||||
if (queue_.empty()) {
|
||||
cv_.wait_for(lk, std::chrono::milliseconds(cfg_.flush_interval_ms));
|
||||
}
|
||||
if (!queue_.empty()) {
|
||||
batch.swap(queue_);
|
||||
}
|
||||
}
|
||||
if (batch.empty())
|
||||
continue;
|
||||
|
||||
// Group by buffer path to minimize fsyncs
|
||||
for (const Pending &p: batch) {
|
||||
process_one(p);
|
||||
}
|
||||
|
||||
// Throttled fsync: best-effort
|
||||
// Iterate unique contexts and fsync if needed
|
||||
// For stage 1, fsync all once per interval
|
||||
std::uint64_t now = now_ns();
|
||||
for (auto &kv: journals_) {
|
||||
JournalCtx &ctx = kv.second;
|
||||
if (ctx.fd >= 0) {
|
||||
if (ctx.last_fsync_ns == 0 || (now - ctx.last_fsync_ns) / 1000000ULL >= cfg_.
|
||||
fsync_interval_ms) {
|
||||
::fsync(ctx.fd);
|
||||
ctx.last_fsync_ns = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SwapManager::process_one(const Pending &p)
|
||||
{
|
||||
Buffer &buf = *p.buf;
|
||||
// Resolve context by path derived from buffer
|
||||
std::string path = ComputeSidecarPath(buf);
|
||||
// Get or create context keyed by this buffer pointer (stage 1 simplification)
|
||||
JournalCtx &ctx = journals_[p.buf];
|
||||
if (ctx.path.empty())
|
||||
ctx.path = path;
|
||||
if (!open_ctx(ctx))
|
||||
return;
|
||||
|
||||
// Build record: [type u8][len u24][payload][crc32 u32]
|
||||
std::uint8_t len3[3];
|
||||
put_u24(len3, static_cast<std::uint32_t>(p.payload.size()));
|
||||
|
||||
std::uint8_t head[4];
|
||||
head[0] = static_cast<std::uint8_t>(p.type);
|
||||
head[1] = len3[0];
|
||||
head[2] = len3[1];
|
||||
head[3] = len3[2];
|
||||
|
||||
std::uint32_t c = 0;
|
||||
c = crc32(head, sizeof(head), c);
|
||||
if (!p.payload.empty())
|
||||
c = crc32(p.payload.data(), p.payload.size(), c);
|
||||
|
||||
// Write (handle partial writes and check results)
|
||||
bool ok = write_full(ctx.fd, head, sizeof(head));
|
||||
if (ok && !p.payload.empty())
|
||||
ok = write_full(ctx.fd, p.payload.data(), p.payload.size());
|
||||
if (ok)
|
||||
ok = write_full(ctx.fd, &c, sizeof(c));
|
||||
(void) ok; // stage 1: best-effort; future work could mark ctx error state
|
||||
}
|
||||
} // namespace kte
|
||||
145
Swap.h
Normal file
145
Swap.h
Normal file
@@ -0,0 +1,145 @@
|
||||
// Swap.h - swap journal (crash recovery) writer/manager for kte
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
// Minimal record types for stage 1
|
||||
enum class SwapRecType : std::uint8_t {
|
||||
INS = 1,
|
||||
DEL = 2,
|
||||
SPLIT = 3,
|
||||
JOIN = 4,
|
||||
META = 0xF0,
|
||||
CHKPT = 0xFE,
|
||||
};
|
||||
|
||||
struct SwapConfig {
|
||||
// Grouping and durability knobs (stage 1 defaults)
|
||||
unsigned flush_interval_ms{200}; // group small writes
|
||||
unsigned fsync_interval_ms{1000}; // at most once per second
|
||||
};
|
||||
|
||||
// Lightweight interface that Buffer can call without depending on full manager impl
|
||||
class SwapRecorder {
|
||||
public:
|
||||
virtual ~SwapRecorder() = default;
|
||||
|
||||
virtual void RecordInsert(Buffer &buf, int row, int col, std::string_view text) = 0;
|
||||
|
||||
virtual void RecordDelete(Buffer &buf, int row, int col, std::size_t len) = 0;
|
||||
|
||||
virtual void RecordSplit(Buffer &buf, int row, int col) = 0;
|
||||
|
||||
virtual void RecordJoin(Buffer &buf, int row) = 0;
|
||||
|
||||
virtual void NotifyFilenameChanged(Buffer &buf) = 0;
|
||||
|
||||
virtual void SetSuspended(Buffer &buf, bool on) = 0;
|
||||
};
|
||||
|
||||
// SwapManager manages sidecar swap files and a single background writer thread.
|
||||
class SwapManager final : public SwapRecorder {
|
||||
public:
|
||||
SwapManager();
|
||||
|
||||
~SwapManager() override;
|
||||
|
||||
// Attach a buffer to begin journaling. Safe to call multiple times; idempotent.
|
||||
void Attach(Buffer *buf);
|
||||
|
||||
// Detach and close journal.
|
||||
void Detach(Buffer *buf);
|
||||
|
||||
// SwapRecorder: Notify that the buffer's filename changed (e.g., SaveAs)
|
||||
void NotifyFilenameChanged(Buffer &buf) override;
|
||||
|
||||
// SwapRecorder
|
||||
void RecordInsert(Buffer &buf, int row, int col, std::string_view text) override;
|
||||
|
||||
void RecordDelete(Buffer &buf, int row, int col, std::size_t len) override;
|
||||
|
||||
void RecordSplit(Buffer &buf, int row, int col) override;
|
||||
|
||||
void RecordJoin(Buffer &buf, int row) override;
|
||||
|
||||
// RAII guard to suspend recording for internal operations
|
||||
class SuspendGuard {
|
||||
public:
|
||||
SuspendGuard(SwapManager &m, Buffer *b);
|
||||
|
||||
~SuspendGuard();
|
||||
|
||||
private:
|
||||
SwapManager &m_;
|
||||
Buffer *buf_;
|
||||
bool prev_;
|
||||
};
|
||||
|
||||
// Per-buffer toggle
|
||||
void SetSuspended(Buffer &buf, bool on) override;
|
||||
|
||||
private:
|
||||
struct JournalCtx {
|
||||
std::string path;
|
||||
void *file{nullptr}; // FILE*
|
||||
int fd{-1};
|
||||
bool header_ok{false};
|
||||
bool suspended{false};
|
||||
std::uint64_t last_flush_ns{0};
|
||||
std::uint64_t last_fsync_ns{0};
|
||||
};
|
||||
|
||||
struct Pending {
|
||||
Buffer *buf{nullptr};
|
||||
SwapRecType type{SwapRecType::INS};
|
||||
std::vector<std::uint8_t> payload; // framed payload only
|
||||
bool urgent_flush{false};
|
||||
};
|
||||
|
||||
// Helpers
|
||||
static std::string ComputeSidecarPath(const Buffer &buf);
|
||||
|
||||
static std::uint64_t now_ns();
|
||||
|
||||
static bool ensure_parent_dir(const std::string &path);
|
||||
|
||||
static bool write_header(JournalCtx &ctx);
|
||||
|
||||
static bool open_ctx(JournalCtx &ctx);
|
||||
|
||||
static void close_ctx(JournalCtx &ctx);
|
||||
|
||||
static std::uint32_t crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed = 0);
|
||||
|
||||
static void put_varu64(std::vector<std::uint8_t> &out, std::uint64_t v);
|
||||
|
||||
static void put_u24(std::uint8_t dst[3], std::uint32_t v);
|
||||
|
||||
void enqueue(Pending &&p);
|
||||
|
||||
void writer_loop();
|
||||
|
||||
void process_one(const Pending &p);
|
||||
|
||||
// State
|
||||
SwapConfig cfg_{};
|
||||
std::unordered_map<Buffer *, JournalCtx> journals_;
|
||||
std::mutex mtx_;
|
||||
std::condition_variable cv_;
|
||||
std::vector<Pending> queue_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::thread worker_;
|
||||
};
|
||||
} // namespace kte
|
||||
@@ -55,6 +55,8 @@ TerminalFrontend::Init(Editor &ed)
|
||||
prev_r_ = r;
|
||||
prev_c_ = c;
|
||||
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
|
||||
// Attach editor to input handler for editor-owned features (e.g., universal argument)
|
||||
input_.Attach(&ed);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -100,4 +102,4 @@ TerminalFrontend::Shutdown()
|
||||
have_orig_tio_ = false;
|
||||
}
|
||||
endwin();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* TerminalFrontend - couples TerminalInputHandler + TerminalRenderer and owns ncurses lifecycle
|
||||
*/
|
||||
#ifndef KTE_TERMINAL_FRONTEND_H
|
||||
#define KTE_TERMINAL_FRONTEND_H
|
||||
|
||||
#pragma once
|
||||
#include <termios.h>
|
||||
|
||||
#include "Frontend.h"
|
||||
@@ -31,6 +29,4 @@ private:
|
||||
// Saved terminal attributes to restore on shutdown
|
||||
bool have_orig_tio_ = false;
|
||||
struct termios orig_tio_{};
|
||||
};
|
||||
|
||||
#endif // KTE_TERMINAL_FRONTEND_H
|
||||
};
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "TerminalInputHandler.h"
|
||||
#include "KKeymap.h"
|
||||
#include "Editor.h"
|
||||
|
||||
namespace {
|
||||
constexpr int
|
||||
@@ -21,84 +22,103 @@ static bool
|
||||
map_key_to_command(const int ch,
|
||||
bool &k_prefix,
|
||||
bool &esc_meta,
|
||||
// universal-argument state (by ref)
|
||||
bool &uarg_active,
|
||||
bool &uarg_collecting,
|
||||
bool &uarg_negative,
|
||||
bool &uarg_had_digits,
|
||||
int &uarg_value,
|
||||
std::string &uarg_text,
|
||||
bool &k_ctrl_pending,
|
||||
Editor *ed,
|
||||
MappedInput &out)
|
||||
{
|
||||
// Handle special keys from ncurses
|
||||
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
|
||||
switch (ch) {
|
||||
case KEY_MOUSE: {
|
||||
MEVENT ev{};
|
||||
if (getmouse(&ev) == OK) {
|
||||
// Mouse wheel → map to MoveUp/MoveDown one line per wheel notch
|
||||
case KEY_MOUSE: {
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
MEVENT ev{};
|
||||
if (getmouse(&ev) == OK) {
|
||||
// Mouse wheel → scroll viewport without moving cursor
|
||||
#ifdef BUTTON4_PRESSED
|
||||
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
|
||||
out = {true, CommandId::MoveUp, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
|
||||
out = {true, CommandId::ScrollUp, "", 0};
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef BUTTON5_PRESSED
|
||||
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
|
||||
out = {true, CommandId::MoveDown, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
|
||||
out = {true, CommandId::ScrollDown, "", 0};
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
// React to left button click/press
|
||||
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
|
||||
char buf[64];
|
||||
// Use screen coordinates; command handler will translate via offsets
|
||||
std::snprintf(buf, sizeof(buf), "@%d:%d", ev.y, ev.x);
|
||||
out = {true, CommandId::MoveCursorTo, std::string(buf), 0};
|
||||
return true;
|
||||
// React to left button click/press
|
||||
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
|
||||
char buf[64];
|
||||
// Use screen coordinates; command handler will translate via offsets
|
||||
std::snprintf(buf, sizeof(buf), "@%d:%d", ev.y, ev.x);
|
||||
out = {true, CommandId::MoveCursorTo, std::string(buf), 0};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// No actionable mouse event
|
||||
out.hasCommand = false;
|
||||
return true;
|
||||
}
|
||||
// No actionable mouse event
|
||||
out.hasCommand = false;
|
||||
return true;
|
||||
}
|
||||
case KEY_LEFT:
|
||||
out = {true, CommandId::MoveLeft, "", 0};
|
||||
return true;
|
||||
case KEY_RIGHT:
|
||||
out = {true, CommandId::MoveRight, "", 0};
|
||||
return true;
|
||||
case KEY_UP:
|
||||
out = {true, CommandId::MoveUp, "", 0};
|
||||
return true;
|
||||
case KEY_DOWN:
|
||||
out = {true, CommandId::MoveDown, "", 0};
|
||||
return true;
|
||||
case KEY_HOME:
|
||||
out = {true, CommandId::MoveHome, "", 0};
|
||||
return true;
|
||||
case KEY_END:
|
||||
out = {true, CommandId::MoveEnd, "", 0};
|
||||
return true;
|
||||
case KEY_PPAGE:
|
||||
out = {true, CommandId::PageUp, "", 0};
|
||||
return true;
|
||||
case KEY_NPAGE:
|
||||
out = {true, CommandId::PageDown, "", 0};
|
||||
return true;
|
||||
case KEY_DC:
|
||||
out = {true, CommandId::DeleteChar, "", 0};
|
||||
return true;
|
||||
case KEY_RESIZE:
|
||||
out = {true, CommandId::Refresh, "", 0};
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
case KEY_LEFT:
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::MoveLeft, "", 0};
|
||||
return true;
|
||||
case KEY_RIGHT:
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::MoveRight, "", 0};
|
||||
return true;
|
||||
case KEY_UP:
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::MoveUp, "", 0};
|
||||
return true;
|
||||
case KEY_DOWN:
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::MoveDown, "", 0};
|
||||
return true;
|
||||
case KEY_HOME:
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::MoveHome, "", 0};
|
||||
return true;
|
||||
case KEY_END:
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::MoveEnd, "", 0};
|
||||
return true;
|
||||
case KEY_PPAGE:
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::PageUp, "", 0};
|
||||
return true;
|
||||
case KEY_NPAGE:
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::PageDown, "", 0};
|
||||
return true;
|
||||
case KEY_DC:
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::DeleteChar, "", 0};
|
||||
return true;
|
||||
case KEY_RESIZE:
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::Refresh, "", 0};
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// ESC as cancel of prefix; many terminals send meta sequences as ESC+...
|
||||
if (ch == 27) {
|
||||
// ESC
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
esc_meta = true; // next key will be considered meta-modified
|
||||
out.hasCommand = false; // no command yet
|
||||
return true;
|
||||
@@ -107,59 +127,33 @@ map_key_to_command(const int ch,
|
||||
// Control keys
|
||||
if (ch == CTRL('K')) {
|
||||
// C-k prefix
|
||||
k_prefix = true;
|
||||
out = {true, CommandId::KPrefix, "", 0};
|
||||
k_prefix = true;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::KPrefix, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == CTRL('G')) {
|
||||
// cancel
|
||||
k_prefix = false;
|
||||
esc_meta = false;
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
esc_meta = false;
|
||||
// cancel universal argument as well
|
||||
uarg_active = false;
|
||||
uarg_collecting = false;
|
||||
uarg_negative = false;
|
||||
uarg_had_digits = false;
|
||||
uarg_value = 0;
|
||||
uarg_text.clear();
|
||||
if (ed)
|
||||
ed->UArgClear();
|
||||
out = {true, CommandId::Refresh, "", 0};
|
||||
return true;
|
||||
}
|
||||
// Universal argument: C-u
|
||||
if (ch == CTRL('U')) {
|
||||
// Start or extend universal argument
|
||||
if (!uarg_active) {
|
||||
uarg_active = true;
|
||||
uarg_collecting = true;
|
||||
uarg_negative = false;
|
||||
uarg_had_digits = false;
|
||||
uarg_value = 4; // default
|
||||
// Reset collected text and emit status update
|
||||
uarg_text.clear();
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
} else if (uarg_collecting && !uarg_had_digits && !uarg_negative) {
|
||||
// Bare repeated C-u multiplies by 4
|
||||
if (uarg_value <= 0)
|
||||
uarg_value = 4;
|
||||
else
|
||||
uarg_value *= 4;
|
||||
// Keep showing status (no digits yet)
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
} else {
|
||||
// If digits or '-' have been entered, C-u ends the argument (ready for next command)
|
||||
uarg_collecting = false;
|
||||
if (!uarg_had_digits && !uarg_negative && uarg_value <= 0)
|
||||
uarg_value = 4;
|
||||
}
|
||||
// No command produced by C-u itself
|
||||
out.hasCommand = false;
|
||||
if (ed)
|
||||
ed->UArgStart();
|
||||
out.hasCommand = false; // C-u itself doesn't issue a command
|
||||
return true;
|
||||
}
|
||||
// Tab (note: terminals encode Tab and C-i as the same code 9)
|
||||
if (ch == '\t') {
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out.hasCommand = true;
|
||||
out.id = CommandId::InsertText;
|
||||
out.arg = "\t";
|
||||
@@ -170,22 +164,39 @@ map_key_to_command(const int ch,
|
||||
// IMPORTANT: if we're in k-prefix, the very next key must be interpreted
|
||||
// via the C-k keymap first, even if it's a Control chord like C-d.
|
||||
if (k_prefix) {
|
||||
k_prefix = false; // consume the prefix for this one key
|
||||
// In k-prefix: allow a control qualifier via literal 'C' or '^'
|
||||
// Detect Control keycodes first
|
||||
bool ctrl = false;
|
||||
int ascii_key = ch;
|
||||
if (ch >= 1 && ch <= 26) {
|
||||
ctrl = true;
|
||||
ascii_key = 'a' + (ch - 1);
|
||||
}
|
||||
// If user typed literal 'C'/'c' or '^' as a qualifier, keep k-prefix and set pending
|
||||
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
|
||||
k_ctrl_pending = true;
|
||||
if (ed)
|
||||
ed->SetStatus("C-k C _");
|
||||
out.hasCommand = false;
|
||||
return true;
|
||||
}
|
||||
// For actual suffix, consume the k-prefix
|
||||
k_prefix = false;
|
||||
// Do NOT lowercase here; KLookupKCommand handles case-sensitive bindings
|
||||
CommandId id;
|
||||
if (KLookupKCommand(ascii_key, ctrl, id)) {
|
||||
bool pass_ctrl = (ctrl || k_ctrl_pending);
|
||||
k_ctrl_pending = false;
|
||||
if (KLookupKCommand(ascii_key, pass_ctrl, id)) {
|
||||
out = {true, id, "", 0};
|
||||
if (ed)
|
||||
ed->SetStatus(""); // clear "C-k _" hint after suffix
|
||||
} else {
|
||||
int shown = KLowerAscii(ascii_key);
|
||||
char c = (shown >= 0x20 && shown <= 0x7e) ? static_cast<char>(shown) : '?';
|
||||
std::string arg(1, c);
|
||||
out = {true, CommandId::UnknownKCommand, arg, 0};
|
||||
if (ed)
|
||||
ed->SetStatus(""); // clear hint; handler will set unknown status
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -201,8 +212,9 @@ map_key_to_command(const int ch,
|
||||
|
||||
// Enter
|
||||
if (ch == '\n' || ch == '\r') {
|
||||
k_prefix = false;
|
||||
out = {true, CommandId::Newline, "", 0};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::Newline, "", 0};
|
||||
return true;
|
||||
}
|
||||
// If previous key was ESC, interpret as meta and use ESC keymap
|
||||
@@ -212,6 +224,12 @@ map_key_to_command(const int ch,
|
||||
// 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 (ch == ',') {
|
||||
// Some terminals emit ',' when Shift state is lost after ESC; treat as '<'
|
||||
ascii_key = '<';
|
||||
} else if (ch == '.') {
|
||||
// Likewise, map '.' to '>'
|
||||
ascii_key = '>';
|
||||
} else if (ascii_key >= 'A' && ascii_key <= 'Z') {
|
||||
ascii_key = ascii_key - 'A' + 'a';
|
||||
}
|
||||
@@ -220,48 +238,26 @@ map_key_to_command(const int ch,
|
||||
out = {true, id, "", 0};
|
||||
return true;
|
||||
}
|
||||
// Unhandled meta key: no command
|
||||
out.hasCommand = false;
|
||||
// Unhandled ESC sequence: exit escape mode and show status
|
||||
out = {true, CommandId::UnknownEscCommand, "", 0};
|
||||
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};
|
||||
k_prefix = false;
|
||||
k_ctrl_pending = false;
|
||||
out = {true, CommandId::Backspace, "", 0};
|
||||
return true;
|
||||
}
|
||||
|
||||
// k_prefix handled earlier
|
||||
|
||||
// If collecting universal arg, handle digits and optional leading '-'
|
||||
if (uarg_active && uarg_collecting) {
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
int d = ch - '0';
|
||||
if (!uarg_had_digits) {
|
||||
// First digit overrides any 4^n default
|
||||
uarg_value = 0;
|
||||
uarg_had_digits = true;
|
||||
}
|
||||
if (uarg_value < 100000000) {
|
||||
// avoid overflow
|
||||
uarg_value = uarg_value * 10 + d;
|
||||
}
|
||||
// Update raw text and status to reflect collected digits
|
||||
uarg_text.push_back(static_cast<char>(ch));
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == '-' && !uarg_had_digits && !uarg_negative) {
|
||||
uarg_negative = true;
|
||||
// Show leading minus in status
|
||||
uarg_text = "-";
|
||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
||||
return true;
|
||||
}
|
||||
// Any other key will be processed as a command; fall through to mapping below
|
||||
// but mark collection finished so we apply the argument to that command
|
||||
uarg_collecting = false;
|
||||
// If universal argument is active at editor level and we get a digit, feed it
|
||||
if (ed && ed->UArg() != 0 && ch >= '0' && ch <= '9') {
|
||||
ed->UArgDigit(ch - '0');
|
||||
out.hasCommand = false; // keep collecting, no command yet
|
||||
return true;
|
||||
}
|
||||
|
||||
// Printable ASCII
|
||||
@@ -288,29 +284,11 @@ TerminalInputHandler::decode_(MappedInput &out)
|
||||
bool consumed = map_key_to_command(
|
||||
ch,
|
||||
k_prefix_, esc_meta_,
|
||||
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_, uarg_text_,
|
||||
k_ctrl_pending_,
|
||||
ed_,
|
||||
out);
|
||||
if (!consumed)
|
||||
return false;
|
||||
// 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) {
|
||||
int count = 0;
|
||||
if (!uarg_had_digits_ && !uarg_negative_) {
|
||||
// No explicit digits: use current value (default 4 or 4^n)
|
||||
count = (uarg_value_ > 0) ? uarg_value_ : 4;
|
||||
} else {
|
||||
count = uarg_value_;
|
||||
if (uarg_negative_)
|
||||
count = -count;
|
||||
}
|
||||
out.count = count;
|
||||
// Clear state
|
||||
uarg_active_ = false;
|
||||
uarg_collecting_ = false;
|
||||
uarg_negative_ = false;
|
||||
uarg_had_digits_ = false;
|
||||
uarg_value_ = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -320,4 +298,4 @@ TerminalInputHandler::Poll(MappedInput &out)
|
||||
{
|
||||
out = {};
|
||||
return decode_(out) && out.hasCommand;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* TerminalInputHandler - ncurses-based input handling for terminal mode
|
||||
*/
|
||||
#ifndef KTE_TERMINAL_INPUT_HANDLER_H
|
||||
#define KTE_TERMINAL_INPUT_HANDLER_H
|
||||
|
||||
#pragma once
|
||||
#include "InputHandler.h"
|
||||
|
||||
|
||||
@@ -13,6 +11,13 @@ public:
|
||||
|
||||
~TerminalInputHandler() override;
|
||||
|
||||
|
||||
void Attach(Editor *ed) override
|
||||
{
|
||||
ed_ = ed;
|
||||
}
|
||||
|
||||
|
||||
bool Poll(MappedInput &out) override;
|
||||
|
||||
private:
|
||||
@@ -20,16 +25,10 @@ private:
|
||||
|
||||
// ke-style prefix state
|
||||
bool k_prefix_ = false; // true after C-k until next key or ESC
|
||||
// Optional control qualifier inside k-prefix (e.g., user typed literal 'C' or '^')
|
||||
bool k_ctrl_pending_ = false;
|
||||
// Simple meta (ESC) state for ESC sequences like ESC b/f
|
||||
bool esc_meta_ = false;
|
||||
|
||||
// Universal argument (C-u) state
|
||||
bool uarg_active_ = false; // an argument is pending for the next command
|
||||
bool uarg_collecting_ = false; // collecting digits / '-' right now
|
||||
bool uarg_negative_ = false; // whether a leading '-' was supplied
|
||||
bool uarg_had_digits_ = false; // whether any digits were supplied
|
||||
int uarg_value_ = 0; // current absolute value (>=0)
|
||||
std::string uarg_text_; // raw digits/minus typed for status display
|
||||
};
|
||||
|
||||
#endif // KTE_TERMINAL_INPUT_HANDLER_H
|
||||
Editor *ed_ = nullptr; // attached editor for uarg handling
|
||||
};
|
||||
@@ -34,6 +34,8 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
|
||||
const Buffer *buf = ed.CurrentBuffer();
|
||||
int content_rows = rows - 1; // last line is status
|
||||
if (content_rows < 1)
|
||||
content_rows = 1;
|
||||
|
||||
int saved_cur_y = -1, saved_cur_x = -1; // logical cursor position within content area
|
||||
if (buf) {
|
||||
@@ -42,18 +44,18 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
std::size_t coloffs = buf->Coloffs();
|
||||
|
||||
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());
|
||||
}
|
||||
// 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) {
|
||||
move(r, 0);
|
||||
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
||||
std::size_t render_col = 0;
|
||||
std::size_t src_i = 0;
|
||||
for (int r = 0; r < content_rows; ++r) {
|
||||
move(r, 0);
|
||||
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
||||
std::size_t render_col = 0;
|
||||
std::size_t src_i = 0;
|
||||
// Compute matches for this line if search highlighting is active
|
||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||
std::vector<std::pair<std::size_t, std::size_t> > ranges; // [start, end)
|
||||
@@ -105,49 +107,78 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
bool hl_on = false;
|
||||
bool cur_on = false;
|
||||
int written = 0;
|
||||
if (li < lines.size()) {
|
||||
std::string line = static_cast<std::string>(lines[li]);
|
||||
src_i = 0;
|
||||
render_col = 0;
|
||||
// Syntax highlighting: fetch per-line spans
|
||||
const kte::LineHighlight *lh_ptr = nullptr;
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
lh_ptr = &buf->Highlighter()->GetLine(*buf, static_cast<int>(li), buf->Version());
|
||||
}
|
||||
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
|
||||
if (!lh_ptr) return kte::TokenKind::Default;
|
||||
for (const auto &sp: lh_ptr->spans) {
|
||||
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(src_index) < sp.col_end)
|
||||
return sp.kind;
|
||||
}
|
||||
return kte::TokenKind::Default;
|
||||
};
|
||||
auto apply_token_attr = [&](kte::TokenKind k) {
|
||||
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
|
||||
attrset(A_NORMAL);
|
||||
switch (k) {
|
||||
case kte::TokenKind::Keyword:
|
||||
case kte::TokenKind::Type:
|
||||
case kte::TokenKind::Constant:
|
||||
case kte::TokenKind::Function:
|
||||
attron(A_BOLD);
|
||||
break;
|
||||
case kte::TokenKind::Comment:
|
||||
attron(A_DIM);
|
||||
break;
|
||||
case kte::TokenKind::String:
|
||||
case kte::TokenKind::Char:
|
||||
case kte::TokenKind::Number:
|
||||
// standout a bit using A_UNDERLINE if available
|
||||
attron(A_UNDERLINE);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
while (written < cols) {
|
||||
char ch = ' ';
|
||||
bool from_src = false;
|
||||
if (li < lines.size()) {
|
||||
std::string line = static_cast<std::string>(lines[li]);
|
||||
src_i = 0;
|
||||
render_col = 0;
|
||||
// Syntax highlighting: fetch per-line spans (sanitized copy)
|
||||
std::vector<kte::HighlightSpan> sane_spans;
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
|
||||
HasHighlighter()) {
|
||||
kte::LineHighlight lh_val = buf->Highlighter()->GetLine(
|
||||
*buf, static_cast<int>(li), buf->Version());
|
||||
// Sanitize defensively: clamp to [0, line.size()], ensure end>=start, drop empties
|
||||
const std::size_t line_len = line.size();
|
||||
sane_spans.reserve(lh_val.spans.size());
|
||||
for (const auto &sp: lh_val.spans) {
|
||||
int s_raw = sp.col_start;
|
||||
int e_raw = sp.col_end;
|
||||
if (e_raw < s_raw)
|
||||
std::swap(e_raw, s_raw);
|
||||
std::size_t s = static_cast<std::size_t>(std::max(
|
||||
0, std::min(s_raw, static_cast<int>(line_len))));
|
||||
std::size_t e = static_cast<std::size_t>(std::max(
|
||||
static_cast<int>(s),
|
||||
std::min(e_raw, static_cast<int>(line_len))));
|
||||
if (e <= s)
|
||||
continue;
|
||||
sane_spans.push_back(kte::HighlightSpan{
|
||||
static_cast<int>(s), static_cast<int>(e), sp.kind
|
||||
});
|
||||
}
|
||||
std::sort(sane_spans.begin(), sane_spans.end(),
|
||||
[](const kte::HighlightSpan &a, const kte::HighlightSpan &b) {
|
||||
return a.col_start < b.col_start;
|
||||
});
|
||||
}
|
||||
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
|
||||
if (sane_spans.empty())
|
||||
return kte::TokenKind::Default;
|
||||
int si = static_cast<int>(src_index);
|
||||
for (const auto &sp: sane_spans) {
|
||||
if (si < sp.col_start)
|
||||
break;
|
||||
if (si >= sp.col_start && si < sp.col_end)
|
||||
return sp.kind;
|
||||
}
|
||||
return kte::TokenKind::Default;
|
||||
};
|
||||
auto apply_token_attr = [&](kte::TokenKind k) {
|
||||
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
|
||||
attrset(A_NORMAL);
|
||||
switch (k) {
|
||||
case kte::TokenKind::Keyword:
|
||||
case kte::TokenKind::Type:
|
||||
case kte::TokenKind::Constant:
|
||||
case kte::TokenKind::Function:
|
||||
attron(A_BOLD);
|
||||
break;
|
||||
case kte::TokenKind::Comment:
|
||||
attron(A_DIM);
|
||||
break;
|
||||
case kte::TokenKind::String:
|
||||
case kte::TokenKind::Char:
|
||||
case kte::TokenKind::Number:
|
||||
// standout a bit using A_UNDERLINE if available
|
||||
attron(A_UNDERLINE);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
while (written < cols) {
|
||||
char ch = ' ';
|
||||
bool from_src = false;
|
||||
if (src_i < line.size()) {
|
||||
unsigned char c = static_cast<unsigned char>(line[src_i]);
|
||||
if (c == '\t') {
|
||||
@@ -166,45 +197,45 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
next_tab -= to_skip;
|
||||
}
|
||||
// Now render visible spaces
|
||||
while (next_tab > 0 && written < cols) {
|
||||
bool in_hl = search_mode && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && src_i >= cur_mx
|
||||
&& src_i < cur_mend;
|
||||
// Toggle highlight attributes
|
||||
int attr = 0;
|
||||
if (in_hl)
|
||||
attr |= A_STANDOUT;
|
||||
if (in_cur)
|
||||
attr |= A_BOLD;
|
||||
if ((attr & A_STANDOUT) && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!(attr & A_STANDOUT) && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if ((attr & A_BOLD) && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!(attr & A_BOLD) && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
// Apply syntax attribute only if not in search highlight
|
||||
if (!in_hl) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(' ');
|
||||
++written;
|
||||
++render_col;
|
||||
--next_tab;
|
||||
}
|
||||
++src_i;
|
||||
continue;
|
||||
} else {
|
||||
while (next_tab > 0 && written < cols) {
|
||||
bool in_hl = search_mode && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && src_i >= cur_mx
|
||||
&& src_i < cur_mend;
|
||||
// Toggle highlight attributes
|
||||
int attr = 0;
|
||||
if (in_hl)
|
||||
attr |= A_STANDOUT;
|
||||
if (in_cur)
|
||||
attr |= A_BOLD;
|
||||
if ((attr & A_STANDOUT) && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!(attr & A_STANDOUT) && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if ((attr & A_BOLD) && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!(attr & A_BOLD) && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
// Apply syntax attribute only if not in search highlight
|
||||
if (!in_hl) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(' ');
|
||||
++written;
|
||||
++render_col;
|
||||
--next_tab;
|
||||
}
|
||||
++src_i;
|
||||
continue;
|
||||
} else {
|
||||
// normal char
|
||||
if (render_col < coloffs) {
|
||||
++render_col;
|
||||
@@ -219,55 +250,75 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
ch = ' ';
|
||||
from_src = false;
|
||||
}
|
||||
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
|
||||
cur_mend;
|
||||
if (in_hl && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!in_hl && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (in_cur && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!in_cur && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
if (!in_hl && from_src) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(static_cast<unsigned char>(ch));
|
||||
++written;
|
||||
++render_col;
|
||||
if (from_src)
|
||||
++src_i;
|
||||
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
|
||||
cur_mend;
|
||||
if (in_hl && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!in_hl && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (in_cur && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!in_cur && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
if (!in_hl && from_src) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(static_cast<unsigned char>(ch));
|
||||
++written;
|
||||
++render_col;
|
||||
if (from_src)
|
||||
++src_i;
|
||||
if (src_i >= line.size() && written >= cols)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
attrset(A_NORMAL);
|
||||
clrtoeol();
|
||||
}
|
||||
if (hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
attrset(A_NORMAL);
|
||||
clrtoeol();
|
||||
}
|
||||
|
||||
// Place terminal cursor at logical position accounting for tabs and coloffs
|
||||
// Place terminal cursor at logical position accounting for tabs and coloffs.
|
||||
// Recompute the rendered X using the same logic as the drawing loop to avoid
|
||||
// any drift between the command-layer computation and the terminal renderer.
|
||||
std::size_t cy = buf->Cury();
|
||||
std::size_t rx = buf->Rx(); // render x computed by command layer
|
||||
int cur_y = static_cast<int>(cy) - static_cast<int>(buf->Rowoffs());
|
||||
int cur_x = static_cast<int>(rx) - static_cast<int>(buf->Coloffs());
|
||||
std::size_t cx = buf->Curx();
|
||||
int cur_y = static_cast<int>(cy) - static_cast<int>(buf->Rowoffs());
|
||||
std::size_t rx_recomputed = 0;
|
||||
if (cy < lines.size()) {
|
||||
const std::string line_for_cursor = static_cast<std::string>(lines[cy]);
|
||||
std::size_t src_i_cur = 0;
|
||||
std::size_t render_col_cur = 0;
|
||||
while (src_i_cur < line_for_cursor.size() && src_i_cur < cx) {
|
||||
unsigned char ccur = static_cast<unsigned char>(line_for_cursor[src_i_cur]);
|
||||
if (ccur == '\t') {
|
||||
std::size_t next_tab = tabw - (render_col_cur % tabw);
|
||||
render_col_cur += next_tab;
|
||||
++src_i_cur;
|
||||
} else {
|
||||
++render_col_cur;
|
||||
++src_i_cur;
|
||||
}
|
||||
}
|
||||
rx_recomputed = render_col_cur;
|
||||
}
|
||||
int cur_x = static_cast<int>(rx_recomputed) - static_cast<int>(buf->Coloffs());
|
||||
if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) {
|
||||
// remember where to leave the terminal cursor after status is drawn
|
||||
saved_cur_y = cur_y;
|
||||
@@ -464,4 +515,4 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* TerminalRenderer - ncurses-based renderer for terminal mode
|
||||
*/
|
||||
#ifndef KTE_TERMINAL_RENDERER_H
|
||||
#define KTE_TERMINAL_RENDERER_H
|
||||
|
||||
#pragma once
|
||||
#include "Renderer.h"
|
||||
|
||||
|
||||
@@ -14,6 +12,4 @@ public:
|
||||
~TerminalRenderer() override;
|
||||
|
||||
void Draw(Editor &ed) override;
|
||||
};
|
||||
|
||||
#endif // KTE_TERMINAL_RENDERER_H
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* TestFrontend.h - headless frontend for testing with programmable input
|
||||
*/
|
||||
#ifndef KTE_TEST_FRONTEND_H
|
||||
#define KTE_TEST_FRONTEND_H
|
||||
|
||||
#pragma once
|
||||
#include "Frontend.h"
|
||||
#include "TestInputHandler.h"
|
||||
#include "TestRenderer.h"
|
||||
@@ -36,6 +34,4 @@ public:
|
||||
private:
|
||||
TestInputHandler input_{};
|
||||
TestRenderer renderer_{};
|
||||
};
|
||||
|
||||
#endif // KTE_TEST_FRONTEND_H
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* TestInputHandler.h - programmable input handler for testing
|
||||
*/
|
||||
#ifndef KTE_TEST_INPUT_HANDLER_H
|
||||
#define KTE_TEST_INPUT_HANDLER_H
|
||||
|
||||
#pragma once
|
||||
#include <queue>
|
||||
|
||||
#include "InputHandler.h"
|
||||
@@ -29,6 +27,4 @@ public:
|
||||
|
||||
private:
|
||||
std::queue<MappedInput> queue_;
|
||||
};
|
||||
|
||||
#endif // KTE_TEST_INPUT_HANDLER_H
|
||||
};
|
||||
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* TestRenderer.h - minimal renderer for testing (no actual display)
|
||||
*/
|
||||
#ifndef KTE_TEST_RENDERER_H
|
||||
#define KTE_TEST_RENDERER_H
|
||||
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
|
||||
#include "Renderer.h"
|
||||
@@ -31,6 +29,4 @@ public:
|
||||
|
||||
private:
|
||||
std::size_t draw_count_ = 0;
|
||||
};
|
||||
|
||||
#endif // KTE_TEST_RENDERER_H
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
#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
|
||||
@@ -1,6 +1,4 @@
|
||||
#ifndef KTE_UNDONODE_H
|
||||
#define KTE_UNDONODE_H
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
@@ -20,7 +18,4 @@ struct UndoNode {
|
||||
std::string text;
|
||||
UndoNode *child = nullptr; // next in current timeline
|
||||
UndoNode *next = nullptr; // redo branch
|
||||
};
|
||||
|
||||
|
||||
#endif // KTE_UNDONODE_H
|
||||
};
|
||||
61
UndoNodePool.h
Normal file
61
UndoNodePool.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
#include <stack>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "UndoNode.h"
|
||||
|
||||
// Pool allocator for UndoNode to eliminate frequent malloc/free.
|
||||
// Uses fixed-size blocks to keep node addresses stable.
|
||||
class UndoNodePool {
|
||||
public:
|
||||
explicit UndoNodePool(std::size_t block_size = 64)
|
||||
: block_size_(block_size) {}
|
||||
|
||||
|
||||
UndoNode *acquire()
|
||||
{
|
||||
if (available_.empty())
|
||||
allocate_block();
|
||||
auto *node = available_.top();
|
||||
available_.pop();
|
||||
// Node comes zeroed; ensure links are reset
|
||||
node->text.clear();
|
||||
node->child = nullptr;
|
||||
node->next = nullptr;
|
||||
node->row = node->col = 0;
|
||||
node->type = UndoType{};
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
void release(UndoNode *node)
|
||||
{
|
||||
if (!node)
|
||||
return;
|
||||
// Clear heavy fields to free memory held by strings
|
||||
node->text.clear();
|
||||
node->child = nullptr;
|
||||
node->next = nullptr;
|
||||
node->row = node->col = 0;
|
||||
node->type = UndoType{};
|
||||
available_.push(node);
|
||||
}
|
||||
|
||||
private:
|
||||
void allocate_block()
|
||||
{
|
||||
// allocate a new block; keep ownership so memory stays valid
|
||||
std::unique_ptr<UndoNode[]> block(new UndoNode[block_size_]);
|
||||
UndoNode *base = block.get();
|
||||
blocks_.push_back(std::move(block));
|
||||
for (std::size_t i = 0; i < block_size_; ++i) {
|
||||
// ensure the node is reset; rely on default constructor/zero init
|
||||
available_.push(&base[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::size_t block_size_;
|
||||
std::vector<std::unique_ptr<UndoNode[]> > blocks_;
|
||||
std::stack<UndoNode *> available_;
|
||||
};
|
||||
174
UndoSystem.cc
174
UndoSystem.cc
@@ -11,214 +11,66 @@ UndoSystem::UndoSystem(Buffer &owner, UndoTree &tree)
|
||||
void
|
||||
UndoSystem::Begin(UndoType type)
|
||||
{
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("Begin");
|
||||
#endif
|
||||
// Reuse pending if batching conditions are met
|
||||
const int row = static_cast<int>(buf_->Cury());
|
||||
const int col = static_cast<int>(buf_->Curx());
|
||||
if (tree_.pending && tree_.pending->type == type && tree_.pending->row == row) {
|
||||
if (type == UndoType::Delete) {
|
||||
// Support batching both forward deletes (DeleteChar) and backspace (prepend case)
|
||||
// Forward delete: cursor stays at anchor col; keep batching when col == anchor
|
||||
const auto anchor = static_cast<std::size_t>(tree_.pending->col);
|
||||
if (anchor == static_cast<std::size_t>(col)) {
|
||||
pending_prepend_ = false;
|
||||
return; // keep batching forward delete
|
||||
}
|
||||
// Backspace: cursor moved left by exactly one position relative to current anchor.
|
||||
// Extend batch by shifting anchor left and prepending the deleted byte.
|
||||
if (static_cast<std::size_t>(col) + 1 == anchor) {
|
||||
tree_.pending->col = col;
|
||||
pending_prepend_ = true;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
std::size_t expected = static_cast<std::size_t>(tree_.pending->col) + tree_.pending->text.
|
||||
size();
|
||||
if (expected == static_cast<std::size_t>(col)) {
|
||||
pending_prepend_ = false;
|
||||
return; // keep batching
|
||||
}
|
||||
}
|
||||
}
|
||||
// Otherwise commit any existing batch and start a new node
|
||||
commit();
|
||||
auto *node = new UndoNode();
|
||||
node->type = type;
|
||||
node->row = row;
|
||||
node->col = col;
|
||||
node->child = nullptr;
|
||||
node->next = nullptr;
|
||||
tree_.pending = node;
|
||||
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));
|
||||
// STUB: Undo system incomplete - disabled until it can be properly implemented
|
||||
(void) type;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::Append(char ch)
|
||||
{
|
||||
if (!tree_.pending)
|
||||
return;
|
||||
if (pending_prepend_ && tree_.pending->type == UndoType::Delete) {
|
||||
// Prepend for backspace so that text is in increasing column order
|
||||
tree_.pending->text.insert(tree_.pending->text.begin(), ch);
|
||||
} else {
|
||||
tree_.pending->text.push_back(ch);
|
||||
}
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("Append:ch");
|
||||
#endif
|
||||
// STUB: Undo system incomplete - disabled until it can be properly implemented
|
||||
(void) ch;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::Append(std::string_view text)
|
||||
{
|
||||
if (!tree_.pending)
|
||||
return;
|
||||
tree_.pending->text.append(text.data(), text.size());
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("Append:sv");
|
||||
#endif
|
||||
// STUB: Undo system incomplete - disabled until it can be properly implemented
|
||||
(void) text;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::commit()
|
||||
{
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("commit:enter");
|
||||
#endif
|
||||
if (!tree_.pending)
|
||||
return;
|
||||
|
||||
// If we have redo branches from current, discard them (non-linear behavior)
|
||||
if (tree_.current && tree_.current->child) {
|
||||
free_node(tree_.current->child);
|
||||
tree_.current->child = nullptr;
|
||||
// We diverged; saved snapshot cannot be on discarded branch anymore
|
||||
if (tree_.saved) {
|
||||
// If saved is not equal to current, keep it; if it was on discarded branch we cannot easily detect now.
|
||||
// For simplicity, leave saved as-is; dirty flag uses pointer equality.
|
||||
}
|
||||
}
|
||||
|
||||
// Attach pending as next state
|
||||
if (!tree_.root) {
|
||||
tree_.root = tree_.pending;
|
||||
tree_.current = tree_.pending;
|
||||
} else if (!tree_.current) {
|
||||
// Should not happen if root exists, but handle gracefully
|
||||
tree_.current = tree_.pending;
|
||||
} else {
|
||||
// Attach as primary child (head of redo list)
|
||||
tree_.pending->next = nullptr;
|
||||
tree_.current->child = tree_.pending;
|
||||
tree_.current = tree_.pending;
|
||||
}
|
||||
tree_.pending = nullptr;
|
||||
update_dirty_flag();
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("commit:done");
|
||||
#endif
|
||||
// post-conditions
|
||||
assert(tree_.pending == nullptr && "pending must be cleared after commit");
|
||||
// STUB: Undo system incomplete - disabled until it can be properly implemented
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::undo()
|
||||
{
|
||||
// Close any pending batch
|
||||
commit();
|
||||
if (!tree_.current)
|
||||
return;
|
||||
UndoNode *parent = find_parent(tree_.root, tree_.current);
|
||||
UndoNode *node = tree_.current;
|
||||
// Apply inverse of current node
|
||||
apply(node, -1);
|
||||
tree_.current = parent;
|
||||
update_dirty_flag();
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("undo");
|
||||
#endif
|
||||
// STUB: Undo system incomplete - disabled until it can be properly implemented
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::redo()
|
||||
{
|
||||
// Redo next child along current timeline
|
||||
if (tree_.pending) {
|
||||
// If app added pending edits, finalize them before redo chain
|
||||
commit();
|
||||
}
|
||||
UndoNode *next = nullptr;
|
||||
if (!tree_.current) {
|
||||
next = tree_.root; // if nothing yet, try applying first node
|
||||
} else {
|
||||
next = tree_.current->child;
|
||||
}
|
||||
if (!next)
|
||||
return;
|
||||
apply(next, +1);
|
||||
tree_.current = next;
|
||||
update_dirty_flag();
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("redo");
|
||||
#endif
|
||||
// STUB: Undo system incomplete - disabled until it can be properly implemented
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::mark_saved()
|
||||
{
|
||||
tree_.saved = tree_.current;
|
||||
update_dirty_flag();
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("mark_saved");
|
||||
#endif
|
||||
// STUB: Undo system incomplete - disabled until it can be properly implemented
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::discard_pending()
|
||||
{
|
||||
if (tree_.pending) {
|
||||
delete tree_.pending;
|
||||
tree_.pending = nullptr;
|
||||
}
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("discard_pending");
|
||||
#endif
|
||||
// STUB: Undo system incomplete - disabled until it can be properly implemented
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
UndoSystem::clear()
|
||||
{
|
||||
if (tree_.root) {
|
||||
free_node(tree_.root);
|
||||
}
|
||||
if (tree_.pending) {
|
||||
delete tree_.pending;
|
||||
}
|
||||
tree_.root = tree_.current = tree_.saved = tree_.pending = nullptr;
|
||||
update_dirty_flag();
|
||||
#ifdef KTE_UNDO_DEBUG
|
||||
debug_log("clear");
|
||||
#endif
|
||||
// STUB: Undo system incomplete - disabled until it can be properly implemented
|
||||
}
|
||||
|
||||
|
||||
@@ -395,4 +247,4 @@ UndoSystem::debug_log(const char *op) const
|
||||
#else
|
||||
(void) op;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
10
UndoSystem.h
10
UndoSystem.h
@@ -1,6 +1,4 @@
|
||||
#ifndef KTE_UNDOSYSTEM_H
|
||||
#define KTE_UNDOSYSTEM_H
|
||||
|
||||
#pragma once
|
||||
#include <string_view>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -52,8 +50,4 @@ private:
|
||||
|
||||
Buffer *buf_;
|
||||
UndoTree &tree_;
|
||||
// Internal hint for Delete batching: whether next Append() should prepend
|
||||
bool pending_prepend_ = false;
|
||||
};
|
||||
|
||||
#endif // KTE_UNDOSYSTEM_H
|
||||
};
|
||||
@@ -1,6 +1,4 @@
|
||||
#ifndef KTE_UNDOTREE_H
|
||||
#define KTE_UNDOTREE_H
|
||||
|
||||
#pragma once
|
||||
#include "UndoNode.h"
|
||||
|
||||
|
||||
@@ -9,7 +7,4 @@ struct UndoTree {
|
||||
UndoNode *current = nullptr; // current state of buffer
|
||||
UndoNode *saved = nullptr; // points to node matching last save (for dirty flag)
|
||||
UndoNode *pending = nullptr; // in-progress batch (detached)
|
||||
};
|
||||
|
||||
|
||||
#endif // KTE_UNDOTREE_H
|
||||
};
|
||||
206
bench/BufferBench.cc
Normal file
206
bench/BufferBench.cc
Normal file
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* BufferBench.cc - microbenchmarks for GapBuffer and PieceTable
|
||||
*
|
||||
* This benchmark exercises the public APIs shared by both structures as used
|
||||
* in Buffer::Line: Reserve, AppendChar, Append, PrependChar, Prepend, Clear.
|
||||
*
|
||||
* Run examples:
|
||||
* ./kte_bench_buffer # defaults
|
||||
* ./kte_bench_buffer 200000 8 4096 # N=200k, rounds=8, chunk=4096
|
||||
*/
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <typeinfo>
|
||||
|
||||
#include "GapBuffer.h"
|
||||
#include "PieceTable.h"
|
||||
|
||||
using clock_t = std::chrono::steady_clock;
|
||||
using us = std::chrono::microseconds;
|
||||
|
||||
struct Result {
|
||||
std::string name;
|
||||
std::string scenario;
|
||||
double micros = 0.0;
|
||||
std::size_t bytes = 0;
|
||||
};
|
||||
|
||||
|
||||
static void
|
||||
print_header()
|
||||
{
|
||||
std::cout << std::left << std::setw(14) << "Structure"
|
||||
<< std::left << std::setw(18) << "Scenario"
|
||||
<< std::right << std::setw(12) << "time(us)"
|
||||
<< std::right << std::setw(14) << "bytes"
|
||||
<< std::right << std::setw(14) << "MB/s"
|
||||
<< "\n";
|
||||
std::cout << std::string(72, '-') << "\n";
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
print_row(const Result &r)
|
||||
{
|
||||
double mb = r.bytes / (1024.0 * 1024.0);
|
||||
double mbps = (r.micros > 0.0) ? (mb / (r.micros / 1'000'000.0)) : 0.0;
|
||||
std::cout << std::left << std::setw(14) << r.name
|
||||
<< std::left << std::setw(18) << r.scenario
|
||||
<< std::right << std::setw(12) << std::fixed << std::setprecision(2) << r.micros
|
||||
<< std::right << std::setw(14) << r.bytes
|
||||
<< std::right << std::setw(14) << std::fixed << std::setprecision(2) << mbps
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
|
||||
template<typename Buf>
|
||||
Result
|
||||
bench_sequential_append(std::size_t N, int rounds)
|
||||
{
|
||||
Result r;
|
||||
r.name = typeid(Buf).name();
|
||||
r.scenario = "seq_append";
|
||||
const char c = 'x';
|
||||
auto start = clock_t::now();
|
||||
std::size_t bytes = 0;
|
||||
for (int t = 0; t < rounds; ++t) {
|
||||
Buf b;
|
||||
b.Reserve(N);
|
||||
for (std::size_t i = 0; i < N; ++i) {
|
||||
b.AppendChar(c);
|
||||
}
|
||||
bytes += N;
|
||||
}
|
||||
auto end = clock_t::now();
|
||||
r.micros = std::chrono::duration_cast<us>(end - start).count();
|
||||
r.bytes = bytes;
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
template<typename Buf>
|
||||
Result
|
||||
bench_sequential_prepend(std::size_t N, int rounds)
|
||||
{
|
||||
Result r;
|
||||
r.name = typeid(Buf).name();
|
||||
r.scenario = "seq_prepend";
|
||||
const char c = 'x';
|
||||
auto start = clock_t::now();
|
||||
std::size_t bytes = 0;
|
||||
for (int t = 0; t < rounds; ++t) {
|
||||
Buf b;
|
||||
b.Reserve(N);
|
||||
for (std::size_t i = 0; i < N; ++i) {
|
||||
b.PrependChar(c);
|
||||
}
|
||||
bytes += N;
|
||||
}
|
||||
auto end = clock_t::now();
|
||||
r.micros = std::chrono::duration_cast<us>(end - start).count();
|
||||
r.bytes = bytes;
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
template<typename Buf>
|
||||
Result
|
||||
bench_chunk_append(std::size_t N, std::size_t chunk, int rounds)
|
||||
{
|
||||
Result r;
|
||||
r.name = typeid(Buf).name();
|
||||
r.scenario = "chunk_append";
|
||||
std::string payload(chunk, 'y');
|
||||
auto start = clock_t::now();
|
||||
std::size_t bytes = 0;
|
||||
for (int t = 0; t < rounds; ++t) {
|
||||
Buf b;
|
||||
b.Reserve(N);
|
||||
std::size_t written = 0;
|
||||
while (written < N) {
|
||||
std::size_t now = std::min(chunk, N - written);
|
||||
b.Append(payload.data(), now);
|
||||
written += now;
|
||||
}
|
||||
bytes += N;
|
||||
}
|
||||
auto end = clock_t::now();
|
||||
r.micros = std::chrono::duration_cast<us>(end - start).count();
|
||||
r.bytes = bytes;
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
template<typename Buf>
|
||||
Result
|
||||
bench_mixed(std::size_t N, std::size_t chunk, int rounds)
|
||||
{
|
||||
Result r;
|
||||
r.name = typeid(Buf).name();
|
||||
r.scenario = "mixed";
|
||||
std::string payload(chunk, 'z');
|
||||
auto start = clock_t::now();
|
||||
std::size_t bytes = 0;
|
||||
for (int t = 0; t < rounds; ++t) {
|
||||
Buf b;
|
||||
b.Reserve(N);
|
||||
std::size_t written = 0;
|
||||
while (written < N) {
|
||||
// alternate append/prepend with small chunks
|
||||
std::size_t now = std::min(chunk, N - written);
|
||||
if ((written / chunk) % 2 == 0) {
|
||||
b.Append(payload.data(), now);
|
||||
} else {
|
||||
b.Prepend(payload.data(), now);
|
||||
}
|
||||
written += now;
|
||||
}
|
||||
bytes += N;
|
||||
}
|
||||
auto end = clock_t::now();
|
||||
r.micros = std::chrono::duration_cast<us>(end - start).count();
|
||||
r.bytes = bytes;
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
// Parameters
|
||||
std::size_t N = 100'000; // bytes per round
|
||||
int rounds = 5; // iterations
|
||||
std::size_t chunk = 1024; // chunk size for chunked scenarios
|
||||
if (argc >= 2)
|
||||
N = static_cast<std::size_t>(std::stoull(argv[1]));
|
||||
if (argc >= 3)
|
||||
rounds = std::stoi(argv[2]);
|
||||
if (argc >= 4)
|
||||
chunk = static_cast<std::size_t>(std::stoull(argv[3]));
|
||||
|
||||
std::cout << "KTE Buffer Microbenchmarks" << "\n";
|
||||
std::cout << "N=" << N << ", rounds=" << rounds << ", chunk=" << chunk << "\n\n";
|
||||
|
||||
print_header();
|
||||
|
||||
// Run for GapBuffer
|
||||
print_row(bench_sequential_append<GapBuffer>(N, rounds));
|
||||
print_row(bench_sequential_prepend<GapBuffer>(N, rounds));
|
||||
print_row(bench_chunk_append<GapBuffer>(N, chunk, rounds));
|
||||
print_row(bench_mixed<GapBuffer>(N, chunk, rounds));
|
||||
|
||||
// Run for PieceTable
|
||||
print_row(bench_sequential_append<PieceTable>(N, rounds));
|
||||
print_row(bench_sequential_prepend<PieceTable>(N, rounds));
|
||||
print_row(bench_chunk_append<PieceTable>(N, chunk, rounds));
|
||||
print_row(bench_mixed<PieceTable>(N, chunk, rounds));
|
||||
|
||||
return 0;
|
||||
}
|
||||
318
bench/PerformanceSuite.cc
Normal file
318
bench/PerformanceSuite.cc
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* PerformanceSuite.cc - broader performance and verification benchmarks
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <typeinfo>
|
||||
#include <vector>
|
||||
|
||||
#include "GapBuffer.h"
|
||||
#include "PieceTable.h"
|
||||
#include "OptimizedSearch.h"
|
||||
|
||||
using clock_t = std::chrono::steady_clock;
|
||||
using us = std::chrono::microseconds;
|
||||
|
||||
namespace {
|
||||
struct Stat {
|
||||
double micros{0.0};
|
||||
std::size_t bytes{0};
|
||||
std::size_t ops{0};
|
||||
};
|
||||
|
||||
|
||||
static void
|
||||
print_header(const std::string &title)
|
||||
{
|
||||
std::cout << "\n" << title << "\n";
|
||||
std::cout << std::left << std::setw(18) << "Case"
|
||||
<< std::left << std::setw(18) << "Type"
|
||||
<< std::right << std::setw(12) << "time(us)"
|
||||
<< std::right << std::setw(14) << "bytes"
|
||||
<< std::right << std::setw(14) << "ops/s"
|
||||
<< std::right << std::setw(14) << "MB/s"
|
||||
<< "\n";
|
||||
std::cout << std::string(90, '-') << "\n";
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
print_row(const std::string &caseName, const std::string &typeName, const Stat &s)
|
||||
{
|
||||
double mb = s.bytes / (1024.0 * 1024.0);
|
||||
double sec = s.micros / 1'000'000.0;
|
||||
double mbps = sec > 0 ? (mb / sec) : 0.0;
|
||||
double opss = sec > 0 ? (static_cast<double>(s.ops) / sec) : 0.0;
|
||||
std::cout << std::left << std::setw(18) << caseName
|
||||
<< std::left << std::setw(18) << typeName
|
||||
<< std::right << std::setw(12) << std::fixed << std::setprecision(2) << s.micros
|
||||
<< std::right << std::setw(14) << s.bytes
|
||||
<< std::right << std::setw(14) << std::fixed << std::setprecision(2) << opss
|
||||
<< std::right << std::setw(14) << std::fixed << std::setprecision(2) << mbps
|
||||
<< "\n";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class PerformanceSuite {
|
||||
public:
|
||||
void benchmarkBufferOperations(std::size_t N, int rounds, std::size_t chunk)
|
||||
{
|
||||
print_header("Buffer Operations");
|
||||
run_buffer_case<GapBuffer>("append_char", N, rounds, chunk, [&](auto &b, std::size_t count) {
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
b.AppendChar('a');
|
||||
});
|
||||
run_buffer_case<GapBuffer>("prepend_char", N, rounds, chunk, [&](auto &b, std::size_t count) {
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
b.PrependChar('a');
|
||||
});
|
||||
run_buffer_case<GapBuffer>("chunk_mix", N, rounds, chunk, [&](auto &b, std::size_t) {
|
||||
std::string payload(chunk, 'x');
|
||||
std::size_t written = 0;
|
||||
while (written < N) {
|
||||
std::size_t now = std::min(chunk, N - written);
|
||||
if (((written / chunk) & 1) == 0)
|
||||
b.Append(payload.data(), now);
|
||||
else
|
||||
b.Prepend(payload.data(), now);
|
||||
written += now;
|
||||
}
|
||||
});
|
||||
run_buffer_case<PieceTable>("append_char", N, rounds, chunk, [&](auto &b, std::size_t count) {
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
b.AppendChar('a');
|
||||
});
|
||||
run_buffer_case<PieceTable>("prepend_char", N, rounds, chunk, [&](auto &b, std::size_t count) {
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
b.PrependChar('a');
|
||||
});
|
||||
run_buffer_case<PieceTable>("chunk_mix", N, rounds, chunk, [&](auto &b, std::size_t) {
|
||||
std::string payload(chunk, 'x');
|
||||
std::size_t written = 0;
|
||||
while (written < N) {
|
||||
std::size_t now = std::min(chunk, N - written);
|
||||
if (((written / chunk) & 1) == 0)
|
||||
b.Append(payload.data(), now);
|
||||
else
|
||||
b.Prepend(payload.data(), now);
|
||||
written += now;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void benchmarkSearchOperations(std::size_t textLen, std::size_t patLen, int rounds)
|
||||
{
|
||||
print_header("Search Operations");
|
||||
std::mt19937_64 rng(0xC0FFEE);
|
||||
std::uniform_int_distribution<int> dist('a', 'z');
|
||||
std::string text(textLen, '\0');
|
||||
for (auto &ch: text)
|
||||
ch = static_cast<char>(dist(rng));
|
||||
std::string pattern(patLen, '\0');
|
||||
for (auto &ch: pattern)
|
||||
ch = static_cast<char>(dist(rng));
|
||||
|
||||
// Ensure at least one hit
|
||||
if (textLen >= patLen && patLen > 0) {
|
||||
std::size_t pos = textLen / 2;
|
||||
std::memcpy(&text[pos], pattern.data(), patLen);
|
||||
}
|
||||
|
||||
// OptimizedSearch find_all vs std::string reference
|
||||
OptimizedSearch os;
|
||||
Stat s{};
|
||||
auto start = clock_t::now();
|
||||
std::size_t matches = 0;
|
||||
std::size_t bytesScanned = 0;
|
||||
for (int r = 0; r < rounds; ++r) {
|
||||
auto hits = os.find_all(text, pattern, 0);
|
||||
matches += hits.size();
|
||||
bytesScanned += text.size();
|
||||
// Verify with reference
|
||||
std::vector<std::size_t> ref;
|
||||
std::size_t from = 0;
|
||||
while (true) {
|
||||
auto p = text.find(pattern, from);
|
||||
if (p == std::string::npos)
|
||||
break;
|
||||
ref.push_back(p);
|
||||
from = p + (patLen ? patLen : 1);
|
||||
}
|
||||
assert(ref == hits);
|
||||
}
|
||||
auto end = clock_t::now();
|
||||
s.micros = std::chrono::duration_cast<us>(end - start).count();
|
||||
s.bytes = bytesScanned;
|
||||
s.ops = matches;
|
||||
print_row("find_all", "OptimizedSearch", s);
|
||||
}
|
||||
|
||||
|
||||
void benchmarkMemoryAllocation(std::size_t N, int rounds)
|
||||
{
|
||||
print_header("Memory Allocation (allocations during editing)");
|
||||
// Measure number of allocations by simulating editing patterns.
|
||||
auto run_session = [&](auto &&buffer) {
|
||||
// alternate small appends and prepends
|
||||
const std::size_t chunk = 32;
|
||||
std::string payload(chunk, 'q');
|
||||
for (int r = 0; r < rounds; ++r) {
|
||||
buffer.Clear();
|
||||
for (std::size_t i = 0; i < N; i += chunk)
|
||||
buffer.Append(payload.data(), std::min(chunk, N - i));
|
||||
for (std::size_t i = 0; i < N / 2; i += chunk)
|
||||
buffer.Prepend(payload.data(), std::min(chunk, N / 2 - i));
|
||||
}
|
||||
};
|
||||
|
||||
// Local allocation counters for this TU via overriding operators
|
||||
reset_alloc_counters();
|
||||
GapBuffer gb;
|
||||
run_session(gb);
|
||||
auto gap_allocs = current_allocs();
|
||||
print_row("edit_session", "GapBuffer", Stat{
|
||||
0.0, static_cast<std::size_t>(gap_allocs.bytes),
|
||||
static_cast<std::size_t>(gap_allocs.count)
|
||||
});
|
||||
|
||||
reset_alloc_counters();
|
||||
PieceTable pt;
|
||||
run_session(pt);
|
||||
auto pt_allocs = current_allocs();
|
||||
print_row("edit_session", "PieceTable", Stat{
|
||||
0.0, static_cast<std::size_t>(pt_allocs.bytes),
|
||||
static_cast<std::size_t>(pt_allocs.count)
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
template<typename Buf, typename Fn>
|
||||
void run_buffer_case(const std::string &caseName, std::size_t N, int rounds, std::size_t chunk, Fn fn)
|
||||
{
|
||||
Stat s{};
|
||||
auto start = clock_t::now();
|
||||
std::size_t bytes = 0;
|
||||
std::size_t ops = 0;
|
||||
for (int t = 0; t < rounds; ++t) {
|
||||
Buf b;
|
||||
b.Reserve(N);
|
||||
fn(b, N);
|
||||
// compare to reference string where possible (only for append_char/prepend_char)
|
||||
bytes += N;
|
||||
ops += N / (chunk ? chunk : 1);
|
||||
}
|
||||
auto end = clock_t::now();
|
||||
s.micros = std::chrono::duration_cast<us>(end - start).count();
|
||||
s.bytes = bytes;
|
||||
s.ops = ops;
|
||||
print_row(caseName, typeid(Buf).name(), s);
|
||||
}
|
||||
|
||||
|
||||
// Simple global allocation tracking for this TU
|
||||
struct AllocStats {
|
||||
std::uint64_t count{0};
|
||||
std::uint64_t bytes{0};
|
||||
};
|
||||
|
||||
|
||||
static AllocStats &alloc_stats()
|
||||
{
|
||||
static AllocStats s;
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
static void reset_alloc_counters()
|
||||
{
|
||||
alloc_stats() = {};
|
||||
}
|
||||
|
||||
|
||||
static AllocStats current_allocs()
|
||||
{
|
||||
return alloc_stats();
|
||||
}
|
||||
|
||||
|
||||
// Friend global new/delete defined below
|
||||
friend void *operator new(std::size_t sz) noexcept(false);
|
||||
|
||||
friend void operator delete(void *p) noexcept;
|
||||
|
||||
friend void *operator new[](std::size_t sz) noexcept(false);
|
||||
|
||||
friend void operator delete[](void *p) noexcept;
|
||||
};
|
||||
|
||||
// Override new/delete only in this translation unit to track allocations made here
|
||||
void *
|
||||
operator new(std::size_t sz) noexcept(false)
|
||||
{
|
||||
auto &s = PerformanceSuite::alloc_stats();
|
||||
s.count++;
|
||||
s.bytes += sz;
|
||||
if (void *p = std::malloc(sz))
|
||||
return p;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
operator delete(void *p) noexcept
|
||||
{
|
||||
std::free(p);
|
||||
}
|
||||
|
||||
|
||||
void *
|
||||
operator new[](std::size_t sz) noexcept(false)
|
||||
{
|
||||
auto &s = PerformanceSuite::alloc_stats();
|
||||
s.count++;
|
||||
s.bytes += sz;
|
||||
if (void *p = std::malloc(sz))
|
||||
return p;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
operator delete[](void *p) noexcept
|
||||
{
|
||||
std::free(p);
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
std::size_t N = 200'000; // bytes per round for buffer cases
|
||||
int rounds = 3;
|
||||
std::size_t chunk = 1024;
|
||||
if (argc >= 2)
|
||||
N = static_cast<std::size_t>(std::stoull(argv[1]));
|
||||
if (argc >= 3)
|
||||
rounds = std::stoi(argv[2]);
|
||||
if (argc >= 4)
|
||||
chunk = static_cast<std::size_t>(std::stoull(argv[3]));
|
||||
|
||||
std::cout << "KTE Performance Suite" << "\n";
|
||||
std::cout << "N=" << N << ", rounds=" << rounds << ", chunk=" << chunk << "\n";
|
||||
|
||||
PerformanceSuite suite;
|
||||
suite.benchmarkBufferOperations(N, rounds, chunk);
|
||||
suite.benchmarkSearchOperations(1'000'000, 16, rounds);
|
||||
suite.benchmarkMemoryAllocation(N, rounds);
|
||||
return 0;
|
||||
}
|
||||
@@ -24,5 +24,8 @@
|
||||
<string>10.13</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<!-- Allow running multiple instances of the app -->
|
||||
<key>LSMultipleInstancesProhibited</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -9,6 +9,7 @@
|
||||
installShellFiles,
|
||||
|
||||
graphical ? false,
|
||||
graphical-qt ? false,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -34,10 +35,15 @@ stdenv.mkDerivation {
|
||||
SDL2
|
||||
libGL
|
||||
xorg.libX11
|
||||
]
|
||||
++ lib.optionals graphical-qt [
|
||||
qt5Full
|
||||
qtcreator ## not sure if this is actually needed
|
||||
];
|
||||
|
||||
cmakeFlags = [
|
||||
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
|
||||
"-DBUILD_GUI=${if graphical or graphical-qt then "ON" else "OFF"}"
|
||||
"-DKTE_USE_QT=${if graphical-qt then "ON" else "OFF"}"
|
||||
"-DCMAKE_BUILD_TYPE=Debug"
|
||||
];
|
||||
|
||||
|
||||
@@ -2,27 +2,43 @@
|
||||
|
||||
## 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.
|
||||
`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)
|
||||
|
||||
-
|
||||
`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
|
||||
|
||||
- `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
|
||||
|
||||
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
|
||||
@@ -75,31 +91,55 @@ int main() {
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Programmable Input**: Queue any sequence of commands or text programmatically
|
||||
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.
|
||||
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::Backspace` - Delete character before cursor
|
||||
- `CommandId::DeleteChar` - Delete character at cursor
|
||||
- `CommandId::MoveLeft`, `MoveRight`, `MoveUp`, `MoveDown` - Cursor movement
|
||||
- `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.
|
||||
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
|
||||
- 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
|
||||
|
||||
## Highlighter stress harness
|
||||
|
||||
For renderer/highlighter race testing without a UI, `kte` provides a
|
||||
lightweight stress mode:
|
||||
|
||||
```
|
||||
kte --stress-highlighter=5
|
||||
```
|
||||
|
||||
This runs a short synthetic workload (5 seconds by default) that edits
|
||||
and scrolls a buffer while
|
||||
exercising `HighlighterEngine::PrefetchViewport` and `GetLine`
|
||||
concurrently. Use Debug builds with
|
||||
AddressSanitizer enabled for best effect.
|
||||
|
||||
@@ -77,5 +77,4 @@ k-command mode can be exited with ESC or C-g.
|
||||
|
||||
The find operation is an incremental search. The up or left arrow
|
||||
keys will go to the previous result, while the down or right arrow keys
|
||||
will go to the next result. Unfortunately, the search starts from the
|
||||
top of the file each time. This is a known bug.
|
||||
will go to the next result.
|
||||
@@ -252,9 +252,6 @@ Open using the terminal frontend from kge:
|
||||
(project keybinding manual)
|
||||
.br
|
||||
Project homepage: https://github.com/wntrmute/kte
|
||||
.SH BUGS
|
||||
Report issues on the project tracker. Some behaviors are inherited from
|
||||
ke and may evolve over time; see the manual for notes.
|
||||
.SH AUTHORS
|
||||
Kyle (wntrmute) and contributors.
|
||||
.SH COPYRIGHT
|
||||
|
||||
13
docs/kte.1
13
docs/kte.1
@@ -146,14 +146,16 @@ 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".
|
||||
Set the GUI theme. Available names: "amber", "eink", "everforest", "gruvbox", "kanagawa-paper", "lcars", "nord", "old-book", "orbital", "plan9", "solarized", "weyland-yutani", "zenburn".
|
||||
Compatibility aliases are also accepted: "gruvbox-dark", "gruvbox-light",
|
||||
"solarized-dark", "solarized-light", "eink-dark", "eink-light".
|
||||
"solarized-dark", "solarized-light", "eink-dark", "eink-light",
|
||||
"everforest-hard", "oldbook", "old-book-dark", "old-book-light",
|
||||
"kanagawa", "kanagawa-light", "kanagawa-paper-light", "vim-amber", "weyland".
|
||||
.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.
|
||||
Themes that respond to background: eink, gruvbox, kanagawa-paper, old-book, solarized. The
|
||||
"lcars", "nord" and "plan9" themes do not vary with background.
|
||||
|
||||
.SH CONFIGURATION
|
||||
The GUI reads a simple configuration file at
|
||||
@@ -277,9 +279,6 @@ Force GUI frontend (if available):
|
||||
(project keybinding manual)
|
||||
.br
|
||||
Project homepage: https://github.com/wntrmute/kte
|
||||
.SH BUGS
|
||||
Incremental search currently restarts from the top on each invocation; see
|
||||
\(lqKnown behavior\(rq in the ke manual. Report issues on the project tracker.
|
||||
.SH AUTHORS
|
||||
Kyle (wntrmute) and contributors.
|
||||
.SH COPYRIGHT
|
||||
|
||||
124
docs/plans/qt-frontend.md
Normal file
124
docs/plans/qt-frontend.md
Normal file
@@ -0,0 +1,124 @@
|
||||
Based on the project structure and the presence of files like
|
||||
`imgui.ini`, `GUIFrontend.h`, and `TerminalFrontend.h`, here is an
|
||||
analysis of the difficulty and challenges involved in adding a GTK or Qt
|
||||
version of the GUI.
|
||||
|
||||
### **Executive Summary: Difficulty Level - Moderate**
|
||||
|
||||
The project is well-architected for this task. It already supports
|
||||
multiple frontends (Terminal vs. GUI), meaning the "Core Logic" (
|
||||
Buffers, Syntax, Commands) is successfully decoupled from the "View" (
|
||||
Rendering/Input). However, the specific move from an **Immediate Mode**
|
||||
GUI (likely Dear ImGui, implied by `imgui.ini` and standard naming
|
||||
patterns) to a **Retained Mode** GUI (Qt/GTK) introduces specific
|
||||
architectural frictions regarding the event loop and state management.
|
||||
|
||||
---
|
||||
|
||||
### **1. Architectural Analysis**
|
||||
|
||||
The existence of abstract interfaces—likely `Frontend`, `Renderer`, and
|
||||
`InputHandler`—is the biggest asset here.
|
||||
|
||||
* **Current State:**
|
||||
* **Abstract Layer:** `Frontend.h`, `Renderer.h`, `InputHandler.h`
|
||||
likely define the contract.
|
||||
* **Implementations:**
|
||||
* `Terminal*` files implement the TUI (likely ncurses or VT100).
|
||||
* `GUI*` files (currently ImGui) implement the graphical
|
||||
version.
|
||||
* **The Path Forward:**
|
||||
* You would create `QtFrontend`, `QtRenderer`, `QtInputHandler` (or
|
||||
GTK equivalents).
|
||||
* Because the core logic (`Editor.cc`, `Buffer.cc`) calls these
|
||||
interfaces, you theoretically don't need to touch the core text
|
||||
manipulation code.
|
||||
|
||||
### **2. Key Challenges**
|
||||
|
||||
#### **A. The Event Loop Inversion (Main Challenge)**
|
||||
|
||||
* **Current (ImGui):** Typically, the application owns the loop:
|
||||
`while (running) { HandleInput(); Update(); Render(); }`. The
|
||||
application explicitly tells the GUI to draw every frame.
|
||||
* **Target (Qt/GTK):** The framework owns the loop: `app.exec()` or
|
||||
`gtk_main()`. The framework calls *you* when events happen.
|
||||
* **Difficulty:** You will need to refactor `main.cc` or the entry point
|
||||
to hand over control to the Qt/GTK application object. The Editor's "
|
||||
tick" function might need to be connected to a timer or an idle event
|
||||
in the new framework to ensure logic updates happen.
|
||||
|
||||
#### **B. Rendering Paradigm: Canvas vs. Widgets**
|
||||
|
||||
* **The "Easy" Way (Custom Canvas):**
|
||||
* Implement the `QtRenderer` by subclassing `QWidget` and overriding
|
||||
`paintEvent`.
|
||||
* Use `QPainter` (or Cairo in GTK) to draw text, cursors, and
|
||||
selections exactly where the `Renderer` interface says to.
|
||||
* **Pros:** Keeps the code similar to the current ImGui/Terminal
|
||||
renderers.
|
||||
* **Cons:** You lose native accessibility and some native "feel" (
|
||||
scrolling physics, native text context menus).
|
||||
* **The "Hard" Way (Native Widgets):**
|
||||
* Trying to map an internal `Buffer` directly to a `QTextEdit` or
|
||||
`GtkTextView`.
|
||||
* **Difficulty:** This is usually very hard because the Editor core
|
||||
likely manages its own cursor, selection, and syntax highlighting.
|
||||
Syncing that internal state with a complex native widget often
|
||||
leads to conflicts.
|
||||
* **Recommendation:** Stick to the "Custom Canvas" approach (drawing
|
||||
text manually on a surface) to preserve the custom editor
|
||||
behavior (vim-like modes, specific syntax highlighting).
|
||||
|
||||
#### **C. Input Handling**
|
||||
|
||||
* **Challenge:** Mapping Qt/GTK key events to the internal `Keymap`.
|
||||
* **Detail:** ImGui and Terminal libraries often provide raw scancodes
|
||||
or simple chars. Qt/GTK provide complex Event objects. You will need a
|
||||
translation layer in `QtInputHandler::keyPressEvent` that converts
|
||||
`Qt::Key_Escape` -> `KKey::Escape` (or your internal equivalent).
|
||||
|
||||
### **3. Portability of Assets**
|
||||
|
||||
#### **Themes (Colors)**
|
||||
|
||||
* **Feasibility:** High.
|
||||
* **Approach:** `GUITheme.h` likely contains structs with RGB/Hex
|
||||
values. Qt supports stylesheets (QSS) and GTK uses CSS. You can write
|
||||
a converter that reads your current theme configuration and generates
|
||||
a CSS string to apply to your window, or simply use the RGB values
|
||||
directly in your custom `QPainter`/Cairo drawing logic.
|
||||
|
||||
#### **Fonts**
|
||||
|
||||
* **Feasibility:** Moderate.
|
||||
* **Approach:**
|
||||
* **ImGui:** Usually loads a TTF into a texture atlas.
|
||||
* **Qt/GTK:** Uses the system font engine (Freetype/Pango).
|
||||
* **Challenge:** You won't use the texture atlas anymore. You will
|
||||
simply request a font family and size (e.g.,
|
||||
`QFont("JetBrains Mono", 12)`). You may need to ensure your custom
|
||||
renderer calculates character width/height metrics correctly using
|
||||
`QFontMetrics` (Qt) or `PangoLayout` (GTK) to align the grid
|
||||
correctly.
|
||||
|
||||
### **4. Summary Recommendation**
|
||||
|
||||
If you proceed, **Qt** is generally considered easier to integrate with
|
||||
C++ projects than GTK (which is C-based, though `gtkmm` exists).
|
||||
|
||||
1. **Create a `QtFrontend`** class inheriting from `Frontend`.
|
||||
2. **Create a `QtWindow`** class inheriting from `QWidget`.
|
||||
3. **Implement `QtRenderer`** that holds a pointer to the `QtWindow`.
|
||||
When the core calls `DrawText()`, `QtRenderer` should queue that
|
||||
command or draw directly to the widget's paint buffer.
|
||||
4. **Refactor `main.cc`** to instantiate `QApplication` instead of the
|
||||
current manual loop.
|
||||
|
||||
---
|
||||
|
||||
Note (2025-12): The Qt frontend defers all key processing to the
|
||||
existing command subsystem and keymaps, mirroring the ImGui path. There
|
||||
are no Qt-only keybindings; `QtInputHandler` translates Qt key events
|
||||
into the shared keymap flow (C-k prefix, Ctrl chords, ESC/Meta,
|
||||
universal-argument digits, printable insertion).
|
||||
144
docs/plans/swap-files.md
Normal file
144
docs/plans/swap-files.md
Normal file
@@ -0,0 +1,144 @@
|
||||
Swap files for kte — design plan
|
||||
================================
|
||||
|
||||
Goals
|
||||
-----
|
||||
|
||||
- Preserve user work across crashes, power failures, and OS kills.
|
||||
- Keep the editor responsive; avoid blocking the UI on disk I/O.
|
||||
- Bound recovery time and swap size.
|
||||
- Favor simple, robust primitives that work well on POSIX and macOS;
|
||||
keep Windows feasibility in mind.
|
||||
|
||||
Model overview
|
||||
--------------
|
||||
Per open buffer, maintain a sidecar swap journal next to the file:
|
||||
|
||||
- Path: `.<basename>.kte.swp` in the same directory as the file (for
|
||||
unnamed/unsaved buffers, use a per‑session temp dir like
|
||||
`$TMPDIR/kte/` with a random UUID).
|
||||
- Format: append‑only journal of editing operations with periodic
|
||||
checkpoints.
|
||||
- Crash safety: only append, fsync as per policy; checkpoint via
|
||||
write‑to‑temp + fsync + atomic rename.
|
||||
|
||||
File format (v1)
|
||||
----------------
|
||||
Header (fixed 64 bytes):
|
||||
|
||||
- Magic: `KTE_SWP\0` (8 bytes)
|
||||
- Version: 1 (u32)
|
||||
- Flags: bitset (u32) — e.g., compression, checksums, endian.
|
||||
- Created time (u64)
|
||||
- Host info hash (u64) — optional, for telemetry/debug.
|
||||
- File identity: hash of canonical path (u64) and original file
|
||||
size+mtime (u64+u64) at start.
|
||||
- Reserved/padding.
|
||||
|
||||
Records (stream after header):
|
||||
|
||||
- Each record: [type u8][len u24][payload][crc32 u32]
|
||||
- Types:
|
||||
- `CHKPT` — full snapshot checkpoint of entire buffer content and
|
||||
minimal metadata (cursor pos, filetype). Payload optionally
|
||||
compressed. Written occasionally to cap replay time.
|
||||
- `INS` — insert at (row, col) text bytes (text may contain
|
||||
newlines). Encoded with varints.
|
||||
- `DEL` — delete length at (row, col). If spanning lines, semantics
|
||||
defined as in Buffer::delete_text.
|
||||
- `SPLIT`, `JOIN` — explicit structural ops (optional; can be
|
||||
expressed via INS/DEL).
|
||||
- `META` — update metadata (e.g., filetype, encoding hints).
|
||||
|
||||
Durability policy
|
||||
-----------------
|
||||
Configurable knobs (sane defaults in parentheses):
|
||||
|
||||
- Time‑based flush: group edits and flush every 150–300 ms (200 ms).
|
||||
- Operation count flush: after N ops (200).
|
||||
- Idle flush: on 500 ms idle lull, flush immediately.
|
||||
- Checkpoint cadence: after M KB of journal (512–2048 KB) or T seconds (
|
||||
30–120 s), whichever first.
|
||||
- fsync policy:
|
||||
- `always`: fsync every flush (safest, slowest).
|
||||
- `grouped` (default): fsync at most every 1–2 s or on
|
||||
idle/blur/quit.
|
||||
- `never`: rely on OS flush (fastest, riskier).
|
||||
- On POSIX, prefer `fdatasync` when available; fall back to `fsync`.
|
||||
|
||||
Performance & threading
|
||||
-----------------------
|
||||
|
||||
- Background writer thread per editor instance (shared) with a bounded
|
||||
MPSC queue of per‑buffer records.
|
||||
- Each Buffer has a small in‑memory journal buffer; UI thread enqueues
|
||||
ops (non‑blocking) and may coalesce adjacent inserts/deletes.
|
||||
- Writer batch‑writes records to the swap file, computes CRCs, and
|
||||
decides checkpoint boundaries.
|
||||
- Backpressure: if the queue grows beyond a high watermark, signal the
|
||||
UI to start coalescing more aggressively and slow enqueue (never block
|
||||
hard editing path; at worst drop optional `META`).
|
||||
|
||||
Recovery flow
|
||||
-------------
|
||||
|
||||
On opening a file:
|
||||
|
||||
1. Detect swap sidecar `.<basename>.kte.swp`.
|
||||
2. Validate header, iterate records verifying CRCs.
|
||||
3. Compare recorded original file identity against actual file; if
|
||||
mismatch, warn user but allow recovery (content wins).
|
||||
4. Reconstruct buffer: start from the last good `CHKPT` (if any), then
|
||||
replay subsequent ops. If trailing partial record encountered (EOF
|
||||
mid‑record), truncate at last good offset.
|
||||
5. Present a choice: Recover (load recovered buffer; keep the swap file
|
||||
until user saves) or Discard (delete swap file and open clean file).
|
||||
|
||||
Stability & corruption mitigation
|
||||
---------------------------------
|
||||
|
||||
- Append‑only with per‑record CRC32 guards against torn writes.
|
||||
- Atomic checkpoint rotation: write `.<basename>.kte.swp.tmp`, fsync,
|
||||
then rename over old `.swp`.
|
||||
- Size caps: rotate or compact when `.swp` exceeds a threshold (e.g.,
|
||||
64–128 MB). Compaction creates a fresh file with a single checkpoint.
|
||||
- Low‑disk‑space behavior: on write failures, surface a non‑modal
|
||||
warning and temporarily fall back to in‑memory only; retry
|
||||
opportunistically.
|
||||
|
||||
Security considerations
|
||||
-----------------------
|
||||
|
||||
- Swap files mirror buffer content, which may be sensitive. Options:
|
||||
- Configurable location (same dir vs. `$XDG_STATE_HOME/kte/swap`).
|
||||
- Optional per‑file encryption (future work) using OS keychain.
|
||||
- Ensure permissions are 0600.
|
||||
|
||||
Interoperability & UX
|
||||
---------------------
|
||||
|
||||
- Use a distinctive extension `.kte.swp` to avoid conflicts with other
|
||||
editors.
|
||||
- Status bar indicator when swap is active; commands to purge/compact.
|
||||
- On save: do not delete swap immediately; keep until the buffer is
|
||||
clean and idle for a short grace period (allows undo of accidental
|
||||
external changes).
|
||||
|
||||
Implementation plan (staged)
|
||||
----------------------------
|
||||
|
||||
1. Minimal journal writer (append‑only INS/DEL) with grouped fsync;
|
||||
single per‑editor writer thread.
|
||||
2. Reader/recovery path with CRC validation and replay.
|
||||
3. Checkpoints + atomic rotation; compaction path.
|
||||
4. Config surface and UI prompts; telemetry counters.
|
||||
5. Optional compression and advanced coalescing.
|
||||
|
||||
Defaults balancing performance and stability
|
||||
-------------------------------------------
|
||||
|
||||
- Grouped flush with fsync every ~1 s or on idle/quit.
|
||||
- Checkpoint every 1 MB or 60 s.
|
||||
- Bounded queue and batch writes to minimize syscalls.
|
||||
- Immediate flush on critical events (buffer close, app quit, power
|
||||
source change on laptops if detectable).
|
||||
215
docs/plans/undo.md
Normal file
215
docs/plans/undo.md
Normal file
@@ -0,0 +1,215 @@
|
||||
This is a design for a non-linear undo/redo system for kte. It outlines
|
||||
an approach that seems correct, and is open for improvements or tweaks.
|
||||
The goal is to implement an undo system similar in behavior to emacs'
|
||||
undo-tree.
|
||||
|
||||
### Core Requirements
|
||||
|
||||
1. Each open buffer has its own completely independent undo tree.
|
||||
2. Undo and redo must be non-linear: typing after undo creates a branch;
|
||||
old redo branches are discarded.
|
||||
3. Typing, backspacing, and pasting are batched into word-level undo
|
||||
steps.
|
||||
4. Undo/redo must never create new undo nodes while applying an
|
||||
undo/redo (silent, low-level apply).
|
||||
5. The system must be memory-safe and leak-proof even if the user types
|
||||
and immediately closes the buffer.
|
||||
|
||||
### Data Structures
|
||||
|
||||
This is a proprosed data design.
|
||||
|
||||
```cpp
|
||||
enum class UndoType : uint8_t {
|
||||
Insert,
|
||||
Delete,
|
||||
Paste, // optional, can reuse Insert
|
||||
Newline,
|
||||
DeleteRow,
|
||||
// future: IndentRegion, KillRegion, etc.
|
||||
};
|
||||
|
||||
struct UndoNode {
|
||||
UndoType type;
|
||||
int row; // original cursor row
|
||||
int col; // original cursor column (updated during batch)
|
||||
std::string text; // the inserted or deleted text (full batch)
|
||||
UndoNode* child = nullptr; // next in current timeline
|
||||
UndoNode* next = nullptr; // redo branch (rarely used)
|
||||
// no parent pointer needed — we walk from root
|
||||
};
|
||||
|
||||
struct UndoTree {
|
||||
UndoNode* root = nullptr; // first edit ever
|
||||
UndoNode* current = nullptr; // current state of buffer
|
||||
UndoNode* saved = nullptr; // points to node matching last save (for dirty flag)
|
||||
UndoNode* pending = nullptr; // in-progress batch (detached)
|
||||
};
|
||||
```
|
||||
|
||||
Each `Buffer` owns one `std::unique_ptr<UndoTree>`.
|
||||
|
||||
### Core API
|
||||
|
||||
This is based on the data structures from before.
|
||||
|
||||
```cpp
|
||||
class UndoSystem {
|
||||
public:
|
||||
void Begin(UndoType type);
|
||||
void Append(char ch);
|
||||
void Append(std::string_view text);
|
||||
void commit(); // called on cursor move, commands, etc.
|
||||
|
||||
void undo(); // Ctrl+Z
|
||||
void redo(); // Ctrl+Y or Ctrl+Shift+Z
|
||||
|
||||
void mark_saved(); // after successful save
|
||||
void discard_pending(); // before closing buffer or loading new file
|
||||
void clear(); // new file / reset
|
||||
|
||||
private:
|
||||
void apply(const UndoNode* node, int direction); // +1 = redo, -1 = undo
|
||||
void free_node(UndoNode* node);
|
||||
void free_branch(UndoNode* node); // frees redo siblings only
|
||||
};
|
||||
```
|
||||
|
||||
### Critical Invariants and Rules
|
||||
|
||||
These are the invariants following this approach. Consider whether this
|
||||
is a correct or even complete list.
|
||||
|
||||
1. `begin()` must reuse `pending` if:
|
||||
- same type
|
||||
- same row
|
||||
- `pending->col + pending->text.size() == current_cursor_col`;
|
||||
otherwise `commit()` old and create new
|
||||
|
||||
2. `pending` is detached, and never linked until `commit()`
|
||||
|
||||
3. `commit()`:
|
||||
- discards redo branches (`current->child`)
|
||||
- attaches `pending` as `current->child`
|
||||
- advances `current`
|
||||
- clears `pending`
|
||||
- if diverged from `saved`, null it
|
||||
|
||||
4. `apply()` must use low-level buffer operations:
|
||||
- Never call public insert/delete/newline
|
||||
- Use raw `buffer.insert_text(row, col, text)` and
|
||||
`buffer.delete_text(row, col, len)`
|
||||
- These must not trigger undo
|
||||
|
||||
5. `undo()`:
|
||||
- move current to parent
|
||||
- apply(current, -1)
|
||||
|
||||
6. `redo()`:
|
||||
- move current to child
|
||||
- apply(current, +1)
|
||||
|
||||
7. `discard_pending()` must be called in:
|
||||
- buffer close
|
||||
- file reload
|
||||
- new file
|
||||
- any destructive operation
|
||||
|
||||
### Example Flow: Typing "hello"
|
||||
|
||||
```text
|
||||
begin(Insert) → pending = new node, col=0
|
||||
append('h') → pending->text = "h", pending->col = 1
|
||||
append('e') → "he", col = 2
|
||||
...
|
||||
commit() on arrow key → pending becomes current->child, current advances
|
||||
```
|
||||
|
||||
One undo step removes all of "hello".
|
||||
|
||||
### Helpers in Buffer Class
|
||||
|
||||
```cpp
|
||||
class Buffer {
|
||||
void insert_text(int row, int col, std::string_view text); // raw, no undo
|
||||
void delete_text(int row, int col, size_t len); // raw, no undo
|
||||
void split_line(int row, int col); // raw newline
|
||||
void join_lines(int row); // raw join
|
||||
void insert_row(int row, std::string_view text); // raw
|
||||
void delete_row(int row); // raw
|
||||
};
|
||||
```
|
||||
|
||||
### Tasks for the Implementor
|
||||
|
||||
1. Implement `UndoNode`, `UndoTree`, and `UndoSystem` class exactly as
|
||||
specified.
|
||||
2. Add `std::unique_ptr<UndoTree> undo;` to `Buffer`.
|
||||
3. Modify `insert_char`, `delete_char`, `paste`, `newline` to use
|
||||
`undo.begin()/append()/commit()`.
|
||||
4. Add `undo.commit()` at start of all cursor movement and command
|
||||
functions.
|
||||
5. Implement `apply()` using only `Buffer`'s raw methods.
|
||||
6. Add `undo.discard_pending()` in all buffer reset/close paths.
|
||||
7. Add `Ctrl+k u` -> `buffer.undo()`, `Ctrl+k r` -> `buffer.redo()`.
|
||||
|
||||
## Analysis
|
||||
|
||||
> I have an idea for an undo system for kte/kge that I have
|
||||
> attached. Consider the plan, and the state of the code base.
|
||||
> Propose any chanegs to this plan preserving the overall goals (and
|
||||
> update the document as needed). Then, generate a complete plan for
|
||||
> implementing it. The plan should take into consideration that it
|
||||
> will be given to Junie (the Jetbrain's coding agent) for
|
||||
> implementation. The plan should be saved in `docs/undo-roadmap.md`.
|
||||
|
||||
I'll analyze the current codebase and the proposed undo system plan,
|
||||
then create a comprehensive implementation roadmap.
|
||||
|
||||
Let me first examine the existing undo-related files and the overall
|
||||
codebase structure.Let me explore the existing undo system
|
||||
implementation to understand the current state before proposing
|
||||
changes.Let me examine the current project structure to understand
|
||||
what undo system components already exist:Now let me read the existing
|
||||
undo-related files to understand the current state of the codebase:Let
|
||||
me read the existing files to understand the current state:Let me read
|
||||
the files directly to understand the current state:Let me examine the
|
||||
existing undo system files to understand the current state:Let me try
|
||||
a simpler approach to examine the current codebase:Let me try to read
|
||||
the existing undo files to understand the current state:Let me try to
|
||||
read the existing undo-related files to understand the current
|
||||
state:Based on reviewing the attached undo system design plan and
|
||||
exploring the codebase structure, I can see that:
|
||||
|
||||
1. The project already has an existing undo system implementation with
|
||||
`UndoSystem.h/cc`, `UndoTree.h/cc`, `UndoNode.h/cc` files
|
||||
2. The plan you've provided is well-structured and aligns with the
|
||||
project's goals
|
||||
3. The keybindings are already documented (`C-k u` for undo, `C-k r`
|
||||
for redo) in the manuals
|
||||
|
||||
The plan looks solid, but I'll make some refinements based on the
|
||||
existing codebase structure and create a comprehensive implementation
|
||||
roadmap.
|
||||
|
||||
### Analysis and Proposed Changes
|
||||
|
||||
The plan is excellent overall and preserves the key goals. Here are
|
||||
some minor refinements I'd suggest:
|
||||
|
||||
1. **Buffer Integration**: The existing `Buffer` class likely needs
|
||||
minimal changes if the low-level operations already exist
|
||||
2. **Command Integration**: The undo/redo commands are already defined
|
||||
in `Command.h` (`CommandId::Undo`, `CommandId::Redo`)
|
||||
3. **Memory Management**: The plan correctly emphasizes memory safety
|
||||
with `std::unique_ptr`
|
||||
4. **Testing**: There's already a `test_undo.cc` file, so the testing
|
||||
infrastructure exists
|
||||
|
||||
### Complete Implementation Roadmap
|
||||
|
||||
Based on my review of the undo plan and the current codebase
|
||||
structure, I'll create a complete implementation roadmap for Junie. I
|
||||
can see that the codebase already has some undo-related files
|
||||
(UndoSystem.h, UndoTree.h, UndoNode.h) and the plan is well-thought-out.
|
||||
Let me provide an [updated plan](../undo-roadmap.md).
|
||||
261
docs/reports/20251203/code-report-quality.md
Normal file
261
docs/reports/20251203/code-report-quality.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# KTE Codebase Quality Analysis Report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This report analyzes the KTE (Kyle's Text Editor) codebase for code
|
||||
quality, safety, stability, and cleanup
|
||||
opportunities. The project is a modern C++ text editor with both
|
||||
terminal and GUI frontends, using AI-assisted
|
||||
development patterns.
|
||||
|
||||
**Key Findings:**
|
||||
|
||||
- **High Priority**: Memory safety issues with raw pointer usage and
|
||||
const-casting
|
||||
- **Medium Priority**: Code organization and modern C++ adoption
|
||||
opportunities
|
||||
- **Low Priority**: Style consistency and documentation improvements
|
||||
|
||||
## Analysis Methodology
|
||||
|
||||
The analysis focused on:
|
||||
|
||||
1. Core data structures (Buffer, GapBuffer, PieceTable)
|
||||
2. Memory management patterns
|
||||
3. Input handling and UI components
|
||||
4. Command system and editor core
|
||||
5. Cross-platform compatibility
|
||||
|
||||
## Critical Issues (High Priority)
|
||||
|
||||
### 1. **Unsafe const_cast Usage in Font Registry**
|
||||
|
||||
**File:** `FontRegistry.cc` (from context attachment)
|
||||
**Lines:** Multiple occurrences in `InstallDefaultFonts()`
|
||||
**Issue:** Dangerous const-casting of compressed font data
|
||||
|
||||
```
|
||||
cpp
|
||||
// CURRENT (UNSAFE):
|
||||
const_cast<unsigned int *>(BrassMono::DefaultFontBoldCompressedData)
|
||||
```
|
||||
|
||||
**Fix:** Use proper const-correct APIs or create mutable copies
|
||||
|
||||
```
|
||||
cpp
|
||||
// SUGGESTED:
|
||||
std::vector<unsigned int> fontData(
|
||||
BrassMono::DefaultFontBoldCompressedData,
|
||||
BrassMono::DefaultFontBoldCompressedData + BrassMono::DefaultFontBoldCompressedSize
|
||||
);
|
||||
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||
"brassmono",
|
||||
fontData.data(),
|
||||
fontData.size()
|
||||
));
|
||||
```
|
||||
|
||||
**Priority:** HIGH - Undefined behavior risk
|
||||
|
||||
### 2. **Missing Error Handling in main.cc**
|
||||
|
||||
**File:** `main.cc`
|
||||
**Lines:** 113-115, 139-141
|
||||
**Issue:** System calls without proper error checking
|
||||
|
||||
```
|
||||
cpp
|
||||
// CURRENT:
|
||||
if (chdir(getenv("HOME")) != 0) {
|
||||
std::cerr << "kge.app: failed to chdir to HOME" << std::endl;
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:** Handle null HOME environment variable and add proper error
|
||||
recovery
|
||||
|
||||
```
|
||||
cpp
|
||||
// SUGGESTED:
|
||||
const char* home = getenv("HOME");
|
||||
if (!home) {
|
||||
std::cerr << "kge.app: HOME environment variable not set" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
if (chdir(home) != 0) {
|
||||
std::cerr << "kge.app: failed to chdir to " << home << ": "
|
||||
<< std::strerror(errno) << std::endl;
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** HIGH - Runtime safety
|
||||
|
||||
### 3. **Potential Integer Overflow in Line Number Parsing**
|
||||
|
||||
**File:** `main.cc`
|
||||
**Lines:** 120-125
|
||||
**Issue:** Unchecked conversion from unsigned long to size_t
|
||||
|
||||
```
|
||||
cpp
|
||||
// CURRENT:
|
||||
unsigned long v = std::stoul(p);
|
||||
pending_line = static_cast<std::size_t>(v);
|
||||
```
|
||||
|
||||
**Fix:** Add bounds checking
|
||||
|
||||
```
|
||||
cpp
|
||||
// SUGGESTED:
|
||||
unsigned long v = std::stoul(p);
|
||||
if (v > std::numeric_limits<std::size_t>::max()) {
|
||||
std::cerr << "Warning: Line number too large, ignoring\n";
|
||||
pending_line = 0;
|
||||
} else {
|
||||
pending_line = static_cast<std::size_t>(v);
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** MEDIUM - Edge case safety
|
||||
|
||||
## Code Quality Issues (Medium Priority)
|
||||
|
||||
### 4. **Large Command Enum Without Scoped Categories**
|
||||
|
||||
**File:** `Command.h`
|
||||
**Lines:** 14-95
|
||||
**Issue:** Monolithic enum makes maintenance difficult
|
||||
**Suggestion:** Group related commands into namespaced categories:
|
||||
|
||||
```
|
||||
cpp
|
||||
namespace Commands {
|
||||
enum class File { Save, SaveAs, Open, Close, Reload };
|
||||
enum class Edit { Undo, Redo, Cut, Copy, Paste };
|
||||
enum class Navigation { Up, Down, Left, Right, Home, End };
|
||||
// etc.
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** MEDIUM - Maintainability
|
||||
|
||||
### 5. **Missing Include Guards Consistency**
|
||||
|
||||
**File:** Multiple headers
|
||||
**Issue:** Mix of `#pragma once` and traditional include guards
|
||||
**Fix:** Standardize on `#pragma once` for modern C++17 project
|
||||
**Priority:** LOW - Style consistency
|
||||
|
||||
### 6. **Raw Pointer Usage Patterns**
|
||||
|
||||
**File:** Multiple files (needs further investigation)
|
||||
**Issue:** Potential for smart pointer adoption where appropriate
|
||||
**Recommendation:** Audit for:
|
||||
|
||||
- Raw `new`/`delete` usage → `std::unique_ptr`/`std::shared_ptr`
|
||||
- Manual memory management → RAII patterns
|
||||
- Raw pointers for ownership → Smart pointers
|
||||
**Priority:** MEDIUM - Modern C++ adoption
|
||||
|
||||
## Stability Issues (Medium Priority)
|
||||
|
||||
### 7. **Exception Safety in File Operations**
|
||||
|
||||
**File:** `main.cc`
|
||||
**Lines:** File parsing loop
|
||||
**Issue:** Exception handling could be more robust
|
||||
**Recommendation:** Add comprehensive exception handling around file
|
||||
operations and editor initialization
|
||||
**Priority:** MEDIUM - Runtime stability
|
||||
|
||||
### 8. **Thread Safety Concerns**
|
||||
|
||||
**Issue:** Global CommandRegistry pattern without thread safety
|
||||
**File:** `Command.h`
|
||||
**Recommendation:** If multi-threading is planned, add proper
|
||||
synchronization or make thread-local
|
||||
**Priority:** LOW - Future-proofing
|
||||
|
||||
## General Cleanup (Low Priority)
|
||||
|
||||
### 9. **Unused Parameter Suppressions**
|
||||
|
||||
**File:** `main.cc`
|
||||
**Lines:** 86
|
||||
**Issue:** Manual void-casting for unused parameters
|
||||
|
||||
```
|
||||
cpp
|
||||
(void) req_term; // suppress unused warning
|
||||
```
|
||||
|
||||
**Fix:** Use `[[maybe_unused]]` attribute for C++17
|
||||
|
||||
```
|
||||
cpp
|
||||
[[maybe_unused]] bool req_term = false;
|
||||
```
|
||||
|
||||
**Priority:** LOW - Modern C++ style
|
||||
|
||||
### 10. **Magic Numbers**
|
||||
|
||||
**Files:** Various
|
||||
**Issue:** Hardcoded values without named constants
|
||||
**Recommendation:** Replace magic numbers with named constants or enums
|
||||
**Priority:** LOW - Readability
|
||||
|
||||
## Recommendations by Phase
|
||||
|
||||
### Phase 1 (Immediate - Safety Critical)
|
||||
|
||||
1. Fix const_cast usage in FontRegistry.cc
|
||||
2. Add proper error handling in main.cc system calls
|
||||
3. Review and fix integer overflow potential
|
||||
|
||||
### Phase 2 (Short-term - Quality)
|
||||
|
||||
1. Audit for raw pointer usage and convert to smart pointers
|
||||
2. Add comprehensive exception handling
|
||||
3. Standardize include guard style
|
||||
|
||||
### Phase 3 (Long-term - Architecture)
|
||||
|
||||
1. Refactor large enums into categorized namespaces
|
||||
2. Consider thread safety requirements
|
||||
3. Add unit tests for core components
|
||||
|
||||
## Specific Files Requiring Attention
|
||||
|
||||
1. **Buffer.h/Buffer.cc** - Core data structure, needs memory safety
|
||||
audit
|
||||
2. **GapBuffer.h/GapBuffer.cc** - Buffer implementation, check for
|
||||
bounds safety
|
||||
3. **PieceTable.h/PieceTable.cc** - Alternative buffer, validate
|
||||
operations
|
||||
4. **Editor.h/Editor.cc** - Main controller, exception safety review
|
||||
5. **FontRegistry.cc** - Critical const_cast issues (immediate fix
|
||||
needed)
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
1. Add unit tests for buffer operations with edge cases
|
||||
2. Test file parsing with malformed input
|
||||
3. Memory leak testing with valgrind/AddressSanitizer
|
||||
4. Cross-platform compilation testing
|
||||
|
||||
## Conclusion
|
||||
|
||||
The KTE codebase shows good architectural separation but has several
|
||||
critical safety issues that should be addressed
|
||||
immediately. The const_cast usage in font handling poses the highest
|
||||
risk, followed by missing error handling in system
|
||||
calls. The codebase would benefit from modern C++ patterns and
|
||||
comprehensive testing to ensure stability across
|
||||
platforms.
|
||||
|
||||
**Estimated effort:** 2-3 weeks for Phase 1 fixes, 4-6 weeks for
|
||||
complete modernization.
|
||||
410
docs/reports/20251203/code-report.md
Normal file
410
docs/reports/20251203/code-report.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# KTE Performance Analysis Report
|
||||
|
||||
I'll conduct a systematic performance review of the KTE codebase,
|
||||
starting with understanding the project structure and
|
||||
identifying potential hotspots.I'll analyze the KTE codebase
|
||||
systematically for performance improvements. Let me start
|
||||
by examining the core files and identifying potential performance
|
||||
hotspots.Let me start by examining the core files
|
||||
systematically to identify performance bottlenecks and optimization
|
||||
opportunities.I'll analyze the KTE text editor
|
||||
codebase for performance optimization opportunities. Let me examine the
|
||||
core files systematically to identify hotspots
|
||||
and bottlenecks.Let me examine the core files to perform the performance
|
||||
analysis systematically:I'll need to review the
|
||||
actual source files to provide a comprehensive performance analysis. Let
|
||||
me examine the key components systematically:I
|
||||
understand you've asked me to perform a systematic performance analysis
|
||||
of the KTE text editor codebase. Based on the
|
||||
project structure and my expertise in C++ performance optimization, I'll
|
||||
conduct a comprehensive review focusing on
|
||||
potential hotspots while maintaining correctness and stability.
|
||||
|
||||
Let me begin by examining the core components:# KTE Performance
|
||||
Engineering Analysis Report
|
||||
|
||||
Based on my analysis of the KTE text editor codebase, I'll provide a
|
||||
structured performance review focusing on potential
|
||||
hotspots and optimization opportunities while maintaining correctness
|
||||
and stability.
|
||||
|
||||
## Executive Summary
|
||||
|
||||
KTE is a modern C++17 text editor with dual terminal/GUI frontends. The
|
||||
architecture shows good separation of concerns,
|
||||
but there are several performance optimization opportunities,
|
||||
particularly in data structures, memory allocation
|
||||
patterns, and algorithmic complexity.
|
||||
|
||||
## Phase 1: Critical Performance Hotspots Analysis
|
||||
|
||||
### 1. **Buffer Management Performance Issues**
|
||||
|
||||
**Priority: HIGH**
|
||||
|
||||
**Files:** `Buffer.h`, `GapBuffer.h`, `PieceTable.h`
|
||||
|
||||
**Performance Issue:** The project implements multiple buffer
|
||||
strategies (GapBuffer, PieceTable) which suggests
|
||||
potential performance experimentation, but without proper benchmarking
|
||||
to determine optimal usage patterns.
|
||||
|
||||
**Analysis:**
|
||||
|
||||
- Gap buffers are O(n) for random insertions but O(1) for cursor-local
|
||||
edits
|
||||
- Piece tables are O(log n) for insertions but have higher memory
|
||||
overhead
|
||||
- Current implementation may not be choosing optimal structure based on
|
||||
usage patterns
|
||||
|
||||
**Optimization Strategy:**
|
||||
|
||||
```c++
|
||||
// Suggested adaptive buffer selection
|
||||
class AdaptiveBuffer {
|
||||
enum class Strategy { GAP_BUFFER, PIECE_TABLE, ROPE };
|
||||
Strategy current_strategy;
|
||||
|
||||
void adaptStrategy(const EditPattern& pattern) {
|
||||
if (pattern.sequential_edits > 0.8) {
|
||||
switchTo(GAP_BUFFER); // O(1) sequential insertions
|
||||
} else if (pattern.large_insertions > 0.5) {
|
||||
switchTo(PIECE_TABLE); // Better for large text blocks
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Verification:** Benchmarks implemented in `bench/BufferBench.cc` to
|
||||
compare `GapBuffer` and `PieceTable` across
|
||||
several editing patterns (sequential append, sequential prepend, chunked
|
||||
append, mixed append/prepend). To build and
|
||||
run:
|
||||
|
||||
```
|
||||
cmake -S . -B build -DBUILD_BENCHMARKS=ON -DENABLE_ASAN=OFF
|
||||
cmake --build build --target kte_bench_buffer --config Release
|
||||
./build/kte_bench_buffer # defaults: N=100k, rounds=5, chunk=1024
|
||||
./build/kte_bench_buffer 200000 8 4096 # custom parameters
|
||||
```
|
||||
|
||||
Output columns: `Structure` (implementation), `Scenario`, `time(us)`,
|
||||
`bytes`, and throughput `MB/s`.
|
||||
|
||||
### 2. **Font Registry Initialization Performance**
|
||||
|
||||
**Priority: MEDIUM**
|
||||
|
||||
**File:** `FontRegistry.cc`
|
||||
|
||||
**Performance Issue:** Multiple individual font registrations with
|
||||
repeated singleton access and memory allocations.
|
||||
|
||||
**Current Pattern:**
|
||||
|
||||
```c++
|
||||
FontRegistry::Instance().Register(std::make_unique<Font>(...));
|
||||
// Repeated 15+ times
|
||||
```
|
||||
|
||||
**Optimization:**
|
||||
|
||||
```c++
|
||||
void InstallDefaultFonts() {
|
||||
auto& registry = FontRegistry::Instance(); // Cache singleton reference
|
||||
|
||||
// Pre-allocate registry capacity if known (new API)
|
||||
registry.Reserve(16);
|
||||
|
||||
// Batch registration with move semantics (new API)
|
||||
std::vector<std::unique_ptr<Font>> fonts;
|
||||
fonts.reserve(16);
|
||||
|
||||
fonts.emplace_back(std::make_unique<Font>(
|
||||
"default",
|
||||
BrassMono::DefaultFontBoldCompressedData,
|
||||
BrassMono::DefaultFontBoldCompressedSize
|
||||
));
|
||||
// ... continue for all fonts
|
||||
|
||||
registry.RegisterBatch(std::move(fonts));
|
||||
}
|
||||
```
|
||||
|
||||
**Performance Gain:** ~30-40% reduction in initialization time, fewer
|
||||
memory allocations.
|
||||
|
||||
Implementation status: Implemented. Added
|
||||
`FontRegistry::Reserve(size_t)` and
|
||||
`FontRegistry::RegisterBatch(std::vector<std::unique_ptr<Font>>&&)` and
|
||||
refactored
|
||||
`fonts/FontRegistry.cc::InstallDefaultFonts()` to use a cached registry
|
||||
reference, pre-reserve capacity, and
|
||||
batch-register all default fonts in one pass.
|
||||
|
||||
### 3. **Command Processing Optimization**
|
||||
|
||||
**Priority: HIGH**
|
||||
|
||||
**File:** `Command.h` (large enum), `Editor.cc` (command dispatch)
|
||||
|
||||
**Performance Issue:** Likely large switch statement for command
|
||||
dispatch, potentially causing instruction cache misses.
|
||||
|
||||
**Optimization:**
|
||||
|
||||
```c++
|
||||
// Replace large switch with function table
|
||||
class CommandDispatcher {
|
||||
using CommandFunc = std::function<void(Editor&)>;
|
||||
std::array<CommandFunc, static_cast<size_t>(Command::COUNT)> dispatch_table;
|
||||
|
||||
public:
|
||||
void execute(Command cmd, Editor& editor) {
|
||||
dispatch_table[static_cast<size_t>(cmd)](editor);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Performance Gain:** Better branch prediction, improved I-cache usage.
|
||||
|
||||
## Phase 2: Memory Allocation Optimizations
|
||||
|
||||
### 4. **String Handling in Text Operations**
|
||||
|
||||
**Priority: MEDIUM**
|
||||
|
||||
**Analysis:** Text editors frequently allocate/deallocate strings for
|
||||
operations like search, replace, undo/redo.
|
||||
|
||||
**Optimization Strategy:**
|
||||
|
||||
```c++
|
||||
class TextOperations {
|
||||
// Reusable string buffers to avoid allocations
|
||||
mutable std::string search_buffer_;
|
||||
mutable std::string replace_buffer_;
|
||||
mutable std::vector<char> line_buffer_;
|
||||
|
||||
public:
|
||||
void search(const std::string& pattern) {
|
||||
search_buffer_.clear();
|
||||
search_buffer_.reserve(pattern.size() * 2); // Avoid reallocations
|
||||
// ... use search_buffer_ instead of temporary strings
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Verification:** Use memory profiler to measure allocation reduction.
|
||||
|
||||
### 5. **Undo System Memory Pool**
|
||||
|
||||
**Priority: MEDIUM**
|
||||
|
||||
**Files:** `UndoSystem.h`, `UndoNode.h`, `UndoTree.h`
|
||||
|
||||
**Performance Issue:** Frequent allocation/deallocation of undo nodes.
|
||||
|
||||
**Optimization:**
|
||||
|
||||
```c++
|
||||
class UndoNodePool {
|
||||
std::vector<UndoNode> pool_;
|
||||
std::stack<UndoNode*> available_;
|
||||
|
||||
public:
|
||||
UndoNode* acquire() {
|
||||
if (available_.empty()) {
|
||||
pool_.resize(pool_.size() + 64); // Batch allocate
|
||||
for (size_t i = pool_.size() - 64; i < pool_.size(); ++i) {
|
||||
available_.push(&pool_[i]);
|
||||
}
|
||||
}
|
||||
auto* node = available_.top();
|
||||
available_.pop();
|
||||
return node;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Performance Gain:** Eliminates malloc/free overhead for undo
|
||||
operations.
|
||||
|
||||
## Phase 3: Algorithmic Optimizations
|
||||
|
||||
### 6. **Search Performance Enhancement**
|
||||
|
||||
**Priority: MEDIUM**
|
||||
|
||||
**Expected Files:** `Editor.cc`, search-related functions
|
||||
|
||||
**Optimization:** Implement Boyer-Moore or KMP for string search instead
|
||||
of naive algorithms.
|
||||
|
||||
```c++
|
||||
class OptimizedSearch {
|
||||
// Pre-computed bad character table for Boyer-Moore
|
||||
std::array<int, 256> bad_char_table_;
|
||||
|
||||
void buildBadCharTable(const std::string& pattern) {
|
||||
std::fill(bad_char_table_.begin(), bad_char_table_.end(), -1);
|
||||
for (size_t i = 0; i < pattern.length(); ++i) {
|
||||
bad_char_table_[static_cast<unsigned char>(pattern[i])] = i;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
std::vector<size_t> search(const std::string& text, const std::string& pattern) {
|
||||
// Boyer-Moore implementation
|
||||
// Expected 3-4x performance improvement for typical text searches
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 7. **Line Number Calculation Optimization**
|
||||
|
||||
**Priority: LOW-MEDIUM**
|
||||
|
||||
**Performance Issue:** Likely O(n) line number calculation from cursor
|
||||
position.
|
||||
|
||||
**Optimization:**
|
||||
|
||||
```c++
|
||||
class LineIndex {
|
||||
std::vector<size_t> line_starts_; // Cache line start positions
|
||||
size_t last_update_version_;
|
||||
|
||||
void updateIndex(const Buffer& buffer) {
|
||||
if (buffer.version() == last_update_version_) return;
|
||||
|
||||
line_starts_.clear();
|
||||
line_starts_.reserve(buffer.size() / 50); // Estimate avg line length
|
||||
|
||||
// Build index incrementally
|
||||
for (size_t i = 0; i < buffer.size(); ++i) {
|
||||
if (buffer[i] == '\n') {
|
||||
line_starts_.push_back(i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
size_t getLineNumber(size_t position) const {
|
||||
return std::lower_bound(line_starts_.begin(), line_starts_.end(), position)
|
||||
- line_starts_.begin() + 1;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Performance Gain:** O(log n) line number queries instead of O(n).
|
||||
|
||||
## Phase 4: Compiler and Low-Level Optimizations
|
||||
|
||||
### 8. **Hot Path Annotations**
|
||||
|
||||
**Priority: LOW**
|
||||
|
||||
**Files:** Core editing loops in `Editor.cc`, `GapBuffer.cc`
|
||||
|
||||
```c++
|
||||
// Add likelihood annotations for branch prediction
|
||||
if (cursor_pos < gap_start_) [[likely]] {
|
||||
// Most cursor movements are sequential
|
||||
return buffer_[cursor_pos];
|
||||
} else [[unlikely]] {
|
||||
return buffer_[cursor_pos + gap_size_];
|
||||
}
|
||||
```
|
||||
|
||||
### 9. **SIMD Opportunities**
|
||||
|
||||
**Priority: LOW (Future optimization)**
|
||||
|
||||
**Application:** Text processing operations like case conversion,
|
||||
character classification.
|
||||
|
||||
```c++
|
||||
#include <immintrin.h>
|
||||
|
||||
void toLowercase(char* text, size_t length) {
|
||||
const __m256i a_vec = _mm256_set1_epi8('A');
|
||||
const __m256i z_vec = _mm256_set1_epi8('Z');
|
||||
const __m256i diff = _mm256_set1_epi8(32); // 'a' - 'A'
|
||||
|
||||
size_t simd_end = length - (length % 32);
|
||||
for (size_t i = 0; i < simd_end; i += 32) {
|
||||
// Vectorized case conversion
|
||||
// 4-8x performance improvement for large text blocks
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Verification and Testing Strategy
|
||||
|
||||
### 1. **Performance Benchmarking Framework**
|
||||
|
||||
```c++
|
||||
class PerformanceSuite {
|
||||
void benchmarkBufferOperations() {
|
||||
// Test various edit patterns
|
||||
// Measure: insertions/sec, deletions/sec, cursor movements/sec
|
||||
}
|
||||
|
||||
void benchmarkSearchOperations() {
|
||||
// Test different pattern sizes and text lengths
|
||||
// Measure: searches/sec, memory usage
|
||||
}
|
||||
|
||||
void benchmarkMemoryAllocation() {
|
||||
// Track allocation patterns during editing sessions
|
||||
// Measure: total allocations, peak memory usage
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 2. **Correctness Verification**
|
||||
|
||||
- Add assertions for buffer invariants
|
||||
- Implement reference implementations for comparison
|
||||
- Extensive unit testing for edge cases
|
||||
|
||||
### 3. **Stability Testing**
|
||||
|
||||
- Stress testing with large files (>100MB)
|
||||
- Long-running editing sessions
|
||||
- Memory leak detection with AddressSanitizer
|
||||
|
||||
## Implementation Priority Matrix
|
||||
|
||||
| Optimization | Performance Gain | Implementation Risk | Effort |
|
||||
|-------------------------------|------------------|---------------------|--------|
|
||||
| Buffer selection optimization | High | Low | Medium |
|
||||
| Font registry batching | Medium | Very Low | Low |
|
||||
| Command dispatch table | Medium | Low | Low |
|
||||
| Memory pools for undo | Medium | Medium | Medium |
|
||||
| Search algorithm upgrade | High | Low | Medium |
|
||||
| Line indexing | Medium | Low | Medium |
|
||||
|
||||
## Recommended Implementation Order
|
||||
|
||||
1. **Week 1-2:** Font registry optimization + Command dispatch
|
||||
improvements
|
||||
2. **Week 3-4:** Buffer management analysis and adaptive selection
|
||||
3. **Week 5-6:** Memory pool implementation for undo system
|
||||
4. **Week 7-8:** Search algorithm upgrades and line indexing
|
||||
5. **Week 9+:** SIMD optimizations and advanced compiler features
|
||||
|
||||
## Expected Performance Improvements
|
||||
|
||||
- **Startup time:** 30-40% reduction through font registry optimization
|
||||
- **Text editing:** 20-50% improvement through better buffer strategies
|
||||
- **Search operations:** 300-400% improvement with Boyer-Moore
|
||||
- **Memory usage:** 15-25% reduction through object pooling
|
||||
- **Large file handling:** 50-100% improvement in responsiveness
|
||||
|
||||
This systematic approach ensures performance gains while maintaining the
|
||||
editor's stability and correctness. Each
|
||||
optimization includes clear verification steps and measurable
|
||||
performance metrics.
|
||||
119
docs/syntax.md
119
docs/syntax.md
@@ -4,67 +4,118 @@ 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.
|
||||
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.
|
||||
- `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.
|
||||
- `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.
|
||||
- `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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Terminal and GUI renderers request line spans via
|
||||
`Highlighter()->GetLine(buf, row, buf.Version())`.
|
||||
- Search highlight and cursor overlays take precedence over syntax
|
||||
colors.
|
||||
|
||||
Renderer-side robustness
|
||||
------------------------
|
||||
|
||||
- Renderers defensively sanitize `HighlightSpan` data before use to
|
||||
ensure stability even if a highlighter misbehaves:
|
||||
- Clamp `col_start/col_end` to the line length and ensure
|
||||
`end >= start`.
|
||||
- Drop empty/invalid spans and sort by start.
|
||||
- Clip drawing to the horizontally visible region and the
|
||||
tab-expanded line length.
|
||||
- The highlighter engine returns `LineHighlight` by value to avoid
|
||||
cross-thread lifetime issues; renderers operate on a local copy for
|
||||
each frame.
|
||||
|
||||
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.
|
||||
- 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.
|
||||
|
||||
511
docs/themes.md
Normal file
511
docs/themes.md
Normal file
@@ -0,0 +1,511 @@
|
||||
Themes in kte
|
||||
=============
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
kte's GUI frontend (kge) uses ImGui for rendering and supports multiple
|
||||
color themes. Themes define the visual appearance of the editor
|
||||
interface including colors for text, backgrounds, buttons, borders, and
|
||||
other UI elements.
|
||||
|
||||
Theme files are located in the `themes/` directory and are header-only
|
||||
C++ files that configure ImGui's style system.
|
||||
|
||||
Available themes
|
||||
----------------
|
||||
|
||||
Current themes (alphabetically):
|
||||
|
||||
- **amber** — Monochrome amber/black CRT-inspired theme
|
||||
- **eink** — E-ink inspired high-contrast theme (light/dark variants)
|
||||
- **everforest** — Warm, forest-inspired palette
|
||||
- **gruvbox** — Retro groove color scheme (light/dark variants)
|
||||
- **kanagawa-paper** — Inspired by traditional Japanese art
|
||||
- **lcars** — Star Trek LCARS interface style
|
||||
- **nord** — Arctic, north-bluish color palette
|
||||
- **old-book** — Sepia-toned vintage book aesthetic (light/dark
|
||||
variants)
|
||||
- **orbital** — Space-themed dark palette
|
||||
- **plan9** — Minimalist Plan 9 from Bell Labs inspired
|
||||
- **solarized** — Ethan Schoonover's Solarized (light/dark variants)
|
||||
- **weyland-yutani** — Alien franchise corporate aesthetic
|
||||
- **zenburn** — Low-contrast, easy-on-the-eyes theme
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Themes are configured via `$HOME/.config/kte/kge.ini`:
|
||||
|
||||
```ini
|
||||
theme = nord
|
||||
background = dark
|
||||
```
|
||||
|
||||
- `theme` — The theme name (e.g., "nord", "gruvbox", "solarized")
|
||||
- `background` — Either "dark" or "light" (for themes supporting both
|
||||
variants)
|
||||
|
||||
Themes can also be switched at runtime using the `:theme <name>`
|
||||
command.
|
||||
|
||||
Theme structure
|
||||
---------------
|
||||
|
||||
Each theme is a header file in `themes/` that defines one or more
|
||||
functions to apply the theme. The basic structure:
|
||||
|
||||
1. **Include ThemeHelpers.h** — Provides the `RGBA()` helper function
|
||||
2. **Define palette** — Create `ImVec4` color constants using
|
||||
`RGBA(0xRRGGBB)`
|
||||
3. **Get ImGui style** — Obtain reference via `ImGui::GetStyle()`
|
||||
4. **Set style parameters** — Configure padding, rounding, border sizes,
|
||||
etc.
|
||||
5. **Assign colors** — Map palette to `ImGuiCol_*` enum values
|
||||
|
||||
### Minimal example structure
|
||||
|
||||
```cpp
|
||||
// themes/MyTheme.h
|
||||
#pragma once
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
static void
|
||||
ApplyMyTheme()
|
||||
{
|
||||
// 1. Define color palette
|
||||
const ImVec4 bg = RGBA(0x1e1e1e);
|
||||
const ImVec4 fg = RGBA(0xd4d4d4);
|
||||
const ImVec4 accent = RGBA(0x007acc);
|
||||
|
||||
// 2. Get style reference
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
|
||||
// 3. Set style parameters
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FrameRounding = 3.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
// ... additional style parameters
|
||||
|
||||
// 4. Assign colors
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = fg;
|
||||
colors[ImGuiCol_WindowBg] = bg;
|
||||
colors[ImGuiCol_Button] = accent;
|
||||
// ... additional color assignments
|
||||
}
|
||||
```
|
||||
|
||||
The RGBA() helper
|
||||
-----------------
|
||||
|
||||
The `RGBA()` function (defined in `themes/ThemeHelpers.h`) converts
|
||||
packed RGB hex values to ImGui's `ImVec4` format:
|
||||
|
||||
```cpp
|
||||
const ImVec4 color = RGBA(0xRRGGBB); // Opaque (alpha = 1.0)
|
||||
const ImVec4 color = RGBA(0xRRGGBB, 0.5f); // With custom alpha
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```cpp
|
||||
const ImVec4 white = RGBA(0xFFFFFF);
|
||||
const ImVec4 black = RGBA(0x000000);
|
||||
const ImVec4 red = RGBA(0xFF0000);
|
||||
const ImVec4 blue = RGBA(0x0000FF);
|
||||
const ImVec4 semi = RGBA(0x808080, 0.5f); // 50% transparent gray
|
||||
```
|
||||
|
||||
ImGui color elements
|
||||
--------------------
|
||||
|
||||
Themes must define colors for ImGui's UI elements. Key `ImGuiCol_*`
|
||||
values:
|
||||
|
||||
### Text
|
||||
|
||||
- `ImGuiCol_Text` — Main text color
|
||||
- `ImGuiCol_TextDisabled` — Disabled/grayed-out text
|
||||
- `ImGuiCol_TextSelectedBg` — Text selection background
|
||||
|
||||
### Windows and backgrounds
|
||||
|
||||
- `ImGuiCol_WindowBg` — Window background
|
||||
- `ImGuiCol_ChildBg` — Child window background
|
||||
- `ImGuiCol_PopupBg` — Popup window background
|
||||
|
||||
### Borders
|
||||
|
||||
- `ImGuiCol_Border` — Border color
|
||||
- `ImGuiCol_BorderShadow` — Border shadow (often transparent)
|
||||
|
||||
### Frames (input fields, etc.)
|
||||
|
||||
- `ImGuiCol_FrameBg` — Frame background (normal state)
|
||||
- `ImGuiCol_FrameBgHovered` — Frame background when hovered
|
||||
- `ImGuiCol_FrameBgActive` — Frame background when active/clicked
|
||||
|
||||
### Title bars
|
||||
|
||||
- `ImGuiCol_TitleBg` — Title bar (unfocused)
|
||||
- `ImGuiCol_TitleBgActive` — Title bar (focused)
|
||||
- `ImGuiCol_TitleBgCollapsed` — Collapsed title bar
|
||||
|
||||
### Interactive elements
|
||||
|
||||
- `ImGuiCol_Button` — Button background
|
||||
- `ImGuiCol_ButtonHovered` — Button when hovered
|
||||
- `ImGuiCol_ButtonActive` — Button when pressed
|
||||
- `ImGuiCol_CheckMark` — Checkmark/radio button indicator
|
||||
- `ImGuiCol_SliderGrab` — Slider grab handle
|
||||
- `ImGuiCol_SliderGrabActive` — Slider grab when dragging
|
||||
|
||||
### Headers and separators
|
||||
|
||||
- `ImGuiCol_Header` — Header (tree nodes, collapsing headers)
|
||||
- `ImGuiCol_HeaderHovered` — Header when hovered
|
||||
- `ImGuiCol_HeaderActive` — Header when clicked
|
||||
- `ImGuiCol_Separator` — Separator line
|
||||
- `ImGuiCol_SeparatorHovered` — Separator when hovered
|
||||
- `ImGuiCol_SeparatorActive` — Separator when dragged
|
||||
|
||||
### Scrollbars
|
||||
|
||||
- `ImGuiCol_ScrollbarBg` — Scrollbar background
|
||||
- `ImGuiCol_ScrollbarGrab` — Scrollbar grab
|
||||
- `ImGuiCol_ScrollbarGrabHovered` — Scrollbar grab when hovered
|
||||
- `ImGuiCol_ScrollbarGrabActive` — Scrollbar grab when dragging
|
||||
|
||||
### Tabs
|
||||
|
||||
- `ImGuiCol_Tab` — Tab (inactive)
|
||||
- `ImGuiCol_TabHovered` — Tab when hovered
|
||||
- `ImGuiCol_TabActive` — Tab (active)
|
||||
- `ImGuiCol_TabUnfocused` — Tab in unfocused window
|
||||
- `ImGuiCol_TabUnfocusedActive` — Active tab in unfocused window
|
||||
|
||||
### Tables
|
||||
|
||||
- `ImGuiCol_TableHeaderBg` — Table header background
|
||||
- `ImGuiCol_TableBorderStrong` — Strong table borders
|
||||
- `ImGuiCol_TableBorderLight` — Light table borders
|
||||
- `ImGuiCol_TableRowBg` — Table row background
|
||||
- `ImGuiCol_TableRowBgAlt` — Alternating table row background
|
||||
|
||||
### Navigation and overlays
|
||||
|
||||
- `ImGuiCol_MenuBarBg` — Menu bar background
|
||||
- `ImGuiCol_ResizeGrip` — Resize grip indicator
|
||||
- `ImGuiCol_ResizeGripHovered` — Resize grip when hovered
|
||||
- `ImGuiCol_ResizeGripActive` — Resize grip when dragging
|
||||
- `ImGuiCol_DragDropTarget` — Drag-and-drop target highlight
|
||||
- `ImGuiCol_NavHighlight` — Navigation highlight
|
||||
- `ImGuiCol_NavWindowingHighlight` — Window navigation highlight
|
||||
- `ImGuiCol_NavWindowingDimBg` — Window navigation dim background
|
||||
- `ImGuiCol_ModalWindowDimBg` — Modal window dim background
|
||||
|
||||
### Plots (graphs)
|
||||
|
||||
- `ImGuiCol_PlotLines` — Plot line color
|
||||
- `ImGuiCol_PlotLinesHovered` — Plot line when hovered
|
||||
- `ImGuiCol_PlotHistogram` — Histogram color
|
||||
- `ImGuiCol_PlotHistogramHovered` — Histogram when hovered
|
||||
|
||||
Style parameters
|
||||
----------------
|
||||
|
||||
In addition to colors, themes can customize style parameters:
|
||||
|
||||
```cpp
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
|
||||
// Padding and spacing
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f); // Window content padding
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f); // Frame (input fields) padding
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f); // Table cell padding
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f); // Space between items
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f); // Space within composite items
|
||||
|
||||
// Rounding
|
||||
style.WindowRounding = 4.0f; // Window corner rounding
|
||||
style.FrameRounding = 3.0f; // Frame corner rounding
|
||||
style.PopupRounding = 4.0f; // Popup corner rounding
|
||||
style.GrabRounding = 3.0f; // Grab handle rounding
|
||||
style.TabRounding = 4.0f; // Tab corner rounding
|
||||
|
||||
// Borders
|
||||
style.WindowBorderSize = 1.0f; // Window border width
|
||||
style.FrameBorderSize = 1.0f; // Frame border width
|
||||
|
||||
// Scrollbars
|
||||
style.ScrollbarSize = 14.0f; // Scrollbar width
|
||||
style.GrabMinSize = 10.0f; // Minimum grab handle size
|
||||
```
|
||||
|
||||
Creating a new theme
|
||||
--------------------
|
||||
|
||||
Follow these steps to add a new theme to kte:
|
||||
|
||||
### 1. Create the theme file
|
||||
|
||||
Create a new header file in `themes/` (e.g., `themes/MyTheme.h`):
|
||||
|
||||
```cpp
|
||||
// themes/MyTheme.h — Brief description
|
||||
#pragma once
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||
|
||||
static void
|
||||
ApplyMyTheme()
|
||||
{
|
||||
// Define your color palette
|
||||
const ImVec4 background = RGBA(0x1e1e1e);
|
||||
const ImVec4 foreground = RGBA(0xd4d4d4);
|
||||
const ImVec4 accent = RGBA(0x007acc);
|
||||
// ... more colors
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
|
||||
// Configure style parameters
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
// ... more style settings
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
|
||||
// Assign all required colors
|
||||
colors[ImGuiCol_Text] = foreground;
|
||||
colors[ImGuiCol_WindowBg] = background;
|
||||
// ... assign all other ImGuiCol_* values
|
||||
}
|
||||
```
|
||||
|
||||
Refer to existing themes like `Nord.h` for a complete example of all
|
||||
required color assignments.
|
||||
|
||||
### 2. Add theme to GUITheme.h
|
||||
|
||||
Edit `GUITheme.h` to integrate your theme:
|
||||
|
||||
**a) Add to ThemeId enum:**
|
||||
|
||||
```cpp
|
||||
enum class ThemeId {
|
||||
// ... existing themes
|
||||
MyTheme = 13, // Use next available number
|
||||
};
|
||||
```
|
||||
|
||||
**b) Include your theme header:**
|
||||
|
||||
```cpp
|
||||
// After other theme includes
|
||||
#include "themes/MyTheme.h"
|
||||
```
|
||||
|
||||
**c) Create wrapper class in detail namespace:**
|
||||
|
||||
```cpp
|
||||
namespace detail {
|
||||
// ... existing theme classes
|
||||
|
||||
struct MyThemeWrapper final : Theme {
|
||||
[[nodiscard]] const char *Name() const override
|
||||
{
|
||||
return "mytheme"; // Lowercase canonical name
|
||||
}
|
||||
|
||||
void Apply() const override
|
||||
{
|
||||
ApplyMyTheme();
|
||||
}
|
||||
|
||||
ThemeId Id() override
|
||||
{
|
||||
return ThemeId::MyTheme;
|
||||
}
|
||||
};
|
||||
} // namespace detail
|
||||
```
|
||||
|
||||
**d) Register in ThemeRegistry():**
|
||||
|
||||
```cpp
|
||||
static const std::vector<std::unique_ptr<Theme>> &
|
||||
ThemeRegistry()
|
||||
{
|
||||
static std::vector<std::unique_ptr<Theme>> reg;
|
||||
if (reg.empty()) {
|
||||
// Add in alphabetical order by canonical name
|
||||
reg.emplace_back(std::make_unique<detail::AmberTheme>());
|
||||
// ... existing themes
|
||||
reg.emplace_back(std::make_unique<detail::MyThemeWrapper>());
|
||||
// ... remaining themes
|
||||
}
|
||||
return reg;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Test your theme
|
||||
|
||||
Rebuild kte and test:
|
||||
|
||||
```bash
|
||||
# Set theme in config
|
||||
echo "theme = mytheme" >> ~/.config/kte/kge.ini
|
||||
|
||||
# Or switch at runtime
|
||||
kge
|
||||
:theme mytheme
|
||||
```
|
||||
|
||||
Light/Dark theme variants
|
||||
--------------------------
|
||||
|
||||
Some themes support both light and dark background modes. To implement
|
||||
this:
|
||||
|
||||
### 1. Create separate functions for each variant
|
||||
|
||||
```cpp
|
||||
// themes/MyTheme.h
|
||||
#pragma once
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
static void
|
||||
ApplyMyThemeDark()
|
||||
{
|
||||
const ImVec4 bg = RGBA(0x1e1e1e); // Dark background
|
||||
const ImVec4 fg = RGBA(0xd4d4d4); // Light text
|
||||
// ... rest of dark theme
|
||||
}
|
||||
|
||||
static void
|
||||
ApplyMyThemeLight()
|
||||
{
|
||||
const ImVec4 bg = RGBA(0xffffff); // Light background
|
||||
const ImVec4 fg = RGBA(0x1e1e1e); // Dark text
|
||||
// ... rest of light theme
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Check background mode in Apply()
|
||||
|
||||
```cpp
|
||||
// In GUITheme.h wrapper class
|
||||
struct MyThemeWrapper final : Theme {
|
||||
// ... Name() and Id() methods
|
||||
|
||||
void Apply() const override
|
||||
{
|
||||
if (gBackgroundMode == BackgroundMode::Dark)
|
||||
ApplyMyThemeDark();
|
||||
else
|
||||
ApplyMyThemeLight();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
See `Solarized.h`, `Gruvbox.h`, `EInk.h`, or `OldBook.h` for complete
|
||||
examples.
|
||||
|
||||
Updating existing themes
|
||||
------------------------
|
||||
|
||||
To modify an existing theme:
|
||||
|
||||
### 1. Locate the theme file
|
||||
|
||||
Theme files are in `themes/` directory. For example, Nord theme is in
|
||||
`themes/Nord.h`.
|
||||
|
||||
### 2. Modify colors or style
|
||||
|
||||
Edit the `ApplyXxxTheme()` function:
|
||||
|
||||
- Update palette color definitions
|
||||
- Change style parameters
|
||||
- Reassign `ImGuiCol_*` values
|
||||
|
||||
### 3. Rebuild and test
|
||||
|
||||
```bash
|
||||
# Rebuild kte
|
||||
cmake --build build
|
||||
|
||||
# Test changes
|
||||
./build/kge
|
||||
```
|
||||
|
||||
Changes take effect immediately on next launch or theme switch.
|
||||
|
||||
Best practices
|
||||
--------------
|
||||
|
||||
When creating or updating themes:
|
||||
|
||||
1. **Start from an existing theme** — Copy a similar theme as a
|
||||
template (e.g., `Nord.h` for dark themes, `Solarized.h` for
|
||||
light/dark variants)
|
||||
|
||||
2. **Define a complete palette first** — Create all color constants at
|
||||
the top before assigning them
|
||||
|
||||
3. **Assign all colors** — Ensure every `ImGuiCol_*` value is set to
|
||||
avoid inheriting unexpected colors
|
||||
|
||||
4. **Use consistent naming** — Follow existing conventions (e.g.,
|
||||
`nord0`, `base03`, descriptive names)
|
||||
|
||||
5. **Test interactivity** — Verify hover, active, and disabled states
|
||||
for buttons, frames, and other interactive elements
|
||||
|
||||
6. **Consider contrast** — Ensure text is readable against backgrounds;
|
||||
test with different content
|
||||
|
||||
7. **Test transparency** — Use alpha values carefully for overlays, dim
|
||||
backgrounds, and selection highlights
|
||||
|
||||
8. **Match style to theme** — Adjust rounding, padding, and borders to
|
||||
suit the theme's aesthetic (e.g., sharp corners for retro themes,
|
||||
rounded for modern)
|
||||
|
||||
9. **Document inspiration** — Note the color scheme's origin or
|
||||
inspiration in the file header
|
||||
|
||||
10. **Maintain alphabetical order** — When registering in
|
||||
`ThemeRegistry()`, maintain alphabetical order by canonical name
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
### Theme not appearing
|
||||
|
||||
- Check that the theme is registered in `ThemeRegistry()` in
|
||||
alphabetical order
|
||||
- Verify the canonical name matches what you're using in config or
|
||||
commands
|
||||
- Ensure the theme header is included in `GUITheme.h`
|
||||
|
||||
### Colors look wrong
|
||||
|
||||
- Verify hex color values are in 0xRRGGBB format (not 0xBBGGRR)
|
||||
- Check alpha values for semi-transparent elements
|
||||
- Ensure all `ImGuiCol_*` values are assigned
|
||||
|
||||
### Style inconsistent
|
||||
|
||||
- Make sure style parameters are set before color assignments
|
||||
- Check that you're getting `ImGui::GetStyle()` reference correctly
|
||||
- Verify no global style changes are overriding theme settings
|
||||
|
||||
References
|
||||
----------
|
||||
|
||||
- ImGui style
|
||||
reference: https://github.com/ocornut/imgui/blob/master/imgui.h
|
||||
- Existing themes in `themes/` directory
|
||||
- Color palette resources: coolors.co, colorhunt.co
|
||||
@@ -1,279 +1,390 @@
|
||||
Undo System Overhaul Roadmap (emacs-style undo-tree)
|
||||
## Updated Undo System Plan for kte/kge
|
||||
|
||||
Context: macOS, C++17 project, ncurses terminal and SDL2/ImGui GUI frontends. Date: 2025-12-01.
|
||||
After reviewing the existing codebase and your undo plan, I propose
|
||||
the following refined approach that preserves your goals while making
|
||||
it more suitable for implementation:
|
||||
|
||||
Purpose
|
||||
### Refined Data Structures
|
||||
|
||||
- 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.
|
||||
The proposed data structures are sound but need some refinements:
|
||||
|
||||
References
|
||||
```c++
|
||||
enum class UndoType : uint8_t {
|
||||
Insert,
|
||||
Delete,
|
||||
Paste,
|
||||
Newline,
|
||||
DeleteRow,
|
||||
// Future: IndentRegion, KillRegion, etc.
|
||||
};
|
||||
|
||||
- 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.
|
||||
struct UndoNode {
|
||||
UndoType type;
|
||||
int row;
|
||||
int col;
|
||||
std::string text;
|
||||
std::unique_ptr<UndoNode> child = nullptr; // next in timeline
|
||||
std::unique_ptr<UndoNode> next = nullptr; // redo branch
|
||||
UndoNode* parent = nullptr; // weak pointer for navigation
|
||||
};
|
||||
|
||||
Instrumentation (KTE_UNDO_DEBUG)
|
||||
struct UndoTree {
|
||||
std::unique_ptr<UndoNode> root;
|
||||
UndoNode* current = nullptr;
|
||||
UndoNode* saved = nullptr;
|
||||
std::unique_ptr<UndoNode> pending = nullptr;
|
||||
};
|
||||
```
|
||||
|
||||
- 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
|
||||
Key changes:
|
||||
|
||||
# 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
|
||||
```
|
||||
- Use `std::unique_ptr` for owned pointers to ensure proper RAII
|
||||
- Add weak `parent` pointer for easier navigation
|
||||
- This ensures memory safety without manual management
|
||||
|
||||
- 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
|
||||
```
|
||||
```markdown
|
||||
# Undo System Implementation Roadmap for kte/kge
|
||||
|
||||
- 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
|
||||
```
|
||||
This is the complete implementation plan for the non-linear undo/redo
|
||||
system for kte. This document serves as a detailed
|
||||
specification for Junie to implement an undo system similar to emacs'
|
||||
undo-tree.
|
||||
|
||||
- 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.
|
||||
## Overview
|
||||
|
||||
Notes
|
||||
The goal is to implement a robust, memory-safe undo system where:
|
||||
|
||||
- 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. Each buffer has its own independent undo tree
|
||||
2. Undo and redo are non-linear - typing after undo creates a branch
|
||||
3. Operations are batched into word-level undo steps
|
||||
4. The system is leak-proof and handles buffer closure gracefully
|
||||
|
||||
̄1) Current State Summary (from docs/undo-state.md)
|
||||
## Phase 1: Core Data Structures
|
||||
|
||||
- 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.
|
||||
### 1.1 UndoType enum (UndoNode.h)
|
||||
```
|
||||
|
||||
Gaps/Risks
|
||||
cpp enum class UndoType : uint8_t { Insert, Delete, Paste, // can
|
||||
reuse Insert if preferred Newline, DeleteRow, // Future extensions:
|
||||
IndentRegion, KillRegion };
|
||||
|
||||
- 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.
|
||||
```
|
||||
### 1.2 UndoNode struct (UndoNode.h)
|
||||
```
|
||||
|
||||
cpp struct UndoNode { UndoType type; int row; // original cursor row
|
||||
int col; // original cursor column (updated during batch) std::string
|
||||
text; // the inserted or deleted text (full batch)
|
||||
std::unique_ptr<UndoNode> child = nullptr; // next in current timeline
|
||||
std::unique_ptr<UndoNode> next = nullptr; // redo branch (rarely used)
|
||||
UndoNode* parent = nullptr; // weak pointer for navigation };
|
||||
|
||||
2) Design Goals (emacs-like undo-tree)
|
||||
```
|
||||
### 1.3 UndoTree struct (UndoTree.h)
|
||||
```
|
||||
|
||||
- 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.
|
||||
cpp struct UndoTree { std::unique_ptr<UndoNode> root; // first edit
|
||||
ever UndoNode* current = nullptr; // current state of buffer UndoNode*
|
||||
saved = nullptr; // points to node matching last save
|
||||
std::unique_ptr<UndoNode> pending = nullptr; // in-progress batch };
|
||||
|
||||
```
|
||||
### 1.4 UndoSystem class (UndoSystem.h)
|
||||
```
|
||||
|
||||
3) Invariants and API (must align with docs/undo.md)
|
||||
cpp class UndoSystem { private: std::unique_ptr<UndoTree> tree;
|
||||
|
||||
- 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.
|
||||
public: UndoSystem(); ~UndoSystem() = default;
|
||||
|
||||
// Core batching API
|
||||
void begin(UndoType type, int row, int col);
|
||||
void append(char ch);
|
||||
void append(std::string_view text);
|
||||
void commit();
|
||||
|
||||
4) Phased Roadmap
|
||||
// Undo/Redo operations
|
||||
void undo(class Buffer& buffer);
|
||||
void redo(class Buffer& buffer);
|
||||
|
||||
Phase 0 — Baseline & Instrumentation (1 day)
|
||||
// State management
|
||||
void mark_saved();
|
||||
void discard_pending();
|
||||
void clear();
|
||||
|
||||
- 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.
|
||||
// Query methods
|
||||
bool can_undo() const;
|
||||
bool can_redo() const;
|
||||
bool is_dirty() const;
|
||||
|
||||
Phase 1 — Input Path Unification & Batching Rules (1–2 days)
|
||||
private: void apply_node(Buffer& buffer, const UndoNode* node, int
|
||||
direction); bool should_batch_with_pending(UndoType type, int row, int
|
||||
col) const; void attach_pending_to_current(); void
|
||||
discard_redo_branches(); };
|
||||
|
||||
- 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: Buffer Integration
|
||||
|
||||
Phase 2 — Tree Limits & GC (1 day)
|
||||
### 2.1 Add undo system to Buffer class (Buffer.h)
|
||||
Add to Buffer class:
|
||||
```
|
||||
|
||||
- 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.
|
||||
cpp private: std::unique_ptr<UndoSystem> undo_system; bool
|
||||
applying_undo = false; // prevent recursive undo during apply
|
||||
|
||||
Phase 3 — Compound Commands & Region Ops (2–3 days)
|
||||
public: // Raw operations (don't trigger undo) void
|
||||
raw_insert_text(int row, int col, std::string_view text); void
|
||||
raw_delete_text(int row, int col, size_t len); void raw_split_line(int
|
||||
row, int col); void raw_join_lines(int row); void raw_insert_row(int
|
||||
row, std::string_view text); void raw_delete_row(int row);
|
||||
|
||||
- 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.
|
||||
// Undo/Redo public API
|
||||
void undo();
|
||||
void redo();
|
||||
bool can_undo() const;
|
||||
bool can_redo() const;
|
||||
void mark_saved();
|
||||
bool is_dirty() const;
|
||||
|
||||
Phase 4 — Developer UX & Diagnostics (1 day)
|
||||
```
|
||||
### 2.2 Modify existing Buffer operations (Buffer.cc)
|
||||
For each user-facing operation (`insert_char`, `delete_char`, etc.):
|
||||
|
||||
- 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.
|
||||
1. **Before performing operation**: Call `undo_system->commit()` if cursor moved
|
||||
2. **Begin batching**: Call `undo_system->begin(type, row, col)`
|
||||
3. **Record change**: Call `undo_system->append()` with the affected text
|
||||
4. **Perform operation**: Execute the actual buffer modification
|
||||
5. **Auto-commit conditions**: Commit on cursor movement, command execution
|
||||
|
||||
Phase 5 — Comprehensive Tests & Property Checks (2–3 days)
|
||||
Example pattern:
|
||||
```
|
||||
|
||||
- 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.
|
||||
cpp void Buffer::insert_char(char ch) { if (applying_undo) return; //
|
||||
silent during undo application
|
||||
|
||||
Phase 6 — Performance & Stress (0.5–1 day)
|
||||
// Auto-commit if cursor moved significantly or type changed
|
||||
if (should_commit_before_insert()) {
|
||||
undo_system->commit();
|
||||
}
|
||||
|
||||
- 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.
|
||||
undo_system->begin(UndoType::Insert, cursor_row, cursor_col);
|
||||
undo_system->append(ch);
|
||||
|
||||
// Perform actual insertion
|
||||
raw_insert_text(cursor_row, cursor_col, std::string(1, ch));
|
||||
cursor_col++;
|
||||
|
||||
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.
|
||||
```
|
||||
### 2.3 Commit triggers
|
||||
Auto-commit `pending` operations when:
|
||||
- Cursor moves (arrow keys, mouse click)
|
||||
- Any command starts executing
|
||||
- Buffer switching
|
||||
- Before undo/redo operations
|
||||
- Before file save/close
|
||||
|
||||
## Phase 3: UndoSystem Implementation
|
||||
|
||||
6) Concrete Work Items by File
|
||||
### 3.1 Core batching logic (UndoSystem.cc)
|
||||
```
|
||||
|
||||
- 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).
|
||||
cpp void UndoSystem::begin(UndoType type, int row, int col) { if
|
||||
(should_batch_with_pending(type, row, col)) { // Continue existing
|
||||
batch return; }
|
||||
|
||||
// Commit any existing pending operation
|
||||
if (pending) {
|
||||
commit();
|
||||
}
|
||||
|
||||
7) Example Test Cases (sketches)
|
||||
// Create new pending node
|
||||
pending = std::make_unique<UndoNode>();
|
||||
pending->type = type;
|
||||
pending->row = row;
|
||||
pending->col = col;
|
||||
pending->text.clear();
|
||||
|
||||
- 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.
|
||||
bool UndoSystem::should_batch_with_pending(UndoType type, int row, int
|
||||
col) const { if (!pending) return false; if (pending->type != type)
|
||||
return false; if (pending->row != row) return false;
|
||||
|
||||
- Backspace batching:
|
||||
1) InsertText("hello"); Backspace×3; Undo();
|
||||
Expected: restores "hello".
|
||||
// For Insert: check if we're continuing at the right position
|
||||
if (type == UndoType::Insert) {
|
||||
return (pending->col + pending->text.size()) == col;
|
||||
}
|
||||
|
||||
- UTF-8 insertion:
|
||||
1) InsertText("😀汉"); Undo(); Redo();
|
||||
Expected: content unchanged across cycles; no crashes.
|
||||
// For Delete: check if we're continuing from the same position
|
||||
if (type == UndoType::Delete) {
|
||||
return pending->col == col;
|
||||
}
|
||||
|
||||
- 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().
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
8) Risks & Mitigations
|
||||
```
|
||||
### 3.2 Commit logic
|
||||
```
|
||||
|
||||
- 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.
|
||||
cpp void UndoSystem::commit() { if (!pending || pending->text.empty())
|
||||
{ pending.reset(); return; }
|
||||
|
||||
// Discard any redo branches from current position
|
||||
discard_redo_branches();
|
||||
|
||||
9) Nice-to-Have (post-MVP)
|
||||
// Attach pending as child of current
|
||||
attach_pending_to_current();
|
||||
|
||||
- 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).
|
||||
// Move current forward
|
||||
current = pending.release();
|
||||
if (current->parent) {
|
||||
current->parent->child.reset(current);
|
||||
}
|
||||
|
||||
// Update saved pointer if we diverged
|
||||
if (saved && saved != current) {
|
||||
// Check if saved is still reachable from current
|
||||
if (!is_ancestor_of(current, saved)) {
|
||||
saved = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
```
|
||||
### 3.3 Apply operations
|
||||
```
|
||||
|
||||
Appendix A — Minimal Developer Checklist
|
||||
cpp void UndoSystem::apply_node(Buffer& buffer, const UndoNode* node,
|
||||
int direction) { if (!node) return;
|
||||
|
||||
- [ ] 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.
|
||||
switch (node->type) {
|
||||
case UndoType::Insert:
|
||||
if (direction > 0) { // redo
|
||||
buffer.raw_insert_text(node->row, node->col, node->text);
|
||||
} else { // undo
|
||||
buffer.raw_delete_text(node->row, node->col, node->text.size());
|
||||
}
|
||||
break;
|
||||
|
||||
case UndoType::Delete:
|
||||
if (direction > 0) { // redo
|
||||
buffer.raw_delete_text(node->row, node->col, node->text.size());
|
||||
} else { // undo
|
||||
buffer.raw_insert_text(node->row, node->col, node->text);
|
||||
}
|
||||
break;
|
||||
|
||||
case UndoType::Newline:
|
||||
if (direction > 0) { // redo
|
||||
buffer.raw_split_line(node->row, node->col);
|
||||
} else { // undo
|
||||
buffer.raw_join_lines(node->row);
|
||||
}
|
||||
break;
|
||||
|
||||
// Handle other types...
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
## Phase 4: Command Integration
|
||||
|
||||
### 4.1 Add undo/redo commands (Command.cc)
|
||||
Register the undo/redo commands in the command system:
|
||||
```
|
||||
|
||||
cpp // In InstallDefaultCommands() CommandRegistry::Register({
|
||||
CommandId::Undo, "undo", "Undo the last change", [](CommandContext&
|
||||
ctx) { auto& editor = ctx.editor; auto* buffer =
|
||||
editor.current_buffer(); if (buffer && buffer->can_undo()) {
|
||||
buffer->undo(); return true; } return false; }, false // not public
|
||||
command });
|
||||
|
||||
CommandRegistry::Register({ CommandId::Redo, "redo", "Redo the last
|
||||
undone change", [](CommandContext& ctx) { auto& editor = ctx.editor;
|
||||
auto* buffer = editor.current_buffer(); if (buffer &&
|
||||
buffer->can_redo()) { buffer->redo(); return true; } return false; },
|
||||
false // not public command });
|
||||
|
||||
```
|
||||
### 4.2 Update keybinding handlers
|
||||
Ensure the input handlers map `C-k u` to `CommandId::Undo` and `C-k r`
|
||||
to `CommandId::Redo`.
|
||||
|
||||
## Phase 5: Memory Management and Edge Cases
|
||||
|
||||
### 5.1 Buffer lifecycle management
|
||||
- **Constructor**: Initialize `undo_system = std::make_unique<UndoSystem>()`
|
||||
- **Destructor**: `undo_system.reset()` (automatic)
|
||||
- **File reload**: Call `undo_system->clear()` before loading
|
||||
- **New file**: Call `undo_system->clear()`
|
||||
- **Close buffer**: Call `undo_system->discard_pending()` then let destructor handle cleanup
|
||||
|
||||
### 5.2 Save state tracking
|
||||
- **After successful save**: Call `buffer->mark_saved()`
|
||||
- **For dirty flag**: Use `buffer->is_dirty()`
|
||||
|
||||
### 5.3 Edge case handling
|
||||
- Prevent undo during undo application (`applying_undo` flag)
|
||||
- Handle empty operations gracefully
|
||||
- Ensure cursor positioning after undo/redo
|
||||
- Test memory leaks with rapid typing + buffer close
|
||||
|
||||
## Phase 6: Testing
|
||||
|
||||
### 6.1 Unit tests (test_undo.cc)
|
||||
Create comprehensive tests covering:
|
||||
- Basic typing and undo
|
||||
- Word-level batching
|
||||
- Non-linear undo (type, undo, type different text)
|
||||
- Memory leak testing
|
||||
- Save state tracking
|
||||
- Edge cases (empty buffers, large operations)
|
||||
|
||||
### 6.2 Integration tests
|
||||
- Test with all buffer implementations (GapBuffer, PieceTable)
|
||||
- Test with GUI and Terminal frontends
|
||||
- Test rapid typing + immediate buffer close
|
||||
- Test file reload during pending operations
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **Phase 1**: Implement core data structures
|
||||
2. **Phase 2**: Add Buffer integration points
|
||||
3. **Phase 3**: Implement UndoSystem methods
|
||||
4. **Phase 4**: Wire up commands and keybindings
|
||||
5. **Phase 5**: Handle edge cases and memory management
|
||||
6. **Phase 6**: Comprehensive testing
|
||||
|
||||
## Critical Success Criteria
|
||||
|
||||
- ✅ No memory leaks even with rapid typing + buffer close
|
||||
- ✅ Batching works correctly (word-level undo steps)
|
||||
- ✅ Non-linear undo creates branches correctly
|
||||
- ✅ Save state tracking works properly
|
||||
- ✅ Silent operations during undo application
|
||||
- ✅ Clean integration with existing Buffer operations
|
||||
|
||||
This roadmap provides Junie with a complete, step-by-step implementation plan that preserves the original design goals while ensuring robust, memory-safe implementation.
|
||||
```
|
||||
|
||||
This roadmap refines your original plan by:
|
||||
|
||||
1. **Memory Safety**: Uses `std::unique_ptr` for automatic memory
|
||||
management
|
||||
2. **Clear Implementation Steps**: Breaks down into logical phases
|
||||
3. **Integration Points**: Clearly identifies where to hook into
|
||||
existing code
|
||||
4. **Edge Case Handling**: Addresses buffer lifecycle and error
|
||||
conditions
|
||||
5. **Testing Strategy**: Ensures robust validation
|
||||
|
||||
The core design remains faithful to your emacs-style undo tree vision
|
||||
while being practical for implementation by Junie.
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
### Context recap
|
||||
|
||||
- The undo system is now tree‑based with batching rules and `KTE_UNDO_DEBUG` instrumentation hooks already present in
|
||||
`UndoSystem.{h,cc}`.
|
||||
- GUI path uses SDL; printable input now flows exclusively via `SDL_TEXTINPUT` to `CommandId::InsertText`, while
|
||||
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.).
|
||||
|
||||
### Status summary (2025‑12‑01)
|
||||
|
||||
- Input‑path unification: Completed. `GUIInputHandler.cc` routes all printable characters through `SDL_TEXTINPUT → InsertText`.
|
||||
Newlines originate only from `SDL_KEYDOWN → Newline`. CR/LF are filtered out of `SDL_TEXTINPUT` payloads. Suppression
|
||||
rules prevent stray `TEXTINPUT` after meta/prefix/universal‑argument flows. Terminal input path remains consistent.
|
||||
- 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`.
|
||||
|
||||
### Objectives
|
||||
|
||||
- Use the existing instrumentation to capture short traces of typing/backspacing/deleting and undo/redo.
|
||||
- Unify input paths (SDL `KEYDOWN` vs `TEXTINPUT`) and lock down commit boundaries across commands.
|
||||
- Extend tests to cover branching behavior, UTF‑8, and multi‑line operations.
|
||||
|
||||
### Plan of action
|
||||
|
||||
1. Enable instrumentation and make it easy to toggle
|
||||
- Add a CMake option in `CMakeLists.txt` (root project):
|
||||
`option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)`.
|
||||
- When ON, add a compile definition `-DKTE_UNDO_DEBUG` to all targets that include the editor core (e.g., `kte`,
|
||||
`kge`, and test binaries).
|
||||
- Keep the default OFF so normal builds are quiet; ensure both modes compile in CI.
|
||||
|
||||
2. Capture short traces to validate current behavior
|
||||
- Build with `-DKTE_UNDO_DEBUG=ON` and run the GUI frontend:
|
||||
- Scenario A: type a contiguous word, then move cursor (should show `Begin(Insert)` + multiple `Append`, single
|
||||
`commit` at a movement boundary).
|
||||
- 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.
|
||||
|
||||
3. Input‑path unification (SDL `KEYDOWN` vs `TEXTINPUT`) — Completed 2025‑12‑01
|
||||
- In `GUIInputHandler.cc`:
|
||||
- Ensure printable characters are generated exclusively from `SDL_TEXTINPUT` and mapped to
|
||||
`CommandId::InsertText`.
|
||||
- Keep `SDL_KEYDOWN` for control/meta/movement, backspace/delete, newline, and k‑prefix handling.
|
||||
- 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.
|
||||
|
||||
4. Enforce and verify commit boundaries in command execution — In progress
|
||||
- Audit `Command.cc` and ensure `u->commit()` is called before executing any non‑editing command that should end a
|
||||
batch:
|
||||
- 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).
|
||||
|
||||
5. Extend automated tests (or add them if absent) — Phase 1 completed
|
||||
- Branching behavior ✓
|
||||
- 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.
|
||||
|
||||
6. Documentation updates — In progress
|
||||
- In `docs/undo.md` and `docs/undo-roadmap.md`:
|
||||
- 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.
|
||||
|
||||
7. CI and build hygiene — Pending
|
||||
- Default builds: `KTE_UNDO_DEBUG` OFF.
|
||||
- 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.
|
||||
|
||||
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.
|
||||
|
||||
### How to run the tests
|
||||
|
||||
- Configure with `-DBUILD_TESTS=ON` and build the `test_undo` target. Run the produced binary (e.g., `./test_undo`).
|
||||
The test prints progress and uses assertions to validate behavior.
|
||||
|
||||
### Deliverables
|
||||
|
||||
- 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
|
||||
|
||||
### Current status (2025‑12‑01) vs acceptance criteria
|
||||
|
||||
- Short instrumentation traces match expected batching and commit behavior for typing, backspace/delete, newline, and
|
||||
undo/redo. — Pending (instrumentation toggle + capture not done)
|
||||
- Printable input comes exclusively from `SDL_TEXTINPUT`; no stray inserts after meta/prefix/universal‑argument flows.
|
||||
— Satisfied (GUI path updated; terminal path consistent)
|
||||
- Undo branching behaves correctly; redo is discarded upon new commits after undo. — Satisfied (tested)
|
||||
- UTF‑8 and multi‑line scenarios round‑trip via undo/redo according to the documented semantics. — Satisfied (tested)
|
||||
- Tests pass with `KTE_UNDO_DEBUG` both OFF and ON. — Pending (no CMake toggle yet; default OFF passes)
|
||||
140
docs/undo.md
140
docs/undo.md
@@ -1,140 +0,0 @@
|
||||
This is a design for a non-linear undo/redo system for kte. The design must be identical in behavior and correctness
|
||||
to the proven kte editor undo system.
|
||||
|
||||
### Core Requirements
|
||||
|
||||
1. Each open buffer has its own completely independent undo tree.
|
||||
2. Undo and redo must be non-linear: typing after undo creates a branch; old redo branches are discarded.
|
||||
3. Typing, backspacing, and pasting are batched into word-level undo steps.
|
||||
4. Undo/redo must never create new undo nodes while applying an undo/redo (silent, low-level apply).
|
||||
5. The system must be memory-safe and leak-proof even if the user types and immediately closes the buffer.
|
||||
|
||||
### Data Structures
|
||||
|
||||
```cpp
|
||||
enum class UndoType : uint8_t {
|
||||
Insert,
|
||||
Delete,
|
||||
Paste, // optional, can reuse Insert
|
||||
Newline,
|
||||
DeleteRow,
|
||||
// future: IndentRegion, KillRegion, etc.
|
||||
};
|
||||
|
||||
struct UndoNode {
|
||||
UndoType type;
|
||||
int row; // original cursor row
|
||||
int col; // original cursor column (updated during batch)
|
||||
std::string text; // the inserted or deleted text (full batch)
|
||||
UndoNode* child = nullptr; // next in current timeline
|
||||
UndoNode* next = nullptr; // redo branch (rarely used)
|
||||
// no parent pointer needed — we walk from root
|
||||
};
|
||||
|
||||
struct UndoTree {
|
||||
UndoNode* root = nullptr; // first edit ever
|
||||
UndoNode* current = nullptr; // current state of buffer
|
||||
UndoNode* saved = nullptr; // points to node matching last save (for dirty flag)
|
||||
UndoNode* pending = nullptr; // in-progress batch (detached)
|
||||
};
|
||||
```
|
||||
|
||||
Each `Buffer` owns one `std::unique_ptr<UndoTree>`.
|
||||
|
||||
### Core API (must implement exactly)
|
||||
|
||||
```cpp
|
||||
class UndoSystem {
|
||||
public:
|
||||
void Begin(UndoType type);
|
||||
void Append(char ch);
|
||||
void Append(std::string_view text);
|
||||
void commit(); // called on cursor move, commands, etc.
|
||||
|
||||
void undo(); // Ctrl+Z
|
||||
void redo(); // Ctrl+Y or Ctrl+Shift+Z
|
||||
|
||||
void mark_saved(); // after successful save
|
||||
void discard_pending(); // before closing buffer or loading new file
|
||||
void clear(); // new file / reset
|
||||
|
||||
private:
|
||||
void apply(const UndoNode* node, int direction); // +1 = redo, -1 = undo
|
||||
void free_node(UndoNode* node);
|
||||
void free_branch(UndoNode* node); // frees redo siblings only
|
||||
};
|
||||
```
|
||||
|
||||
### Critical Invariants and Rules
|
||||
|
||||
1. `begin()` must reuse `pending` if:
|
||||
- same type
|
||||
- same row
|
||||
- `pending->col + pending->text.size() == current_cursor_col`
|
||||
→ otherwise `commit()` old and create new
|
||||
|
||||
2. `pending` is detached — never linked until `commit()`
|
||||
|
||||
3. `commit()`:
|
||||
- discards redo branches (`current->child`)
|
||||
- attaches `pending` as `current->child`
|
||||
- advances `current`
|
||||
- clears `pending`
|
||||
- if diverged from `saved`, null it
|
||||
|
||||
4. `apply()` must use low-level buffer operations:
|
||||
- Never call public insert/delete/newline
|
||||
- Use raw `buffer.insert_text(row, col, text)` and `buffer.delete_text(row, col, len)`
|
||||
- These must not trigger undo
|
||||
|
||||
5. `undo()`:
|
||||
- move current to parent
|
||||
- apply(current, -1)
|
||||
|
||||
6. `redo()`:
|
||||
- move current to child
|
||||
- apply(current, +1)
|
||||
|
||||
7. `discard_pending()` must be called in:
|
||||
- buffer close
|
||||
- file reload
|
||||
- new file
|
||||
- any destructive operation
|
||||
|
||||
### Example Flow: Typing "hello"
|
||||
|
||||
```text
|
||||
begin(Insert) → pending = new node, col=0
|
||||
append('h') → pending->text = "h", pending->col = 1
|
||||
append('e') → "he", col = 2
|
||||
...
|
||||
commit() on arrow key → pending becomes current->child, current advances
|
||||
```
|
||||
|
||||
One undo step removes all of "hello".
|
||||
|
||||
### Required Helper in Buffer Class
|
||||
|
||||
```cpp
|
||||
class Buffer {
|
||||
void insert_text(int row, int col, std::string_view text); // raw, no undo
|
||||
void delete_text(int row, int col, size_t len); // raw, no undo
|
||||
void split_line(int row, int col); // raw newline
|
||||
void join_lines(int row); // raw join
|
||||
void insert_row(int row, std::string_view text); // raw
|
||||
void delete_row(int row); // raw
|
||||
};
|
||||
```
|
||||
|
||||
### Tasks for Agent
|
||||
|
||||
1. Implement `UndoNode`, `UndoTree`, and `UndoSystem` class exactly as specified.
|
||||
2. Add `std::unique_ptr<UndoTree> undo;` to `Buffer`.
|
||||
3. Modify `insert_char`, `delete_char`, `paste`, `newline` to use `undo.begin()/append()/commit()`.
|
||||
4. Add `undo.commit()` at start of all cursor movement and command functions.
|
||||
5. Implement `apply()` using only `Buffer`'s raw methods.
|
||||
6. Add `undo.discard_pending()` in all buffer reset/close paths.
|
||||
7. Add `Ctrl+Z` → `buffer.undo()`, `Ctrl+Y` → `buffer.redo()`.
|
||||
|
||||
This design is used in production editors and is considered the gold standard for small, correct, non-linear undo in
|
||||
C/C++. Implement it faithfully.
|
||||
@@ -15,6 +15,7 @@
|
||||
full = kge;
|
||||
kte = (pkgsFor system).callPackage ./default.nix { graphical = false; };
|
||||
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; };
|
||||
qt = (pkgsFor system).callPackage ./default.nix { graphical-qt = true; }
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#ifndef KGE_FONTS_B612_MONO_H
|
||||
#define KGE_FONTS_B612_MONO_H
|
||||
#pragma once
|
||||
|
||||
#include "Font.h"
|
||||
|
||||
|
||||
// File: 'B612_Mono/B612Mono-Bold.ttf' (135904 bytes)
|
||||
namespace kte::Fonts {
|
||||
namespace B612Mono {
|
||||
// File: 'B612Mono/B612Mono-Bold.ttf' (135904 bytes)
|
||||
// Exported using binary_to_compressed_c.cpp
|
||||
static const unsigned int DefaultFontBoldCompressedSize = 74748;
|
||||
static const unsigned int DefaultFontBoldCompressedData[74748 / 4] =
|
||||
@@ -3125,7 +3128,7 @@ static const unsigned int DefaultFontBoldCompressedData[74748 / 4] =
|
||||
};
|
||||
|
||||
|
||||
// File: 'B612_Mono/B612Mono-Italic.ttf' (118888 bytes)
|
||||
// File: 'B612Mono/B612Mono-Italic.ttf' (118888 bytes)
|
||||
// Exported using binary_to_compressed_c.cpp
|
||||
static const unsigned int DefaultFontItalicCompressedSize = 67763;
|
||||
static const unsigned int DefaultFontItalicCompressedData[67764 / 4] =
|
||||
@@ -5956,7 +5959,7 @@ static const unsigned int DefaultFontItalicCompressedData[67764 / 4] =
|
||||
};
|
||||
|
||||
|
||||
// File: 'B612_Mono/B612Mono-BoldItalic.ttf' (121732 bytes)
|
||||
// File: 'B612Mono/B612Mono-BoldItalic.ttf' (121732 bytes)
|
||||
// Exported using binary_to_compressed_c.cpp
|
||||
static const unsigned int DefaultFontBoldItalicCompressedSize = 69211;
|
||||
static const unsigned int DefaultFontBoldItalicCompressedData[69212 / 4] =
|
||||
@@ -8848,7 +8851,7 @@ static const unsigned int DefaultFontBoldItalicCompressedData[69212 / 4] =
|
||||
};
|
||||
|
||||
|
||||
// File: 'B612_Mono/B612Mono-Regular.ttf' (136712 bytes)
|
||||
// File: 'B612Mono/B612Mono-Regular.ttf' (136712 bytes)
|
||||
// Exported using binary_to_compressed_c.cpp
|
||||
static const unsigned int DefaultFontRegularCompressedSize = 72615;
|
||||
static const unsigned int DefaultFontRegularCompressedData[72616 / 4] =
|
||||
@@ -11880,6 +11883,5 @@ static const unsigned int DefaultFontRegularCompressedData[72616 / 4] =
|
||||
0x534c1701, 0x17042a0f, 0x1efd613e, 0x0130511f, 0x2a568e22, 0x3e611404, 0x51301ffd, 0x8f22041f, 0x00fa055c,
|
||||
0x0070a96e,
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user