LSP integration steps 1-4, part of 5.

This commit is contained in:
2025-12-01 20:09:49 -08:00
parent ceef6af3ae
commit e089c6e4d1
56 changed files with 3685 additions and 1638 deletions

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" />

View File

@@ -9,6 +9,7 @@
// For reconstructing highlighter state on copies // For reconstructing highlighter state on copies
#include "HighlighterRegistry.h" #include "HighlighterRegistry.h"
#include "NullHighlighter.h" #include "NullHighlighter.h"
#include "lsp/BufferChangeTracker.h"
Buffer::Buffer() Buffer::Buffer()
@@ -19,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;
@@ -394,6 +398,30 @@ Buffer::AsString() const
} }
std::string
Buffer::FullText() const
{
std::string out;
// Precompute size for fewer reallocations
std::size_t total = 0;
for (std::size_t i = 0; i < rows_.size(); ++i) {
total += rows_[i].Size();
if (i + 1 < rows_.size())
total += 1; // for '\n'
}
out.reserve(total);
for (std::size_t i = 0; i < rows_.size(); ++i) {
const char *d = rows_[i].Data();
std::size_t n = rows_[i].Size();
if (d && n)
out.append(d, n);
if (i + 1 < rows_.size())
out.push_back('\n');
}
return out;
}
// --- Raw editing APIs (no undo recording, cursor untouched) --- // --- 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)
@@ -432,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));
}
} }
@@ -470,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);
}
} }
@@ -543,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

@@ -17,11 +17,20 @@
#include "HighlighterEngine.h" #include "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

@@ -16,36 +16,36 @@ option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" 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) if (KTE_ENABLE_TREESITTER)
add_compile_definitions(KTE_ENABLE_TREESITTER) add_compile_definitions(KTE_ENABLE_TREESITTER)
endif () 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
@@ -55,222 +55,239 @@ find_package(Curses REQUIRED)
include_directories(${CURSES_INCLUDE_DIR}) include_directories(${CURSES_INCLUDE_DIR})
set(COMMON_SOURCES set(COMMON_SOURCES
GapBuffer.cc GapBuffer.cc
PieceTable.cc PieceTable.cc
Buffer.cc Buffer.cc
Editor.cc Editor.cc
Command.cc Command.cc
HelpText.cc HelpText.cc
KKeymap.cc KKeymap.cc
TerminalInputHandler.cc TerminalInputHandler.cc
TerminalRenderer.cc TerminalRenderer.cc
TerminalFrontend.cc TerminalFrontend.cc
TestInputHandler.cc TestInputHandler.cc
TestRenderer.cc TestRenderer.cc
TestFrontend.cc TestFrontend.cc
UndoNode.cc UndoNode.cc
UndoTree.cc UndoTree.cc
UndoSystem.cc UndoSystem.cc
HighlighterEngine.cc HighlighterEngine.cc
CppHighlighter.cc CppHighlighter.cc
HighlighterRegistry.cc HighlighterRegistry.cc
NullHighlighter.cc NullHighlighter.cc
JsonHighlighter.cc JsonHighlighter.cc
MarkdownHighlighter.cc MarkdownHighlighter.cc
ShellHighlighter.cc ShellHighlighter.cc
GoHighlighter.cc GoHighlighter.cc
PythonHighlighter.cc PythonHighlighter.cc
RustHighlighter.cc RustHighlighter.cc
LispHighlighter.cc LispHighlighter.cc
lsp/BufferChangeTracker.cc
lsp/JsonRpcTransport.cc
lsp/LspProcessClient.cc
lsp/DiagnosticStore.cc
lsp/TerminalDiagnosticDisplay.cc
lsp/LspManager.cc
) )
if (KTE_ENABLE_TREESITTER) if (KTE_ENABLE_TREESITTER)
list(APPEND COMMON_SOURCES list(APPEND COMMON_SOURCES
TreeSitterHighlighter.cc) TreeSitterHighlighter.cc)
endif () 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 LanguageHighlighter.h
HighlighterEngine.h HighlighterEngine.h
CppHighlighter.h CppHighlighter.h
HighlighterRegistry.h HighlighterRegistry.h
NullHighlighter.h NullHighlighter.h
JsonHighlighter.h JsonHighlighter.h
MarkdownHighlighter.h MarkdownHighlighter.h
ShellHighlighter.h ShellHighlighter.h
GoHighlighter.h GoHighlighter.h
PythonHighlighter.h PythonHighlighter.h
RustHighlighter.h RustHighlighter.h
LispHighlighter.h LispHighlighter.h
lsp/LspTypes.h
lsp/BufferChangeTracker.h
lsp/JsonRpcTransport.h
lsp/LspClient.h
lsp/LspProcessClient.h
lsp/Diagnostic.h
lsp/DiagnosticStore.h
lsp/DiagnosticDisplay.h
lsp/TerminalDiagnosticDisplay.h
lsp/LspManager.h
lsp/LspServerConfig.h
) )
if (KTE_ENABLE_TREESITTER) if (KTE_ENABLE_TREESITTER)
list(APPEND COMMON_HEADERS list(APPEND COMMON_HEADERS
TreeSitterHighlighter.h) TreeSitterHighlighter.h)
endif () endif ()
# kte (terminal-first) executable # kte (terminal-first) executable
add_executable(kte add_executable(kte
main.cc main.cc
${COMMON_SOURCES} ${COMMON_SOURCES}
${COMMON_HEADERS} ${COMMON_HEADERS}
) )
if (KTE_USE_PIECE_TABLE) if (KTE_USE_PIECE_TABLE)
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1) target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
endif () endif ()
if (KTE_UNDO_DEBUG) if (KTE_UNDO_DEBUG)
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1) target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
endif () endif ()
target_link_libraries(kte ${CURSES_LIBRARIES}) target_link_libraries(kte ${CURSES_LIBRARIES})
if (KTE_ENABLE_TREESITTER) if (KTE_ENABLE_TREESITTER)
# Users can provide their own tree-sitter include/lib via cache variables # 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_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)") set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)")
if (TREESITTER_INCLUDE_DIR) if (TREESITTER_INCLUDE_DIR)
target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR}) target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR})
endif () endif ()
if (TREESITTER_LIBRARY) if (TREESITTER_LIBRARY)
target_link_libraries(kte ${TREESITTER_LIBRARY}) target_link_libraries(kte ${TREESITTER_LIBRARY})
endif () endif ()
endif () endif ()
install(TARGETS kte install(TARGETS kte
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
) )
# Man pages # Man pages
install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
if (BUILD_TESTS) if (BUILD_TESTS)
# test_undo executable for testing undo/redo system # test_undo executable for testing undo/redo system
add_executable(test_undo add_executable(test_undo
test_undo.cc test_undo.cc
${COMMON_SOURCES} ${COMMON_SOURCES}
${COMMON_HEADERS} ${COMMON_HEADERS}
) )
if (KTE_USE_PIECE_TABLE) if (KTE_USE_PIECE_TABLE)
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1) target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
endif () endif ()
if (KTE_UNDO_DEBUG) if (KTE_UNDO_DEBUG)
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1) target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
endif () endif ()
target_link_libraries(test_undo ${CURSES_LIBRARIES}) target_link_libraries(test_undo ${CURSES_LIBRARIES})
if (KTE_ENABLE_TREESITTER) if (KTE_ENABLE_TREESITTER)
if (TREESITTER_INCLUDE_DIR) if (TREESITTER_INCLUDE_DIR)
target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR}) target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR})
endif () endif ()
if (TREESITTER_LIBRARY) if (TREESITTER_LIBRARY)
target_link_libraries(test_undo ${TREESITTER_LIBRARY}) target_link_libraries(test_undo ${TREESITTER_LIBRARY})
endif () endif ()
endif () endif ()
endif () endif ()
if (${BUILD_GUI}) if (${BUILD_GUI})
target_sources(kte PRIVATE target_sources(kte PRIVATE
Font.h Font.h
GUIConfig.cc GUIConfig.cc
GUIConfig.h GUIConfig.h
GUIRenderer.cc GUIRenderer.cc
GUIRenderer.h GUIRenderer.h
GUIInputHandler.cc GUIInputHandler.cc
GUIInputHandler.h GUIInputHandler.h
GUIFrontend.cc GUIFrontend.cc
GUIFrontend.h) GUIFrontend.h)
target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1) target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
target_link_libraries(kte imgui) target_link_libraries(kte imgui)
# kge (GUI-first) executable # kge (GUI-first) executable
add_executable(kge add_executable(kge
main.cc main.cc
${COMMON_SOURCES} ${COMMON_SOURCES}
${COMMON_HEADERS} ${COMMON_HEADERS}
GUIConfig.cc GUIConfig.cc
GUIConfig.h GUIConfig.h
GUIRenderer.cc GUIRenderer.cc
GUIRenderer.h GUIRenderer.h
GUIInputHandler.cc GUIInputHandler.cc
GUIInputHandler.h GUIInputHandler.h
GUIFrontend.cc GUIFrontend.cc
GUIFrontend.h) GUIFrontend.h)
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE}) target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
if (KTE_UNDO_DEBUG) if (KTE_UNDO_DEBUG)
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1) target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
endif () endif ()
target_link_libraries(kge ${CURSES_LIBRARIES} imgui) target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
# On macOS, build kge as a proper .app bundle # On macOS, build kge as a proper .app bundle
if (APPLE) if (APPLE)
# Define the icon file # Define the icon file
set(MACOSX_BUNDLE_ICON_FILE kge.icns) set(MACOSX_BUNDLE_ICON_FILE kge.icns)
set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}") set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
# Add icon to the target sources and mark it as a resource # Add icon to the target sources and mark it as a resource
target_sources(kge PRIVATE ${kge_ICON}) target_sources(kge PRIVATE ${kge_ICON})
set_source_files_properties(${kge_ICON} PROPERTIES set_source_files_properties(${kge_ICON} PROPERTIES
MACOSX_PACKAGE_LOCATION Resources) MACOSX_PACKAGE_LOCATION Resources)
# Configure Info.plist with version and identifiers # Configure Info.plist with version and identifiers
set(KGE_BUNDLE_ID "dev.wntrmute.kge") set(KGE_BUNDLE_ID "dev.wntrmute.kge")
configure_file( configure_file(
${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in ${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in
${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist ${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
@ONLY) @ONLY)
set_target_properties(kge PROPERTIES set_target_properties(kge PROPERTIES
MACOSX_BUNDLE TRUE MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID} MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
MACOSX_BUNDLE_BUNDLE_NAME "kge" MACOSX_BUNDLE_BUNDLE_NAME "kge"
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE} MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist") MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
add_dependencies(kge kte) add_dependencies(kge kte)
add_custom_command(TARGET kge POST_BUILD add_custom_command(TARGET kge POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:kte> $<TARGET_FILE:kte>
$<TARGET_FILE_DIR:kge>/kte $<TARGET_FILE_DIR:kge>/kte
COMMENT "Copying kte binary into kge.app bundle") COMMENT "Copying kte binary into kge.app bundle")
install(TARGETS kge install(TARGETS kge
BUNDLE DESTINATION . BUNDLE DESTINATION .
) )
install(TARGETS kte install(TARGETS kte
RUNTIME DESTINATION kge.app/Contents/MacOS RUNTIME DESTINATION kge.app/Contents/MacOS
) )
else () else ()
install(TARGETS kge install(TARGETS kge
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
) )
endif () endif ()
# Install kge man page only when GUI is built # Install kge man page only when GUI is built
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons) install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
endif () endif ()

View File

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

View File

@@ -3,168 +3,277 @@
#include <cctype> #include <cctype>
namespace kte { namespace kte {
static bool
is_digit(char c)
{
return c >= '0' && c <= '9';
}
static bool is_digit(char c) { return c >= '0' && c <= '9'; }
CppHighlighter::CppHighlighter() CppHighlighter::CppHighlighter()
{ {
const char *kw[] = { const char *kw[] = {
"if","else","for","while","do","switch","case","default","break","continue", "if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue",
"return","goto","struct","class","namespace","using","template","typename", "return", "goto", "struct", "class", "namespace", "using", "template", "typename",
"public","private","protected","virtual","override","const","constexpr","auto", "public", "private", "protected", "virtual", "override", "const", "constexpr", "auto",
"static","inline","operator","new","delete","try","catch","throw","friend", "static", "inline", "operator", "new", "delete", "try", "catch", "throw", "friend",
"enum","union","extern","volatile","mutable","noexcept","sizeof","this" "enum", "union", "extern", "volatile", "mutable", "noexcept", "sizeof", "this"
}; };
for (auto s: kw) keywords_.insert(s); for (auto s: kw)
const char *types[] = { keywords_.insert(s);
"int","long","short","char","signed","unsigned","float","double","void", const char *types[] = {
"bool","wchar_t","size_t","ptrdiff_t","uint8_t","uint16_t","uint32_t","uint64_t", "int", "long", "short", "char", "signed", "unsigned", "float", "double", "void",
"int8_t","int16_t","int32_t","int64_t" "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); };
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 bool
CppHighlighter::is_ident_start(char c)
{ {
// Stateless entry simply delegates to stateful with a clean previous state return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
StatefulHighlighter::LineState prev;
(void)HighlightLineStateful(buf, row, prev, out);
} }
StatefulHighlighter::LineState CppHighlighter::HighlightLineStateful(const Buffer &buf,
int row, bool
const LineState &prev, CppHighlighter::is_ident_char(char c)
std::vector<HighlightSpan> &out) const
{ {
const auto &rows = buf.Rows(); return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
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
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

@@ -11,24 +11,25 @@
class Buffer; class Buffer;
namespace kte { namespace kte {
class CppHighlighter final : public StatefulHighlighter { class CppHighlighter final : public StatefulHighlighter {
public: public:
CppHighlighter(); CppHighlighter();
~CppHighlighter() override = default;
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override; ~CppHighlighter() override = default;
LineState HighlightLineStateful(const Buffer &buf,
int row, void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
const LineState &prev,
std::vector<HighlightSpan> &out) const override; LineState HighlightLineStateful(const Buffer &buf,
int row,
const LineState &prev,
std::vector<HighlightSpan> &out) const override;
private: private:
std::unordered_set<std::string> keywords_; std::unordered_set<std::string> keywords_;
std::unordered_set<std::string> types_; std::unordered_set<std::string> types_;
static bool is_ident_start(char c); static bool is_ident_start(char c);
static bool is_ident_char(char c);
static bool is_ident_char(char c);
}; };
} // namespace kte
} // namespace kte

187
Editor.cc
View File

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

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

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

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

View File

@@ -10,16 +10,17 @@
#include <cctype> #include <cctype>
// Small helper to convert packed RGB (0xRRGGBB) + optional alpha to ImVec4 // Small helper to convert packed RGB (0xRRGGBB) + optional alpha to ImVec4
static inline ImVec4 RGBA(unsigned int rgb, float a = 1.0f) static inline ImVec4
RGBA(unsigned int rgb, float a = 1.0f)
{ {
const float r = static_cast<float>((rgb >> 16) & 0xFF) / 255.0f; const float r = static_cast<float>((rgb >> 16) & 0xFF) / 255.0f;
const float g = static_cast<float>((rgb >> 8) & 0xFF) / 255.0f; const float g = static_cast<float>((rgb >> 8) & 0xFF) / 255.0f;
const float b = static_cast<float>(rgb & 0xFF) / 255.0f; const float b = static_cast<float>(rgb & 0xFF) / 255.0f;
return ImVec4(r, g, b, a); return ImVec4(r, g, b, a);
} }
namespace kte {
namespace kte {
// Background mode selection for light/dark palettes // Background mode selection for light/dark palettes
enum class BackgroundMode { Light, Dark }; enum class BackgroundMode { Light, Dark };
@@ -28,20 +29,21 @@ static inline BackgroundMode gBackgroundMode = BackgroundMode::Dark;
// Basic theme identifier (kept minimal; some ids are aliases) // Basic theme identifier (kept minimal; some ids are aliases)
enum class ThemeId { enum class ThemeId {
EInk = 0, EInk = 0,
GruvboxDarkMedium = 1, GruvboxDarkMedium = 1,
GruvboxLightMedium = 1, // alias to unified gruvbox index GruvboxLightMedium = 1, // alias to unified gruvbox index
Nord = 2, Nord = 2,
Plan9 = 3, Plan9 = 3,
Solarized = 4, Solarized = 4,
}; };
// Current theme tracking // Current theme tracking
static inline ThemeId gCurrentTheme = ThemeId::Nord; static inline ThemeId gCurrentTheme = ThemeId::Nord;
static inline std::size_t gCurrentThemeIndex = 0; static inline std::size_t gCurrentThemeIndex = 0;
// Forward declarations for helpers used below // Forward declarations for helpers used below
static inline size_t ThemeIndexFromId(ThemeId id); static inline size_t ThemeIndexFromId(ThemeId id);
static inline ThemeId ThemeIdFromIndex(size_t idx); static inline ThemeId ThemeIdFromIndex(size_t idx);
// Helpers to set/query background mode // Helpers to set/query background mode
@@ -1098,18 +1100,18 @@ CurrentThemeName()
static inline size_t static inline size_t
ThemeIndexFromId(ThemeId id) ThemeIndexFromId(ThemeId id)
{ {
switch (id) { switch (id) {
case ThemeId::EInk: case ThemeId::EInk:
return 0; return 0;
case ThemeId::GruvboxDarkMedium: case ThemeId::GruvboxDarkMedium:
return 1; return 1;
case ThemeId::Nord: case ThemeId::Nord:
return 2; return 2;
case ThemeId::Plan9: case ThemeId::Plan9:
return 3; return 3;
case ThemeId::Solarized: case ThemeId::Solarized:
return 4; return 4;
} }
return 0; return 0;
} }
@@ -1132,29 +1134,46 @@ ThemeIdFromIndex(size_t idx)
} }
} }
// --- Syntax palette (v1): map TokenKind to ink color per current theme/background --- // --- Syntax palette (v1): map TokenKind to ink color per current theme/background ---
static inline ImVec4 SyntaxInk(TokenKind k) static inline ImVec4
SyntaxInk(TokenKind k)
{ {
// Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults // Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark); const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
// Base text // Base text
ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440); ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
switch (k) { switch (k) {
case TokenKind::Keyword: return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC); case TokenKind::Keyword:
case TokenKind::Type: return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A); return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC);
case TokenKind::String: return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E); case TokenKind::Type:
case TokenKind::Char: return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E); return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A);
case TokenKind::Comment: return dark ? RGBA(0x616E88) : RGBA(0x7A869A); case TokenKind::String:
case TokenKind::Number: return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900); return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
case TokenKind::Preproc: return dark ? RGBA(0xD08770) : RGBA(0xAF3A03); case TokenKind::Char:
case TokenKind::Constant: return dark ? RGBA(0xB48EAD) : RGBA(0x7B4B7F); return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
case TokenKind::Function: return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4); case TokenKind::Comment:
case TokenKind::Operator: return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440); return dark ? RGBA(0x616E88) : RGBA(0x7A869A);
case TokenKind::Punctuation: return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440); case TokenKind::Number:
case TokenKind::Identifier: return def; return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900);
case TokenKind::Whitespace: return def; case TokenKind::Preproc:
case TokenKind::Error: return dark ? RGBA(0xBF616A) : RGBA(0xCC0000); return dark ? RGBA(0xD08770) : RGBA(0xAF3A03);
case TokenKind::Default: default: return def; case TokenKind::Constant:
} return dark ? RGBA(0xB48EAD) : RGBA(0x7B4B7F);
case TokenKind::Function:
return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4);
case TokenKind::Operator:
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
case TokenKind::Punctuation:
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
case TokenKind::Identifier:
return def;
case TokenKind::Whitespace:
return def;
case TokenKind::Error:
return dark ? RGBA(0xBF616A) : RGBA(0xCC0000);
case TokenKind::Default: default:
return def;
}
} }
} // namespace kte } // namespace kte

View File

@@ -3,46 +3,155 @@
#include <cctype> #include <cctype>
namespace kte { 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 == '_';
}
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() 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"}; const char *kw[] = {
for (auto s: kw) kws_.insert(s); "break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", "func",
const char* tp[] = {"bool","byte","complex64","complex128","error","float32","float64","int","int8","int16","int32","int64","rune","string","uint","uint8","uint16","uint32","uint64","uintptr"}; "go", "goto", "if", "import", "interface", "map", "package", "range", "return", "select", "struct",
for (auto s: tp) types_.insert(s); "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
void
GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{ {
const auto &rows = buf.Rows(); const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return; if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]); return;
int n = static_cast<int>(s.size()); std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int i = 0; int n = static_cast<int>(s.size());
int bol=0; while (bol<n && (s[bol]==' '||s[bol]=='\t')) ++bol; int i = 0;
// line comment int bol = 0;
while (i < n) { while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
char c = s[i]; ++bol;
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 comment
if (c=='/' && i+1<n && s[i+1]=='/') { push(out,i,n,TokenKind::Comment); break; } while (i < n) {
if (c=='/' && i+1<n && s[i+1]=='*') { char c = s[i];
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 (c == ' ' || c == '\t') {
if (!closed) { push(out,i,n,TokenKind::Comment); break; } else { push(out,i,j,TokenKind::Comment); i=j; continue; } int j = i + 1;
} while (j < n && (s[j] == ' ' || s[j] == '\t'))
if (c=='"' || c=='`') { ++j;
char q=c; int j=i+1; bool esc=false; if (q=='`') { while (j<n && s[j] != '`') ++j; if (j<n) ++j; } push(out, i, j, TokenKind::Whitespace);
else { while (j<n){ char d=s[j++]; if (esc){esc=false; continue;} if (d=='\\'){esc=true; continue;} if (d=='"') break;} } i = j;
push(out,i,j,TokenKind::String); i=j; continue; 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 (c == '/' && i + 1 < n && s[i + 1] == '/') {
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; } push(out, i, n, TokenKind::Comment);
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; } break;
push(out,i,i+1,TokenKind::Default); ++i; }
} 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
} // namespace kte

View File

@@ -5,14 +5,14 @@
#include <unordered_set> #include <unordered_set>
namespace kte { namespace kte {
class GoHighlighter final : public LanguageHighlighter { class GoHighlighter final : public LanguageHighlighter {
public: public:
GoHighlighter(); 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 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

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

View File

@@ -16,61 +16,70 @@
class Buffer; class Buffer;
namespace kte { namespace kte {
class HighlighterEngine { class HighlighterEngine {
public: public:
HighlighterEngine(); HighlighterEngine();
~HighlighterEngine();
void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl); ~HighlighterEngine();
// Retrieve highlights for a given line and buffer version. void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl);
// 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) // Retrieve highlights for a given line and buffer version.
void InvalidateFrom(int row); // If cache is stale, recompute using the current highlighter.
const LineHighlight &GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
bool HasHighlighter() const { return static_cast<bool>(hl_); } // Invalidate cached lines from row (inclusive)
void InvalidateFrom(int row);
// Phase 3: viewport-first prefetch and background warming
// Compute only the visible range now, and enqueue a background warm-around task. bool HasHighlighter() const
// 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; 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: private:
std::unique_ptr<LanguageHighlighter> hl_; std::unique_ptr<LanguageHighlighter> hl_;
// Simple cache by row index (mutable to allow caching in const GetLine) // Simple cache by row index (mutable to allow caching in const GetLine)
mutable std::unordered_map<int, LineHighlight> cache_; 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 // For stateful highlighters, remember per-line state (state after finishing that row)
mutable std::unordered_map<std::uint64_t, int> state_last_contig_; struct StateEntry {
std::uint64_t version{0};
// Using the interface type; forward-declare via header
StatefulHighlighter::LineState state;
};
// Thread-safety for caches and background worker state mutable std::unordered_map<int, StateEntry> state_cache_;
mutable std::mutex mtx_;
// Background warmer // Track best known contiguous state row for a given version to avoid O(n) scans
struct WarmRequest { mutable std::unordered_map<std::uint64_t, int> state_last_contig_;
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; // Thread-safety for caches and background worker state
void worker_loop() const; 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
} // namespace kte

View File

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

View File

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

View File

@@ -3,40 +3,88 @@
#include <cctype> #include <cctype>
namespace kte { namespace kte {
static bool
static bool is_digit(char c) { return c >= '0' && c <= '9'; } is_digit(char c)
void JSONHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{ {
const auto &rows = buf.Rows(); return c >= '0' && c <= '9';
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
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

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

View File

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

@@ -3,86 +3,130 @@
#include <cctype> #include <cctype>
namespace kte { namespace kte {
static void
static void push_span(std::vector<HighlightSpan> &out, int a, int b, TokenKind k) { 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 if (b > a)
(void)HighlightLineStateful(buf, row, st, out); out.push_back({a, b, k});
} }
StatefulHighlighter::LineState MarkdownHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev, std::vector<HighlightSpan> &out) const
void
MarkdownHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{ {
StatefulHighlighter::LineState state = prev; LineState st; // not used in stateless entry
const auto &rows = buf.Rows(); (void) HighlightLineStateful(buf, row, st, out);
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
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

@@ -4,11 +4,11 @@
#include "LanguageHighlighter.h" #include "LanguageHighlighter.h"
namespace kte { namespace kte {
class MarkdownHighlighter final : public StatefulHighlighter { class MarkdownHighlighter final : public StatefulHighlighter {
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;
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev, std::vector<HighlightSpan> &out) const override;
};
} // namespace kte LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
std::vector<HighlightSpan> &out) const override;
};
} // namespace kte

View File

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

View File

@@ -3,83 +3,170 @@
#include <cctype> #include <cctype>
namespace kte { 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 == '_';
}
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() 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"}; const char *kw[] = {
for (auto s: kw) kws_.insert(s); "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
void
PythonHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{ {
LineState st; (void)HighlightLineStateful(buf, row, st, out); 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
PythonHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
std::vector<HighlightSpan> &out) const
{ {
StatefulHighlighter::LineState state = prev; StatefulHighlighter::LineState state = prev;
const auto &rows = buf.Rows(); const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return state; if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]); return state;
int n = static_cast<int>(s.size()); 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 "\"\"\"" // Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) { if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
auto pos = s.find(state.raw_delim); auto pos = s.find(state.raw_delim);
if (pos == std::string::npos) { if (pos == std::string::npos) {
push(out, 0, n, TokenKind::String); push(out, 0, n, TokenKind::String);
return state; // still inside return state; // still inside
} else { } else {
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size())); int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
push(out, 0, end, TokenKind::String); push(out, 0, end, TokenKind::String);
// remainder processed normally // remainder processed normally
s = s.substr(end); s = s.substr(end);
n = static_cast<int>(s.size()); n = static_cast<int>(s.size());
state.in_raw_string = false; state.raw_delim.clear(); state.in_raw_string = false;
// Continue parsing remainder as a separate small loop state.raw_delim.clear();
int base = end; // original offset, but we already emitted to 'out' with base=0; following spans should be from 'end' // Continue parsing remainder as a separate small loop
// For simplicity, mark rest as Default int base = end;
if (n>0) push(out, base, base + n, TokenKind::Default); // original offset, but we already emitted to 'out' with base=0; following spans should be from 'end'
return state; // For simplicity, mark rest as Default
} if (n > 0)
} push(out, base, base + n, TokenKind::Default);
return state;
}
}
int i = 0; int i = 0;
// Detect comment start '#', ignoring inside strings // Detect comment start '#', ignoring inside strings
while (i < n) { while (i < n) {
char c = s[i]; 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 == ' ' || c == '\t') {
if (c=='#') { push(out,i,n,TokenKind::Comment); break; } int j = i + 1;
// Strings: triple quotes and single-line while (j < n && (s[j] == ' ' || s[j] == '\t'))
if (c=='"' || c=='\'') { ++j;
char q=c; push(out, i, j, TokenKind::Whitespace);
// triple? i = j;
if (i+2 < n && s[i+1]==q && s[i+2]==q) { continue;
std::string delim(3, q); }
int j = i+3; // search for closing triple if (c == '#') {
auto pos = s.find(delim, static_cast<std::size_t>(j)); push(out, i, n, TokenKind::Comment);
if (pos == std::string::npos) { break;
push(out,i,n,TokenKind::String); }
state.in_raw_string = true; state.raw_delim = delim; return state; // Strings: triple quotes and single-line
} else { if (c == '"' || c == '\'') {
int end = static_cast<int>(pos + 3); char q = c;
push(out,i,end,TokenKind::String); i=end; continue; // triple?
} if (i + 2 < n && s[i + 1] == q && s[i + 2] == q) {
} else { std::string delim(3, q);
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; } int j = i + 3; // search for closing triple
push(out,i,j,TokenKind::String); i=j; continue; auto pos = s.find(delim, static_cast<std::size_t>(j));
} if (pos == std::string::npos) {
} push(out, i, n, TokenKind::String);
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; } state.in_raw_string = true;
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; } state.raw_delim = delim;
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; } return state;
push(out,i,i+1,TokenKind::Default); ++i; } else {
} int end = static_cast<int>(pos + 3);
return state; 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
} // namespace kte

View File

@@ -5,14 +5,16 @@
#include <unordered_set> #include <unordered_set>
namespace kte { namespace kte {
class PythonHighlighter final : public StatefulHighlighter { class PythonHighlighter final : public StatefulHighlighter {
public: public:
PythonHighlighter(); 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 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

@@ -3,37 +3,143 @@
#include <cctype> #include <cctype>
namespace kte { 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 == '_';
}
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() 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"}; const char *kw[] = {
for (auto s: kw) kws_.insert(s); "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if",
const char* tp[] = {"u8","u16","u32","u64","u128","usize","i8","i16","i32","i64","i128","isize","f32","f64","bool","char","str"}; "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self",
for (auto s: tp) types_.insert(s); "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
void
RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{ {
const auto &rows = buf.Rows(); const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return; if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]); return;
int n = static_cast<int>(s.size()); std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int i = 0; int n = static_cast<int>(s.size());
while (i < n) { int i = 0;
char c = s[i]; while (i < n) {
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; } char c = s[i];
if (c=='/' && i+1<n && s[i+1]=='/') { push(out,i,n,TokenKind::Comment); break; } if (c == ' ' || c == '\t') {
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; } } int j = i + 1;
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; } while (j < n && (s[j] == ' ' || s[j] == '\t'))
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; } ++j;
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; } push(out, i, j, TokenKind::Whitespace);
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; } i = j;
push(out,i,i+1,TokenKind::Default); ++i; 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
} // namespace kte

View File

@@ -5,14 +5,14 @@
#include <unordered_set> #include <unordered_set>
namespace kte { namespace kte {
class RustHighlighter final : public LanguageHighlighter { class RustHighlighter final : public LanguageHighlighter {
public: public:
RustHighlighter(); 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 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

@@ -3,41 +3,103 @@
#include <cctype> #include <cctype>
namespace kte { namespace kte {
static void
static void push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); } push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
void ShellHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{ {
const auto &rows = buf.Rows(); if (b > a)
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return; out.push_back({a, b, k});
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
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

View File

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

View File

@@ -6,41 +6,46 @@
#include <utility> #include <utility>
namespace kte { namespace kte {
TreeSitterHighlighter::TreeSitterHighlighter(const TSLanguage *lang, std::string filetype)
: language_(lang), filetype_(std::move(filetype)) {}
TreeSitterHighlighter::TreeSitterHighlighter(const TSLanguage* lang, std::string filetype)
: language_(lang), filetype_(std::move(filetype))
{
}
TreeSitterHighlighter::~TreeSitterHighlighter() TreeSitterHighlighter::~TreeSitterHighlighter()
{ {
disposeParser(); disposeParser();
} }
void TreeSitterHighlighter::ensureParsed(const Buffer& /*buf*/) const
void
TreeSitterHighlighter::ensureParsed(const Buffer & /*buf*/) const
{ {
// Intentionally a stub to avoid pulling the Tree-sitter API and library by default. // 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_, // In future, when linking against tree-sitter, initialize parser_, set language_,
// and build tree_ from the buffer contents. // and build tree_ from the buffer contents.
} }
void TreeSitterHighlighter::disposeParser() const
void
TreeSitterHighlighter::disposeParser() const
{ {
// Stub; nothing to dispose when not actually creating parser/tree // Stub; nothing to dispose when not actually creating parser/tree
} }
void TreeSitterHighlighter::HighlightLine(const Buffer &/*buf*/, int /*row*/, std::vector<HighlightSpan> &/*out*/) const
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. // 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)()) std::unique_ptr<LanguageHighlighter>
CreateTreeSitterHighlighter(const char *filetype,
const void * (*get_lang)())
{ {
const auto* lang = reinterpret_cast<const TSLanguage*>(get_lang ? get_lang() : nullptr); const auto *lang = reinterpret_cast<const TSLanguage *>(get_lang ? get_lang() : nullptr);
return std::make_unique<TreeSitterHighlighter>(lang, filetype ? std::string(filetype) : std::string()); return std::make_unique < TreeSitterHighlighter > (lang, filetype ? std::string(filetype) : std::string());
} }
} // namespace kte } // namespace kte
#endif // KTE_ENABLE_TREESITTER #endif // KTE_ENABLE_TREESITTER

View File

@@ -17,32 +17,32 @@ struct TSTree;
} }
namespace kte { namespace kte {
// A minimal adapter that uses Tree-sitter to parse the whole buffer and then, for now, // 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 // 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). // queries. If no queries are provided, it currently produces no spans (safe fallback).
class TreeSitterHighlighter : public LanguageHighlighter { class TreeSitterHighlighter : public LanguageHighlighter {
public: public:
explicit TreeSitterHighlighter(const TSLanguage* lang, std::string filetype); explicit TreeSitterHighlighter(const TSLanguage *lang, std::string filetype);
~TreeSitterHighlighter() override;
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override; ~TreeSitterHighlighter() override;
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private: private:
const TSLanguage* language_{nullptr}; const TSLanguage *language_{nullptr};
std::string filetype_; std::string filetype_;
// Lazy parser to avoid startup cost; mutable to allow creation in const method // Lazy parser to avoid startup cost; mutable to allow creation in const method
mutable TSParser* parser_{nullptr}; mutable TSParser *parser_{nullptr};
mutable TSTree* tree_{nullptr}; mutable TSTree *tree_{nullptr};
void ensureParsed(const Buffer& buf) const; void ensureParsed(const Buffer &buf) const;
void disposeParser() const;
void disposeParser() const;
}; };
// Factory used by HighlighterRegistry when registering via RegisterTreeSitter. // Factory used by HighlighterRegistry when registering via RegisterTreeSitter.
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char* filetype, std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
const void* (*get_lang)()); const void * (*get_lang)());
} // namespace kte } // namespace kte
#endif // KTE_ENABLE_TREESITTER #endif // KTE_ENABLE_TREESITTER

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

19
lsp/JsonRpcTransport.cc Normal file
View File

@@ -0,0 +1,19 @@
/*
* JsonRpcTransport.cc - placeholder
*/
#include "JsonRpcTransport.h"
namespace kte::lsp {
void
JsonRpcTransport::send(const std::string &/*method*/, const std::string &/*payload*/)
{
// stub: no-op
}
std::optional<JsonRpcMessage>
JsonRpcTransport::read()
{
return std::nullopt; // stub
}
} // namespace kte::lsp

29
lsp/JsonRpcTransport.h Normal file
View File

@@ -0,0 +1,29 @@
/*
* JsonRpcTransport.h - placeholder transport for JSON-RPC over stdio (stub)
*/
#ifndef KTE_JSON_RPC_TRANSPORT_H
#define KTE_JSON_RPC_TRANSPORT_H
#include <optional>
#include <string>
namespace kte::lsp {
struct JsonRpcMessage {
std::string raw; // raw JSON payload (stub)
};
class JsonRpcTransport {
public:
JsonRpcTransport() = default;
~JsonRpcTransport() = default;
// Send a method call (request or notification) - stub does nothing
void send(const std::string &method, const std::string &payload);
// Blocking read next message (stub => returns nullopt)
std::optional<JsonRpcMessage> read();
};
} // namespace kte::lsp
#endif // KTE_JSON_RPC_TRANSPORT_H

61
lsp/LspClient.h Normal file
View File

@@ -0,0 +1,61 @@
/*
* 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"
namespace kte::lsp {
// Callback types (stubs for future phases)
using CompletionCallback = std::function<void()>;
using HoverCallback = std::function<void()>;
using LocationCallback = std::function<void()>;
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 (not yet implemented)
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;
};
} // namespace kte::lsp
#endif // KTE_LSP_CLIENT_H

326
lsp/LspManager.cc Normal file
View File

@@ -0,0 +1,326 @@
/*
* LspManager.cc - central coordination of LSP servers and diagnostics
*/
#include "LspManager.h"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <utility>
#include "../Buffer.h"
#include "../Editor.h"
#include "BufferChangeTracker.h"
#include "LspProcessClient.h"
namespace fs = std::filesystem;
namespace kte::lsp {
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;
}
auto client = std::make_unique<LspProcessClient>(cfg.command, cfg.args);
// Determine root as parent of file for now; future: walk rootPatterns
std::string rootPath;
if (!buffer->Filename().empty()) {
fs::path p(buffer->Filename());
rootPath = p.has_parent_path() ? p.parent_path().string() : std::string{};
}
if (!client->initialize(rootPath)) {
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();
}
void
LspManager::onBufferOpened(Buffer *buffer)
{
if (!startServerForBuffer(buffer))
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();
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));
}
client->didChange(uri, version, changes);
}
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))) {
client->completion(getUri(buffer), pos, std::move(callback));
}
}
void
LspManager::requestHover(Buffer *buffer, Position pos, HoverCallback callback)
{
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
client->hover(getUri(buffer), pos, std::move(callback));
}
}
void
LspManager::requestDefinition(Buffer *buffer, Position pos, LocationCallback callback)
{
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
client->definition(getUri(buffer), pos, std::move(callback));
}
}
void
LspManager::handleDiagnostics(const std::string &uri, const std::vector<Diagnostic> &diagnostics)
{
diagnosticStore_.setDiagnostics(uri, diagnostics);
if (display_) {
display_->updateDiagnostics(uri, diagnostics);
display_->updateStatusBar(diagnosticStore_.getErrorCount(uri), diagnosticStore_.getWarningCount(uri));
}
}
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
}
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);
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);
}
} // namespace kte::lsp

85
lsp/LspManager.h Normal file
View File

@@ -0,0 +1,85 @@
/*
* 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"
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();
// 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;
}
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);
};
} // namespace kte::lsp
#endif // KTE_LSP_MANAGER_H

72
lsp/LspProcessClient.cc Normal file
View File

@@ -0,0 +1,72 @@
/*
* LspProcessClient.cc - initial stub implementation
*/
#include "LspProcessClient.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()) {}
LspProcessClient::~LspProcessClient() = default;
bool
LspProcessClient::initialize(const std::string &/*rootPath*/)
{
// Phase 12: no real process spawn yet
running_ = true;
return true;
}
void
LspProcessClient::shutdown()
{
running_ = false;
}
void
LspProcessClient::didOpen(const std::string &/*uri*/, const std::string &/*languageId*/,
int /*version*/, const std::string &/*text*/)
{
// Stub: would send textDocument/didOpen
}
void
LspProcessClient::didChange(const std::string &/*uri*/, int /*version*/,
const std::vector<TextDocumentContentChangeEvent> &/*changes*/)
{
// Stub: would send textDocument/didChange
}
void
LspProcessClient::didClose(const std::string &/*uri*/)
{
// Stub
}
void
LspProcessClient::didSave(const std::string &/*uri*/)
{
// Stub
}
bool
LspProcessClient::isRunning() const
{
return running_;
}
std::string
LspProcessClient::getServerName() const
{
return command_;
}
} // namespace kte::lsp

47
lsp/LspProcessClient.h Normal file
View File

@@ -0,0 +1,47 @@
/*
* 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 "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;
bool isRunning() const override;
std::string getServerName() const override;
private:
std::string command_;
std::vector<std::string> args_;
std::unique_ptr<JsonRpcTransport> transport_;
bool running_ = false;
};
} // 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

29
lsp/LspTypes.h Normal file
View File

@@ -0,0 +1,29 @@
/*
* 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 {
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
};
} // 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