5 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
ceef6af3ae Add extensible highlighter registration and Tree-sitter support.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
- Implemented runtime API for registering custom highlighters.
- Added optional Tree-sitter integration for advanced syntax parsing (disabled by default).
- Updated buffer initialization and copying to support dynamic highlighter configuration.
- Introduced `NullHighlighter` as a fallback for unsupported filetypes.
- Enhanced CMake configuration with `KTE_ENABLE_TREESITTER` option.
2025-12-01 19:04:37 -08:00
e62cf3ee28 Add viewport-aware syntax prefetching and background warming.
- Added prefetching in both terminal and GUI renderers to optimize visible row highlights.
- Introduced background worker for offscreen highlight warming to improve scrolling performance.
- Refactored `HighlighterEngine` to manage thread-safety, caching, and stateful re-computation.
- Integrated changes into `HighlighterEngine`, `TerminalRenderer`, and `GUIRenderer`.
- Bumped version to 1.2.0 in preparation for the release.
2025-12-01 18:37:01 -08:00
105 changed files with 34457 additions and 2542 deletions

1
.gitignore vendored
View File

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

View File

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

104
Buffer.cc
View File

@@ -6,6 +6,10 @@
#include "Buffer.h" #include "Buffer.h"
#include "UndoSystem.h" #include "UndoSystem.h"
#include "UndoTree.h" #include "UndoTree.h"
// For reconstructing highlighter state on copies
#include "syntax/HighlighterRegistry.h"
#include "syntax/NullHighlighter.h"
#include "lsp/BufferChangeTracker.h"
Buffer::Buffer() Buffer::Buffer()
@@ -16,6 +20,9 @@ Buffer::Buffer()
} }
Buffer::~Buffer() = default;
Buffer::Buffer(const std::string &path) Buffer::Buffer(const std::string &path)
{ {
std::string err; std::string err;
@@ -40,9 +47,32 @@ Buffer::Buffer(const Buffer &other)
mark_set_ = other.mark_set_; mark_set_ = other.mark_set_;
mark_curx_ = other.mark_curx_; mark_curx_ = other.mark_curx_;
mark_cury_ = other.mark_cury_; mark_cury_ = other.mark_cury_;
// Copy syntax/highlighting flags
version_ = other.version_;
syntax_enabled_ = other.syntax_enabled_;
filetype_ = other.filetype_;
// Fresh undo system for the copy // Fresh undo system for the copy
undo_tree_ = std::make_unique<UndoTree>(); undo_tree_ = std::make_unique<UndoTree>();
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_); undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
// Recreate a highlighter engine for this copy based on filetype/syntax state
if (syntax_enabled_) {
// Allocate engine and install an appropriate highlighter
highlighter_ = std::make_unique<kte::HighlighterEngine>();
if (!filetype_.empty()) {
auto hl = kte::HighlighterRegistry::CreateFor(filetype_);
if (hl) {
highlighter_->SetHighlighter(std::move(hl));
} else {
// Unsupported filetype -> NullHighlighter keeps syntax pipeline active
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
} else {
// No filetype -> keep syntax enabled but use NullHighlighter
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
// Fresh engine has empty caches; nothing to invalidate
}
} }
@@ -65,9 +95,28 @@ Buffer::operator=(const Buffer &other)
mark_set_ = other.mark_set_; mark_set_ = other.mark_set_;
mark_curx_ = other.mark_curx_; mark_curx_ = other.mark_curx_;
mark_cury_ = other.mark_cury_; mark_cury_ = other.mark_cury_;
version_ = other.version_;
syntax_enabled_ = other.syntax_enabled_;
filetype_ = other.filetype_;
// Recreate undo system for this instance // Recreate undo system for this instance
undo_tree_ = std::make_unique<UndoTree>(); undo_tree_ = std::make_unique<UndoTree>();
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_); undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
// Recreate highlighter engine consistent with syntax settings
highlighter_.reset();
if (syntax_enabled_) {
highlighter_ = std::make_unique<kte::HighlighterEngine>();
if (!filetype_.empty()) {
auto hl = kte::HighlighterRegistry::CreateFor(filetype_);
if (hl) {
highlighter_->SetHighlighter(std::move(hl));
} else {
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
} else {
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
}
return *this; return *this;
} }
@@ -91,6 +140,11 @@ Buffer::Buffer(Buffer &&other) noexcept
undo_tree_(std::move(other.undo_tree_)), undo_tree_(std::move(other.undo_tree_)),
undo_sys_(std::move(other.undo_sys_)) undo_sys_(std::move(other.undo_sys_))
{ {
// Move syntax/highlighting state
version_ = other.version_;
syntax_enabled_ = other.syntax_enabled_;
filetype_ = std::move(other.filetype_);
highlighter_ = std::move(other.highlighter_);
// Update UndoSystem's buffer reference to point to this object // Update UndoSystem's buffer reference to point to this object
if (undo_sys_) { if (undo_sys_) {
undo_sys_->UpdateBufferReference(*this); undo_sys_->UpdateBufferReference(*this);
@@ -122,6 +176,12 @@ Buffer::operator=(Buffer &&other) noexcept
undo_tree_ = std::move(other.undo_tree_); undo_tree_ = std::move(other.undo_tree_);
undo_sys_ = std::move(other.undo_sys_); undo_sys_ = std::move(other.undo_sys_);
// Move syntax/highlighting state
version_ = other.version_;
syntax_enabled_ = other.syntax_enabled_;
filetype_ = std::move(other.filetype_);
highlighter_ = std::move(other.highlighter_);
// Update UndoSystem's buffer reference to point to this object // Update UndoSystem's buffer reference to point to this object
if (undo_sys_) { if (undo_sys_) {
undo_sys_->UpdateBufferReference(*this); undo_sys_->UpdateBufferReference(*this);
@@ -338,6 +398,30 @@ Buffer::AsString() const
} }
std::string
Buffer::FullText() const
{
std::string out;
// Precompute size for fewer reallocations
std::size_t total = 0;
for (std::size_t i = 0; i < rows_.size(); ++i) {
total += rows_[i].Size();
if (i + 1 < rows_.size())
total += 1; // for '\n'
}
out.reserve(total);
for (std::size_t i = 0; i < rows_.size(); ++i) {
const char *d = rows_[i].Data();
std::size_t n = rows_[i].Size();
if (d && n)
out.append(d, n);
if (i + 1 < rows_.size())
out.push_back('\n');
}
return out;
}
// --- Raw editing APIs (no undo recording, cursor untouched) --- // --- Raw editing APIs (no undo recording, cursor untouched) ---
void void
Buffer::insert_text(int row, int col, std::string_view text) Buffer::insert_text(int row, int col, std::string_view text)
@@ -376,6 +460,9 @@ Buffer::insert_text(int row, int col, std::string_view text)
remain.erase(0, pos + 1); remain.erase(0, pos + 1);
} }
// Do not set dirty here; UndoSystem will manage state/dirty externally // Do not set dirty here; UndoSystem will manage state/dirty externally
if (change_tracker_) {
change_tracker_->recordInsertion(row, col, std::string(text));
}
} }
@@ -414,6 +501,9 @@ Buffer::delete_text(int row, int col, std::size_t len)
break; break;
} }
} }
if (change_tracker_) {
change_tracker_->recordDeletion(row, col, len);
}
} }
@@ -487,3 +577,17 @@ Buffer::Undo() const
{ {
return undo_sys_.get(); return undo_sys_.get();
} }
void
Buffer::SetChangeTracker(std::unique_ptr<kte::lsp::BufferChangeTracker> tracker)
{
change_tracker_ = std::move(tracker);
}
kte::lsp::BufferChangeTracker *
Buffer::GetChangeTracker()
{
return change_tracker_.get();
}

View File

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

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

View File

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

View File

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

182
Editor.cc
View File

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

View File

@@ -11,6 +11,13 @@
#include "Buffer.h" #include "Buffer.h"
// fwd decl for LSP wiring
namespace kte {
namespace lsp {
class LspManager;
}
}
class Editor { class Editor {
public: public:
@@ -436,6 +443,22 @@ public:
bool OpenFile(const std::string &path, std::string &err); bool OpenFile(const std::string &path, std::string &err);
// LSP: attach/detach manager
void SetLspManager(kte::lsp::LspManager *mgr)
{
lsp_manager_ = mgr;
}
// LSP helpers: trigger hover/definition at current cursor in current buffer
bool LspHoverAtCursor();
bool LspGotoDefinitionAtCursor();
// LSP: notify buffer saved (used by commands)
void NotifyBufferSaved(Buffer *buf);
// Buffer switching/closing // Buffer switching/closing
bool SwitchTo(std::size_t index); bool SwitchTo(std::size_t index);
@@ -551,6 +574,9 @@ public:
private: private:
std::string replace_find_tmp_; std::string replace_find_tmp_;
std::string replace_with_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) { if (v > 0.0f) {
font_size = v; font_size = v;
} }
} else if (key == "theme") { } else if (key == "theme") {
theme = val; theme = val;
} else if (key == "background" || key == "bg") { } else if (key == "background" || key == "bg") {
std::string v = val; std::string v = val;
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
return (char) std::tolower(c); return (char) std::tolower(c);
}); });
if (v == "light" || v == "dark") if (v == "light" || v == "dark")
background = v; background = v;
} else if (key == "syntax") { } else if (key == "syntax") {
std::string v = val; std::string v = val;
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
return (char) std::tolower(c); return (char) std::tolower(c);
}); });
if (v == "1" || v == "on" || v == "true" || v == "yes") { if (v == "1" || v == "on" || v == "true" || v == "yes") {
syntax = true; syntax = true;
} else if (v == "0" || v == "off" || v == "false" || v == "no") { } else if (v == "0" || v == "off" || v == "false" || v == "no") {
syntax = false; syntax = false;
} }
} }
} }
return true; return true;
} }

View File

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

View File

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

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

525
docs/lsp plan.md Normal file
View File

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

View File

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

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

View File

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

107
syntax/LispHighlighter.cc Normal file
View File

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

View File

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

View File

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

View File

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

17
syntax/NullHighlighter.cc Normal file
View File

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

View File

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

172
syntax/PythonHighlighter.cc Normal file
View File

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

View File

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

145
syntax/RustHighlighter.cc Normal file
View File

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

18
syntax/RustHighlighter.h Normal file
View File

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

105
syntax/ShellHighlighter.cc Normal file
View File

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

View File

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

156
syntax/SqlHighlighter.cc Normal file
View File

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

18
syntax/SqlHighlighter.h Normal file
View File

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

View File

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

View File

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

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;
}

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