3 Commits

Author SHA1 Message Date
051106a233 Enable LSP debug logging, expand language feature support, and fix GUI rendering issues.
- Added `--debug` CLI flag to control LSP debug logging and corresponding environment setting.
- Extended LSP capabilities with basic hover, completion, and definition feature support.
- Removed redundant `NoScrollWithMouse` flag, resolving inconsistencies in GUI scrolling behavior.
- Refined variable usage and type consistency across LSP and rendering modules.
- Updated `LspManager` for improved buffer handling and server diagnostics integration.
2025-12-02 01:21:09 -08:00
33bbb5b98f Add SQL, Erlang, and Forth highlighter implementations and tests for LSP process and transport handling.
- Added highlighters for new languages (SQL, Erlang, Forth) with filetype recognition.
- Updated and reorganized syntax files to maintain consistency and modularity.
- Introduced LSP transport framing unit tests and JSON decoding/dispatch tests.
- Refactored `LspManager`, integrating UTF-16/UTF-8 position conversions and robust diagnostics handling.
- Enhanced server start/restart logic with workspace root detection and logging to improve LSP usability.
2025-12-02 00:15:15 -08:00
e089c6e4d1 LSP integration steps 1-4, part of 5. 2025-12-01 20:09:49 -08:00
103 changed files with 33833 additions and 2895 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
!.idea
cmake-build*
build
build-*
/imgui.ini
result

View File

@@ -141,6 +141,13 @@
<pair source="c++m" header="" fileNamingConvention="NONE" />
</extensions>
</files>
<codeStyleSettings language="CMake">
<indentOptions>
<option name="INDENT_SIZE" value="8" />
<option name="TAB_SIZE" value="8" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="ObjectiveC">
<indentOptions>
<option name="INDENT_SIZE" value="8" />

View File

@@ -7,8 +7,9 @@
#include "UndoSystem.h"
#include "UndoTree.h"
// For reconstructing highlighter state on copies
#include "HighlighterRegistry.h"
#include "NullHighlighter.h"
#include "syntax/HighlighterRegistry.h"
#include "syntax/NullHighlighter.h"
#include "lsp/BufferChangeTracker.h"
Buffer::Buffer()
@@ -19,6 +20,9 @@ Buffer::Buffer()
}
Buffer::~Buffer() = default;
Buffer::Buffer(const std::string &path)
{
std::string err;
@@ -394,6 +398,30 @@ Buffer::AsString() const
}
std::string
Buffer::FullText() const
{
std::string out;
// Precompute size for fewer reallocations
std::size_t total = 0;
for (std::size_t i = 0; i < rows_.size(); ++i) {
total += rows_[i].Size();
if (i + 1 < rows_.size())
total += 1; // for '\n'
}
out.reserve(total);
for (std::size_t i = 0; i < rows_.size(); ++i) {
const char *d = rows_[i].Data();
std::size_t n = rows_[i].Size();
if (d && n)
out.append(d, n);
if (i + 1 < rows_.size())
out.push_back('\n');
}
return out;
}
// --- Raw editing APIs (no undo recording, cursor untouched) ---
void
Buffer::insert_text(int row, int col, std::string_view text)
@@ -432,6 +460,9 @@ Buffer::insert_text(int row, int col, std::string_view text)
remain.erase(0, pos + 1);
}
// Do not set dirty here; UndoSystem will manage state/dirty externally
if (change_tracker_) {
change_tracker_->recordInsertion(row, col, std::string(text));
}
}
@@ -470,6 +501,9 @@ Buffer::delete_text(int row, int col, std::size_t len)
break;
}
}
if (change_tracker_) {
change_tracker_->recordDeletion(row, col, len);
}
}
@@ -543,3 +577,17 @@ Buffer::Undo() const
{
return undo_sys_.get();
}
void
Buffer::SetChangeTracker(std::unique_ptr<kte::lsp::BufferChangeTracker> tracker)
{
change_tracker_ = std::move(tracker);
}
kte::lsp::BufferChangeTracker *
Buffer::GetChangeTracker()
{
return change_tracker_.get();
}

View File

@@ -14,14 +14,23 @@
#include "UndoSystem.h"
#include <cstdint>
#include <memory>
#include "HighlighterEngine.h"
#include "syntax/HighlighterEngine.h"
#include "Highlight.h"
// Forward declarations to avoid heavy includes
namespace kte {
namespace lsp {
class BufferChangeTracker;
}
}
class Buffer {
public:
Buffer();
~Buffer();
Buffer(const Buffer &other);
Buffer &operator=(const Buffer &other);
@@ -374,23 +383,59 @@ public:
[[nodiscard]] std::string AsString() const;
// Compose full text of this buffer with newlines between rows
[[nodiscard]] std::string FullText() const;
// Syntax highlighting integration (per-buffer)
[[nodiscard]] std::uint64_t Version() const { return version_; }
[[nodiscard]] std::uint64_t Version() const
{
return version_;
}
void SetSyntaxEnabled(bool on) { syntax_enabled_ = on; }
[[nodiscard]] bool SyntaxEnabled() const { return syntax_enabled_; }
void SetFiletype(const std::string &ft) { filetype_ = ft; }
[[nodiscard]] const std::string &Filetype() const { return filetype_; }
void SetSyntaxEnabled(bool on)
{
syntax_enabled_ = on;
}
[[nodiscard]] bool SyntaxEnabled() const
{
return syntax_enabled_;
}
void SetFiletype(const std::string &ft)
{
filetype_ = ft;
}
[[nodiscard]] const std::string &Filetype() const
{
return filetype_;
}
kte::HighlighterEngine *Highlighter()
{
return highlighter_.get();
}
const kte::HighlighterEngine *Highlighter() const
{
return highlighter_.get();
}
kte::HighlighterEngine *Highlighter() { return highlighter_.get(); }
const kte::HighlighterEngine *Highlighter() const { return highlighter_.get(); }
void EnsureHighlighter()
{
if (!highlighter_) highlighter_ = std::make_unique<kte::HighlighterEngine>();
if (!highlighter_)
highlighter_ = std::make_unique<kte::HighlighterEngine>();
}
// Raw, low-level editing APIs used by UndoSystem apply().
// These must NOT trigger undo recording. They also do not move the cursor.
void insert_text(int row, int col, std::string_view text);
@@ -410,6 +455,11 @@ public:
[[nodiscard]] const UndoSystem *Undo() const;
// LSP integration: optional change tracker
void SetChangeTracker(std::unique_ptr<kte::lsp::BufferChangeTracker> tracker);
kte::lsp::BufferChangeTracker *GetChangeTracker();
private:
// State mirroring original C struct (without undo_tree)
std::size_t curx_ = 0, cury_ = 0; // cursor position in characters
@@ -430,9 +480,12 @@ private:
// Syntax/highlighting state
std::uint64_t version_ = 0; // increment on edits
bool syntax_enabled_ = true;
bool syntax_enabled_ = true;
std::string filetype_;
std::unique_ptr<kte::HighlighterEngine> highlighter_;
// Optional LSP change tracker (absent by default)
std::unique_ptr<kte::lsp::BufferChangeTracker> change_tracker_;
};
#endif // KTE_BUFFER_H
#endif // KTE_BUFFER_H

View File

@@ -16,36 +16,36 @@ option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
if (CMAKE_HOST_UNIX)
message(STATUS "Build system is POSIX.")
message(STATUS "Build system is POSIX.")
else ()
message(STATUS "Build system is NOT POSIX.")
message(STATUS "Build system is NOT POSIX.")
endif ()
if (MSVC)
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
else ()
add_compile_options(
"-Wall"
"-Wextra"
"-Werror"
"$<$<CONFIG:DEBUG>:-g>"
"$<$<CONFIG:RELEASE>:-O2>")
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
add_compile_options("-stdlib=libc++")
else ()
# nothing special for gcc at the moment
endif ()
add_compile_options(
"-Wall"
"-Wextra"
"-Werror"
"$<$<CONFIG:DEBUG>:-g>"
"$<$<CONFIG:RELEASE>:-O2>")
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
add_compile_options("-stdlib=libc++")
else ()
# nothing special for gcc at the moment
endif ()
endif ()
add_compile_definitions(KGE_PLATFORM=${CMAKE_HOST_SYSTEM_NAME})
add_compile_definitions(KTE_VERSION_STR="v${KTE_VERSION}")
if (KTE_ENABLE_TREESITTER)
add_compile_definitions(KTE_ENABLE_TREESITTER)
add_compile_definitions(KTE_ENABLE_TREESITTER)
endif ()
message(STATUS "Build system: ${CMAKE_HOST_SYSTEM_NAME}")
if (${BUILD_GUI})
include(cmake/imgui.cmake)
include(cmake/imgui.cmake)
endif ()
# NCurses for terminal mode
@@ -54,223 +54,368 @@ set(CURSES_NEED_WIDE)
find_package(Curses REQUIRED)
include_directories(${CURSES_INCLUDE_DIR})
# Detect availability of get_wch (wide-char input) in the curses headers
include(CheckSymbolExists)
set(CMAKE_REQUIRED_INCLUDES ${CURSES_INCLUDE_DIR})
check_symbol_exists(get_wch "ncurses.h" KTE_HAVE_GET_WCH_IN_NCURSES)
if (NOT KTE_HAVE_GET_WCH_IN_NCURSES)
# Some systems expose curses headers as <curses.h>
check_symbol_exists(get_wch "curses.h" KTE_HAVE_GET_WCH_IN_CURSES)
endif ()
if (KTE_HAVE_GET_WCH_IN_NCURSES OR KTE_HAVE_GET_WCH_IN_CURSES)
add_compile_definitions(KTE_HAVE_GET_WCH)
endif ()
set(SYNTAX_SOURCES
syntax/HighlighterEngine.cc
syntax/CppHighlighter.cc
syntax/HighlighterRegistry.cc
syntax/NullHighlighter.cc
syntax/JsonHighlighter.cc
syntax/MarkdownHighlighter.cc
syntax/ShellHighlighter.cc
syntax/GoHighlighter.cc
syntax/PythonHighlighter.cc
syntax/RustHighlighter.cc
syntax/LispHighlighter.cc
syntax/SqlHighlighter.cc
syntax/ErlangHighlighter.cc
syntax/ForthHighlighter.cc
)
set(COMMON_SOURCES
GapBuffer.cc
PieceTable.cc
Buffer.cc
Editor.cc
Command.cc
HelpText.cc
KKeymap.cc
TerminalInputHandler.cc
TerminalRenderer.cc
TerminalFrontend.cc
TestInputHandler.cc
TestRenderer.cc
TestFrontend.cc
UndoNode.cc
UndoTree.cc
UndoSystem.cc
HighlighterEngine.cc
CppHighlighter.cc
HighlighterRegistry.cc
NullHighlighter.cc
JsonHighlighter.cc
MarkdownHighlighter.cc
ShellHighlighter.cc
GoHighlighter.cc
PythonHighlighter.cc
RustHighlighter.cc
LispHighlighter.cc
GapBuffer.cc
PieceTable.cc
Buffer.cc
Editor.cc
Command.cc
HelpText.cc
KKeymap.cc
TerminalInputHandler.cc
TerminalRenderer.cc
TerminalFrontend.cc
TestInputHandler.cc
TestRenderer.cc
TestFrontend.cc
UndoNode.cc
UndoTree.cc
UndoSystem.cc
lsp/UtfCodec.cc
lsp/BufferChangeTracker.cc
lsp/JsonRpcTransport.cc
lsp/LspProcessClient.cc
lsp/DiagnosticStore.cc
lsp/TerminalDiagnosticDisplay.cc
lsp/LspManager.cc
${SYNTAX_SOURCES}
)
if (KTE_ENABLE_TREESITTER)
list(APPEND COMMON_SOURCES
TreeSitterHighlighter.cc)
list(APPEND SYNTAX_SOURCES
syntax/TreeSitterHighlighter.cc)
endif ()
set(THEME_HEADERS
themes/EInk.h
themes/Gruvbox.h
themes/Nord.h
themes/Plan9.h
themes/Solarized.h
themes/ThemeHelpers.h
)
set(SYNTAX_HEADERS
syntax/LanguageHighlighter.h
syntax/HighlighterEngine.h
syntax/CppHighlighter.h
syntax/HighlighterRegistry.h
syntax/NullHighlighter.h
syntax/JsonHighlighter.h
syntax/MarkdownHighlighter.h
syntax/ShellHighlighter.h
syntax/GoHighlighter.h
syntax/PythonHighlighter.h
syntax/RustHighlighter.h
syntax/LispHighlighter.h
)
if (KTE_ENABLE_TREESITTER)
list(APPEND SYNTAX_HEADERS
syntax/TreeSitterHighlighter.h)
endif ()
set(COMMON_HEADERS
GapBuffer.h
PieceTable.h
Buffer.h
Editor.h
AppendBuffer.h
Command.h
HelpText.h
KKeymap.h
InputHandler.h
TerminalInputHandler.h
Renderer.h
TerminalRenderer.h
Frontend.h
TerminalFrontend.h
TestInputHandler.h
TestRenderer.h
TestFrontend.h
UndoNode.h
UndoTree.h
UndoSystem.h
Highlight.h
LanguageHighlighter.h
HighlighterEngine.h
CppHighlighter.h
HighlighterRegistry.h
NullHighlighter.h
JsonHighlighter.h
MarkdownHighlighter.h
ShellHighlighter.h
GoHighlighter.h
PythonHighlighter.h
RustHighlighter.h
LispHighlighter.h
)
GapBuffer.h
PieceTable.h
Buffer.h
Editor.h
AppendBuffer.h
Command.h
HelpText.h
KKeymap.h
InputHandler.h
TerminalInputHandler.h
Renderer.h
TerminalRenderer.h
Frontend.h
TerminalFrontend.h
TestInputHandler.h
TestRenderer.h
TestFrontend.h
UndoNode.h
UndoTree.h
UndoSystem.h
Highlight.h
lsp/UtfCodec.h
lsp/LspTypes.h
lsp/BufferChangeTracker.h
lsp/JsonRpcTransport.h
lsp/LspClient.h
lsp/LspProcessClient.h
lsp/Diagnostic.h
lsp/DiagnosticStore.h
lsp/DiagnosticDisplay.h
lsp/TerminalDiagnosticDisplay.h
lsp/LspManager.h
lsp/LspServerConfig.h
ext/json.h
ext/json_fwd.h
if (KTE_ENABLE_TREESITTER)
list(APPEND COMMON_HEADERS
TreeSitterHighlighter.h)
endif ()
${THEME_HEADERS}
${SYNTAX_HEADERS}
)
# kte (terminal-first) executable
add_executable(kte
main.cc
${COMMON_SOURCES}
${COMMON_HEADERS}
main.cc
${COMMON_SOURCES}
${COMMON_HEADERS}
)
if (KTE_USE_PIECE_TABLE)
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
endif ()
if (KTE_UNDO_DEBUG)
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(kte ${CURSES_LIBRARIES})
# Ensure vendored headers (e.g., ext/json.h) are on the include path
target_include_directories(kte PRIVATE ${CMAKE_SOURCE_DIR}/ext)
if (KTE_ENABLE_TREESITTER)
# Users can provide their own tree-sitter include/lib via cache variables
set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)")
if (TREESITTER_INCLUDE_DIR)
target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR})
endif ()
if (TREESITTER_LIBRARY)
target_link_libraries(kte ${TREESITTER_LIBRARY})
endif ()
# 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
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
# Man pages
install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
if (BUILD_TESTS)
# test_undo executable for testing undo/redo system
add_executable(test_undo
test_undo.cc
${COMMON_SOURCES}
${COMMON_HEADERS}
)
# test_undo executable for testing undo/redo system
add_executable(test_undo
test_undo.cc
${COMMON_SOURCES}
${COMMON_HEADERS}
)
if (KTE_USE_PIECE_TABLE)
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
endif ()
if (KTE_USE_PIECE_TABLE)
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
endif ()
if (KTE_UNDO_DEBUG)
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
endif ()
if (KTE_UNDO_DEBUG)
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
endif ()
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 ()
target_link_libraries(test_undo ${CURSES_LIBRARIES})
# Ensure vendored headers (e.g., ext/json.h) are on the include path for tests as well
target_include_directories(test_undo PRIVATE ${CMAKE_SOURCE_DIR}/ext)
if (KTE_ENABLE_TREESITTER)
if (TREESITTER_INCLUDE_DIR)
target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR})
endif ()
if (TREESITTER_LIBRARY)
target_link_libraries(test_undo ${TREESITTER_LIBRARY})
endif ()
endif ()
# test_utfcodec executable for UTF conversion helpers
add_executable(test_utfcodec
test_utfcodec.cc
${COMMON_SOURCES}
${COMMON_HEADERS}
)
if (KTE_USE_PIECE_TABLE)
target_compile_definitions(test_utfcodec PRIVATE KTE_USE_PIECE_TABLE=1)
endif ()
if (KTE_UNDO_DEBUG)
target_compile_definitions(test_utfcodec PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(test_utfcodec ${CURSES_LIBRARIES})
# Ensure vendored headers (e.g., ext/json.h) are on the include path for tests as well
target_include_directories(test_utfcodec PRIVATE ${CMAKE_SOURCE_DIR}/ext)
if (KTE_ENABLE_TREESITTER)
if (TREESITTER_INCLUDE_DIR)
target_include_directories(test_utfcodec PRIVATE ${TREESITTER_INCLUDE_DIR})
endif ()
if (TREESITTER_LIBRARY)
target_link_libraries(test_utfcodec ${TREESITTER_LIBRARY})
endif ()
endif ()
# test_transport executable for JSON-RPC framing
add_executable(test_transport
test_transport.cc
${COMMON_SOURCES}
${COMMON_HEADERS}
)
if (KTE_USE_PIECE_TABLE)
target_compile_definitions(test_transport PRIVATE KTE_USE_PIECE_TABLE=1)
endif ()
if (KTE_UNDO_DEBUG)
target_compile_definitions(test_transport PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(test_transport ${CURSES_LIBRARIES})
# Ensure vendored headers (e.g., ext/json.h) are on the include path for tests as well
target_include_directories(test_transport PRIVATE ${CMAKE_SOURCE_DIR}/ext)
if (KTE_ENABLE_TREESITTER)
if (TREESITTER_INCLUDE_DIR)
target_include_directories(test_transport PRIVATE ${TREESITTER_INCLUDE_DIR})
endif ()
if (TREESITTER_LIBRARY)
target_link_libraries(test_transport ${TREESITTER_LIBRARY})
endif ()
endif ()
# test_lsp_decode executable for dispatcher decoding
add_executable(test_lsp_decode
test_lsp_decode.cc
${COMMON_SOURCES}
${COMMON_HEADERS}
)
if (KTE_USE_PIECE_TABLE)
target_compile_definitions(test_lsp_decode PRIVATE KTE_USE_PIECE_TABLE=1)
endif ()
if (KTE_UNDO_DEBUG)
target_compile_definitions(test_lsp_decode PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(test_lsp_decode ${CURSES_LIBRARIES})
# Ensure vendored headers (e.g., ext/json.h) are on the include path for tests as well
target_include_directories(test_lsp_decode PRIVATE ${CMAKE_SOURCE_DIR}/ext)
if (KTE_ENABLE_TREESITTER)
if (TREESITTER_INCLUDE_DIR)
target_include_directories(test_lsp_decode PRIVATE ${TREESITTER_INCLUDE_DIR})
endif ()
if (TREESITTER_LIBRARY)
target_link_libraries(test_lsp_decode ${TREESITTER_LIBRARY})
endif ()
endif ()
endif ()
if (${BUILD_GUI})
target_sources(kte PRIVATE
Font.h
GUIConfig.cc
GUIConfig.h
GUIRenderer.cc
GUIRenderer.h
GUIInputHandler.cc
GUIInputHandler.h
GUIFrontend.cc
GUIFrontend.h)
target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
target_link_libraries(kte imgui)
target_sources(kte PRIVATE
Font.h
GUIConfig.cc
GUIConfig.h
GUIRenderer.cc
GUIRenderer.h
GUIInputHandler.cc
GUIInputHandler.h
GUIFrontend.cc
GUIFrontend.h)
target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
target_link_libraries(kte imgui)
# kge (GUI-first) executable
add_executable(kge
main.cc
${COMMON_SOURCES}
${COMMON_HEADERS}
GUIConfig.cc
GUIConfig.h
GUIRenderer.cc
GUIRenderer.h
GUIInputHandler.cc
GUIInputHandler.h
GUIFrontend.cc
GUIFrontend.h)
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
if (KTE_UNDO_DEBUG)
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
# kge (GUI-first) executable
add_executable(kge
main.cc
${COMMON_SOURCES}
${COMMON_HEADERS}
GUIConfig.cc
GUIConfig.h
GUIRenderer.cc
GUIRenderer.h
GUIInputHandler.cc
GUIInputHandler.h
GUIFrontend.cc
GUIFrontend.h)
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
if (KTE_UNDO_DEBUG)
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
target_include_directories(kge PRIVATE ${CMAKE_SOURCE_DIR}/ext)
# On macOS, build kge as a proper .app bundle
if (APPLE)
# Define the icon file
set(MACOSX_BUNDLE_ICON_FILE kge.icns)
set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
# On macOS, build kge as a proper .app bundle
if (APPLE)
# Define the icon file
set(MACOSX_BUNDLE_ICON_FILE kge.icns)
set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
# Add icon to the target sources and mark it as a resource
target_sources(kge PRIVATE ${kge_ICON})
set_source_files_properties(${kge_ICON} PROPERTIES
MACOSX_PACKAGE_LOCATION Resources)
# Add icon to the target sources and mark it as a resource
target_sources(kge PRIVATE ${kge_ICON})
set_source_files_properties(${kge_ICON} PROPERTIES
MACOSX_PACKAGE_LOCATION Resources)
# Configure Info.plist with version and identifiers
set(KGE_BUNDLE_ID "dev.wntrmute.kge")
configure_file(
${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in
${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
@ONLY)
# Configure Info.plist with version and identifiers
set(KGE_BUNDLE_ID "dev.wntrmute.kge")
configure_file(
${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in
${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
@ONLY)
set_target_properties(kge PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
MACOSX_BUNDLE_BUNDLE_NAME "kge"
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
set_target_properties(kge PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
MACOSX_BUNDLE_BUNDLE_NAME "kge"
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
add_dependencies(kge kte)
add_custom_command(TARGET kge POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:kte>
$<TARGET_FILE_DIR:kge>/kte
COMMENT "Copying kte binary into kge.app bundle")
add_dependencies(kge kte)
add_custom_command(TARGET kge POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:kte>
$<TARGET_FILE_DIR:kge>/kte
COMMENT "Copying kte binary into kge.app bundle")
install(TARGETS kge
BUNDLE DESTINATION .
)
install(TARGETS kge
BUNDLE DESTINATION .
)
install(TARGETS kte
RUNTIME DESTINATION kge.app/Contents/MacOS
)
else ()
install(TARGETS kge
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
endif ()
# Install kge man page only when GUI is built
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
install(TARGETS kte
RUNTIME DESTINATION kge.app/Contents/MacOS
)
else ()
install(TARGETS kge
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
endif ()
# Install kge man page only when GUI is built
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
endif ()

View File

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

View File

@@ -94,10 +94,13 @@ enum class CommandId {
// Theme by name
ThemeSetByName,
// Background mode (GUI)
BackgroundSet,
// Syntax highlighting
Syntax, // ":syntax on|off|reload"
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
BackgroundSet,
// Syntax highlighting
Syntax, // ":syntax on|off|reload"
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
// LSP
LspHover,
LspGotoDefinition,
};
@@ -151,4 +154,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);
#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

215
Editor.cc
View File

@@ -1,13 +1,14 @@
#include <algorithm>
#include <utility>
#include <filesystem>
#include "HighlighterRegistry.h"
#include "NullHighlighter.h"
#include "syntax/HighlighterRegistry.h"
#include "syntax/NullHighlighter.h"
#include "Editor.h"
#include "HighlighterRegistry.h"
#include "CppHighlighter.h"
#include "NullHighlighter.h"
#include "lsp/LspManager.h"
#include "syntax/HighlighterRegistry.h"
#include "syntax/CppHighlighter.h"
#include "syntax/NullHighlighter.h"
Editor::Editor() = default;
@@ -29,6 +30,15 @@ Editor::SetStatus(const std::string &message)
}
void
Editor::NotifyBufferSaved(Buffer *buf)
{
if (lsp_manager_ && buf) {
lsp_manager_->onBufferSaved(buf);
}
}
Buffer *
Editor::CurrentBuffer()
{
@@ -148,104 +158,115 @@ Editor::OpenFile(const std::string &path, std::string &err)
{
// If there is exactly one unnamed, empty, clean buffer, reuse it instead
// of creating a new one.
if (buffers_.size() == 1) {
Buffer &cur = buffers_[curbuf_];
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
const bool clean = !cur.Dirty();
const auto &rows = cur.Rows();
const bool rows_empty = rows.empty();
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
if (unnamed && clean && (rows_empty || single_empty_line)) {
bool ok = cur.OpenFromFile(path, err);
if (!ok) return false;
// Setup highlighting using registry (extension + shebang)
cur.EnsureHighlighter();
std::string first = "";
const auto &rows = cur.Rows();
if (!rows.empty()) first = static_cast<std::string>(rows[0]);
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
if (!ft.empty()) {
cur.SetFiletype(ft);
cur.SetSyntaxEnabled(true);
if (auto *eng = cur.Highlighter()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
eng->InvalidateFrom(0);
}
} else {
cur.SetFiletype("");
cur.SetSyntaxEnabled(true);
if (auto *eng = cur.Highlighter()) {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
eng->InvalidateFrom(0);
}
}
return true;
}
}
if (buffers_.size() == 1) {
Buffer &cur = buffers_[curbuf_];
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
const bool clean = !cur.Dirty();
const auto &rows = cur.Rows();
const bool rows_empty = rows.empty();
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
if (unnamed && clean && (rows_empty || single_empty_line)) {
bool ok = cur.OpenFromFile(path, err);
if (!ok)
return false;
// Setup highlighting using registry (extension + shebang)
cur.EnsureHighlighter();
std::string first = "";
const auto &rows = cur.Rows();
if (!rows.empty())
first = static_cast<std::string>(rows[0]);
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
if (!ft.empty()) {
cur.SetFiletype(ft);
cur.SetSyntaxEnabled(true);
if (auto *eng = cur.Highlighter()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
eng->InvalidateFrom(0);
}
} else {
cur.SetFiletype("");
cur.SetSyntaxEnabled(true);
if (auto *eng = cur.Highlighter()) {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
eng->InvalidateFrom(0);
}
}
// Notify LSP (if wired) for current buffer open
if (lsp_manager_) {
lsp_manager_->onBufferOpened(&cur);
}
return true;
}
}
Buffer b;
if (!b.OpenFromFile(path, err)) {
return false;
}
// Initialize syntax highlighting by extension + shebang via registry (v2)
b.EnsureHighlighter();
std::string first = "";
{
const auto &rows = b.Rows();
if (!rows.empty()) first = static_cast<std::string>(rows[0]);
}
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
if (!ft.empty()) {
b.SetFiletype(ft);
b.SetSyntaxEnabled(true);
if (auto *eng = b.Highlighter()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
eng->InvalidateFrom(0);
}
} else {
b.SetFiletype("");
b.SetSyntaxEnabled(true);
if (auto *eng = b.Highlighter()) {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
eng->InvalidateFrom(0);
}
}
// Add as a new buffer and switch to it
std::size_t idx = AddBuffer(std::move(b));
SwitchTo(idx);
return true;
Buffer b;
if (!b.OpenFromFile(path, err)) {
return false;
}
// Initialize syntax highlighting by extension + shebang via registry (v2)
b.EnsureHighlighter();
std::string first = "";
{
const auto &rows = b.Rows();
if (!rows.empty())
first = static_cast<std::string>(rows[0]);
}
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
if (!ft.empty()) {
b.SetFiletype(ft);
b.SetSyntaxEnabled(true);
if (auto *eng = b.Highlighter()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
eng->InvalidateFrom(0);
}
} else {
b.SetFiletype("");
b.SetSyntaxEnabled(true);
if (auto *eng = b.Highlighter()) {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
eng->InvalidateFrom(0);
}
}
// Add as a new buffer and switch to it
std::size_t idx = AddBuffer(std::move(b));
SwitchTo(idx);
// Notify LSP (if wired) for current buffer open
if (lsp_manager_) {
lsp_manager_->onBufferOpened(&buffers_[curbuf_]);
}
return true;
}
bool
Editor::SwitchTo(std::size_t index)
{
if (index >= buffers_.size()) {
return false;
}
curbuf_ = index;
// Robustness: ensure a valid highlighter is installed when switching buffers
Buffer &b = buffers_[curbuf_];
if (b.SyntaxEnabled()) {
b.EnsureHighlighter();
if (auto *eng = b.Highlighter()) {
if (!eng->HasHighlighter()) {
// Try to set based on existing filetype; fall back to NullHighlighter
if (!b.Filetype().empty()) {
auto hl = kte::HighlighterRegistry::CreateFor(b.Filetype());
if (hl) {
eng->SetHighlighter(std::move(hl));
} else {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
} else {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
eng->InvalidateFrom(0);
}
}
}
return true;
if (index >= buffers_.size()) {
return false;
}
curbuf_ = index;
// Robustness: ensure a valid highlighter is installed when switching buffers
Buffer &b = buffers_[curbuf_];
if (b.SyntaxEnabled()) {
b.EnsureHighlighter();
if (auto *eng = b.Highlighter()) {
if (!eng->HasHighlighter()) {
// Try to set based on existing filetype; fall back to NullHighlighter
if (!b.Filetype().empty()) {
auto hl = kte::HighlighterRegistry::CreateFor(b.Filetype());
if (hl) {
eng->SetHighlighter(std::move(hl));
} else {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
} else {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
eng->InvalidateFrom(0);
}
}
}
return true;
}
@@ -281,4 +302,4 @@ Editor::Reset()
quit_confirm_pending_ = false;
buffers_.clear();
curbuf_ = 0;
}
}

View File

@@ -11,6 +11,13 @@
#include "Buffer.h"
// fwd decl for LSP wiring
namespace kte {
namespace lsp {
class LspManager;
}
}
class Editor {
public:
@@ -436,6 +443,22 @@ public:
bool OpenFile(const std::string &path, std::string &err);
// LSP: attach/detach manager
void SetLspManager(kte::lsp::LspManager *mgr)
{
lsp_manager_ = mgr;
}
// LSP helpers: trigger hover/definition at current cursor in current buffer
bool LspHoverAtCursor();
bool LspGotoDefinitionAtCursor();
// LSP: notify buffer saved (used by commands)
void NotifyBufferSaved(Buffer *buf);
// Buffer switching/closing
bool SwitchTo(std::size_t index);
@@ -551,6 +574,9 @@ public:
private:
std::string replace_find_tmp_;
std::string replace_with_tmp_;
// Non-owning pointer to LSP manager (if provided)
kte::lsp::LspManager *lsp_manager_ = nullptr;
};
#endif // KTE_EDITOR_H
#endif // KTE_EDITOR_H

View File

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

View File

@@ -12,18 +12,18 @@
class GUIConfig {
public:
bool fullscreen = false;
int columns = 80;
int rows = 42;
float font_size = (float) KTE_FONT_SIZE;
std::string theme = "nord";
// Background mode for themes that support light/dark variants
// Values: "dark" (default), "light"
std::string background = "dark";
bool fullscreen = false;
int columns = 80;
int rows = 42;
float font_size = (float) KTE_FONT_SIZE;
std::string theme = "nord";
// Background mode for themes that support light/dark variants
// Values: "dark" (default), "light"
std::string background = "dark";
// Default syntax highlighting state for GUI (kge): on/off
// Accepts: on/off/true/false/yes/no/1/0 in the ini file.
bool syntax = true; // default: enabled
// Default syntax highlighting state for GUI (kge): on/off
// Accepts: on/off/true/false/yes/no/1/0 in the ini file.
bool syntax = true; // default: enabled
// Load from default path: $HOME/.config/kte/kge.ini
static GUIConfig Load();
@@ -32,4 +32,4 @@ public:
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 "GUIConfig.h"
#include "GUITheme.h"
#include "HighlighterRegistry.h"
#include "NullHighlighter.h"
#include "syntax/HighlighterRegistry.h"
#include "syntax/NullHighlighter.h"
#ifndef KTE_FONT_SIZE
@@ -108,42 +108,44 @@ GUIFrontend::Init(Editor &ed)
(void) io;
ImGui::StyleColorsDark();
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
if (cfg.background == "light")
kte::SetBackgroundMode(kte::BackgroundMode::Light);
else
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
kte::ApplyThemeByName(cfg.theme);
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
if (cfg.background == "light")
kte::SetBackgroundMode(kte::BackgroundMode::Light);
else
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
kte::ApplyThemeByName(cfg.theme);
// Apply default syntax highlighting preference from GUI config to the current buffer
if (Buffer *b = ed.CurrentBuffer()) {
if (cfg.syntax) {
b->SetSyntaxEnabled(true);
// Ensure a highlighter is available if possible
b->EnsureHighlighter();
if (auto *eng = b->Highlighter()) {
if (!eng->HasHighlighter()) {
// Try detect from filename and first line; fall back to cpp or existing filetype
std::string first_line;
const auto &rows = b->Rows();
if (!rows.empty()) first_line = static_cast<std::string>(rows[0]);
std::string ft = kte::HighlighterRegistry::DetectForPath(b->Filename(), first_line);
if (!ft.empty()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
b->SetFiletype(ft);
eng->InvalidateFrom(0);
} else {
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
b->SetFiletype("");
eng->InvalidateFrom(0);
}
}
}
} else {
b->SetSyntaxEnabled(false);
}
}
// Apply default syntax highlighting preference from GUI config to the current buffer
if (Buffer *b = ed.CurrentBuffer()) {
if (cfg.syntax) {
b->SetSyntaxEnabled(true);
// Ensure a highlighter is available if possible
b->EnsureHighlighter();
if (auto *eng = b->Highlighter()) {
if (!eng->HasHighlighter()) {
// Try detect from filename and first line; fall back to cpp or existing filetype
std::string first_line;
const auto &rows = b->Rows();
if (!rows.empty())
first_line = static_cast<std::string>(rows[0]);
std::string ft = kte::HighlighterRegistry::DetectForPath(
b->Filename(), first_line);
if (!ft.empty()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
b->SetFiletype(ft);
eng->InvalidateFrom(0);
} else {
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
b->SetFiletype("");
eng->InvalidateFrom(0);
}
}
}
} else {
b->SetSyntaxEnabled(false);
}
}
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
return false;
@@ -316,4 +318,4 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
}
// No runtime font reload or system font resolution in this simplified build.
// No runtime font reload or system font resolution in this simplified build.

View File

@@ -47,7 +47,6 @@ GUIRenderer::Draw(Editor &ed)
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoScrollbar
| ImGuiWindowFlags_NoScrollWithMouse
| ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoCollapse
@@ -60,7 +59,7 @@ GUIRenderer::Draw(Editor &ed)
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.f, 6.f));
ImGui::Begin("kte", nullptr, flags);
ImGui::Begin("kte", nullptr, flags | ImGuiWindowFlags_NoScrollWithMouse);
const Buffer *buf = ed.CurrentBuffer();
if (!buf) {
@@ -69,7 +68,7 @@ GUIRenderer::Draw(Editor &ed)
const auto &lines = buf->Rows();
// Reserve space for status bar at bottom
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGuiWindowFlags_HorizontalScrollbar);
// Detect click-to-move inside this scroll region
ImVec2 list_origin = ImGui::GetCursorScreenPos();
float scroll_y = ImGui::GetScrollY();
@@ -109,13 +108,13 @@ GUIRenderer::Draw(Editor &ed)
}
// If user scrolled, update buffer offsets accordingly
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
if (auto mbuf = const_cast<Buffer *>(buf)) {
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
mbuf->Coloffs());
}
}
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x) {
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
if (auto mbuf = const_cast<Buffer *>(buf)) {
mbuf->SetOffsets(mbuf->Rowoffs(),
static_cast<std::size_t>(std::max(0L, scroll_left)));
}
@@ -139,34 +138,36 @@ GUIRenderer::Draw(Editor &ed)
vis_rows = 1;
long last_row = first_row + vis_rows - 1;
if (!forced_scroll) {
long cyr = static_cast<long>(cy);
if (cyr < first_row || cyr > last_row) {
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
float max_y = ImGui::GetScrollMaxY();
if (target < 0.f)
target = 0.f;
if (max_y >= 0.f && target > max_y)
target = max_y;
ImGui::SetScrollY(target);
// refresh local variables
scroll_y = ImGui::GetScrollY();
first_row = static_cast<long>(scroll_y / row_h);
last_row = first_row + vis_rows - 1;
}
}
// Phase 3: prefetch visible viewport highlights and warm around in background
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
int fr = static_cast<int>(std::max(0L, first_row));
int rc = static_cast<int>(std::max(1L, vis_rows));
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
}
}
// Handle mouse click before rendering to avoid dependent on drawn items
if (!forced_scroll) {
long cyr = static_cast<long>(cy);
if (cyr < first_row || cyr > last_row) {
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
float max_y = ImGui::GetScrollMaxY();
if (target < 0.f)
target = 0.f;
if (max_y >= 0.f && target > max_y)
target = max_y;
ImGui::SetScrollY(target);
// refresh local variables
scroll_y = ImGui::GetScrollY();
first_row = static_cast<long>(scroll_y / row_h);
last_row = first_row + vis_rows - 1;
}
}
// Phase 3: prefetch visible viewport highlights and warm around in background
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
int fr = static_cast<int>(std::max(0L, first_row));
int rc = static_cast<int>(std::max(1L, vis_rows));
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
}
}
// Handle mouse click before rendering to avoid dependency on drawn items
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImVec2 mp = ImGui::GetIO().MousePos;
// Compute viewport-relative row so (0) is top row of the visible area
float vy_f = (mp.y - list_origin.y - scroll_y) / row_h;
// Note: list_origin is already in the scrolled space of the child window,
// so we must NOT subtract scroll_y again (would double-apply).
float vy_f = (mp.y - list_origin.y) / row_h;
long vy = static_cast<long>(vy_f);
if (vy < 0)
vy = 0;
@@ -190,8 +191,9 @@ GUIRenderer::Draw(Editor &ed)
by = 0;
}
// Compute desired pixel X inside the viewport content (subtract horizontal scroll)
float px = (mp.x - list_origin.x - scroll_x);
// Compute desired pixel X inside the viewport content.
// list_origin is already scrolled; do not subtract scroll_x here.
float px = (mp.x - list_origin.x);
if (px < 0.0f)
px = 0.0f;
@@ -254,11 +256,11 @@ GUIRenderer::Draw(Editor &ed)
const std::size_t coloffs_now = buf->Coloffs();
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
// Capture the screen position before drawing the line
ImVec2 line_pos = ImGui::GetCursorScreenPos();
std::string line = static_cast<std::string>(lines[i]);
ImVec2 line_pos = ImGui::GetCursorScreenPos();
auto line = static_cast<std::string>(lines[i]);
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
const std::size_t tabw = 8;
constexpr std::size_t tabw = 8;
std::string expanded;
expanded.reserve(line.size() + 16);
std::size_t rx_abs_draw = 0; // rendered column for drawing
@@ -275,7 +277,7 @@ GUIRenderer::Draw(Editor &ed)
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
it != std::sregex_iterator(); ++it) {
const auto &m = *it;
std::size_t sx = static_cast<std::size_t>(m.position());
auto sx = static_cast<std::size_t>(m.position());
std::size_t ex = sx + static_cast<std::size_t>(m.length());
hl_src_ranges.emplace_back(sx, ex);
}
@@ -318,9 +320,9 @@ GUIRenderer::Draw(Editor &ed)
continue; // fully left of view
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
std::size_t vx1 = rx_end - coloffs_now;
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
line_pos.y + line_h);
auto p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
auto p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
line_pos.y + line_h);
// Choose color: current match stronger
bool is_current = has_current && sx == cur_x && ex == cur_end;
ImU32 col = is_current
@@ -329,50 +331,58 @@ GUIRenderer::Draw(Editor &ed)
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
}
}
// Emit entire line to an expanded buffer (tabs -> spaces)
for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src];
if (c == '\t') {
std::size_t adv = (tabw - (rx_abs_draw % tabw));
expanded.append(adv, ' ');
rx_abs_draw += adv;
} else {
expanded.push_back(c);
rx_abs_draw += 1;
}
}
// Emit entire line to an expanded buffer (tabs -> spaces)
for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src];
if (c == '\t') {
std::size_t adv = (tabw - (rx_abs_draw % tabw));
expanded.append(adv, ' ');
rx_abs_draw += adv;
} else {
expanded.push_back(c);
rx_abs_draw += 1;
}
}
// Draw syntax-colored runs (text above background highlights)
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(*buf, static_cast<int>(i), buf->Version());
// Helper to convert a src column to expanded rx position
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
std::size_t rx = 0;
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
}
return rx;
};
for (const auto &sp: lh.spans) {
std::size_t rx_s = src_to_rx_full(static_cast<std::size_t>(std::max(0, sp.col_start)));
std::size_t rx_e = src_to_rx_full(static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
if (rx_e <= coloffs_now)
continue;
std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
if (vx0 >= expanded.size()) continue;
vx1 = std::min<std::size_t>(vx1, expanded.size());
if (vx1 <= vx0) continue;
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
ImGui::GetWindowDrawList()->AddText(p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
}
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + line_h));
} else {
// No syntax: draw as one run
ImGui::TextUnformatted(expanded.c_str());
}
// Draw syntax-colored runs (text above background highlights)
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(
*buf, static_cast<int>(i), buf->Version());
// Helper to convert a src column to expanded rx position
auto src_to_rx_full = [&](const std::size_t sidx) -> std::size_t {
std::size_t rx = 0;
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
}
return rx;
};
for (const auto &sp: lh.spans) {
std::size_t rx_s = src_to_rx_full(
static_cast<std::size_t>(std::max(0, sp.col_start)));
std::size_t rx_e = src_to_rx_full(
static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
if (rx_e <= coloffs_now)
continue;
std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
if (vx0 >= expanded.size())
continue;
vx1 = std::min<std::size_t>(vx1, expanded.size());
if (vx1 <= vx0)
continue;
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
auto p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
ImGui::GetWindowDrawList()->AddText(
p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
}
// We drew text via draw list (no layout advance). Advance by the same amount
// ImGui uses between lines (line height + spacing) so hit-testing (which
// divides by row_h) aligns with drawing.
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
} else {
// No syntax: draw as one run
ImGui::TextUnformatted(expanded.c_str());
}
// Draw a visible cursor indicator on the current line
if (i == cy) {
@@ -411,9 +421,9 @@ GUIRenderer::Draw(Editor &ed)
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
// If a prompt is active, replace the entire status bar with the prompt text
if (ed.PromptActive()) {
std::string label = ed.PromptLabel();
std::string ptext = ed.PromptText();
auto kind = ed.CurrentPromptKind();
const std::string &label = ed.PromptLabel();
std::string ptext = ed.PromptText();
auto kind = ed.CurrentPromptKind();
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
kind == Editor::PromptKind::Chdir) {
const char *home_c = std::getenv("HOME");
@@ -460,8 +470,8 @@ GUIRenderer::Draw(Editor &ed)
float ratio = tail_sz.x / avail_px;
size_t skip = ratio > 1.5f
? std::min(tail.size() - start,
(size_t) std::max<size_t>(
1, (size_t) (tail.size() / 4)))
static_cast<size_t>(std::max<size_t>(
1, tail.size() / 4)))
: 1;
start += skip;
std::string candidate = tail.substr(start);
@@ -518,8 +528,7 @@ GUIRenderer::Draw(Editor &ed)
left += " ";
// Insert buffer position prefix "[x/N] " before filename
{
std::size_t total = ed.BufferCount();
if (total > 0) {
if (std::size_t total = ed.BufferCount(); total > 0) {
std::size_t idx1 = ed.CurrentBufferIndex() + 1; // 1-based for display
left += "[";
left += std::to_string(static_cast<unsigned long long>(idx1));
@@ -533,7 +542,7 @@ GUIRenderer::Draw(Editor &ed)
left += " *";
// Append total line count as "<n>L"
{
unsigned long lcount = static_cast<unsigned long>(buf->Rows().size());
auto lcount = buf->Rows().size();
left += " ";
left += std::to_string(lcount);
left += "L";
@@ -619,9 +628,9 @@ GUIRenderer::Draw(Editor &ed)
ImGuiViewport *vp2 = ImGui::GetMainViewport();
// Desired size, min size, and margins
const ImVec2 want(800.0f, 500.0f);
const ImVec2 min_sz(240.0f, 160.0f);
const float margin = 20.0f; // space from viewport edges
constexpr ImVec2 want(800.0f, 500.0f);
constexpr ImVec2 min_sz(240.0f, 160.0f);
constexpr float margin = 20.0f; // space from viewport edges
// Compute the maximum allowed size (viewport minus margins) and make sure it's not negative
ImVec2 max_sz(std::max(32.0f, vp2->Size.x - 2.0f * margin),

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

View File

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

View File

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

View File

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

View File

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

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

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

@@ -1,6 +1,12 @@
#include <ncurses.h>
#include <clocale>
#include <termios.h>
#include <unistd.h>
#ifdef __APPLE__
#include <xlocale.h>
#endif
#include <langinfo.h>
#include <cctype>
#include "TerminalFrontend.h"
#include "Command.h"
@@ -10,6 +16,35 @@
bool
TerminalFrontend::Init(Editor &ed)
{
// Enable UTF-8 locale so ncurses and the terminal handle multibyte correctly
// This relies on the user's environment (e.g., LANG/LC_ALL) being set to a UTF-8 locale.
// If not set, try a couple of common UTF-8 fallbacks.
const char *loc = std::setlocale(LC_ALL, "");
auto is_utf8_codeset = []() -> bool {
const char *cs = nl_langinfo(CODESET);
if (!cs)
return false;
std::string s(cs);
for (auto &ch: s)
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
return (s.find("UTF-8") != std::string::npos) || (s.find("UTF8") != std::string::npos);
};
bool utf8_ok = (MB_CUR_MAX > 1) && is_utf8_codeset();
if (!utf8_ok) {
// Try common UTF-8 locales
loc = std::setlocale(LC_CTYPE, "C.UTF-8");
utf8_ok = (loc != nullptr) && (MB_CUR_MAX > 1) && is_utf8_codeset();
if (!utf8_ok) {
loc = std::setlocale(LC_CTYPE, "en_US.UTF-8");
utf8_ok = (loc != nullptr) && (MB_CUR_MAX > 1) && is_utf8_codeset();
}
if (!utf8_ok) {
// macOS often uses plain "UTF-8" locale identifier
loc = std::setlocale(LC_CTYPE, "UTF-8");
utf8_ok = (loc != nullptr) && (MB_CUR_MAX > 1) && is_utf8_codeset();
}
}
// Ensure Control keys reach the app: disable XON/XOFF and dsusp/susp bindings (e.g., ^S/^Q, ^Y on macOS)
{
struct termios tio{};
@@ -55,6 +90,9 @@ TerminalFrontend::Init(Editor &ed)
prev_r_ = r;
prev_c_ = c;
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
// Inform renderer of UTF-8 capability so it can choose proper output path
renderer_.SetUtf8Enabled(utf8_ok);
input_.SetUtf8Enabled(utf8_ok);
return true;
}
@@ -100,4 +138,4 @@ TerminalFrontend::Shutdown()
have_orig_tio_ = false;
}
endwin();
}
}

View File

@@ -1,4 +1,6 @@
#include <cstdio>
#include <cwchar>
#include <climits>
#include <ncurses.h>
#include "TerminalInputHandler.h"
@@ -36,18 +38,48 @@ map_key_to_command(const int ch,
MEVENT ev{};
if (getmouse(&ev) == OK) {
// Mouse wheel → map to MoveUp/MoveDown one line per wheel notch
unsigned long wheel_up_mask = 0;
unsigned long wheel_dn_mask = 0;
#ifdef BUTTON4_PRESSED
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
out = {true, CommandId::MoveUp, "", 0};
return true;
}
wheel_up_mask |= BUTTON4_PRESSED;
#endif
#ifdef BUTTON4_RELEASED
wheel_up_mask |= BUTTON4_RELEASED;
#endif
#ifdef BUTTON4_CLICKED
wheel_up_mask |= BUTTON4_CLICKED;
#endif
#ifdef BUTTON4_DOUBLE_CLICKED
wheel_up_mask |= BUTTON4_DOUBLE_CLICKED;
#endif
#ifdef BUTTON4_TRIPLE_CLICKED
wheel_up_mask |= BUTTON4_TRIPLE_CLICKED;
#endif
#ifdef BUTTON5_PRESSED
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
out = {true, CommandId::MoveDown, "", 0};
wheel_dn_mask |= BUTTON5_PRESSED;
#endif
#ifdef BUTTON5_RELEASED
wheel_dn_mask |= BUTTON5_RELEASED;
#endif
#ifdef BUTTON5_CLICKED
wheel_dn_mask |= BUTTON5_CLICKED;
#endif
#ifdef BUTTON5_DOUBLE_CLICKED
wheel_dn_mask |= BUTTON5_DOUBLE_CLICKED;
#endif
#ifdef BUTTON5_TRIPLE_CLICKED
wheel_dn_mask |= BUTTON5_TRIPLE_CLICKED;
#endif
if (wheel_up_mask && (ev.bstate & wheel_up_mask)) {
// Prefer viewport scrolling for wheel: page up
out = {true, CommandId::PageUp, "", 0};
return true;
}
if (wheel_dn_mask && (ev.bstate & wheel_dn_mask)) {
// Prefer viewport scrolling for wheel: page down
out = {true, CommandId::PageDown, "", 0};
return true;
}
#endif
// React to left button click/press
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
char buf[64];
@@ -281,6 +313,77 @@ map_key_to_command(const int ch,
bool
TerminalInputHandler::decode_(MappedInput &out)
{
#if defined(KTE_HAVE_GET_WCH)
if (utf8_enabled_) {
// Prefer wide-character input so we can capture Unicode code points
wint_t wch = 0;
int rc = get_wch(&wch);
if (rc == ERR) {
return false;
}
if (rc == KEY_CODE_YES) {
// Function/special key; pass through existing mapper
int sk = static_cast<int>(wch);
bool consumed = map_key_to_command(
sk,
k_prefix_, esc_meta_,
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_,
uarg_text_,
out);
if (!consumed)
return false;
} else {
// Regular character
if (wch <= 0x7F) {
// ASCII path -> reuse existing mapping (handles control, ESC, etc.)
int ch = static_cast<int>(wch);
bool consumed = map_key_to_command(
ch,
k_prefix_, esc_meta_,
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_,
uarg_text_,
out);
if (!consumed)
return false;
} else {
// Non-ASCII printable -> insert UTF-8 text directly
if (iswcntrl(static_cast<wint_t>(wch))) {
out.hasCommand = false;
return true;
}
char mb[MB_LEN_MAX];
mbstate_t st{};
std::size_t n = wcrtomb(mb, static_cast<wchar_t>(wch), &st);
if (n == static_cast<std::size_t>(-1)) {
// Fallback placeholder if encoding failed
out.hasCommand = true;
out.id = CommandId::InsertText;
out.arg = "?";
out.count = 0;
} else {
out.hasCommand = true;
out.id = CommandId::InsertText;
out.arg.assign(mb, mb + n);
out.count = 0;
}
}
}
} else {
int ch = getch();
if (ch == ERR) {
return false; // no input
}
bool consumed = map_key_to_command(
ch,
k_prefix_, esc_meta_,
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_, uarg_text_,
out);
if (!consumed)
return false;
}
#else
// Wide-character input not available in this curses; fall back to byte-wise getch
(void) utf8_enabled_;
int ch = getch();
if (ch == ERR) {
return false; // no input
@@ -292,6 +395,7 @@ TerminalInputHandler::decode_(MappedInput &out)
out);
if (!consumed)
return false;
#endif
// If a command was produced and a universal argument is active, attach it and clear state
if (out.hasCommand && uarg_active_ && out.id != CommandId::UArgStatus) {
int count = 0;
@@ -320,4 +424,4 @@ TerminalInputHandler::Poll(MappedInput &out)
{
out = {};
return decode_(out) && out.hasCommand;
}
}

View File

@@ -15,6 +15,12 @@ public:
bool Poll(MappedInput &out) override;
void SetUtf8Enabled(bool on)
{
utf8_enabled_ = on;
}
private:
bool decode_(MappedInput &out);
@@ -30,6 +36,8 @@ private:
bool uarg_had_digits_ = false; // whether any digits were supplied
int uarg_value_ = 0; // current absolute value (>=0)
std::string uarg_text_; // raw digits/minus typed for status display
bool utf8_enabled_ = true;
};
#endif // KTE_TERMINAL_INPUT_HANDLER_H
#endif // KTE_TERMINAL_INPUT_HANDLER_H

View File

@@ -4,6 +4,7 @@
#include <cstdlib>
#include <ncurses.h>
#include <regex>
#include <cwchar>
#include <string>
#include "TerminalRenderer.h"
@@ -42,18 +43,18 @@ TerminalRenderer::Draw(Editor &ed)
std::size_t coloffs = buf->Coloffs();
const int tabw = 8;
// Phase 3: prefetch visible viewport highlights (current terminal area)
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
int fr = static_cast<int>(rowoffs);
int rc = std::max(0, content_rows);
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
}
// Phase 3: prefetch visible viewport highlights (current terminal area)
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
int fr = static_cast<int>(rowoffs);
int rc = std::max(0, content_rows);
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
}
for (int r = 0; r < content_rows; ++r) {
move(r, 0);
std::size_t li = rowoffs + static_cast<std::size_t>(r);
std::size_t render_col = 0;
std::size_t src_i = 0;
for (int r = 0; r < content_rows; ++r) {
move(r, 0);
std::size_t li = rowoffs + static_cast<std::size_t>(r);
std::size_t render_col = 0;
std::size_t src_i = 0;
// Compute matches for this line if search highlighting is active
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
std::vector<std::pair<std::size_t, std::size_t> > ranges; // [start, end)
@@ -105,49 +106,55 @@ TerminalRenderer::Draw(Editor &ed)
bool hl_on = false;
bool cur_on = false;
int written = 0;
if (li < lines.size()) {
std::string line = static_cast<std::string>(lines[li]);
src_i = 0;
render_col = 0;
// Syntax highlighting: fetch per-line spans
const kte::LineHighlight *lh_ptr = nullptr;
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
lh_ptr = &buf->Highlighter()->GetLine(*buf, static_cast<int>(li), buf->Version());
}
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
if (!lh_ptr) return kte::TokenKind::Default;
for (const auto &sp: lh_ptr->spans) {
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(src_index) < sp.col_end)
return sp.kind;
}
return kte::TokenKind::Default;
};
auto apply_token_attr = [&](kte::TokenKind k) {
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
attrset(A_NORMAL);
switch (k) {
case kte::TokenKind::Keyword:
case kte::TokenKind::Type:
case kte::TokenKind::Constant:
case kte::TokenKind::Function:
attron(A_BOLD);
break;
case kte::TokenKind::Comment:
attron(A_DIM);
break;
case kte::TokenKind::String:
case kte::TokenKind::Char:
case kte::TokenKind::Number:
// standout a bit using A_UNDERLINE if available
attron(A_UNDERLINE);
break;
default:
break;
}
};
while (written < cols) {
char ch = ' ';
bool from_src = false;
if (li < lines.size()) {
std::string line = static_cast<std::string>(lines[li]);
src_i = 0;
render_col = 0;
// Syntax highlighting: fetch per-line spans
const kte::LineHighlight *lh_ptr = nullptr;
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
HasHighlighter()) {
lh_ptr = &buf->Highlighter()->GetLine(
*buf, static_cast<int>(li), buf->Version());
}
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
if (!lh_ptr)
return kte::TokenKind::Default;
for (const auto &sp: lh_ptr->spans) {
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(
src_index) < sp.col_end)
return sp.kind;
}
return kte::TokenKind::Default;
};
auto apply_token_attr = [&](kte::TokenKind k) {
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
attrset(A_NORMAL);
switch (k) {
case kte::TokenKind::Keyword:
case kte::TokenKind::Type:
case kte::TokenKind::Constant:
case kte::TokenKind::Function:
attron(A_BOLD);
break;
case kte::TokenKind::Comment:
attron(A_DIM);
break;
case kte::TokenKind::String:
case kte::TokenKind::Char:
case kte::TokenKind::Number:
// standout a bit using A_UNDERLINE if available
attron(A_UNDERLINE);
break;
default:
break;
}
};
while (written < cols) {
// Default to space when beyond EOL
bool from_src = false;
int wcw = 1; // display width
std::size_t advance_bytes = 0;
if (src_i < line.size()) {
unsigned char c = static_cast<unsigned char>(line[src_i]);
if (c == '\t') {
@@ -166,102 +173,139 @@ TerminalRenderer::Draw(Editor &ed)
next_tab -= to_skip;
}
// Now render visible spaces
while (next_tab > 0 && written < cols) {
bool in_hl = search_mode && is_src_in_hl(src_i);
bool in_cur =
has_current && li == cur_my && src_i >= cur_mx
&& src_i < cur_mend;
// Toggle highlight attributes
int attr = 0;
if (in_hl)
attr |= A_STANDOUT;
if (in_cur)
attr |= A_BOLD;
if ((attr & A_STANDOUT) && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!(attr & A_STANDOUT) && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if ((attr & A_BOLD) && !cur_on) {
attron(A_BOLD);
cur_on = true;
}
if (!(attr & A_BOLD) && cur_on) {
attroff(A_BOLD);
cur_on = false;
}
// Apply syntax attribute only if not in search highlight
if (!in_hl) {
apply_token_attr(token_at(src_i));
}
addch(' ');
++written;
++render_col;
--next_tab;
}
++src_i;
continue;
} else {
// normal char
if (render_col < coloffs) {
while (next_tab > 0 && written < cols) {
bool in_hl = search_mode && is_src_in_hl(src_i);
bool in_cur =
has_current && li == cur_my && src_i >= cur_mx
&& src_i < cur_mend;
// Toggle highlight attributes
int attr = 0;
if (in_hl)
attr |= A_STANDOUT;
if (in_cur)
attr |= A_BOLD;
if ((attr & A_STANDOUT) && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!(attr & A_STANDOUT) && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if ((attr & A_BOLD) && !cur_on) {
attron(A_BOLD);
cur_on = true;
}
if (!(attr & A_BOLD) && cur_on) {
attroff(A_BOLD);
cur_on = false;
}
// Apply syntax attribute only if not in search highlight
if (!in_hl) {
apply_token_attr(token_at(src_i));
}
addch(' ');
++written;
++render_col;
++src_i;
continue;
--next_tab;
}
++src_i;
continue;
} else {
if (!Utf8Enabled()) {
// ASCII fallback: treat each byte as single width
if (render_col + 1 <= coloffs) {
++render_col;
++src_i;
continue;
}
wcw = 1;
advance_bytes = 1;
from_src = true;
} else {
// Decode one UTF-8 codepoint
mbstate_t st{};
const char *p = line.data() + src_i;
std::size_t rem = line.size() - src_i;
wchar_t tmp_wc = 0;
std::size_t n = mbrtowc(&tmp_wc, p, rem, &st);
if (n == static_cast<std::size_t>(-1) || n ==
static_cast<std::size_t>(-2) || n == 0) {
// Invalid/incomplete -> treat as single-byte placeholder
tmp_wc = L'?';
n = 1;
}
int w = wcwidth(tmp_wc);
if (w < 0)
w = 1;
// If this codepoint is scrolled off to the left, skip it
if (render_col + static_cast<std::size_t>(w) <=
coloffs) {
render_col += static_cast<std::size_t>(w);
src_i += n;
continue;
}
wcw = w;
advance_bytes = n;
from_src = true;
}
ch = static_cast<char>(c);
from_src = true;
}
} else {
// beyond EOL, fill spaces
ch = ' ';
from_src = false;
}
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
bool in_cur =
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
cur_mend;
if (in_hl && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!in_hl && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if (in_cur && !cur_on) {
attron(A_BOLD);
cur_on = true;
}
if (!in_cur && cur_on) {
attroff(A_BOLD);
cur_on = false;
}
if (!in_hl && from_src) {
apply_token_attr(token_at(src_i));
}
addch(static_cast<unsigned char>(ch));
++written;
++render_col;
if (from_src)
++src_i;
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
bool in_cur =
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
cur_mend;
if (in_hl && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!in_hl && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if (in_cur && !cur_on) {
attron(A_BOLD);
cur_on = true;
}
if (!in_cur && cur_on) {
attroff(A_BOLD);
cur_on = false;
}
if (!in_hl && from_src) {
apply_token_attr(token_at(src_i));
}
if (written + wcw > cols) {
break;
}
if (from_src) {
// Output original bytes for this unit (UTF-8 codepoint or ASCII byte)
const char *cp = line.data() + (src_i);
int out_n = Utf8Enabled() ? static_cast<int>(advance_bytes) : 1;
addnstr(cp, out_n);
src_i += static_cast<std::size_t>(out_n);
} else {
addch(' ');
}
written += wcw;
render_col += wcw;
if (src_i >= line.size() && written >= cols)
break;
}
}
if (hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if (cur_on) {
attroff(A_BOLD);
cur_on = false;
}
attrset(A_NORMAL);
clrtoeol();
}
if (hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if (cur_on) {
attroff(A_BOLD);
cur_on = false;
}
attrset(A_NORMAL);
clrtoeol();
}
// Place terminal cursor at logical position accounting for tabs and coloffs
std::size_t cy = buf->Cury();
@@ -418,6 +462,10 @@ TerminalRenderer::Draw(Editor &ed)
else
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
right = rbuf;
// If UTF-8 is not enabled (ASCII fallback), append a short hint
if (!Utf8Enabled()) {
right += " | ASCII";
}
}
// Compute placements with truncation rules: prioritize left and right; middle gets remaining
@@ -464,4 +512,4 @@ TerminalRenderer::Draw(Editor &ed)
}
refresh();
}
}

View File

@@ -14,6 +14,21 @@ public:
~TerminalRenderer() override;
void Draw(Editor &ed) override;
// Enable/disable UTF-8 aware rendering (set by TerminalFrontend after locale init)
void SetUtf8Enabled(bool on)
{
utf8_enabled_ = on;
}
[[nodiscard]] bool Utf8Enabled() const
{
return utf8_enabled_;
}
private:
bool utf8_enabled_ = true;
};
#endif // KTE_TERMINAL_RENDERER_H
#endif // KTE_TERMINAL_RENDERER_H

View File

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

25626
ext/json.h Normal file

File diff suppressed because it is too large Load Diff

185
ext/json_fwd.h Normal file
View File

@@ -0,0 +1,185 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2025 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_
#define INCLUDE_NLOHMANN_JSON_FWD_HPP_
#include <cstdint> // int64_t, uint64_t
#include <map> // map
#include <memory> // allocator
#include <string> // string
#include <vector> // vector
// #include <nlohmann/detail/abi_macros.hpp>
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2025 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
// This file contains all macro definitions affecting or depending on the ABI
#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK
#if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH)
#if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0
#warning "Already included a different version of the library!"
#endif
#endif
#endif
#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum)
#define NLOHMANN_JSON_VERSION_MINOR 12 // NOLINT(modernize-macro-to-enum)
#define NLOHMANN_JSON_VERSION_PATCH 0 // NOLINT(modernize-macro-to-enum)
#ifndef JSON_DIAGNOSTICS
#define JSON_DIAGNOSTICS 0
#endif
#ifndef JSON_DIAGNOSTIC_POSITIONS
#define JSON_DIAGNOSTIC_POSITIONS 0
#endif
#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
#define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0
#endif
#if JSON_DIAGNOSTICS
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
#else
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS
#endif
#if JSON_DIAGNOSTIC_POSITIONS
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp
#else
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS
#endif
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp
#else
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
#endif
// Construct the namespace ABI tags component
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c)
#define NLOHMANN_JSON_ABI_TAGS \
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \
NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \
NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS)
// Construct the namespace version component
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
_v ## major ## _ ## minor ## _ ## patch
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch)
#if NLOHMANN_JSON_NAMESPACE_NO_VERSION
#define NLOHMANN_JSON_NAMESPACE_VERSION
#else
#define NLOHMANN_JSON_NAMESPACE_VERSION \
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \
NLOHMANN_JSON_VERSION_MINOR, \
NLOHMANN_JSON_VERSION_PATCH)
#endif
// Combine namespace components
#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b
#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \
NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b)
#ifndef NLOHMANN_JSON_NAMESPACE
#define NLOHMANN_JSON_NAMESPACE \
nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \
NLOHMANN_JSON_ABI_TAGS, \
NLOHMANN_JSON_NAMESPACE_VERSION)
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN
#define NLOHMANN_JSON_NAMESPACE_BEGIN \
namespace nlohmann \
{ \
inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \
NLOHMANN_JSON_ABI_TAGS, \
NLOHMANN_JSON_NAMESPACE_VERSION) \
{
#endif
#ifndef NLOHMANN_JSON_NAMESPACE_END
#define NLOHMANN_JSON_NAMESPACE_END \
} /* namespace (inline namespace) NOLINT(readability/namespace) */ \
} // namespace nlohmann
#endif
/*!
@brief namespace for Niels Lohmann
@see https://github.com/nlohmann
@since version 1.0.0
*/
NLOHMANN_JSON_NAMESPACE_BEGIN
/*!
@brief default JSONSerializer template argument
This serializer ignores the template arguments and uses ADL
([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl))
for serialization.
*/
template<typename T = void, typename SFINAE = void>
struct adl_serializer;
/// a class to store JSON values
/// @sa https://json.nlohmann.me/api/basic_json/
template<template<typename U, typename V, typename... Args> class ObjectType =
std::map,
template<typename U, typename... Args> class ArrayType = std::vector,
class StringType = std::string, class BooleanType = bool,
class NumberIntegerType = std::int64_t,
class NumberUnsignedType = std::uint64_t,
class NumberFloatType = double,
template<typename U> class AllocatorType = std::allocator,
template<typename T, typename SFINAE = void> class JSONSerializer =
adl_serializer,
class BinaryType = std::vector<std::uint8_t>, // cppcheck-suppress syntaxError
class CustomBaseClass = void>
class basic_json;
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
/// @sa https://json.nlohmann.me/api/json_pointer/
template<typename RefStringType>
class json_pointer;
/*!
@brief default specialization
@sa https://json.nlohmann.me/api/json/
*/
using json = basic_json<>;
/// @brief a minimal map-like container that preserves insertion order
/// @sa https://json.nlohmann.me/api/ordered_map/
template<class Key, class T, class IgnoredLess, class Allocator>
struct ordered_map;
/// @brief specialization that maintains the insertion order of object keys
/// @sa https://json.nlohmann.me/api/ordered_json/
using ordered_json = basic_json<nlohmann::ordered_map>;
NLOHMANN_JSON_NAMESPACE_END
#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_

View File

@@ -6,4 +6,4 @@ then
fmt_args="-fmt 3"
fi
ls -1 *.cc *.h | grep -v '^Font.h$' | xargs cloc ${fmt_args}
ls -1 *.cc *.h lsp/*.{cc,h} | grep -v '^Font.h$' | xargs cloc ${fmt_args}

View File

@@ -0,0 +1,49 @@
/*
* BufferChangeTracker.cc - minimal initial implementation
*/
#include "BufferChangeTracker.h"
#include "../Buffer.h"
namespace kte::lsp {
BufferChangeTracker::BufferChangeTracker(const Buffer *buffer)
: buffer_(buffer) {}
void
BufferChangeTracker::recordInsertion(int /*row*/, int /*col*/, const std::string &/*text*/)
{
// For Phase 12 bring-up, coalesce to full-document changes
fullChangePending_ = true;
++version_;
}
void
BufferChangeTracker::recordDeletion(int /*row*/, int /*col*/, std::size_t /*len*/)
{
fullChangePending_ = true;
++version_;
}
std::vector<TextDocumentContentChangeEvent>
BufferChangeTracker::getChanges() const
{
std::vector<TextDocumentContentChangeEvent> v;
if (!buffer_)
return v;
if (fullChangePending_) {
TextDocumentContentChangeEvent ev;
ev.text = buffer_->FullText();
v.push_back(std::move(ev));
}
return v;
}
void
BufferChangeTracker::clearChanges()
{
fullChangePending_ = false;
}
} // namespace kte::lsp

44
lsp/BufferChangeTracker.h Normal file
View File

@@ -0,0 +1,44 @@
/*
* BufferChangeTracker.h - integrates with Buffer to accumulate LSP-friendly changes
*/
#ifndef KTE_BUFFER_CHANGE_TRACKER_H
#define KTE_BUFFER_CHANGE_TRACKER_H
#include <memory>
#include <vector>
#include <string>
#include "LspTypes.h"
class Buffer; // forward declare from core
namespace kte::lsp {
class BufferChangeTracker {
public:
explicit BufferChangeTracker(const Buffer *buffer);
// Called by Buffer on each edit operation
void recordInsertion(int row, int col, const std::string &text);
void recordDeletion(int row, int col, std::size_t len);
// Get accumulated changes since last sync
std::vector<TextDocumentContentChangeEvent> getChanges() const;
// Clear changes after sending to LSP
void clearChanges();
// Get current document version for LSP
int getVersion() const
{
return version_;
}
private:
const Buffer *buffer_ = nullptr;
bool fullChangePending_ = false;
int version_ = 0;
};
} // namespace kte::lsp
#endif // KTE_BUFFER_CHANGE_TRACKER_H

37
lsp/Diagnostic.h Normal file
View File

@@ -0,0 +1,37 @@
/*
* Diagnostic.h - LSP diagnostic data types
*/
#ifndef KTE_LSP_DIAGNOSTIC_H
#define KTE_LSP_DIAGNOSTIC_H
#include <optional>
#include <string>
#include <vector>
#include "LspTypes.h"
namespace kte::lsp {
enum class DiagnosticSeverity {
Error = 1,
Warning = 2,
Information = 3,
Hint = 4
};
struct DiagnosticRelatedInformation {
std::string uri; // related location URI
Range range; // related range
std::string message;
};
struct Diagnostic {
Range range{};
DiagnosticSeverity severity{DiagnosticSeverity::Information};
std::optional<std::string> code;
std::optional<std::string> source;
std::string message;
std::vector<DiagnosticRelatedInformation> relatedInfo;
};
} // namespace kte::lsp
#endif // KTE_LSP_DIAGNOSTIC_H

30
lsp/DiagnosticDisplay.h Normal file
View File

@@ -0,0 +1,30 @@
/*
* DiagnosticDisplay.h - Abstract interface for showing diagnostics
*/
#ifndef KTE_LSP_DIAGNOSTIC_DISPLAY_H
#define KTE_LSP_DIAGNOSTIC_DISPLAY_H
#include <string>
#include <vector>
#include "Diagnostic.h"
namespace kte::lsp {
class DiagnosticDisplay {
public:
virtual ~DiagnosticDisplay() = default;
virtual void updateDiagnostics(const std::string &uri,
const std::vector<Diagnostic> &diagnostics) = 0;
virtual void showInlineDiagnostic(const Diagnostic &diagnostic) = 0;
virtual void showDiagnosticList(const std::vector<Diagnostic> &diagnostics) = 0;
virtual void hideDiagnosticList() = 0;
virtual void updateStatusBar(int errorCount, int warningCount) = 0;
};
} // namespace kte::lsp
#endif // KTE_LSP_DIAGNOSTIC_DISPLAY_H

123
lsp/DiagnosticStore.cc Normal file
View File

@@ -0,0 +1,123 @@
/*
* DiagnosticStore.cc - implementation
*/
#include "DiagnosticStore.h"
#include <algorithm>
namespace kte::lsp {
void
DiagnosticStore::setDiagnostics(const std::string &uri, std::vector<Diagnostic> diagnostics)
{
diagnostics_[uri] = std::move(diagnostics);
}
const std::vector<Diagnostic> &
DiagnosticStore::getDiagnostics(const std::string &uri) const
{
auto it = diagnostics_.find(uri);
static const std::vector<Diagnostic> kEmpty;
if (it == diagnostics_.end())
return kEmpty;
return it->second;
}
std::vector<Diagnostic>
DiagnosticStore::getDiagnosticsAtLine(const std::string &uri, int line) const
{
std::vector<Diagnostic> out;
auto it = diagnostics_.find(uri);
if (it == diagnostics_.end())
return out;
out.reserve(it->second.size());
for (const auto &d: it->second) {
if (containsLine(d.range, line))
out.push_back(d);
}
return out;
}
std::optional<Diagnostic>
DiagnosticStore::getDiagnosticAtPosition(const std::string &uri, Position pos) const
{
auto it = diagnostics_.find(uri);
if (it == diagnostics_.end())
return std::nullopt;
for (const auto &d: it->second) {
if (containsPosition(d.range, pos))
return d;
}
return std::nullopt;
}
void
DiagnosticStore::clear(const std::string &uri)
{
diagnostics_.erase(uri);
}
void
DiagnosticStore::clearAll()
{
diagnostics_.clear();
}
int
DiagnosticStore::getErrorCount(const std::string &uri) const
{
auto it = diagnostics_.find(uri);
if (it == diagnostics_.end())
return 0;
int count = 0;
for (const auto &d: it->second) {
if (d.severity == DiagnosticSeverity::Error)
++count;
}
return count;
}
int
DiagnosticStore::getWarningCount(const std::string &uri) const
{
auto it = diagnostics_.find(uri);
if (it == diagnostics_.end())
return 0;
int count = 0;
for (const auto &d: it->second) {
if (d.severity == DiagnosticSeverity::Warning)
++count;
}
return count;
}
bool
DiagnosticStore::containsLine(const Range &r, int line)
{
return (line > r.start.line || line == r.start.line) &&
(line < r.end.line || line == r.end.line);
}
bool
DiagnosticStore::containsPosition(const Range &r, const Position &p)
{
if (p.line < r.start.line || p.line > r.end.line)
return false;
if (r.start.line == r.end.line) {
return p.line == r.start.line && p.character >= r.start.character && p.character <= r.end.character;
}
if (p.line == r.start.line)
return p.character >= r.start.character;
if (p.line == r.end.line)
return p.character <= r.end.character;
return true; // between start and end lines
}
} // namespace kte::lsp

42
lsp/DiagnosticStore.h Normal file
View File

@@ -0,0 +1,42 @@
/*
* DiagnosticStore.h - Central storage for diagnostics by document URI
*/
#ifndef KTE_LSP_DIAGNOSTIC_STORE_H
#define KTE_LSP_DIAGNOSTIC_STORE_H
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "Diagnostic.h"
namespace kte::lsp {
class DiagnosticStore {
public:
void setDiagnostics(const std::string &uri, std::vector<Diagnostic> diagnostics);
const std::vector<Diagnostic> &getDiagnostics(const std::string &uri) const;
std::vector<Diagnostic> getDiagnosticsAtLine(const std::string &uri, int line) const;
std::optional<Diagnostic> getDiagnosticAtPosition(const std::string &uri, Position pos) const;
void clear(const std::string &uri);
void clearAll();
int getErrorCount(const std::string &uri) const;
int getWarningCount(const std::string &uri) const;
private:
std::unordered_map<std::string, std::vector<Diagnostic> > diagnostics_;
static bool containsLine(const Range &r, int line);
static bool containsPosition(const Range &r, const Position &p);
};
} // namespace kte::lsp
#endif // KTE_LSP_DIAGNOSTIC_STORE_H

147
lsp/JsonRpcTransport.cc Normal file
View File

@@ -0,0 +1,147 @@
/*
* JsonRpcTransport.cc - minimal stdio JSON-RPC framing (Content-Length)
*/
#include "JsonRpcTransport.h"
#include <cerrno>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <string>
#include <optional>
#include <unistd.h>
namespace kte::lsp {
void
JsonRpcTransport::connect(int inFd, int outFd)
{
inFd_ = inFd;
outFd_ = outFd;
}
void
JsonRpcTransport::send(const std::string &/*method*/, const std::string &payload)
{
if (outFd_ < 0)
return;
const std::string header = "Content-Length: " + std::to_string(payload.size()) + "\r\n\r\n";
std::lock_guard<std::mutex> lk(writeMutex_);
// write header
const char *hbuf = header.data();
size_t hleft = header.size();
while (hleft > 0) {
ssize_t n = ::write(outFd_, hbuf, hleft);
if (n < 0) {
if (errno == EINTR)
continue;
return;
}
hbuf += static_cast<size_t>(n);
hleft -= static_cast<size_t>(n);
}
// write payload
const char *pbuf = payload.data();
size_t pleft = payload.size();
while (pleft > 0) {
ssize_t n = ::write(outFd_, pbuf, pleft);
if (n < 0) {
if (errno == EINTR)
continue;
return;
}
pbuf += static_cast<size_t>(n);
pleft -= static_cast<size_t>(n);
}
}
static bool
readLineCrlf(int fd, std::string &out, size_t maxLen)
{
out.clear();
char ch;
while (true) {
ssize_t n = ::read(fd, &ch, 1);
if (n == 0)
return false; // EOF
if (n < 0) {
if (errno == EINTR)
continue;
return false;
}
out.push_back(ch);
// Handle CRLF or bare LF as end-of-line
if ((out.size() >= 2 && out[out.size() - 2] == '\r' && out[out.size() - 1] == '\n') ||
(out.size() >= 1 && out[out.size() - 1] == '\n')) {
return true;
}
if (out.size() > maxLen) {
// sanity cap
return false;
}
}
}
std::optional<JsonRpcMessage>
JsonRpcTransport::read()
{
if (inFd_ < 0)
return std::nullopt;
// Parse headers (case-insensitive), accept/ignore extras
size_t contentLength = 0;
while (true) {
std::string line;
if (!readLineCrlf(inFd_, line, kMaxHeaderLine))
return std::nullopt;
// Normalize end-of-line handling: consider blank line as end of headers
if (line == "\r\n" || line == "\n" || line == "\r")
break;
// Trim trailing CRLF
if (!line.empty() && (line.back() == '\n' || line.back() == '\r')) {
while (!line.empty() && (line.back() == '\n' || line.back() == '\r'))
line.pop_back();
}
// Find colon
auto pos = line.find(':');
if (pos == std::string::npos)
continue;
std::string name = line.substr(0, pos);
std::string value = line.substr(pos + 1);
// trim leading spaces in value
size_t i = 0;
while (i < value.size() && (value[i] == ' ' || value[i] == '\t'))
++i;
value.erase(0, i);
// lower-case name for comparison
for (auto &c: name)
c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
if (name == "content-length") {
size_t len = static_cast<size_t>(std::strtoull(value.c_str(), nullptr, 10));
if (len > kMaxBody) {
return std::nullopt; // drop too-large message
}
contentLength = len;
}
// else: ignore other headers
}
if (contentLength == 0)
return std::nullopt;
std::string body;
body.resize(contentLength);
size_t readTotal = 0;
while (readTotal < contentLength) {
ssize_t n = ::read(inFd_, &body[readTotal], contentLength - readTotal);
if (n == 0)
return std::nullopt;
if (n < 0) {
if (errno == EINTR)
continue;
return std::nullopt;
}
readTotal += static_cast<size_t>(n);
}
return JsonRpcMessage{std::move(body)};
}
} // namespace kte::lsp

43
lsp/JsonRpcTransport.h Normal file
View File

@@ -0,0 +1,43 @@
/*
* JsonRpcTransport.h - minimal JSON-RPC over stdio transport
*/
#ifndef KTE_JSON_RPC_TRANSPORT_H
#define KTE_JSON_RPC_TRANSPORT_H
#include <optional>
#include <string>
#include <mutex>
namespace kte::lsp {
struct JsonRpcMessage {
std::string raw; // raw JSON payload (stub)
};
class JsonRpcTransport {
public:
JsonRpcTransport() = default;
~JsonRpcTransport() = default;
// Connect this transport to file descriptors (read from inFd, write to outFd)
void connect(int inFd, int outFd);
// Send a method call (request or notification)
// 'payload' should be a complete JSON object string to send as the message body.
void send(const std::string &method, const std::string &payload);
// Blocking read next message; returns nullopt on EOF or error
std::optional<JsonRpcMessage> read();
private:
int inFd_ = -1;
int outFd_ = -1;
std::mutex writeMutex_;
// Limits to keep the transport resilient
static constexpr size_t kMaxHeaderLine = 16 * 1024; // 16 KiB per header line
static constexpr size_t kMaxBody = 64ull * 1024ull * 1024ull; // 64 MiB body cap
};
} // namespace kte::lsp
#endif // KTE_JSON_RPC_TRANSPORT_H

75
lsp/LspClient.h Normal file
View File

@@ -0,0 +1,75 @@
/*
* LspClient.h - Core LSP client abstraction (initial stub)
*/
#ifndef KTE_LSP_CLIENT_H
#define KTE_LSP_CLIENT_H
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "LspTypes.h"
#include "Diagnostic.h"
namespace kte::lsp {
// Callback types for initial language features
// If error is non-empty, the result may be default-constructed/empty
using CompletionCallback = std::function<void(const CompletionList & result, const std::string & error)>;
using HoverCallback = std::function<void(const HoverResult & result, const std::string & error)>;
using LocationCallback = std::function<void(const std::vector<Location> & result, const std::string & error)>;
class LspClient {
public:
virtual ~LspClient() = default;
// Lifecycle
virtual bool initialize(const std::string &rootPath) = 0;
virtual void shutdown() = 0;
// Document Synchronization
virtual void didOpen(const std::string &uri, const std::string &languageId,
int version, const std::string &text) = 0;
virtual void didChange(const std::string &uri, int version,
const std::vector<TextDocumentContentChangeEvent> &changes) = 0;
virtual void didClose(const std::string &uri) = 0;
virtual void didSave(const std::string &uri) = 0;
// Language Features (initial)
virtual void completion(const std::string &, Position,
CompletionCallback) {}
virtual void hover(const std::string &, Position,
HoverCallback) {}
virtual void definition(const std::string &, Position,
LocationCallback) {}
// Process Management
virtual bool isRunning() const = 0;
virtual std::string getServerName() const = 0;
// Handlers (optional; set by manager)
using DiagnosticsHandler = std::function<void(const std::string & uri,
const std::vector<Diagnostic> &diagnostics
)
>;
virtual void setDiagnosticsHandler(DiagnosticsHandler h)
{
(void) h;
}
};
} // namespace kte::lsp
#endif // KTE_LSP_CLIENT_H

736
lsp/LspManager.cc Normal file
View File

@@ -0,0 +1,736 @@
/*
* LspManager.cc - central coordination of LSP servers and diagnostics
*/
#include "LspManager.h"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <utility>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <cstdarg>
#include "../Buffer.h"
#include "../Editor.h"
#include "BufferChangeTracker.h"
#include "LspProcessClient.h"
#include "UtfCodec.h"
namespace fs = std::filesystem;
namespace kte::lsp {
static void
lsp_debug_file(const char *fmt, ...)
{
FILE *f = std::fopen("/tmp/kte-lsp.log", "a");
if (!f)
return;
// prepend timestamp
std::time_t t = std::time(nullptr);
char ts[32];
std::strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
std::fprintf(f, "[%s] ", ts);
va_list ap;
va_start(ap, fmt);
std::vfprintf(f, fmt, ap);
va_end(ap);
std::fputc('\n', f);
std::fclose(f);
}
LspManager::LspManager(Editor *editor, DiagnosticDisplay *display)
: editor_(editor), display_(display)
{
// Pre-populate with sensible default server configs
registerDefaultServers();
}
void
LspManager::registerServer(const std::string &languageId, const LspServerConfig &config)
{
serverConfigs_[languageId] = config;
}
bool
LspManager::startServerForBuffer(Buffer *buffer)
{
const auto lang = getLanguageId(buffer);
if (lang.empty())
return false;
if (servers_.find(lang) != servers_.end() && servers_[lang]->isRunning()) {
return true;
}
auto it = serverConfigs_.find(lang);
if (it == serverConfigs_.end()) {
return false;
}
const auto &cfg = it->second;
// Respect autostart for automatic starts on buffer open
if (!cfg.autostart) {
return false;
}
// Allow env override of server path
std::string command = cfg.command;
if (lang == "cpp") {
if (const char *p = std::getenv("KTE_LSP_CLANGD"); p && *p)
command = p;
} else if (lang == "go") {
if (const char *p = std::getenv("KTE_LSP_GOPLS"); p && *p)
command = p;
} else if (lang == "rust") {
if (const char *p = std::getenv("KTE_LSP_RUST_ANALYZER"); p && *p)
command = p;
}
if (debug_) {
std::fprintf(stderr, "[kte][lsp] startServerForBuffer: lang=%s cmd=%s args=%zu file=%s\n",
lang.c_str(), command.c_str(), cfg.args.size(), buffer->Filename().c_str());
lsp_debug_file("startServerForBuffer: lang=%s cmd=%s args=%zu file=%s",
lang.c_str(), command.c_str(), cfg.args.size(), buffer->Filename().c_str());
}
auto client = std::make_unique<LspProcessClient>(command, cfg.args);
// Wire diagnostics handler to manager
client->setDiagnosticsHandler([this](const std::string &uri, const std::vector<Diagnostic> &diags) {
this->handleDiagnostics(uri, diags);
});
// Determine workspace root using rootPatterns if set; fallback to file's parent
std::string rootPath;
if (!buffer->Filename().empty()) {
rootPath = detectWorkspaceRoot(buffer->Filename(), cfg);
if (rootPath.empty()) {
fs::path p(buffer->Filename());
rootPath = p.has_parent_path() ? p.parent_path().string() : std::string{};
}
}
if (debug_) {
const char *pathEnv = std::getenv("PATH");
std::fprintf(stderr, "[kte][lsp] initializing server: rootPath=%s PATH=%s\n",
rootPath.c_str(), pathEnv ? pathEnv : "<null>");
lsp_debug_file("initializing server: rootPath=%s PATH=%s",
rootPath.c_str(), pathEnv ? pathEnv : "<null>");
}
if (!client->initialize(rootPath)) {
if (debug_) {
std::fprintf(stderr, "[kte][lsp] initialize failed for lang=%s\n", lang.c_str());
lsp_debug_file("initialize failed for lang=%s", lang.c_str());
}
return false;
}
servers_[lang] = std::move(client);
return true;
}
void
LspManager::stopServer(const std::string &languageId)
{
auto it = servers_.find(languageId);
if (it != servers_.end()) {
it->second->shutdown();
servers_.erase(it);
}
}
void
LspManager::stopAllServers()
{
for (auto &kv: servers_) {
kv.second->shutdown();
}
servers_.clear();
}
bool
LspManager::startServerForLanguage(const std::string &languageId, const std::string &rootPath)
{
auto cfgIt = serverConfigs_.find(languageId);
if (cfgIt == serverConfigs_.end())
return false;
// If already running, nothing to do
auto it = servers_.find(languageId);
if (it != servers_.end() && it->second && it->second->isRunning()) {
return true;
}
const auto &cfg = cfgIt->second;
std::string command = cfg.command;
if (languageId == "cpp") {
if (const char *p = std::getenv("KTE_LSP_CLANGD"); p && *p)
command = p;
} else if (languageId == "go") {
if (const char *p = std::getenv("KTE_LSP_GOPLS"); p && *p)
command = p;
} else if (languageId == "rust") {
if (const char *p = std::getenv("KTE_LSP_RUST_ANALYZER"); p && *p)
command = p;
}
if (debug_) {
std::fprintf(stderr, "[kte][lsp] startServerForLanguage: lang=%s cmd=%s args=%zu root=%s\n",
languageId.c_str(), command.c_str(), cfg.args.size(), rootPath.c_str());
lsp_debug_file("startServerForLanguage: lang=%s cmd=%s args=%zu root=%s",
languageId.c_str(), command.c_str(), cfg.args.size(), rootPath.c_str());
}
auto client = std::make_unique<LspProcessClient>(command, cfg.args);
client->setDiagnosticsHandler([this](const std::string &uri, const std::vector<Diagnostic> &diags) {
this->handleDiagnostics(uri, diags);
});
std::string root = rootPath;
if (!root.empty()) {
// keep
} else {
// Try cwd if not provided
root = std::string();
}
if (!client->initialize(root)) {
if (debug_) {
std::fprintf(stderr, "[kte][lsp] initialize failed for lang=%s\n", languageId.c_str());
lsp_debug_file("initialize failed for lang=%s", languageId.c_str());
}
return false;
}
servers_[languageId] = std::move(client);
return true;
}
bool
LspManager::restartServer(const std::string &languageId, const std::string &rootPath)
{
stopServer(languageId);
return startServerForLanguage(languageId, rootPath);
}
void
LspManager::onBufferOpened(Buffer *buffer)
{
if (debug_) {
std::fprintf(stderr, "[kte][lsp] onBufferOpened: file=%s lang=%s\n",
buffer->Filename().c_str(), getLanguageId(buffer).c_str());
lsp_debug_file("onBufferOpened: file=%s lang=%s",
buffer->Filename().c_str(), getLanguageId(buffer).c_str());
}
if (!startServerForBuffer(buffer)) {
if (debug_) {
std::fprintf(stderr, "[kte][lsp] onBufferOpened: server did not start\n");
lsp_debug_file("onBufferOpened: server did not start");
}
return;
}
auto *client = ensureServerForLanguage(getLanguageId(buffer));
if (!client)
return;
const auto uri = getUri(buffer);
const auto lang = getLanguageId(buffer);
const int version = static_cast<int>(buffer->Version());
const std::string text = buffer->FullText();
if (debug_) {
std::fprintf(stderr, "[kte][lsp] didOpen: uri=%s lang=%s version=%d bytes=%zu\n",
uri.c_str(), lang.c_str(), version, text.size());
lsp_debug_file("didOpen: uri=%s lang=%s version=%d bytes=%zu",
uri.c_str(), lang.c_str(), version, text.size());
}
client->didOpen(uri, lang, version, text);
}
void
LspManager::onBufferChanged(Buffer *buffer)
{
auto *client = ensureServerForLanguage(getLanguageId(buffer));
if (!client)
return;
const auto uri = getUri(buffer);
int version = static_cast<int>(buffer->Version());
std::vector<TextDocumentContentChangeEvent> changes;
if (auto *tracker = buffer->GetChangeTracker()) {
changes = tracker->getChanges();
tracker->clearChanges();
version = tracker->getVersion();
} else {
// Fallback: full document change
TextDocumentContentChangeEvent ev;
ev.range.reset();
ev.text = buffer->FullText();
changes.push_back(std::move(ev));
}
// Option A: convert ranges from UTF-8 (editor coords) -> UTF-16 (LSP wire)
std::vector<TextDocumentContentChangeEvent> changes16;
changes16.reserve(changes.size());
// LineProvider that serves lines from this buffer by URI
Buffer *bufForUri = buffer; // changes are for this buffer
auto provider = [bufForUri](const std::string &/*u*/, int line) -> std::string_view {
if (!bufForUri)
return std::string_view();
const auto &rows = bufForUri->Rows();
if (line < 0 || static_cast<size_t>(line) >= rows.size())
return std::string_view();
// Materialize one line into a thread_local scratch; return view
thread_local std::string scratch;
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
return std::string_view(scratch);
};
for (const auto &ch: changes) {
TextDocumentContentChangeEvent out = ch;
if (ch.range.has_value()) {
Range r16 = toUtf16(uri, *ch.range, provider);
if (debug_) {
lsp_debug_file("didChange range convert: L%d C%d-%d -> L%d C%d-%d",
ch.range->start.line, ch.range->start.character,
ch.range->end.character,
r16.start.line, r16.start.character, r16.end.character);
}
out.range = r16;
}
changes16.push_back(std::move(out));
}
client->didChange(uri, version, changes16);
}
void
LspManager::onBufferClosed(Buffer *buffer)
{
auto *client = ensureServerForLanguage(getLanguageId(buffer));
if (!client)
return;
client->didClose(getUri(buffer));
// Clear diagnostics for this file
diagnosticStore_.clear(getUri(buffer));
}
void
LspManager::onBufferSaved(Buffer *buffer)
{
auto *client = ensureServerForLanguage(getLanguageId(buffer));
if (!client)
return;
client->didSave(getUri(buffer));
}
void
LspManager::requestCompletion(Buffer *buffer, Position pos, CompletionCallback callback)
{
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
const auto uri = getUri(buffer);
// Convert position to UTF-16 using Option A provider
auto provider = [buffer](const std::string &/*u*/, int line) -> std::string_view {
if (!buffer)
return std::string_view();
const auto &rows = buffer->Rows();
if (line < 0 || static_cast<size_t>(line) >= rows.size())
return std::string_view();
thread_local std::string scratch;
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
return std::string_view(scratch);
};
Position p16 = toUtf16(uri, pos, provider);
if (debug_) {
lsp_debug_file("completion pos convert: L%d C%d -> L%d C%d", pos.line, pos.character, p16.line,
p16.character);
}
client->completion(uri, p16, std::move(callback));
}
}
void
LspManager::requestHover(Buffer *buffer, Position pos, HoverCallback callback)
{
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
const auto uri = getUri(buffer);
auto provider = [buffer](const std::string &/*u*/, int line) -> std::string_view {
if (!buffer)
return std::string_view();
const auto &rows = buffer->Rows();
if (line < 0 || static_cast<size_t>(line) >= rows.size())
return std::string_view();
thread_local std::string scratch;
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
return std::string_view(scratch);
};
Position p16 = toUtf16(uri, pos, provider);
if (debug_) {
lsp_debug_file("hover pos convert: L%d C%d -> L%d C%d", pos.line, pos.character, p16.line,
p16.character);
}
// Wrap the callback to convert any returned range from UTF-16 (wire) -> UTF-8 (editor)
HoverCallback wrapped = [this, uri, provider, cb = std::move(callback)](const HoverResult &res16,
const std::string &err) {
if (!cb)
return;
if (!res16.range.has_value()) {
cb(res16, err);
return;
}
HoverResult res8 = res16;
res8.range = toUtf8(uri, *res16.range, provider);
if (debug_) {
const auto &r16 = *res16.range;
const auto &r8 = *res8.range;
lsp_debug_file("hover range convert: L%d %d-%d -> L%d %d-%d",
r16.start.line, r16.start.character, r16.end.character,
r8.start.line, r8.start.character, r8.end.character);
}
cb(res8, err);
};
client->hover(uri, p16, std::move(wrapped));
}
}
void
LspManager::requestDefinition(Buffer *buffer, Position pos, LocationCallback callback)
{
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
const auto uri = getUri(buffer);
auto provider = [buffer](const std::string &/*u*/, int line) -> std::string_view {
if (!buffer)
return std::string_view();
const auto &rows = buffer->Rows();
if (line < 0 || static_cast<size_t>(line) >= rows.size())
return std::string_view();
thread_local std::string scratch;
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
return std::string_view(scratch);
};
Position p16 = toUtf16(uri, pos, provider);
if (debug_) {
lsp_debug_file("definition pos convert: L%d C%d -> L%d C%d", pos.line, pos.character, p16.line,
p16.character);
}
// Wrap callback to convert Location ranges from UTF-16 (wire) -> UTF-8 (editor)
LocationCallback wrapped = [this, uri, provider, cb = std::move(callback)](
const std::vector<Location> &locs16,
const std::string &err) {
if (!cb)
return;
std::vector<Location> locs8;
locs8.reserve(locs16.size());
for (const auto &l: locs16) {
Location x = l;
x.range = toUtf8(uri, l.range, provider);
if (debug_) {
lsp_debug_file("definition range convert: L%d %d-%d -> L%d %d-%d",
l.range.start.line, l.range.start.character,
l.range.end.character,
x.range.start.line, x.range.start.character,
x.range.end.character);
}
locs8.push_back(std::move(x));
}
cb(locs8, err);
};
client->definition(uri, p16, std::move(wrapped));
}
}
void
LspManager::handleDiagnostics(const std::string &uri, const std::vector<Diagnostic> &diagnostics)
{
// Convert incoming ranges from UTF-16 (wire) -> UTF-8 (editor)
std::vector<Diagnostic> conv = diagnostics;
Buffer *buf = findBufferByUri(uri);
auto provider = [buf](const std::string &/*u*/, int line) -> std::string_view {
if (!buf)
return std::string_view();
const auto &rows = buf->Rows();
if (line < 0 || static_cast<size_t>(line) >= rows.size())
return std::string_view();
thread_local std::string scratch;
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
return std::string_view(scratch);
};
for (auto &d: conv) {
Range r8 = toUtf8(uri, d.range, provider);
if (debug_) {
lsp_debug_file("diagnostic range convert: L%d C%d-%d -> L%d C%d-%d",
d.range.start.line, d.range.start.character, d.range.end.character,
r8.start.line, r8.start.character, r8.end.character);
}
d.range = r8;
}
diagnosticStore_.setDiagnostics(uri, conv);
if (display_) {
display_->updateDiagnostics(uri, conv);
display_->updateStatusBar(diagnosticStore_.getErrorCount(uri), diagnosticStore_.getWarningCount(uri));
}
}
bool
LspManager::toggleAutostart(const std::string &languageId)
{
auto it = serverConfigs_.find(languageId);
if (it == serverConfigs_.end())
return false;
it->second.autostart = !it->second.autostart;
return it->second.autostart;
}
std::vector<std::string>
LspManager::configuredLanguages() const
{
std::vector<std::string> out;
out.reserve(serverConfigs_.size());
for (const auto &kv: serverConfigs_)
out.push_back(kv.first);
std::sort(out.begin(), out.end());
return out;
}
std::vector<std::string>
LspManager::runningLanguages() const
{
std::vector<std::string> out;
for (const auto &kv: servers_) {
if (kv.second && kv.second->isRunning())
out.push_back(kv.first);
}
std::sort(out.begin(), out.end());
return out;
}
std::string
LspManager::getLanguageId(Buffer *buffer)
{
// Prefer explicit filetype if set
const auto &ft = buffer->Filetype();
if (!ft.empty())
return ft;
// Otherwise map extension
fs::path p(buffer->Filename());
return extToLanguageId(p.extension().string());
}
std::string
LspManager::getUri(Buffer *buffer)
{
const auto &path = buffer->Filename();
if (path.empty()) {
// Untitled buffer: use a pseudo-URI
return std::string("untitled:") + std::to_string(reinterpret_cast<std::uintptr_t>(buffer));
}
fs::path p(path);
p = fs::weakly_canonical(p);
#ifdef _WIN32
// rudimentary file URI; future: robust encoding
return std::string("file:/") + p.string();
#else
return std::string("file://") + p.string();
#endif
}
// Resolve a Buffer* by matching constructed file URI
Buffer *
LspManager::findBufferByUri(const std::string &uri)
{
if (!editor_)
return nullptr;
// Compare against getUri for each buffer
auto &bufs = editor_->Buffers();
for (auto &b: bufs) {
if (getUri(&b) == uri)
return &b;
}
return nullptr;
}
std::string
LspManager::extToLanguageId(const std::string &ext)
{
std::string e = ext;
if (!e.empty() && e[0] == '.')
e.erase(0, 1);
std::string lower;
lower.resize(e.size());
std::transform(e.begin(), e.end(), lower.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
if (lower == "rs")
return "rust";
if (lower == "c" || lower == "cc" || lower == "cpp" || lower == "h" || lower == "hpp")
return "cpp";
if (lower == "go")
return "go";
if (lower == "py")
return "python";
if (lower == "js")
return "javascript";
if (lower == "ts")
return "typescript";
if (lower == "json")
return "json";
if (lower == "sh" || lower == "bash" || lower == "zsh")
return "shell";
if (lower == "md")
return "markdown";
return lower; // best-effort
}
LspClient *
LspManager::ensureServerForLanguage(const std::string &languageId)
{
auto it = servers_.find(languageId);
if (it != servers_.end() && it->second && it->second->isRunning()) {
return it->second.get();
}
// Attempt to start from config if present
auto cfg = serverConfigs_.find(languageId);
if (cfg == serverConfigs_.end())
return nullptr;
auto client = std::make_unique<LspProcessClient>(cfg->second.command, cfg->second.args);
client->setDiagnosticsHandler([this](const std::string &uri, const std::vector<Diagnostic> &diags) {
this->handleDiagnostics(uri, diags);
});
// No specific file context here; initialize with empty or current working dir
if (!client->initialize(""))
return nullptr;
auto *ret = client.get();
servers_[languageId] = std::move(client);
return ret;
}
void
LspManager::registerDefaultServers()
{
// Import defaults and register by inferred languageId from file patterns
for (const auto &cfg: GetDefaultServerConfigs()) {
if (cfg.filePatterns.empty()) {
// If no patterns, we can't infer; skip
continue;
}
for (const auto &pat: cfg.filePatterns) {
const auto lang = patternToLanguageId(pat);
if (lang.empty())
continue;
// Don't overwrite if user already registered a server for this lang
if (serverConfigs_.find(lang) == serverConfigs_.end()) {
serverConfigs_.emplace(lang, cfg);
}
}
}
}
std::string
LspManager::patternToLanguageId(const std::string &pattern)
{
// Expect patterns like "*.rs", "*.cpp" etc. Extract extension and reuse extToLanguageId
// Find last '.' in the pattern and take substring after it, stripping any trailing wildcards
std::string ext;
// Common case: starts with *.
auto pos = pattern.rfind('.');
if (pos != std::string::npos && pos + 1 < pattern.size()) {
ext = pattern.substr(pos + 1);
// Remove any trailing wildcard characters
while (!ext.empty() && (ext.back() == '*' || ext.back() == '?')) {
ext.pop_back();
}
} else {
// No dot; try to treat whole pattern as extension after trimming leading '*'
ext = pattern;
while (!ext.empty() && (ext.front() == '*' || ext.front() == '.')) {
ext.erase(ext.begin());
}
}
if (ext.empty())
return {};
return extToLanguageId(ext);
}
// Detect workspace root by walking up from filePath looking for any of the
// configured rootPatterns (simple filenames). Supports comma/semicolon-separated
// patterns in cfg.rootPatterns.
std::string
LspManager::detectWorkspaceRoot(const std::string &filePath, const LspServerConfig &cfg)
{
if (filePath.empty())
return {};
fs::path start(filePath);
fs::path dir = start.has_parent_path() ? start.parent_path() : start;
// Build cache key
const std::string cacheKey = (dir.string() + "|" + cfg.rootPatterns);
auto it = rootCache_.find(cacheKey);
if (it != rootCache_.end()) {
return it->second;
}
// Split patterns by ',', ';', or ':'
std::vector<std::string> pats;
{
std::string acc;
for (char c: cfg.rootPatterns) {
if (c == ',' || c == ';' || c == ':') {
if (!acc.empty()) {
pats.push_back(acc);
acc.clear();
}
} else if (!std::isspace(static_cast<unsigned char>(c))) {
acc.push_back(c);
}
}
if (!acc.empty())
pats.push_back(acc);
}
// If no patterns defined, cache empty and return {}
if (pats.empty()) {
rootCache_[cacheKey] = {};
return {};
}
fs::path cur = dir;
while (true) {
// Check each pattern in this directory
for (const auto &pat: pats) {
if (pat.empty())
continue;
fs::path candidate = cur / pat;
std::error_code ec;
bool exists = fs::exists(candidate, ec);
if (!ec && exists) {
rootCache_[cacheKey] = cur.string();
return rootCache_[cacheKey];
}
}
if (cur.has_parent_path()) {
fs::path parent = cur.parent_path();
if (parent == cur)
break; // reached root guard
cur = parent;
} else {
break;
}
}
rootCache_[cacheKey] = {};
return {};
}
} // namespace kte::lsp

108
lsp/LspManager.h Normal file
View File

@@ -0,0 +1,108 @@
/*
* LspManager.h - central coordination of LSP servers and diagnostics
*/
#ifndef KTE_LSP_MANAGER_H
#define KTE_LSP_MANAGER_H
#include <memory>
#include <string>
#include <unordered_map>
class Buffer; // fwd
class Editor; // fwd
#include "DiagnosticDisplay.h"
#include "DiagnosticStore.h"
#include "LspClient.h"
#include "LspServerConfig.h"
#include "UtfCodec.h"
namespace kte::lsp {
class LspManager {
public:
explicit LspManager(Editor *editor, DiagnosticDisplay *display);
// Server management
void registerServer(const std::string &languageId, const LspServerConfig &config);
bool startServerForBuffer(Buffer *buffer);
void stopServer(const std::string &languageId);
void stopAllServers();
// Manual lifecycle controls
bool startServerForLanguage(const std::string &languageId, const std::string &rootPath = std::string());
bool restartServer(const std::string &languageId, const std::string &rootPath = std::string());
// Document sync (to be called by editor/buffer events)
void onBufferOpened(Buffer *buffer);
void onBufferChanged(Buffer *buffer);
void onBufferClosed(Buffer *buffer);
void onBufferSaved(Buffer *buffer);
// Feature requests (stubs)
void requestCompletion(Buffer *buffer, Position pos, CompletionCallback callback);
void requestHover(Buffer *buffer, Position pos, HoverCallback callback);
void requestDefinition(Buffer *buffer, Position pos, LocationCallback callback);
// Diagnostics (public so LspClient impls can forward results here later)
void handleDiagnostics(const std::string &uri, const std::vector<Diagnostic> &diagnostics);
void setDebugLogging(bool enabled)
{
debug_ = enabled;
}
// Configuration utilities
bool toggleAutostart(const std::string &languageId);
std::vector<std::string> configuredLanguages() const;
std::vector<std::string> runningLanguages() const;
private:
[[maybe_unused]] Editor *editor_{}; // non-owning
DiagnosticDisplay *display_{}; // non-owning
DiagnosticStore diagnosticStore_{};
// Key: languageId → client
std::unordered_map<std::string, std::unique_ptr<LspClient> > servers_;
std::unordered_map<std::string, LspServerConfig> serverConfigs_;
// Helpers
static std::string getLanguageId(Buffer *buffer);
static std::string getUri(Buffer *buffer);
static std::string extToLanguageId(const std::string &ext);
LspClient *ensureServerForLanguage(const std::string &languageId);
bool debug_ = false;
// Configuration helpers
void registerDefaultServers();
static std::string patternToLanguageId(const std::string &pattern);
// Workspace root detection helpers/cache
std::string detectWorkspaceRoot(const std::string &filePath, const LspServerConfig &cfg);
// key = startDir + "|" + cfg.rootPatterns
std::unordered_map<std::string, std::string> rootCache_;
// Resolve a buffer by its file:// (or untitled:) URI
Buffer *findBufferByUri(const std::string &uri);
};
} // namespace kte::lsp
#endif // KTE_LSP_MANAGER_H

948
lsp/LspProcessClient.cc Normal file
View File

@@ -0,0 +1,948 @@
/*
* LspProcessClient.cc - process-based LSP client (Phase 1 minimal)
*/
#include "LspProcessClient.h"
#include <sstream>
#include <vector>
#include <string>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
#include <thread>
#include "json.h"
namespace kte::lsp {
LspProcessClient::LspProcessClient(std::string serverCommand, std::vector<std::string> serverArgs)
: command_(std::move(serverCommand)), args_(std::move(serverArgs)), transport_(new JsonRpcTransport())
{
if (const char *dbg = std::getenv("KTE_LSP_DEBUG"); dbg && *dbg) {
debug_ = true;
}
if (const char *to = std::getenv("KTE_LSP_REQ_TIMEOUT_MS"); to && *to) {
char *end = nullptr;
long long v = std::strtoll(to, &end, 10);
if (end && *end == '\0' && v >= 0) {
requestTimeoutMs_ = v;
}
}
if (const char *mp = std::getenv("KTE_LSP_MAX_PENDING"); mp && *mp) {
char *end = nullptr;
long long v = std::strtoll(mp, &end, 10);
if (end && *end == '\0' && v >= 0) {
maxPending_ = static_cast<size_t>(v);
}
}
}
LspProcessClient::~LspProcessClient()
{
shutdown();
}
bool
LspProcessClient::spawnServerProcess()
{
int toChild[2]; // parent writes toChild[1] -> child's stdin
int fromChild[2]; // child writes fromChild[1] -> parent's stdout reader
if (pipe(toChild) != 0) {
if (debug_)
std::fprintf(stderr, "[kte][lsp] pipe(toChild) failed: %s\n", std::strerror(errno));
return false;
}
if (pipe(fromChild) != 0) {
::close(toChild[0]);
::close(toChild[1]);
if (debug_)
std::fprintf(stderr, "[kte][lsp] pipe(fromChild) failed: %s\n", std::strerror(errno));
return false;
}
pid_t pid = fork();
if (pid < 0) {
// fork failed
::close(toChild[0]);
::close(toChild[1]);
::close(fromChild[0]);
::close(fromChild[1]);
if (debug_)
std::fprintf(stderr, "[kte][lsp] fork failed: %s\n", std::strerror(errno));
return false;
}
if (pid == 0) {
// Child: set up stdio
::dup2(toChild[0], STDIN_FILENO);
::dup2(fromChild[1], STDOUT_FILENO);
// Close extra fds
::close(toChild[0]);
::close(toChild[1]);
::close(fromChild[0]);
::close(fromChild[1]);
// Build argv
std::vector<char *> argv;
argv.push_back(const_cast<char *>(command_.c_str()));
for (auto &s: args_)
argv.push_back(const_cast<char *>(s.c_str()));
argv.push_back(nullptr);
// Exec
execvp(command_.c_str(), argv.data());
// If exec fails
// Note: in child; cannot easily log to parent. Attempt to write to stderr.
std::fprintf(stderr, "[kte][lsp] execvp failed for '%s': %s\n", command_.c_str(), std::strerror(errno));
_exit(127);
}
// Parent: keep ends
childPid_ = pid;
outFd_ = toChild[1]; // write to child's stdin
inFd_ = fromChild[0]; // read from child's stdout
// Close the other ends we don't use
::close(toChild[0]);
::close(fromChild[1]);
// Set CLOEXEC on our fds
fcntl(outFd_, F_SETFD, FD_CLOEXEC);
fcntl(inFd_, F_SETFD, FD_CLOEXEC);
if (debug_) {
std::ostringstream oss;
oss << command_;
for (const auto &a: args_) {
oss << ' ' << a;
}
const char *pathEnv = std::getenv("PATH");
std::fprintf(stderr, "[kte][lsp] spawned pid=%d argv=[%s] inFd=%d outFd=%d PATH=%s\n",
static_cast<int>(childPid_), oss.str().c_str(), inFd_, outFd_,
pathEnv ? pathEnv : "<null>");
}
transport_->connect(inFd_, outFd_);
return true;
}
void
LspProcessClient::terminateProcess()
{
if (outFd_ >= 0) {
::close(outFd_);
outFd_ = -1;
}
if (inFd_ >= 0) {
::close(inFd_);
inFd_ = -1;
}
if (childPid_ > 0) {
// Try to wait non-blocking; if still running, send SIGTERM
int status = 0;
pid_t r = waitpid(childPid_, &status, WNOHANG);
if (r == 0) {
// still running
kill(childPid_, SIGTERM);
waitpid(childPid_, &status, 0);
}
childPid_ = -1;
}
}
void
LspProcessClient::sendInitialize(const std::string &rootPath)
{
int idNum = nextRequestIntId_++;
pendingInitializeId_ = std::to_string(idNum);
nlohmann::json j;
j["jsonrpc"] = "2.0";
j["id"] = idNum;
j["method"] = "initialize";
nlohmann::json params;
params["processId"] = static_cast<int>(getpid());
params["rootUri"] = toFileUri(rootPath);
// Minimal client capabilities for now
nlohmann::json caps;
caps["textDocument"]["synchronization"]["didSave"] = true;
params["capabilities"] = std::move(caps);
j["params"] = std::move(params);
transport_->send("initialize", j.dump());
}
bool
LspProcessClient::initialize(const std::string &rootPath)
{
if (running_)
return true;
if (debug_)
std::fprintf(stderr, "[kte][lsp] initialize: rootPath=%s\n", rootPath.c_str());
if (!spawnServerProcess())
return false;
running_ = true;
sendInitialize(rootPath);
startReader();
startTimeoutWatchdog();
return true;
}
void
LspProcessClient::shutdown()
{
if (!running_)
return;
if (debug_)
std::fprintf(stderr, "[kte][lsp] shutdown\n");
// Send shutdown request then exit notification (best-effort)
int id = nextRequestIntId_++;
{
nlohmann::json j;
j["jsonrpc"] = "2.0";
j["id"] = id;
j["method"] = "shutdown";
transport_->send("shutdown", j.dump());
}
{
nlohmann::json j;
j["jsonrpc"] = "2.0";
j["method"] = "exit";
transport_->send("exit", j.dump());
}
// Close pipes to unblock reader, then join thread, then ensure child is gone
terminateProcess();
stopReader();
stopTimeoutWatchdog();
// Clear any pending callbacks
{
std::lock_guard<std::mutex> lk(pendingMutex_);
pending_.clear();
pendingOrder_.clear();
}
running_ = false;
}
void
LspProcessClient::didOpen(const std::string &uri, const std::string &languageId,
int version, const std::string &text)
{
if (!running_)
return;
if (debug_)
std::fprintf(stderr, "[kte][lsp] -> didOpen uri=%s lang=%s version=%d bytes=%zu\n",
uri.c_str(), languageId.c_str(), version, text.size());
nlohmann::json j;
j["jsonrpc"] = "2.0";
j["method"] = "textDocument/didOpen";
j["params"]["textDocument"]["uri"] = uri;
j["params"]["textDocument"]["languageId"] = languageId;
j["params"]["textDocument"]["version"] = version;
j["params"]["textDocument"]["text"] = text;
transport_->send("textDocument/didOpen", j.dump());
}
void
LspProcessClient::didChange(const std::string &uri, int version,
const std::vector<TextDocumentContentChangeEvent> &changes)
{
if (!running_)
return;
if (debug_)
std::fprintf(stderr, "[kte][lsp] -> didChange uri=%s version=%d changes=%zu\n",
uri.c_str(), version, changes.size());
// Phase 1: send full or ranged changes using proper JSON construction
nlohmann::json j;
j["jsonrpc"] = "2.0";
j["method"] = "textDocument/didChange";
j["params"]["textDocument"]["uri"] = uri;
j["params"]["textDocument"]["version"] = version;
auto &arr = j["params"]["contentChanges"];
arr = nlohmann::json::array();
for (const auto &ch: changes) {
nlohmann::json c;
if (ch.range.has_value()) {
c["range"]["start"]["line"] = ch.range->start.line;
c["range"]["start"]["character"] = ch.range->start.character;
c["range"]["end"]["line"] = ch.range->end.line;
c["range"]["end"]["character"] = ch.range->end.character;
}
c["text"] = ch.text;
arr.push_back(std::move(c));
}
transport_->send("textDocument/didChange", j.dump());
}
void
LspProcessClient::didClose(const std::string &uri)
{
if (!running_)
return;
if (debug_)
std::fprintf(stderr, "[kte][lsp] -> didClose uri=%s\n", uri.c_str());
nlohmann::json j;
j["jsonrpc"] = "2.0";
j["method"] = "textDocument/didClose";
j["params"]["textDocument"]["uri"] = uri;
transport_->send("textDocument/didClose", j.dump());
}
void
LspProcessClient::didSave(const std::string &uri)
{
if (!running_)
return;
if (debug_)
std::fprintf(stderr, "[kte][lsp] -> didSave uri=%s\n", uri.c_str());
nlohmann::json j;
j["jsonrpc"] = "2.0";
j["method"] = "textDocument/didSave";
j["params"]["textDocument"]["uri"] = uri;
transport_->send("textDocument/didSave", j.dump());
}
void
LspProcessClient::startReader()
{
stopReader_ = false;
reader_ = std::thread([this] {
this->readerLoop();
});
}
void
LspProcessClient::stopReader()
{
stopReader_ = true;
if (reader_.joinable()) {
// Waking up read() by closing inFd_ is handled in terminateProcess(); ensure its closed first
// Here, best-effort join with small delay
reader_.join();
}
}
void
LspProcessClient::readerLoop()
{
if (debug_)
std::fprintf(stderr, "[kte][lsp] readerLoop start\n");
while (!stopReader_) {
auto msg = transport_->read();
if (!msg.has_value()) {
// EOF or error
break;
}
handleIncoming(msg->raw);
}
if (debug_)
std::fprintf(stderr, "[kte][lsp] readerLoop end\n");
}
void
LspProcessClient::handleIncoming(const std::string &json)
{
try {
auto j = nlohmann::json::parse(json, nullptr, false);
if (j.is_discarded())
return; // malformed JSON
// Validate jsonrpc if present
if (auto itRpc = j.find("jsonrpc"); itRpc != j.end()) {
if (!itRpc->is_string() || *itRpc != "2.0")
return;
}
auto normalizeId = [](const nlohmann::json &idVal) -> std::string {
if (idVal.is_string())
return idVal.get<std::string>();
if (idVal.is_number_integer())
return std::to_string(idVal.get<long long>());
return std::string();
};
// Handle responses (have id and no method) or server -> client requests (have id and method)
if (auto itId = j.find("id"); itId != j.end() && !itId->is_null()) {
const std::string respIdStr = normalizeId(*itId);
// If it's a request from server, it will also have a method
if (auto itMeth = j.find("method"); itMeth != j.end() && itMeth->is_string()) {
const std::string method = *itMeth;
if (method == "workspace/configuration") {
// Respond with default empty settings array matching requested items length
size_t n = 0;
if (auto itParams = j.find("params");
itParams != j.end() && itParams->is_object()) {
if (auto itItems = itParams->find("items");
itItems != itParams->end() && itItems->is_array()) {
n = itItems->size();
}
}
nlohmann::json resp;
resp["jsonrpc"] = "2.0";
// echo id type: if original was string, send string; else number
if (itId->is_string())
resp["id"] = *itId;
else if (itId->is_number_integer())
resp["id"] = *itId;
nlohmann::json arr = nlohmann::json::array();
for (size_t i = 0; i < n; ++i)
arr.push_back(nlohmann::json::object());
resp["result"] = std::move(arr);
transport_->send("response", resp.dump());
return;
}
if (method == "window/showMessageRequest") {
// Best-effort respond with null result (dismiss)
nlohmann::json resp;
resp["jsonrpc"] = "2.0";
if (itId->is_string())
resp["id"] = *itId;
else if (itId->is_number_integer())
resp["id"] = *itId;
resp["result"] = nullptr;
transport_->send("response", resp.dump());
return;
}
// Unknown server request: respond with MethodNotFound
nlohmann::json err;
err["code"] = -32601;
err["message"] = "Method not found";
nlohmann::json resp;
resp["jsonrpc"] = "2.0";
if (itId->is_string())
resp["id"] = *itId;
else if (itId->is_number_integer())
resp["id"] = *itId;
resp["error"] = std::move(err);
transport_->send("response", resp.dump());
return;
}
// Initialize handshake special-case
if (!pendingInitializeId_.empty() && respIdStr == pendingInitializeId_) {
nlohmann::json init;
init["jsonrpc"] = "2.0";
init["method"] = "initialized";
init["params"] = nlohmann::json::object();
transport_->send("initialized", init.dump());
pendingInitializeId_.clear();
}
// Dispatcher lookup
std::function < void(const nlohmann::json &, const nlohmann::json *) > cb;
{
std::lock_guard<std::mutex> lk(pendingMutex_);
auto it = pending_.find(respIdStr);
if (it != pending_.end()) {
cb = it->second.callback;
if (it->second.orderIt != pendingOrder_.end()) {
pendingOrder_.erase(it->second.orderIt);
}
pending_.erase(it);
}
}
if (cb) {
const nlohmann::json *errPtr = nullptr;
const auto itErr = j.find("error");
if (itErr != j.end() && itErr->is_object())
errPtr = &(*itErr);
nlohmann::json result;
const auto itRes = j.find("result");
if (itRes != j.end())
result = *itRes; // may be null
cb(result, errPtr);
}
return;
}
const auto itMethod = j.find("method");
if (itMethod == j.end() || !itMethod->is_string())
return;
const std::string method = *itMethod;
if (method == "window/logMessage") {
if (debug_) {
const auto itParams = j.find("params");
if (itParams != j.end()) {
const auto itMsg = itParams->find("message");
if (itMsg != itParams->end() && itMsg->is_string()) {
std::fprintf(stderr, "[kte][lsp] logMessage: %s\n",
itMsg->get_ref<const std::string &>().c_str());
}
}
}
return;
}
if (method == "window/showMessage") {
const auto itParams = j.find("params");
if (debug_ &&itParams
!=
j.end() && itParams->is_object()
)
{
int typ = 0;
std::string msg;
if (auto itm = itParams->find("message"); itm != itParams->end() && itm->is_string())
msg = *itm;
if (auto ity = itParams->find("type");
ity != itParams->end() && ity->is_number_integer())
typ = *ity;
std::fprintf(stderr, "[kte][lsp] showMessage(type=%d): %s\n", typ, msg.c_str());
}
return;
}
if (method != "textDocument/publishDiagnostics") {
return;
}
const auto itParams = j.find("params");
if (itParams == j.end() || !itParams->is_object())
return;
const auto itUri = itParams->find("uri");
if (itUri == itParams->end() || !itUri->is_string())
return;
const std::string uri = *itUri;
std::vector<Diagnostic> diags;
const auto itDiag = itParams->find("diagnostics");
if (itDiag != itParams->end() && itDiag->is_array()) {
for (const auto &djson: *itDiag) {
if (!djson.is_object())
continue;
Diagnostic d;
// severity
int sev = 3;
if (auto itS = djson.find("severity"); itS != djson.end() && itS->is_number_integer()) {
sev = *itS;
}
switch (sev) {
case 1:
d.severity = DiagnosticSeverity::Error;
break;
case 2:
d.severity = DiagnosticSeverity::Warning;
break;
case 3:
d.severity = DiagnosticSeverity::Information;
break;
case 4:
d.severity = DiagnosticSeverity::Hint;
break;
default:
d.severity = DiagnosticSeverity::Information;
break;
}
if (auto itM = djson.find("message"); itM != djson.end() && itM->is_string()) {
d.message = *itM;
}
if (auto itR = djson.find("range"); itR != djson.end() && itR->is_object()) {
if (auto itStart = itR->find("start");
itStart != itR->end() && itStart->is_object()) {
if (auto itL = itStart->find("line");
itL != itStart->end() && itL->is_number_integer()) {
d.range.start.line = *itL;
}
if (auto itC = itStart->find("character");
itC != itStart->end() && itC->is_number_integer()) {
d.range.start.character = *itC;
}
}
if (auto itEnd = itR->find("end"); itEnd != itR->end() && itEnd->is_object()) {
if (auto itL = itEnd->find("line");
itL != itEnd->end() && itL->is_number_integer()) {
d.range.end.line = *itL;
}
if (auto itC = itEnd->find("character");
itC != itEnd->end() && itC->is_number_integer()) {
d.range.end.character = *itC;
}
}
}
// optional code/source
if (auto itCode = djson.find("code"); itCode != djson.end()) {
if (itCode->is_string())
d.code = itCode->get<std::string>();
else if (itCode->is_number_integer())
d.code = std::to_string(itCode->get<int>());
}
if (auto itSrc = djson.find("source"); itSrc != djson.end() && itSrc->is_string()) {
d.source = itSrc->get<std::string>();
}
diags.push_back(std::move(d));
}
}
if (diagnosticsHandler_) {
diagnosticsHandler_(uri, diags);
}
} catch (...) {
// swallow parse errors
}
}
int
LspProcessClient::sendRequest(const std::string &method, const nlohmann::json &params,
std::function<void(const nlohmann::json & result, const nlohmann::json * errorJson)> cb)
{
if (!running_)
return 0;
int id = nextRequestIntId_++;
nlohmann::json j;
j["jsonrpc"] = "2.0";
j["id"] = id;
j["method"] = method;
if (!params.is_null())
j["params"] = params;
if (debug_)
std::fprintf(stderr, "[kte][lsp] -> request method=%s id=%d\n", method.c_str(), id);
transport_->send(method, j.dump());
if (cb) {
std::function < void() > callDropped;
{
std::lock_guard<std::mutex> lk(pendingMutex_);
if (maxPending_ > 0 && pending_.size() >= maxPending_) {
// Evict oldest
if (!pendingOrder_.empty()) {
std::string oldestId = pendingOrder_.front();
auto it = pending_.find(oldestId);
if (it != pending_.end()) {
auto cbOld = it->second.callback;
std::string methOld = it->second.method;
if (debug_) {
std::fprintf(
stderr,
"[kte][lsp] dropping oldest pending id=%s method=%s (cap=%zu)\n",
oldestId.c_str(), methOld.c_str(), maxPending_);
}
// Prepare drop callback to run outside lock
callDropped = [cbOld] {
if (cbOld) {
nlohmann::json err;
err["code"] = -32001;
err["message"] =
"Request dropped (max pending exceeded)";
cbOld(nlohmann::json(), &err);
}
};
pending_.erase(it);
}
pendingOrder_.pop_front();
}
}
pendingOrder_.push_back(std::to_string(id));
auto itOrder = pendingOrder_.end();
--itOrder;
PendingRequest pr;
pr.method = method;
pr.callback = std::move(cb);
if (requestTimeoutMs_ > 0) {
pr.deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(
requestTimeoutMs_);
}
pr.orderIt = itOrder;
pending_[std::to_string(id)] = std::move(pr);
}
if (callDropped)
callDropped();
}
return id;
}
void
LspProcessClient::completion(const std::string &uri, Position pos, CompletionCallback cb)
{
nlohmann::json params;
params["textDocument"]["uri"] = uri;
params["position"]["line"] = pos.line;
params["position"]["character"] = pos.character;
sendRequest("textDocument/completion", params,
[cb = std::move(cb)](const nlohmann::json &result, const nlohmann::json *error) {
CompletionList out{};
std::string err;
if (error) {
if (auto itMsg = error->find("message");
itMsg != error->end() && itMsg->is_string())
err = itMsg->get<std::string>();
else
err = "LSP error";
} else {
auto parseItem = [](const nlohmann::json &j) -> CompletionItem {
CompletionItem it{};
if (auto il = j.find("label"); il != j.end() && il->is_string())
it.label = il->get<std::string>();
if (auto idt = j.find("detail"); idt != j.end() && idt->is_string())
it.detail = idt->get<std::string>();
if (auto ins = j.find("insertText"); ins != j.end() && ins->is_string())
it.insertText = ins->get<std::string>();
return it;
};
if (result.is_array()) {
for (const auto &ji: result) {
if (ji.is_object())
out.items.push_back(parseItem(ji));
}
} else if (result.is_object()) {
if (auto ii = result.find("isIncomplete");
ii != result.end() && ii->is_boolean())
out.isIncomplete = ii->get<bool>();
if (auto itms = result.find("items");
itms != result.end() && itms->is_array()) {
for (const auto &ji: *itms) {
if (ji.is_object())
out.items.push_back(parseItem(ji));
}
}
}
}
if (cb)
cb(out, err);
});
}
void
LspProcessClient::hover(const std::string &uri, Position pos, HoverCallback cb)
{
nlohmann::json params;
params["textDocument"]["uri"] = uri;
params["position"]["line"] = pos.line;
params["position"]["character"] = pos.character;
sendRequest("textDocument/hover", params,
[cb = std::move(cb)](const nlohmann::json &result, const nlohmann::json *error) {
HoverResult out{};
std::string err;
if (error) {
if (auto itMsg = error->find("message");
itMsg != error->end() && itMsg->is_string())
err = itMsg->get<std::string>();
else
err = "LSP error";
} else if (!result.is_null()) {
auto appendText = [&](const std::string &s) {
if (!out.contents.empty())
out.contents.push_back('\n');
out.contents += s;
};
if (result.is_object()) {
if (auto itC = result.find("contents"); itC != result.end()) {
if (itC->is_string()) {
appendText(itC->get<std::string>());
} else if (itC->is_object()) {
if (auto itV = itC->find("value");
itV != itC->end() && itV->is_string())
appendText(itV->get<std::string>());
} else if (itC->is_array()) {
for (const auto &el: *itC) {
if (el.is_string())
appendText(el.get<std::string>());
else if (el.is_object()) {
if (auto itV = el.find("value");
itV != el.end() && itV->is_string())
appendText(itV->get<std::string>());
}
}
}
}
if (auto itR = result.find("range");
itR != result.end() && itR->is_object()) {
Range r{};
if (auto s = itR->find("start");
s != itR->end() && s->is_object()) {
if (auto il = s->find("line");
il != s->end() && il->is_number_integer())
r.start.line = *il;
if (auto ic = s->find("character");
ic != s->end() && ic->is_number_integer())
r.start.character = *ic;
}
if (auto e = itR->find("end"); e != itR->end() && e->is_object()) {
if (auto il = e->find("line");
il != e->end() && il->is_number_integer())
r.end.line = *il;
if (auto ic = e->find("character");
ic != e->end() && ic->is_number_integer())
r.end.character = *ic;
}
out.range = r;
}
}
}
if (cb)
cb(out, err);
});
}
void
LspProcessClient::definition(const std::string &uri, Position pos, LocationCallback cb)
{
nlohmann::json params;
params["textDocument"]["uri"] = uri;
params["position"]["line"] = pos.line;
params["position"]["character"] = pos.character;
sendRequest("textDocument/definition", params,
[cb = std::move(cb)](const nlohmann::json &result, const nlohmann::json *error) {
std::vector<Location> out;
std::string err;
auto parseRange = [](const nlohmann::json &jr) -> Range {
Range r{};
if (!jr.is_object())
return r;
if (auto s = jr.find("start"); s != jr.end() && s->is_object()) {
if (auto il = s->find("line"); il != s->end() && il->is_number_integer())
r.start.line = *il;
if (auto ic = s->find("character");
ic != s->end() && ic->is_number_integer())
r.start.character = *ic;
}
if (auto e = jr.find("end"); e != jr.end() && e->is_object()) {
if (auto il = e->find("line"); il != e->end() && il->is_number_integer())
r.end.line = *il;
if (auto ic = e->find("character");
ic != e->end() && e->is_number_integer())
r.end.character = *ic;
}
return r;
};
auto pushLocObj = [&](const nlohmann::json &jo) {
Location loc{};
if (auto iu = jo.find("uri"); iu != jo.end() && iu->is_string())
loc.uri = iu->get<std::string>();
if (auto ir = jo.find("range"); ir != jo.end())
loc.range = parseRange(*ir);
out.push_back(std::move(loc));
};
if (error) {
if (auto itMsg = error->find("message");
itMsg != error->end() && itMsg->is_string())
err = itMsg->get<std::string>();
else
err = "LSP error";
} else if (!result.is_null()) {
if (result.is_object()) {
if (result.contains("uri") && result.contains("range")) {
pushLocObj(result);
} else if (result.contains("targetUri")) {
Location loc{};
if (auto tu = result.find("targetUri");
tu != result.end() && tu->is_string())
loc.uri = tu->get<std::string>();
if (auto tr = result.find("targetRange"); tr != result.end())
loc.range = parseRange(*tr);
out.push_back(std::move(loc));
}
} else if (result.is_array()) {
for (const auto &el: result) {
if (el.is_object()) {
if (el.contains("uri")) {
pushLocObj(el);
} else if (el.contains("targetUri")) {
Location loc{};
if (auto tu = el.find("targetUri");
tu != el.end() && tu->is_string())
loc.uri = tu->get<std::string>();
if (auto tr = el.find("targetRange");
tr != el.end())
loc.range = parseRange(*tr);
out.push_back(std::move(loc));
}
}
}
}
}
if (cb)
cb(out, err);
});
}
bool
LspProcessClient::isRunning() const
{
return running_;
}
std::string
LspProcessClient::getServerName() const
{
return command_;
}
std::string
LspProcessClient::toFileUri(const std::string &path)
{
if (path.empty())
return std::string();
#ifdef _WIN32
return std::string("file:/") + path;
#else
return std::string("file://") + path;
#endif
}
void
LspProcessClient::startTimeoutWatchdog()
{
stopTimeout_ = false;
if (requestTimeoutMs_ <= 0)
return;
timeoutThread_ = std::thread([this] {
while (!stopTimeout_) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
auto now = std::chrono::steady_clock::now();
struct Expired {
std::string id;
std::string method;
std::function<void(const nlohmann::json &, const nlohmann::json *)> cb;
};
std::vector<Expired> expired;
{
std::lock_guard<std::mutex> lk(pendingMutex_);
for (auto it = pending_.begin(); it != pending_.end();) {
const auto &pr = it->second;
if (pr.deadline.time_since_epoch().count() != 0 && now >= pr.deadline) {
expired.push_back(Expired{it->first, pr.method, pr.callback});
if (pr.orderIt != pendingOrder_.end())
pendingOrder_.erase(pr.orderIt);
it = pending_.erase(it);
} else {
++it;
}
}
}
for (auto &kv: expired) {
if (debug_) {
std::fprintf(stderr, "[kte][lsp] request timeout id=%s method=%s\n",
kv.id.c_str(), kv.method.c_str());
}
if (kv.cb) {
nlohmann::json err;
err["code"] = -32000;
err["message"] = "Request timed out";
kv.cb(nlohmann::json(), &err);
}
}
}
});
}
void
LspProcessClient::stopTimeoutWatchdog()
{
stopTimeout_ = true;
if (timeoutThread_.joinable())
timeoutThread_.join();
}
} // namespace kte::lsp

189
lsp/LspProcessClient.h Normal file
View File

@@ -0,0 +1,189 @@
/*
* LspProcessClient.h - process-based LSP client (initial stub)
*/
#ifndef KTE_LSP_PROCESS_CLIENT_H
#define KTE_LSP_PROCESS_CLIENT_H
#include <memory>
#include <string>
#include <vector>
#include <thread>
#include <atomic>
#include <functional>
#include <unordered_map>
#include <mutex>
#include <chrono>
#include <list>
#include "json.h"
#include "LspClient.h"
#include "JsonRpcTransport.h"
namespace kte::lsp {
class LspProcessClient : public LspClient {
public:
LspProcessClient(std::string serverCommand, std::vector<std::string> serverArgs);
~LspProcessClient() override;
bool initialize(const std::string &rootPath) override;
void shutdown() override;
void didOpen(const std::string &uri, const std::string &languageId,
int version, const std::string &text) override;
void didChange(const std::string &uri, int version,
const std::vector<TextDocumentContentChangeEvent> &changes) override;
void didClose(const std::string &uri) override;
void didSave(const std::string &uri) override;
// Language Features (wire-up via dispatcher; minimal callbacks for now)
void completion(const std::string &uri, Position pos,
CompletionCallback cb) override;
void hover(const std::string &uri, Position pos,
HoverCallback cb) override;
void definition(const std::string &uri, Position pos,
LocationCallback cb) override;
bool isRunning() const override;
std::string getServerName() const override;
void setDiagnosticsHandler(DiagnosticsHandler h) override
{
diagnosticsHandler_ = std::move(h);
}
private:
std::string command_;
std::vector<std::string> args_;
std::unique_ptr<JsonRpcTransport> transport_;
bool running_ = false;
bool debug_ = false;
int inFd_ = -1; // read from server (server stdout)
int outFd_ = -1; // write to server (server stdin)
pid_t childPid_ = -1;
int nextRequestIntId_ = 1;
std::string pendingInitializeId_{}; // echo exactly as sent (string form)
// Incoming processing
std::thread reader_;
std::atomic<bool> stopReader_{false};
DiagnosticsHandler diagnosticsHandler_{};
// Simple request dispatcher: map request id -> callback
struct PendingRequest {
std::string method;
// If error is present, errorJson points to it; otherwise nullptr
std::function<void(const nlohmann::json & result, const nlohmann::json * errorJson)> callback;
// Optional timeout
std::chrono::steady_clock::time_point deadline{}; // epoch if no timeout
// Order tracking for LRU eviction
std::list<std::string>::iterator orderIt{};
};
std::unordered_map<std::string, PendingRequest> pending_;
// Maintain insertion order (oldest at front)
std::list<std::string> pendingOrder_;
std::mutex pendingMutex_;
// Timeout/watchdog for pending requests
std::thread timeoutThread_;
std::atomic<bool> stopTimeout_{false};
int64_t requestTimeoutMs_ = 0; // 0 = disabled
size_t maxPending_ = 0; // 0 = unlimited
bool spawnServerProcess();
void terminateProcess();
static std::string toFileUri(const std::string &path);
void sendInitialize(const std::string &rootPath);
void startReader();
void stopReader();
void readerLoop();
void handleIncoming(const std::string &json);
// Helper to send a request with params and register a response callback
int sendRequest(const std::string &method, const nlohmann::json &params,
std::function<void(const nlohmann::json & result, const nlohmann::json * errorJson)> cb);
// Start/stop timeout thread
void startTimeoutWatchdog();
void stopTimeoutWatchdog();
public:
// Test hook: inject a raw JSON message as if received from server
void debugInjectMessageForTest(const std::string &raw)
{
handleIncoming(raw);
}
// Test hook: add a pending request entry manually
void debugAddPendingForTest(const std::string &id, const std::string &method,
std::function<void(const nlohmann::json & result,
const nlohmann::json *errorJson)
>
cb
)
{
std::lock_guard<std::mutex> lk(pendingMutex_);
pendingOrder_.push_back(id);
auto it = pendingOrder_.end();
--it;
PendingRequest pr{method, std::move(cb), {}, it};
pending_[id] = std::move(pr);
}
// Test hook: override timeout
void setRequestTimeoutMsForTest(int64_t ms)
{
requestTimeoutMs_ = ms;
}
// Test hook: set max pending
void setMaxPendingForTest(size_t maxPending)
{
maxPending_ = maxPending;
}
// Test hook: set running flag (to allow sendRequest in tests without spawning)
void setRunningForTest(bool r)
{
running_ = r;
}
// Test hook: send a raw request using internal machinery
int debugSendRequestForTest(const std::string &method, const nlohmann::json &params,
std::function<void(const nlohmann::json & result,
const nlohmann::json *errorJson)
>
cb
)
{
return sendRequest(method, params, std::move(cb));
}
};
} // namespace kte::lsp
#endif // KTE_LSP_PROCESS_CLIENT_H

47
lsp/LspServerConfig.h Normal file
View File

@@ -0,0 +1,47 @@
/*
* LspServerConfig.h - per-language LSP server configuration
*/
#ifndef KTE_LSP_SERVER_CONFIG_H
#define KTE_LSP_SERVER_CONFIG_H
#include <string>
#include <unordered_map>
#include <vector>
namespace kte::lsp {
enum class LspSyncMode {
None = 0,
Full = 1,
Incremental = 2,
};
struct LspServerConfig {
std::string command; // executable name/path
std::vector<std::string> args; // CLI args
std::vector<std::string> filePatterns; // e.g. {"*.rs"}
std::string rootPatterns; // e.g. "Cargo.toml"
LspSyncMode preferredSyncMode = LspSyncMode::Incremental;
bool autostart = true;
std::unordered_map<std::string, std::string> initializationOptions; // placeholder
std::unordered_map<std::string, std::string> settings; // placeholder
};
// Provide a small set of defaults; callers may ignore
inline std::vector<LspServerConfig>
GetDefaultServerConfigs()
{
return std::vector<LspServerConfig>{
LspServerConfig{
.command = "rust-analyzer", .args = {}, .filePatterns = {"*.rs"}, .rootPatterns = "Cargo.toml"
},
LspServerConfig{
.command = "clangd", .args = {"--background-index"},
.filePatterns = {"*.c", "*.cc", "*.cpp", "*.h", "*.hpp"},
.rootPatterns = "compile_commands.json"
},
LspServerConfig{.command = "gopls", .args = {}, .filePatterns = {"*.go"}, .rootPatterns = "go.mod"},
};
}
} // namespace kte::lsp
#endif // KTE_LSP_SERVER_CONFIG_H

55
lsp/LspTypes.h Normal file
View File

@@ -0,0 +1,55 @@
/*
* LspTypes.h - minimal LSP-related data types for initial integration
*/
#ifndef KTE_LSP_TYPES_H
#define KTE_LSP_TYPES_H
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace kte::lsp {
// NOTE on coordinates:
// - Internal editor coordinates use UTF-8 columns counted by Unicode scalars.
// - LSP wire protocol uses UTF-16 code units for the `character` field.
// Conversions are performed in higher layers via `lsp/UtfCodec.h` helpers.
struct Position {
int line = 0;
int character = 0;
};
struct Range {
Position start;
Position end;
};
struct TextDocumentContentChangeEvent {
std::optional<Range> range; // if not set, represents full document change
std::string text; // new text for the given range
};
// Minimal feature result types for phase 1
struct CompletionItem {
std::string label;
std::optional<std::string> detail; // optional extra info
std::optional<std::string> insertText; // if present, use instead of label
};
struct CompletionList {
bool isIncomplete = false;
std::vector<CompletionItem> items;
};
struct HoverResult {
std::string contents; // concatenated plaintext/markdown for now
std::optional<Range> range; // optional range
};
struct Location {
std::string uri;
Range range;
};
} // namespace kte::lsp
#endif // KTE_LSP_TYPES_H

View File

@@ -0,0 +1,53 @@
/*
* TerminalDiagnosticDisplay.cc - minimal stub implementation
*/
#include "TerminalDiagnosticDisplay.h"
#include "../TerminalRenderer.h"
namespace kte::lsp {
TerminalDiagnosticDisplay::TerminalDiagnosticDisplay(TerminalRenderer *renderer)
: renderer_(renderer) {}
void
TerminalDiagnosticDisplay::updateDiagnostics(const std::string &uri,
const std::vector<Diagnostic> &diagnostics)
{
(void) uri;
(void) diagnostics;
// Stub: no rendering yet. Future: gutter markers, underlines, virtual text.
}
void
TerminalDiagnosticDisplay::showInlineDiagnostic(const Diagnostic &diagnostic)
{
(void) diagnostic;
// Stub: show as message line in future.
}
void
TerminalDiagnosticDisplay::showDiagnosticList(const std::vector<Diagnostic> &diagnostics)
{
(void) diagnostics;
// Stub: open a panel/list in future.
}
void
TerminalDiagnosticDisplay::hideDiagnosticList()
{
// Stub
}
void
TerminalDiagnosticDisplay::updateStatusBar(int errorCount, int warningCount)
{
(void) errorCount;
(void) warningCount;
// Stub: integrate with status bar rendering later.
}
} // namespace kte::lsp

View File

@@ -0,0 +1,35 @@
/*
* TerminalDiagnosticDisplay.h - Terminal (ncurses) diagnostics visualization stub
*/
#ifndef KTE_LSP_TERMINAL_DIAGNOSTIC_DISPLAY_H
#define KTE_LSP_TERMINAL_DIAGNOSTIC_DISPLAY_H
#include <string>
#include <vector>
#include "DiagnosticDisplay.h"
class TerminalRenderer; // fwd
namespace kte::lsp {
class TerminalDiagnosticDisplay final : public DiagnosticDisplay {
public:
explicit TerminalDiagnosticDisplay(TerminalRenderer *renderer);
void updateDiagnostics(const std::string &uri,
const std::vector<Diagnostic> &diagnostics) override;
void showInlineDiagnostic(const Diagnostic &diagnostic) override;
void showDiagnosticList(const std::vector<Diagnostic> &diagnostics) override;
void hideDiagnosticList() override;
void updateStatusBar(int errorCount, int warningCount) override;
private:
[[maybe_unused]] TerminalRenderer *renderer_{}; // non-owning
};
} // namespace kte::lsp
#endif // KTE_LSP_TERMINAL_DIAGNOSTIC_DISPLAY_H

155
lsp/UtfCodec.cc Normal file
View File

@@ -0,0 +1,155 @@
/*
* UtfCodec.cc - UTF-8 <-> UTF-16 code unit position conversions
*/
#include "UtfCodec.h"
#include <cassert>
namespace kte::lsp {
// Decode next code point from a UTF-8 string.
// On invalid input, consumes 1 byte and returns U+FFFD.
// Returns: (codepoint, bytesConsumed)
static inline std::pair<uint32_t, size_t>
decodeUtf8(std::string_view s, size_t i)
{
if (i >= s.size())
return {0, 0};
unsigned char c0 = static_cast<unsigned char>(s[i]);
if (c0 < 0x80) {
return {c0, 1};
}
// Determine sequence length
if ((c0 & 0xE0) == 0xC0) {
if (i + 1 >= s.size())
return {0xFFFD, 1};
unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
if ((c1 & 0xC0) != 0x80)
return {0xFFFD, 1};
uint32_t cp = ((c0 & 0x1F) << 6) | (c1 & 0x3F);
// Overlong check: must be >= 0x80
if (cp < 0x80)
return {0xFFFD, 1};
return {cp, 2};
}
if ((c0 & 0xF0) == 0xE0) {
if (i + 2 >= s.size())
return {0xFFFD, 1};
unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
unsigned char c2 = static_cast<unsigned char>(s[i + 2]);
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80)
return {0xFFFD, 1};
uint32_t cp = ((c0 & 0x0F) << 12) | ((c1 & 0x3F) << 6) | (c2 & 0x3F);
// Overlong / surrogate range check
if (cp < 0x800 || (cp >= 0xD800 && cp <= 0xDFFF))
return {0xFFFD, 1};
return {cp, 3};
}
if ((c0 & 0xF8) == 0xF0) {
if (i + 3 >= s.size())
return {0xFFFD, 1};
unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
unsigned char c2 = static_cast<unsigned char>(s[i + 2]);
unsigned char c3 = static_cast<unsigned char>(s[i + 3]);
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80 || (c3 & 0xC0) != 0x80)
return {0xFFFD, 1};
uint32_t cp = ((c0 & 0x07) << 18) | ((c1 & 0x3F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
// Overlong / max range check
if (cp < 0x10000 || cp > 0x10FFFF)
return {0xFFFD, 1};
return {cp, 4};
}
return {0xFFFD, 1};
}
static inline size_t
utf16UnitsForCodepoint(uint32_t cp)
{
return (cp <= 0xFFFF) ? 1 : 2;
}
size_t
utf8ColToUtf16Units(std::string_view lineUtf8, size_t utf8Col)
{
// Count by Unicode scalars up to utf8Col; clamp at EOL
size_t units = 0;
size_t col = 0;
size_t i = 0;
while (i < lineUtf8.size()) {
if (col >= utf8Col)
break;
auto [cp, n] = decodeUtf8(lineUtf8, i);
if (n == 0)
break;
units += utf16UnitsForCodepoint(cp);
i += n;
++col;
}
return units;
}
size_t
utf16UnitsToUtf8Col(std::string_view lineUtf8, size_t utf16Units)
{
// Traverse code points until consuming utf16Units (or reaching EOL)
size_t units = 0;
size_t col = 0;
size_t i = 0;
while (i < lineUtf8.size()) {
auto [cp, n] = decodeUtf8(lineUtf8, i);
if (n == 0)
break;
size_t add = utf16UnitsForCodepoint(cp);
if (units + add > utf16Units)
break;
units += add;
i += n;
++col;
if (units == utf16Units)
break;
}
return col;
}
Position
toUtf16(const std::string &uri, const Position &pUtf8, const LineProvider &provider)
{
Position out = pUtf8;
std::string_view line = provider ? provider(uri, pUtf8.line) : std::string_view();
out.character = static_cast<int>(utf8ColToUtf16Units(line, static_cast<size_t>(pUtf8.character)));
return out;
}
Position
toUtf8(const std::string &uri, const Position &pUtf16, const LineProvider &provider)
{
Position out = pUtf16;
std::string_view line = provider ? provider(uri, pUtf16.line) : std::string_view();
out.character = static_cast<int>(utf16UnitsToUtf8Col(line, static_cast<size_t>(pUtf16.character)));
return out;
}
Range
toUtf16(const std::string &uri, const Range &rUtf8, const LineProvider &provider)
{
Range r;
r.start = toUtf16(uri, rUtf8.start, provider);
r.end = toUtf16(uri, rUtf8.end, provider);
return r;
}
Range
toUtf8(const std::string &uri, const Range &rUtf16, const LineProvider &provider)
{
Range r;
r.start = toUtf8(uri, rUtf16.start, provider);
r.end = toUtf8(uri, rUtf16.end, provider);
return r;
}
} // namespace kte::lsp

37
lsp/UtfCodec.h Normal file
View File

@@ -0,0 +1,37 @@
/*
* UtfCodec.h - Helpers for UTF-8 <-> UTF-16 code unit position conversions
*/
#ifndef KTE_LSP_UTF_CODEC_H
#define KTE_LSP_UTF_CODEC_H
#include <cstddef>
#include <functional>
#include <string>
#include <string_view>
#include "LspTypes.h"
namespace kte::lsp {
// Map between editor-internal UTF-8 columns (by Unicode scalar count)
// and LSP wire UTF-16 code units (per LSP spec).
// Convert a UTF-8 column index (in Unicode scalars) to UTF-16 code units for a given line.
size_t utf8ColToUtf16Units(std::string_view lineUtf8, size_t utf8Col);
// Convert a UTF-16 code unit count to a UTF-8 column index (in Unicode scalars) for a given line.
size_t utf16UnitsToUtf8Col(std::string_view lineUtf8, size_t utf16Units);
// Line text provider to allow conversions without giving the codec direct buffer access.
using LineProvider = std::function<std::string_view(const std::string & uri, int line)>;
// Convenience helpers for positions and ranges using a line provider.
Position toUtf16(const std::string &uri, const Position &pUtf8, const LineProvider &provider);
Position toUtf8(const std::string &uri, const Position &pUtf16, const LineProvider &provider);
Range toUtf16(const std::string &uri, const Range &rUtf8, const LineProvider &provider);
Range toUtf8(const std::string &uri, const Range &rUtf16, const LineProvider &provider);
} // namespace kte::lsp
#endif // KTE_LSP_UTF_CODEC_H

49
main.cc
View File

@@ -12,6 +12,7 @@
#include "Editor.h"
#include "Frontend.h"
#include "TerminalFrontend.h"
#include "lsp/LspManager.h"
#if defined(KTE_BUILD_GUI)
#include "GUIFrontend.h"
@@ -28,6 +29,8 @@ PrintUsage(const char *prog)
{
std::cerr << "Usage: " << prog << " [OPTIONS] [files]\n"
<< "Options:\n"
<< " -c, --chdir DIR Change working directory before opening files\n"
<< " -d, --debug Enable LSP debug logging\n"
<< " -g, --gui Use GUI frontend (if built)\n"
<< " -t, --term Use terminal (ncurses) frontend [default]\n"
<< " -h, --help Show this help and exit\n"
@@ -36,17 +39,25 @@ PrintUsage(const char *prog)
int
main(int argc, const char *argv[])
main(const int argc, const char *argv[])
{
Editor editor;
// Wire up LSP manager (no diagnostic UI yet; frontends may provide later)
kte::lsp::LspManager lspMgr(&editor, nullptr);
editor.SetLspManager(&lspMgr);
// CLI parsing using getopt_long
bool req_gui = false;
bool req_term = false;
bool show_help = false;
bool show_version = false;
bool lsp_debug = false;
std::string nwd;
static struct option long_opts[] = {
{"chdir", required_argument, nullptr, 'c'},
{"debug", no_argument, nullptr, 'd'},
{"gui", no_argument, nullptr, 'g'},
{"term", no_argument, nullptr, 't'},
{"help", no_argument, nullptr, 'h'},
@@ -56,8 +67,14 @@ main(int argc, const char *argv[])
int opt;
int long_index = 0;
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "gthV", long_opts, &long_index)) != -1) {
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "c:dgthV", long_opts, &long_index)) != -1) {
switch (opt) {
case 'c':
nwd = optarg;
break;
case 'd':
lsp_debug = true;
break;
case 'g':
req_gui = true;
break;
@@ -90,6 +107,16 @@ main(int argc, const char *argv[])
(void) req_term; // suppress unused warning when GUI is not compiled in
#endif
// Apply LSP debug setting strictly based on -d flag
lspMgr.setDebugLogging(lsp_debug);
if (lsp_debug) {
// Ensure LSP subprocess client picks up debug via environment
::setenv("KTE_LSP_DEBUG", "1", 1);
} else {
// Prevent environment from enabling debug implicitly
::unsetenv("KTE_LSP_DEBUG");
}
// Determine frontend
#if !defined(KTE_BUILD_GUI)
if (req_gui) {
@@ -104,11 +131,14 @@ main(int argc, const char *argv[])
} else if (req_term) {
use_gui = false;
} else {
// Default depends on build target: kge defaults to GUI, kte to terminal
// Default depends on build target: kge defaults to GUI, kte to terminal
#if defined(KTE_DEFAULT_GUI)
use_gui = true;
use_gui = true;
#else
use_gui = false;
use_gui = false;
#endif
}
#endif
@@ -199,6 +229,13 @@ main(int argc, const char *argv[])
}
#endif
#if defined(KTE_BUILD_GUI)
if (!nwd.empty()) {
if (chdir(nwd.c_str()) != 0) {
std::cerr << "kge: failed to chdir to " << nwd << std::endl;
}
}
#endif
if (!fe->Init(editor)) {
std::cerr << "kte: failed to initialize frontend" << std::endl;
return 1;
@@ -212,4 +249,4 @@ main(int argc, const char *argv[])
fe->Shutdown();
return 0;
}
}

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>
namespace kte {
class JSONHighlighter final : public LanguageHighlighter {
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>
namespace kte {
class LispHighlighter final : public LanguageHighlighter {
public:
LispHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
};
LispHighlighter();
} // 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"
namespace kte {
class NullHighlighter final : public LanguageHighlighter {
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"
namespace kte {
class ShellHighlighter final : public LanguageHighlighter {
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

@@ -17,32 +17,32 @@ 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;
explicit TreeSitterHighlighter(const TSLanguage *lang, std::string filetype);
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
~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};
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;
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)());
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
const void * (*get_lang)());
} // namespace kte
#endif // KTE_ENABLE_TREESITTER
#endif // KTE_ENABLE_TREESITTER

119
test_lsp_decode.cc Normal file
View File

@@ -0,0 +1,119 @@
// test_lsp_decode.cc - tests for LspProcessClient JSON decoding/dispatch
#include <cassert>
#include <atomic>
#include <string>
#include <vector>
#include "lsp/LspProcessClient.h"
#include "lsp/Diagnostic.h"
using namespace kte::lsp;
int
main()
{
// Create client (won't start a process for these tests)
LspProcessClient client("/bin/echo", {});
// 1) Numeric id response should match string key "42"
{
std::atomic<bool> called{false};
client.debugAddPendingForTest("42", "dummy",
[&](const nlohmann::json &result, const nlohmann::json *err) {
(void) result;
(void) err;
called = true;
});
std::string resp = "{\"jsonrpc\":\"2.0\",\"id\":42,\"result\":null}";
client.debugInjectMessageForTest(resp);
assert(called.load());
}
// 2) String id response should resolve
{
std::atomic<bool> called{false};
client.debugAddPendingForTest("abc123", "dummy",
[&](const nlohmann::json &result, const nlohmann::json *err) {
(void) result;
(void) err;
called = true;
});
std::string resp = "{\"jsonrpc\":\"2.0\",\"id\":\"abc123\",\"result\":{}}";
client.debugInjectMessageForTest(resp);
assert(called.load());
}
// 3) Diagnostics notification decoding
{
std::atomic<bool> diagCalled{false};
client.setDiagnosticsHandler([&](const std::string &uri, const std::vector<Diagnostic> &d) {
assert(uri == "file:///tmp/x.rs");
assert(!d.empty());
diagCalled = true;
});
std::string notif = R"({
"jsonrpc":"2.0",
"method":"textDocument/publishDiagnostics",
"params":{
"uri":"file:///tmp/x.rs",
"diagnostics":[{
"range": {"start": {"line":0, "character":1}, "end": {"line":0, "character":2}},
"severity": 1,
"message": "oops"
}]
}
})";
client.debugInjectMessageForTest(notif);
assert(diagCalled.load());
}
// 4) ShowMessage notification should be safely handled (no crash)
{
std::string msg =
"{\"jsonrpc\":\"2.0\",\"method\":\"window/showMessage\",\"params\":{\"type\":2,\"message\":\"hi\"}}";
client.debugInjectMessageForTest(msg);
}
// 5) workspace/configuration request should be responded to (no crash)
{
std::string req = R"({
"jsonrpc":"2.0",
"id": 7,
"method":"workspace/configuration",
"params": {"items": [{"section":"x"},{"section":"y"}]}
})";
client.debugInjectMessageForTest(req);
}
// 6) Pending cap eviction: oldest request is dropped with -32001
{
LspProcessClient c2("/bin/echo", {});
c2.setRunningForTest(true);
c2.setMaxPendingForTest(2);
std::atomic<int> drops{0};
auto make_cb = [&](const char *tag) {
return [&, tag](const nlohmann::json &res, const nlohmann::json *err) {
(void) res;
if (err && err->is_object()) {
auto it = err->find("code");
if (it != err->end() && it->is_number_integer() && *it == -32001) {
// Only the oldest (first) should be dropped
if (std::string(tag) == "first")
drops.fetch_add(1);
}
}
};
};
// Enqueue 3 requests; cap=2 -> first should be dropped immediately when third is added
c2.debugSendRequestForTest("a", nlohmann::json::object(), make_cb("first"));
c2.debugSendRequestForTest("b", nlohmann::json::object(), make_cb("second"));
c2.debugSendRequestForTest("c", nlohmann::json::object(), make_cb("third"));
// Allow callbacks (none are async here, drop is invoked inline after send)
assert(drops.load() == 1);
}
std::puts("test_lsp_decode: OK");
return 0;
}

76
test_transport.cc Normal file
View File

@@ -0,0 +1,76 @@
// test_transport.cc - transport framing tests
#include <cassert>
#include <cstdio>
#include <cstring>
#include <string>
#include <optional>
#include <unistd.h>
#include "lsp/JsonRpcTransport.h"
using namespace kte::lsp;
static void
write_all(int fd, const void *bufv, size_t len)
{
const char *buf = static_cast<const char *>(bufv);
size_t left = len;
while (left > 0) {
ssize_t n = ::write(fd, buf, left);
if (n < 0) {
if (errno == EINTR)
continue;
std::perror("write");
std::abort();
}
buf += static_cast<size_t>(n);
left -= static_cast<size_t>(n);
}
}
int
main()
{
int p[2];
assert(pipe(p) == 0);
int readFd = p[0];
int writeFd = p[1];
JsonRpcTransport t;
// We only need inFd for read tests; pass writeFd for completeness
t.connect(readFd, writeFd);
auto sendMsg = [&](const std::string &payload, bool lowerHeader) {
std::string header = (lowerHeader ? std::string("content-length: ") : std::string("Content-Length: ")) +
std::to_string(payload.size()) + "\r\n\r\n";
write_all(writeFd, header.data(), header.size());
// Send body in two parts to exercise partial reads
size_t mid = payload.size() / 2;
write_all(writeFd, payload.data(), mid);
write_all(writeFd, payload.data() + mid, payload.size() - mid);
};
std::string p1 = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":null}";
std::string p2 = "{\"jsonrpc\":\"2.0\",\"method\":\"ping\"}";
sendMsg(p1, false);
sendMsg(p2, true); // case-insensitive header
auto m1 = t.read();
assert(m1.has_value());
assert(m1->raw == p1);
auto m2 = t.read();
assert(m2.has_value());
assert(m2->raw == p2);
// Close write end to signal EOF; next read should return nullopt
::close(writeFd);
auto m3 = t.read();
assert(!m3.has_value());
::close(readFd);
std::puts("test_transport: OK");
return 0;
}

101
test_utfcodec.cc Normal file
View File

@@ -0,0 +1,101 @@
// test_utfcodec.cc - simple tests for UtfCodec helpers
#include <cassert>
#include <cstdio>
#include <string>
#include <string_view>
#include "lsp/UtfCodec.h"
using namespace kte::lsp;
static std::string_view
lp(const std::string &, int)
{
return std::string_view();
}
int
main()
{
// ASCII: each scalar = 1 UTF-16 unit
{
std::string s = "hello"; // 5 ASCII
assert(utf8ColToUtf16Units(s, 0) == 0);
assert(utf8ColToUtf16Units(s, 3) == 3);
assert(utf16UnitsToUtf8Col(s, 3) == 3);
assert(utf16UnitsToUtf8Col(s, 10) == 5); // clamp to EOL
}
// BMP multibyte (e.g., ü U+00FC, α U+03B1) -> still 1 UTF-16 unit
{
std::string s = u8"αb"; // bytes: a [C3 BC] [CE B1] b
// columns by codepoints: a(0), ü(1), α(2), b(3)
assert(utf8ColToUtf16Units(s, 0) == 0);
assert(utf8ColToUtf16Units(s, 1) == 1);
assert(utf8ColToUtf16Units(s, 2) == 2);
assert(utf8ColToUtf16Units(s, 4) == 4); // past EOL clamps to 4 units
assert(utf16UnitsToUtf8Col(s, 0) == 0);
assert(utf16UnitsToUtf8Col(s, 2) == 2);
assert(utf16UnitsToUtf8Col(s, 4) == 4);
}
// Non-BMP (emoji) -> 2 UTF-16 units per code point
{
std::string s = u8"A😀B"; // U+1F600 between A and B
// codepoints: A, 😀, B => utf8 columns 0..3
// utf16 units: A(1), 😀(2), B(1) cumulative: 0,1,3,4
assert(utf8ColToUtf16Units(s, 0) == 0);
assert(utf8ColToUtf16Units(s, 1) == 1); // after A
assert(utf8ColToUtf16Units(s, 2) == 3); // after 😀 (2 units)
assert(utf8ColToUtf16Units(s, 3) == 4); // after B
assert(utf16UnitsToUtf8Col(s, 0) == 0);
assert(utf16UnitsToUtf8Col(s, 1) == 1); // A
assert(utf16UnitsToUtf8Col(s, 2) == 1); // mid-surrogate -> stays before 😀
assert(utf16UnitsToUtf8Col(s, 3) == 2); // end of 😀
assert(utf16UnitsToUtf8Col(s, 4) == 3); // after B
assert(utf16UnitsToUtf8Col(s, 10) == 3); // clamp
}
// Invalid UTF-8: treat invalid byte as U+FFFD (1 UTF-16 unit), consume 1 byte
{
std::string s;
s.push_back('X');
s.push_back(char(0xFF)); // invalid single byte
s.push_back('Y');
// Columns by codepoints as we decode: 'X', U+FFFD, 'Y'
assert(utf8ColToUtf16Units(s, 0) == 0);
assert(utf8ColToUtf16Units(s, 1) == 1);
assert(utf8ColToUtf16Units(s, 2) == 2);
assert(utf8ColToUtf16Units(s, 3) == 3);
assert(utf16UnitsToUtf8Col(s, 0) == 0);
assert(utf16UnitsToUtf8Col(s, 1) == 1);
assert(utf16UnitsToUtf8Col(s, 2) == 2);
assert(utf16UnitsToUtf8Col(s, 3) == 3);
}
// Position/Range helpers with a simple provider
{
std::string lines[] = {u8"A😀B"};
LineProvider provider = [&](const std::string &, int line) -> std::string_view {
return (line == 0) ? std::string_view(lines[0]) : std::string_view();
};
Position p8{0, 2}; // after 😀 in utf8 columns
Position p16 = toUtf16("file:///x", p8, provider);
assert(p16.line == 0 && p16.character == 3);
Position back = toUtf8("file:///x", p16, provider);
assert(back.line == 0 && back.character == 2);
Range r8{{0, 1}, {0, 3}}; // A|😀|B end
Range r16 = toUtf16("file:///x", r8, provider);
assert(r16.start.character == 1 && r16.end.character == 4);
}
std::puts("test_utfcodec: OK");
return 0;
}

177
themes/EInk.h Normal file
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;
}

Some files were not shown because too many files have changed in this diff Show More