4 Commits

Author SHA1 Message Date
0bfe75fbf0 Refactor indentation for consistent style across codebase.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2025-12-02 01:37:44 -08:00
d15b241140 Refactor syntax highlighting infrastructure and related classes.
- Moved all language highlighter implementations (`CppHighlighter`, `GoHighlighter`, `JsonHighlighter`, etc.), the engine, and registry to `syntax/`.
2025-12-02 01:36:26 -08:00
ceef6af3ae Add extensible highlighter registration and Tree-sitter support.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
- Implemented runtime API for registering custom highlighters.
- Added optional Tree-sitter integration for advanced syntax parsing (disabled by default).
- Updated buffer initialization and copying to support dynamic highlighter configuration.
- Introduced `NullHighlighter` as a fallback for unsupported filetypes.
- Enhanced CMake configuration with `KTE_ENABLE_TREESITTER` option.
2025-12-01 19:04:37 -08:00
e62cf3ee28 Add viewport-aware syntax prefetching and background warming.
- Added prefetching in both terminal and GUI renderers to optimize visible row highlights.
- Introduced background worker for offscreen highlight warming to improve scrolling performance.
- Refactored `HighlighterEngine` to manage thread-safety, caching, and stateful re-computation.
- Integrated changes into `HighlighterEngine`, `TerminalRenderer`, and `GUIRenderer`.
- Bumped version to 1.2.0 in preparation for the release.
2025-12-01 18:37:01 -08:00
73 changed files with 4847 additions and 2485 deletions

View File

@@ -6,6 +6,9 @@
#include "Buffer.h" #include "Buffer.h"
#include "UndoSystem.h" #include "UndoSystem.h"
#include "UndoTree.h" #include "UndoTree.h"
// For reconstructing highlighter state on copies
#include "syntax/HighlighterRegistry.h"
#include "syntax/NullHighlighter.h"
Buffer::Buffer() Buffer::Buffer()
@@ -40,9 +43,32 @@ Buffer::Buffer(const Buffer &other)
mark_set_ = other.mark_set_; mark_set_ = other.mark_set_;
mark_curx_ = other.mark_curx_; mark_curx_ = other.mark_curx_;
mark_cury_ = other.mark_cury_; mark_cury_ = other.mark_cury_;
// Copy syntax/highlighting flags
version_ = other.version_;
syntax_enabled_ = other.syntax_enabled_;
filetype_ = other.filetype_;
// Fresh undo system for the copy // Fresh undo system for the copy
undo_tree_ = std::make_unique<UndoTree>(); undo_tree_ = std::make_unique<UndoTree>();
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_); undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
// Recreate a highlighter engine for this copy based on filetype/syntax state
if (syntax_enabled_) {
// Allocate engine and install an appropriate highlighter
highlighter_ = std::make_unique<kte::HighlighterEngine>();
if (!filetype_.empty()) {
auto hl = kte::HighlighterRegistry::CreateFor(filetype_);
if (hl) {
highlighter_->SetHighlighter(std::move(hl));
} else {
// Unsupported filetype -> NullHighlighter keeps syntax pipeline active
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
} else {
// No filetype -> keep syntax enabled but use NullHighlighter
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
// Fresh engine has empty caches; nothing to invalidate
}
} }
@@ -65,9 +91,28 @@ Buffer::operator=(const Buffer &other)
mark_set_ = other.mark_set_; mark_set_ = other.mark_set_;
mark_curx_ = other.mark_curx_; mark_curx_ = other.mark_curx_;
mark_cury_ = other.mark_cury_; mark_cury_ = other.mark_cury_;
version_ = other.version_;
syntax_enabled_ = other.syntax_enabled_;
filetype_ = other.filetype_;
// Recreate undo system for this instance // Recreate undo system for this instance
undo_tree_ = std::make_unique<UndoTree>(); undo_tree_ = std::make_unique<UndoTree>();
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_); undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
// Recreate highlighter engine consistent with syntax settings
highlighter_.reset();
if (syntax_enabled_) {
highlighter_ = std::make_unique<kte::HighlighterEngine>();
if (!filetype_.empty()) {
auto hl = kte::HighlighterRegistry::CreateFor(filetype_);
if (hl) {
highlighter_->SetHighlighter(std::move(hl));
} else {
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
} else {
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
}
return *this; return *this;
} }
@@ -91,6 +136,11 @@ Buffer::Buffer(Buffer &&other) noexcept
undo_tree_(std::move(other.undo_tree_)), undo_tree_(std::move(other.undo_tree_)),
undo_sys_(std::move(other.undo_sys_)) undo_sys_(std::move(other.undo_sys_))
{ {
// Move syntax/highlighting state
version_ = other.version_;
syntax_enabled_ = other.syntax_enabled_;
filetype_ = std::move(other.filetype_);
highlighter_ = std::move(other.highlighter_);
// Update UndoSystem's buffer reference to point to this object // Update UndoSystem's buffer reference to point to this object
if (undo_sys_) { if (undo_sys_) {
undo_sys_->UpdateBufferReference(*this); undo_sys_->UpdateBufferReference(*this);
@@ -122,6 +172,12 @@ Buffer::operator=(Buffer &&other) noexcept
undo_tree_ = std::move(other.undo_tree_); undo_tree_ = std::move(other.undo_tree_);
undo_sys_ = std::move(other.undo_sys_); undo_sys_ = std::move(other.undo_sys_);
// Move syntax/highlighting state
version_ = other.version_;
syntax_enabled_ = other.syntax_enabled_;
filetype_ = std::move(other.filetype_);
highlighter_ = std::move(other.highlighter_);
// Update UndoSystem's buffer reference to point to this object // Update UndoSystem's buffer reference to point to this object
if (undo_sys_) { if (undo_sys_) {
undo_sys_->UpdateBufferReference(*this); undo_sys_->UpdateBufferReference(*this);

View File

@@ -14,7 +14,7 @@
#include "UndoSystem.h" #include "UndoSystem.h"
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include "HighlighterEngine.h" #include "syntax/HighlighterEngine.h"
#include "Highlight.h" #include "Highlight.h"
@@ -375,22 +375,55 @@ public:
[[nodiscard]] std::string AsString() const; [[nodiscard]] std::string AsString() const;
// Syntax highlighting integration (per-buffer) // 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; } void SetSyntaxEnabled(bool on)
[[nodiscard]] const std::string &Filetype() const { return filetype_; } {
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() void EnsureHighlighter()
{ {
if (!highlighter_) highlighter_ = std::make_unique<kte::HighlighterEngine>(); if (!highlighter_)
highlighter_ = std::make_unique<kte::HighlighterEngine>();
} }
// Raw, low-level editing APIs used by UndoSystem apply(). // Raw, low-level editing APIs used by UndoSystem apply().
// These must NOT trigger undo recording. They also do not move the cursor. // These must NOT trigger undo recording. They also do not move the cursor.
void insert_text(int row, int col, std::string_view text); void insert_text(int row, int col, std::string_view text);
@@ -430,7 +463,7 @@ private:
// Syntax/highlighting state // Syntax/highlighting state
std::uint64_t version_ = 0; // increment on edits std::uint64_t version_ = 0; // increment on edits
bool syntax_enabled_ = true; bool syntax_enabled_ = true;
std::string filetype_; std::string filetype_;
std::unique_ptr<kte::HighlighterEngine> highlighter_; std::unique_ptr<kte::HighlighterEngine> highlighter_;
}; };

View File

@@ -4,7 +4,7 @@ project(kte)
include(GNUInstallDirs) include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(KTE_VERSION "1.1.2") set(KTE_VERSION "1.2.1")
# Default to terminal-only build to avoid SDL/OpenGL dependency by default. # Default to terminal-only build to avoid SDL/OpenGL dependency by default.
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available. # Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
@@ -13,35 +13,39 @@ set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON) option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI") set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF) option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
if (CMAKE_HOST_UNIX) if (CMAKE_HOST_UNIX)
message(STATUS "Build system is POSIX.") message(STATUS "Build system is POSIX.")
else () else ()
message(STATUS "Build system is NOT POSIX.") message(STATUS "Build system is NOT POSIX.")
endif () endif ()
if (MSVC) if (MSVC)
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>") add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
else () else ()
add_compile_options( add_compile_options(
"-Wall" "-Wall"
"-Wextra" "-Wextra"
"-Werror" "-Werror"
"$<$<CONFIG:DEBUG>:-g>" "$<$<CONFIG:DEBUG>:-g>"
"$<$<CONFIG:RELEASE>:-O2>") "$<$<CONFIG:RELEASE>:-O2>")
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
add_compile_options("-stdlib=libc++") add_compile_options("-stdlib=libc++")
else () else ()
# nothing special for gcc at the moment # nothing special for gcc at the moment
endif () endif ()
endif () endif ()
add_compile_definitions(KGE_PLATFORM=${CMAKE_HOST_SYSTEM_NAME}) add_compile_definitions(KGE_PLATFORM=${CMAKE_HOST_SYSTEM_NAME})
add_compile_definitions(KTE_VERSION_STR="v${KTE_VERSION}") add_compile_definitions(KTE_VERSION_STR="v${KTE_VERSION}")
if (KTE_ENABLE_TREESITTER)
add_compile_definitions(KTE_ENABLE_TREESITTER)
endif ()
message(STATUS "Build system: ${CMAKE_HOST_SYSTEM_NAME}") message(STATUS "Build system: ${CMAKE_HOST_SYSTEM_NAME}")
if (${BUILD_GUI}) if (${BUILD_GUI})
include(cmake/imgui.cmake) include(cmake/imgui.cmake)
endif () endif ()
# NCurses for terminal mode # NCurses for terminal mode
@@ -50,193 +54,251 @@ set(CURSES_NEED_WIDE)
find_package(Curses REQUIRED) find_package(Curses REQUIRED)
include_directories(${CURSES_INCLUDE_DIR}) include_directories(${CURSES_INCLUDE_DIR})
set(SYNTAX_SOURCES
syntax/GoHighlighter.cc
syntax/CppHighlighter.cc
syntax/JsonHighlighter.cc
syntax/ErlangHighlighter.cc
syntax/MarkdownHighlighter.cc
syntax/TreeSitterHighlighter.cc
syntax/LispHighlighter.cc
syntax/HighlighterEngine.cc
syntax/RustHighlighter.cc
syntax/HighlighterRegistry.cc
syntax/SqlHighlighter.cc
syntax/NullHighlighter.cc
syntax/ForthHighlighter.cc
syntax/PythonHighlighter.cc
syntax/ShellHighlighter.cc
)
if (KTE_ENABLE_TREESITTER)
list(APPEND SYNTAX_SOURCES
TreeSitterHighlighter.cc)
endif ()
set(COMMON_SOURCES set(COMMON_SOURCES
GapBuffer.cc GapBuffer.cc
PieceTable.cc PieceTable.cc
Buffer.cc Buffer.cc
Editor.cc Editor.cc
Command.cc Command.cc
HelpText.cc HelpText.cc
KKeymap.cc KKeymap.cc
TerminalInputHandler.cc TerminalInputHandler.cc
TerminalRenderer.cc TerminalRenderer.cc
TerminalFrontend.cc TerminalFrontend.cc
TestInputHandler.cc TestInputHandler.cc
TestRenderer.cc TestRenderer.cc
TestFrontend.cc TestFrontend.cc
UndoNode.cc UndoNode.cc
UndoTree.cc UndoTree.cc
UndoSystem.cc UndoSystem.cc
HighlighterEngine.cc
CppHighlighter.cc ${SYNTAX_SOURCES}
HighlighterRegistry.cc )
NullHighlighter.cc
JsonHighlighter.cc
MarkdownHighlighter.cc set(SYNTAX_HEADERS
ShellHighlighter.cc syntax/GoHighlighter.h
GoHighlighter.cc syntax/HighlighterEngine.h
PythonHighlighter.cc syntax/ShellHighlighter.h
RustHighlighter.cc syntax/MarkdownHighlighter.h
LispHighlighter.cc syntax/LispHighlighter.h
syntax/SqlHighlighter.h
syntax/ForthHighlighter.h
syntax/JsonHighlighter.h
syntax/TreeSitterHighlighter.h
syntax/NullHighlighter.h
syntax/CppHighlighter.h
syntax/ErlangHighlighter.h
syntax/LanguageHighlighter.h
syntax/RustHighlighter.h
syntax/PythonHighlighter.h
)
if (KTE_ENABLE_TREESITTER)
list(APPEND THEME_HEADERS
TreeSitterHighlighter.h)
endif ()
set(THEME_HEADERS
themes/ThemeHelpers.h
themes/EInk.h
themes/Gruvbox.h
themes/Solarized.h
themes/Plan9.h
themes/Nord.h
) )
set(COMMON_HEADERS set(COMMON_HEADERS
GapBuffer.h GapBuffer.h
PieceTable.h PieceTable.h
Buffer.h Buffer.h
Editor.h Editor.h
AppendBuffer.h AppendBuffer.h
Command.h Command.h
HelpText.h HelpText.h
KKeymap.h KKeymap.h
InputHandler.h InputHandler.h
TerminalInputHandler.h TerminalInputHandler.h
Renderer.h Renderer.h
TerminalRenderer.h TerminalRenderer.h
Frontend.h Frontend.h
TerminalFrontend.h TerminalFrontend.h
TestInputHandler.h TestInputHandler.h
TestRenderer.h TestRenderer.h
TestFrontend.h TestFrontend.h
UndoNode.h UndoNode.h
UndoTree.h UndoTree.h
UndoSystem.h UndoSystem.h
Highlight.h Highlight.h
LanguageHighlighter.h
HighlighterEngine.h ${SYNTAX_HEADERS}
CppHighlighter.h ${THEME_HEADERS}
HighlighterRegistry.h
NullHighlighter.h
JsonHighlighter.h
MarkdownHighlighter.h
ShellHighlighter.h
GoHighlighter.h
PythonHighlighter.h
RustHighlighter.h
LispHighlighter.h
) )
# kte (terminal-first) executable # kte (terminal-first) executable
add_executable(kte add_executable(kte
main.cc main.cc
${COMMON_SOURCES} ${COMMON_SOURCES}
${COMMON_HEADERS} ${COMMON_HEADERS}
) )
if (KTE_USE_PIECE_TABLE) if (KTE_USE_PIECE_TABLE)
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1) target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
endif () endif ()
if (KTE_UNDO_DEBUG) if (KTE_UNDO_DEBUG)
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1) target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
endif () endif ()
target_link_libraries(kte ${CURSES_LIBRARIES}) target_link_libraries(kte ${CURSES_LIBRARIES})
if (KTE_ENABLE_TREESITTER)
# Users can provide their own tree-sitter include/lib via cache variables
set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)")
if (TREESITTER_INCLUDE_DIR)
target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR})
endif ()
if (TREESITTER_LIBRARY)
target_link_libraries(kte ${TREESITTER_LIBRARY})
endif ()
endif ()
install(TARGETS kte install(TARGETS kte
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
) )
# Man pages # Man pages
install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
if (BUILD_TESTS) if (BUILD_TESTS)
# test_undo executable for testing undo/redo system # test_undo executable for testing undo/redo system
add_executable(test_undo add_executable(test_undo
test_undo.cc test_undo.cc
${COMMON_SOURCES} ${COMMON_SOURCES}
${COMMON_HEADERS} ${COMMON_HEADERS}
) )
if (KTE_USE_PIECE_TABLE) if (KTE_USE_PIECE_TABLE)
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1) target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
endif () endif ()
if (KTE_UNDO_DEBUG) if (KTE_UNDO_DEBUG)
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1) target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
endif () endif ()
target_link_libraries(test_undo ${CURSES_LIBRARIES}) target_link_libraries(test_undo ${CURSES_LIBRARIES})
if (KTE_ENABLE_TREESITTER)
if (TREESITTER_INCLUDE_DIR)
target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR})
endif ()
if (TREESITTER_LIBRARY)
target_link_libraries(test_undo ${TREESITTER_LIBRARY})
endif ()
endif ()
endif () endif ()
if (${BUILD_GUI}) if (${BUILD_GUI})
target_sources(kte PRIVATE target_sources(kte PRIVATE
Font.h Font.h
GUIConfig.cc GUIConfig.cc
GUIConfig.h GUIConfig.h
GUIRenderer.cc GUIRenderer.cc
GUIRenderer.h GUIRenderer.h
GUIInputHandler.cc GUIInputHandler.cc
GUIInputHandler.h GUIInputHandler.h
GUIFrontend.cc GUIFrontend.cc
GUIFrontend.h) GUIFrontend.h)
target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1) target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
target_link_libraries(kte imgui) target_link_libraries(kte imgui)
# kge (GUI-first) executable # kge (GUI-first) executable
add_executable(kge add_executable(kge
main.cc main.cc
${COMMON_SOURCES} ${COMMON_SOURCES}
${COMMON_HEADERS} ${COMMON_HEADERS}
GUIConfig.cc GUIConfig.cc
GUIConfig.h GUIConfig.h
GUIRenderer.cc GUIRenderer.cc
GUIRenderer.h GUIRenderer.h
GUIInputHandler.cc GUIInputHandler.cc
GUIInputHandler.h GUIInputHandler.h
GUIFrontend.cc GUIFrontend.cc
GUIFrontend.h) GUIFrontend.h)
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE}) target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
if (KTE_UNDO_DEBUG) if (KTE_UNDO_DEBUG)
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1) target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
endif () endif ()
target_link_libraries(kge ${CURSES_LIBRARIES} imgui) target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
# On macOS, build kge as a proper .app bundle # On macOS, build kge as a proper .app bundle
if (APPLE) if (APPLE)
# Define the icon file # Define the icon file
set(MACOSX_BUNDLE_ICON_FILE kge.icns) set(MACOSX_BUNDLE_ICON_FILE kge.icns)
set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}") set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
# Add icon to the target sources and mark it as a resource # Add icon to the target sources and mark it as a resource
target_sources(kge PRIVATE ${kge_ICON}) target_sources(kge PRIVATE ${kge_ICON})
set_source_files_properties(${kge_ICON} PROPERTIES set_source_files_properties(${kge_ICON} PROPERTIES
MACOSX_PACKAGE_LOCATION Resources) MACOSX_PACKAGE_LOCATION Resources)
# Configure Info.plist with version and identifiers # Configure Info.plist with version and identifiers
set(KGE_BUNDLE_ID "dev.wntrmute.kge") set(KGE_BUNDLE_ID "dev.wntrmute.kge")
configure_file( configure_file(
${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in ${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in
${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist ${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
@ONLY) @ONLY)
set_target_properties(kge PROPERTIES set_target_properties(kge PROPERTIES
MACOSX_BUNDLE TRUE MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID} MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
MACOSX_BUNDLE_BUNDLE_NAME "kge" MACOSX_BUNDLE_BUNDLE_NAME "kge"
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE} MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist") MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
add_dependencies(kge kte) add_dependencies(kge kte)
add_custom_command(TARGET kge POST_BUILD add_custom_command(TARGET kge POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:kte> $<TARGET_FILE:kte>
$<TARGET_FILE_DIR:kge>/kte $<TARGET_FILE_DIR:kge>/kte
COMMENT "Copying kte binary into kge.app bundle") COMMENT "Copying kte binary into kge.app bundle")
install(TARGETS kge install(TARGETS kge
BUNDLE DESTINATION . BUNDLE DESTINATION .
) )
install(TARGETS kte install(TARGETS kte
RUNTIME DESTINATION kge.app/Contents/MacOS RUNTIME DESTINATION kge.app/Contents/MacOS
) )
else () else ()
install(TARGETS kge install(TARGETS kge
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
) )
endif () endif ()
# Install kge man page only when GUI is built # Install kge man page only when GUI is built
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons) install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
endif () endif ()

View File

@@ -7,15 +7,15 @@
#include <cctype> #include <cctype>
#include "Command.h" #include "Command.h"
#include "HighlighterRegistry.h" #include "syntax/HighlighterRegistry.h"
#include "NullHighlighter.h" #include "syntax/NullHighlighter.h"
#include "Editor.h" #include "Editor.h"
#include "Buffer.h" #include "Buffer.h"
#include "UndoSystem.h" #include "UndoSystem.h"
#include "HelpText.h" #include "HelpText.h"
#include "LanguageHighlighter.h" #include "syntax/LanguageHighlighter.h"
#include "HighlighterEngine.h" #include "syntax/HighlighterEngine.h"
#include "CppHighlighter.h" #include "syntax/CppHighlighter.h"
#ifdef KTE_BUILD_GUI #ifdef KTE_BUILD_GUI
#include "GUITheme.h" #include "GUITheme.h"
#endif #endif
@@ -762,122 +762,140 @@ cmd_unknown_kcommand(CommandContext &ctx)
return true; return true;
} }
// --- Syntax highlighting commands --- // --- Syntax highlighting commands ---
static void apply_filetype(Buffer &buf, const std::string &ft) static void
apply_filetype(Buffer &buf, const std::string &ft)
{ {
buf.EnsureHighlighter(); buf.EnsureHighlighter();
auto *eng = buf.Highlighter(); auto *eng = buf.Highlighter();
if (!eng) return; if (!eng)
std::string val = ft; return;
// trim + lower std::string val = ft;
auto trim = [](const std::string &s){ // trim + lower
std::string r = s; auto trim = [](const std::string &s) {
auto notsp = [](int ch){ return !std::isspace(ch); }; std::string r = s;
r.erase(r.begin(), std::find_if(r.begin(), r.end(), notsp)); auto notsp = [](int ch) {
r.erase(std::find_if(r.rbegin(), r.rend(), notsp).base(), r.end()); return !std::isspace(ch);
return r; };
}; r.erase(r.begin(), std::find_if(r.begin(), r.end(), notsp));
val = trim(val); r.erase(std::find_if(r.rbegin(), r.rend(), notsp).base(), r.end());
for (auto &ch: val) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch))); return r;
if (val == "off") { };
eng->SetHighlighter(nullptr); val = trim(val);
buf.SetFiletype(""); for (auto &ch: val)
buf.SetSyntaxEnabled(false); ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
return; if (val == "off") {
} eng->SetHighlighter(nullptr);
if (val.empty()) { buf.SetFiletype("");
// Empty means unknown/unspecified -> use NullHighlighter but keep syntax enabled buf.SetSyntaxEnabled(false);
buf.SetFiletype(""); return;
buf.SetSyntaxEnabled(true); }
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>()); if (val.empty()) {
eng->InvalidateFrom(0); // Empty means unknown/unspecified -> use NullHighlighter but keep syntax enabled
return; buf.SetFiletype("");
} buf.SetSyntaxEnabled(true);
// Normalize and create via registry eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
std::string norm = kte::HighlighterRegistry::Normalize(val); eng->InvalidateFrom(0);
auto hl = kte::HighlighterRegistry::CreateFor(norm); return;
if (hl) { }
eng->SetHighlighter(std::move(hl)); // Normalize and create via registry
buf.SetFiletype(norm); std::string norm = kte::HighlighterRegistry::Normalize(val);
buf.SetSyntaxEnabled(true); auto hl = kte::HighlighterRegistry::CreateFor(norm);
eng->InvalidateFrom(0); if (hl) {
} else { eng->SetHighlighter(std::move(hl));
// Unknown -> install NullHighlighter and keep syntax enabled buf.SetFiletype(norm);
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>()); buf.SetSyntaxEnabled(true);
buf.SetFiletype(val); // record what user asked even if unsupported eng->InvalidateFrom(0);
buf.SetSyntaxEnabled(true); } else {
eng->InvalidateFrom(0); // Unknown -> install NullHighlighter and keep syntax enabled
} eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
buf.SetFiletype(val); // record what user asked even if unsupported
buf.SetSyntaxEnabled(true);
eng->InvalidateFrom(0);
}
} }
static bool cmd_syntax(CommandContext &ctx)
static bool
cmd_syntax(CommandContext &ctx)
{ {
Buffer *b = ctx.editor.CurrentBuffer(); Buffer *b = ctx.editor.CurrentBuffer();
if (!b) { if (!b) {
ctx.editor.SetStatus("No buffer"); ctx.editor.SetStatus("No buffer");
return true; return true;
} }
std::string arg = ctx.arg; std::string arg = ctx.arg;
// trim // trim
auto trim = [](std::string &s){ auto trim = [](std::string &s) {
auto notsp = [](int ch){ return !std::isspace(ch); }; auto notsp = [](int ch) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp)); return !std::isspace(ch);
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end()); };
}; s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
trim(arg); s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
if (arg == "on") { };
b->SetSyntaxEnabled(true); trim(arg);
// If no highlighter but filetype is cpp by extension, set it if (arg == "on") {
if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) { b->SetSyntaxEnabled(true);
apply_filetype(*b, b->Filetype().empty() ? std::string("cpp") : b->Filetype()); // If no highlighter but filetype is cpp by extension, set it
} if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
ctx.editor.SetStatus("syntax: on"); apply_filetype(*b, b->Filetype().empty() ? std::string("cpp") : b->Filetype());
} else if (arg == "off") { }
b->SetSyntaxEnabled(false); ctx.editor.SetStatus("syntax: on");
ctx.editor.SetStatus("syntax: off"); } else if (arg == "off") {
} else if (arg == "reload") { b->SetSyntaxEnabled(false);
if (auto *eng = b->Highlighter()) eng->InvalidateFrom(0); ctx.editor.SetStatus("syntax: off");
ctx.editor.SetStatus("syntax: reloaded"); } else if (arg == "reload") {
} else { if (auto *eng = b->Highlighter())
ctx.editor.SetStatus("usage: :syntax on|off|reload"); eng->InvalidateFrom(0);
} ctx.editor.SetStatus("syntax: reloaded");
return true; } else {
ctx.editor.SetStatus("usage: :syntax on|off|reload");
}
return true;
} }
static bool cmd_set_option(CommandContext &ctx)
static bool
cmd_set_option(CommandContext &ctx)
{ {
Buffer *b = ctx.editor.CurrentBuffer(); Buffer *b = ctx.editor.CurrentBuffer();
if (!b) { if (!b) {
ctx.editor.SetStatus("No buffer"); ctx.editor.SetStatus("No buffer");
return true; return true;
} }
// Expect key=value // Expect key=value
auto eq = ctx.arg.find('='); auto eq = ctx.arg.find('=');
if (eq == std::string::npos) { if (eq == std::string::npos) {
ctx.editor.SetStatus("usage: :set key=value"); ctx.editor.SetStatus("usage: :set key=value");
return true; return true;
} }
std::string key = ctx.arg.substr(0, eq); std::string key = ctx.arg.substr(0, eq);
std::string val = ctx.arg.substr(eq + 1); std::string val = ctx.arg.substr(eq + 1);
// trim // trim
auto trim = [](std::string &s){ auto trim = [](std::string &s) {
auto notsp = [](int ch){ return !std::isspace(ch); }; auto notsp = [](int ch) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp)); return !std::isspace(ch);
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end()); };
}; s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
trim(key); trim(val); s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
// lower-case value for filetype };
for (auto &ch: val) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch))); trim(key);
if (key == "filetype") { trim(val);
apply_filetype(*b, val); // lower-case value for filetype
if (b->SyntaxEnabled()) for (auto &ch: val)
ctx.editor.SetStatus(std::string("filetype: ") + (b->Filetype().empty()?"off":b->Filetype())); ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
else if (key == "filetype") {
ctx.editor.SetStatus("filetype: off"); apply_filetype(*b, val);
return true; if (b->SyntaxEnabled())
} ctx.editor.SetStatus(
ctx.editor.SetStatus("unknown option: " + key); std::string("filetype: ") + (b->Filetype().empty() ? "off" : b->Filetype()));
return true; else
ctx.editor.SetStatus("filetype: off");
return true;
}
ctx.editor.SetStatus("unknown option: " + key);
return true;
} }
@@ -907,6 +925,7 @@ cmd_theme_next(CommandContext &ctx)
return true; return true;
} }
static bool static bool
cmd_theme_prev(CommandContext &ctx) cmd_theme_prev(CommandContext &ctx)
{ {
@@ -3734,12 +3753,12 @@ InstallDefaultCommands()
CommandId::ChangeWorkingDirectory, "change-working-directory", "Change current working directory", CommandId::ChangeWorkingDirectory, "change-working-directory", "Change current working directory",
cmd_change_working_directory_start cmd_change_working_directory_start
}); });
// UI helpers // UI helpers
CommandRegistry::Register( CommandRegistry::Register(
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status}); {CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
// Syntax highlighting (public commands) // Syntax highlighting (public commands)
CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true}); CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true});
CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true}); CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true});
} }

View File

@@ -94,10 +94,10 @@ enum class CommandId {
// Theme by name // Theme by name
ThemeSetByName, ThemeSetByName,
// Background mode (GUI) // Background mode (GUI)
BackgroundSet, BackgroundSet,
// Syntax highlighting // Syntax highlighting
Syntax, // ":syntax on|off|reload" Syntax, // ":syntax on|off|reload"
SetOption, // generic ":set key=value" (v1: filetype=<lang>) SetOption, // generic ":set key=value" (v1: filetype=<lang>)
}; };
@@ -151,4 +151,4 @@ bool Execute(Editor &ed, CommandId id, const std::string &arg = std::string(), i
bool Execute(Editor &ed, const std::string &name, 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 #endif // KTE_COMMAND_H

View File

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

View File

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

160
Editor.cc
View File

@@ -3,9 +3,9 @@
#include <filesystem> #include <filesystem>
#include "Editor.h" #include "Editor.h"
#include "HighlighterRegistry.h" #include "syntax/HighlighterRegistry.h"
#include "CppHighlighter.h" #include "syntax/CppHighlighter.h"
#include "NullHighlighter.h" #include "syntax/NullHighlighter.h"
Editor::Editor() = default; Editor::Editor() = default;
@@ -146,72 +146,75 @@ Editor::OpenFile(const std::string &path, std::string &err)
{ {
// If there is exactly one unnamed, empty, clean buffer, reuse it instead // If there is exactly one unnamed, empty, clean buffer, reuse it instead
// of creating a new one. // of creating a new one.
if (buffers_.size() == 1) { if (buffers_.size() == 1) {
Buffer &cur = buffers_[curbuf_]; Buffer &cur = buffers_[curbuf_];
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked(); const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
const bool clean = !cur.Dirty(); const bool clean = !cur.Dirty();
const auto &rows = cur.Rows(); const auto &rows = cur.Rows();
const bool rows_empty = rows.empty(); const bool rows_empty = rows.empty();
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0); const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
if (unnamed && clean && (rows_empty || single_empty_line)) { if (unnamed && clean && (rows_empty || single_empty_line)) {
bool ok = cur.OpenFromFile(path, err); bool ok = cur.OpenFromFile(path, err);
if (!ok) return false; if (!ok)
// Setup highlighting using registry (extension + shebang) return false;
cur.EnsureHighlighter(); // Setup highlighting using registry (extension + shebang)
std::string first = ""; cur.EnsureHighlighter();
const auto &rows = cur.Rows(); std::string first = "";
if (!rows.empty()) first = static_cast<std::string>(rows[0]); const auto &rows = cur.Rows();
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first); if (!rows.empty())
if (!ft.empty()) { first = static_cast<std::string>(rows[0]);
cur.SetFiletype(ft); std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
cur.SetSyntaxEnabled(true); if (!ft.empty()) {
if (auto *eng = cur.Highlighter()) { cur.SetFiletype(ft);
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft)); cur.SetSyntaxEnabled(true);
eng->InvalidateFrom(0); if (auto *eng = cur.Highlighter()) {
} eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
} else { eng->InvalidateFrom(0);
cur.SetFiletype(""); }
cur.SetSyntaxEnabled(true); } else {
if (auto *eng = cur.Highlighter()) { cur.SetFiletype("");
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>()); cur.SetSyntaxEnabled(true);
eng->InvalidateFrom(0); if (auto *eng = cur.Highlighter()) {
} eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
} eng->InvalidateFrom(0);
return true; }
} }
} return true;
}
}
Buffer b; Buffer b;
if (!b.OpenFromFile(path, err)) { if (!b.OpenFromFile(path, err)) {
return false; return false;
} }
// Initialize syntax highlighting by extension + shebang via registry (v2) // Initialize syntax highlighting by extension + shebang via registry (v2)
b.EnsureHighlighter(); b.EnsureHighlighter();
std::string first = ""; std::string first = "";
{ {
const auto &rows = b.Rows(); const auto &rows = b.Rows();
if (!rows.empty()) first = static_cast<std::string>(rows[0]); if (!rows.empty())
} first = static_cast<std::string>(rows[0]);
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first); }
if (!ft.empty()) { std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
b.SetFiletype(ft); if (!ft.empty()) {
b.SetSyntaxEnabled(true); b.SetFiletype(ft);
if (auto *eng = b.Highlighter()) { b.SetSyntaxEnabled(true);
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft)); if (auto *eng = b.Highlighter()) {
eng->InvalidateFrom(0); eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
} eng->InvalidateFrom(0);
} else { }
b.SetFiletype(""); } else {
b.SetSyntaxEnabled(true); b.SetFiletype("");
if (auto *eng = b.Highlighter()) { b.SetSyntaxEnabled(true);
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>()); if (auto *eng = b.Highlighter()) {
eng->InvalidateFrom(0); 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)); // Add as a new buffer and switch to it
SwitchTo(idx); std::size_t idx = AddBuffer(std::move(b));
return true; SwitchTo(idx);
return true;
} }
@@ -222,6 +225,27 @@ Editor::SwitchTo(std::size_t index)
return false; return false;
} }
curbuf_ = index; curbuf_ = index;
// Robustness: ensure a valid highlighter is installed when switching buffers
Buffer &b = buffers_[curbuf_];
if (b.SyntaxEnabled()) {
b.EnsureHighlighter();
if (auto *eng = b.Highlighter()) {
if (!eng->HasHighlighter()) {
// Try to set based on existing filetype; fall back to NullHighlighter
if (!b.Filetype().empty()) {
auto hl = kte::HighlighterRegistry::CreateFor(b.Filetype());
if (hl) {
eng->SetHighlighter(std::move(hl));
} else {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
} else {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
eng->InvalidateFrom(0);
}
}
}
return true; return true;
} }

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
#include <ncurses.h> #include <ncurses.h>
#include <SDL.h> #include <SDL.h>
#include <imgui.h>
#include "GUIInputHandler.h" #include "GUIInputHandler.h"
#include "KKeymap.h" #include "KKeymap.h"
@@ -284,6 +285,14 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
bool produced = false; bool produced = false;
switch (e.type) { switch (e.type) {
case SDL_MOUSEWHEEL: { case SDL_MOUSEWHEEL: {
// If ImGui wants to capture the mouse (e.g., hovering the File Picker list),
// don't translate wheel events into editor scrolling.
// This prevents background buffer scroll while using GUI widgets.
ImGuiIO &io = ImGui::GetIO();
if (io.WantCaptureMouse) {
return true; // consumed by GUI
}
// Map vertical wheel to line-wise cursor movement (MoveUp/MoveDown) // Map vertical wheel to line-wise cursor movement (MoveUp/MoveDown)
int dy = e.wheel.y; int dy = e.wheel.y;
#ifdef SDL_MOUSEWHEEL_FLIPPED #ifdef SDL_MOUSEWHEEL_FLIPPED

View File

@@ -155,6 +155,12 @@ GUIRenderer::Draw(Editor &ed)
last_row = first_row + vis_rows - 1; last_row = first_row + vis_rows - 1;
} }
} }
// Phase 3: prefetch visible viewport highlights and warm around in background
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
int fr = static_cast<int>(std::max(0L, first_row));
int rc = static_cast<int>(std::max(1L, vis_rows));
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
}
} }
// Handle mouse click before rendering to avoid dependent on drawn items // Handle mouse click before rendering to avoid dependent on drawn items
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
@@ -323,50 +329,56 @@ GUIRenderer::Draw(Editor &ed)
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col); ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
} }
} }
// Emit entire line to an expanded buffer (tabs -> spaces) // Emit entire line to an expanded buffer (tabs -> spaces)
for (std::size_t src = 0; src < line.size(); ++src) { for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src]; char c = line[src];
if (c == '\t') { if (c == '\t') {
std::size_t adv = (tabw - (rx_abs_draw % tabw)); std::size_t adv = (tabw - (rx_abs_draw % tabw));
expanded.append(adv, ' '); expanded.append(adv, ' ');
rx_abs_draw += adv; rx_abs_draw += adv;
} else { } else {
expanded.push_back(c); expanded.push_back(c);
rx_abs_draw += 1; rx_abs_draw += 1;
} }
} }
// Draw syntax-colored runs (text above background highlights) // Draw syntax-colored runs (text above background highlights)
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) { if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(*buf, static_cast<int>(i), buf->Version()); const kte::LineHighlight &lh = buf->Highlighter()->GetLine(
// Helper to convert a src column to expanded rx position *buf, static_cast<int>(i), buf->Version());
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t { // Helper to convert a src column to expanded rx position
std::size_t rx = 0; auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) { std::size_t rx = 0;
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1; for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
} rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
return rx; }
}; 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))); for (const auto &sp: lh.spans) {
std::size_t rx_e = src_to_rx_full(static_cast<std::size_t>(std::max(sp.col_start, sp.col_end))); std::size_t rx_s = src_to_rx_full(
if (rx_e <= coloffs_now) static_cast<std::size_t>(std::max(0, sp.col_start)));
continue; std::size_t rx_e = src_to_rx_full(
std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0; static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0; if (rx_e <= coloffs_now)
if (vx0 >= expanded.size()) continue; continue;
vx1 = std::min<std::size_t>(vx1, expanded.size()); std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
if (vx1 <= vx0) continue; std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind)); if (vx0 >= expanded.size())
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y); continue;
ImGui::GetWindowDrawList()->AddText(p, col, expanded.c_str() + vx0, expanded.c_str() + vx1); vx1 = std::min<std::size_t>(vx1, expanded.size());
} if (vx1 <= vx0)
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line. continue;
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + line_h)); ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
} else { ImVec2 p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
// No syntax: draw as one run ImGui::GetWindowDrawList()->AddText(
ImGui::TextUnformatted(expanded.c_str()); p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
} }
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + line_h));
} else {
// No syntax: draw as one run
ImGui::TextUnformatted(expanded.c_str());
}
// Draw a visible cursor indicator on the current line // Draw a visible cursor indicator on the current line
if (i == cy) { if (i == cy) {
@@ -755,4 +767,4 @@ GUIRenderer::Draw(Editor &ed)
ed.SetFilePickerVisible(false); ed.SetFilePickerVisible(false);
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -5,35 +5,33 @@
#include <vector> #include <vector>
namespace kte { namespace kte {
// Token kinds shared between renderers and highlighters // Token kinds shared between renderers and highlighters
enum class TokenKind { enum class TokenKind {
Default, Default,
Keyword, Keyword,
Type, Type,
String, String,
Char, Char,
Comment, Comment,
Number, Number,
Preproc, Preproc,
Constant, Constant,
Function, Function,
Operator, Operator,
Punctuation, Punctuation,
Identifier, Identifier,
Whitespace, Whitespace,
Error Error
}; };
struct HighlightSpan { struct HighlightSpan {
int col_start{0}; // inclusive, 0-based columns in buffer indices int col_start{0}; // inclusive, 0-based columns in buffer indices
int col_end{0}; // exclusive int col_end{0}; // exclusive
TokenKind kind{TokenKind::Default}; TokenKind kind{TokenKind::Default};
}; };
struct LineHighlight { struct LineHighlight {
std::vector<HighlightSpan> spans; std::vector<HighlightSpan> spans;
std::uint64_t version{0}; // buffer version used for this line std::uint64_t version{0}; // buffer version used for this line
}; };
} // namespace kte
} // namespace kte

View File

@@ -1,94 +0,0 @@
#include "HighlighterEngine.h"
#include "Buffer.h"
#include "LanguageHighlighter.h"
namespace kte {
HighlighterEngine::HighlighterEngine() = default;
HighlighterEngine::~HighlighterEngine() = default;
void
HighlighterEngine::SetHighlighter(std::unique_ptr<LanguageHighlighter> hl)
{
hl_ = std::move(hl);
cache_.clear();
state_cache_.clear();
}
const LineHighlight &
HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
{
auto it = cache_.find(row);
if (it != cache_.end()) {
if (it->second.version == buf_version) {
return it->second;
}
}
LineHighlight updated;
updated.version = buf_version;
updated.spans.clear();
if (!hl_) {
auto &slot = cache_[row];
slot = std::move(updated);
return cache_[row];
}
if (auto *stateful = dynamic_cast<StatefulHighlighter *>(hl_.get())) {
// Find nearest cached state at or before row-1 with matching version
StatefulHighlighter::LineState prev_state;
int start_row = -1;
if (!state_cache_.empty()) {
// linear search over map (unordered), track best candidate
int best = -1;
for (const auto &kv : state_cache_) {
int r = kv.first;
if (r <= row - 1 && kv.second.version == buf_version) {
if (r > best) {
best = r;
}
}
}
if (best >= 0) {
start_row = best;
prev_state = state_cache_.at(best).state;
}
}
// Walk from start_row+1 up to row computing states; only collect spans at the target row
for (int r = start_row + 1; r <= row; ++r) {
std::vector<HighlightSpan> tmp;
std::vector<HighlightSpan> &out = (r == row) ? updated.spans : tmp;
auto next_state = stateful->HighlightLineStateful(buf, r, prev_state, out);
// store state for this row (state after finishing r)
StateEntry se;
se.version = buf_version;
se.state = next_state;
state_cache_[r] = se;
prev_state = next_state;
}
} else {
// Stateless path
hl_->HighlightLine(buf, row, updated.spans);
}
auto &slot = cache_[row];
slot = std::move(updated);
return cache_[row];
}
void
HighlighterEngine::InvalidateFrom(int row)
{
if (cache_.empty()) return;
// Simple implementation: erase all rows >= row
for (auto it = cache_.begin(); it != cache_.end(); ) {
if (it->first >= row) it = cache_.erase(it); else ++it;
}
if (!state_cache_.empty()) {
for (auto it = state_cache_.begin(); it != state_cache_.end(); ) {
if (it->first >= row) it = state_cache_.erase(it); else ++it;
}
}
}
} // namespace kte

View File

@@ -1,45 +0,0 @@
// HighlighterEngine.h - caching layer for per-line highlights
#pragma once
#include <cstdint>
#include <memory>
#include <unordered_map>
#include <vector>
#include "Highlight.h"
#include "LanguageHighlighter.h"
class Buffer;
namespace kte {
class HighlighterEngine {
public:
HighlighterEngine();
~HighlighterEngine();
void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl);
// Retrieve highlights for a given line and buffer version.
// If cache is stale, recompute using the current highlighter.
const LineHighlight &GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
// Invalidate cached lines from row (inclusive)
void InvalidateFrom(int row);
bool HasHighlighter() const { return static_cast<bool>(hl_); }
private:
std::unique_ptr<LanguageHighlighter> hl_;
// Simple cache by row index (mutable to allow caching in const GetLine)
mutable std::unordered_map<int, LineHighlight> cache_;
// For stateful highlighters, remember per-line state (state after finishing that row)
struct StateEntry {
std::uint64_t version{0};
// Using the interface type; forward-declare via header
StatefulHighlighter::LineState state;
};
mutable std::unordered_map<int, StateEntry> state_cache_;
};
} // namespace kte

View File

@@ -1,93 +0,0 @@
#include "HighlighterRegistry.h"
#include "CppHighlighter.h"
#include <algorithm>
#include <filesystem>
// Forward declare simple highlighters implemented in this project
namespace kte {
class JSONHighlighter; class MarkdownHighlighter; class ShellHighlighter;
class GoHighlighter; class PythonHighlighter; class RustHighlighter; class LispHighlighter;
}
// Headers for the above
#include "JsonHighlighter.h"
#include "MarkdownHighlighter.h"
#include "ShellHighlighter.h"
#include "GoHighlighter.h"
#include "PythonHighlighter.h"
#include "RustHighlighter.h"
#include "LispHighlighter.h"
namespace kte {
static std::string to_lower(std::string_view s) {
std::string r(s);
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
return r;
}
std::string HighlighterRegistry::Normalize(std::string_view ft)
{
std::string f = to_lower(ft);
if (f == "c" || f == "c++" || f == "cc" || f == "hpp" || f == "hh" || f == "h" || f == "cxx") return "cpp";
if (f == "cpp") return "cpp";
if (f == "json") return "json";
if (f == "markdown" || f == "md" || f == "mkd" || f == "mdown") return "markdown";
if (f == "shell" || f == "sh" || f == "bash" || f == "zsh" || f == "ksh" || f == "fish") return "shell";
if (f == "go" || f == "golang") return "go";
if (f == "py" || f == "python") return "python";
if (f == "rs" || f == "rust") return "rust";
if (f == "lisp" || f == "scheme" || f == "scm" || f == "rkt" || f == "el" || f == "clj" || f == "cljc" || f == "cl") return "lisp";
return f;
}
std::unique_ptr<LanguageHighlighter> HighlighterRegistry::CreateFor(std::string_view filetype)
{
std::string ft = Normalize(filetype);
if (ft == "cpp") return std::make_unique<CppHighlighter>();
if (ft == "json") return std::make_unique<JSONHighlighter>();
if (ft == "markdown") return std::make_unique<MarkdownHighlighter>();
if (ft == "shell") return std::make_unique<ShellHighlighter>();
if (ft == "go") return std::make_unique<GoHighlighter>();
if (ft == "python") return std::make_unique<PythonHighlighter>();
if (ft == "rust") return std::make_unique<RustHighlighter>();
if (ft == "lisp") return std::make_unique<LispHighlighter>();
return nullptr;
}
static std::string shebang_to_ft(std::string_view first_line) {
if (first_line.size() < 2 || first_line.substr(0,2) != "#!") return "";
std::string low = to_lower(first_line);
if (low.find("python") != std::string::npos) return "python";
if (low.find("bash") != std::string::npos) return "shell";
if (low.find("sh") != std::string::npos) return "shell";
if (low.find("zsh") != std::string::npos) return "shell";
if (low.find("fish") != std::string::npos) return "shell";
if (low.find("scheme") != std::string::npos || low.find("racket") != std::string::npos || low.find("guile") != std::string::npos) return "lisp";
return "";
}
std::string HighlighterRegistry::DetectForPath(std::string_view path, std::string_view first_line)
{
// Extension
std::string p(path);
std::error_code ec;
std::string ext = std::filesystem::path(p).extension().string();
for (auto &ch: ext) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
if (!ext.empty()) {
if (ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".cxx" || ext == ".h" || ext == ".hpp" || ext == ".hh") return "cpp";
if (ext == ".json") return "json";
if (ext == ".md" || ext == ".markdown" || ext == ".mkd") return "markdown";
if (ext == ".sh" || ext == ".bash" || ext == ".zsh" || ext == ".ksh" || ext == ".fish") return "shell";
if (ext == ".go") return "go";
if (ext == ".py") return "python";
if (ext == ".rs") return "rust";
if (ext == ".lisp" || ext == ".scm" || ext == ".rkt" || ext == ".el" || ext == ".clj" || ext == ".cljc" || ext == ".cl") return "lisp";
}
// Shebang
std::string ft = shebang_to_ft(first_line);
return ft;
}
} // namespace kte

View File

@@ -1,26 +0,0 @@
// HighlighterRegistry.h - create/detect language highlighters
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include "LanguageHighlighter.h"
namespace kte {
class HighlighterRegistry {
public:
// Create a highlighter for normalized filetype id (e.g., "cpp", "json", "markdown", "shell", "go", "python", "rust", "lisp").
static std::unique_ptr<LanguageHighlighter> CreateFor(std::string_view filetype);
// Detect filetype by path extension and shebang (first line).
// Returns normalized id or empty string if unknown.
static std::string DetectForPath(std::string_view path, std::string_view first_line);
// Normalize various aliases/extensions to canonical ids.
static std::string Normalize(std::string_view ft);
};
} // namespace kte

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,9 +2,11 @@ ROADMAP / TODO:
- [x] Search + Replace - [x] Search + Replace
- [x] Regex search + replace - [x] Regex search + replace
- [ ] The undo system should actually work
- [x] Able to mark buffers as read-only - [x] Able to mark buffers as read-only
- [x] Built-in help text - [x] Built-in help text
- [x] Shorten paths in the homedir with ~ - [x] Shorten paths in the homedir with ~
- [x] When the filename is longer than the message window, scoot left to - [x] When the filename is longer than the message window, scoot left to
keep it in view keep it in view
- [x] Syntax highlighting
- [ ] The undo system should actually work
- [ ] LSP integration

View File

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

View File

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

View File

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

View File

@@ -42,6 +42,13 @@ TerminalRenderer::Draw(Editor &ed)
std::size_t coloffs = buf->Coloffs(); std::size_t coloffs = buf->Coloffs();
const int tabw = 8; const int tabw = 8;
// Phase 3: prefetch visible viewport highlights (current terminal area)
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
int fr = static_cast<int>(rowoffs);
int rc = std::max(0, content_rows);
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
}
for (int r = 0; r < content_rows; ++r) { for (int r = 0; r < content_rows; ++r) {
move(r, 0); move(r, 0);
std::size_t li = rowoffs + static_cast<std::size_t>(r); std::size_t li = rowoffs + static_cast<std::size_t>(r);
@@ -98,49 +105,53 @@ TerminalRenderer::Draw(Editor &ed)
bool hl_on = false; bool hl_on = false;
bool cur_on = false; bool cur_on = false;
int written = 0; int written = 0;
if (li < lines.size()) { if (li < lines.size()) {
std::string line = static_cast<std::string>(lines[li]); std::string line = static_cast<std::string>(lines[li]);
src_i = 0; src_i = 0;
render_col = 0; render_col = 0;
// Syntax highlighting: fetch per-line spans // Syntax highlighting: fetch per-line spans
const kte::LineHighlight *lh_ptr = nullptr; const kte::LineHighlight *lh_ptr = nullptr;
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) { if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
lh_ptr = &buf->Highlighter()->GetLine(*buf, static_cast<int>(li), buf->Version()); HasHighlighter()) {
} lh_ptr = &buf->Highlighter()->GetLine(
auto token_at = [&](std::size_t src_index) -> kte::TokenKind { *buf, static_cast<int>(li), buf->Version());
if (!lh_ptr) return kte::TokenKind::Default; }
for (const auto &sp: lh_ptr->spans) { auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(src_index) < sp.col_end) if (!lh_ptr)
return sp.kind; return kte::TokenKind::Default;
} for (const auto &sp: lh_ptr->spans) {
return kte::TokenKind::Default; if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(
}; src_index) < sp.col_end)
auto apply_token_attr = [&](kte::TokenKind k) { return sp.kind;
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below }
attrset(A_NORMAL); return kte::TokenKind::Default;
switch (k) { };
case kte::TokenKind::Keyword: auto apply_token_attr = [&](kte::TokenKind k) {
case kte::TokenKind::Type: // Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
case kte::TokenKind::Constant: attrset(A_NORMAL);
case kte::TokenKind::Function: switch (k) {
attron(A_BOLD); case kte::TokenKind::Keyword:
break; case kte::TokenKind::Type:
case kte::TokenKind::Comment: case kte::TokenKind::Constant:
attron(A_DIM); case kte::TokenKind::Function:
break; attron(A_BOLD);
case kte::TokenKind::String: break;
case kte::TokenKind::Char: case kte::TokenKind::Comment:
case kte::TokenKind::Number: attron(A_DIM);
// standout a bit using A_UNDERLINE if available break;
attron(A_UNDERLINE); case kte::TokenKind::String:
break; case kte::TokenKind::Char:
default: case kte::TokenKind::Number:
break; // standout a bit using A_UNDERLINE if available
} attron(A_UNDERLINE);
}; break;
while (written < cols) { default:
char ch = ' '; break;
bool from_src = false; }
};
while (written < cols) {
char ch = ' ';
bool from_src = false;
if (src_i < line.size()) { if (src_i < line.size()) {
unsigned char c = static_cast<unsigned char>(line[src_i]); unsigned char c = static_cast<unsigned char>(line[src_i]);
if (c == '\t') { if (c == '\t') {
@@ -159,45 +170,45 @@ TerminalRenderer::Draw(Editor &ed)
next_tab -= to_skip; next_tab -= to_skip;
} }
// Now render visible spaces // Now render visible spaces
while (next_tab > 0 && written < cols) { while (next_tab > 0 && written < cols) {
bool in_hl = search_mode && is_src_in_hl(src_i); bool in_hl = search_mode && is_src_in_hl(src_i);
bool in_cur = bool in_cur =
has_current && li == cur_my && src_i >= cur_mx has_current && li == cur_my && src_i >= cur_mx
&& src_i < cur_mend; && src_i < cur_mend;
// Toggle highlight attributes // Toggle highlight attributes
int attr = 0; int attr = 0;
if (in_hl) if (in_hl)
attr |= A_STANDOUT; attr |= A_STANDOUT;
if (in_cur) if (in_cur)
attr |= A_BOLD; attr |= A_BOLD;
if ((attr & A_STANDOUT) && !hl_on) { if ((attr & A_STANDOUT) && !hl_on) {
attron(A_STANDOUT); attron(A_STANDOUT);
hl_on = true; hl_on = true;
} }
if (!(attr & A_STANDOUT) && hl_on) { if (!(attr & A_STANDOUT) && hl_on) {
attroff(A_STANDOUT); attroff(A_STANDOUT);
hl_on = false; hl_on = false;
} }
if ((attr & A_BOLD) && !cur_on) { if ((attr & A_BOLD) && !cur_on) {
attron(A_BOLD); attron(A_BOLD);
cur_on = true; cur_on = true;
} }
if (!(attr & A_BOLD) && cur_on) { if (!(attr & A_BOLD) && cur_on) {
attroff(A_BOLD); attroff(A_BOLD);
cur_on = false; cur_on = false;
} }
// Apply syntax attribute only if not in search highlight // Apply syntax attribute only if not in search highlight
if (!in_hl) { if (!in_hl) {
apply_token_attr(token_at(src_i)); apply_token_attr(token_at(src_i));
} }
addch(' '); addch(' ');
++written; ++written;
++render_col; ++render_col;
--next_tab; --next_tab;
} }
++src_i; ++src_i;
continue; continue;
} else { } else {
// normal char // normal char
if (render_col < coloffs) { if (render_col < coloffs) {
++render_col; ++render_col;
@@ -212,49 +223,49 @@ TerminalRenderer::Draw(Editor &ed)
ch = ' '; ch = ' ';
from_src = false; from_src = false;
} }
bool in_hl = search_mode && from_src && is_src_in_hl(src_i); bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
bool in_cur = bool in_cur =
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i < has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
cur_mend; cur_mend;
if (in_hl && !hl_on) { if (in_hl && !hl_on) {
attron(A_STANDOUT); attron(A_STANDOUT);
hl_on = true; hl_on = true;
} }
if (!in_hl && hl_on) { if (!in_hl && hl_on) {
attroff(A_STANDOUT); attroff(A_STANDOUT);
hl_on = false; hl_on = false;
} }
if (in_cur && !cur_on) { if (in_cur && !cur_on) {
attron(A_BOLD); attron(A_BOLD);
cur_on = true; cur_on = true;
} }
if (!in_cur && cur_on) { if (!in_cur && cur_on) {
attroff(A_BOLD); attroff(A_BOLD);
cur_on = false; cur_on = false;
} }
if (!in_hl && from_src) { if (!in_hl && from_src) {
apply_token_attr(token_at(src_i)); apply_token_attr(token_at(src_i));
} }
addch(static_cast<unsigned char>(ch)); addch(static_cast<unsigned char>(ch));
++written; ++written;
++render_col; ++render_col;
if (from_src) if (from_src)
++src_i; ++src_i;
if (src_i >= line.size() && written >= cols) if (src_i >= line.size() && written >= cols)
break; break;
} }
} }
if (hl_on) { if (hl_on) {
attroff(A_STANDOUT); attroff(A_STANDOUT);
hl_on = false; hl_on = false;
} }
if (cur_on) { if (cur_on) {
attroff(A_BOLD); attroff(A_BOLD);
cur_on = false; cur_on = false;
} }
attrset(A_NORMAL); attrset(A_NORMAL);
clrtoeol(); clrtoeol();
} }
// Place terminal cursor at logical position accounting for tabs and coloffs // Place terminal cursor at logical position accounting for tabs and coloffs
std::size_t cy = buf->Cury(); std::size_t cy = buf->Cury();
@@ -457,4 +468,4 @@ TerminalRenderer::Draw(Editor &ed)
} }
refresh(); refresh();
} }

525
docs/lsp plan.md Normal file
View File

@@ -0,0 +1,525 @@
# LSP Support Implementation Plan for kte
## Executive Summary
This plan outlines a comprehensive approach to integrating Language Server Protocol (LSP) support into kte while
respecting its core architectural principles: **frontend/backend separation**, **testability**, and **dual terminal/GUI
support**.
---
## 1. Core Architecture
### 1.1 LSP Client Module Structure
```c++
// LspClient.h - Core LSP client abstraction
class LspClient {
public:
virtual ~LspClient() = default;
// Lifecycle
virtual bool initialize(const std::string& rootPath) = 0;
virtual void shutdown() = 0;
// Document Synchronization
virtual void didOpen(const std::string& uri, const std::string& languageId,
int version, const std::string& text) = 0;
virtual void didChange(const std::string& uri, int version,
const std::vector<TextDocumentContentChangeEvent>& changes) = 0;
virtual void didClose(const std::string& uri) = 0;
virtual void didSave(const std::string& uri) = 0;
// Language Features
virtual void completion(const std::string& uri, Position pos,
CompletionCallback callback) = 0;
virtual void hover(const std::string& uri, Position pos,
HoverCallback callback) = 0;
virtual void definition(const std::string& uri, Position pos,
LocationCallback callback) = 0;
virtual void references(const std::string& uri, Position pos,
LocationsCallback callback) = 0;
virtual void diagnostics(DiagnosticsCallback callback) = 0;
// Process Management
virtual bool isRunning() const = 0;
virtual std::string getServerName() const = 0;
};
```
### 1.2 Process-based LSP Implementation
```c++
// LspProcessClient.h - Manages LSP server subprocess
class LspProcessClient : public LspClient {
private:
std::string serverCommand_;
std::vector<std::string> serverArgs_;
std::unique_ptr<Process> process_;
std::unique_ptr<JsonRpcTransport> transport_;
std::unordered_map<int, PendingRequest> pendingRequests_;
int nextRequestId_ = 1;
// Async I/O handling
std::thread readerThread_;
std::mutex mutex_;
std::condition_variable cv_;
public:
LspProcessClient(const std::string& command,
const std::vector<std::string>& args);
// ... implementation of LspClient interface
};
```
### 1.3 JSON-RPC Transport Layer
```c++
// JsonRpcTransport.h
class JsonRpcTransport {
public:
// Send a request and get the request ID
int sendRequest(const std::string& method, const nlohmann::json& params);
// Send a notification (no response expected)
void sendNotification(const std::string& method, const nlohmann::json& params);
// Read next message (blocking)
std::optional<JsonRpcMessage> readMessage();
private:
void writeMessage(const nlohmann::json& message);
std::string readContentLength();
int fdIn_; // stdin to server
int fdOut_; // stdout from server
};
```
---
## 2. Incremental Document Updates
### 2.1 Change Tracking in Buffer
The key to efficient LSP integration is tracking changes incrementally. This integrates with the existing `Buffer`
class:
```c++
// TextDocumentContentChangeEvent.h
struct TextDocumentContentChangeEvent {
std::optional<Range> range; // If nullopt, entire document changed
std::optional<int> rangeLength; // Deprecated but some servers use it
std::string text;
};
// BufferChangeTracker.h - Integrates with Buffer to track changes
class BufferChangeTracker {
public:
explicit BufferChangeTracker(Buffer* buffer);
// Called by Buffer on each edit operation
void recordInsertion(Position pos, const std::string& text);
void recordDeletion(Range range, const std::string& deletedText);
// Get accumulated changes since last sync
std::vector<TextDocumentContentChangeEvent> getChanges();
// Clear changes after sending to LSP
void clearChanges();
// Get current document version
int getVersion() const { return version_; }
private:
Buffer* buffer_;
int version_ = 0;
std::vector<TextDocumentContentChangeEvent> pendingChanges_;
// Optional: Coalesce adjacent changes
void coalesceChanges();
};
```
### 2.2 Integration with Buffer Operations
```c++
// Buffer.h additions
class Buffer {
// ... existing code ...
// LSP integration
void setChangeTracker(std::unique_ptr<BufferChangeTracker> tracker);
BufferChangeTracker* getChangeTracker() { return changeTracker_.get(); }
// These methods should call tracker when present
void insertText(Position pos, const std::string& text);
void deleteRange(Range range);
private:
std::unique_ptr<BufferChangeTracker> changeTracker_;
};
```
### 2.3 Sync Strategy Selection
```c++
// LspSyncMode.h
enum class LspSyncMode {
None, // No sync
Full, // Send full document on each change
Incremental // Send only changes (preferred)
};
// Determined during server capability negotiation
LspSyncMode negotiateSyncMode(const ServerCapabilities& caps);
```
---
## 3. Diagnostics Display System
### 3.1 Diagnostic Data Model
```c++
// Diagnostic.h
enum class DiagnosticSeverity {
Error = 1,
Warning = 2,
Information = 3,
Hint = 4
};
struct Diagnostic {
Range range;
DiagnosticSeverity severity;
std::optional<std::string> code;
std::optional<std::string> source;
std::string message;
std::vector<DiagnosticRelatedInformation> relatedInfo;
};
// DiagnosticStore.h - Central storage for diagnostics
class DiagnosticStore {
public:
void setDiagnostics(const std::string& uri,
std::vector<Diagnostic> diagnostics);
const std::vector<Diagnostic>& getDiagnostics(const std::string& uri) const;
std::vector<Diagnostic> getDiagnosticsAtLine(const std::string& uri,
int line) const;
std::optional<Diagnostic> getDiagnosticAtPosition(const std::string& uri,
Position pos) const;
void clear(const std::string& uri);
void clearAll();
// Statistics
int getErrorCount(const std::string& uri) const;
int getWarningCount(const std::string& uri) const;
private:
std::unordered_map<std::string, std::vector<Diagnostic>> diagnostics_;
};
```
### 3.2 Frontend-Agnostic Diagnostic Display Interface
Following kte's existing abstraction pattern with `Frontend`, `Renderer`, and `InputHandler`:
```c++
// DiagnosticDisplay.h - Abstract interface for showing diagnostics
class DiagnosticDisplay {
public:
virtual ~DiagnosticDisplay() = default;
// Update the diagnostic indicators for a buffer
virtual void updateDiagnostics(const std::string& uri,
const std::vector<Diagnostic>& diagnostics) = 0;
// Show inline diagnostic at cursor position
virtual void showInlineDiagnostic(const Diagnostic& diagnostic) = 0;
// Show diagnostic list/panel
virtual void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) = 0;
virtual void hideDiagnosticList() = 0;
// Status bar summary
virtual void updateStatusBar(int errorCount, int warningCount) = 0;
};
```
### 3.3 Terminal Diagnostic Display
```c++
// TerminalDiagnosticDisplay.h
class TerminalDiagnosticDisplay : public DiagnosticDisplay {
public:
explicit TerminalDiagnosticDisplay(TerminalRenderer* renderer);
void updateDiagnostics(const std::string& uri,
const std::vector<Diagnostic>& diagnostics) override;
void showInlineDiagnostic(const Diagnostic& diagnostic) override;
void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) override;
void hideDiagnosticList() override;
void updateStatusBar(int errorCount, int warningCount) override;
private:
TerminalRenderer* renderer_;
// Terminal-specific display strategies
void renderGutterMarkers(const std::vector<Diagnostic>& diagnostics);
void renderUnderlines(const std::vector<Diagnostic>& diagnostics);
void renderVirtualText(const Diagnostic& diagnostic);
};
```
**Terminal Display Strategies:**
1. **Gutter markers**: Show `E` (error), `W` (warning), `I` (info), `H` (hint) in left gutter
2. **Underlines**: Use terminal underline/curly underline capabilities (where supported)
3. **Virtual text**: Display diagnostic message at end of line (configurable)
4. **Status line**: `[E:3 W:5]` summary
5. **Message line**: Full diagnostic on cursor line shown in bottom bar
```
1 │ fn main() {
E 2 │ let x: i32 = "hello";
3 │ }
──────────────────────────────────────
error[E0308]: mismatched types
expected `i32`, found `&str`
[E:1 W:0] main.rs
```
### 3.4 GUI Diagnostic Display
```c++
// GUIDiagnosticDisplay.h
class GUIDiagnosticDisplay : public DiagnosticDisplay {
public:
explicit GUIDiagnosticDisplay(GUIRenderer* renderer, GUITheme* theme);
void updateDiagnostics(const std::string& uri,
const std::vector<Diagnostic>& diagnostics) override;
void showInlineDiagnostic(const Diagnostic& diagnostic) override;
void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) override;
void hideDiagnosticList() override;
void updateStatusBar(int errorCount, int warningCount) override;
private:
GUIRenderer* renderer_;
GUITheme* theme_;
// GUI-specific display
void renderWavyUnderlines(const std::vector<Diagnostic>& diagnostics);
void renderTooltip(Position pos, const Diagnostic& diagnostic);
void renderDiagnosticPanel();
};
```
**GUI Display Features:**
1. **Wavy underlines**: Classic IDE-style (red for errors, yellow for warnings, etc.)
2. **Gutter icons**: Colored icons/dots in the gutter
3. **Hover tooltips**: Rich tooltips on hover showing full diagnostic
4. **Diagnostic panel**: Bottom panel with clickable diagnostic list
5. **Minimap markers**: Colored marks on the minimap (if present)
---
## 4. LspManager - Central Coordination
```c++
// LspManager.h
class LspManager {
public:
explicit LspManager(Editor* editor, DiagnosticDisplay* display);
// Server management
void registerServer(const std::string& languageId,
const LspServerConfig& config);
bool startServerForBuffer(Buffer* buffer);
void stopServer(const std::string& languageId);
void stopAllServers();
// Document sync
void onBufferOpened(Buffer* buffer);
void onBufferChanged(Buffer* buffer);
void onBufferClosed(Buffer* buffer);
void onBufferSaved(Buffer* buffer);
// Feature requests
void requestCompletion(Buffer* buffer, Position pos,
CompletionCallback callback);
void requestHover(Buffer* buffer, Position pos,
HoverCallback callback);
void requestDefinition(Buffer* buffer, Position pos,
LocationCallback callback);
// Configuration
void setDebugLogging(bool enabled);
private:
Editor* editor_;
DiagnosticDisplay* display_;
DiagnosticStore diagnosticStore_;
std::unordered_map<std::string, std::unique_ptr<LspClient>> servers_;
std::unordered_map<std::string, LspServerConfig> serverConfigs_;
void handleDiagnostics(const std::string& uri,
const std::vector<Diagnostic>& diagnostics);
std::string getLanguageId(Buffer* buffer);
std::string getUri(Buffer* buffer);
};
```
---
## 5. Configuration
```c++
// LspServerConfig.h
struct LspServerConfig {
std::string command;
std::vector<std::string> args;
std::vector<std::string> filePatterns; // e.g., {"*.rs", "*.toml"}
std::string rootPatterns; // e.g., "Cargo.toml"
LspSyncMode preferredSyncMode = LspSyncMode::Incremental;
bool autostart = true;
std::unordered_map<std::string, nlohmann::json> initializationOptions;
std::unordered_map<std::string, nlohmann::json> settings;
};
// Default configurations
std::vector<LspServerConfig> getDefaultServerConfigs() {
return {
{
.command = "rust-analyzer",
.filePatterns = {"*.rs"},
.rootPatterns = "Cargo.toml"
},
{
.command = "clangd",
.args = {"--background-index"},
.filePatterns = {"*.c", "*.cc", "*.cpp", "*.h", "*.hpp"},
.rootPatterns = "compile_commands.json"
},
{
.command = "gopls",
.filePatterns = {"*.go"},
.rootPatterns = "go.mod"
},
// ... more servers
};
}
```
---
## 6. Implementation Phases
### Phase 1: Foundation (2-3 weeks)
- [ ] JSON-RPC transport layer
- [ ] Process management for LSP servers
- [ ] Basic `LspClient` with initialize/shutdown
- [ ] `textDocument/didOpen`, `textDocument/didClose` (full sync)
### Phase 2: Incremental Sync (1-2 weeks)
- [ ] `BufferChangeTracker` integration with `Buffer`
- [ ] `textDocument/didChange` with incremental updates
- [ ] Change coalescing for rapid edits
- [ ] Version tracking
### Phase 3: Diagnostics (2-3 weeks)
- [ ] `DiagnosticStore` implementation
- [ ] `TerminalDiagnosticDisplay` with gutter markers & status line
- [ ] `GUIDiagnosticDisplay` with wavy underlines & tooltips
- [ ] `textDocument/publishDiagnostics` handling
### Phase 4: Language Features (3-4 weeks)
- [ ] Completion (`textDocument/completion`)
- [ ] Hover (`textDocument/hover`)
- [ ] Go to definition (`textDocument/definition`)
- [ ] Find references (`textDocument/references`)
- [ ] Code actions (`textDocument/codeAction`)
### Phase 5: Polish & Advanced Features (2-3 weeks)
- [ ] Multiple server support
- [ ] Server auto-detection
- [ ] Configuration file support
- [ ] Workspace symbol search
- [ ] Rename refactoring
---
## 7. Alignment with kte Core Principles
### 7.1 Frontend/Backend Separation
- LSP logic is completely separate from display
- `DiagnosticDisplay` interface allows identical behavior across Terminal/GUI
- Follows existing pattern: `Renderer`, `InputHandler`, `Frontend`
### 7.2 Testability
- `LspClient` is abstract, enabling `MockLspClient` for testing
- `DiagnosticDisplay` can be mocked for testing diagnostic flow
- Change tracking can be unit tested in isolation
### 7.3 Performance
- Incremental sync minimizes data sent to LSP servers
- Async message handling doesn't block UI
- Diagnostic rendering is batched
### 7.4 Simplicity
- Minimal dependencies (nlohmann/json for JSON handling)
- Self-contained process management
- Clear separation of concerns
---
## 8. File Organization
```
kte/
├── lsp/
│ ├── LspClient.h
│ ├── LspProcessClient.h
│ ├── LspProcessClient.cc
│ ├── LspManager.h
│ ├── LspManager.cc
│ ├── LspServerConfig.h
│ ├── JsonRpcTransport.h
│ ├── JsonRpcTransport.cc
│ ├── LspTypes.h # Position, Range, Location, etc.
│ ├── Diagnostic.h
│ ├── DiagnosticStore.h
│ ├── DiagnosticStore.cc
│ └── BufferChangeTracker.h
├── diagnostic/
│ ├── DiagnosticDisplay.h
│ ├── TerminalDiagnosticDisplay.h
│ ├── TerminalDiagnosticDisplay.cc
│ ├── GUIDiagnosticDisplay.h
│ └── GUIDiagnosticDisplay.cc
```
---
## 9. Dependencies
- **nlohmann/json**: JSON parsing/serialization (header-only)
- **POSIX/Windows process APIs**: For spawning LSP servers
- Existing kte infrastructure: `Buffer`, `Renderer`, `Frontend`, etc.
---
This plan provides a solid foundation for LSP support while maintaining kte's clean architecture. The key insight is
that LSP is fundamentally a backend feature that should be displayed through the existing frontend abstraction layer,
ensuring consistent behavior across terminal and GUI modes.

View File

@@ -50,3 +50,21 @@ Renderer integration
- Terminal and GUI renderers request line spans via `Highlighter()->GetLine(buf, row, buf.Version())`. - Terminal and GUI renderers request line spans via `Highlighter()->GetLine(buf, row, buf.Version())`.
- Search highlight and cursor overlays take precedence over syntax colors. - Search highlight and cursor overlays take precedence over syntax colors.
Extensibility (Phase 4)
-----------------------
- Public registration API: external code can register custom highlighters by filetype.
- Use `HighlighterRegistry::Register("mylang", []{ return std::make_unique<MyHighlighter>(); });`
- Registered factories are preferred over built-ins for the same filetype key.
- Filetype keys are normalized via `HighlighterRegistry::Normalize()`.
- Optional Tree-sitter adapter: disabled by default to keep dependencies minimal.
- Enable with CMake option `-DKTE_ENABLE_TREESITTER=ON` and provide
`-DTREESITTER_INCLUDE_DIR=...` and `-DTREESITTER_LIBRARY=...` if needed.
- Register a Tree-sitter-backed highlighter for a language (example assumes you link a grammar):
```c++
extern "C" const TSLanguage* tree_sitter_c();
kte::HighlighterRegistry::RegisterTreeSitter("c", &tree_sitter_c);
```
- Current adapter is a stub scaffold; it compiles and integrates cleanly when enabled, but
intentionally emits no spans until Tree-sitter node-to-token mapping is implemented.

279
syntax/CppHighlighter.cc Normal file
View File

@@ -0,0 +1,279 @@
#include "CppHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static bool
is_digit(char c)
{
return c >= '0' && c <= '9';
}
CppHighlighter::CppHighlighter()
{
const char *kw[] = {
"if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue",
"return", "goto", "struct", "class", "namespace", "using", "template", "typename",
"public", "private", "protected", "virtual", "override", "const", "constexpr", "auto",
"static", "inline", "operator", "new", "delete", "try", "catch", "throw", "friend",
"enum", "union", "extern", "volatile", "mutable", "noexcept", "sizeof", "this"
};
for (auto s: kw)
keywords_.insert(s);
const char *types[] = {
"int", "long", "short", "char", "signed", "unsigned", "float", "double", "void",
"bool", "wchar_t", "size_t", "ptrdiff_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t",
"int8_t", "int16_t", "int32_t", "int64_t"
};
for (auto s: types)
types_.insert(s);
}
bool
CppHighlighter::is_ident_start(char c)
{
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
}
bool
CppHighlighter::is_ident_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
}
void
CppHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
// Stateless entry simply delegates to stateful with a clean previous state
StatefulHighlighter::LineState prev;
(void) HighlightLineStateful(buf, row, prev, out);
}
StatefulHighlighter::LineState
CppHighlighter::HighlightLineStateful(const Buffer &buf,
int row,
const LineState &prev,
std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
StatefulHighlighter::LineState state = prev;
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return state;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
if (s.empty())
return state;
auto push = [&](int a, int b, TokenKind k) {
if (b > a)
out.push_back({a, b, k});
};
int n = static_cast<int>(s.size());
int bol = 0;
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
++bol;
int i = 0;
// Continue multi-line raw string from previous line
if (state.in_raw_string) {
std::string needle = ")" + state.raw_delim + "\"";
auto pos = s.find(needle);
if (pos == std::string::npos) {
push(0, n, TokenKind::String);
state.in_raw_string = true;
return state;
} else {
int end = static_cast<int>(pos + needle.size());
push(0, end, TokenKind::String);
i = end;
state.in_raw_string = false;
state.raw_delim.clear();
}
}
// Continue multi-line block comment from previous line
if (state.in_block_comment) {
int j = i;
while (i + 1 < n) {
if (s[i] == '*' && s[i + 1] == '/') {
i += 2;
push(j, i, TokenKind::Comment);
state.in_block_comment = false;
break;
}
++i;
}
if (state.in_block_comment) {
push(j, n, TokenKind::Comment);
return state;
}
}
while (i < n) {
char c = s[i];
// Preprocessor at beginning of line (after leading whitespace)
if (i == bol && c == '#') {
push(0, n, TokenKind::Preproc);
break;
}
// Whitespace
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(i, j, TokenKind::Whitespace);
i = j;
continue;
}
// Line comment
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
push(i, n, TokenKind::Comment);
break;
}
// Block comment
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
int j = i + 2;
bool closed = false;
while (j + 1 <= n) {
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
j += 2;
closed = true;
break;
}
++j;
}
if (closed) {
push(i, j, TokenKind::Comment);
i = j;
continue;
}
// Spill to next lines
push(i, n, TokenKind::Comment);
state.in_block_comment = true;
return state;
}
// Raw string start: very simple detection: R"delim(
if (c == 'R' && i + 1 < n && s[i + 1] == '"') {
int k = i + 2;
std::string delim;
while (k < n && s[k] != '(') {
delim.push_back(s[k]);
++k;
}
if (k < n && s[k] == '(') {
int body_start = k + 1;
std::string needle = ")" + delim + "\"";
auto pos = s.find(needle, static_cast<std::size_t>(body_start));
if (pos == std::string::npos) {
push(i, n, TokenKind::String);
state.in_raw_string = true;
state.raw_delim = delim;
return state;
} else {
int end = static_cast<int>(pos + needle.size());
push(i, end, TokenKind::String);
i = end;
continue;
}
}
// If malformed, just treat 'R' as identifier fallback
}
// Regular string literal
if (c == '"') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
}
push(i, j, TokenKind::String);
i = j;
continue;
}
// Char literal
if (c == '\'') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '\'')
break;
}
push(i, j, TokenKind::Char);
i = j;
continue;
}
// Number literal (simple)
if (is_digit(c) || (c == '.' && i + 1 < n && is_digit(s[i + 1]))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'x' ||
s[j] == 'X' || s[j] == 'b' || s[j] == 'B' || s[j] == '_'))
++j;
push(i, j, TokenKind::Number);
i = j;
continue;
}
// Identifier / keyword / type
if (is_ident_start(c)) {
int j = i + 1;
while (j < n && is_ident_char(s[j]))
++j;
std::string id = s.substr(i, j - i);
TokenKind k = TokenKind::Identifier;
if (keywords_.count(id))
k = TokenKind::Keyword;
else if (types_.count(id))
k = TokenKind::Type;
push(i, j, k);
i = j;
continue;
}
// Operators and punctuation (single char for now)
TokenKind kind = TokenKind::Operator;
if (std::ispunct(static_cast<unsigned char>(c)) && c != '_' && c != '#') {
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
']')
kind = TokenKind::Punctuation;
push(i, i + 1, kind);
++i;
continue;
}
// Fallback
push(i, i + 1, TokenKind::Default);
++i;
}
return state;
}
} // namespace kte

35
syntax/CppHighlighter.h Normal file
View File

@@ -0,0 +1,35 @@
// CppHighlighter.h - minimal stateless C/C++ line highlighter
#pragma once
#include <regex>
#include <string>
#include <unordered_set>
#include <vector>
#include "LanguageHighlighter.h"
class Buffer;
namespace kte {
class CppHighlighter final : public StatefulHighlighter {
public:
CppHighlighter();
~CppHighlighter() override = default;
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
LineState HighlightLineStateful(const Buffer &buf,
int row,
const LineState &prev,
std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> keywords_;
std::unordered_set<std::string> types_;
static bool is_ident_start(char c);
static bool is_ident_char(char c);
};
} // namespace kte

159
syntax/ErlangHighlighter.cc Normal file
View File

@@ -0,0 +1,159 @@
#include "ErlangHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
static bool
is_ident_start(char c)
{
return std::isalpha(static_cast<unsigned char>(c)) || c == '_' || c == '\'';
}
static bool
is_ident_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '@' || c == ':' || c == '?';
}
ErlangHighlighter::ErlangHighlighter()
{
const char *kw[] = {
"after", "begin", "case", "catch", "cond", "div", "end", "fun", "if", "let", "of",
"receive", "when", "try", "rem", "and", "andalso", "orelse", "not", "band", "bor", "bxor",
"bnot", "xor", "module", "export", "import", "record", "define", "undef", "include", "include_lib"
};
for (auto s: kw)
kws_.insert(s);
}
void
ErlangHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
// comment
if (c == '%') {
push(out, i, n, TokenKind::Comment);
break;
}
// strings
if (c == '"') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
// char literal $X
if (c == '$') {
int j = i + 1;
if (j < n && s[j] == '\\' && j + 1 < n)
j += 2;
else if (j < n)
++j;
push(out, i, j, TokenKind::Char);
i = j;
continue;
}
// numbers
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '#' || s[j] == '.' ||
s[j] == '_'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
// atoms/variables/identifiers (including quoted atoms)
if (is_ident_start(c)) {
// quoted atom: '...'
if (c == '\'') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (d == '\'') {
if (j < n && s[j] == '\'') {
++j;
continue;
}
break;
}
if (d == '\\')
esc = !esc;
}
push(out, i, j, TokenKind::Identifier);
i = j;
continue;
}
int j = i + 1;
while (j < n && is_ident_char(s[j]))
++j;
std::string id = s.substr(i, j - i);
// lowercase leading -> atom/function/module; uppercase or '_' -> variable
TokenKind k = TokenKind::Identifier;
// keyword check (lowercase)
std::string lower;
lower.reserve(id.size());
for (char ch: id)
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
if (kws_.count(lower))
k = TokenKind::Keyword;
push(out, i, j, k);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Operator;
if (c == ',' || c == ';' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c ==
'}')
k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

View File

@@ -0,0 +1,17 @@
// ErlangHighlighter.h - simple Erlang highlighter
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class ErlangHighlighter final : public LanguageHighlighter {
public:
ErlangHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
};
} // namespace kte

121
syntax/ForthHighlighter.cc Normal file
View File

@@ -0,0 +1,121 @@
#include "ForthHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
static bool
is_word_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '>' || c == '<' || c == '?';
}
ForthHighlighter::ForthHighlighter()
{
const char *kw[] = {
":", ";", "if", "else", "then", "begin", "until", "while", "repeat",
"do", "loop", "+loop", "leave", "again", "case", "of", "endof", "endcase",
".", ".r", ".s", ".\"", ",", "cr", "emit", "type", "key",
"+", "-", "*", "/", "mod", "/mod", "+-", "abs", "min", "max",
"dup", "drop", "swap", "over", "rot", "-rot", "nip", "tuck", "pick", "roll",
"and", "or", "xor", "invert", "lshift", "rshift",
"variable", "constant", "value", "to", "create", "does>", "allot", ",",
"cells", "cell+", "chars", "char+",
"[", "]", "immediate",
"s\"", ".\""
};
for (auto s: kw)
kws_.insert(s);
}
void
ForthHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
// backslash comment to end of line
if (c == '\\') {
push(out, i, n, TokenKind::Comment);
break;
}
// parenthesis comment ( ... ) if at word boundary
if (c == '(') {
int j = i + 1;
while (j < n && s[j] != ')')
++j;
if (j < n)
++j;
push(out, i, j, TokenKind::Comment);
i = j;
continue;
}
// strings: ." ... " and S" ... " and raw "..."
if (c == '"') {
int j = i + 1;
while (j < n && s[j] != '"')
++j;
if (j < n)
++j;
push(out, i, j, TokenKind::String);
i = j;
continue;
}
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '#'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
// word/identifier
if (std::isalpha(static_cast<unsigned char>(c)) || std::ispunct(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && is_word_char(s[j]))
++j;
std::string w = s.substr(i, j - i);
// normalize to lowercase for keyword compare (Forth is case-insensitive typically)
std::string lower;
lower.reserve(w.size());
for (char ch: w)
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
TokenKind k = kws_.count(lower) ? TokenKind::Keyword : TokenKind::Identifier;
// Single-char punctuation fallback
if (w.size() == 1 && std::ispunct(static_cast<unsigned char>(w[0])) && !kws_.count(lower)) {
k = (w[0] == '(' || w[0] == ')' || w[0] == ',')
? TokenKind::Punctuation
: TokenKind::Operator;
}
push(out, i, j, k);
i = j;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

17
syntax/ForthHighlighter.h Normal file
View File

@@ -0,0 +1,17 @@
// ForthHighlighter.h - simple Forth highlighter
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class ForthHighlighter final : public LanguageHighlighter {
public:
ForthHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
};
} // namespace kte

157
syntax/GoHighlighter.cc Normal file
View File

@@ -0,0 +1,157 @@
#include "GoHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
static bool
is_ident_start(char c)
{
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
}
static bool
is_ident_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
}
GoHighlighter::GoHighlighter()
{
const char *kw[] = {
"break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", "func",
"go", "goto", "if", "import", "interface", "map", "package", "range", "return", "select", "struct",
"switch", "type", "var"
};
for (auto s: kw)
kws_.insert(s);
const char *tp[] = {
"bool", "byte", "complex64", "complex128", "error", "float32", "float64", "int", "int8", "int16",
"int32", "int64", "rune", "string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr"
};
for (auto s: tp)
types_.insert(s);
}
void
GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
int bol = 0;
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
++bol;
// line comment
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
push(out, i, n, TokenKind::Comment);
break;
}
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
int j = i + 2;
bool closed = false;
while (j + 1 <= n) {
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
j += 2;
closed = true;
break;
}
++j;
}
if (!closed) {
push(out, i, n, TokenKind::Comment);
break;
} else {
push(out, i, j, TokenKind::Comment);
i = j;
continue;
}
}
if (c == '"' || c == '`') {
char q = c;
int j = i + 1;
bool esc = false;
if (q == '`') {
while (j < n && s[j] != '`')
++j;
if (j < n)
++j;
} else {
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
}
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'x' ||
s[j] == 'X' || s[j] == '_'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
if (is_ident_start(c)) {
int j = i + 1;
while (j < n && is_ident_char(s[j]))
++j;
std::string id = s.substr(i, j - i);
TokenKind k = TokenKind::Identifier;
if (kws_.count(id))
k = TokenKind::Keyword;
else if (types_.count(id))
k = TokenKind::Type;
push(out, i, j, k);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Operator;
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
']')
k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

18
syntax/GoHighlighter.h Normal file
View File

@@ -0,0 +1,18 @@
// GoHighlighter.h - simple Go highlighter
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class GoHighlighter final : public LanguageHighlighter {
public:
GoHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
std::unordered_set<std::string> types_;
};
} // namespace kte

209
syntax/HighlighterEngine.cc Normal file
View File

@@ -0,0 +1,209 @@
#include "HighlighterEngine.h"
#include "../Buffer.h"
#include "LanguageHighlighter.h"
#include <thread>
namespace kte {
HighlighterEngine::HighlighterEngine() = default;
HighlighterEngine::~HighlighterEngine()
{
// stop background worker
if (worker_running_.load()) {
{
std::lock_guard<std::mutex> lock(mtx_);
worker_running_.store(false);
has_request_ = true; // wake it up to exit
}
cv_.notify_one();
if (worker_.joinable())
worker_.join();
}
}
void
HighlighterEngine::SetHighlighter(std::unique_ptr<LanguageHighlighter> hl)
{
std::lock_guard<std::mutex> lock(mtx_);
hl_ = std::move(hl);
cache_.clear();
state_cache_.clear();
state_last_contig_.clear();
}
const LineHighlight &
HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
{
std::unique_lock<std::mutex> lock(mtx_);
auto it = cache_.find(row);
if (it != cache_.end() && it->second.version == buf_version) {
return it->second;
}
// Prepare destination slot to reuse its capacity and avoid allocations
LineHighlight &slot = cache_[row];
slot.version = buf_version;
slot.spans.clear();
if (!hl_) {
return slot;
}
// Copy shared_ptr-like raw pointer for use outside critical sections
LanguageHighlighter *hl_ptr = hl_.get();
bool is_stateful = dynamic_cast<StatefulHighlighter *>(hl_ptr) != nullptr;
if (!is_stateful) {
// Stateless fast path: we can release the lock while computing to reduce contention
auto &out = slot.spans;
lock.unlock();
hl_ptr->HighlightLine(buf, row, out);
return cache_.at(row);
}
// Stateful path: we need to walk from a known previous state. Keep lock while consulting caches,
// but release during heavy computation.
auto *stateful = static_cast<StatefulHighlighter *>(hl_ptr);
StatefulHighlighter::LineState prev_state;
int start_row = -1;
if (!state_cache_.empty()) {
// linear search over map (unordered), track best candidate
int best = -1;
for (const auto &kv: state_cache_) {
int r = kv.first;
if (r <= row - 1 && kv.second.version == buf_version) {
if (r > best)
best = r;
}
}
if (best >= 0) {
start_row = best;
prev_state = state_cache_.at(best).state;
}
}
// We'll compute states and the target line's spans without holding the lock for most of the work.
// Create a local copy of prev_state and iterate rows; we will update caches under lock.
lock.unlock();
StatefulHighlighter::LineState cur_state = prev_state;
for (int r = start_row + 1; r <= row; ++r) {
std::vector<HighlightSpan> tmp;
std::vector<HighlightSpan> &out = (r == row) ? slot.spans : tmp;
auto next_state = stateful->HighlightLineStateful(buf, r, cur_state, out);
// Update state cache for r
std::lock_guard<std::mutex> gl(mtx_);
StateEntry se;
se.version = buf_version;
se.state = next_state;
state_cache_[r] = se;
cur_state = next_state;
}
// Return reference under lock to ensure slot's address stability in map
lock.lock();
return cache_.at(row);
}
void
HighlighterEngine::InvalidateFrom(int row)
{
std::lock_guard<std::mutex> lock(mtx_);
if (cache_.empty())
return;
// Simple implementation: erase all rows >= row
for (auto it = cache_.begin(); it != cache_.end();) {
if (it->first >= row)
it = cache_.erase(it);
else
++it;
}
if (!state_cache_.empty()) {
for (auto it = state_cache_.begin(); it != state_cache_.end();) {
if (it->first >= row)
it = state_cache_.erase(it);
else
++it;
}
}
}
void
HighlighterEngine::ensure_worker_started() const
{
if (worker_running_.load())
return;
worker_running_.store(true);
worker_ = std::thread([this]() {
this->worker_loop();
});
}
void
HighlighterEngine::worker_loop() const
{
std::unique_lock<std::mutex> lock(mtx_);
while (worker_running_.load()) {
cv_.wait(lock, [this]() {
return has_request_ || !worker_running_.load();
});
if (!worker_running_.load())
break;
WarmRequest req = pending_;
has_request_ = false;
// Copy locals then release lock while computing
lock.unlock();
if (req.buf) {
int start = std::max(0, req.start_row);
int end = std::max(start, req.end_row);
for (int r = start; r <= end; ++r) {
// Re-check version staleness quickly by peeking cache version; not strictly necessary
// Compute line; GetLine is thread-safe
(void) this->GetLine(*req.buf, r, req.version);
}
}
lock.lock();
}
}
void
HighlighterEngine::PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version,
int warm_margin) const
{
if (row_count <= 0)
return;
// Synchronously compute visible rows to ensure cache hits during draw
int start = std::max(0, first_row);
int end = start + row_count - 1;
int max_rows = static_cast<int>(buf.Nrows());
if (start >= max_rows)
return;
if (end >= max_rows)
end = max_rows - 1;
for (int r = start; r <= end; ++r) {
(void) GetLine(buf, r, buf_version);
}
// Enqueue background warm-around
int warm_start = std::max(0, start - warm_margin);
int warm_end = std::min(max_rows - 1, end + warm_margin);
{
std::lock_guard<std::mutex> lock(mtx_);
pending_.buf = &buf;
pending_.version = buf_version;
pending_.start_row = warm_start;
pending_.end_row = warm_end;
has_request_ = true;
}
ensure_worker_started();
cv_.notify_one();
}
} // namespace kte

View File

@@ -0,0 +1,85 @@
// HighlighterEngine.h - caching layer for per-line highlights
#pragma once
#include <cstdint>
#include <memory>
#include <unordered_map>
#include <vector>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <thread>
#include "../Highlight.h"
#include "LanguageHighlighter.h"
class Buffer;
namespace kte {
class HighlighterEngine {
public:
HighlighterEngine();
~HighlighterEngine();
void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl);
// Retrieve highlights for a given line and buffer version.
// If cache is stale, recompute using the current highlighter.
const LineHighlight &GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
// Invalidate cached lines from row (inclusive)
void InvalidateFrom(int row);
bool HasHighlighter() const
{
return static_cast<bool>(hl_);
}
// Phase 3: viewport-first prefetch and background warming
// Compute only the visible range now, and enqueue a background warm-around task.
// warm_margin: how many extra lines above/below to warm in the background.
void PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version,
int warm_margin = 200) const;
private:
std::unique_ptr<LanguageHighlighter> hl_;
// Simple cache by row index (mutable to allow caching in const GetLine)
mutable std::unordered_map<int, LineHighlight> cache_;
// For stateful highlighters, remember per-line state (state after finishing that row)
struct StateEntry {
std::uint64_t version{0};
// Using the interface type; forward-declare via header
StatefulHighlighter::LineState state;
};
mutable std::unordered_map<int, StateEntry> state_cache_;
// Track best known contiguous state row for a given version to avoid O(n) scans
mutable std::unordered_map<std::uint64_t, int> state_last_contig_;
// Thread-safety for caches and background worker state
mutable std::mutex mtx_;
// Background warmer
struct WarmRequest {
const Buffer *buf{nullptr};
std::uint64_t version{0};
int start_row{0};
int end_row{0}; // inclusive
};
mutable std::condition_variable cv_;
mutable std::thread worker_;
mutable std::atomic<bool> worker_running_{false};
mutable bool has_request_{false};
mutable WarmRequest pending_{};
void ensure_worker_started() const;
void worker_loop() const;
};
} // namespace kte

View File

@@ -0,0 +1,247 @@
#include "HighlighterRegistry.h"
#include "CppHighlighter.h"
#include <algorithm>
#include <filesystem>
#include <vector>
#include <cctype>
// Forward declare simple highlighters implemented in this project
namespace kte {
// Registration storage
struct RegEntry {
std::string ft; // normalized
HighlighterRegistry::Factory factory;
};
static std::vector<RegEntry> &
registry()
{
static std::vector<RegEntry> reg;
return reg;
}
class JSONHighlighter;
class MarkdownHighlighter;
class ShellHighlighter;
class GoHighlighter;
class PythonHighlighter;
class RustHighlighter;
class LispHighlighter;
class SqlHighlighter;
class ErlangHighlighter;
class ForthHighlighter;
}
// Headers for the above
#include "JsonHighlighter.h"
#include "MarkdownHighlighter.h"
#include "ShellHighlighter.h"
#include "GoHighlighter.h"
#include "PythonHighlighter.h"
#include "RustHighlighter.h"
#include "LispHighlighter.h"
#include "SqlHighlighter.h"
#include "ErlangHighlighter.h"
#include "ForthHighlighter.h"
namespace kte {
static std::string
to_lower(std::string_view s)
{
std::string r(s);
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return r;
}
std::string
HighlighterRegistry::Normalize(std::string_view ft)
{
std::string f = to_lower(ft);
if (f == "c" || f == "c++" || f == "cc" || f == "hpp" || f == "hh" || f == "h" || f == "cxx")
return "cpp";
if (f == "cpp")
return "cpp";
if (f == "json")
return "json";
if (f == "markdown" || f == "md" || f == "mkd" || f == "mdown")
return "markdown";
if (f == "shell" || f == "sh" || f == "bash" || f == "zsh" || f == "ksh" || f == "fish")
return "shell";
if (f == "go" || f == "golang")
return "go";
if (f == "py" || f == "python")
return "python";
if (f == "rs" || f == "rust")
return "rust";
if (f == "lisp" || f == "scheme" || f == "scm" || f == "rkt" || f == "el" || f == "clj" || f == "cljc" || f ==
"cl")
return "lisp";
if (f == "sql" || f == "sqlite" || f == "sqlite3")
return "sql";
if (f == "erlang" || f == "erl" || f == "hrl")
return "erlang";
if (f == "forth" || f == "fth" || f == "4th" || f == "fs")
return "forth";
return f;
}
std::unique_ptr<LanguageHighlighter>
HighlighterRegistry::CreateFor(std::string_view filetype)
{
std::string ft = Normalize(filetype);
// Prefer externally registered factories
for (const auto &e: registry()) {
if (e.ft == ft && e.factory)
return e.factory();
}
if (ft == "cpp")
return std::make_unique<CppHighlighter>();
if (ft == "json")
return std::make_unique<JSONHighlighter>();
if (ft == "markdown")
return std::make_unique<MarkdownHighlighter>();
if (ft == "shell")
return std::make_unique<ShellHighlighter>();
if (ft == "go")
return std::make_unique<GoHighlighter>();
if (ft == "python")
return std::make_unique<PythonHighlighter>();
if (ft == "rust")
return std::make_unique<RustHighlighter>();
if (ft == "lisp")
return std::make_unique<LispHighlighter>();
if (ft == "sql")
return std::make_unique<SqlHighlighter>();
if (ft == "erlang")
return std::make_unique<ErlangHighlighter>();
if (ft == "forth")
return std::make_unique<ForthHighlighter>();
return nullptr;
}
static std::string
shebang_to_ft(std::string_view first_line)
{
if (first_line.size() < 2 || first_line.substr(0, 2) != "#!")
return "";
std::string low = to_lower(first_line);
if (low.find("python") != std::string::npos)
return "python";
if (low.find("bash") != std::string::npos)
return "shell";
if (low.find("sh") != std::string::npos)
return "shell";
if (low.find("zsh") != std::string::npos)
return "shell";
if (low.find("fish") != std::string::npos)
return "shell";
if (low.find("scheme") != std::string::npos || low.find("racket") != std::string::npos || low.find("guile") !=
std::string::npos)
return "lisp";
return "";
}
std::string
HighlighterRegistry::DetectForPath(std::string_view path, std::string_view first_line)
{
// Extension
std::string p(path);
std::error_code ec;
std::string ext = std::filesystem::path(p).extension().string();
for (auto &ch: ext)
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
if (!ext.empty()) {
if (ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".cxx" || ext == ".h" || ext == ".hpp" || ext
== ".hh")
return "cpp";
if (ext == ".json")
return "json";
if (ext == ".md" || ext == ".markdown" || ext == ".mkd")
return "markdown";
if (ext == ".sh" || ext == ".bash" || ext == ".zsh" || ext == ".ksh" || ext == ".fish")
return "shell";
if (ext == ".go")
return "go";
if (ext == ".py")
return "python";
if (ext == ".rs")
return "rust";
if (ext == ".lisp" || ext == ".scm" || ext == ".rkt" || ext == ".el" || ext == ".clj" || ext == ".cljc"
|| ext == ".cl")
return "lisp";
if (ext == ".sql" || ext == ".sqlite")
return "sql";
if (ext == ".erl" || ext == ".hrl")
return "erlang";
if (ext == ".forth" || ext == ".fth" || ext == ".4th" || ext == ".fs")
return "forth";
}
// Shebang
std::string ft = shebang_to_ft(first_line);
return ft;
}
} // namespace kte
// Extensibility API implementations
namespace kte {
void
HighlighterRegistry::Register(std::string_view filetype, Factory factory, bool override_existing)
{
std::string ft = Normalize(filetype);
for (auto &e: registry()) {
if (e.ft == ft) {
if (override_existing)
e.factory = std::move(factory);
return;
}
}
registry().push_back(RegEntry{ft, std::move(factory)});
}
bool
HighlighterRegistry::IsRegistered(std::string_view filetype)
{
std::string ft = Normalize(filetype);
for (const auto &e: registry())
if (e.ft == ft)
return true;
return false;
}
std::vector<std::string>
HighlighterRegistry::RegisteredFiletypes()
{
std::vector<std::string> out;
out.reserve(registry().size());
for (const auto &e: registry())
out.push_back(e.ft);
return out;
}
#ifdef KTE_ENABLE_TREESITTER
// Forward declare adapter factory
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
const void * (*get_lang)());
void
HighlighterRegistry::RegisterTreeSitter(std::string_view filetype,
const TSLanguage * (*get_language)())
{
std::string ft = Normalize(filetype);
Register(ft, [ft, get_language]() {
return CreateTreeSitterHighlighter(ft.c_str(), reinterpret_cast<const void* (*)()>(get_language));
}, /*override_existing=*/true);
}
#endif
} // namespace kte

View File

@@ -0,0 +1,47 @@
// HighlighterRegistry.h - create/detect language highlighters and allow external registration
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include "LanguageHighlighter.h"
namespace kte {
class HighlighterRegistry {
public:
using Factory = std::function<std::unique_ptr<LanguageHighlighter>()>;
// Create a highlighter for normalized filetype id (e.g., "cpp", "json", "markdown", "shell", "go", "python", "rust", "lisp").
static std::unique_ptr<LanguageHighlighter> CreateFor(std::string_view filetype);
// Detect filetype by path extension and shebang (first line).
// Returns normalized id or empty string if unknown.
static std::string DetectForPath(std::string_view path, std::string_view first_line);
// Normalize various aliases/extensions to canonical ids.
static std::string Normalize(std::string_view ft);
// Extensibility: allow external code to register highlighters at runtime.
// The filetype key is normalized via Normalize(). If a factory is already registered for the
// normalized key and override=false, the existing factory is kept.
static void Register(std::string_view filetype, Factory factory, bool override_existing = true);
// Returns true if a factory is registered for the (normalized) filetype.
static bool IsRegistered(std::string_view filetype);
// Return a list of currently registered (normalized) filetypes. Primarily for diagnostics/tests.
static std::vector<std::string> RegisteredFiletypes();
#ifdef KTE_ENABLE_TREESITTER
// Forward declaration to avoid hard dependency when disabled.
struct TSLanguage;
// Convenience: register a Tree-sitter-backed highlighter for a filetype.
// The getter should return a non-null language pointer for the grammar.
static void RegisterTreeSitter(std::string_view filetype,
const TSLanguage * (*get_language)());
#endif
};
} // namespace kte

90
syntax/JsonHighlighter.cc Normal file
View File

@@ -0,0 +1,90 @@
#include "JsonHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static bool
is_digit(char c)
{
return c >= '0' && c <= '9';
}
void
JSONHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
auto push = [&](int a, int b, TokenKind k) {
if (b > a)
out.push_back({a, b, k});
};
int i = 0;
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(i, j, TokenKind::Whitespace);
i = j;
continue;
}
if (c == '"') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
}
push(i, j, TokenKind::String);
i = j;
continue;
}
if (is_digit(c) || (c == '-' && i + 1 < n && is_digit(s[i + 1]))) {
int j = i + 1;
while (j < n && (std::isdigit(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'e' ||
s[j] == 'E' || s[j] == '+' || s[j] == '-' || s[j] == '_'))
++j;
push(i, j, TokenKind::Number);
i = j;
continue;
}
// booleans/null
if (std::isalpha(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && std::isalpha(static_cast<unsigned char>(s[j])))
++j;
std::string id = s.substr(i, j - i);
if (id == "true" || id == "false" || id == "null")
push(i, j, TokenKind::Constant);
else
push(i, j, TokenKind::Identifier);
i = j;
continue;
}
// punctuation
if (c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == ':') {
push(i, i + 1, TokenKind::Punctuation);
++i;
continue;
}
// fallback
push(i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

View File

@@ -5,10 +5,8 @@
#include <vector> #include <vector>
namespace kte { namespace kte {
class JSONHighlighter final : public LanguageHighlighter { class JSONHighlighter final : public LanguageHighlighter {
public: public:
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override; void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
}; };
} // namespace kte
} // namespace kte

View File

@@ -0,0 +1,51 @@
// LanguageHighlighter.h - interface for line-based highlighters
#pragma once
#include <memory>
#include <vector>
#include <string>
#include "../Highlight.h"
class Buffer;
namespace kte {
class LanguageHighlighter {
public:
virtual ~LanguageHighlighter() = default;
// Produce highlight spans for a given buffer row. Implementations should append to out.
virtual void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const = 0;
virtual bool Stateful() const
{
return false;
}
};
// Optional extension for stateful highlighters (e.g., multi-line comments/strings).
// Engines may detect and use this via dynamic_cast without breaking stateless impls.
class StatefulHighlighter : public LanguageHighlighter {
public:
struct LineState {
bool in_block_comment{false};
bool in_raw_string{false};
// For raw strings, remember the delimiter between the opening R"delim( and closing )delim"
std::string raw_delim;
};
// Highlight one line given the previous line state; return the resulting state after this line.
// Implementations should append spans for this line to out and compute the next state.
virtual LineState HighlightLineStateful(const Buffer &buf,
int row,
const LineState &prev,
std::vector<HighlightSpan> &out) const = 0;
bool Stateful() const override
{
return true;
}
};
} // namespace kte

107
syntax/LispHighlighter.cc Normal file
View File

@@ -0,0 +1,107 @@
#include "LispHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
LispHighlighter::LispHighlighter()
{
const char *kw[] = {
"defun", "lambda", "let", "let*", "define", "set!", "if", "cond", "begin", "quote", "quasiquote",
"unquote", "unquote-splicing", "loop", "do", "and", "or", "not"
};
for (auto s: kw)
kws_.insert(s);
}
void
LispHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
int bol = 0;
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
++bol;
if (bol < n && s[bol] == ';') {
push(out, bol, n, TokenKind::Comment);
if (bol > 0)
push(out, 0, bol, TokenKind::Whitespace);
return;
}
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
if (c == ';') {
push(out, i, n, TokenKind::Comment);
break;
}
if (c == '"') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
if (std::isalpha(static_cast<unsigned char>(c)) || c == '*' || c == '-' || c == '+' || c == '/' || c ==
'_') {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '*' || s[j] == '-' ||
s[j] == '+' || s[j] == '/' || s[j] == '_' || s[j] == '!'))
++j;
std::string id = s.substr(i, j - i);
TokenKind k = kws_.count(id) ? TokenKind::Keyword : TokenKind::Identifier;
push(out, i, j, k);
i = j;
continue;
}
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isdigit(static_cast<unsigned char>(s[j])) || s[j] == '.'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

View File

@@ -5,13 +5,13 @@
#include <unordered_set> #include <unordered_set>
namespace kte { namespace kte {
class LispHighlighter final : public LanguageHighlighter { class LispHighlighter final : public LanguageHighlighter {
public: public:
LispHighlighter(); LispHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
};
} // namespace kte void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
};
} // namespace kte

View File

@@ -0,0 +1,132 @@
#include "MarkdownHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push_span(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
void
MarkdownHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
LineState st; // not used in stateless entry
(void) HighlightLineStateful(buf, row, st, out);
}
StatefulHighlighter::LineState
MarkdownHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
std::vector<HighlightSpan> &out) const
{
StatefulHighlighter::LineState state = prev;
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return state;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
// Reuse in_block_comment flag as "in fenced code" state.
if (state.in_block_comment) {
// If line contains closing fence ``` then close after it
auto pos = s.find("```");
if (pos == std::string::npos) {
push_span(out, 0, n, TokenKind::String);
state.in_block_comment = true;
return state;
} else {
int end = static_cast<int>(pos + 3);
push_span(out, 0, end, TokenKind::String);
// rest of line processed normally after fence
int i = end;
// whitespace
if (i < n)
push_span(out, i, n, TokenKind::Default);
state.in_block_comment = false;
return state;
}
}
// Detect fenced code block start at beginning (allow leading spaces)
int bol = 0;
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
++bol;
if (bol + 3 <= n && s.compare(bol, 3, "```") == 0) {
push_span(out, bol, n, TokenKind::String);
state.in_block_comment = true; // enter fenced mode
return state;
}
// Headings: lines starting with 1-6 '#'
if (bol < n && s[bol] == '#') {
int j = bol;
while (j < n && s[j] == '#')
++j; // hashes
// include following space and text as Keyword to stand out
push_span(out, bol, n, TokenKind::Keyword);
return state;
}
// Process inline: emphasis and code spans
int i = 0;
while (i < n) {
char c = s[i];
if (c == '`') {
int j = i + 1;
while (j < n && s[j] != '`')
++j;
if (j < n)
++j;
push_span(out, i, j, TokenKind::String);
i = j;
continue;
}
if (c == '*' || c == '_') {
// bold/italic markers: treat the marker and until next same marker as Type to highlight
char m = c;
int j = i + 1;
while (j < n && s[j] != m)
++j;
if (j < n)
++j;
push_span(out, i, j, TokenKind::Type);
i = j;
continue;
}
// links []() minimal: treat [text](url) as Function
if (c == '[') {
int j = i + 1;
while (j < n && s[j] != ']')
++j;
if (j < n)
++j; // include ]
if (j < n && s[j] == '(') {
while (j < n && s[j] != ')')
++j;
if (j < n)
++j;
}
push_span(out, i, j, TokenKind::Function);
i = j;
continue;
}
// whitespace
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push_span(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
// fallback: default single char
push_span(out, i, i + 1, TokenKind::Default);
++i;
}
return state;
}
} // namespace kte

View File

@@ -0,0 +1,14 @@
// MarkdownHighlighter.h - simple Markdown highlighter
#pragma once
#include "LanguageHighlighter.h"
namespace kte {
class MarkdownHighlighter final : public StatefulHighlighter {
public:
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
std::vector<HighlightSpan> &out) const override;
};
} // namespace kte

17
syntax/NullHighlighter.cc Normal file
View File

@@ -0,0 +1,17 @@
#include "NullHighlighter.h"
#include "../Buffer.h"
namespace kte {
void
NullHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
if (n <= 0)
return;
out.push_back({0, n, TokenKind::Default});
}
} // namespace kte

View File

@@ -4,10 +4,8 @@
#include "LanguageHighlighter.h" #include "LanguageHighlighter.h"
namespace kte { namespace kte {
class NullHighlighter final : public LanguageHighlighter { class NullHighlighter final : public LanguageHighlighter {
public: public:
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override; void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
}; };
} // namespace kte
} // namespace kte

172
syntax/PythonHighlighter.cc Normal file
View File

@@ -0,0 +1,172 @@
#include "PythonHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
static bool
is_ident_start(char c)
{
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
}
static bool
is_ident_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
}
PythonHighlighter::PythonHighlighter()
{
const char *kw[] = {
"and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else", "except", "False",
"finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "None", "nonlocal", "not",
"or", "pass", "raise", "return", "True", "try", "while", "with", "yield"
};
for (auto s: kw)
kws_.insert(s);
}
void
PythonHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
LineState st;
(void) HighlightLineStateful(buf, row, st, out);
}
StatefulHighlighter::LineState
PythonHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
std::vector<HighlightSpan> &out) const
{
StatefulHighlighter::LineState state = prev;
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return state;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
// Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
auto pos = s.find(state.raw_delim);
if (pos == std::string::npos) {
push(out, 0, n, TokenKind::String);
return state; // still inside
} else {
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
push(out, 0, end, TokenKind::String);
// remainder processed normally
s = s.substr(end);
n = static_cast<int>(s.size());
state.in_raw_string = false;
state.raw_delim.clear();
// Continue parsing remainder as a separate small loop
int base = end;
// original offset, but we already emitted to 'out' with base=0; following spans should be from 'end'
// For simplicity, mark rest as Default
if (n > 0)
push(out, base, base + n, TokenKind::Default);
return state;
}
}
int i = 0;
// Detect comment start '#', ignoring inside strings
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
if (c == '#') {
push(out, i, n, TokenKind::Comment);
break;
}
// Strings: triple quotes and single-line
if (c == '"' || c == '\'') {
char q = c;
// triple?
if (i + 2 < n && s[i + 1] == q && s[i + 2] == q) {
std::string delim(3, q);
int j = i + 3; // search for closing triple
auto pos = s.find(delim, static_cast<std::size_t>(j));
if (pos == std::string::npos) {
push(out, i, n, TokenKind::String);
state.in_raw_string = true;
state.raw_delim = delim;
return state;
} else {
int end = static_cast<int>(pos + 3);
push(out, i, end, TokenKind::String);
i = end;
continue;
}
} else {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == q)
break;
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
}
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
if (is_ident_start(c)) {
int j = i + 1;
while (j < n && is_ident_char(s[j]))
++j;
std::string id = s.substr(i, j - i);
TokenKind k = TokenKind::Identifier;
if (kws_.count(id))
k = TokenKind::Keyword;
push(out, i, j, k);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Operator;
if (c == ':' || c == ',' || c == '(' || c == ')' || c == '[' || c == ']')
k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
return state;
}
} // namespace kte

View File

@@ -0,0 +1,20 @@
// PythonHighlighter.h - simple Python highlighter with triple-quote state
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class PythonHighlighter final : public StatefulHighlighter {
public:
PythonHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
};
} // namespace kte

145
syntax/RustHighlighter.cc Normal file
View File

@@ -0,0 +1,145 @@
#include "RustHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
static bool
is_ident_start(char c)
{
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
}
static bool
is_ident_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
}
RustHighlighter::RustHighlighter()
{
const char *kw[] = {
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if",
"impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self",
"static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while", "dyn", "async",
"await", "try"
};
for (auto s: kw)
kws_.insert(s);
const char *tp[] = {
"u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32", "f64",
"bool", "char", "str"
};
for (auto s: tp)
types_.insert(s);
}
void
RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
push(out, i, n, TokenKind::Comment);
break;
}
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
int j = i + 2;
bool closed = false;
while (j + 1 <= n) {
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
j += 2;
closed = true;
break;
}
++j;
}
if (!closed) {
push(out, i, n, TokenKind::Comment);
break;
} else {
push(out, i, j, TokenKind::Comment);
i = j;
continue;
}
}
if (c == '"') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
if (is_ident_start(c)) {
int j = i + 1;
while (j < n && is_ident_char(s[j]))
++j;
std::string id = s.substr(i, j - i);
TokenKind k = TokenKind::Identifier;
if (kws_.count(id))
k = TokenKind::Keyword;
else if (types_.count(id))
k = TokenKind::Type;
push(out, i, j, k);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Operator;
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
']')
k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

18
syntax/RustHighlighter.h Normal file
View File

@@ -0,0 +1,18 @@
// RustHighlighter.h - simple Rust highlighter
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class RustHighlighter final : public LanguageHighlighter {
public:
RustHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
std::unordered_set<std::string> types_;
};
} // namespace kte

105
syntax/ShellHighlighter.cc Normal file
View File

@@ -0,0 +1,105 @@
#include "ShellHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
void
ShellHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
// if first non-space is '#', whole line is comment
int bol = 0;
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
++bol;
if (bol < n && s[bol] == '#') {
push(out, bol, n, TokenKind::Comment);
if (bol > 0)
push(out, 0, bol, TokenKind::Whitespace);
return;
}
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
if (c == '#') {
push(out, i, n, TokenKind::Comment);
break;
}
if (c == '\'' || c == '"') {
char q = c;
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (q == '"') {
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
} else {
if (d == '\'')
break;
}
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
// simple keywords
if (std::isalpha(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '_'))
++j;
std::string id = s.substr(i, j - i);
static const char *kws[] = {
"if", "then", "fi", "for", "in", "do", "done", "case", "esac", "while", "function",
"elif", "else"
};
bool kw = false;
for (auto k: kws)
if (id == k) {
kw = true;
break;
}
push(out, i, j, kw ? TokenKind::Keyword : TokenKind::Identifier);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Operator;
if (c == '(' || c == ')' || c == '{' || c == '}' || c == ',' || c == ';')
k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

View File

@@ -4,10 +4,8 @@
#include "LanguageHighlighter.h" #include "LanguageHighlighter.h"
namespace kte { namespace kte {
class ShellHighlighter final : public LanguageHighlighter { class ShellHighlighter final : public LanguageHighlighter {
public: public:
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override; void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
}; };
} // namespace kte
} // namespace kte

156
syntax/SqlHighlighter.cc Normal file
View File

@@ -0,0 +1,156 @@
#include "SqlHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
static bool
is_ident_start(char c)
{
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
}
static bool
is_ident_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '$';
}
SqlHighlighter::SqlHighlighter()
{
const char *kw[] = {
"select", "insert", "update", "delete", "from", "where", "group", "by", "order", "limit",
"offset", "values", "into", "create", "table", "index", "unique", "on", "as", "and", "or",
"not", "null", "is", "primary", "key", "constraint", "foreign", "references", "drop", "alter",
"add", "column", "rename", "to", "if", "exists", "join", "left", "right", "inner", "outer",
"cross", "using", "set", "distinct", "having", "union", "all", "case", "when", "then", "else",
"end", "pragma", "transaction", "begin", "commit", "rollback", "replace"
};
for (auto s: kw)
kws_.insert(s);
const char *types[] = {"integer", "real", "text", "blob", "numeric", "boolean", "date", "datetime"};
for (auto s: types)
types_.insert(s);
}
void
SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
// line comments: -- ...
if (c == '-' && i + 1 < n && s[i + 1] == '-') {
push(out, i, n, TokenKind::Comment);
break;
}
// simple block comment on same line: /* ... */
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
int j = i + 2;
bool closed = false;
while (j + 1 <= n) {
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
j += 2;
closed = true;
break;
}
++j;
}
if (!closed) {
push(out, i, n, TokenKind::Comment);
break;
} else {
push(out, i, j, TokenKind::Comment);
i = j;
continue;
}
}
// strings: '...' or "..."
if (c == '\'' || c == '"') {
char q = c;
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (d == q) {
// Handle doubled quote escaping for SQL single quotes
if (q == '\'' && j < n && s[j] == '\'') {
++j;
continue;
}
break;
}
if (d == '\\') {
esc = !esc;
} else {
esc = false;
}
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
if (is_ident_start(c)) {
int j = i + 1;
while (j < n && is_ident_char(s[j]))
++j;
std::string id = s.substr(i, j - i);
std::string lower;
lower.reserve(id.size());
for (char ch: id)
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
TokenKind k = TokenKind::Identifier;
if (kws_.count(lower))
k = TokenKind::Keyword;
else if (types_.count(lower))
k = TokenKind::Type;
push(out, i, j, k);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Operator;
if (c == ',' || c == ';' || c == '(' || c == ')')
k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

18
syntax/SqlHighlighter.h Normal file
View File

@@ -0,0 +1,18 @@
// SqlHighlighter.h - simple SQL/SQLite highlighter
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class SqlHighlighter final : public LanguageHighlighter {
public:
SqlHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
std::unordered_set<std::string> types_;
};
} // namespace kte

View File

@@ -0,0 +1,51 @@
#include "TreeSitterHighlighter.h"
#ifdef KTE_ENABLE_TREESITTER
#include "Buffer.h"
#include <utility>
namespace kte {
TreeSitterHighlighter::TreeSitterHighlighter(const TSLanguage *lang, std::string filetype)
: language_(lang), filetype_(std::move(filetype)) {}
TreeSitterHighlighter::~TreeSitterHighlighter()
{
disposeParser();
}
void
TreeSitterHighlighter::ensureParsed(const Buffer & /*buf*/) const
{
// Intentionally a stub to avoid pulling the Tree-sitter API and library by default.
// In future, when linking against tree-sitter, initialize parser_, set language_,
// and build tree_ from the buffer contents.
}
void
TreeSitterHighlighter::disposeParser() const
{
// Stub; nothing to dispose when not actually creating parser/tree
}
void
TreeSitterHighlighter::HighlightLine(const Buffer &/*buf*/, int /*row*/, std::vector<HighlightSpan> &/*out*/) const
{
// For now, no-op. When tree-sitter is wired, map nodes to TokenKind spans per line.
}
std::unique_ptr<LanguageHighlighter>
CreateTreeSitterHighlighter(const char *filetype,
const void * (*get_lang)())
{
const auto *lang = reinterpret_cast<const TSLanguage *>(get_lang ? get_lang() : nullptr);
return std::make_unique < TreeSitterHighlighter > (lang, filetype ? std::string(filetype) : std::string());
}
} // namespace kte
#endif // KTE_ENABLE_TREESITTER

View File

@@ -0,0 +1,48 @@
// TreeSitterHighlighter.h - optional adapter for Tree-sitter (behind KTE_ENABLE_TREESITTER)
#pragma once
#ifdef KTE_ENABLE_TREESITTER
#include <memory>
#include <string>
#include <vector>
#include "LanguageHighlighter.h"
// Forward-declare Tree-sitter C API to avoid hard coupling in headers if includes are not present
extern "C" {
struct TSLanguage;
struct TSParser;
struct TSTree;
}
namespace kte {
// A minimal adapter that uses Tree-sitter to parse the whole buffer and then, for now,
// does very limited token classification. This acts as a scaffold for future richer
// queries. If no queries are provided, it currently produces no spans (safe fallback).
class TreeSitterHighlighter : public LanguageHighlighter {
public:
explicit TreeSitterHighlighter(const TSLanguage *lang, std::string filetype);
~TreeSitterHighlighter() override;
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
const TSLanguage *language_{nullptr};
std::string filetype_;
// Lazy parser to avoid startup cost; mutable to allow creation in const method
mutable TSParser *parser_{nullptr};
mutable TSTree *tree_{nullptr};
void ensureParsed(const Buffer &buf) const;
void disposeParser() const;
};
// Factory used by HighlighterRegistry when registering via RegisterTreeSitter.
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
const void * (*get_lang)());
} // namespace kte
#endif // KTE_ENABLE_TREESITTER

177
themes/EInk.h Normal file
View File

@@ -0,0 +1,177 @@
// themes/EInk.h — Monochrome e-ink inspired ImGui themes (header-only)
#pragma once
#include "imgui.h"
#include "ThemeHelpers.h"
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
static void
ApplyEInkImGuiTheme()
{
// E-Ink grayscale palette (light background)
const ImVec4 paper = RGBA(0xF2F2EE); // light paper
const ImVec4 bg1 = RGBA(0xE6E6E2);
const ImVec4 bg2 = RGBA(0xDADAD5);
const ImVec4 bg3 = RGBA(0xCFCFCA);
const ImVec4 ink = RGBA(0x111111); // primary text (near black)
const ImVec4 dim = RGBA(0x666666); // disabled text
const ImVec4 border = RGBA(0xB8B8B3);
const ImVec4 accent = RGBA(0x222222); // controls/active
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 0.0f;
style.FrameRounding = 0.0f;
style.PopupRounding = 0.0f;
style.GrabRounding = 0.0f;
style.TabRounding = 0.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = ink;
colors[ImGuiCol_TextDisabled] = ImVec4(dim.x, dim.y, dim.z, 1.0f);
colors[ImGuiCol_WindowBg] = paper;
colors[ImGuiCol_ChildBg] = paper;
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
colors[ImGuiCol_Border] = border;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
colors[ImGuiCol_FrameBg] = bg2;
colors[ImGuiCol_FrameBgHovered] = bg3;
colors[ImGuiCol_FrameBgActive] = bg1;
colors[ImGuiCol_TitleBg] = bg1;
colors[ImGuiCol_TitleBgActive] = bg2;
colors[ImGuiCol_TitleBgCollapsed] = bg1;
colors[ImGuiCol_MenuBarBg] = bg1;
colors[ImGuiCol_ScrollbarBg] = paper;
colors[ImGuiCol_ScrollbarGrab] = bg3;
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
colors[ImGuiCol_CheckMark] = accent;
colors[ImGuiCol_SliderGrab] = accent;
colors[ImGuiCol_SliderGrabActive] = ink;
colors[ImGuiCol_Button] = bg3;
colors[ImGuiCol_ButtonHovered] = bg2;
colors[ImGuiCol_ButtonActive] = bg1;
colors[ImGuiCol_Header] = bg3;
colors[ImGuiCol_HeaderHovered] = bg2;
colors[ImGuiCol_HeaderActive] = bg2;
colors[ImGuiCol_Separator] = border;
colors[ImGuiCol_SeparatorHovered] = bg2;
colors[ImGuiCol_SeparatorActive] = accent;
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.12f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(accent.x, accent.y, accent.z, 0.50f);
colors[ImGuiCol_ResizeGripActive] = ink;
colors[ImGuiCol_Tab] = bg2;
colors[ImGuiCol_TabHovered] = bg1;
colors[ImGuiCol_TabActive] = bg3;
colors[ImGuiCol_TabUnfocused] = bg2;
colors[ImGuiCol_TabUnfocusedActive] = bg3;
colors[ImGuiCol_TableHeaderBg] = bg2;
colors[ImGuiCol_TableBorderStrong] = bg1;
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(accent.x, accent.y, accent.z, 0.30f);
colors[ImGuiCol_DragDropTarget] = accent;
colors[ImGuiCol_NavHighlight] = accent;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_PlotLines] = accent;
colors[ImGuiCol_PlotLinesHovered] = ink;
colors[ImGuiCol_PlotHistogram] = accent;
colors[ImGuiCol_PlotHistogramHovered] = ink;
}
static inline void
ApplyEInkDarkImGuiTheme()
{
// E-Ink dark variant (dark background, light ink)
const ImVec4 paper = RGBA(0x1A1A1A);
const ImVec4 bg1 = RGBA(0x222222);
const ImVec4 bg2 = RGBA(0x2B2B2B);
const ImVec4 bg3 = RGBA(0x343434);
const ImVec4 ink = RGBA(0xEDEDEA);
const ImVec4 dim = RGBA(0xB5B5B3);
const ImVec4 border = RGBA(0x444444);
const ImVec4 accent = RGBA(0xDDDDDD);
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 0.0f;
style.FrameRounding = 0.0f;
style.PopupRounding = 0.0f;
style.GrabRounding = 0.0f;
style.TabRounding = 0.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = ink;
colors[ImGuiCol_TextDisabled] = ImVec4(dim.x, dim.y, dim.z, 1.0f);
colors[ImGuiCol_WindowBg] = paper;
colors[ImGuiCol_ChildBg] = paper;
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
colors[ImGuiCol_Border] = border;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
colors[ImGuiCol_FrameBg] = bg2;
colors[ImGuiCol_FrameBgHovered] = bg3;
colors[ImGuiCol_FrameBgActive] = bg1;
colors[ImGuiCol_TitleBg] = bg1;
colors[ImGuiCol_TitleBgActive] = bg2;
colors[ImGuiCol_TitleBgCollapsed] = bg1;
colors[ImGuiCol_MenuBarBg] = bg1;
colors[ImGuiCol_ScrollbarBg] = paper;
colors[ImGuiCol_ScrollbarGrab] = bg3;
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
colors[ImGuiCol_CheckMark] = accent;
colors[ImGuiCol_SliderGrab] = accent;
colors[ImGuiCol_SliderGrabActive] = ink;
colors[ImGuiCol_Button] = bg3;
colors[ImGuiCol_ButtonHovered] = bg2;
colors[ImGuiCol_ButtonActive] = bg1;
colors[ImGuiCol_Header] = bg3;
colors[ImGuiCol_HeaderHovered] = bg2;
colors[ImGuiCol_HeaderActive] = bg2;
colors[ImGuiCol_Separator] = border;
colors[ImGuiCol_SeparatorHovered] = bg2;
colors[ImGuiCol_SeparatorActive] = accent;
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.12f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(accent.x, accent.y, accent.z, 0.50f);
colors[ImGuiCol_ResizeGripActive] = ink;
colors[ImGuiCol_Tab] = bg2;
colors[ImGuiCol_TabHovered] = bg1;
colors[ImGuiCol_TabActive] = bg3;
colors[ImGuiCol_TabUnfocused] = bg2;
colors[ImGuiCol_TabUnfocusedActive] = bg3;
colors[ImGuiCol_TableHeaderBg] = bg2;
colors[ImGuiCol_TableBorderStrong] = bg1;
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(accent.x, accent.y, accent.z, 0.30f);
colors[ImGuiCol_DragDropTarget] = accent;
colors[ImGuiCol_NavHighlight] = accent;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_PlotLines] = accent;
colors[ImGuiCol_PlotLinesHovered] = ink;
colors[ImGuiCol_PlotHistogram] = accent;
colors[ImGuiCol_PlotHistogramHovered] = ink;
}

204
themes/Gruvbox.h Normal file
View File

@@ -0,0 +1,204 @@
// themes/Gruvbox.h — Gruvbox Dark/Light (medium) ImGui themes (header-only)
#pragma once
#include "ThemeHelpers.h"
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
static void
ApplyGruvboxDarkMediumTheme()
{
// Gruvbox (dark, medium) palette
const ImVec4 bg0 = RGBA(0x282828); // dark0
const ImVec4 bg1 = RGBA(0x3C3836); // dark1
const ImVec4 bg2 = RGBA(0x504945); // dark2
const ImVec4 bg3 = RGBA(0x665C54); // dark3
const ImVec4 fg1 = RGBA(0xEBDBB2); // light1
const ImVec4 fg0 = RGBA(0xFBF1C7); // light0
// accent colors
const ImVec4 yellow = RGBA(0xFABD2F);
const ImVec4 blue = RGBA(0x83A598);
const ImVec4 aqua = RGBA(0x8EC07C);
const ImVec4 orange = RGBA(0xFE8019);
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 4.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 4.0f;
style.GrabRounding = 3.0f;
style.TabRounding = 4.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = fg1;
colors[ImGuiCol_TextDisabled] = ImVec4(fg1.x, fg1.y, fg1.z, 0.55f);
colors[ImGuiCol_WindowBg] = bg0;
colors[ImGuiCol_ChildBg] = bg0;
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
colors[ImGuiCol_Border] = bg2;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
colors[ImGuiCol_FrameBg] = bg2;
colors[ImGuiCol_FrameBgHovered] = bg3;
colors[ImGuiCol_FrameBgActive] = bg1;
colors[ImGuiCol_TitleBg] = bg1;
colors[ImGuiCol_TitleBgActive] = bg2;
colors[ImGuiCol_TitleBgCollapsed] = bg1;
colors[ImGuiCol_MenuBarBg] = bg1;
colors[ImGuiCol_ScrollbarBg] = bg0;
colors[ImGuiCol_ScrollbarGrab] = bg3;
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
colors[ImGuiCol_CheckMark] = aqua;
colors[ImGuiCol_SliderGrab] = aqua;
colors[ImGuiCol_SliderGrabActive] = blue;
colors[ImGuiCol_Button] = bg3;
colors[ImGuiCol_ButtonHovered] = bg2;
colors[ImGuiCol_ButtonActive] = bg1;
colors[ImGuiCol_Header] = bg3;
colors[ImGuiCol_HeaderHovered] = bg2;
colors[ImGuiCol_HeaderActive] = bg2;
colors[ImGuiCol_Separator] = bg2;
colors[ImGuiCol_SeparatorHovered] = bg1;
colors[ImGuiCol_SeparatorActive] = blue;
colors[ImGuiCol_ResizeGrip] = ImVec4(fg0.x, fg0.y, fg0.z, 0.12f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(aqua.x, aqua.y, aqua.z, 0.67f);
colors[ImGuiCol_ResizeGripActive] = blue;
colors[ImGuiCol_Tab] = bg2;
colors[ImGuiCol_TabHovered] = bg1;
colors[ImGuiCol_TabActive] = bg3;
colors[ImGuiCol_TabUnfocused] = bg2;
colors[ImGuiCol_TabUnfocusedActive] = bg3;
colors[ImGuiCol_TableHeaderBg] = bg2;
colors[ImGuiCol_TableBorderStrong] = bg1;
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(orange.x, orange.y, orange.z, 0.30f);
colors[ImGuiCol_DragDropTarget] = orange;
colors[ImGuiCol_NavHighlight] = orange;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(fg0.x, fg0.y, fg0.z, 0.70f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_PlotLines] = aqua;
colors[ImGuiCol_PlotLinesHovered] = blue;
colors[ImGuiCol_PlotHistogram] = yellow;
colors[ImGuiCol_PlotHistogramHovered] = orange;
}
static inline void
ApplyGruvboxLightMediumTheme()
{
// Gruvbox (light, medium) palette
const ImVec4 bg0 = RGBA(0xFBF1C7); // light0
const ImVec4 bg1 = RGBA(0xEBDBB2); // light1
const ImVec4 bg2 = RGBA(0xD5C4A1); // light2
const ImVec4 bg3 = RGBA(0xBDAE93); // light3
const ImVec4 fg1 = RGBA(0x3C3836); // dark1
const ImVec4 fg0 = RGBA(0x282828); // dark0
// accents
const ImVec4 yellow = RGBA(0xB57614);
const ImVec4 blue = RGBA(0x076678);
const ImVec4 aqua = RGBA(0x427B58);
const ImVec4 orange = RGBA(0xAF3A03);
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 4.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 4.0f;
style.GrabRounding = 3.0f;
style.TabRounding = 4.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = fg1;
colors[ImGuiCol_TextDisabled] = ImVec4(fg1.x, fg1.y, fg1.z, 0.55f);
colors[ImGuiCol_WindowBg] = bg0;
colors[ImGuiCol_ChildBg] = bg0;
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
colors[ImGuiCol_Border] = bg2;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
colors[ImGuiCol_FrameBg] = bg2;
colors[ImGuiCol_FrameBgHovered] = bg3;
colors[ImGuiCol_FrameBgActive] = bg1;
colors[ImGuiCol_TitleBg] = bg1;
colors[ImGuiCol_TitleBgActive] = bg2;
colors[ImGuiCol_TitleBgCollapsed] = bg1;
colors[ImGuiCol_MenuBarBg] = bg1;
colors[ImGuiCol_ScrollbarBg] = bg0;
colors[ImGuiCol_ScrollbarGrab] = bg3;
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
colors[ImGuiCol_CheckMark] = aqua;
colors[ImGuiCol_SliderGrab] = aqua;
colors[ImGuiCol_SliderGrabActive] = blue;
colors[ImGuiCol_Button] = bg3;
colors[ImGuiCol_ButtonHovered] = bg2;
colors[ImGuiCol_ButtonActive] = bg1;
colors[ImGuiCol_Header] = bg3;
colors[ImGuiCol_HeaderHovered] = bg2;
colors[ImGuiCol_HeaderActive] = bg2;
colors[ImGuiCol_Separator] = bg2;
colors[ImGuiCol_SeparatorHovered] = bg1;
colors[ImGuiCol_SeparatorActive] = blue;
colors[ImGuiCol_ResizeGrip] = ImVec4(fg0.x, fg0.y, fg0.z, 0.12f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(aqua.x, aqua.y, aqua.z, 0.67f);
colors[ImGuiCol_ResizeGripActive] = blue;
colors[ImGuiCol_Tab] = bg2;
colors[ImGuiCol_TabHovered] = bg1;
colors[ImGuiCol_TabActive] = bg3;
colors[ImGuiCol_TabUnfocused] = bg2;
colors[ImGuiCol_TabUnfocusedActive] = bg3;
colors[ImGuiCol_TableHeaderBg] = bg2;
colors[ImGuiCol_TableBorderStrong] = bg1;
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(orange.x, orange.y, orange.z, 0.30f);
colors[ImGuiCol_DragDropTarget] = orange;
colors[ImGuiCol_NavHighlight] = orange;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(fg0.x, fg0.y, fg0.z, 0.70f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_PlotLines] = aqua;
colors[ImGuiCol_PlotLinesHovered] = blue;
colors[ImGuiCol_PlotHistogram] = yellow;
colors[ImGuiCol_PlotHistogramHovered] = orange;
}

111
themes/Nord.h Normal file
View File

@@ -0,0 +1,111 @@
// themes/Nord.h — Nord-inspired ImGui theme (header-only)
#pragma once
#include "ThemeHelpers.h"
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
static void
ApplyNordImGuiTheme()
{
// Nord palette
const ImVec4 nord0 = RGBA(0x2E3440); // darkest bg
const ImVec4 nord1 = RGBA(0x3B4252);
const ImVec4 nord2 = RGBA(0x434C5E);
const ImVec4 nord3 = RGBA(0x4C566A);
const ImVec4 nord4 = RGBA(0xD8DEE9);
const ImVec4 nord6 = RGBA(0xECEFF4); // lightest
const ImVec4 nord8 = RGBA(0x88C0D0); // cyan
const ImVec4 nord9 = RGBA(0x81A1C1); // blue
const ImVec4 nord10 = RGBA(0x5E81AC); // blue dark
const ImVec4 nord12 = RGBA(0xD08770); // orange
const ImVec4 nord13 = RGBA(0xEBCB8B); // yellow
ImGuiStyle &style = ImGui::GetStyle();
// Base style tweaks to suit Nord aesthetics
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 4.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 4.0f;
style.GrabRounding = 3.0f;
style.TabRounding = 4.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = nord4; // primary text
colors[ImGuiCol_TextDisabled] = ImVec4(nord4.x, nord4.y, nord4.z, 0.55f);
colors[ImGuiCol_WindowBg] = nord0;
colors[ImGuiCol_ChildBg] = nord0;
colors[ImGuiCol_PopupBg] = ImVec4(nord1.x, nord1.y, nord1.z, 0.98f);
colors[ImGuiCol_Border] = nord2;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
colors[ImGuiCol_FrameBg] = nord2;
colors[ImGuiCol_FrameBgHovered] = nord3;
colors[ImGuiCol_FrameBgActive] = nord1;
colors[ImGuiCol_TitleBg] = nord1;
colors[ImGuiCol_TitleBgActive] = nord2;
colors[ImGuiCol_TitleBgCollapsed] = nord1;
colors[ImGuiCol_MenuBarBg] = nord1;
colors[ImGuiCol_ScrollbarBg] = nord10;
colors[ImGuiCol_ScrollbarGrab] = nord3;
colors[ImGuiCol_ScrollbarGrabHovered] = nord2;
colors[ImGuiCol_ScrollbarGrabActive] = nord1;
colors[ImGuiCol_CheckMark] = nord8;
colors[ImGuiCol_SliderGrab] = nord8;
colors[ImGuiCol_SliderGrabActive] = nord9;
colors[ImGuiCol_Button] = nord3;
colors[ImGuiCol_ButtonHovered] = nord2;
colors[ImGuiCol_ButtonActive] = nord1;
colors[ImGuiCol_Header] = nord3;
colors[ImGuiCol_HeaderHovered] = nord10;
colors[ImGuiCol_HeaderActive] = nord10;
colors[ImGuiCol_Separator] = nord2;
colors[ImGuiCol_SeparatorHovered] = nord10;
colors[ImGuiCol_SeparatorActive] = nord9;
colors[ImGuiCol_ResizeGrip] = ImVec4(nord6.x, nord6.y, nord6.z, 0.12f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(nord8.x, nord8.y, nord8.z, 0.67f);
colors[ImGuiCol_ResizeGripActive] = nord9;
colors[ImGuiCol_Tab] = nord2;
colors[ImGuiCol_TabHovered] = nord10;
colors[ImGuiCol_TabActive] = nord3;
colors[ImGuiCol_TabUnfocused] = nord2;
colors[ImGuiCol_TabUnfocusedActive] = nord3;
// Docking colors omitted for compatibility
colors[ImGuiCol_TableHeaderBg] = nord2;
colors[ImGuiCol_TableBorderStrong] = nord1;
colors[ImGuiCol_TableBorderLight] = ImVec4(nord1.x, nord1.y, nord1.z, 0.6f);
colors[ImGuiCol_TableRowBg] = ImVec4(nord1.x, nord1.y, nord1.z, 0.2f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(nord1.x, nord1.y, nord1.z, 0.35f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(nord8.x, nord8.y, nord8.z, 0.35f);
colors[ImGuiCol_DragDropTarget] = nord13;
colors[ImGuiCol_NavHighlight] = nord9;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(nord6.x, nord6.y, nord6.z, 0.7f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
// Plots
colors[ImGuiCol_PlotLines] = nord8;
colors[ImGuiCol_PlotLinesHovered] = nord9;
colors[ImGuiCol_PlotHistogram] = nord13;
colors[ImGuiCol_PlotHistogramHovered] = nord12;
}

89
themes/Plan9.h Normal file
View File

@@ -0,0 +1,89 @@
// themes/Plan9.h — Plan 9 acme-inspired ImGui theme (header-only)
#pragma once
#include "ThemeHelpers.h"
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
static void
ApplyPlan9Theme()
{
// Acme-like colors
const ImVec4 paper = RGBA(0xFFFFE8); // pale yellow paper
const ImVec4 pane = RGBA(0xFFF4C1); // slightly deeper for frames
const ImVec4 ink = RGBA(0x000000); // black text
constexpr auto dim = ImVec4(0, 0, 0, 0.60f);
const ImVec4 border = RGBA(0x000000); // 1px black
const ImVec4 blue = RGBA(0x0064FF); // acme-ish blue accents
const ImVec4 blueH = RGBA(0x4C8DFF); // hover/active
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(6.0f, 6.0f);
style.FramePadding = ImVec2(5.0f, 3.0f);
style.CellPadding = ImVec2(5.0f, 3.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 0.0f;
style.FrameRounding = 0.0f;
style.PopupRounding = 0.0f;
style.GrabRounding = 0.0f;
style.TabRounding = 0.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *c = style.Colors;
c[ImGuiCol_Text] = ink;
c[ImGuiCol_TextDisabled] = dim;
c[ImGuiCol_WindowBg] = paper;
c[ImGuiCol_ChildBg] = paper;
c[ImGuiCol_PopupBg] = ImVec4(pane.x, pane.y, pane.z, 0.98f);
c[ImGuiCol_Border] = border;
c[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
c[ImGuiCol_FrameBg] = pane;
c[ImGuiCol_FrameBgHovered] = RGBA(0xFFEBA0);
c[ImGuiCol_FrameBgActive] = RGBA(0xFFE387);
c[ImGuiCol_TitleBg] = pane;
c[ImGuiCol_TitleBgActive] = RGBA(0xFFE8A6);
c[ImGuiCol_TitleBgCollapsed] = pane;
c[ImGuiCol_MenuBarBg] = pane;
c[ImGuiCol_ScrollbarBg] = paper;
c[ImGuiCol_ScrollbarGrab] = RGBA(0xEADFA5);
c[ImGuiCol_ScrollbarGrabHovered] = RGBA(0xE2D37F);
c[ImGuiCol_ScrollbarGrabActive] = RGBA(0xD8C757);
c[ImGuiCol_CheckMark] = blue;
c[ImGuiCol_SliderGrab] = blue;
c[ImGuiCol_SliderGrabActive] = blueH;
c[ImGuiCol_Button] = RGBA(0xFFF1B0);
c[ImGuiCol_ButtonHovered] = RGBA(0xFFE892);
c[ImGuiCol_ButtonActive] = RGBA(0xFFE072);
c[ImGuiCol_Header] = RGBA(0xFFF1B0);
c[ImGuiCol_HeaderHovered] = RGBA(0xFFE892);
c[ImGuiCol_HeaderActive] = RGBA(0xFFE072);
c[ImGuiCol_Separator] = border;
c[ImGuiCol_SeparatorHovered] = blue;
c[ImGuiCol_SeparatorActive] = blueH;
c[ImGuiCol_ResizeGrip] = ImVec4(0, 0, 0, 0.12f);
c[ImGuiCol_ResizeGripHovered] = ImVec4(blue.x, blue.y, blue.z, 0.67f);
c[ImGuiCol_ResizeGripActive] = blueH;
c[ImGuiCol_Tab] = RGBA(0xFFE8A6);
c[ImGuiCol_TabHovered] = RGBA(0xFFE072);
c[ImGuiCol_TabActive] = RGBA(0xFFD859);
c[ImGuiCol_TabUnfocused] = RGBA(0xFFE8A6);
c[ImGuiCol_TabUnfocusedActive] = RGBA(0xFFD859);
c[ImGuiCol_TableHeaderBg] = RGBA(0xFFE8A6);
c[ImGuiCol_TableBorderStrong] = border;
c[ImGuiCol_TableBorderLight] = ImVec4(0, 0, 0, 0.35f);
c[ImGuiCol_TableRowBg] = ImVec4(0, 0, 0, 0.04f);
c[ImGuiCol_TableRowBgAlt] = ImVec4(0, 0, 0, 0.08f);
c[ImGuiCol_TextSelectedBg] = ImVec4(blueH.x, blueH.y, blueH.z, 0.35f);
c[ImGuiCol_DragDropTarget] = blue;
c[ImGuiCol_NavHighlight] = blue;
c[ImGuiCol_NavWindowingHighlight] = ImVec4(0, 0, 0, 0.20f);
c[ImGuiCol_NavWindowingDimBg] = ImVec4(0, 0, 0, 0.20f);
c[ImGuiCol_ModalWindowDimBg] = ImVec4(0, 0, 0, 0.20f);
c[ImGuiCol_PlotLines] = blue;
c[ImGuiCol_PlotLinesHovered] = blueH;
c[ImGuiCol_PlotHistogram] = blue;
c[ImGuiCol_PlotHistogramHovered] = blueH;
}

184
themes/Solarized.h Normal file
View File

@@ -0,0 +1,184 @@
// themes/Solarized.h — Solarized Dark/Light ImGui themes (header-only)
#pragma once
#include "ThemeHelpers.h"
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
static void
ApplySolarizedDarkTheme()
{
// Base colors from Ethan Schoonover Solarized
const ImVec4 base03 = RGBA(0x002b36);
const ImVec4 base02 = RGBA(0x073642);
const ImVec4 base01 = RGBA(0x586e75);
const ImVec4 base00 = RGBA(0x657b83);
const ImVec4 base0 = RGBA(0x839496);
const ImVec4 base1 = RGBA(0x93a1a1);
const ImVec4 base2 = RGBA(0xeee8d5);
const ImVec4 yellow = RGBA(0xb58900);
const ImVec4 orange = RGBA(0xcb4b16);
const ImVec4 blue = RGBA(0x268bd2);
const ImVec4 cyan = RGBA(0x2aa198);
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 3.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 3.0f;
style.GrabRounding = 3.0f;
style.TabRounding = 3.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *c = style.Colors;
c[ImGuiCol_Text] = base0;
c[ImGuiCol_TextDisabled] = ImVec4(base01.x, base01.y, base01.z, 1.0f);
c[ImGuiCol_WindowBg] = base03;
c[ImGuiCol_ChildBg] = base03;
c[ImGuiCol_PopupBg] = ImVec4(base02.x, base02.y, base02.z, 0.98f);
c[ImGuiCol_Border] = base02;
c[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
c[ImGuiCol_FrameBg] = base02;
c[ImGuiCol_FrameBgHovered] = base01;
c[ImGuiCol_FrameBgActive] = base00;
c[ImGuiCol_TitleBg] = base02;
c[ImGuiCol_TitleBgActive] = base01;
c[ImGuiCol_TitleBgCollapsed] = base02;
c[ImGuiCol_MenuBarBg] = base02;
c[ImGuiCol_ScrollbarBg] = base02;
c[ImGuiCol_ScrollbarGrab] = base01;
c[ImGuiCol_ScrollbarGrabHovered] = base00;
c[ImGuiCol_ScrollbarGrabActive] = blue;
c[ImGuiCol_CheckMark] = cyan;
c[ImGuiCol_SliderGrab] = cyan;
c[ImGuiCol_SliderGrabActive] = blue;
c[ImGuiCol_Button] = base01;
c[ImGuiCol_ButtonHovered] = base00;
c[ImGuiCol_ButtonActive] = blue;
c[ImGuiCol_Header] = base01;
c[ImGuiCol_HeaderHovered] = base00;
c[ImGuiCol_HeaderActive] = base00;
c[ImGuiCol_Separator] = base01;
c[ImGuiCol_SeparatorHovered] = base00;
c[ImGuiCol_SeparatorActive] = blue;
c[ImGuiCol_ResizeGrip] = ImVec4(base1.x, base1.y, base1.z, 0.12f);
c[ImGuiCol_ResizeGripHovered] = ImVec4(cyan.x, cyan.y, cyan.z, 0.67f);
c[ImGuiCol_ResizeGripActive] = blue;
c[ImGuiCol_Tab] = base01;
c[ImGuiCol_TabHovered] = base00;
c[ImGuiCol_TabActive] = base02;
c[ImGuiCol_TabUnfocused] = base01;
c[ImGuiCol_TabUnfocusedActive] = base02;
c[ImGuiCol_TableHeaderBg] = base01;
c[ImGuiCol_TableBorderStrong] = base00;
c[ImGuiCol_TableBorderLight] = ImVec4(base00.x, base00.y, base00.z, 0.6f);
c[ImGuiCol_TableRowBg] = ImVec4(base02.x, base02.y, base02.z, 0.2f);
c[ImGuiCol_TableRowBgAlt] = ImVec4(base02.x, base02.y, base02.z, 0.35f);
c[ImGuiCol_TextSelectedBg] = ImVec4(cyan.x, cyan.y, cyan.z, 0.30f);
c[ImGuiCol_DragDropTarget] = yellow;
c[ImGuiCol_NavHighlight] = blue;
c[ImGuiCol_NavWindowingHighlight] = ImVec4(base2.x, base2.y, base2.z, 0.70f);
c[ImGuiCol_NavWindowingDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
c[ImGuiCol_ModalWindowDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
c[ImGuiCol_PlotLines] = cyan;
c[ImGuiCol_PlotLinesHovered] = blue;
c[ImGuiCol_PlotHistogram] = yellow;
c[ImGuiCol_PlotHistogramHovered] = orange;
}
static inline void
ApplySolarizedLightTheme()
{
// Base colors from Ethan Schoonover Solarized (light variant)
const ImVec4 base3 = RGBA(0xfdf6e3);
const ImVec4 base2 = RGBA(0xeee8d5);
const ImVec4 base1 = RGBA(0x93a1a1);
const ImVec4 base0 = RGBA(0x839496);
const ImVec4 base00 = RGBA(0x657b83);
const ImVec4 base01 = RGBA(0x586e75);
const ImVec4 base02 = RGBA(0x073642);
const ImVec4 base03 = RGBA(0x002b36);
const ImVec4 yellow = RGBA(0xb58900);
const ImVec4 orange = RGBA(0xcb4b16);
const ImVec4 blue = RGBA(0x268bd2);
const ImVec4 cyan = RGBA(0x2aa198);
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 3.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 3.0f;
style.GrabRounding = 3.0f;
style.TabRounding = 3.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *c = style.Colors;
c[ImGuiCol_Text] = base00;
c[ImGuiCol_TextDisabled] = ImVec4(base01.x, base01.y, base01.z, 1.0f);
c[ImGuiCol_WindowBg] = base3;
c[ImGuiCol_ChildBg] = base3;
c[ImGuiCol_PopupBg] = ImVec4(base2.x, base2.y, base2.z, 0.98f);
c[ImGuiCol_Border] = base1;
c[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
c[ImGuiCol_FrameBg] = base2;
c[ImGuiCol_FrameBgHovered] = base1;
c[ImGuiCol_FrameBgActive] = base0;
c[ImGuiCol_TitleBg] = base2;
c[ImGuiCol_TitleBgActive] = base1;
c[ImGuiCol_TitleBgCollapsed] = base2;
c[ImGuiCol_MenuBarBg] = base2;
c[ImGuiCol_ScrollbarBg] = base2;
c[ImGuiCol_ScrollbarGrab] = base1;
c[ImGuiCol_ScrollbarGrabHovered] = base0;
c[ImGuiCol_ScrollbarGrabActive] = blue;
c[ImGuiCol_CheckMark] = cyan;
c[ImGuiCol_SliderGrab] = cyan;
c[ImGuiCol_SliderGrabActive] = blue;
c[ImGuiCol_Button] = base1;
c[ImGuiCol_ButtonHovered] = base0;
c[ImGuiCol_ButtonActive] = blue;
c[ImGuiCol_Header] = base1;
c[ImGuiCol_HeaderHovered] = base0;
c[ImGuiCol_HeaderActive] = base0;
c[ImGuiCol_Separator] = base1;
c[ImGuiCol_SeparatorHovered] = base0;
c[ImGuiCol_SeparatorActive] = blue;
c[ImGuiCol_ResizeGrip] = ImVec4(base1.x, base1.y, base1.z, 0.12f);
c[ImGuiCol_ResizeGripHovered] = ImVec4(cyan.x, cyan.y, cyan.z, 0.67f);
c[ImGuiCol_ResizeGripActive] = blue;
c[ImGuiCol_Tab] = base1;
c[ImGuiCol_TabHovered] = base0;
c[ImGuiCol_TabActive] = base2;
c[ImGuiCol_TabUnfocused] = base1;
c[ImGuiCol_TabUnfocusedActive] = base2;
c[ImGuiCol_TableHeaderBg] = base1;
c[ImGuiCol_TableBorderStrong] = base0;
c[ImGuiCol_TableBorderLight] = ImVec4(base0.x, base0.y, base0.z, 0.6f);
c[ImGuiCol_TableRowBg] = ImVec4(base02.x, base02.y, base02.z, 0.2f);
c[ImGuiCol_TableRowBgAlt] = ImVec4(base02.x, base02.y, base02.z, 0.35f);
c[ImGuiCol_TextSelectedBg] = ImVec4(cyan.x, cyan.y, cyan.z, 0.30f);
c[ImGuiCol_DragDropTarget] = yellow;
c[ImGuiCol_NavHighlight] = blue;
c[ImGuiCol_NavWindowingHighlight] = ImVec4(base2.x, base2.y, base2.z, 0.70f);
c[ImGuiCol_NavWindowingDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
c[ImGuiCol_ModalWindowDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
c[ImGuiCol_PlotLines] = cyan;
c[ImGuiCol_PlotLinesHovered] = blue;
c[ImGuiCol_PlotHistogram] = yellow;
c[ImGuiCol_PlotHistogramHovered] = orange;
}

17
themes/ThemeHelpers.h Normal file
View File

@@ -0,0 +1,17 @@
#ifndef KTE_THEMEHELPERS_H
#define KTE_THEMEHELPERS_H
#include "imgui.h"
// Small helper to convert packed RGB (0xRRGGBB) + optional alpha to ImVec4
static ImVec4
RGBA(const unsigned int rgb, float a = 1.0f)
{
const float r = static_cast<float>(rgb >> 16 & 0xFF) / 255.0f;
const float g = static_cast<float>(rgb >> 8 & 0xFF) / 255.0f;
const float b = static_cast<float>(rgb & 0xFF) / 255.0f;
return {r, g, b, a};
}
#endif //KTE_THEMEHELPERS_H