LSP integration steps 1-4, part of 5.
This commit is contained in:
7
.idea/codeStyles/Project.xml
generated
7
.idea/codeStyles/Project.xml
generated
@@ -141,6 +141,13 @@
|
||||
<pair source="c++m" header="" fileNamingConvention="NONE" />
|
||||
</extensions>
|
||||
</files>
|
||||
<codeStyleSettings language="CMake">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="8" />
|
||||
<option name="TAB_SIZE" value="8" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="ObjectiveC">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="8" />
|
||||
|
||||
48
Buffer.cc
48
Buffer.cc
@@ -9,6 +9,7 @@
|
||||
// For reconstructing highlighter state on copies
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "NullHighlighter.h"
|
||||
#include "lsp/BufferChangeTracker.h"
|
||||
|
||||
|
||||
Buffer::Buffer()
|
||||
@@ -19,6 +20,9 @@ Buffer::Buffer()
|
||||
}
|
||||
|
||||
|
||||
Buffer::~Buffer() = default;
|
||||
|
||||
|
||||
Buffer::Buffer(const std::string &path)
|
||||
{
|
||||
std::string err;
|
||||
@@ -394,6 +398,30 @@ Buffer::AsString() const
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
Buffer::FullText() const
|
||||
{
|
||||
std::string out;
|
||||
// Precompute size for fewer reallocations
|
||||
std::size_t total = 0;
|
||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
||||
total += rows_[i].Size();
|
||||
if (i + 1 < rows_.size())
|
||||
total += 1; // for '\n'
|
||||
}
|
||||
out.reserve(total);
|
||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
||||
const char *d = rows_[i].Data();
|
||||
std::size_t n = rows_[i].Size();
|
||||
if (d && n)
|
||||
out.append(d, n);
|
||||
if (i + 1 < rows_.size())
|
||||
out.push_back('\n');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
// --- Raw editing APIs (no undo recording, cursor untouched) ---
|
||||
void
|
||||
Buffer::insert_text(int row, int col, std::string_view text)
|
||||
@@ -432,6 +460,9 @@ Buffer::insert_text(int row, int col, std::string_view text)
|
||||
remain.erase(0, pos + 1);
|
||||
}
|
||||
// Do not set dirty here; UndoSystem will manage state/dirty externally
|
||||
if (change_tracker_) {
|
||||
change_tracker_->recordInsertion(row, col, std::string(text));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -470,6 +501,9 @@ Buffer::delete_text(int row, int col, std::size_t len)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (change_tracker_) {
|
||||
change_tracker_->recordDeletion(row, col, len);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -543,3 +577,17 @@ Buffer::Undo() const
|
||||
{
|
||||
return undo_sys_.get();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Buffer::SetChangeTracker(std::unique_ptr<kte::lsp::BufferChangeTracker> tracker)
|
||||
{
|
||||
change_tracker_ = std::move(tracker);
|
||||
}
|
||||
|
||||
|
||||
kte::lsp::BufferChangeTracker *
|
||||
Buffer::GetChangeTracker()
|
||||
{
|
||||
return change_tracker_.get();
|
||||
}
|
||||
73
Buffer.h
73
Buffer.h
@@ -17,11 +17,20 @@
|
||||
#include "HighlighterEngine.h"
|
||||
#include "Highlight.h"
|
||||
|
||||
// Forward declarations to avoid heavy includes
|
||||
namespace kte {
|
||||
namespace lsp {
|
||||
class BufferChangeTracker;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Buffer {
|
||||
public:
|
||||
Buffer();
|
||||
|
||||
~Buffer();
|
||||
|
||||
Buffer(const Buffer &other);
|
||||
|
||||
Buffer &operator=(const Buffer &other);
|
||||
@@ -374,23 +383,59 @@ public:
|
||||
|
||||
[[nodiscard]] std::string AsString() const;
|
||||
|
||||
// Compose full text of this buffer with newlines between rows
|
||||
[[nodiscard]] std::string FullText() const;
|
||||
|
||||
// Syntax highlighting integration (per-buffer)
|
||||
[[nodiscard]] std::uint64_t Version() const { return version_; }
|
||||
[[nodiscard]] std::uint64_t Version() const
|
||||
{
|
||||
return version_;
|
||||
}
|
||||
|
||||
void SetSyntaxEnabled(bool on) { syntax_enabled_ = on; }
|
||||
[[nodiscard]] bool SyntaxEnabled() const { return syntax_enabled_; }
|
||||
|
||||
void SetFiletype(const std::string &ft) { filetype_ = ft; }
|
||||
[[nodiscard]] const std::string &Filetype() const { return filetype_; }
|
||||
void SetSyntaxEnabled(bool on)
|
||||
{
|
||||
syntax_enabled_ = on;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] bool SyntaxEnabled() const
|
||||
{
|
||||
return syntax_enabled_;
|
||||
}
|
||||
|
||||
|
||||
void SetFiletype(const std::string &ft)
|
||||
{
|
||||
filetype_ = ft;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] const std::string &Filetype() const
|
||||
{
|
||||
return filetype_;
|
||||
}
|
||||
|
||||
|
||||
kte::HighlighterEngine *Highlighter()
|
||||
{
|
||||
return highlighter_.get();
|
||||
}
|
||||
|
||||
|
||||
const kte::HighlighterEngine *Highlighter() const
|
||||
{
|
||||
return highlighter_.get();
|
||||
}
|
||||
|
||||
kte::HighlighterEngine *Highlighter() { return highlighter_.get(); }
|
||||
const kte::HighlighterEngine *Highlighter() const { return highlighter_.get(); }
|
||||
|
||||
void EnsureHighlighter()
|
||||
{
|
||||
if (!highlighter_) highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
||||
if (!highlighter_)
|
||||
highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
||||
}
|
||||
|
||||
|
||||
// Raw, low-level editing APIs used by UndoSystem apply().
|
||||
// These must NOT trigger undo recording. They also do not move the cursor.
|
||||
void insert_text(int row, int col, std::string_view text);
|
||||
@@ -410,6 +455,11 @@ public:
|
||||
|
||||
[[nodiscard]] const UndoSystem *Undo() const;
|
||||
|
||||
// LSP integration: optional change tracker
|
||||
void SetChangeTracker(std::unique_ptr<kte::lsp::BufferChangeTracker> tracker);
|
||||
|
||||
kte::lsp::BufferChangeTracker *GetChangeTracker();
|
||||
|
||||
private:
|
||||
// State mirroring original C struct (without undo_tree)
|
||||
std::size_t curx_ = 0, cury_ = 0; // cursor position in characters
|
||||
@@ -430,9 +480,12 @@ private:
|
||||
|
||||
// Syntax/highlighting state
|
||||
std::uint64_t version_ = 0; // increment on edits
|
||||
bool syntax_enabled_ = true;
|
||||
bool syntax_enabled_ = true;
|
||||
std::string filetype_;
|
||||
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
||||
|
||||
// Optional LSP change tracker (absent by default)
|
||||
std::unique_ptr<kte::lsp::BufferChangeTracker> change_tracker_;
|
||||
};
|
||||
|
||||
#endif // KTE_BUFFER_H
|
||||
#endif // KTE_BUFFER_H
|
||||
391
CMakeLists.txt
391
CMakeLists.txt
@@ -16,36 +16,36 @@ option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
|
||||
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
|
||||
|
||||
if (CMAKE_HOST_UNIX)
|
||||
message(STATUS "Build system is POSIX.")
|
||||
message(STATUS "Build system is POSIX.")
|
||||
else ()
|
||||
message(STATUS "Build system is NOT POSIX.")
|
||||
message(STATUS "Build system is NOT POSIX.")
|
||||
endif ()
|
||||
|
||||
if (MSVC)
|
||||
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
|
||||
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
|
||||
else ()
|
||||
add_compile_options(
|
||||
"-Wall"
|
||||
"-Wextra"
|
||||
"-Werror"
|
||||
"$<$<CONFIG:DEBUG>:-g>"
|
||||
"$<$<CONFIG:RELEASE>:-O2>")
|
||||
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
||||
add_compile_options("-stdlib=libc++")
|
||||
else ()
|
||||
# nothing special for gcc at the moment
|
||||
endif ()
|
||||
add_compile_options(
|
||||
"-Wall"
|
||||
"-Wextra"
|
||||
"-Werror"
|
||||
"$<$<CONFIG:DEBUG>:-g>"
|
||||
"$<$<CONFIG:RELEASE>:-O2>")
|
||||
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
||||
add_compile_options("-stdlib=libc++")
|
||||
else ()
|
||||
# nothing special for gcc at the moment
|
||||
endif ()
|
||||
endif ()
|
||||
add_compile_definitions(KGE_PLATFORM=${CMAKE_HOST_SYSTEM_NAME})
|
||||
add_compile_definitions(KTE_VERSION_STR="v${KTE_VERSION}")
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
add_compile_definitions(KTE_ENABLE_TREESITTER)
|
||||
add_compile_definitions(KTE_ENABLE_TREESITTER)
|
||||
endif ()
|
||||
|
||||
message(STATUS "Build system: ${CMAKE_HOST_SYSTEM_NAME}")
|
||||
|
||||
if (${BUILD_GUI})
|
||||
include(cmake/imgui.cmake)
|
||||
include(cmake/imgui.cmake)
|
||||
endif ()
|
||||
|
||||
# NCurses for terminal mode
|
||||
@@ -55,222 +55,239 @@ find_package(Curses REQUIRED)
|
||||
include_directories(${CURSES_INCLUDE_DIR})
|
||||
|
||||
set(COMMON_SOURCES
|
||||
GapBuffer.cc
|
||||
PieceTable.cc
|
||||
Buffer.cc
|
||||
Editor.cc
|
||||
Command.cc
|
||||
HelpText.cc
|
||||
KKeymap.cc
|
||||
TerminalInputHandler.cc
|
||||
TerminalRenderer.cc
|
||||
TerminalFrontend.cc
|
||||
TestInputHandler.cc
|
||||
TestRenderer.cc
|
||||
TestFrontend.cc
|
||||
UndoNode.cc
|
||||
UndoTree.cc
|
||||
UndoSystem.cc
|
||||
HighlighterEngine.cc
|
||||
CppHighlighter.cc
|
||||
HighlighterRegistry.cc
|
||||
NullHighlighter.cc
|
||||
JsonHighlighter.cc
|
||||
MarkdownHighlighter.cc
|
||||
ShellHighlighter.cc
|
||||
GoHighlighter.cc
|
||||
PythonHighlighter.cc
|
||||
RustHighlighter.cc
|
||||
LispHighlighter.cc
|
||||
GapBuffer.cc
|
||||
PieceTable.cc
|
||||
Buffer.cc
|
||||
Editor.cc
|
||||
Command.cc
|
||||
HelpText.cc
|
||||
KKeymap.cc
|
||||
TerminalInputHandler.cc
|
||||
TerminalRenderer.cc
|
||||
TerminalFrontend.cc
|
||||
TestInputHandler.cc
|
||||
TestRenderer.cc
|
||||
TestFrontend.cc
|
||||
UndoNode.cc
|
||||
UndoTree.cc
|
||||
UndoSystem.cc
|
||||
HighlighterEngine.cc
|
||||
CppHighlighter.cc
|
||||
HighlighterRegistry.cc
|
||||
NullHighlighter.cc
|
||||
JsonHighlighter.cc
|
||||
MarkdownHighlighter.cc
|
||||
ShellHighlighter.cc
|
||||
GoHighlighter.cc
|
||||
PythonHighlighter.cc
|
||||
RustHighlighter.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)
|
||||
list(APPEND COMMON_SOURCES
|
||||
TreeSitterHighlighter.cc)
|
||||
list(APPEND COMMON_SOURCES
|
||||
TreeSitterHighlighter.cc)
|
||||
endif ()
|
||||
|
||||
set(COMMON_HEADERS
|
||||
GapBuffer.h
|
||||
PieceTable.h
|
||||
Buffer.h
|
||||
Editor.h
|
||||
AppendBuffer.h
|
||||
Command.h
|
||||
HelpText.h
|
||||
KKeymap.h
|
||||
InputHandler.h
|
||||
TerminalInputHandler.h
|
||||
Renderer.h
|
||||
TerminalRenderer.h
|
||||
Frontend.h
|
||||
TerminalFrontend.h
|
||||
TestInputHandler.h
|
||||
TestRenderer.h
|
||||
TestFrontend.h
|
||||
UndoNode.h
|
||||
UndoTree.h
|
||||
UndoSystem.h
|
||||
Highlight.h
|
||||
LanguageHighlighter.h
|
||||
HighlighterEngine.h
|
||||
CppHighlighter.h
|
||||
HighlighterRegistry.h
|
||||
NullHighlighter.h
|
||||
JsonHighlighter.h
|
||||
MarkdownHighlighter.h
|
||||
ShellHighlighter.h
|
||||
GoHighlighter.h
|
||||
PythonHighlighter.h
|
||||
RustHighlighter.h
|
||||
LispHighlighter.h
|
||||
GapBuffer.h
|
||||
PieceTable.h
|
||||
Buffer.h
|
||||
Editor.h
|
||||
AppendBuffer.h
|
||||
Command.h
|
||||
HelpText.h
|
||||
KKeymap.h
|
||||
InputHandler.h
|
||||
TerminalInputHandler.h
|
||||
Renderer.h
|
||||
TerminalRenderer.h
|
||||
Frontend.h
|
||||
TerminalFrontend.h
|
||||
TestInputHandler.h
|
||||
TestRenderer.h
|
||||
TestFrontend.h
|
||||
UndoNode.h
|
||||
UndoTree.h
|
||||
UndoSystem.h
|
||||
Highlight.h
|
||||
LanguageHighlighter.h
|
||||
HighlighterEngine.h
|
||||
CppHighlighter.h
|
||||
HighlighterRegistry.h
|
||||
NullHighlighter.h
|
||||
JsonHighlighter.h
|
||||
MarkdownHighlighter.h
|
||||
ShellHighlighter.h
|
||||
GoHighlighter.h
|
||||
PythonHighlighter.h
|
||||
RustHighlighter.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)
|
||||
list(APPEND COMMON_HEADERS
|
||||
TreeSitterHighlighter.h)
|
||||
list(APPEND COMMON_HEADERS
|
||||
TreeSitterHighlighter.h)
|
||||
endif ()
|
||||
|
||||
# kte (terminal-first) executable
|
||||
add_executable(kte
|
||||
main.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
main.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
)
|
||||
|
||||
if (KTE_USE_PIECE_TABLE)
|
||||
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
endif ()
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
|
||||
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(kte ${CURSES_LIBRARIES})
|
||||
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
# Users can provide their own tree-sitter include/lib via cache variables
|
||||
set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
|
||||
set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)")
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(kte ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
# Users can provide their own tree-sitter include/lib via cache variables
|
||||
set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
|
||||
set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)")
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(kte ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
install(TARGETS kte
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
|
||||
# Man pages
|
||||
install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
||||
|
||||
if (BUILD_TESTS)
|
||||
# test_undo executable for testing undo/redo system
|
||||
add_executable(test_undo
|
||||
test_undo.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
)
|
||||
# test_undo executable for testing undo/redo system
|
||||
add_executable(test_undo
|
||||
test_undo.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
)
|
||||
|
||||
if (KTE_USE_PIECE_TABLE)
|
||||
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
endif ()
|
||||
if (KTE_USE_PIECE_TABLE)
|
||||
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
endif ()
|
||||
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
|
||||
|
||||
target_link_libraries(test_undo ${CURSES_LIBRARIES})
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(test_undo ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
endif ()
|
||||
target_link_libraries(test_undo ${CURSES_LIBRARIES})
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(test_undo ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (${BUILD_GUI})
|
||||
target_sources(kte PRIVATE
|
||||
Font.h
|
||||
GUIConfig.cc
|
||||
GUIConfig.h
|
||||
GUIRenderer.cc
|
||||
GUIRenderer.h
|
||||
GUIInputHandler.cc
|
||||
GUIInputHandler.h
|
||||
GUIFrontend.cc
|
||||
GUIFrontend.h)
|
||||
target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
|
||||
target_link_libraries(kte imgui)
|
||||
target_sources(kte PRIVATE
|
||||
Font.h
|
||||
GUIConfig.cc
|
||||
GUIConfig.h
|
||||
GUIRenderer.cc
|
||||
GUIRenderer.h
|
||||
GUIInputHandler.cc
|
||||
GUIInputHandler.h
|
||||
GUIFrontend.cc
|
||||
GUIFrontend.h)
|
||||
target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
|
||||
target_link_libraries(kte imgui)
|
||||
|
||||
# kge (GUI-first) executable
|
||||
add_executable(kge
|
||||
main.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
GUIConfig.cc
|
||||
GUIConfig.h
|
||||
GUIRenderer.cc
|
||||
GUIRenderer.h
|
||||
GUIInputHandler.cc
|
||||
GUIInputHandler.h
|
||||
GUIFrontend.cc
|
||||
GUIFrontend.h)
|
||||
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
||||
# kge (GUI-first) executable
|
||||
add_executable(kge
|
||||
main.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
GUIConfig.cc
|
||||
GUIConfig.h
|
||||
GUIRenderer.cc
|
||||
GUIRenderer.h
|
||||
GUIInputHandler.cc
|
||||
GUIInputHandler.h
|
||||
GUIFrontend.cc
|
||||
GUIFrontend.h)
|
||||
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
||||
|
||||
# On macOS, build kge as a proper .app bundle
|
||||
if (APPLE)
|
||||
# Define the icon file
|
||||
set(MACOSX_BUNDLE_ICON_FILE kge.icns)
|
||||
set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
|
||||
# On macOS, build kge as a proper .app bundle
|
||||
if (APPLE)
|
||||
# Define the icon file
|
||||
set(MACOSX_BUNDLE_ICON_FILE kge.icns)
|
||||
set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
|
||||
|
||||
# Add icon to the target sources and mark it as a resource
|
||||
target_sources(kge PRIVATE ${kge_ICON})
|
||||
set_source_files_properties(${kge_ICON} PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Resources)
|
||||
# Add icon to the target sources and mark it as a resource
|
||||
target_sources(kge PRIVATE ${kge_ICON})
|
||||
set_source_files_properties(${kge_ICON} PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Resources)
|
||||
|
||||
# Configure Info.plist with version and identifiers
|
||||
set(KGE_BUNDLE_ID "dev.wntrmute.kge")
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
|
||||
@ONLY)
|
||||
# Configure Info.plist with version and identifiers
|
||||
set(KGE_BUNDLE_ID "dev.wntrmute.kge")
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
|
||||
@ONLY)
|
||||
|
||||
set_target_properties(kge PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "kge"
|
||||
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
|
||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
|
||||
set_target_properties(kge PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "kge"
|
||||
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
|
||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
|
||||
|
||||
add_dependencies(kge kte)
|
||||
add_custom_command(TARGET kge POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
$<TARGET_FILE:kte>
|
||||
$<TARGET_FILE_DIR:kge>/kte
|
||||
COMMENT "Copying kte binary into kge.app bundle")
|
||||
add_dependencies(kge kte)
|
||||
add_custom_command(TARGET kge POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
$<TARGET_FILE:kte>
|
||||
$<TARGET_FILE_DIR:kge>/kte
|
||||
COMMENT "Copying kte binary into kge.app bundle")
|
||||
|
||||
install(TARGETS kge
|
||||
BUNDLE DESTINATION .
|
||||
)
|
||||
install(TARGETS kge
|
||||
BUNDLE DESTINATION .
|
||||
)
|
||||
|
||||
install(TARGETS kte
|
||||
RUNTIME DESTINATION kge.app/Contents/MacOS
|
||||
)
|
||||
else ()
|
||||
install(TARGETS kge
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
endif ()
|
||||
# Install kge man page only when GUI is built
|
||||
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
||||
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
|
||||
install(TARGETS kte
|
||||
RUNTIME DESTINATION kge.app/Contents/MacOS
|
||||
)
|
||||
else ()
|
||||
install(TARGETS kge
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
endif ()
|
||||
# Install kge man page only when GUI is built
|
||||
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
||||
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
|
||||
endif ()
|
||||
|
||||
249
Command.cc
249
Command.cc
@@ -762,122 +762,140 @@ cmd_unknown_kcommand(CommandContext &ctx)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// --- Syntax highlighting commands ---
|
||||
static void apply_filetype(Buffer &buf, const std::string &ft)
|
||||
static void
|
||||
apply_filetype(Buffer &buf, const std::string &ft)
|
||||
{
|
||||
buf.EnsureHighlighter();
|
||||
auto *eng = buf.Highlighter();
|
||||
if (!eng) return;
|
||||
std::string val = ft;
|
||||
// trim + lower
|
||||
auto trim = [](const std::string &s){
|
||||
std::string r = s;
|
||||
auto notsp = [](int ch){ return !std::isspace(ch); };
|
||||
r.erase(r.begin(), std::find_if(r.begin(), r.end(), notsp));
|
||||
r.erase(std::find_if(r.rbegin(), r.rend(), notsp).base(), r.end());
|
||||
return r;
|
||||
};
|
||||
val = trim(val);
|
||||
for (auto &ch: val) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (val == "off") {
|
||||
eng->SetHighlighter(nullptr);
|
||||
buf.SetFiletype("");
|
||||
buf.SetSyntaxEnabled(false);
|
||||
return;
|
||||
}
|
||||
if (val.empty()) {
|
||||
// Empty means unknown/unspecified -> use NullHighlighter but keep syntax enabled
|
||||
buf.SetFiletype("");
|
||||
buf.SetSyntaxEnabled(true);
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
return;
|
||||
}
|
||||
// Normalize and create via registry
|
||||
std::string norm = kte::HighlighterRegistry::Normalize(val);
|
||||
auto hl = kte::HighlighterRegistry::CreateFor(norm);
|
||||
if (hl) {
|
||||
eng->SetHighlighter(std::move(hl));
|
||||
buf.SetFiletype(norm);
|
||||
buf.SetSyntaxEnabled(true);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
// Unknown -> install NullHighlighter and keep syntax enabled
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
buf.SetFiletype(val); // record what user asked even if unsupported
|
||||
buf.SetSyntaxEnabled(true);
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
buf.EnsureHighlighter();
|
||||
auto *eng = buf.Highlighter();
|
||||
if (!eng)
|
||||
return;
|
||||
std::string val = ft;
|
||||
// trim + lower
|
||||
auto trim = [](const std::string &s) {
|
||||
std::string r = s;
|
||||
auto notsp = [](int ch) {
|
||||
return !std::isspace(ch);
|
||||
};
|
||||
r.erase(r.begin(), std::find_if(r.begin(), r.end(), notsp));
|
||||
r.erase(std::find_if(r.rbegin(), r.rend(), notsp).base(), r.end());
|
||||
return r;
|
||||
};
|
||||
val = trim(val);
|
||||
for (auto &ch: val)
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (val == "off") {
|
||||
eng->SetHighlighter(nullptr);
|
||||
buf.SetFiletype("");
|
||||
buf.SetSyntaxEnabled(false);
|
||||
return;
|
||||
}
|
||||
if (val.empty()) {
|
||||
// Empty means unknown/unspecified -> use NullHighlighter but keep syntax enabled
|
||||
buf.SetFiletype("");
|
||||
buf.SetSyntaxEnabled(true);
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
return;
|
||||
}
|
||||
// Normalize and create via registry
|
||||
std::string norm = kte::HighlighterRegistry::Normalize(val);
|
||||
auto hl = kte::HighlighterRegistry::CreateFor(norm);
|
||||
if (hl) {
|
||||
eng->SetHighlighter(std::move(hl));
|
||||
buf.SetFiletype(norm);
|
||||
buf.SetSyntaxEnabled(true);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
// Unknown -> install NullHighlighter and keep syntax enabled
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
buf.SetFiletype(val); // record what user asked even if unsupported
|
||||
buf.SetSyntaxEnabled(true);
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
|
||||
static bool cmd_syntax(CommandContext &ctx)
|
||||
|
||||
static bool
|
||||
cmd_syntax(CommandContext &ctx)
|
||||
{
|
||||
Buffer *b = ctx.editor.CurrentBuffer();
|
||||
if (!b) {
|
||||
ctx.editor.SetStatus("No buffer");
|
||||
return true;
|
||||
}
|
||||
std::string arg = ctx.arg;
|
||||
// trim
|
||||
auto trim = [](std::string &s){
|
||||
auto notsp = [](int ch){ return !std::isspace(ch); };
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
|
||||
};
|
||||
trim(arg);
|
||||
if (arg == "on") {
|
||||
b->SetSyntaxEnabled(true);
|
||||
// If no highlighter but filetype is cpp by extension, set it
|
||||
if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
|
||||
apply_filetype(*b, b->Filetype().empty() ? std::string("cpp") : b->Filetype());
|
||||
}
|
||||
ctx.editor.SetStatus("syntax: on");
|
||||
} else if (arg == "off") {
|
||||
b->SetSyntaxEnabled(false);
|
||||
ctx.editor.SetStatus("syntax: off");
|
||||
} else if (arg == "reload") {
|
||||
if (auto *eng = b->Highlighter()) eng->InvalidateFrom(0);
|
||||
ctx.editor.SetStatus("syntax: reloaded");
|
||||
} else {
|
||||
ctx.editor.SetStatus("usage: :syntax on|off|reload");
|
||||
}
|
||||
return true;
|
||||
Buffer *b = ctx.editor.CurrentBuffer();
|
||||
if (!b) {
|
||||
ctx.editor.SetStatus("No buffer");
|
||||
return true;
|
||||
}
|
||||
std::string arg = ctx.arg;
|
||||
// trim
|
||||
auto trim = [](std::string &s) {
|
||||
auto notsp = [](int ch) {
|
||||
return !std::isspace(ch);
|
||||
};
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
|
||||
};
|
||||
trim(arg);
|
||||
if (arg == "on") {
|
||||
b->SetSyntaxEnabled(true);
|
||||
// If no highlighter but filetype is cpp by extension, set it
|
||||
if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
|
||||
apply_filetype(*b, b->Filetype().empty() ? std::string("cpp") : b->Filetype());
|
||||
}
|
||||
ctx.editor.SetStatus("syntax: on");
|
||||
} else if (arg == "off") {
|
||||
b->SetSyntaxEnabled(false);
|
||||
ctx.editor.SetStatus("syntax: off");
|
||||
} else if (arg == "reload") {
|
||||
if (auto *eng = b->Highlighter())
|
||||
eng->InvalidateFrom(0);
|
||||
ctx.editor.SetStatus("syntax: reloaded");
|
||||
} else {
|
||||
ctx.editor.SetStatus("usage: :syntax on|off|reload");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cmd_set_option(CommandContext &ctx)
|
||||
|
||||
static bool
|
||||
cmd_set_option(CommandContext &ctx)
|
||||
{
|
||||
Buffer *b = ctx.editor.CurrentBuffer();
|
||||
if (!b) {
|
||||
ctx.editor.SetStatus("No buffer");
|
||||
return true;
|
||||
}
|
||||
// Expect key=value
|
||||
auto eq = ctx.arg.find('=');
|
||||
if (eq == std::string::npos) {
|
||||
ctx.editor.SetStatus("usage: :set key=value");
|
||||
return true;
|
||||
}
|
||||
std::string key = ctx.arg.substr(0, eq);
|
||||
std::string val = ctx.arg.substr(eq + 1);
|
||||
// trim
|
||||
auto trim = [](std::string &s){
|
||||
auto notsp = [](int ch){ return !std::isspace(ch); };
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
|
||||
};
|
||||
trim(key); trim(val);
|
||||
// lower-case value for filetype
|
||||
for (auto &ch: val) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (key == "filetype") {
|
||||
apply_filetype(*b, val);
|
||||
if (b->SyntaxEnabled())
|
||||
ctx.editor.SetStatus(std::string("filetype: ") + (b->Filetype().empty()?"off":b->Filetype()));
|
||||
else
|
||||
ctx.editor.SetStatus("filetype: off");
|
||||
return true;
|
||||
}
|
||||
ctx.editor.SetStatus("unknown option: " + key);
|
||||
return true;
|
||||
Buffer *b = ctx.editor.CurrentBuffer();
|
||||
if (!b) {
|
||||
ctx.editor.SetStatus("No buffer");
|
||||
return true;
|
||||
}
|
||||
// Expect key=value
|
||||
auto eq = ctx.arg.find('=');
|
||||
if (eq == std::string::npos) {
|
||||
ctx.editor.SetStatus("usage: :set key=value");
|
||||
return true;
|
||||
}
|
||||
std::string key = ctx.arg.substr(0, eq);
|
||||
std::string val = ctx.arg.substr(eq + 1);
|
||||
// trim
|
||||
auto trim = [](std::string &s) {
|
||||
auto notsp = [](int ch) {
|
||||
return !std::isspace(ch);
|
||||
};
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
|
||||
};
|
||||
trim(key);
|
||||
trim(val);
|
||||
// lower-case value for filetype
|
||||
for (auto &ch: val)
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (key == "filetype") {
|
||||
apply_filetype(*b, val);
|
||||
if (b->SyntaxEnabled())
|
||||
ctx.editor.SetStatus(
|
||||
std::string("filetype: ") + (b->Filetype().empty() ? "off" : b->Filetype()));
|
||||
else
|
||||
ctx.editor.SetStatus("filetype: off");
|
||||
return true;
|
||||
}
|
||||
ctx.editor.SetStatus("unknown option: " + key);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -907,6 +925,7 @@ cmd_theme_next(CommandContext &ctx)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
cmd_theme_prev(CommandContext &ctx)
|
||||
{
|
||||
@@ -3734,12 +3753,12 @@ InstallDefaultCommands()
|
||||
CommandId::ChangeWorkingDirectory, "change-working-directory", "Change current working directory",
|
||||
cmd_change_working_directory_start
|
||||
});
|
||||
// UI helpers
|
||||
CommandRegistry::Register(
|
||||
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
|
||||
// Syntax highlighting (public commands)
|
||||
CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true});
|
||||
CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true});
|
||||
// UI helpers
|
||||
CommandRegistry::Register(
|
||||
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
|
||||
// Syntax highlighting (public commands)
|
||||
CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true});
|
||||
CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true});
|
||||
}
|
||||
|
||||
|
||||
@@ -3781,4 +3800,4 @@ Execute(Editor &ed, const std::string &name, const std::string &arg, int count)
|
||||
return false;
|
||||
CommandContext ctx{ed, arg, count};
|
||||
return cmd->handler ? cmd->handler(ctx) : false;
|
||||
}
|
||||
}
|
||||
10
Command.h
10
Command.h
@@ -94,10 +94,10 @@ enum class CommandId {
|
||||
// Theme by name
|
||||
ThemeSetByName,
|
||||
// Background mode (GUI)
|
||||
BackgroundSet,
|
||||
// Syntax highlighting
|
||||
Syntax, // ":syntax on|off|reload"
|
||||
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
||||
BackgroundSet,
|
||||
// Syntax highlighting
|
||||
Syntax, // ":syntax on|off|reload"
|
||||
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
#endif // KTE_COMMAND_H
|
||||
#endif // KTE_COMMAND_H
|
||||
@@ -3,168 +3,277 @@
|
||||
#include <cctype>
|
||||
|
||||
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()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"if","else","for","while","do","switch","case","default","break","continue",
|
||||
"return","goto","struct","class","namespace","using","template","typename",
|
||||
"public","private","protected","virtual","override","const","constexpr","auto",
|
||||
"static","inline","operator","new","delete","try","catch","throw","friend",
|
||||
"enum","union","extern","volatile","mutable","noexcept","sizeof","this"
|
||||
};
|
||||
for (auto s: kw) keywords_.insert(s);
|
||||
const char *types[] = {
|
||||
"int","long","short","char","signed","unsigned","float","double","void",
|
||||
"bool","wchar_t","size_t","ptrdiff_t","uint8_t","uint16_t","uint32_t","uint64_t",
|
||||
"int8_t","int16_t","int32_t","int64_t"
|
||||
};
|
||||
for (auto s: types) types_.insert(s);
|
||||
const char *kw[] = {
|
||||
"if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue",
|
||||
"return", "goto", "struct", "class", "namespace", "using", "template", "typename",
|
||||
"public", "private", "protected", "virtual", "override", "const", "constexpr", "auto",
|
||||
"static", "inline", "operator", "new", "delete", "try", "catch", "throw", "friend",
|
||||
"enum", "union", "extern", "volatile", "mutable", "noexcept", "sizeof", "this"
|
||||
};
|
||||
for (auto s: kw)
|
||||
keywords_.insert(s);
|
||||
const char *types[] = {
|
||||
"int", "long", "short", "char", "signed", "unsigned", "float", "double", "void",
|
||||
"bool", "wchar_t", "size_t", "ptrdiff_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t",
|
||||
"int8_t", "int16_t", "int32_t", "int64_t"
|
||||
};
|
||||
for (auto s: types)
|
||||
types_.insert(s);
|
||||
}
|
||||
|
||||
bool CppHighlighter::is_ident_start(char c) { return std::isalpha(static_cast<unsigned char>(c)) || c == '_'; }
|
||||
bool CppHighlighter::is_ident_char(char c) { return std::isalnum(static_cast<unsigned char>(c)) || c == '_'; }
|
||||
|
||||
void CppHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
bool
|
||||
CppHighlighter::is_ident_start(char c)
|
||||
{
|
||||
// Stateless entry simply delegates to stateful with a clean previous state
|
||||
StatefulHighlighter::LineState prev;
|
||||
(void)HighlightLineStateful(buf, row, prev, out);
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
StatefulHighlighter::LineState CppHighlighter::HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const
|
||||
|
||||
bool
|
||||
CppHighlighter::is_ident_char(char c)
|
||||
{
|
||||
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;
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
} // 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
|
||||
@@ -11,24 +11,25 @@
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
|
||||
class CppHighlighter final : public StatefulHighlighter {
|
||||
public:
|
||||
CppHighlighter();
|
||||
~CppHighlighter() override = default;
|
||||
CppHighlighter();
|
||||
|
||||
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;
|
||||
~CppHighlighter() override = default;
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
LineState HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> keywords_;
|
||||
std::unordered_set<std::string> types_;
|
||||
std::unordered_set<std::string> keywords_;
|
||||
std::unordered_set<std::string> types_;
|
||||
|
||||
static bool is_ident_start(char c);
|
||||
static bool is_ident_char(char c);
|
||||
static bool is_ident_start(char c);
|
||||
|
||||
static bool is_ident_char(char c);
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
187
Editor.cc
187
Editor.cc
@@ -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
|
||||
// of creating a new one.
|
||||
if (buffers_.size() == 1) {
|
||||
Buffer &cur = buffers_[curbuf_];
|
||||
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
||||
const bool clean = !cur.Dirty();
|
||||
const auto &rows = cur.Rows();
|
||||
const bool rows_empty = rows.empty();
|
||||
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
|
||||
if (unnamed && clean && (rows_empty || single_empty_line)) {
|
||||
bool ok = cur.OpenFromFile(path, err);
|
||||
if (!ok) return false;
|
||||
// Setup highlighting using registry (extension + shebang)
|
||||
cur.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
const auto &rows = cur.Rows();
|
||||
if (!rows.empty()) first = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
cur.SetFiletype(ft);
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
cur.SetFiletype("");
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (buffers_.size() == 1) {
|
||||
Buffer &cur = buffers_[curbuf_];
|
||||
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
||||
const bool clean = !cur.Dirty();
|
||||
const auto &rows = cur.Rows();
|
||||
const bool rows_empty = rows.empty();
|
||||
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
|
||||
if (unnamed && clean && (rows_empty || single_empty_line)) {
|
||||
bool ok = cur.OpenFromFile(path, err);
|
||||
if (!ok)
|
||||
return false;
|
||||
// Setup highlighting using registry (extension + shebang)
|
||||
cur.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
const auto &rows = cur.Rows();
|
||||
if (!rows.empty())
|
||||
first = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
cur.SetFiletype(ft);
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
cur.SetFiletype("");
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Buffer b;
|
||||
if (!b.OpenFromFile(path, err)) {
|
||||
return false;
|
||||
}
|
||||
// Initialize syntax highlighting by extension + shebang via registry (v2)
|
||||
b.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
{
|
||||
const auto &rows = b.Rows();
|
||||
if (!rows.empty()) first = static_cast<std::string>(rows[0]);
|
||||
}
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
b.SetFiletype(ft);
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
b.SetFiletype("");
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
// Add as a new buffer and switch to it
|
||||
std::size_t idx = AddBuffer(std::move(b));
|
||||
SwitchTo(idx);
|
||||
return true;
|
||||
Buffer b;
|
||||
if (!b.OpenFromFile(path, err)) {
|
||||
return false;
|
||||
}
|
||||
// Initialize syntax highlighting by extension + shebang via registry (v2)
|
||||
b.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
{
|
||||
const auto &rows = b.Rows();
|
||||
if (!rows.empty())
|
||||
first = static_cast<std::string>(rows[0]);
|
||||
}
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
b.SetFiletype(ft);
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
b.SetFiletype("");
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
// Add as a new buffer and switch to it
|
||||
std::size_t idx = AddBuffer(std::move(b));
|
||||
SwitchTo(idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
Editor::SwitchTo(std::size_t index)
|
||||
{
|
||||
if (index >= buffers_.size()) {
|
||||
return false;
|
||||
}
|
||||
curbuf_ = index;
|
||||
// Robustness: ensure a valid highlighter is installed when switching buffers
|
||||
Buffer &b = buffers_[curbuf_];
|
||||
if (b.SyntaxEnabled()) {
|
||||
b.EnsureHighlighter();
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try to set based on existing filetype; fall back to NullHighlighter
|
||||
if (!b.Filetype().empty()) {
|
||||
auto hl = kte::HighlighterRegistry::CreateFor(b.Filetype());
|
||||
if (hl) {
|
||||
eng->SetHighlighter(std::move(hl));
|
||||
} else {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
} else {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
if (index >= buffers_.size()) {
|
||||
return false;
|
||||
}
|
||||
curbuf_ = index;
|
||||
// Robustness: ensure a valid highlighter is installed when switching buffers
|
||||
Buffer &b = buffers_[curbuf_];
|
||||
if (b.SyntaxEnabled()) {
|
||||
b.EnsureHighlighter();
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try to set based on existing filetype; fall back to NullHighlighter
|
||||
if (!b.Filetype().empty()) {
|
||||
auto hl = kte::HighlighterRegistry::CreateFor(b.Filetype());
|
||||
if (hl) {
|
||||
eng->SetHighlighter(std::move(hl));
|
||||
} else {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
} else {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -281,4 +284,4 @@ Editor::Reset()
|
||||
quit_confirm_pending_ = false;
|
||||
buffers_.clear();
|
||||
curbuf_ = 0;
|
||||
}
|
||||
}
|
||||
46
GUIConfig.cc
46
GUIConfig.cc
@@ -102,27 +102,27 @@ GUIConfig::LoadFromFile(const std::string &path)
|
||||
if (v > 0.0f) {
|
||||
font_size = v;
|
||||
}
|
||||
} else if (key == "theme") {
|
||||
theme = val;
|
||||
} else if (key == "background" || key == "bg") {
|
||||
std::string v = val;
|
||||
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
if (v == "light" || v == "dark")
|
||||
background = v;
|
||||
} else if (key == "syntax") {
|
||||
std::string v = val;
|
||||
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
if (v == "1" || v == "on" || v == "true" || v == "yes") {
|
||||
syntax = true;
|
||||
} else if (v == "0" || v == "off" || v == "false" || v == "no") {
|
||||
syntax = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (key == "theme") {
|
||||
theme = val;
|
||||
} else if (key == "background" || key == "bg") {
|
||||
std::string v = val;
|
||||
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
if (v == "light" || v == "dark")
|
||||
background = v;
|
||||
} else if (key == "syntax") {
|
||||
std::string v = val;
|
||||
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||
return (char) std::tolower(c);
|
||||
});
|
||||
if (v == "1" || v == "on" || v == "true" || v == "yes") {
|
||||
syntax = true;
|
||||
} else if (v == "0" || v == "off" || v == "false" || v == "no") {
|
||||
syntax = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
24
GUIConfig.h
24
GUIConfig.h
@@ -12,18 +12,18 @@
|
||||
|
||||
class GUIConfig {
|
||||
public:
|
||||
bool fullscreen = false;
|
||||
int columns = 80;
|
||||
int rows = 42;
|
||||
float font_size = (float) KTE_FONT_SIZE;
|
||||
std::string theme = "nord";
|
||||
// Background mode for themes that support light/dark variants
|
||||
// Values: "dark" (default), "light"
|
||||
std::string background = "dark";
|
||||
bool fullscreen = false;
|
||||
int columns = 80;
|
||||
int rows = 42;
|
||||
float font_size = (float) KTE_FONT_SIZE;
|
||||
std::string theme = "nord";
|
||||
// Background mode for themes that support light/dark variants
|
||||
// Values: "dark" (default), "light"
|
||||
std::string background = "dark";
|
||||
|
||||
// Default syntax highlighting state for GUI (kge): on/off
|
||||
// Accepts: on/off/true/false/yes/no/1/0 in the ini file.
|
||||
bool syntax = true; // default: enabled
|
||||
// Default syntax highlighting state for GUI (kge): on/off
|
||||
// Accepts: on/off/true/false/yes/no/1/0 in the ini file.
|
||||
bool syntax = true; // default: enabled
|
||||
|
||||
// Load from default path: $HOME/.config/kte/kge.ini
|
||||
static GUIConfig Load();
|
||||
@@ -32,4 +32,4 @@ public:
|
||||
bool LoadFromFile(const std::string &path);
|
||||
};
|
||||
|
||||
#endif // KTE_GUI_CONFIG_H
|
||||
#endif // KTE_GUI_CONFIG_H
|
||||
@@ -108,42 +108,44 @@ GUIFrontend::Init(Editor &ed)
|
||||
(void) io;
|
||||
ImGui::StyleColorsDark();
|
||||
|
||||
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
||||
if (cfg.background == "light")
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||
else
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||
kte::ApplyThemeByName(cfg.theme);
|
||||
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
||||
if (cfg.background == "light")
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||
else
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||
kte::ApplyThemeByName(cfg.theme);
|
||||
|
||||
// Apply default syntax highlighting preference from GUI config to the current buffer
|
||||
if (Buffer *b = ed.CurrentBuffer()) {
|
||||
if (cfg.syntax) {
|
||||
b->SetSyntaxEnabled(true);
|
||||
// Ensure a highlighter is available if possible
|
||||
b->EnsureHighlighter();
|
||||
if (auto *eng = b->Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try detect from filename and first line; fall back to cpp or existing filetype
|
||||
std::string first_line;
|
||||
const auto &rows = b->Rows();
|
||||
if (!rows.empty()) first_line = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(b->Filename(), first_line);
|
||||
if (!ft.empty()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
b->SetFiletype(ft);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
b->SetFiletype("");
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b->SetSyntaxEnabled(false);
|
||||
}
|
||||
}
|
||||
// Apply default syntax highlighting preference from GUI config to the current buffer
|
||||
if (Buffer *b = ed.CurrentBuffer()) {
|
||||
if (cfg.syntax) {
|
||||
b->SetSyntaxEnabled(true);
|
||||
// Ensure a highlighter is available if possible
|
||||
b->EnsureHighlighter();
|
||||
if (auto *eng = b->Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try detect from filename and first line; fall back to cpp or existing filetype
|
||||
std::string first_line;
|
||||
const auto &rows = b->Rows();
|
||||
if (!rows.empty())
|
||||
first_line = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(
|
||||
b->Filename(), first_line);
|
||||
if (!ft.empty()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
b->SetFiletype(ft);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
b->SetFiletype("");
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b->SetSyntaxEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
||||
return false;
|
||||
@@ -316,4 +318,4 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
||||
}
|
||||
|
||||
|
||||
// No runtime font reload or system font resolution in this simplified build.
|
||||
// No runtime font reload or system font resolution in this simplified build.
|
||||
140
GUIRenderer.cc
140
GUIRenderer.cc
@@ -139,29 +139,29 @@ GUIRenderer::Draw(Editor &ed)
|
||||
vis_rows = 1;
|
||||
long last_row = first_row + vis_rows - 1;
|
||||
|
||||
if (!forced_scroll) {
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
|
||||
float max_y = ImGui::GetScrollMaxY();
|
||||
if (target < 0.f)
|
||||
target = 0.f;
|
||||
if (max_y >= 0.f && target > max_y)
|
||||
target = max_y;
|
||||
ImGui::SetScrollY(target);
|
||||
// refresh local variables
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
first_row = static_cast<long>(scroll_y / row_h);
|
||||
last_row = first_row + vis_rows - 1;
|
||||
}
|
||||
}
|
||||
// Phase 3: prefetch visible viewport highlights and warm around in background
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
int fr = static_cast<int>(std::max(0L, first_row));
|
||||
int rc = static_cast<int>(std::max(1L, vis_rows));
|
||||
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
||||
}
|
||||
}
|
||||
if (!forced_scroll) {
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
|
||||
float max_y = ImGui::GetScrollMaxY();
|
||||
if (target < 0.f)
|
||||
target = 0.f;
|
||||
if (max_y >= 0.f && target > max_y)
|
||||
target = max_y;
|
||||
ImGui::SetScrollY(target);
|
||||
// refresh local variables
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
first_row = static_cast<long>(scroll_y / row_h);
|
||||
last_row = first_row + vis_rows - 1;
|
||||
}
|
||||
}
|
||||
// Phase 3: prefetch visible viewport highlights and warm around in background
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
int fr = static_cast<int>(std::max(0L, first_row));
|
||||
int rc = static_cast<int>(std::max(1L, vis_rows));
|
||||
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
||||
}
|
||||
}
|
||||
// Handle mouse click before rendering to avoid dependent on drawn items
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||
@@ -329,50 +329,56 @@ GUIRenderer::Draw(Editor &ed)
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||
}
|
||||
}
|
||||
// Emit entire line to an expanded buffer (tabs -> spaces)
|
||||
for (std::size_t src = 0; src < line.size(); ++src) {
|
||||
char c = line[src];
|
||||
if (c == '\t') {
|
||||
std::size_t adv = (tabw - (rx_abs_draw % tabw));
|
||||
expanded.append(adv, ' ');
|
||||
rx_abs_draw += adv;
|
||||
} else {
|
||||
expanded.push_back(c);
|
||||
rx_abs_draw += 1;
|
||||
}
|
||||
}
|
||||
// Emit entire line to an expanded buffer (tabs -> spaces)
|
||||
for (std::size_t src = 0; src < line.size(); ++src) {
|
||||
char c = line[src];
|
||||
if (c == '\t') {
|
||||
std::size_t adv = (tabw - (rx_abs_draw % tabw));
|
||||
expanded.append(adv, ' ');
|
||||
rx_abs_draw += adv;
|
||||
} else {
|
||||
expanded.push_back(c);
|
||||
rx_abs_draw += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw syntax-colored runs (text above background highlights)
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(*buf, static_cast<int>(i), buf->Version());
|
||||
// Helper to convert a src column to expanded rx position
|
||||
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
|
||||
std::size_t rx = 0;
|
||||
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
|
||||
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||
}
|
||||
return rx;
|
||||
};
|
||||
for (const auto &sp: lh.spans) {
|
||||
std::size_t rx_s = src_to_rx_full(static_cast<std::size_t>(std::max(0, sp.col_start)));
|
||||
std::size_t rx_e = src_to_rx_full(static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
|
||||
if (rx_e <= coloffs_now)
|
||||
continue;
|
||||
std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
|
||||
std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
|
||||
if (vx0 >= expanded.size()) continue;
|
||||
vx1 = std::min<std::size_t>(vx1, expanded.size());
|
||||
if (vx1 <= vx0) continue;
|
||||
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
|
||||
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||
ImGui::GetWindowDrawList()->AddText(p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
|
||||
}
|
||||
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
|
||||
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + line_h));
|
||||
} else {
|
||||
// No syntax: draw as one run
|
||||
ImGui::TextUnformatted(expanded.c_str());
|
||||
}
|
||||
// Draw syntax-colored runs (text above background highlights)
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(
|
||||
*buf, static_cast<int>(i), buf->Version());
|
||||
// Helper to convert a src column to expanded rx position
|
||||
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
|
||||
std::size_t rx = 0;
|
||||
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
|
||||
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||
}
|
||||
return rx;
|
||||
};
|
||||
for (const auto &sp: lh.spans) {
|
||||
std::size_t rx_s = src_to_rx_full(
|
||||
static_cast<std::size_t>(std::max(0, sp.col_start)));
|
||||
std::size_t rx_e = src_to_rx_full(
|
||||
static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
|
||||
if (rx_e <= coloffs_now)
|
||||
continue;
|
||||
std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
|
||||
std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
|
||||
if (vx0 >= expanded.size())
|
||||
continue;
|
||||
vx1 = std::min<std::size_t>(vx1, expanded.size());
|
||||
if (vx1 <= vx0)
|
||||
continue;
|
||||
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
|
||||
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
|
||||
}
|
||||
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
|
||||
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + line_h));
|
||||
} else {
|
||||
// No syntax: draw as one run
|
||||
ImGui::TextUnformatted(expanded.c_str());
|
||||
}
|
||||
|
||||
// Draw a visible cursor indicator on the current line
|
||||
if (i == cy) {
|
||||
@@ -761,4 +767,4 @@ GUIRenderer::Draw(Editor &ed)
|
||||
ed.SetFilePickerVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
115
GUITheme.h
115
GUITheme.h
@@ -10,16 +10,17 @@
|
||||
#include <cctype>
|
||||
|
||||
// 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 g = static_cast<float>((rgb >> 8) & 0xFF) / 255.0f;
|
||||
const float b = static_cast<float>(rgb & 0xFF) / 255.0f;
|
||||
return ImVec4(r, g, b, a);
|
||||
const float r = static_cast<float>((rgb >> 16) & 0xFF) / 255.0f;
|
||||
const float g = static_cast<float>((rgb >> 8) & 0xFF) / 255.0f;
|
||||
const float b = static_cast<float>(rgb & 0xFF) / 255.0f;
|
||||
return ImVec4(r, g, b, a);
|
||||
}
|
||||
|
||||
namespace kte {
|
||||
|
||||
namespace kte {
|
||||
// Background mode selection for light/dark palettes
|
||||
enum class BackgroundMode { Light, Dark };
|
||||
|
||||
@@ -28,20 +29,21 @@ static inline BackgroundMode gBackgroundMode = BackgroundMode::Dark;
|
||||
|
||||
// Basic theme identifier (kept minimal; some ids are aliases)
|
||||
enum class ThemeId {
|
||||
EInk = 0,
|
||||
GruvboxDarkMedium = 1,
|
||||
GruvboxLightMedium = 1, // alias to unified gruvbox index
|
||||
Nord = 2,
|
||||
Plan9 = 3,
|
||||
Solarized = 4,
|
||||
EInk = 0,
|
||||
GruvboxDarkMedium = 1,
|
||||
GruvboxLightMedium = 1, // alias to unified gruvbox index
|
||||
Nord = 2,
|
||||
Plan9 = 3,
|
||||
Solarized = 4,
|
||||
};
|
||||
|
||||
// Current theme tracking
|
||||
static inline ThemeId gCurrentTheme = ThemeId::Nord;
|
||||
static inline ThemeId gCurrentTheme = ThemeId::Nord;
|
||||
static inline std::size_t gCurrentThemeIndex = 0;
|
||||
|
||||
// Forward declarations for helpers used below
|
||||
static inline size_t ThemeIndexFromId(ThemeId id);
|
||||
|
||||
static inline ThemeId ThemeIdFromIndex(size_t idx);
|
||||
|
||||
// Helpers to set/query background mode
|
||||
@@ -1098,18 +1100,18 @@ CurrentThemeName()
|
||||
static inline size_t
|
||||
ThemeIndexFromId(ThemeId id)
|
||||
{
|
||||
switch (id) {
|
||||
case ThemeId::EInk:
|
||||
return 0;
|
||||
case ThemeId::GruvboxDarkMedium:
|
||||
return 1;
|
||||
case ThemeId::Nord:
|
||||
return 2;
|
||||
case ThemeId::Plan9:
|
||||
return 3;
|
||||
case ThemeId::Solarized:
|
||||
return 4;
|
||||
}
|
||||
switch (id) {
|
||||
case ThemeId::EInk:
|
||||
return 0;
|
||||
case ThemeId::GruvboxDarkMedium:
|
||||
return 1;
|
||||
case ThemeId::Nord:
|
||||
return 2;
|
||||
case ThemeId::Plan9:
|
||||
return 3;
|
||||
case ThemeId::Solarized:
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1132,29 +1134,46 @@ ThemeIdFromIndex(size_t idx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- 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
|
||||
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
|
||||
// Base text
|
||||
ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
|
||||
switch (k) {
|
||||
case TokenKind::Keyword: return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC);
|
||||
case TokenKind::Type: return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A);
|
||||
case TokenKind::String: return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
|
||||
case TokenKind::Char: return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
|
||||
case TokenKind::Comment: return dark ? RGBA(0x616E88) : RGBA(0x7A869A);
|
||||
case TokenKind::Number: return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900);
|
||||
case TokenKind::Preproc: return dark ? RGBA(0xD08770) : RGBA(0xAF3A03);
|
||||
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;
|
||||
}
|
||||
// Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults
|
||||
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
|
||||
// Base text
|
||||
ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
|
||||
switch (k) {
|
||||
case TokenKind::Keyword:
|
||||
return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC);
|
||||
case TokenKind::Type:
|
||||
return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A);
|
||||
case TokenKind::String:
|
||||
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
|
||||
case TokenKind::Char:
|
||||
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
|
||||
case TokenKind::Comment:
|
||||
return dark ? RGBA(0x616E88) : RGBA(0x7A869A);
|
||||
case TokenKind::Number:
|
||||
return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900);
|
||||
case TokenKind::Preproc:
|
||||
return dark ? RGBA(0xD08770) : RGBA(0xAF3A03);
|
||||
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
|
||||
179
GoHighlighter.cc
179
GoHighlighter.cc
@@ -3,46 +3,155 @@
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
static void push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
|
||||
static bool is_ident_start(char c){ return std::isalpha(static_cast<unsigned char>(c)) || c=='_'; }
|
||||
static bool is_ident_char(char c){ return std::isalnum(static_cast<unsigned char>(c)) || c=='_'; }
|
||||
|
||||
GoHighlighter::GoHighlighter()
|
||||
{
|
||||
const char* kw[] = {"break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"};
|
||||
for (auto s: kw) kws_.insert(s);
|
||||
const char* tp[] = {"bool","byte","complex64","complex128","error","float32","float64","int","int8","int16","int32","int64","rune","string","uint","uint8","uint16","uint32","uint64","uintptr"};
|
||||
for (auto s: tp) types_.insert(s);
|
||||
const char *kw[] = {
|
||||
"break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", "func",
|
||||
"go", "goto", "if", "import", "interface", "map", "package", "range", "return", "select", "struct",
|
||||
"switch", "type", "var"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
const char *tp[] = {
|
||||
"bool", "byte", "complex64", "complex128", "error", "float32", "float64", "int", "int8", "int16",
|
||||
"int32", "int64", "rune", "string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr"
|
||||
};
|
||||
for (auto s: tp)
|
||||
types_.insert(s);
|
||||
}
|
||||
|
||||
void GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
|
||||
void
|
||||
GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
int bol=0; while (bol<n && (s[bol]==' '||s[bol]=='\t')) ++bol;
|
||||
// line comment
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c==' '||c=='\t') { int j=i+1; while (j<n && (s[j]==' '||s[j]=='\t')) ++j; push(out,i,j,TokenKind::Whitespace); i=j; continue; }
|
||||
if (c=='/' && i+1<n && s[i+1]=='/') { push(out,i,n,TokenKind::Comment); break; }
|
||||
if (c=='/' && i+1<n && s[i+1]=='*') {
|
||||
int j=i+2; bool closed=false; while (j+1<=n) { if (j+1<n && s[j]=='*' && s[j+1]=='/') { j+=2; closed=true; break; } ++j; }
|
||||
if (!closed) { push(out,i,n,TokenKind::Comment); break; } else { push(out,i,j,TokenKind::Comment); i=j; continue; }
|
||||
}
|
||||
if (c=='"' || c=='`') {
|
||||
char q=c; int j=i+1; bool esc=false; if (q=='`') { while (j<n && s[j] != '`') ++j; if (j<n) ++j; }
|
||||
else { while (j<n){ char d=s[j++]; if (esc){esc=false; continue;} if (d=='\\'){esc=true; continue;} if (d=='"') break;} }
|
||||
push(out,i,j,TokenKind::String); i=j; continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) { int j=i+1; while (j<n && (std::isalnum(static_cast<unsigned char>(s[j]))||s[j]=='.'||s[j]=='x'||s[j]=='X'||s[j]=='_')) ++j; push(out,i,j,TokenKind::Number); i=j; continue; }
|
||||
if (is_ident_start(c)) { int j=i+1; while (j<n && is_ident_char(s[j])) ++j; std::string id=s.substr(i,j-i); TokenKind k=TokenKind::Identifier; if (kws_.count(id)) k=TokenKind::Keyword; else if (types_.count(id)) k=TokenKind::Type; push(out,i,j,k); i=j; continue; }
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) { TokenKind k=TokenKind::Operator; if (c==';'||c==','||c=='('||c==')'||c=='{'||c=='}'||c=='['||c==']') k=TokenKind::Punctuation; push(out,i,i+1,k); ++i; continue; }
|
||||
push(out,i,i+1,TokenKind::Default); ++i;
|
||||
}
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
int bol = 0;
|
||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||
++bol;
|
||||
// line comment
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||
int j = i + 2;
|
||||
bool closed = false;
|
||||
while (j + 1 <= n) {
|
||||
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||
j += 2;
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (!closed) {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
} else {
|
||||
push(out, i, j, TokenKind::Comment);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (c == '"' || c == '`') {
|
||||
char q = c;
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
if (q == '`') {
|
||||
while (j < n && s[j] != '`')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
} else {
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'x' ||
|
||||
s[j] == 'X' || s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (kws_.count(id))
|
||||
k = TokenKind::Keyword;
|
||||
else if (types_.count(id))
|
||||
k = TokenKind::Type;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
|
||||
']')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -5,14 +5,14 @@
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
|
||||
class GoHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
GoHighlighter();
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
std::unordered_set<std::string> types_;
|
||||
};
|
||||
GoHighlighter();
|
||||
|
||||
} // 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
|
||||
44
Highlight.h
44
Highlight.h
@@ -5,35 +5,33 @@
|
||||
#include <vector>
|
||||
|
||||
namespace kte {
|
||||
|
||||
// Token kinds shared between renderers and highlighters
|
||||
enum class TokenKind {
|
||||
Default,
|
||||
Keyword,
|
||||
Type,
|
||||
String,
|
||||
Char,
|
||||
Comment,
|
||||
Number,
|
||||
Preproc,
|
||||
Constant,
|
||||
Function,
|
||||
Operator,
|
||||
Punctuation,
|
||||
Identifier,
|
||||
Whitespace,
|
||||
Error
|
||||
Default,
|
||||
Keyword,
|
||||
Type,
|
||||
String,
|
||||
Char,
|
||||
Comment,
|
||||
Number,
|
||||
Preproc,
|
||||
Constant,
|
||||
Function,
|
||||
Operator,
|
||||
Punctuation,
|
||||
Identifier,
|
||||
Whitespace,
|
||||
Error
|
||||
};
|
||||
|
||||
struct HighlightSpan {
|
||||
int col_start{0}; // inclusive, 0-based columns in buffer indices
|
||||
int col_end{0}; // exclusive
|
||||
TokenKind kind{TokenKind::Default};
|
||||
int col_start{0}; // inclusive, 0-based columns in buffer indices
|
||||
int col_end{0}; // exclusive
|
||||
TokenKind kind{TokenKind::Default};
|
||||
};
|
||||
|
||||
struct LineHighlight {
|
||||
std::vector<HighlightSpan> spans;
|
||||
std::uint64_t version{0}; // buffer version used for this line
|
||||
std::vector<HighlightSpan> spans;
|
||||
std::uint64_t version{0}; // buffer version used for this line
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -4,178 +4,206 @@
|
||||
#include <thread>
|
||||
|
||||
namespace kte {
|
||||
|
||||
HighlighterEngine::HighlighterEngine() = default;
|
||||
|
||||
|
||||
HighlighterEngine::~HighlighterEngine()
|
||||
{
|
||||
// stop background worker
|
||||
if (worker_running_.load()) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
worker_running_.store(false);
|
||||
has_request_ = true; // wake it up to exit
|
||||
}
|
||||
cv_.notify_one();
|
||||
if (worker_.joinable()) worker_.join();
|
||||
}
|
||||
// stop background worker
|
||||
if (worker_running_.load()) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
worker_running_.store(false);
|
||||
has_request_ = true; // wake it up to exit
|
||||
}
|
||||
cv_.notify_one();
|
||||
if (worker_.joinable())
|
||||
worker_.join();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HighlighterEngine::SetHighlighter(std::unique_ptr<LanguageHighlighter> hl)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
hl_ = std::move(hl);
|
||||
cache_.clear();
|
||||
state_cache_.clear();
|
||||
state_last_contig_.clear();
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
hl_ = std::move(hl);
|
||||
cache_.clear();
|
||||
state_cache_.clear();
|
||||
state_last_contig_.clear();
|
||||
}
|
||||
|
||||
|
||||
const LineHighlight &
|
||||
HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mtx_);
|
||||
auto it = cache_.find(row);
|
||||
if (it != cache_.end() && it->second.version == buf_version) {
|
||||
return it->second;
|
||||
}
|
||||
std::unique_lock<std::mutex> lock(mtx_);
|
||||
auto it = cache_.find(row);
|
||||
if (it != cache_.end() && it->second.version == buf_version) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Prepare destination slot to reuse its capacity and avoid allocations
|
||||
LineHighlight &slot = cache_[row];
|
||||
slot.version = buf_version;
|
||||
slot.spans.clear();
|
||||
// Prepare destination slot to reuse its capacity and avoid allocations
|
||||
LineHighlight &slot = cache_[row];
|
||||
slot.version = buf_version;
|
||||
slot.spans.clear();
|
||||
|
||||
if (!hl_) {
|
||||
return slot;
|
||||
}
|
||||
if (!hl_) {
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Copy shared_ptr-like raw pointer for use outside critical sections
|
||||
LanguageHighlighter *hl_ptr = hl_.get();
|
||||
bool is_stateful = dynamic_cast<StatefulHighlighter *>(hl_ptr) != nullptr;
|
||||
// Copy shared_ptr-like raw pointer for use outside critical sections
|
||||
LanguageHighlighter *hl_ptr = hl_.get();
|
||||
bool is_stateful = dynamic_cast<StatefulHighlighter *>(hl_ptr) != nullptr;
|
||||
|
||||
if (!is_stateful) {
|
||||
// Stateless fast path: we can release the lock while computing to reduce contention
|
||||
auto &out = slot.spans;
|
||||
lock.unlock();
|
||||
hl_ptr->HighlightLine(buf, row, out);
|
||||
return cache_.at(row);
|
||||
}
|
||||
if (!is_stateful) {
|
||||
// Stateless fast path: we can release the lock while computing to reduce contention
|
||||
auto &out = slot.spans;
|
||||
lock.unlock();
|
||||
hl_ptr->HighlightLine(buf, row, out);
|
||||
return cache_.at(row);
|
||||
}
|
||||
|
||||
// Stateful path: we need to walk from a known previous state. Keep lock while consulting caches,
|
||||
// but release during heavy computation.
|
||||
auto *stateful = static_cast<StatefulHighlighter *>(hl_ptr);
|
||||
// Stateful path: we need to walk from a known previous state. Keep lock while consulting caches,
|
||||
// but release during heavy computation.
|
||||
auto *stateful = static_cast<StatefulHighlighter *>(hl_ptr);
|
||||
|
||||
StatefulHighlighter::LineState prev_state;
|
||||
int start_row = -1;
|
||||
if (!state_cache_.empty()) {
|
||||
// linear search over map (unordered), track best candidate
|
||||
int best = -1;
|
||||
for (const auto &kv : state_cache_) {
|
||||
int r = kv.first;
|
||||
if (r <= row - 1 && kv.second.version == buf_version) {
|
||||
if (r > best) best = r;
|
||||
}
|
||||
}
|
||||
if (best >= 0) {
|
||||
start_row = best;
|
||||
prev_state = state_cache_.at(best).state;
|
||||
}
|
||||
}
|
||||
StatefulHighlighter::LineState prev_state;
|
||||
int start_row = -1;
|
||||
if (!state_cache_.empty()) {
|
||||
// linear search over map (unordered), track best candidate
|
||||
int best = -1;
|
||||
for (const auto &kv: state_cache_) {
|
||||
int r = kv.first;
|
||||
if (r <= row - 1 && kv.second.version == buf_version) {
|
||||
if (r > best)
|
||||
best = r;
|
||||
}
|
||||
}
|
||||
if (best >= 0) {
|
||||
start_row = best;
|
||||
prev_state = state_cache_.at(best).state;
|
||||
}
|
||||
}
|
||||
|
||||
// We'll compute states and the target line's spans without holding the lock for most of the work.
|
||||
// Create a local copy of prev_state and iterate rows; we will update caches under lock.
|
||||
lock.unlock();
|
||||
StatefulHighlighter::LineState cur_state = prev_state;
|
||||
for (int r = start_row + 1; r <= row; ++r) {
|
||||
std::vector<HighlightSpan> tmp;
|
||||
std::vector<HighlightSpan> &out = (r == row) ? slot.spans : tmp;
|
||||
auto next_state = stateful->HighlightLineStateful(buf, r, cur_state, out);
|
||||
// Update state cache for r
|
||||
std::lock_guard<std::mutex> gl(mtx_);
|
||||
StateEntry se;
|
||||
se.version = buf_version;
|
||||
se.state = next_state;
|
||||
state_cache_[r] = se;
|
||||
cur_state = next_state;
|
||||
}
|
||||
// We'll compute states and the target line's spans without holding the lock for most of the work.
|
||||
// Create a local copy of prev_state and iterate rows; we will update caches under lock.
|
||||
lock.unlock();
|
||||
StatefulHighlighter::LineState cur_state = prev_state;
|
||||
for (int r = start_row + 1; r <= row; ++r) {
|
||||
std::vector<HighlightSpan> tmp;
|
||||
std::vector<HighlightSpan> &out = (r == row) ? slot.spans : tmp;
|
||||
auto next_state = stateful->HighlightLineStateful(buf, r, cur_state, out);
|
||||
// Update state cache for r
|
||||
std::lock_guard<std::mutex> gl(mtx_);
|
||||
StateEntry se;
|
||||
se.version = buf_version;
|
||||
se.state = next_state;
|
||||
state_cache_[r] = se;
|
||||
cur_state = next_state;
|
||||
}
|
||||
|
||||
// Return reference under lock to ensure slot's address stability in map
|
||||
lock.lock();
|
||||
return cache_.at(row);
|
||||
// Return reference under lock to ensure slot's address stability in map
|
||||
lock.lock();
|
||||
return cache_.at(row);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HighlighterEngine::InvalidateFrom(int row)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
if (cache_.empty()) return;
|
||||
// Simple implementation: erase all rows >= row
|
||||
for (auto it = cache_.begin(); it != cache_.end(); ) {
|
||||
if (it->first >= row) it = cache_.erase(it); else ++it;
|
||||
}
|
||||
if (!state_cache_.empty()) {
|
||||
for (auto it = state_cache_.begin(); it != state_cache_.end(); ) {
|
||||
if (it->first >= row) it = state_cache_.erase(it); else ++it;
|
||||
}
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
if (cache_.empty())
|
||||
return;
|
||||
// Simple implementation: erase all rows >= row
|
||||
for (auto it = cache_.begin(); it != cache_.end();) {
|
||||
if (it->first >= row)
|
||||
it = cache_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
if (!state_cache_.empty()) {
|
||||
for (auto it = state_cache_.begin(); it != state_cache_.end();) {
|
||||
if (it->first >= row)
|
||||
it = state_cache_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HighlighterEngine::ensure_worker_started() const
|
||||
|
||||
void
|
||||
HighlighterEngine::ensure_worker_started() const
|
||||
{
|
||||
if (worker_running_.load()) return;
|
||||
worker_running_.store(true);
|
||||
worker_ = std::thread([this]() { this->worker_loop(); });
|
||||
if (worker_running_.load())
|
||||
return;
|
||||
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_);
|
||||
while (worker_running_.load()) {
|
||||
cv_.wait(lock, [this]() { return has_request_ || !worker_running_.load(); });
|
||||
if (!worker_running_.load()) break;
|
||||
WarmRequest req = pending_;
|
||||
has_request_ = false;
|
||||
// Copy locals then release lock while computing
|
||||
lock.unlock();
|
||||
if (req.buf) {
|
||||
int start = std::max(0, req.start_row);
|
||||
int end = std::max(start, req.end_row);
|
||||
for (int r = start; r <= end; ++r) {
|
||||
// Re-check version staleness quickly by peeking cache version; not strictly necessary
|
||||
// Compute line; GetLine is thread-safe
|
||||
(void)this->GetLine(*req.buf, r, req.version);
|
||||
}
|
||||
}
|
||||
lock.lock();
|
||||
}
|
||||
std::unique_lock<std::mutex> lock(mtx_);
|
||||
while (worker_running_.load()) {
|
||||
cv_.wait(lock, [this]() {
|
||||
return has_request_ || !worker_running_.load();
|
||||
});
|
||||
if (!worker_running_.load())
|
||||
break;
|
||||
WarmRequest req = pending_;
|
||||
has_request_ = false;
|
||||
// Copy locals then release lock while computing
|
||||
lock.unlock();
|
||||
if (req.buf) {
|
||||
int start = std::max(0, req.start_row);
|
||||
int end = std::max(start, req.end_row);
|
||||
for (int r = start; r <= end; ++r) {
|
||||
// Re-check version staleness quickly by peeking cache version; not strictly necessary
|
||||
// Compute line; GetLine is thread-safe
|
||||
(void) this->GetLine(*req.buf, r, req.version);
|
||||
}
|
||||
}
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
|
||||
void HighlighterEngine::PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version, int warm_margin) const
|
||||
|
||||
void
|
||||
HighlighterEngine::PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version,
|
||||
int warm_margin) const
|
||||
{
|
||||
if (row_count <= 0) return;
|
||||
// Synchronously compute visible rows to ensure cache hits during draw
|
||||
int start = std::max(0, first_row);
|
||||
int end = start + row_count - 1;
|
||||
int max_rows = static_cast<int>(buf.Nrows());
|
||||
if (start >= max_rows) return;
|
||||
if (end >= max_rows) end = max_rows - 1;
|
||||
if (row_count <= 0)
|
||||
return;
|
||||
// Synchronously compute visible rows to ensure cache hits during draw
|
||||
int start = std::max(0, first_row);
|
||||
int end = start + row_count - 1;
|
||||
int max_rows = static_cast<int>(buf.Nrows());
|
||||
if (start >= max_rows)
|
||||
return;
|
||||
if (end >= max_rows)
|
||||
end = max_rows - 1;
|
||||
|
||||
for (int r = start; r <= end; ++r) {
|
||||
(void)GetLine(buf, r, buf_version);
|
||||
}
|
||||
for (int r = start; r <= end; ++r) {
|
||||
(void) GetLine(buf, r, buf_version);
|
||||
}
|
||||
|
||||
// Enqueue background warm-around
|
||||
int warm_start = std::max(0, start - warm_margin);
|
||||
int warm_end = std::min(max_rows - 1, end + warm_margin);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
pending_.buf = &buf;
|
||||
pending_.version = buf_version;
|
||||
pending_.start_row = warm_start;
|
||||
pending_.end_row = warm_end;
|
||||
has_request_ = true;
|
||||
}
|
||||
ensure_worker_started();
|
||||
cv_.notify_one();
|
||||
// Enqueue background warm-around
|
||||
int warm_start = std::max(0, start - warm_margin);
|
||||
int warm_end = std::min(max_rows - 1, end + warm_margin);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
pending_.buf = &buf;
|
||||
pending_.version = buf_version;
|
||||
pending_.start_row = warm_start;
|
||||
pending_.end_row = warm_end;
|
||||
has_request_ = true;
|
||||
}
|
||||
ensure_worker_started();
|
||||
cv_.notify_one();
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -16,61 +16,70 @@
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
|
||||
class HighlighterEngine {
|
||||
public:
|
||||
HighlighterEngine();
|
||||
~HighlighterEngine();
|
||||
HighlighterEngine();
|
||||
|
||||
void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl);
|
||||
~HighlighterEngine();
|
||||
|
||||
// Retrieve highlights for a given line and buffer version.
|
||||
// If cache is stale, recompute using the current highlighter.
|
||||
const LineHighlight &GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
|
||||
void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl);
|
||||
|
||||
// Invalidate cached lines from row (inclusive)
|
||||
void InvalidateFrom(int row);
|
||||
// Retrieve highlights for a given line and buffer version.
|
||||
// If cache is stale, recompute using the current highlighter.
|
||||
const LineHighlight &GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
|
||||
|
||||
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.
|
||||
// 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;
|
||||
|
||||
bool HasHighlighter() const
|
||||
{
|
||||
return static_cast<bool>(hl_);
|
||||
}
|
||||
|
||||
|
||||
// Phase 3: viewport-first prefetch and background warming
|
||||
// Compute only the visible range now, and enqueue a background warm-around task.
|
||||
// warm_margin: how many extra lines above/below to warm in the background.
|
||||
void PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version,
|
||||
int warm_margin = 200) const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<LanguageHighlighter> hl_;
|
||||
// Simple cache by row index (mutable to allow caching in const GetLine)
|
||||
mutable std::unordered_map<int, LineHighlight> cache_;
|
||||
// For stateful highlighters, remember per-line state (state after finishing that row)
|
||||
struct StateEntry {
|
||||
std::uint64_t version{0};
|
||||
// Using the interface type; forward-declare via header
|
||||
StatefulHighlighter::LineState state;
|
||||
};
|
||||
mutable std::unordered_map<int, StateEntry> state_cache_;
|
||||
std::unique_ptr<LanguageHighlighter> hl_;
|
||||
// Simple cache by row index (mutable to allow caching in const GetLine)
|
||||
mutable std::unordered_map<int, LineHighlight> cache_;
|
||||
|
||||
// Track best known contiguous state row for a given version to avoid O(n) scans
|
||||
mutable std::unordered_map<std::uint64_t, int> state_last_contig_;
|
||||
// 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;
|
||||
};
|
||||
|
||||
// Thread-safety for caches and background worker state
|
||||
mutable std::mutex mtx_;
|
||||
mutable std::unordered_map<int, StateEntry> state_cache_;
|
||||
|
||||
// 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_{};
|
||||
// Track best known contiguous state row for a given version to avoid O(n) scans
|
||||
mutable std::unordered_map<std::uint64_t, int> state_last_contig_;
|
||||
|
||||
void ensure_worker_started() const;
|
||||
void worker_loop() const;
|
||||
// Thread-safety for caches and background worker state
|
||||
mutable std::mutex mtx_;
|
||||
|
||||
// Background warmer
|
||||
struct WarmRequest {
|
||||
const Buffer *buf{nullptr};
|
||||
std::uint64_t version{0};
|
||||
int start_row{0};
|
||||
int end_row{0}; // inclusive
|
||||
};
|
||||
|
||||
mutable std::condition_variable cv_;
|
||||
mutable std::thread worker_;
|
||||
mutable std::atomic<bool> worker_running_{false};
|
||||
mutable bool has_request_{false};
|
||||
mutable WarmRequest pending_{};
|
||||
|
||||
void ensure_worker_started() const;
|
||||
|
||||
void worker_loop() const;
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -8,19 +8,28 @@
|
||||
|
||||
// Forward declare simple highlighters implemented in this project
|
||||
namespace kte {
|
||||
|
||||
// Registration storage
|
||||
struct RegEntry {
|
||||
std::string ft; // normalized
|
||||
HighlighterRegistry::Factory factory;
|
||||
std::string ft; // normalized
|
||||
HighlighterRegistry::Factory factory;
|
||||
};
|
||||
|
||||
static std::vector<RegEntry> ®istry() {
|
||||
static std::vector<RegEntry> reg;
|
||||
return reg;
|
||||
|
||||
static std::vector<RegEntry> &
|
||||
registry()
|
||||
{
|
||||
static std::vector<RegEntry> reg;
|
||||
return reg;
|
||||
}
|
||||
class JSONHighlighter; class MarkdownHighlighter; class ShellHighlighter;
|
||||
class GoHighlighter; class PythonHighlighter; class RustHighlighter; class LispHighlighter;
|
||||
|
||||
|
||||
class JSONHighlighter;
|
||||
class MarkdownHighlighter;
|
||||
class ShellHighlighter;
|
||||
class GoHighlighter;
|
||||
class PythonHighlighter;
|
||||
class RustHighlighter;
|
||||
class LispHighlighter;
|
||||
}
|
||||
|
||||
// Headers for the above
|
||||
@@ -33,125 +42,182 @@ class GoHighlighter; class PythonHighlighter; class RustHighlighter; class LispH
|
||||
#include "LispHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
static std::string to_lower(std::string_view s) {
|
||||
std::string r(s);
|
||||
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
||||
return r;
|
||||
}
|
||||
|
||||
std::string HighlighterRegistry::Normalize(std::string_view ft)
|
||||
static std::string
|
||||
to_lower(std::string_view s)
|
||||
{
|
||||
std::string f = to_lower(ft);
|
||||
if (f == "c" || f == "c++" || f == "cc" || f == "hpp" || f == "hh" || f == "h" || f == "cxx") return "cpp";
|
||||
if (f == "cpp") return "cpp";
|
||||
if (f == "json") return "json";
|
||||
if (f == "markdown" || f == "md" || f == "mkd" || f == "mdown") return "markdown";
|
||||
if (f == "shell" || f == "sh" || f == "bash" || f == "zsh" || f == "ksh" || f == "fish") return "shell";
|
||||
if (f == "go" || f == "golang") return "go";
|
||||
if (f == "py" || f == "python") return "python";
|
||||
if (f == "rs" || f == "rust") return "rust";
|
||||
if (f == "lisp" || f == "scheme" || f == "scm" || f == "rkt" || f == "el" || f == "clj" || f == "cljc" || f == "cl") return "lisp";
|
||||
return f;
|
||||
std::string r(s);
|
||||
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
std::unique_ptr<LanguageHighlighter> HighlighterRegistry::CreateFor(std::string_view filetype)
|
||||
|
||||
std::string
|
||||
HighlighterRegistry::Normalize(std::string_view ft)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
// Prefer externally registered factories
|
||||
for (const auto &e : registry()) {
|
||||
if (e.ft == ft && e.factory) return e.factory();
|
||||
}
|
||||
if (ft == "cpp") return std::make_unique<CppHighlighter>();
|
||||
if (ft == "json") return std::make_unique<JSONHighlighter>();
|
||||
if (ft == "markdown") return std::make_unique<MarkdownHighlighter>();
|
||||
if (ft == "shell") return std::make_unique<ShellHighlighter>();
|
||||
if (ft == "go") return std::make_unique<GoHighlighter>();
|
||||
if (ft == "python") return std::make_unique<PythonHighlighter>();
|
||||
if (ft == "rust") return std::make_unique<RustHighlighter>();
|
||||
if (ft == "lisp") return std::make_unique<LispHighlighter>();
|
||||
return nullptr;
|
||||
std::string f = to_lower(ft);
|
||||
if (f == "c" || f == "c++" || f == "cc" || f == "hpp" || f == "hh" || f == "h" || f == "cxx")
|
||||
return "cpp";
|
||||
if (f == "cpp")
|
||||
return "cpp";
|
||||
if (f == "json")
|
||||
return "json";
|
||||
if (f == "markdown" || f == "md" || f == "mkd" || f == "mdown")
|
||||
return "markdown";
|
||||
if (f == "shell" || f == "sh" || f == "bash" || f == "zsh" || f == "ksh" || f == "fish")
|
||||
return "shell";
|
||||
if (f == "go" || f == "golang")
|
||||
return "go";
|
||||
if (f == "py" || f == "python")
|
||||
return "python";
|
||||
if (f == "rs" || f == "rust")
|
||||
return "rust";
|
||||
if (f == "lisp" || f == "scheme" || f == "scm" || f == "rkt" || f == "el" || f == "clj" || f == "cljc" || f ==
|
||||
"cl")
|
||||
return "lisp";
|
||||
return f;
|
||||
}
|
||||
|
||||
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 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;
|
||||
std::string ft = Normalize(filetype);
|
||||
// Prefer externally registered factories
|
||||
for (const auto &e: registry()) {
|
||||
if (e.ft == ft && e.factory)
|
||||
return e.factory();
|
||||
}
|
||||
if (ft == "cpp")
|
||||
return std::make_unique<CppHighlighter>();
|
||||
if (ft == "json")
|
||||
return std::make_unique<JSONHighlighter>();
|
||||
if (ft == "markdown")
|
||||
return std::make_unique<MarkdownHighlighter>();
|
||||
if (ft == "shell")
|
||||
return std::make_unique<ShellHighlighter>();
|
||||
if (ft == "go")
|
||||
return std::make_unique<GoHighlighter>();
|
||||
if (ft == "python")
|
||||
return std::make_unique<PythonHighlighter>();
|
||||
if (ft == "rust")
|
||||
return std::make_unique<RustHighlighter>();
|
||||
if (ft == "lisp")
|
||||
return std::make_unique<LispHighlighter>();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
static std::string
|
||||
shebang_to_ft(std::string_view first_line)
|
||||
{
|
||||
if (first_line.size() < 2 || first_line.substr(0, 2) != "#!")
|
||||
return "";
|
||||
std::string low = to_lower(first_line);
|
||||
if (low.find("python") != std::string::npos)
|
||||
return "python";
|
||||
if (low.find("bash") != std::string::npos)
|
||||
return "shell";
|
||||
if (low.find("sh") != std::string::npos)
|
||||
return "shell";
|
||||
if (low.find("zsh") != std::string::npos)
|
||||
return "shell";
|
||||
if (low.find("fish") != std::string::npos)
|
||||
return "shell";
|
||||
if (low.find("scheme") != std::string::npos || low.find("racket") != std::string::npos || low.find("guile") !=
|
||||
std::string::npos)
|
||||
return "lisp";
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
HighlighterRegistry::DetectForPath(std::string_view path, std::string_view first_line)
|
||||
{
|
||||
// Extension
|
||||
std::string p(path);
|
||||
std::error_code ec;
|
||||
std::string ext = std::filesystem::path(p).extension().string();
|
||||
for (auto &ch: ext)
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (!ext.empty()) {
|
||||
if (ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".cxx" || ext == ".h" || ext == ".hpp" || ext
|
||||
== ".hh")
|
||||
return "cpp";
|
||||
if (ext == ".json")
|
||||
return "json";
|
||||
if (ext == ".md" || ext == ".markdown" || ext == ".mkd")
|
||||
return "markdown";
|
||||
if (ext == ".sh" || ext == ".bash" || ext == ".zsh" || ext == ".ksh" || ext == ".fish")
|
||||
return "shell";
|
||||
if (ext == ".go")
|
||||
return "go";
|
||||
if (ext == ".py")
|
||||
return "python";
|
||||
if (ext == ".rs")
|
||||
return "rust";
|
||||
if (ext == ".lisp" || ext == ".scm" || ext == ".rkt" || ext == ".el" || ext == ".clj" || ext == ".cljc"
|
||||
|| ext == ".cl")
|
||||
return "lisp";
|
||||
}
|
||||
// Shebang
|
||||
std::string ft = shebang_to_ft(first_line);
|
||||
return ft;
|
||||
}
|
||||
} // namespace kte
|
||||
|
||||
// Extensibility API implementations
|
||||
namespace kte {
|
||||
|
||||
void HighlighterRegistry::Register(std::string_view filetype, Factory factory, bool override_existing)
|
||||
void
|
||||
HighlighterRegistry::Register(std::string_view filetype, Factory factory, bool override_existing)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
for (auto &e : registry()) {
|
||||
if (e.ft == ft) {
|
||||
if (override_existing) e.factory = std::move(factory);
|
||||
return;
|
||||
}
|
||||
}
|
||||
registry().push_back(RegEntry{ft, std::move(factory)});
|
||||
std::string ft = Normalize(filetype);
|
||||
for (auto &e: registry()) {
|
||||
if (e.ft == ft) {
|
||||
if (override_existing)
|
||||
e.factory = std::move(factory);
|
||||
return;
|
||||
}
|
||||
}
|
||||
registry().push_back(RegEntry{ft, std::move(factory)});
|
||||
}
|
||||
|
||||
bool HighlighterRegistry::IsRegistered(std::string_view filetype)
|
||||
|
||||
bool
|
||||
HighlighterRegistry::IsRegistered(std::string_view filetype)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
for (const auto &e : registry()) if (e.ft == ft) return true;
|
||||
return false;
|
||||
std::string ft = Normalize(filetype);
|
||||
for (const auto &e: registry())
|
||||
if (e.ft == ft)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> HighlighterRegistry::RegisteredFiletypes()
|
||||
|
||||
std::vector<std::string>
|
||||
HighlighterRegistry::RegisteredFiletypes()
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
out.reserve(registry().size());
|
||||
for (const auto &e : registry()) out.push_back(e.ft);
|
||||
return out;
|
||||
std::vector<std::string> out;
|
||||
out.reserve(registry().size());
|
||||
for (const auto &e: registry())
|
||||
out.push_back(e.ft);
|
||||
return out;
|
||||
}
|
||||
|
||||
#ifdef KTE_ENABLE_TREESITTER
|
||||
// Forward declare adapter factory
|
||||
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char* filetype,
|
||||
const void* (*get_lang)());
|
||||
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
|
||||
const void * (*get_lang)());
|
||||
|
||||
void HighlighterRegistry::RegisterTreeSitter(std::string_view filetype,
|
||||
const TSLanguage* (*get_language)())
|
||||
void
|
||||
HighlighterRegistry::RegisterTreeSitter(std::string_view filetype,
|
||||
const TSLanguage * (*get_language)())
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
Register(ft, [ft, get_language]() {
|
||||
return CreateTreeSitterHighlighter(ft.c_str(), reinterpret_cast<const void* (*)()>(get_language));
|
||||
}, /*override_existing=*/true);
|
||||
std::string ft = Normalize(filetype);
|
||||
Register(ft, [ft, get_language]() {
|
||||
return CreateTreeSitterHighlighter(ft.c_str(), reinterpret_cast<const void* (*)()>(get_language));
|
||||
}, /*override_existing=*/true);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -10,40 +10,38 @@
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
class HighlighterRegistry {
|
||||
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").
|
||||
static std::unique_ptr<LanguageHighlighter> CreateFor(std::string_view filetype);
|
||||
// Create a highlighter for normalized filetype id (e.g., "cpp", "json", "markdown", "shell", "go", "python", "rust", "lisp").
|
||||
static std::unique_ptr<LanguageHighlighter> CreateFor(std::string_view filetype);
|
||||
|
||||
// Detect filetype by path extension and shebang (first line).
|
||||
// Returns normalized id or empty string if unknown.
|
||||
static std::string DetectForPath(std::string_view path, std::string_view first_line);
|
||||
// Detect filetype by path extension and shebang (first line).
|
||||
// Returns normalized id or empty string if unknown.
|
||||
static std::string DetectForPath(std::string_view path, std::string_view first_line);
|
||||
|
||||
// Normalize various aliases/extensions to canonical ids.
|
||||
static std::string Normalize(std::string_view ft);
|
||||
// Normalize various aliases/extensions to canonical ids.
|
||||
static std::string Normalize(std::string_view ft);
|
||||
|
||||
// Extensibility: allow external code to register highlighters at runtime.
|
||||
// The filetype key is normalized via Normalize(). If a factory is already registered for the
|
||||
// normalized key and override=false, the existing factory is kept.
|
||||
static void Register(std::string_view filetype, Factory factory, bool override_existing = true);
|
||||
// Extensibility: allow external code to register highlighters at runtime.
|
||||
// The filetype key is normalized via Normalize(). If a factory is already registered for the
|
||||
// normalized key and override=false, the existing factory is kept.
|
||||
static void Register(std::string_view filetype, Factory factory, bool override_existing = true);
|
||||
|
||||
// Returns true if a factory is registered for the (normalized) filetype.
|
||||
static bool IsRegistered(std::string_view filetype);
|
||||
// Returns true if a factory is registered for the (normalized) filetype.
|
||||
static bool IsRegistered(std::string_view filetype);
|
||||
|
||||
// Return a list of currently registered (normalized) filetypes. Primarily for diagnostics/tests.
|
||||
static std::vector<std::string> RegisteredFiletypes();
|
||||
// Return a list of currently registered (normalized) filetypes. Primarily for diagnostics/tests.
|
||||
static std::vector<std::string> RegisteredFiletypes();
|
||||
|
||||
#ifdef KTE_ENABLE_TREESITTER
|
||||
// Forward declaration to avoid hard dependency when disabled.
|
||||
struct TSLanguage;
|
||||
// Convenience: register a Tree-sitter-backed highlighter for a filetype.
|
||||
// The getter should return a non-null language pointer for the grammar.
|
||||
static void RegisterTreeSitter(std::string_view filetype,
|
||||
const TSLanguage* (*get_language)());
|
||||
// Forward declaration to avoid hard dependency when disabled.
|
||||
struct TSLanguage;
|
||||
// Convenience: register a Tree-sitter-backed highlighter for a filetype.
|
||||
// The getter should return a non-null language pointer for the grammar.
|
||||
static void RegisterTreeSitter(std::string_view filetype,
|
||||
const TSLanguage * (*get_language)());
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -3,40 +3,88 @@
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
|
||||
static bool is_digit(char c) { return c >= '0' && c <= '9'; }
|
||||
|
||||
void JSONHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
static bool
|
||||
is_digit(char c)
|
||||
{
|
||||
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;
|
||||
}
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
} // 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
|
||||
@@ -5,10 +5,8 @@
|
||||
#include <vector>
|
||||
|
||||
namespace kte {
|
||||
|
||||
class JSONHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -10,34 +10,42 @@
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
|
||||
class LanguageHighlighter {
|
||||
public:
|
||||
virtual ~LanguageHighlighter() = default;
|
||||
// Produce highlight spans for a given buffer row. Implementations should append to out.
|
||||
virtual void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const = 0;
|
||||
virtual bool Stateful() const { return false; }
|
||||
virtual ~LanguageHighlighter() = default;
|
||||
|
||||
// Produce highlight spans for a given buffer row. Implementations should append to out.
|
||||
virtual void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const = 0;
|
||||
|
||||
|
||||
virtual bool Stateful() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Optional extension for stateful highlighters (e.g., multi-line comments/strings).
|
||||
// Engines may detect and use this via dynamic_cast without breaking stateless impls.
|
||||
class StatefulHighlighter : public LanguageHighlighter {
|
||||
public:
|
||||
struct LineState {
|
||||
bool in_block_comment{false};
|
||||
bool in_raw_string{false};
|
||||
// For raw strings, remember the delimiter between the opening R"delim( and closing )delim"
|
||||
std::string raw_delim;
|
||||
};
|
||||
struct LineState {
|
||||
bool in_block_comment{false};
|
||||
bool in_raw_string{false};
|
||||
// For raw strings, remember the delimiter between the opening R"delim( and closing )delim"
|
||||
std::string raw_delim;
|
||||
};
|
||||
|
||||
// Highlight one line given the previous line state; return the resulting state after this line.
|
||||
// Implementations should append spans for this line to out and compute the next state.
|
||||
virtual LineState HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const = 0;
|
||||
// Highlight one line given the previous line state; return the resulting state after this line.
|
||||
// Implementations should append spans for this line to out and compute the next state.
|
||||
virtual LineState HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const = 0;
|
||||
|
||||
bool Stateful() const override { return true; }
|
||||
|
||||
bool Stateful() const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -3,39 +3,105 @@
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
static void push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
|
||||
|
||||
LispHighlighter::LispHighlighter()
|
||||
{
|
||||
const char* kw[] = {"defun","lambda","let","let*","define","set!","if","cond","begin","quote","quasiquote","unquote","unquote-splicing","loop","do","and","or","not"};
|
||||
for (auto s: kw) kws_.insert(s);
|
||||
const char *kw[] = {
|
||||
"defun", "lambda", "let", "let*", "define", "set!", "if", "cond", "begin", "quote", "quasiquote",
|
||||
"unquote", "unquote-splicing", "loop", "do", "and", "or", "not"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
}
|
||||
|
||||
void LispHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
|
||||
void
|
||||
LispHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
int bol = 0; while (bol<n && (s[bol]==' '||s[bol]=='\t')) ++bol;
|
||||
if (bol < n && s[bol] == ';') { push(out, bol, n, TokenKind::Comment); if (bol>0) push(out,0,bol,TokenKind::Whitespace); return; }
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c==' '||c=='\t') { int j=i+1; while (j<n && (s[j]==' '||s[j]=='\t')) ++j; push(out,i,j,TokenKind::Whitespace); i=j; continue; }
|
||||
if (c==';') { push(out,i,n,TokenKind::Comment); break; }
|
||||
if (c=='"') { int j=i+1; bool esc=false; while (j<n){ char d=s[j++]; if (esc){esc=false; continue;} if (d=='\\'){esc=true; continue;} if (d=='"') break; } push(out,i,j,TokenKind::String); i=j; continue; }
|
||||
if (std::isalpha(static_cast<unsigned char>(c)) || c=='*' || c=='-' || c=='+' || c=='/' || c=='_' ) {
|
||||
int j=i+1; while (j<n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j]=='*' || s[j]=='-' || s[j]=='+' || s[j]=='/' || s[j]=='_' || s[j]=='!')) ++j;
|
||||
std::string id=s.substr(i,j-i);
|
||||
TokenKind k = kws_.count(id) ? TokenKind::Keyword : TokenKind::Identifier;
|
||||
push(out,i,j,k); i=j; continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) { int j=i+1; while (j<n && (std::isdigit(static_cast<unsigned char>(s[j]))||s[j]=='.')) ++j; push(out,i,j,TokenKind::Number); i=j; continue; }
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) { TokenKind k=TokenKind::Punctuation; push(out,i,i+1,k); ++i; continue; }
|
||||
push(out,i,i+1,TokenKind::Default); ++i;
|
||||
}
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
int bol = 0;
|
||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||
++bol;
|
||||
if (bol < n && s[bol] == ';') {
|
||||
push(out, bol, n, TokenKind::Comment);
|
||||
if (bol > 0)
|
||||
push(out, 0, bol, TokenKind::Whitespace);
|
||||
return;
|
||||
}
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == ';') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isalpha(static_cast<unsigned char>(c)) || c == '*' || c == '-' || c == '+' || c == '/' || c ==
|
||||
'_') {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '*' || s[j] == '-' ||
|
||||
s[j] == '+' || s[j] == '/' || s[j] == '_' || s[j] == '!'))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = kws_.count(id) ? TokenKind::Keyword : TokenKind::Identifier;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isdigit(static_cast<unsigned char>(s[j])) || s[j] == '.'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -5,13 +5,13 @@
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
|
||||
class LispHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
LispHighlighter();
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
};
|
||||
LispHighlighter();
|
||||
|
||||
} // namespace kte
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
};
|
||||
} // namespace kte
|
||||
@@ -3,86 +3,130 @@
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
|
||||
static void push_span(std::vector<HighlightSpan> &out, int a, int b, TokenKind k) {
|
||||
if (b > a) out.push_back({a,b,k});
|
||||
}
|
||||
|
||||
void MarkdownHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
static void
|
||||
push_span(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
LineState st; // not used in stateless entry
|
||||
(void)HighlightLineStateful(buf, row, st, out);
|
||||
if (b > a)
|
||||
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;
|
||||
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;
|
||||
LineState st; // not used in stateless entry
|
||||
(void) HighlightLineStateful(buf, row, st, out);
|
||||
}
|
||||
|
||||
} // 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
|
||||
@@ -4,11 +4,11 @@
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
class MarkdownHighlighter final : public StatefulHighlighter {
|
||||
public:
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev, std::vector<HighlightSpan> &out) const override;
|
||||
};
|
||||
void HighlightLine(const Buffer &buf, int row, 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
|
||||
@@ -2,15 +2,16 @@
|
||||
#include "Buffer.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
void NullHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
void
|
||||
NullHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
if (n <= 0) return;
|
||||
out.push_back({0, n, TokenKind::Default});
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
if (n <= 0)
|
||||
return;
|
||||
out.push_back({0, n, TokenKind::Default});
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -4,10 +4,8 @@
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
class NullHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -3,83 +3,170 @@
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
static void push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
|
||||
static bool is_ident_start(char c){ return std::isalpha(static_cast<unsigned char>(c)) || c=='_'; }
|
||||
static bool is_ident_char(char c){ return std::isalnum(static_cast<unsigned char>(c)) || c=='_'; }
|
||||
|
||||
PythonHighlighter::PythonHighlighter()
|
||||
{
|
||||
const char* kw[] = {"and","as","assert","break","class","continue","def","del","elif","else","except","False","finally","for","from","global","if","import","in","is","lambda","None","nonlocal","not","or","pass","raise","return","True","try","while","with","yield"};
|
||||
for (auto s: kw) kws_.insert(s);
|
||||
const char *kw[] = {
|
||||
"and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else", "except", "False",
|
||||
"finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "None", "nonlocal", "not",
|
||||
"or", "pass", "raise", "return", "True", "try", "while", "with", "yield"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
}
|
||||
|
||||
void PythonHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
|
||||
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;
|
||||
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());
|
||||
StatefulHighlighter::LineState state = prev;
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return state;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
|
||||
// Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
|
||||
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
|
||||
auto pos = s.find(state.raw_delim);
|
||||
if (pos == std::string::npos) {
|
||||
push(out, 0, n, TokenKind::String);
|
||||
return state; // still inside
|
||||
} else {
|
||||
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
|
||||
push(out, 0, end, TokenKind::String);
|
||||
// remainder processed normally
|
||||
s = s.substr(end);
|
||||
n = static_cast<int>(s.size());
|
||||
state.in_raw_string = false; state.raw_delim.clear();
|
||||
// Continue parsing remainder as a separate small loop
|
||||
int base = end; // original offset, but we already emitted to 'out' with base=0; following spans should be from 'end'
|
||||
// For simplicity, mark rest as Default
|
||||
if (n>0) push(out, base, base + n, TokenKind::Default);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
// Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
|
||||
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
|
||||
auto pos = s.find(state.raw_delim);
|
||||
if (pos == std::string::npos) {
|
||||
push(out, 0, n, TokenKind::String);
|
||||
return state; // still inside
|
||||
} else {
|
||||
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
|
||||
push(out, 0, end, TokenKind::String);
|
||||
// remainder processed normally
|
||||
s = s.substr(end);
|
||||
n = static_cast<int>(s.size());
|
||||
state.in_raw_string = false;
|
||||
state.raw_delim.clear();
|
||||
// Continue parsing remainder as a separate small loop
|
||||
int base = end;
|
||||
// original offset, but we already emitted to 'out' with base=0; following spans should be from 'end'
|
||||
// For simplicity, mark rest as Default
|
||||
if (n > 0)
|
||||
push(out, base, base + n, TokenKind::Default);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
// Detect comment start '#', ignoring inside strings
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c==' '||c=='\t') { int j=i+1; while (j<n && (s[j]==' '||s[j]=='\t')) ++j; push(out,i,j,TokenKind::Whitespace); i=j; continue; }
|
||||
if (c=='#') { push(out,i,n,TokenKind::Comment); break; }
|
||||
// Strings: triple quotes and single-line
|
||||
if (c=='"' || c=='\'') {
|
||||
char q=c;
|
||||
// triple?
|
||||
if (i+2 < n && s[i+1]==q && s[i+2]==q) {
|
||||
std::string delim(3, q);
|
||||
int j = i+3; // search for closing triple
|
||||
auto pos = s.find(delim, static_cast<std::size_t>(j));
|
||||
if (pos == std::string::npos) {
|
||||
push(out,i,n,TokenKind::String);
|
||||
state.in_raw_string = true; state.raw_delim = delim; return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + 3);
|
||||
push(out,i,end,TokenKind::String); i=end; continue;
|
||||
}
|
||||
} else {
|
||||
int j=i+1; bool esc=false; while (j<n) { char d=s[j++]; if (esc){esc=false; continue;} if (d=='\\'){esc=true; continue;} if (d==q) break; }
|
||||
push(out,i,j,TokenKind::String); i=j; continue;
|
||||
}
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) { int j=i+1; while (j<n && (std::isalnum(static_cast<unsigned char>(s[j]))||s[j]=='.'||s[j]=='_' )) ++j; push(out,i,j,TokenKind::Number); i=j; continue; }
|
||||
if (is_ident_start(c)) { int j=i+1; while (j<n && is_ident_char(s[j])) ++j; std::string id=s.substr(i,j-i); TokenKind k=TokenKind::Identifier; if (kws_.count(id)) k=TokenKind::Keyword; push(out,i,j,k); i=j; continue; }
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) { TokenKind k=TokenKind::Operator; if (c==':'||c==','||c=='('||c==')'||c=='['||c==']') k=TokenKind::Punctuation; push(out,i,i+1,k); ++i; continue; }
|
||||
push(out,i,i+1,TokenKind::Default); ++i;
|
||||
}
|
||||
return state;
|
||||
int i = 0;
|
||||
// Detect comment start '#', ignoring inside strings
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '#') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
// Strings: triple quotes and single-line
|
||||
if (c == '"' || c == '\'') {
|
||||
char q = c;
|
||||
// triple?
|
||||
if (i + 2 < n && s[i + 1] == q && s[i + 2] == q) {
|
||||
std::string delim(3, q);
|
||||
int j = i + 3; // search for closing triple
|
||||
auto pos = s.find(delim, static_cast<std::size_t>(j));
|
||||
if (pos == std::string::npos) {
|
||||
push(out, i, n, TokenKind::String);
|
||||
state.in_raw_string = true;
|
||||
state.raw_delim = delim;
|
||||
return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + 3);
|
||||
push(out, i, end, TokenKind::String);
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == q)
|
||||
break;
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (kws_.count(id))
|
||||
k = TokenKind::Keyword;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ':' || c == ',' || c == '(' || c == ')' || c == '[' || c == ']')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -5,14 +5,16 @@
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
|
||||
class PythonHighlighter final : public StatefulHighlighter {
|
||||
public:
|
||||
PythonHighlighter();
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev, std::vector<HighlightSpan> &out) const override;
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
};
|
||||
PythonHighlighter();
|
||||
|
||||
} // 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
|
||||
@@ -3,37 +3,143 @@
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
static void push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
|
||||
static bool is_ident_start(char c){ return std::isalpha(static_cast<unsigned char>(c)) || c=='_'; }
|
||||
static bool is_ident_char(char c){ return std::isalnum(static_cast<unsigned char>(c)) || c=='_'; }
|
||||
|
||||
RustHighlighter::RustHighlighter()
|
||||
{
|
||||
const char* kw[] = {"as","break","const","continue","crate","else","enum","extern","false","fn","for","if","impl","in","let","loop","match","mod","move","mut","pub","ref","return","self","Self","static","struct","super","trait","true","type","unsafe","use","where","while","dyn","async","await","try"};
|
||||
for (auto s: kw) kws_.insert(s);
|
||||
const char* tp[] = {"u8","u16","u32","u64","u128","usize","i8","i16","i32","i64","i128","isize","f32","f64","bool","char","str"};
|
||||
for (auto s: tp) types_.insert(s);
|
||||
const char *kw[] = {
|
||||
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if",
|
||||
"impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self",
|
||||
"static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while", "dyn", "async",
|
||||
"await", "try"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
const char *tp[] = {
|
||||
"u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32", "f64",
|
||||
"bool", "char", "str"
|
||||
};
|
||||
for (auto s: tp)
|
||||
types_.insert(s);
|
||||
}
|
||||
|
||||
void RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
|
||||
void
|
||||
RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c==' '||c=='\t') { int j=i+1; while (j<n && (s[j]==' '||s[j]=='\t')) ++j; push(out,i,j,TokenKind::Whitespace); i=j; continue; }
|
||||
if (c=='/' && i+1<n && s[i+1]=='/') { push(out,i,n,TokenKind::Comment); break; }
|
||||
if (c=='/' && i+1<n && s[i+1]=='*') { int j=i+2; bool closed=false; while (j+1<=n) { if (j+1<n && s[j]=='*' && s[j+1]=='/') { j+=2; closed=true; break; } ++j; } if (!closed) { push(out,i,n,TokenKind::Comment); break; } else { push(out,i,j,TokenKind::Comment); i=j; continue; } }
|
||||
if (c=='"') { int j=i+1; bool esc=false; while (j<n){ char d=s[j++]; if (esc){esc=false; continue;} if (d=='\\'){esc=true; continue;} if (d=='"') break; } push(out,i,j,TokenKind::String); i=j; continue; }
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) { int j=i+1; while (j<n && (std::isalnum(static_cast<unsigned char>(s[j]))||s[j]=='.'||s[j]=='_' )) ++j; push(out,i,j,TokenKind::Number); i=j; continue; }
|
||||
if (is_ident_start(c)) { int j=i+1; while (j<n && is_ident_char(s[j])) ++j; std::string id=s.substr(i,j-i); TokenKind k=TokenKind::Identifier; if (kws_.count(id)) k=TokenKind::Keyword; else if (types_.count(id)) k=TokenKind::Type; push(out,i,j,k); i=j; continue; }
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) { TokenKind k=TokenKind::Operator; if (c==';'||c==','||c=='('||c==')'||c=='{'||c=='}'||c=='['||c==']') k=TokenKind::Punctuation; push(out,i,i+1,k); ++i; continue; }
|
||||
push(out,i,i+1,TokenKind::Default); ++i;
|
||||
}
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||
int j = i + 2;
|
||||
bool closed = false;
|
||||
while (j + 1 <= n) {
|
||||
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||
j += 2;
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (!closed) {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
} else {
|
||||
push(out, i, j, TokenKind::Comment);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (kws_.count(id))
|
||||
k = TokenKind::Keyword;
|
||||
else if (types_.count(id))
|
||||
k = TokenKind::Type;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
|
||||
']')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -5,14 +5,14 @@
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
|
||||
class RustHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
RustHighlighter();
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
std::unordered_set<std::string> types_;
|
||||
};
|
||||
RustHighlighter();
|
||||
|
||||
} // 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
|
||||
@@ -3,41 +3,103 @@
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
|
||||
static void push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
|
||||
|
||||
void ShellHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
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;
|
||||
}
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
} // 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
|
||||
@@ -4,10 +4,8 @@
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
class ShellHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
@@ -42,18 +42,18 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
std::size_t coloffs = buf->Coloffs();
|
||||
|
||||
const int tabw = 8;
|
||||
// Phase 3: prefetch visible viewport highlights (current terminal area)
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
int fr = static_cast<int>(rowoffs);
|
||||
int rc = std::max(0, content_rows);
|
||||
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
||||
}
|
||||
// Phase 3: prefetch visible viewport highlights (current terminal area)
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
int fr = static_cast<int>(rowoffs);
|
||||
int rc = std::max(0, content_rows);
|
||||
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
||||
}
|
||||
|
||||
for (int r = 0; r < content_rows; ++r) {
|
||||
move(r, 0);
|
||||
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
||||
std::size_t render_col = 0;
|
||||
std::size_t src_i = 0;
|
||||
for (int r = 0; r < content_rows; ++r) {
|
||||
move(r, 0);
|
||||
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
||||
std::size_t render_col = 0;
|
||||
std::size_t src_i = 0;
|
||||
// Compute matches for this line if search highlighting is active
|
||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||
std::vector<std::pair<std::size_t, std::size_t> > ranges; // [start, end)
|
||||
@@ -105,49 +105,53 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
bool hl_on = false;
|
||||
bool cur_on = false;
|
||||
int written = 0;
|
||||
if (li < lines.size()) {
|
||||
std::string line = static_cast<std::string>(lines[li]);
|
||||
src_i = 0;
|
||||
render_col = 0;
|
||||
// Syntax highlighting: fetch per-line spans
|
||||
const kte::LineHighlight *lh_ptr = nullptr;
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
lh_ptr = &buf->Highlighter()->GetLine(*buf, static_cast<int>(li), buf->Version());
|
||||
}
|
||||
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
|
||||
if (!lh_ptr) return kte::TokenKind::Default;
|
||||
for (const auto &sp: lh_ptr->spans) {
|
||||
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(src_index) < sp.col_end)
|
||||
return sp.kind;
|
||||
}
|
||||
return kte::TokenKind::Default;
|
||||
};
|
||||
auto apply_token_attr = [&](kte::TokenKind k) {
|
||||
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
|
||||
attrset(A_NORMAL);
|
||||
switch (k) {
|
||||
case kte::TokenKind::Keyword:
|
||||
case kte::TokenKind::Type:
|
||||
case kte::TokenKind::Constant:
|
||||
case kte::TokenKind::Function:
|
||||
attron(A_BOLD);
|
||||
break;
|
||||
case kte::TokenKind::Comment:
|
||||
attron(A_DIM);
|
||||
break;
|
||||
case kte::TokenKind::String:
|
||||
case kte::TokenKind::Char:
|
||||
case kte::TokenKind::Number:
|
||||
// standout a bit using A_UNDERLINE if available
|
||||
attron(A_UNDERLINE);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
while (written < cols) {
|
||||
char ch = ' ';
|
||||
bool from_src = false;
|
||||
if (li < lines.size()) {
|
||||
std::string line = static_cast<std::string>(lines[li]);
|
||||
src_i = 0;
|
||||
render_col = 0;
|
||||
// Syntax highlighting: fetch per-line spans
|
||||
const kte::LineHighlight *lh_ptr = nullptr;
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
|
||||
HasHighlighter()) {
|
||||
lh_ptr = &buf->Highlighter()->GetLine(
|
||||
*buf, static_cast<int>(li), buf->Version());
|
||||
}
|
||||
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
|
||||
if (!lh_ptr)
|
||||
return kte::TokenKind::Default;
|
||||
for (const auto &sp: lh_ptr->spans) {
|
||||
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(
|
||||
src_index) < sp.col_end)
|
||||
return sp.kind;
|
||||
}
|
||||
return kte::TokenKind::Default;
|
||||
};
|
||||
auto apply_token_attr = [&](kte::TokenKind k) {
|
||||
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
|
||||
attrset(A_NORMAL);
|
||||
switch (k) {
|
||||
case kte::TokenKind::Keyword:
|
||||
case kte::TokenKind::Type:
|
||||
case kte::TokenKind::Constant:
|
||||
case kte::TokenKind::Function:
|
||||
attron(A_BOLD);
|
||||
break;
|
||||
case kte::TokenKind::Comment:
|
||||
attron(A_DIM);
|
||||
break;
|
||||
case kte::TokenKind::String:
|
||||
case kte::TokenKind::Char:
|
||||
case kte::TokenKind::Number:
|
||||
// standout a bit using A_UNDERLINE if available
|
||||
attron(A_UNDERLINE);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
while (written < cols) {
|
||||
char ch = ' ';
|
||||
bool from_src = false;
|
||||
if (src_i < line.size()) {
|
||||
unsigned char c = static_cast<unsigned char>(line[src_i]);
|
||||
if (c == '\t') {
|
||||
@@ -166,45 +170,45 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
next_tab -= to_skip;
|
||||
}
|
||||
// Now render visible spaces
|
||||
while (next_tab > 0 && written < cols) {
|
||||
bool in_hl = search_mode && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && src_i >= cur_mx
|
||||
&& src_i < cur_mend;
|
||||
// Toggle highlight attributes
|
||||
int attr = 0;
|
||||
if (in_hl)
|
||||
attr |= A_STANDOUT;
|
||||
if (in_cur)
|
||||
attr |= A_BOLD;
|
||||
if ((attr & A_STANDOUT) && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!(attr & A_STANDOUT) && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if ((attr & A_BOLD) && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!(attr & A_BOLD) && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
// Apply syntax attribute only if not in search highlight
|
||||
if (!in_hl) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(' ');
|
||||
++written;
|
||||
++render_col;
|
||||
--next_tab;
|
||||
}
|
||||
++src_i;
|
||||
continue;
|
||||
} else {
|
||||
while (next_tab > 0 && written < cols) {
|
||||
bool in_hl = search_mode && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && src_i >= cur_mx
|
||||
&& src_i < cur_mend;
|
||||
// Toggle highlight attributes
|
||||
int attr = 0;
|
||||
if (in_hl)
|
||||
attr |= A_STANDOUT;
|
||||
if (in_cur)
|
||||
attr |= A_BOLD;
|
||||
if ((attr & A_STANDOUT) && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!(attr & A_STANDOUT) && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if ((attr & A_BOLD) && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!(attr & A_BOLD) && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
// Apply syntax attribute only if not in search highlight
|
||||
if (!in_hl) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(' ');
|
||||
++written;
|
||||
++render_col;
|
||||
--next_tab;
|
||||
}
|
||||
++src_i;
|
||||
continue;
|
||||
} else {
|
||||
// normal char
|
||||
if (render_col < coloffs) {
|
||||
++render_col;
|
||||
@@ -219,49 +223,49 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
ch = ' ';
|
||||
from_src = false;
|
||||
}
|
||||
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
|
||||
cur_mend;
|
||||
if (in_hl && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!in_hl && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (in_cur && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!in_cur && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
if (!in_hl && from_src) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(static_cast<unsigned char>(ch));
|
||||
++written;
|
||||
++render_col;
|
||||
if (from_src)
|
||||
++src_i;
|
||||
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
|
||||
cur_mend;
|
||||
if (in_hl && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!in_hl && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (in_cur && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!in_cur && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
if (!in_hl && from_src) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(static_cast<unsigned char>(ch));
|
||||
++written;
|
||||
++render_col;
|
||||
if (from_src)
|
||||
++src_i;
|
||||
if (src_i >= line.size() && written >= cols)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
attrset(A_NORMAL);
|
||||
clrtoeol();
|
||||
}
|
||||
if (hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
attrset(A_NORMAL);
|
||||
clrtoeol();
|
||||
}
|
||||
|
||||
// Place terminal cursor at logical position accounting for tabs and coloffs
|
||||
std::size_t cy = buf->Cury();
|
||||
@@ -464,4 +468,4 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
@@ -6,41 +6,46 @@
|
||||
#include <utility>
|
||||
|
||||
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()
|
||||
{
|
||||
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.
|
||||
// In future, when linking against tree-sitter, initialize parser_, set language_,
|
||||
// and build tree_ from the buffer contents.
|
||||
// Intentionally a stub to avoid pulling the Tree-sitter API and library by default.
|
||||
// In future, when linking against tree-sitter, initialize parser_, set language_,
|
||||
// and build tree_ from the buffer contents.
|
||||
}
|
||||
|
||||
void TreeSitterHighlighter::disposeParser() const
|
||||
|
||||
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);
|
||||
return std::make_unique<TreeSitterHighlighter>(lang, filetype ? std::string(filetype) : std::string());
|
||||
const auto *lang = reinterpret_cast<const TSLanguage *>(get_lang ? get_lang() : nullptr);
|
||||
return std::make_unique < TreeSitterHighlighter > (lang, filetype ? std::string(filetype) : std::string());
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
|
||||
#endif // KTE_ENABLE_TREESITTER
|
||||
#endif // KTE_ENABLE_TREESITTER
|
||||
@@ -17,32 +17,32 @@ struct TSTree;
|
||||
}
|
||||
|
||||
namespace kte {
|
||||
|
||||
// A minimal adapter that uses Tree-sitter to parse the whole buffer and then, for now,
|
||||
// does very limited token classification. This acts as a scaffold for future richer
|
||||
// queries. If no queries are provided, it currently produces no spans (safe fallback).
|
||||
class TreeSitterHighlighter : public LanguageHighlighter {
|
||||
public:
|
||||
explicit TreeSitterHighlighter(const TSLanguage* lang, std::string filetype);
|
||||
~TreeSitterHighlighter() override;
|
||||
explicit TreeSitterHighlighter(const TSLanguage *lang, std::string filetype);
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
~TreeSitterHighlighter() override;
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
const TSLanguage* language_{nullptr};
|
||||
std::string filetype_;
|
||||
// Lazy parser to avoid startup cost; mutable to allow creation in const method
|
||||
mutable TSParser* parser_{nullptr};
|
||||
mutable TSTree* tree_{nullptr};
|
||||
const TSLanguage *language_{nullptr};
|
||||
std::string filetype_;
|
||||
// Lazy parser to avoid startup cost; mutable to allow creation in const method
|
||||
mutable TSParser *parser_{nullptr};
|
||||
mutable TSTree *tree_{nullptr};
|
||||
|
||||
void ensureParsed(const Buffer& buf) const;
|
||||
void disposeParser() const;
|
||||
void ensureParsed(const Buffer &buf) const;
|
||||
|
||||
void disposeParser() const;
|
||||
};
|
||||
|
||||
// Factory used by HighlighterRegistry when registering via RegisterTreeSitter.
|
||||
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char* filetype,
|
||||
const void* (*get_lang)());
|
||||
|
||||
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
|
||||
const void * (*get_lang)());
|
||||
} // namespace kte
|
||||
|
||||
#endif // KTE_ENABLE_TREESITTER
|
||||
#endif // KTE_ENABLE_TREESITTER
|
||||
49
lsp/BufferChangeTracker.cc
Normal file
49
lsp/BufferChangeTracker.cc
Normal 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 1–2 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
44
lsp/BufferChangeTracker.h
Normal 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
37
lsp/Diagnostic.h
Normal 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
30
lsp/DiagnosticDisplay.h
Normal 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
123
lsp/DiagnosticStore.cc
Normal 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
42
lsp/DiagnosticStore.h
Normal 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
19
lsp/JsonRpcTransport.cc
Normal 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
29
lsp/JsonRpcTransport.h
Normal 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
61
lsp/LspClient.h
Normal 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
326
lsp/LspManager.cc
Normal 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
85
lsp/LspManager.h
Normal 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
72
lsp/LspProcessClient.cc
Normal 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 1–2: 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
47
lsp/LspProcessClient.h
Normal 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
47
lsp/LspServerConfig.h
Normal 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
29
lsp/LspTypes.h
Normal 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
|
||||
53
lsp/TerminalDiagnosticDisplay.cc
Normal file
53
lsp/TerminalDiagnosticDisplay.cc
Normal 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
|
||||
35
lsp/TerminalDiagnosticDisplay.h
Normal file
35
lsp/TerminalDiagnosticDisplay.h
Normal 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
|
||||
Reference in New Issue
Block a user