Compare commits
3 Commits
master
...
kyle/check
| Author | SHA1 | Date | |
|---|---|---|---|
| 051106a233 | |||
| 33bbb5b98f | |||
| e089c6e4d1 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
!.idea
|
||||
cmake-build*
|
||||
build
|
||||
build-*
|
||||
/imgui.ini
|
||||
result
|
||||
|
||||
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" />
|
||||
|
||||
52
Buffer.cc
52
Buffer.cc
@@ -7,8 +7,9 @@
|
||||
#include "UndoSystem.h"
|
||||
#include "UndoTree.h"
|
||||
// For reconstructing highlighter state on copies
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "NullHighlighter.h"
|
||||
#include "syntax/HighlighterRegistry.h"
|
||||
#include "syntax/NullHighlighter.h"
|
||||
#include "lsp/BufferChangeTracker.h"
|
||||
|
||||
|
||||
Buffer::Buffer()
|
||||
@@ -19,6 +20,9 @@ Buffer::Buffer()
|
||||
}
|
||||
|
||||
|
||||
Buffer::~Buffer() = default;
|
||||
|
||||
|
||||
Buffer::Buffer(const std::string &path)
|
||||
{
|
||||
std::string err;
|
||||
@@ -394,6 +398,30 @@ Buffer::AsString() const
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
Buffer::FullText() const
|
||||
{
|
||||
std::string out;
|
||||
// Precompute size for fewer reallocations
|
||||
std::size_t total = 0;
|
||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
||||
total += rows_[i].Size();
|
||||
if (i + 1 < rows_.size())
|
||||
total += 1; // for '\n'
|
||||
}
|
||||
out.reserve(total);
|
||||
for (std::size_t i = 0; i < rows_.size(); ++i) {
|
||||
const char *d = rows_[i].Data();
|
||||
std::size_t n = rows_[i].Size();
|
||||
if (d && n)
|
||||
out.append(d, n);
|
||||
if (i + 1 < rows_.size())
|
||||
out.push_back('\n');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
// --- Raw editing APIs (no undo recording, cursor untouched) ---
|
||||
void
|
||||
Buffer::insert_text(int row, int col, std::string_view text)
|
||||
@@ -432,6 +460,9 @@ Buffer::insert_text(int row, int col, std::string_view text)
|
||||
remain.erase(0, pos + 1);
|
||||
}
|
||||
// Do not set dirty here; UndoSystem will manage state/dirty externally
|
||||
if (change_tracker_) {
|
||||
change_tracker_->recordInsertion(row, col, std::string(text));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -470,6 +501,9 @@ Buffer::delete_text(int row, int col, std::size_t len)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (change_tracker_) {
|
||||
change_tracker_->recordDeletion(row, col, len);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -543,3 +577,17 @@ Buffer::Undo() const
|
||||
{
|
||||
return undo_sys_.get();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Buffer::SetChangeTracker(std::unique_ptr<kte::lsp::BufferChangeTracker> tracker)
|
||||
{
|
||||
change_tracker_ = std::move(tracker);
|
||||
}
|
||||
|
||||
|
||||
kte::lsp::BufferChangeTracker *
|
||||
Buffer::GetChangeTracker()
|
||||
{
|
||||
return change_tracker_.get();
|
||||
}
|
||||
75
Buffer.h
75
Buffer.h
@@ -14,14 +14,23 @@
|
||||
#include "UndoSystem.h"
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include "HighlighterEngine.h"
|
||||
#include "syntax/HighlighterEngine.h"
|
||||
#include "Highlight.h"
|
||||
|
||||
// Forward declarations to avoid heavy includes
|
||||
namespace kte {
|
||||
namespace lsp {
|
||||
class BufferChangeTracker;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Buffer {
|
||||
public:
|
||||
Buffer();
|
||||
|
||||
~Buffer();
|
||||
|
||||
Buffer(const Buffer &other);
|
||||
|
||||
Buffer &operator=(const Buffer &other);
|
||||
@@ -374,23 +383,59 @@ public:
|
||||
|
||||
[[nodiscard]] std::string AsString() const;
|
||||
|
||||
// Compose full text of this buffer with newlines between rows
|
||||
[[nodiscard]] std::string FullText() const;
|
||||
|
||||
// Syntax highlighting integration (per-buffer)
|
||||
[[nodiscard]] std::uint64_t Version() const { return version_; }
|
||||
[[nodiscard]] std::uint64_t Version() const
|
||||
{
|
||||
return version_;
|
||||
}
|
||||
|
||||
void SetSyntaxEnabled(bool on) { syntax_enabled_ = on; }
|
||||
[[nodiscard]] bool SyntaxEnabled() const { return syntax_enabled_; }
|
||||
|
||||
void SetFiletype(const std::string &ft) { filetype_ = ft; }
|
||||
[[nodiscard]] const std::string &Filetype() const { return filetype_; }
|
||||
void SetSyntaxEnabled(bool on)
|
||||
{
|
||||
syntax_enabled_ = on;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] bool SyntaxEnabled() const
|
||||
{
|
||||
return syntax_enabled_;
|
||||
}
|
||||
|
||||
|
||||
void SetFiletype(const std::string &ft)
|
||||
{
|
||||
filetype_ = ft;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] const std::string &Filetype() const
|
||||
{
|
||||
return filetype_;
|
||||
}
|
||||
|
||||
|
||||
kte::HighlighterEngine *Highlighter()
|
||||
{
|
||||
return highlighter_.get();
|
||||
}
|
||||
|
||||
|
||||
const kte::HighlighterEngine *Highlighter() const
|
||||
{
|
||||
return highlighter_.get();
|
||||
}
|
||||
|
||||
kte::HighlighterEngine *Highlighter() { return highlighter_.get(); }
|
||||
const kte::HighlighterEngine *Highlighter() const { return highlighter_.get(); }
|
||||
|
||||
void EnsureHighlighter()
|
||||
{
|
||||
if (!highlighter_) highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
||||
if (!highlighter_)
|
||||
highlighter_ = std::make_unique<kte::HighlighterEngine>();
|
||||
}
|
||||
|
||||
|
||||
// Raw, low-level editing APIs used by UndoSystem apply().
|
||||
// These must NOT trigger undo recording. They also do not move the cursor.
|
||||
void insert_text(int row, int col, std::string_view text);
|
||||
@@ -410,6 +455,11 @@ public:
|
||||
|
||||
[[nodiscard]] const UndoSystem *Undo() const;
|
||||
|
||||
// LSP integration: optional change tracker
|
||||
void SetChangeTracker(std::unique_ptr<kte::lsp::BufferChangeTracker> tracker);
|
||||
|
||||
kte::lsp::BufferChangeTracker *GetChangeTracker();
|
||||
|
||||
private:
|
||||
// State mirroring original C struct (without undo_tree)
|
||||
std::size_t curx_ = 0, cury_ = 0; // cursor position in characters
|
||||
@@ -430,9 +480,12 @@ private:
|
||||
|
||||
// Syntax/highlighting state
|
||||
std::uint64_t version_ = 0; // increment on edits
|
||||
bool syntax_enabled_ = true;
|
||||
bool syntax_enabled_ = true;
|
||||
std::string filetype_;
|
||||
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
||||
|
||||
// Optional LSP change tracker (absent by default)
|
||||
std::unique_ptr<kte::lsp::BufferChangeTracker> change_tracker_;
|
||||
};
|
||||
|
||||
#endif // KTE_BUFFER_H
|
||||
#endif // KTE_BUFFER_H
|
||||
525
CMakeLists.txt
525
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
|
||||
@@ -54,223 +54,368 @@ set(CURSES_NEED_WIDE)
|
||||
find_package(Curses REQUIRED)
|
||||
include_directories(${CURSES_INCLUDE_DIR})
|
||||
|
||||
# Detect availability of get_wch (wide-char input) in the curses headers
|
||||
include(CheckSymbolExists)
|
||||
set(CMAKE_REQUIRED_INCLUDES ${CURSES_INCLUDE_DIR})
|
||||
check_symbol_exists(get_wch "ncurses.h" KTE_HAVE_GET_WCH_IN_NCURSES)
|
||||
if (NOT KTE_HAVE_GET_WCH_IN_NCURSES)
|
||||
# Some systems expose curses headers as <curses.h>
|
||||
check_symbol_exists(get_wch "curses.h" KTE_HAVE_GET_WCH_IN_CURSES)
|
||||
endif ()
|
||||
if (KTE_HAVE_GET_WCH_IN_NCURSES OR KTE_HAVE_GET_WCH_IN_CURSES)
|
||||
add_compile_definitions(KTE_HAVE_GET_WCH)
|
||||
endif ()
|
||||
|
||||
set(SYNTAX_SOURCES
|
||||
syntax/HighlighterEngine.cc
|
||||
syntax/CppHighlighter.cc
|
||||
syntax/HighlighterRegistry.cc
|
||||
syntax/NullHighlighter.cc
|
||||
syntax/JsonHighlighter.cc
|
||||
syntax/MarkdownHighlighter.cc
|
||||
syntax/ShellHighlighter.cc
|
||||
syntax/GoHighlighter.cc
|
||||
syntax/PythonHighlighter.cc
|
||||
syntax/RustHighlighter.cc
|
||||
syntax/LispHighlighter.cc
|
||||
syntax/SqlHighlighter.cc
|
||||
syntax/ErlangHighlighter.cc
|
||||
syntax/ForthHighlighter.cc
|
||||
)
|
||||
|
||||
set(COMMON_SOURCES
|
||||
GapBuffer.cc
|
||||
PieceTable.cc
|
||||
Buffer.cc
|
||||
Editor.cc
|
||||
Command.cc
|
||||
HelpText.cc
|
||||
KKeymap.cc
|
||||
TerminalInputHandler.cc
|
||||
TerminalRenderer.cc
|
||||
TerminalFrontend.cc
|
||||
TestInputHandler.cc
|
||||
TestRenderer.cc
|
||||
TestFrontend.cc
|
||||
UndoNode.cc
|
||||
UndoTree.cc
|
||||
UndoSystem.cc
|
||||
HighlighterEngine.cc
|
||||
CppHighlighter.cc
|
||||
HighlighterRegistry.cc
|
||||
NullHighlighter.cc
|
||||
JsonHighlighter.cc
|
||||
MarkdownHighlighter.cc
|
||||
ShellHighlighter.cc
|
||||
GoHighlighter.cc
|
||||
PythonHighlighter.cc
|
||||
RustHighlighter.cc
|
||||
LispHighlighter.cc
|
||||
GapBuffer.cc
|
||||
PieceTable.cc
|
||||
Buffer.cc
|
||||
Editor.cc
|
||||
Command.cc
|
||||
HelpText.cc
|
||||
KKeymap.cc
|
||||
TerminalInputHandler.cc
|
||||
TerminalRenderer.cc
|
||||
TerminalFrontend.cc
|
||||
TestInputHandler.cc
|
||||
TestRenderer.cc
|
||||
TestFrontend.cc
|
||||
UndoNode.cc
|
||||
UndoTree.cc
|
||||
UndoSystem.cc
|
||||
lsp/UtfCodec.cc
|
||||
lsp/BufferChangeTracker.cc
|
||||
lsp/JsonRpcTransport.cc
|
||||
lsp/LspProcessClient.cc
|
||||
lsp/DiagnosticStore.cc
|
||||
lsp/TerminalDiagnosticDisplay.cc
|
||||
lsp/LspManager.cc
|
||||
|
||||
${SYNTAX_SOURCES}
|
||||
)
|
||||
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
list(APPEND COMMON_SOURCES
|
||||
TreeSitterHighlighter.cc)
|
||||
list(APPEND SYNTAX_SOURCES
|
||||
syntax/TreeSitterHighlighter.cc)
|
||||
endif ()
|
||||
|
||||
set(THEME_HEADERS
|
||||
themes/EInk.h
|
||||
themes/Gruvbox.h
|
||||
themes/Nord.h
|
||||
themes/Plan9.h
|
||||
themes/Solarized.h
|
||||
themes/ThemeHelpers.h
|
||||
)
|
||||
|
||||
set(SYNTAX_HEADERS
|
||||
syntax/LanguageHighlighter.h
|
||||
syntax/HighlighterEngine.h
|
||||
syntax/CppHighlighter.h
|
||||
syntax/HighlighterRegistry.h
|
||||
syntax/NullHighlighter.h
|
||||
syntax/JsonHighlighter.h
|
||||
syntax/MarkdownHighlighter.h
|
||||
syntax/ShellHighlighter.h
|
||||
syntax/GoHighlighter.h
|
||||
syntax/PythonHighlighter.h
|
||||
syntax/RustHighlighter.h
|
||||
syntax/LispHighlighter.h
|
||||
)
|
||||
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
list(APPEND SYNTAX_HEADERS
|
||||
syntax/TreeSitterHighlighter.h)
|
||||
endif ()
|
||||
|
||||
set(COMMON_HEADERS
|
||||
GapBuffer.h
|
||||
PieceTable.h
|
||||
Buffer.h
|
||||
Editor.h
|
||||
AppendBuffer.h
|
||||
Command.h
|
||||
HelpText.h
|
||||
KKeymap.h
|
||||
InputHandler.h
|
||||
TerminalInputHandler.h
|
||||
Renderer.h
|
||||
TerminalRenderer.h
|
||||
Frontend.h
|
||||
TerminalFrontend.h
|
||||
TestInputHandler.h
|
||||
TestRenderer.h
|
||||
TestFrontend.h
|
||||
UndoNode.h
|
||||
UndoTree.h
|
||||
UndoSystem.h
|
||||
Highlight.h
|
||||
LanguageHighlighter.h
|
||||
HighlighterEngine.h
|
||||
CppHighlighter.h
|
||||
HighlighterRegistry.h
|
||||
NullHighlighter.h
|
||||
JsonHighlighter.h
|
||||
MarkdownHighlighter.h
|
||||
ShellHighlighter.h
|
||||
GoHighlighter.h
|
||||
PythonHighlighter.h
|
||||
RustHighlighter.h
|
||||
LispHighlighter.h
|
||||
)
|
||||
GapBuffer.h
|
||||
PieceTable.h
|
||||
Buffer.h
|
||||
Editor.h
|
||||
AppendBuffer.h
|
||||
Command.h
|
||||
HelpText.h
|
||||
KKeymap.h
|
||||
InputHandler.h
|
||||
TerminalInputHandler.h
|
||||
Renderer.h
|
||||
TerminalRenderer.h
|
||||
Frontend.h
|
||||
TerminalFrontend.h
|
||||
TestInputHandler.h
|
||||
TestRenderer.h
|
||||
TestFrontend.h
|
||||
UndoNode.h
|
||||
UndoTree.h
|
||||
UndoSystem.h
|
||||
Highlight.h
|
||||
lsp/UtfCodec.h
|
||||
lsp/LspTypes.h
|
||||
lsp/BufferChangeTracker.h
|
||||
lsp/JsonRpcTransport.h
|
||||
lsp/LspClient.h
|
||||
lsp/LspProcessClient.h
|
||||
lsp/Diagnostic.h
|
||||
lsp/DiagnosticStore.h
|
||||
lsp/DiagnosticDisplay.h
|
||||
lsp/TerminalDiagnosticDisplay.h
|
||||
lsp/LspManager.h
|
||||
lsp/LspServerConfig.h
|
||||
ext/json.h
|
||||
ext/json_fwd.h
|
||||
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
list(APPEND COMMON_HEADERS
|
||||
TreeSitterHighlighter.h)
|
||||
endif ()
|
||||
${THEME_HEADERS}
|
||||
${SYNTAX_HEADERS}
|
||||
)
|
||||
|
||||
# kte (terminal-first) executable
|
||||
add_executable(kte
|
||||
main.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
main.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
)
|
||||
|
||||
if (KTE_USE_PIECE_TABLE)
|
||||
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
endif ()
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
|
||||
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(kte ${CURSES_LIBRARIES})
|
||||
# Ensure vendored headers (e.g., ext/json.h) are on the include path
|
||||
target_include_directories(kte PRIVATE ${CMAKE_SOURCE_DIR}/ext)
|
||||
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
# Users can provide their own tree-sitter include/lib via cache variables
|
||||
set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
|
||||
set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)")
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(kte ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
# Users can provide their own tree-sitter include/lib via cache variables
|
||||
set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
|
||||
set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)")
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(kte ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
install(TARGETS kte
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
|
||||
# Man pages
|
||||
install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
||||
|
||||
if (BUILD_TESTS)
|
||||
# test_undo executable for testing undo/redo system
|
||||
add_executable(test_undo
|
||||
test_undo.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
)
|
||||
# test_undo executable for testing undo/redo system
|
||||
add_executable(test_undo
|
||||
test_undo.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
)
|
||||
|
||||
if (KTE_USE_PIECE_TABLE)
|
||||
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
endif ()
|
||||
if (KTE_USE_PIECE_TABLE)
|
||||
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
endif ()
|
||||
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
|
||||
|
||||
target_link_libraries(test_undo ${CURSES_LIBRARIES})
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(test_undo ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
endif ()
|
||||
target_link_libraries(test_undo ${CURSES_LIBRARIES})
|
||||
# Ensure vendored headers (e.g., ext/json.h) are on the include path for tests as well
|
||||
target_include_directories(test_undo PRIVATE ${CMAKE_SOURCE_DIR}/ext)
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(test_undo ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# test_utfcodec executable for UTF conversion helpers
|
||||
add_executable(test_utfcodec
|
||||
test_utfcodec.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
)
|
||||
|
||||
if (KTE_USE_PIECE_TABLE)
|
||||
target_compile_definitions(test_utfcodec PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
endif ()
|
||||
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(test_utfcodec PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
|
||||
|
||||
target_link_libraries(test_utfcodec ${CURSES_LIBRARIES})
|
||||
# Ensure vendored headers (e.g., ext/json.h) are on the include path for tests as well
|
||||
target_include_directories(test_utfcodec PRIVATE ${CMAKE_SOURCE_DIR}/ext)
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(test_utfcodec PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(test_utfcodec ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# test_transport executable for JSON-RPC framing
|
||||
add_executable(test_transport
|
||||
test_transport.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
)
|
||||
|
||||
if (KTE_USE_PIECE_TABLE)
|
||||
target_compile_definitions(test_transport PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
endif ()
|
||||
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(test_transport PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
|
||||
|
||||
target_link_libraries(test_transport ${CURSES_LIBRARIES})
|
||||
# Ensure vendored headers (e.g., ext/json.h) are on the include path for tests as well
|
||||
target_include_directories(test_transport PRIVATE ${CMAKE_SOURCE_DIR}/ext)
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(test_transport PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(test_transport ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# test_lsp_decode executable for dispatcher decoding
|
||||
add_executable(test_lsp_decode
|
||||
test_lsp_decode.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
)
|
||||
|
||||
if (KTE_USE_PIECE_TABLE)
|
||||
target_compile_definitions(test_lsp_decode PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||
endif ()
|
||||
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(test_lsp_decode PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
|
||||
|
||||
target_link_libraries(test_lsp_decode ${CURSES_LIBRARIES})
|
||||
# Ensure vendored headers (e.g., ext/json.h) are on the include path for tests as well
|
||||
target_include_directories(test_lsp_decode PRIVATE ${CMAKE_SOURCE_DIR}/ext)
|
||||
if (KTE_ENABLE_TREESITTER)
|
||||
if (TREESITTER_INCLUDE_DIR)
|
||||
target_include_directories(test_lsp_decode PRIVATE ${TREESITTER_INCLUDE_DIR})
|
||||
endif ()
|
||||
if (TREESITTER_LIBRARY)
|
||||
target_link_libraries(test_lsp_decode ${TREESITTER_LIBRARY})
|
||||
endif ()
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (${BUILD_GUI})
|
||||
target_sources(kte PRIVATE
|
||||
Font.h
|
||||
GUIConfig.cc
|
||||
GUIConfig.h
|
||||
GUIRenderer.cc
|
||||
GUIRenderer.h
|
||||
GUIInputHandler.cc
|
||||
GUIInputHandler.h
|
||||
GUIFrontend.cc
|
||||
GUIFrontend.h)
|
||||
target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
|
||||
target_link_libraries(kte imgui)
|
||||
target_sources(kte PRIVATE
|
||||
Font.h
|
||||
GUIConfig.cc
|
||||
GUIConfig.h
|
||||
GUIRenderer.cc
|
||||
GUIRenderer.h
|
||||
GUIInputHandler.cc
|
||||
GUIInputHandler.h
|
||||
GUIFrontend.cc
|
||||
GUIFrontend.h)
|
||||
target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
|
||||
target_link_libraries(kte imgui)
|
||||
|
||||
# kge (GUI-first) executable
|
||||
add_executable(kge
|
||||
main.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
GUIConfig.cc
|
||||
GUIConfig.h
|
||||
GUIRenderer.cc
|
||||
GUIRenderer.h
|
||||
GUIInputHandler.cc
|
||||
GUIInputHandler.h
|
||||
GUIFrontend.cc
|
||||
GUIFrontend.h)
|
||||
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
||||
# kge (GUI-first) executable
|
||||
add_executable(kge
|
||||
main.cc
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_HEADERS}
|
||||
GUIConfig.cc
|
||||
GUIConfig.h
|
||||
GUIRenderer.cc
|
||||
GUIRenderer.h
|
||||
GUIInputHandler.cc
|
||||
GUIInputHandler.h
|
||||
GUIFrontend.cc
|
||||
GUIFrontend.h)
|
||||
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
|
||||
if (KTE_UNDO_DEBUG)
|
||||
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
||||
endif ()
|
||||
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
||||
target_include_directories(kge PRIVATE ${CMAKE_SOURCE_DIR}/ext)
|
||||
|
||||
# On macOS, build kge as a proper .app bundle
|
||||
if (APPLE)
|
||||
# Define the icon file
|
||||
set(MACOSX_BUNDLE_ICON_FILE kge.icns)
|
||||
set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
|
||||
# On macOS, build kge as a proper .app bundle
|
||||
if (APPLE)
|
||||
# Define the icon file
|
||||
set(MACOSX_BUNDLE_ICON_FILE kge.icns)
|
||||
set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
|
||||
|
||||
# Add icon to the target sources and mark it as a resource
|
||||
target_sources(kge PRIVATE ${kge_ICON})
|
||||
set_source_files_properties(${kge_ICON} PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Resources)
|
||||
# Add icon to the target sources and mark it as a resource
|
||||
target_sources(kge PRIVATE ${kge_ICON})
|
||||
set_source_files_properties(${kge_ICON} PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Resources)
|
||||
|
||||
# Configure Info.plist with version and identifiers
|
||||
set(KGE_BUNDLE_ID "dev.wntrmute.kge")
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
|
||||
@ONLY)
|
||||
# Configure Info.plist with version and identifiers
|
||||
set(KGE_BUNDLE_ID "dev.wntrmute.kge")
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
|
||||
@ONLY)
|
||||
|
||||
set_target_properties(kge PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "kge"
|
||||
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
|
||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
|
||||
set_target_properties(kge PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "kge"
|
||||
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
|
||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
|
||||
|
||||
add_dependencies(kge kte)
|
||||
add_custom_command(TARGET kge POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
$<TARGET_FILE:kte>
|
||||
$<TARGET_FILE_DIR:kge>/kte
|
||||
COMMENT "Copying kte binary into kge.app bundle")
|
||||
add_dependencies(kge kte)
|
||||
add_custom_command(TARGET kge POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
$<TARGET_FILE:kte>
|
||||
$<TARGET_FILE_DIR:kge>/kte
|
||||
COMMENT "Copying kte binary into kge.app bundle")
|
||||
|
||||
install(TARGETS kge
|
||||
BUNDLE DESTINATION .
|
||||
)
|
||||
install(TARGETS kge
|
||||
BUNDLE DESTINATION .
|
||||
)
|
||||
|
||||
install(TARGETS kte
|
||||
RUNTIME DESTINATION kge.app/Contents/MacOS
|
||||
)
|
||||
else ()
|
||||
install(TARGETS kge
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
endif ()
|
||||
# Install kge man page only when GUI is built
|
||||
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
||||
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
|
||||
install(TARGETS kte
|
||||
RUNTIME DESTINATION kge.app/Contents/MacOS
|
||||
)
|
||||
else ()
|
||||
install(TARGETS kge
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
endif ()
|
||||
# Install kge man page only when GUI is built
|
||||
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
||||
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
|
||||
endif ()
|
||||
|
||||
263
Command.cc
263
Command.cc
@@ -7,15 +7,15 @@
|
||||
#include <cctype>
|
||||
|
||||
#include "Command.h"
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "NullHighlighter.h"
|
||||
#include "syntax/HighlighterRegistry.h"
|
||||
#include "syntax/NullHighlighter.h"
|
||||
#include "Editor.h"
|
||||
#include "Buffer.h"
|
||||
#include "UndoSystem.h"
|
||||
#include "HelpText.h"
|
||||
#include "LanguageHighlighter.h"
|
||||
#include "HighlighterEngine.h"
|
||||
#include "CppHighlighter.h"
|
||||
#include "syntax/LanguageHighlighter.h"
|
||||
#include "syntax/HighlighterEngine.h"
|
||||
#include "syntax/CppHighlighter.h"
|
||||
#ifdef KTE_BUILD_GUI
|
||||
#include "GUITheme.h"
|
||||
#endif
|
||||
@@ -554,6 +554,8 @@ cmd_save(CommandContext &ctx)
|
||||
ctx.editor.SetStatus("Saved " + buf->Filename());
|
||||
if (auto *u = buf->Undo())
|
||||
u->mark_saved();
|
||||
// Notify LSP of save
|
||||
ctx.editor.NotifyBufferSaved(buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -608,6 +610,8 @@ cmd_save_as(CommandContext &ctx)
|
||||
ctx.editor.SetStatus("Saved as " + ctx.arg);
|
||||
if (auto *u = buf->Undo())
|
||||
u->mark_saved();
|
||||
// Notify LSP of save
|
||||
ctx.editor.NotifyBufferSaved(buf);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -762,122 +766,140 @@ cmd_unknown_kcommand(CommandContext &ctx)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// --- Syntax highlighting commands ---
|
||||
static void apply_filetype(Buffer &buf, const std::string &ft)
|
||||
static void
|
||||
apply_filetype(Buffer &buf, const std::string &ft)
|
||||
{
|
||||
buf.EnsureHighlighter();
|
||||
auto *eng = buf.Highlighter();
|
||||
if (!eng) return;
|
||||
std::string val = ft;
|
||||
// trim + lower
|
||||
auto trim = [](const std::string &s){
|
||||
std::string r = s;
|
||||
auto notsp = [](int ch){ return !std::isspace(ch); };
|
||||
r.erase(r.begin(), std::find_if(r.begin(), r.end(), notsp));
|
||||
r.erase(std::find_if(r.rbegin(), r.rend(), notsp).base(), r.end());
|
||||
return r;
|
||||
};
|
||||
val = trim(val);
|
||||
for (auto &ch: val) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (val == "off") {
|
||||
eng->SetHighlighter(nullptr);
|
||||
buf.SetFiletype("");
|
||||
buf.SetSyntaxEnabled(false);
|
||||
return;
|
||||
}
|
||||
if (val.empty()) {
|
||||
// Empty means unknown/unspecified -> use NullHighlighter but keep syntax enabled
|
||||
buf.SetFiletype("");
|
||||
buf.SetSyntaxEnabled(true);
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
return;
|
||||
}
|
||||
// Normalize and create via registry
|
||||
std::string norm = kte::HighlighterRegistry::Normalize(val);
|
||||
auto hl = kte::HighlighterRegistry::CreateFor(norm);
|
||||
if (hl) {
|
||||
eng->SetHighlighter(std::move(hl));
|
||||
buf.SetFiletype(norm);
|
||||
buf.SetSyntaxEnabled(true);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
// Unknown -> install NullHighlighter and keep syntax enabled
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
buf.SetFiletype(val); // record what user asked even if unsupported
|
||||
buf.SetSyntaxEnabled(true);
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
buf.EnsureHighlighter();
|
||||
auto *eng = buf.Highlighter();
|
||||
if (!eng)
|
||||
return;
|
||||
std::string val = ft;
|
||||
// trim + lower
|
||||
auto trim = [](const std::string &s) {
|
||||
std::string r = s;
|
||||
auto notsp = [](int ch) {
|
||||
return !std::isspace(ch);
|
||||
};
|
||||
r.erase(r.begin(), std::find_if(r.begin(), r.end(), notsp));
|
||||
r.erase(std::find_if(r.rbegin(), r.rend(), notsp).base(), r.end());
|
||||
return r;
|
||||
};
|
||||
val = trim(val);
|
||||
for (auto &ch: val)
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (val == "off") {
|
||||
eng->SetHighlighter(nullptr);
|
||||
buf.SetFiletype("");
|
||||
buf.SetSyntaxEnabled(false);
|
||||
return;
|
||||
}
|
||||
if (val.empty()) {
|
||||
// Empty means unknown/unspecified -> use NullHighlighter but keep syntax enabled
|
||||
buf.SetFiletype("");
|
||||
buf.SetSyntaxEnabled(true);
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
return;
|
||||
}
|
||||
// Normalize and create via registry
|
||||
std::string norm = kte::HighlighterRegistry::Normalize(val);
|
||||
auto hl = kte::HighlighterRegistry::CreateFor(norm);
|
||||
if (hl) {
|
||||
eng->SetHighlighter(std::move(hl));
|
||||
buf.SetFiletype(norm);
|
||||
buf.SetSyntaxEnabled(true);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
// Unknown -> install NullHighlighter and keep syntax enabled
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
buf.SetFiletype(val); // record what user asked even if unsupported
|
||||
buf.SetSyntaxEnabled(true);
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
|
||||
static bool cmd_syntax(CommandContext &ctx)
|
||||
|
||||
static bool
|
||||
cmd_syntax(CommandContext &ctx)
|
||||
{
|
||||
Buffer *b = ctx.editor.CurrentBuffer();
|
||||
if (!b) {
|
||||
ctx.editor.SetStatus("No buffer");
|
||||
return true;
|
||||
}
|
||||
std::string arg = ctx.arg;
|
||||
// trim
|
||||
auto trim = [](std::string &s){
|
||||
auto notsp = [](int ch){ return !std::isspace(ch); };
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
|
||||
};
|
||||
trim(arg);
|
||||
if (arg == "on") {
|
||||
b->SetSyntaxEnabled(true);
|
||||
// If no highlighter but filetype is cpp by extension, set it
|
||||
if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
|
||||
apply_filetype(*b, b->Filetype().empty() ? std::string("cpp") : b->Filetype());
|
||||
}
|
||||
ctx.editor.SetStatus("syntax: on");
|
||||
} else if (arg == "off") {
|
||||
b->SetSyntaxEnabled(false);
|
||||
ctx.editor.SetStatus("syntax: off");
|
||||
} else if (arg == "reload") {
|
||||
if (auto *eng = b->Highlighter()) eng->InvalidateFrom(0);
|
||||
ctx.editor.SetStatus("syntax: reloaded");
|
||||
} else {
|
||||
ctx.editor.SetStatus("usage: :syntax on|off|reload");
|
||||
}
|
||||
return true;
|
||||
Buffer *b = ctx.editor.CurrentBuffer();
|
||||
if (!b) {
|
||||
ctx.editor.SetStatus("No buffer");
|
||||
return true;
|
||||
}
|
||||
std::string arg = ctx.arg;
|
||||
// trim
|
||||
auto trim = [](std::string &s) {
|
||||
auto notsp = [](int ch) {
|
||||
return !std::isspace(ch);
|
||||
};
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
|
||||
};
|
||||
trim(arg);
|
||||
if (arg == "on") {
|
||||
b->SetSyntaxEnabled(true);
|
||||
// If no highlighter but filetype is cpp by extension, set it
|
||||
if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
|
||||
apply_filetype(*b, b->Filetype().empty() ? std::string("cpp") : b->Filetype());
|
||||
}
|
||||
ctx.editor.SetStatus("syntax: on");
|
||||
} else if (arg == "off") {
|
||||
b->SetSyntaxEnabled(false);
|
||||
ctx.editor.SetStatus("syntax: off");
|
||||
} else if (arg == "reload") {
|
||||
if (auto *eng = b->Highlighter())
|
||||
eng->InvalidateFrom(0);
|
||||
ctx.editor.SetStatus("syntax: reloaded");
|
||||
} else {
|
||||
ctx.editor.SetStatus("usage: :syntax on|off|reload");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cmd_set_option(CommandContext &ctx)
|
||||
|
||||
static bool
|
||||
cmd_set_option(CommandContext &ctx)
|
||||
{
|
||||
Buffer *b = ctx.editor.CurrentBuffer();
|
||||
if (!b) {
|
||||
ctx.editor.SetStatus("No buffer");
|
||||
return true;
|
||||
}
|
||||
// Expect key=value
|
||||
auto eq = ctx.arg.find('=');
|
||||
if (eq == std::string::npos) {
|
||||
ctx.editor.SetStatus("usage: :set key=value");
|
||||
return true;
|
||||
}
|
||||
std::string key = ctx.arg.substr(0, eq);
|
||||
std::string val = ctx.arg.substr(eq + 1);
|
||||
// trim
|
||||
auto trim = [](std::string &s){
|
||||
auto notsp = [](int ch){ return !std::isspace(ch); };
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
|
||||
};
|
||||
trim(key); trim(val);
|
||||
// lower-case value for filetype
|
||||
for (auto &ch: val) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (key == "filetype") {
|
||||
apply_filetype(*b, val);
|
||||
if (b->SyntaxEnabled())
|
||||
ctx.editor.SetStatus(std::string("filetype: ") + (b->Filetype().empty()?"off":b->Filetype()));
|
||||
else
|
||||
ctx.editor.SetStatus("filetype: off");
|
||||
return true;
|
||||
}
|
||||
ctx.editor.SetStatus("unknown option: " + key);
|
||||
return true;
|
||||
Buffer *b = ctx.editor.CurrentBuffer();
|
||||
if (!b) {
|
||||
ctx.editor.SetStatus("No buffer");
|
||||
return true;
|
||||
}
|
||||
// Expect key=value
|
||||
auto eq = ctx.arg.find('=');
|
||||
if (eq == std::string::npos) {
|
||||
ctx.editor.SetStatus("usage: :set key=value");
|
||||
return true;
|
||||
}
|
||||
std::string key = ctx.arg.substr(0, eq);
|
||||
std::string val = ctx.arg.substr(eq + 1);
|
||||
// trim
|
||||
auto trim = [](std::string &s) {
|
||||
auto notsp = [](int ch) {
|
||||
return !std::isspace(ch);
|
||||
};
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
|
||||
};
|
||||
trim(key);
|
||||
trim(val);
|
||||
// lower-case value for filetype
|
||||
for (auto &ch: val)
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (key == "filetype") {
|
||||
apply_filetype(*b, val);
|
||||
if (b->SyntaxEnabled())
|
||||
ctx.editor.SetStatus(
|
||||
std::string("filetype: ") + (b->Filetype().empty() ? "off" : b->Filetype()));
|
||||
else
|
||||
ctx.editor.SetStatus("filetype: off");
|
||||
return true;
|
||||
}
|
||||
ctx.editor.SetStatus("unknown option: " + key);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -907,6 +929,7 @@ cmd_theme_next(CommandContext &ctx)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
cmd_theme_prev(CommandContext &ctx)
|
||||
{
|
||||
@@ -3734,12 +3757,12 @@ InstallDefaultCommands()
|
||||
CommandId::ChangeWorkingDirectory, "change-working-directory", "Change current working directory",
|
||||
cmd_change_working_directory_start
|
||||
});
|
||||
// UI helpers
|
||||
CommandRegistry::Register(
|
||||
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
|
||||
// Syntax highlighting (public commands)
|
||||
CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true});
|
||||
CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true});
|
||||
// UI helpers
|
||||
CommandRegistry::Register(
|
||||
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
|
||||
// Syntax highlighting (public commands)
|
||||
CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true});
|
||||
CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true});
|
||||
}
|
||||
|
||||
|
||||
@@ -3781,4 +3804,4 @@ Execute(Editor &ed, const std::string &name, const std::string &arg, int count)
|
||||
return false;
|
||||
CommandContext ctx{ed, arg, count};
|
||||
return cmd->handler ? cmd->handler(ctx) : false;
|
||||
}
|
||||
}
|
||||
13
Command.h
13
Command.h
@@ -94,10 +94,13 @@ enum class CommandId {
|
||||
// Theme by name
|
||||
ThemeSetByName,
|
||||
// Background mode (GUI)
|
||||
BackgroundSet,
|
||||
// Syntax highlighting
|
||||
Syntax, // ":syntax on|off|reload"
|
||||
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
||||
BackgroundSet,
|
||||
// Syntax highlighting
|
||||
Syntax, // ":syntax on|off|reload"
|
||||
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
||||
// LSP
|
||||
LspHover,
|
||||
LspGotoDefinition,
|
||||
};
|
||||
|
||||
|
||||
@@ -151,4 +154,4 @@ bool Execute(Editor &ed, CommandId id, const std::string &arg = std::string(), i
|
||||
|
||||
bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0);
|
||||
|
||||
#endif // KTE_COMMAND_H
|
||||
#endif // KTE_COMMAND_H
|
||||
@@ -1,170 +0,0 @@
|
||||
#include "CppHighlighter.h"
|
||||
#include "Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
|
||||
static bool is_digit(char c) { return c >= '0' && c <= '9'; }
|
||||
|
||||
CppHighlighter::CppHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"if","else","for","while","do","switch","case","default","break","continue",
|
||||
"return","goto","struct","class","namespace","using","template","typename",
|
||||
"public","private","protected","virtual","override","const","constexpr","auto",
|
||||
"static","inline","operator","new","delete","try","catch","throw","friend",
|
||||
"enum","union","extern","volatile","mutable","noexcept","sizeof","this"
|
||||
};
|
||||
for (auto s: kw) keywords_.insert(s);
|
||||
const char *types[] = {
|
||||
"int","long","short","char","signed","unsigned","float","double","void",
|
||||
"bool","wchar_t","size_t","ptrdiff_t","uint8_t","uint16_t","uint32_t","uint64_t",
|
||||
"int8_t","int16_t","int32_t","int64_t"
|
||||
};
|
||||
for (auto s: types) types_.insert(s);
|
||||
}
|
||||
|
||||
bool CppHighlighter::is_ident_start(char c) { return std::isalpha(static_cast<unsigned char>(c)) || c == '_'; }
|
||||
bool CppHighlighter::is_ident_char(char c) { return std::isalnum(static_cast<unsigned char>(c)) || c == '_'; }
|
||||
|
||||
void CppHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
// Stateless entry simply delegates to stateful with a clean previous state
|
||||
StatefulHighlighter::LineState prev;
|
||||
(void)HighlightLineStateful(buf, row, prev, out);
|
||||
}
|
||||
|
||||
StatefulHighlighter::LineState CppHighlighter::HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
StatefulHighlighter::LineState state = prev;
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return state;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
if (s.empty()) return state;
|
||||
|
||||
auto push = [&](int a, int b, TokenKind k){ if (b> a) out.push_back({a,b,k}); };
|
||||
int n = static_cast<int>(s.size());
|
||||
int bol = 0; while (bol < n && (s[bol] == ' ' || s[bol] == '\t')) ++bol;
|
||||
int i = 0;
|
||||
|
||||
// Continue multi-line raw string from previous line
|
||||
if (state.in_raw_string) {
|
||||
std::string needle = ")" + state.raw_delim + "\"";
|
||||
auto pos = s.find(needle);
|
||||
if (pos == std::string::npos) {
|
||||
push(0, n, TokenKind::String);
|
||||
state.in_raw_string = true;
|
||||
return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + needle.size());
|
||||
push(0, end, TokenKind::String);
|
||||
i = end;
|
||||
state.in_raw_string = false;
|
||||
state.raw_delim.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Continue multi-line block comment from previous line
|
||||
if (state.in_block_comment) {
|
||||
int j = i;
|
||||
while (i + 1 < n) {
|
||||
if (s[i] == '*' && s[i+1] == '/') { i += 2; push(j, i, TokenKind::Comment); state.in_block_comment = false; break; }
|
||||
++i;
|
||||
}
|
||||
if (state.in_block_comment) { push(j, n, TokenKind::Comment); return state; }
|
||||
}
|
||||
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
// Preprocessor at beginning of line (after leading whitespace)
|
||||
if (i == bol && c == '#') { push(0, n, TokenKind::Preproc); break; }
|
||||
|
||||
// Whitespace
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i+1; while (j < n && (s[j] == ' ' || s[j] == '\t')) ++j; push(i,j,TokenKind::Whitespace); i=j; continue;
|
||||
}
|
||||
|
||||
// Line comment
|
||||
if (c == '/' && i+1 < n && s[i+1] == '/') { push(i, n, TokenKind::Comment); break; }
|
||||
|
||||
// Block comment
|
||||
if (c == '/' && i+1 < n && s[i+1] == '*') {
|
||||
int j = i+2;
|
||||
bool closed = false;
|
||||
while (j + 1 <= n) {
|
||||
if (j + 1 < n && s[j] == '*' && s[j+1] == '/') { j += 2; closed = true; break; }
|
||||
++j;
|
||||
}
|
||||
if (closed) { push(i, j, TokenKind::Comment); i = j; continue; }
|
||||
// Spill to next lines
|
||||
push(i, n, TokenKind::Comment);
|
||||
state.in_block_comment = true;
|
||||
return state;
|
||||
}
|
||||
|
||||
// Raw string start: very simple detection: R"delim(
|
||||
if (c == 'R' && i+1 < n && s[i+1] == '"') {
|
||||
int k = i + 2;
|
||||
std::string delim;
|
||||
while (k < n && s[k] != '(') { delim.push_back(s[k]); ++k; }
|
||||
if (k < n && s[k] == '(') {
|
||||
int body_start = k + 1;
|
||||
std::string needle = ")" + delim + "\"";
|
||||
auto pos = s.find(needle, static_cast<std::size_t>(body_start));
|
||||
if (pos == std::string::npos) {
|
||||
push(i, n, TokenKind::String);
|
||||
state.in_raw_string = true;
|
||||
state.raw_delim = delim;
|
||||
return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + needle.size());
|
||||
push(i, end, TokenKind::String);
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// If malformed, just treat 'R' as identifier fallback
|
||||
}
|
||||
|
||||
// Regular string literal
|
||||
if (c == '"') {
|
||||
int j = i+1; bool esc=false; while (j < n) { char d = s[j++]; if (esc) { esc=false; continue; } if (d == '\\') { esc=true; continue; } if (d == '"') break; }
|
||||
push(i, j, TokenKind::String); i = j; continue;
|
||||
}
|
||||
|
||||
// Char literal
|
||||
if (c == '\'') {
|
||||
int j = i+1; bool esc=false; while (j < n) { char d = s[j++]; if (esc) { esc=false; continue; } if (d == '\\') { esc=true; continue; } if (d == '\'') break; }
|
||||
push(i, j, TokenKind::Char); i = j; continue;
|
||||
}
|
||||
|
||||
// Number literal (simple)
|
||||
if (is_digit(c) || (c == '.' && i+1 < n && is_digit(s[i+1]))) {
|
||||
int j = i+1; while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j]=='.' || s[j]=='x' || s[j]=='X' || s[j]=='b' || s[j]=='B' || s[j]=='_')) ++j;
|
||||
push(i, j, TokenKind::Number); i = j; continue;
|
||||
}
|
||||
|
||||
// Identifier / keyword / type
|
||||
if (is_ident_start(c)) {
|
||||
int j = i+1; while (j < n && is_ident_char(s[j])) ++j; std::string id = s.substr(i, j-i);
|
||||
TokenKind k = TokenKind::Identifier; if (keywords_.count(id)) k = TokenKind::Keyword; else if (types_.count(id)) k = TokenKind::Type; push(i, j, k); i = j; continue;
|
||||
}
|
||||
|
||||
// Operators and punctuation (single char for now)
|
||||
TokenKind kind = TokenKind::Operator;
|
||||
if (std::ispunct(static_cast<unsigned char>(c)) && c != '_' && c != '#') {
|
||||
if (c==';' || c==',' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']') kind = TokenKind::Punctuation;
|
||||
push(i, i+1, kind); ++i; continue;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
push(i, i+1, TokenKind::Default); ++i;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,34 +0,0 @@
|
||||
// CppHighlighter.h - minimal stateless C/C++ line highlighter
|
||||
#pragma once
|
||||
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
|
||||
class CppHighlighter final : public StatefulHighlighter {
|
||||
public:
|
||||
CppHighlighter();
|
||||
~CppHighlighter() override = default;
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
LineState HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> keywords_;
|
||||
std::unordered_set<std::string> types_;
|
||||
|
||||
static bool is_ident_start(char c);
|
||||
static bool is_ident_char(char c);
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
215
Editor.cc
215
Editor.cc
@@ -1,13 +1,14 @@
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <filesystem>
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "NullHighlighter.h"
|
||||
#include "syntax/HighlighterRegistry.h"
|
||||
#include "syntax/NullHighlighter.h"
|
||||
|
||||
#include "Editor.h"
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "CppHighlighter.h"
|
||||
#include "NullHighlighter.h"
|
||||
#include "lsp/LspManager.h"
|
||||
#include "syntax/HighlighterRegistry.h"
|
||||
#include "syntax/CppHighlighter.h"
|
||||
#include "syntax/NullHighlighter.h"
|
||||
|
||||
|
||||
Editor::Editor() = default;
|
||||
@@ -29,6 +30,15 @@ Editor::SetStatus(const std::string &message)
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Editor::NotifyBufferSaved(Buffer *buf)
|
||||
{
|
||||
if (lsp_manager_ && buf) {
|
||||
lsp_manager_->onBufferSaved(buf);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Buffer *
|
||||
Editor::CurrentBuffer()
|
||||
{
|
||||
@@ -148,104 +158,115 @@ Editor::OpenFile(const std::string &path, std::string &err)
|
||||
{
|
||||
// If there is exactly one unnamed, empty, clean buffer, reuse it instead
|
||||
// of creating a new one.
|
||||
if (buffers_.size() == 1) {
|
||||
Buffer &cur = buffers_[curbuf_];
|
||||
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
||||
const bool clean = !cur.Dirty();
|
||||
const auto &rows = cur.Rows();
|
||||
const bool rows_empty = rows.empty();
|
||||
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
|
||||
if (unnamed && clean && (rows_empty || single_empty_line)) {
|
||||
bool ok = cur.OpenFromFile(path, err);
|
||||
if (!ok) return false;
|
||||
// Setup highlighting using registry (extension + shebang)
|
||||
cur.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
const auto &rows = cur.Rows();
|
||||
if (!rows.empty()) first = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
cur.SetFiletype(ft);
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
cur.SetFiletype("");
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (buffers_.size() == 1) {
|
||||
Buffer &cur = buffers_[curbuf_];
|
||||
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
||||
const bool clean = !cur.Dirty();
|
||||
const auto &rows = cur.Rows();
|
||||
const bool rows_empty = rows.empty();
|
||||
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
|
||||
if (unnamed && clean && (rows_empty || single_empty_line)) {
|
||||
bool ok = cur.OpenFromFile(path, err);
|
||||
if (!ok)
|
||||
return false;
|
||||
// Setup highlighting using registry (extension + shebang)
|
||||
cur.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
const auto &rows = cur.Rows();
|
||||
if (!rows.empty())
|
||||
first = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
cur.SetFiletype(ft);
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
cur.SetFiletype("");
|
||||
cur.SetSyntaxEnabled(true);
|
||||
if (auto *eng = cur.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
// Notify LSP (if wired) for current buffer open
|
||||
if (lsp_manager_) {
|
||||
lsp_manager_->onBufferOpened(&cur);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Buffer b;
|
||||
if (!b.OpenFromFile(path, err)) {
|
||||
return false;
|
||||
}
|
||||
// Initialize syntax highlighting by extension + shebang via registry (v2)
|
||||
b.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
{
|
||||
const auto &rows = b.Rows();
|
||||
if (!rows.empty()) first = static_cast<std::string>(rows[0]);
|
||||
}
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
b.SetFiletype(ft);
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
b.SetFiletype("");
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
// Add as a new buffer and switch to it
|
||||
std::size_t idx = AddBuffer(std::move(b));
|
||||
SwitchTo(idx);
|
||||
return true;
|
||||
Buffer b;
|
||||
if (!b.OpenFromFile(path, err)) {
|
||||
return false;
|
||||
}
|
||||
// Initialize syntax highlighting by extension + shebang via registry (v2)
|
||||
b.EnsureHighlighter();
|
||||
std::string first = "";
|
||||
{
|
||||
const auto &rows = b.Rows();
|
||||
if (!rows.empty())
|
||||
first = static_cast<std::string>(rows[0]);
|
||||
}
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
|
||||
if (!ft.empty()) {
|
||||
b.SetFiletype(ft);
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
} else {
|
||||
b.SetFiletype("");
|
||||
b.SetSyntaxEnabled(true);
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
// Add as a new buffer and switch to it
|
||||
std::size_t idx = AddBuffer(std::move(b));
|
||||
SwitchTo(idx);
|
||||
// Notify LSP (if wired) for current buffer open
|
||||
if (lsp_manager_) {
|
||||
lsp_manager_->onBufferOpened(&buffers_[curbuf_]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
Editor::SwitchTo(std::size_t index)
|
||||
{
|
||||
if (index >= buffers_.size()) {
|
||||
return false;
|
||||
}
|
||||
curbuf_ = index;
|
||||
// Robustness: ensure a valid highlighter is installed when switching buffers
|
||||
Buffer &b = buffers_[curbuf_];
|
||||
if (b.SyntaxEnabled()) {
|
||||
b.EnsureHighlighter();
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try to set based on existing filetype; fall back to NullHighlighter
|
||||
if (!b.Filetype().empty()) {
|
||||
auto hl = kte::HighlighterRegistry::CreateFor(b.Filetype());
|
||||
if (hl) {
|
||||
eng->SetHighlighter(std::move(hl));
|
||||
} else {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
} else {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
if (index >= buffers_.size()) {
|
||||
return false;
|
||||
}
|
||||
curbuf_ = index;
|
||||
// Robustness: ensure a valid highlighter is installed when switching buffers
|
||||
Buffer &b = buffers_[curbuf_];
|
||||
if (b.SyntaxEnabled()) {
|
||||
b.EnsureHighlighter();
|
||||
if (auto *eng = b.Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try to set based on existing filetype; fall back to NullHighlighter
|
||||
if (!b.Filetype().empty()) {
|
||||
auto hl = kte::HighlighterRegistry::CreateFor(b.Filetype());
|
||||
if (hl) {
|
||||
eng->SetHighlighter(std::move(hl));
|
||||
} else {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
} else {
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
}
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -281,4 +302,4 @@ Editor::Reset()
|
||||
quit_confirm_pending_ = false;
|
||||
buffers_.clear();
|
||||
curbuf_ = 0;
|
||||
}
|
||||
}
|
||||
28
Editor.h
28
Editor.h
@@ -11,6 +11,13 @@
|
||||
|
||||
#include "Buffer.h"
|
||||
|
||||
// fwd decl for LSP wiring
|
||||
namespace kte {
|
||||
namespace lsp {
|
||||
class LspManager;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Editor {
|
||||
public:
|
||||
@@ -436,6 +443,22 @@ public:
|
||||
|
||||
bool OpenFile(const std::string &path, std::string &err);
|
||||
|
||||
// LSP: attach/detach manager
|
||||
void SetLspManager(kte::lsp::LspManager *mgr)
|
||||
{
|
||||
lsp_manager_ = mgr;
|
||||
}
|
||||
|
||||
|
||||
// LSP helpers: trigger hover/definition at current cursor in current buffer
|
||||
bool LspHoverAtCursor();
|
||||
|
||||
bool LspGotoDefinitionAtCursor();
|
||||
|
||||
|
||||
// LSP: notify buffer saved (used by commands)
|
||||
void NotifyBufferSaved(Buffer *buf);
|
||||
|
||||
// Buffer switching/closing
|
||||
bool SwitchTo(std::size_t index);
|
||||
|
||||
@@ -551,6 +574,9 @@ public:
|
||||
private:
|
||||
std::string replace_find_tmp_;
|
||||
std::string replace_with_tmp_;
|
||||
|
||||
// Non-owning pointer to LSP manager (if provided)
|
||||
kte::lsp::LspManager *lsp_manager_ = nullptr;
|
||||
};
|
||||
|
||||
#endif // KTE_EDITOR_H
|
||||
#endif // KTE_EDITOR_H
|
||||
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
|
||||
@@ -16,8 +16,8 @@
|
||||
#include "Font.h" // embedded default font (DefaultFontRegular)
|
||||
#include "GUIConfig.h"
|
||||
#include "GUITheme.h"
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "NullHighlighter.h"
|
||||
#include "syntax/HighlighterRegistry.h"
|
||||
#include "syntax/NullHighlighter.h"
|
||||
|
||||
|
||||
#ifndef KTE_FONT_SIZE
|
||||
@@ -108,42 +108,44 @@ GUIFrontend::Init(Editor &ed)
|
||||
(void) io;
|
||||
ImGui::StyleColorsDark();
|
||||
|
||||
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
||||
if (cfg.background == "light")
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||
else
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||
kte::ApplyThemeByName(cfg.theme);
|
||||
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
||||
if (cfg.background == "light")
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||
else
|
||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||
kte::ApplyThemeByName(cfg.theme);
|
||||
|
||||
// Apply default syntax highlighting preference from GUI config to the current buffer
|
||||
if (Buffer *b = ed.CurrentBuffer()) {
|
||||
if (cfg.syntax) {
|
||||
b->SetSyntaxEnabled(true);
|
||||
// Ensure a highlighter is available if possible
|
||||
b->EnsureHighlighter();
|
||||
if (auto *eng = b->Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try detect from filename and first line; fall back to cpp or existing filetype
|
||||
std::string first_line;
|
||||
const auto &rows = b->Rows();
|
||||
if (!rows.empty()) first_line = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(b->Filename(), first_line);
|
||||
if (!ft.empty()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
b->SetFiletype(ft);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
b->SetFiletype("");
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b->SetSyntaxEnabled(false);
|
||||
}
|
||||
}
|
||||
// Apply default syntax highlighting preference from GUI config to the current buffer
|
||||
if (Buffer *b = ed.CurrentBuffer()) {
|
||||
if (cfg.syntax) {
|
||||
b->SetSyntaxEnabled(true);
|
||||
// Ensure a highlighter is available if possible
|
||||
b->EnsureHighlighter();
|
||||
if (auto *eng = b->Highlighter()) {
|
||||
if (!eng->HasHighlighter()) {
|
||||
// Try detect from filename and first line; fall back to cpp or existing filetype
|
||||
std::string first_line;
|
||||
const auto &rows = b->Rows();
|
||||
if (!rows.empty())
|
||||
first_line = static_cast<std::string>(rows[0]);
|
||||
std::string ft = kte::HighlighterRegistry::DetectForPath(
|
||||
b->Filename(), first_line);
|
||||
if (!ft.empty()) {
|
||||
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
|
||||
b->SetFiletype(ft);
|
||||
eng->InvalidateFrom(0);
|
||||
} else {
|
||||
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
|
||||
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
|
||||
b->SetFiletype("");
|
||||
eng->InvalidateFrom(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
b->SetSyntaxEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
||||
return false;
|
||||
@@ -316,4 +318,4 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
||||
}
|
||||
|
||||
|
||||
// No runtime font reload or system font resolution in this simplified build.
|
||||
// No runtime font reload or system font resolution in this simplified build.
|
||||
195
GUIRenderer.cc
195
GUIRenderer.cc
@@ -47,7 +47,6 @@ GUIRenderer::Draw(Editor &ed)
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar
|
||||
| ImGuiWindowFlags_NoScrollbar
|
||||
| ImGuiWindowFlags_NoScrollWithMouse
|
||||
| ImGuiWindowFlags_NoResize
|
||||
| ImGuiWindowFlags_NoMove
|
||||
| ImGuiWindowFlags_NoCollapse
|
||||
@@ -60,7 +59,7 @@ GUIRenderer::Draw(Editor &ed)
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.f, 6.f));
|
||||
|
||||
ImGui::Begin("kte", nullptr, flags);
|
||||
ImGui::Begin("kte", nullptr, flags | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
|
||||
const Buffer *buf = ed.CurrentBuffer();
|
||||
if (!buf) {
|
||||
@@ -69,7 +68,7 @@ GUIRenderer::Draw(Editor &ed)
|
||||
const auto &lines = buf->Rows();
|
||||
// Reserve space for status bar at bottom
|
||||
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
|
||||
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGuiWindowFlags_HorizontalScrollbar);
|
||||
// Detect click-to-move inside this scroll region
|
||||
ImVec2 list_origin = ImGui::GetCursorScreenPos();
|
||||
float scroll_y = ImGui::GetScrollY();
|
||||
@@ -109,13 +108,13 @@ GUIRenderer::Draw(Editor &ed)
|
||||
}
|
||||
// If user scrolled, update buffer offsets accordingly
|
||||
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
|
||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||
if (auto mbuf = const_cast<Buffer *>(buf)) {
|
||||
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
||||
mbuf->Coloffs());
|
||||
}
|
||||
}
|
||||
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x) {
|
||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||
if (auto mbuf = const_cast<Buffer *>(buf)) {
|
||||
mbuf->SetOffsets(mbuf->Rowoffs(),
|
||||
static_cast<std::size_t>(std::max(0L, scroll_left)));
|
||||
}
|
||||
@@ -139,34 +138,36 @@ GUIRenderer::Draw(Editor &ed)
|
||||
vis_rows = 1;
|
||||
long last_row = first_row + vis_rows - 1;
|
||||
|
||||
if (!forced_scroll) {
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
|
||||
float max_y = ImGui::GetScrollMaxY();
|
||||
if (target < 0.f)
|
||||
target = 0.f;
|
||||
if (max_y >= 0.f && target > max_y)
|
||||
target = max_y;
|
||||
ImGui::SetScrollY(target);
|
||||
// refresh local variables
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
first_row = static_cast<long>(scroll_y / row_h);
|
||||
last_row = first_row + vis_rows - 1;
|
||||
}
|
||||
}
|
||||
// Phase 3: prefetch visible viewport highlights and warm around in background
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
int fr = static_cast<int>(std::max(0L, first_row));
|
||||
int rc = static_cast<int>(std::max(1L, vis_rows));
|
||||
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
||||
}
|
||||
}
|
||||
// Handle mouse click before rendering to avoid dependent on drawn items
|
||||
if (!forced_scroll) {
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
|
||||
float max_y = ImGui::GetScrollMaxY();
|
||||
if (target < 0.f)
|
||||
target = 0.f;
|
||||
if (max_y >= 0.f && target > max_y)
|
||||
target = max_y;
|
||||
ImGui::SetScrollY(target);
|
||||
// refresh local variables
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
first_row = static_cast<long>(scroll_y / row_h);
|
||||
last_row = first_row + vis_rows - 1;
|
||||
}
|
||||
}
|
||||
// Phase 3: prefetch visible viewport highlights and warm around in background
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
int fr = static_cast<int>(std::max(0L, first_row));
|
||||
int rc = static_cast<int>(std::max(1L, vis_rows));
|
||||
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
||||
}
|
||||
}
|
||||
// Handle mouse click before rendering to avoid dependency on drawn items
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||
// Compute viewport-relative row so (0) is top row of the visible area
|
||||
float vy_f = (mp.y - list_origin.y - scroll_y) / row_h;
|
||||
// Note: list_origin is already in the scrolled space of the child window,
|
||||
// so we must NOT subtract scroll_y again (would double-apply).
|
||||
float vy_f = (mp.y - list_origin.y) / row_h;
|
||||
long vy = static_cast<long>(vy_f);
|
||||
if (vy < 0)
|
||||
vy = 0;
|
||||
@@ -190,8 +191,9 @@ GUIRenderer::Draw(Editor &ed)
|
||||
by = 0;
|
||||
}
|
||||
|
||||
// Compute desired pixel X inside the viewport content (subtract horizontal scroll)
|
||||
float px = (mp.x - list_origin.x - scroll_x);
|
||||
// Compute desired pixel X inside the viewport content.
|
||||
// list_origin is already scrolled; do not subtract scroll_x here.
|
||||
float px = (mp.x - list_origin.x);
|
||||
if (px < 0.0f)
|
||||
px = 0.0f;
|
||||
|
||||
@@ -254,11 +256,11 @@ GUIRenderer::Draw(Editor &ed)
|
||||
const std::size_t coloffs_now = buf->Coloffs();
|
||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
||||
// Capture the screen position before drawing the line
|
||||
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
||||
std::string line = static_cast<std::string>(lines[i]);
|
||||
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
||||
auto line = static_cast<std::string>(lines[i]);
|
||||
|
||||
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
|
||||
const std::size_t tabw = 8;
|
||||
constexpr std::size_t tabw = 8;
|
||||
std::string expanded;
|
||||
expanded.reserve(line.size() + 16);
|
||||
std::size_t rx_abs_draw = 0; // rendered column for drawing
|
||||
@@ -275,7 +277,7 @@ GUIRenderer::Draw(Editor &ed)
|
||||
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
||||
it != std::sregex_iterator(); ++it) {
|
||||
const auto &m = *it;
|
||||
std::size_t sx = static_cast<std::size_t>(m.position());
|
||||
auto sx = static_cast<std::size_t>(m.position());
|
||||
std::size_t ex = sx + static_cast<std::size_t>(m.length());
|
||||
hl_src_ranges.emplace_back(sx, ex);
|
||||
}
|
||||
@@ -318,9 +320,9 @@ GUIRenderer::Draw(Editor &ed)
|
||||
continue; // fully left of view
|
||||
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
|
||||
std::size_t vx1 = rx_end - coloffs_now;
|
||||
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
||||
line_pos.y + line_h);
|
||||
auto p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||
auto p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
||||
line_pos.y + line_h);
|
||||
// Choose color: current match stronger
|
||||
bool is_current = has_current && sx == cur_x && ex == cur_end;
|
||||
ImU32 col = is_current
|
||||
@@ -329,50 +331,58 @@ GUIRenderer::Draw(Editor &ed)
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||
}
|
||||
}
|
||||
// Emit entire line to an expanded buffer (tabs -> spaces)
|
||||
for (std::size_t src = 0; src < line.size(); ++src) {
|
||||
char c = line[src];
|
||||
if (c == '\t') {
|
||||
std::size_t adv = (tabw - (rx_abs_draw % tabw));
|
||||
expanded.append(adv, ' ');
|
||||
rx_abs_draw += adv;
|
||||
} else {
|
||||
expanded.push_back(c);
|
||||
rx_abs_draw += 1;
|
||||
}
|
||||
}
|
||||
// Emit entire line to an expanded buffer (tabs -> spaces)
|
||||
for (std::size_t src = 0; src < line.size(); ++src) {
|
||||
char c = line[src];
|
||||
if (c == '\t') {
|
||||
std::size_t adv = (tabw - (rx_abs_draw % tabw));
|
||||
expanded.append(adv, ' ');
|
||||
rx_abs_draw += adv;
|
||||
} else {
|
||||
expanded.push_back(c);
|
||||
rx_abs_draw += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw syntax-colored runs (text above background highlights)
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(*buf, static_cast<int>(i), buf->Version());
|
||||
// Helper to convert a src column to expanded rx position
|
||||
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
|
||||
std::size_t rx = 0;
|
||||
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
|
||||
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||
}
|
||||
return rx;
|
||||
};
|
||||
for (const auto &sp: lh.spans) {
|
||||
std::size_t rx_s = src_to_rx_full(static_cast<std::size_t>(std::max(0, sp.col_start)));
|
||||
std::size_t rx_e = src_to_rx_full(static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
|
||||
if (rx_e <= coloffs_now)
|
||||
continue;
|
||||
std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
|
||||
std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
|
||||
if (vx0 >= expanded.size()) continue;
|
||||
vx1 = std::min<std::size_t>(vx1, expanded.size());
|
||||
if (vx1 <= vx0) continue;
|
||||
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
|
||||
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||
ImGui::GetWindowDrawList()->AddText(p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
|
||||
}
|
||||
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
|
||||
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + line_h));
|
||||
} else {
|
||||
// No syntax: draw as one run
|
||||
ImGui::TextUnformatted(expanded.c_str());
|
||||
}
|
||||
// Draw syntax-colored runs (text above background highlights)
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(
|
||||
*buf, static_cast<int>(i), buf->Version());
|
||||
// Helper to convert a src column to expanded rx position
|
||||
auto src_to_rx_full = [&](const std::size_t sidx) -> std::size_t {
|
||||
std::size_t rx = 0;
|
||||
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
|
||||
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||
}
|
||||
return rx;
|
||||
};
|
||||
for (const auto &sp: lh.spans) {
|
||||
std::size_t rx_s = src_to_rx_full(
|
||||
static_cast<std::size_t>(std::max(0, sp.col_start)));
|
||||
std::size_t rx_e = src_to_rx_full(
|
||||
static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
|
||||
if (rx_e <= coloffs_now)
|
||||
continue;
|
||||
std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
|
||||
std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
|
||||
if (vx0 >= expanded.size())
|
||||
continue;
|
||||
vx1 = std::min<std::size_t>(vx1, expanded.size());
|
||||
if (vx1 <= vx0)
|
||||
continue;
|
||||
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
|
||||
auto p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
|
||||
}
|
||||
// We drew text via draw list (no layout advance). Advance by the same amount
|
||||
// ImGui uses between lines (line height + spacing) so hit-testing (which
|
||||
// divides by row_h) aligns with drawing.
|
||||
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
||||
} else {
|
||||
// No syntax: draw as one run
|
||||
ImGui::TextUnformatted(expanded.c_str());
|
||||
}
|
||||
|
||||
// Draw a visible cursor indicator on the current line
|
||||
if (i == cy) {
|
||||
@@ -411,9 +421,9 @@ GUIRenderer::Draw(Editor &ed)
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
|
||||
// If a prompt is active, replace the entire status bar with the prompt text
|
||||
if (ed.PromptActive()) {
|
||||
std::string label = ed.PromptLabel();
|
||||
std::string ptext = ed.PromptText();
|
||||
auto kind = ed.CurrentPromptKind();
|
||||
const std::string &label = ed.PromptLabel();
|
||||
std::string ptext = ed.PromptText();
|
||||
auto kind = ed.CurrentPromptKind();
|
||||
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
||||
kind == Editor::PromptKind::Chdir) {
|
||||
const char *home_c = std::getenv("HOME");
|
||||
@@ -460,8 +470,8 @@ GUIRenderer::Draw(Editor &ed)
|
||||
float ratio = tail_sz.x / avail_px;
|
||||
size_t skip = ratio > 1.5f
|
||||
? std::min(tail.size() - start,
|
||||
(size_t) std::max<size_t>(
|
||||
1, (size_t) (tail.size() / 4)))
|
||||
static_cast<size_t>(std::max<size_t>(
|
||||
1, tail.size() / 4)))
|
||||
: 1;
|
||||
start += skip;
|
||||
std::string candidate = tail.substr(start);
|
||||
@@ -518,8 +528,7 @@ GUIRenderer::Draw(Editor &ed)
|
||||
left += " ";
|
||||
// Insert buffer position prefix "[x/N] " before filename
|
||||
{
|
||||
std::size_t total = ed.BufferCount();
|
||||
if (total > 0) {
|
||||
if (std::size_t total = ed.BufferCount(); total > 0) {
|
||||
std::size_t idx1 = ed.CurrentBufferIndex() + 1; // 1-based for display
|
||||
left += "[";
|
||||
left += std::to_string(static_cast<unsigned long long>(idx1));
|
||||
@@ -533,7 +542,7 @@ GUIRenderer::Draw(Editor &ed)
|
||||
left += " *";
|
||||
// Append total line count as "<n>L"
|
||||
{
|
||||
unsigned long lcount = static_cast<unsigned long>(buf->Rows().size());
|
||||
auto lcount = buf->Rows().size();
|
||||
left += " ";
|
||||
left += std::to_string(lcount);
|
||||
left += "L";
|
||||
@@ -619,9 +628,9 @@ GUIRenderer::Draw(Editor &ed)
|
||||
ImGuiViewport *vp2 = ImGui::GetMainViewport();
|
||||
|
||||
// Desired size, min size, and margins
|
||||
const ImVec2 want(800.0f, 500.0f);
|
||||
const ImVec2 min_sz(240.0f, 160.0f);
|
||||
const float margin = 20.0f; // space from viewport edges
|
||||
constexpr ImVec2 want(800.0f, 500.0f);
|
||||
constexpr ImVec2 min_sz(240.0f, 160.0f);
|
||||
constexpr float margin = 20.0f; // space from viewport edges
|
||||
|
||||
// Compute the maximum allowed size (viewport minus margins) and make sure it's not negative
|
||||
ImVec2 max_sz(std::max(32.0f, vp2->Size.x - 2.0f * margin),
|
||||
|
||||
979
GUITheme.h
979
GUITheme.h
File diff suppressed because it is too large
Load Diff
@@ -1,48 +0,0 @@
|
||||
#include "GoHighlighter.h"
|
||||
#include "Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
|
||||
static void push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
|
||||
static bool is_ident_start(char c){ return std::isalpha(static_cast<unsigned char>(c)) || c=='_'; }
|
||||
static bool is_ident_char(char c){ return std::isalnum(static_cast<unsigned char>(c)) || c=='_'; }
|
||||
|
||||
GoHighlighter::GoHighlighter()
|
||||
{
|
||||
const char* kw[] = {"break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"};
|
||||
for (auto s: kw) kws_.insert(s);
|
||||
const char* tp[] = {"bool","byte","complex64","complex128","error","float32","float64","int","int8","int16","int32","int64","rune","string","uint","uint8","uint16","uint32","uint64","uintptr"};
|
||||
for (auto s: tp) types_.insert(s);
|
||||
}
|
||||
|
||||
void GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
int bol=0; while (bol<n && (s[bol]==' '||s[bol]=='\t')) ++bol;
|
||||
// line comment
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c==' '||c=='\t') { int j=i+1; while (j<n && (s[j]==' '||s[j]=='\t')) ++j; push(out,i,j,TokenKind::Whitespace); i=j; continue; }
|
||||
if (c=='/' && i+1<n && s[i+1]=='/') { push(out,i,n,TokenKind::Comment); break; }
|
||||
if (c=='/' && i+1<n && s[i+1]=='*') {
|
||||
int j=i+2; bool closed=false; while (j+1<=n) { if (j+1<n && s[j]=='*' && s[j+1]=='/') { j+=2; closed=true; break; } ++j; }
|
||||
if (!closed) { push(out,i,n,TokenKind::Comment); break; } else { push(out,i,j,TokenKind::Comment); i=j; continue; }
|
||||
}
|
||||
if (c=='"' || c=='`') {
|
||||
char q=c; int j=i+1; bool esc=false; if (q=='`') { while (j<n && s[j] != '`') ++j; if (j<n) ++j; }
|
||||
else { while (j<n){ char d=s[j++]; if (esc){esc=false; continue;} if (d=='\\'){esc=true; continue;} if (d=='"') break;} }
|
||||
push(out,i,j,TokenKind::String); i=j; continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) { int j=i+1; while (j<n && (std::isalnum(static_cast<unsigned char>(s[j]))||s[j]=='.'||s[j]=='x'||s[j]=='X'||s[j]=='_')) ++j; push(out,i,j,TokenKind::Number); i=j; continue; }
|
||||
if (is_ident_start(c)) { int j=i+1; while (j<n && is_ident_char(s[j])) ++j; std::string id=s.substr(i,j-i); TokenKind k=TokenKind::Identifier; if (kws_.count(id)) k=TokenKind::Keyword; else if (types_.count(id)) k=TokenKind::Type; push(out,i,j,k); i=j; continue; }
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) { TokenKind k=TokenKind::Operator; if (c==';'||c==','||c=='('||c==')'||c=='{'||c=='}'||c=='['||c==']') k=TokenKind::Punctuation; push(out,i,i+1,k); ++i; continue; }
|
||||
push(out,i,i+1,TokenKind::Default); ++i;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,18 +0,0 @@
|
||||
// GoHighlighter.h - simple Go highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
|
||||
class GoHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
GoHighlighter();
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
std::unordered_set<std::string> types_;
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
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
|
||||
@@ -1,181 +0,0 @@
|
||||
#include "HighlighterEngine.h"
|
||||
#include "Buffer.h"
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <thread>
|
||||
|
||||
namespace kte {
|
||||
|
||||
HighlighterEngine::HighlighterEngine() = default;
|
||||
HighlighterEngine::~HighlighterEngine()
|
||||
{
|
||||
// stop background worker
|
||||
if (worker_running_.load()) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
worker_running_.store(false);
|
||||
has_request_ = true; // wake it up to exit
|
||||
}
|
||||
cv_.notify_one();
|
||||
if (worker_.joinable()) worker_.join();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
HighlighterEngine::SetHighlighter(std::unique_ptr<LanguageHighlighter> hl)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
hl_ = std::move(hl);
|
||||
cache_.clear();
|
||||
state_cache_.clear();
|
||||
state_last_contig_.clear();
|
||||
}
|
||||
|
||||
const LineHighlight &
|
||||
HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mtx_);
|
||||
auto it = cache_.find(row);
|
||||
if (it != cache_.end() && it->second.version == buf_version) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Prepare destination slot to reuse its capacity and avoid allocations
|
||||
LineHighlight &slot = cache_[row];
|
||||
slot.version = buf_version;
|
||||
slot.spans.clear();
|
||||
|
||||
if (!hl_) {
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Copy shared_ptr-like raw pointer for use outside critical sections
|
||||
LanguageHighlighter *hl_ptr = hl_.get();
|
||||
bool is_stateful = dynamic_cast<StatefulHighlighter *>(hl_ptr) != nullptr;
|
||||
|
||||
if (!is_stateful) {
|
||||
// Stateless fast path: we can release the lock while computing to reduce contention
|
||||
auto &out = slot.spans;
|
||||
lock.unlock();
|
||||
hl_ptr->HighlightLine(buf, row, out);
|
||||
return cache_.at(row);
|
||||
}
|
||||
|
||||
// Stateful path: we need to walk from a known previous state. Keep lock while consulting caches,
|
||||
// but release during heavy computation.
|
||||
auto *stateful = static_cast<StatefulHighlighter *>(hl_ptr);
|
||||
|
||||
StatefulHighlighter::LineState prev_state;
|
||||
int start_row = -1;
|
||||
if (!state_cache_.empty()) {
|
||||
// linear search over map (unordered), track best candidate
|
||||
int best = -1;
|
||||
for (const auto &kv : state_cache_) {
|
||||
int r = kv.first;
|
||||
if (r <= row - 1 && kv.second.version == buf_version) {
|
||||
if (r > best) best = r;
|
||||
}
|
||||
}
|
||||
if (best >= 0) {
|
||||
start_row = best;
|
||||
prev_state = state_cache_.at(best).state;
|
||||
}
|
||||
}
|
||||
|
||||
// We'll compute states and the target line's spans without holding the lock for most of the work.
|
||||
// Create a local copy of prev_state and iterate rows; we will update caches under lock.
|
||||
lock.unlock();
|
||||
StatefulHighlighter::LineState cur_state = prev_state;
|
||||
for (int r = start_row + 1; r <= row; ++r) {
|
||||
std::vector<HighlightSpan> tmp;
|
||||
std::vector<HighlightSpan> &out = (r == row) ? slot.spans : tmp;
|
||||
auto next_state = stateful->HighlightLineStateful(buf, r, cur_state, out);
|
||||
// Update state cache for r
|
||||
std::lock_guard<std::mutex> gl(mtx_);
|
||||
StateEntry se;
|
||||
se.version = buf_version;
|
||||
se.state = next_state;
|
||||
state_cache_[r] = se;
|
||||
cur_state = next_state;
|
||||
}
|
||||
|
||||
// Return reference under lock to ensure slot's address stability in map
|
||||
lock.lock();
|
||||
return cache_.at(row);
|
||||
}
|
||||
|
||||
void
|
||||
HighlighterEngine::InvalidateFrom(int row)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
if (cache_.empty()) return;
|
||||
// Simple implementation: erase all rows >= row
|
||||
for (auto it = cache_.begin(); it != cache_.end(); ) {
|
||||
if (it->first >= row) it = cache_.erase(it); else ++it;
|
||||
}
|
||||
if (!state_cache_.empty()) {
|
||||
for (auto it = state_cache_.begin(); it != state_cache_.end(); ) {
|
||||
if (it->first >= row) it = state_cache_.erase(it); else ++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HighlighterEngine::ensure_worker_started() const
|
||||
{
|
||||
if (worker_running_.load()) return;
|
||||
worker_running_.store(true);
|
||||
worker_ = std::thread([this]() { this->worker_loop(); });
|
||||
}
|
||||
|
||||
void HighlighterEngine::worker_loop() const
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mtx_);
|
||||
while (worker_running_.load()) {
|
||||
cv_.wait(lock, [this]() { return has_request_ || !worker_running_.load(); });
|
||||
if (!worker_running_.load()) break;
|
||||
WarmRequest req = pending_;
|
||||
has_request_ = false;
|
||||
// Copy locals then release lock while computing
|
||||
lock.unlock();
|
||||
if (req.buf) {
|
||||
int start = std::max(0, req.start_row);
|
||||
int end = std::max(start, req.end_row);
|
||||
for (int r = start; r <= end; ++r) {
|
||||
// Re-check version staleness quickly by peeking cache version; not strictly necessary
|
||||
// Compute line; GetLine is thread-safe
|
||||
(void)this->GetLine(*req.buf, r, req.version);
|
||||
}
|
||||
}
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
|
||||
void HighlighterEngine::PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version, int warm_margin) const
|
||||
{
|
||||
if (row_count <= 0) return;
|
||||
// Synchronously compute visible rows to ensure cache hits during draw
|
||||
int start = std::max(0, first_row);
|
||||
int end = start + row_count - 1;
|
||||
int max_rows = static_cast<int>(buf.Nrows());
|
||||
if (start >= max_rows) return;
|
||||
if (end >= max_rows) end = max_rows - 1;
|
||||
|
||||
for (int r = start; r <= end; ++r) {
|
||||
(void)GetLine(buf, r, buf_version);
|
||||
}
|
||||
|
||||
// Enqueue background warm-around
|
||||
int warm_start = std::max(0, start - warm_margin);
|
||||
int warm_end = std::min(max_rows - 1, end + warm_margin);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
pending_.buf = &buf;
|
||||
pending_.version = buf_version;
|
||||
pending_.start_row = warm_start;
|
||||
pending_.end_row = warm_end;
|
||||
has_request_ = true;
|
||||
}
|
||||
ensure_worker_started();
|
||||
cv_.notify_one();
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,76 +0,0 @@
|
||||
// HighlighterEngine.h - caching layer for per-line highlights
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
#include "Highlight.h"
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
|
||||
class HighlighterEngine {
|
||||
public:
|
||||
HighlighterEngine();
|
||||
~HighlighterEngine();
|
||||
|
||||
void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl);
|
||||
|
||||
// Retrieve highlights for a given line and buffer version.
|
||||
// If cache is stale, recompute using the current highlighter.
|
||||
const LineHighlight &GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
|
||||
|
||||
// Invalidate cached lines from row (inclusive)
|
||||
void InvalidateFrom(int row);
|
||||
|
||||
bool HasHighlighter() const { return static_cast<bool>(hl_); }
|
||||
|
||||
// Phase 3: viewport-first prefetch and background warming
|
||||
// Compute only the visible range now, and enqueue a background warm-around task.
|
||||
// warm_margin: how many extra lines above/below to warm in the background.
|
||||
void PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version, int warm_margin = 200) const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<LanguageHighlighter> hl_;
|
||||
// Simple cache by row index (mutable to allow caching in const GetLine)
|
||||
mutable std::unordered_map<int, LineHighlight> cache_;
|
||||
// For stateful highlighters, remember per-line state (state after finishing that row)
|
||||
struct StateEntry {
|
||||
std::uint64_t version{0};
|
||||
// Using the interface type; forward-declare via header
|
||||
StatefulHighlighter::LineState state;
|
||||
};
|
||||
mutable std::unordered_map<int, StateEntry> state_cache_;
|
||||
|
||||
// Track best known contiguous state row for a given version to avoid O(n) scans
|
||||
mutable std::unordered_map<std::uint64_t, int> state_last_contig_;
|
||||
|
||||
// Thread-safety for caches and background worker state
|
||||
mutable std::mutex mtx_;
|
||||
|
||||
// Background warmer
|
||||
struct WarmRequest {
|
||||
const Buffer *buf{nullptr};
|
||||
std::uint64_t version{0};
|
||||
int start_row{0};
|
||||
int end_row{0}; // inclusive
|
||||
};
|
||||
mutable std::condition_variable cv_;
|
||||
mutable std::thread worker_;
|
||||
mutable std::atomic<bool> worker_running_{false};
|
||||
mutable bool has_request_{false};
|
||||
mutable WarmRequest pending_{};
|
||||
|
||||
void ensure_worker_started() const;
|
||||
void worker_loop() const;
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,157 +0,0 @@
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "CppHighlighter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
#include <cctype>
|
||||
|
||||
// Forward declare simple highlighters implemented in this project
|
||||
namespace kte {
|
||||
|
||||
// Registration storage
|
||||
struct RegEntry {
|
||||
std::string ft; // normalized
|
||||
HighlighterRegistry::Factory factory;
|
||||
};
|
||||
|
||||
static std::vector<RegEntry> ®istry() {
|
||||
static std::vector<RegEntry> reg;
|
||||
return reg;
|
||||
}
|
||||
class JSONHighlighter; class MarkdownHighlighter; class ShellHighlighter;
|
||||
class GoHighlighter; class PythonHighlighter; class RustHighlighter; class LispHighlighter;
|
||||
}
|
||||
|
||||
// Headers for the above
|
||||
#include "JsonHighlighter.h"
|
||||
#include "MarkdownHighlighter.h"
|
||||
#include "ShellHighlighter.h"
|
||||
#include "GoHighlighter.h"
|
||||
#include "PythonHighlighter.h"
|
||||
#include "RustHighlighter.h"
|
||||
#include "LispHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
static std::string to_lower(std::string_view s) {
|
||||
std::string r(s);
|
||||
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
||||
return r;
|
||||
}
|
||||
|
||||
std::string HighlighterRegistry::Normalize(std::string_view ft)
|
||||
{
|
||||
std::string f = to_lower(ft);
|
||||
if (f == "c" || f == "c++" || f == "cc" || f == "hpp" || f == "hh" || f == "h" || f == "cxx") return "cpp";
|
||||
if (f == "cpp") return "cpp";
|
||||
if (f == "json") return "json";
|
||||
if (f == "markdown" || f == "md" || f == "mkd" || f == "mdown") return "markdown";
|
||||
if (f == "shell" || f == "sh" || f == "bash" || f == "zsh" || f == "ksh" || f == "fish") return "shell";
|
||||
if (f == "go" || f == "golang") return "go";
|
||||
if (f == "py" || f == "python") return "python";
|
||||
if (f == "rs" || f == "rust") return "rust";
|
||||
if (f == "lisp" || f == "scheme" || f == "scm" || f == "rkt" || f == "el" || f == "clj" || f == "cljc" || f == "cl") return "lisp";
|
||||
return f;
|
||||
}
|
||||
|
||||
std::unique_ptr<LanguageHighlighter> HighlighterRegistry::CreateFor(std::string_view filetype)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
// Prefer externally registered factories
|
||||
for (const auto &e : registry()) {
|
||||
if (e.ft == ft && e.factory) return e.factory();
|
||||
}
|
||||
if (ft == "cpp") return std::make_unique<CppHighlighter>();
|
||||
if (ft == "json") return std::make_unique<JSONHighlighter>();
|
||||
if (ft == "markdown") return std::make_unique<MarkdownHighlighter>();
|
||||
if (ft == "shell") return std::make_unique<ShellHighlighter>();
|
||||
if (ft == "go") return std::make_unique<GoHighlighter>();
|
||||
if (ft == "python") return std::make_unique<PythonHighlighter>();
|
||||
if (ft == "rust") return std::make_unique<RustHighlighter>();
|
||||
if (ft == "lisp") return std::make_unique<LispHighlighter>();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static std::string shebang_to_ft(std::string_view first_line) {
|
||||
if (first_line.size() < 2 || first_line.substr(0,2) != "#!") return "";
|
||||
std::string low = to_lower(first_line);
|
||||
if (low.find("python") != std::string::npos) return "python";
|
||||
if (low.find("bash") != std::string::npos) return "shell";
|
||||
if (low.find("sh") != std::string::npos) return "shell";
|
||||
if (low.find("zsh") != std::string::npos) return "shell";
|
||||
if (low.find("fish") != std::string::npos) return "shell";
|
||||
if (low.find("scheme") != std::string::npos || low.find("racket") != std::string::npos || low.find("guile") != std::string::npos) return "lisp";
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string HighlighterRegistry::DetectForPath(std::string_view path, std::string_view first_line)
|
||||
{
|
||||
// Extension
|
||||
std::string p(path);
|
||||
std::error_code ec;
|
||||
std::string ext = std::filesystem::path(p).extension().string();
|
||||
for (auto &ch: ext) ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (!ext.empty()) {
|
||||
if (ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".cxx" || ext == ".h" || ext == ".hpp" || ext == ".hh") return "cpp";
|
||||
if (ext == ".json") return "json";
|
||||
if (ext == ".md" || ext == ".markdown" || ext == ".mkd") return "markdown";
|
||||
if (ext == ".sh" || ext == ".bash" || ext == ".zsh" || ext == ".ksh" || ext == ".fish") return "shell";
|
||||
if (ext == ".go") return "go";
|
||||
if (ext == ".py") return "python";
|
||||
if (ext == ".rs") return "rust";
|
||||
if (ext == ".lisp" || ext == ".scm" || ext == ".rkt" || ext == ".el" || ext == ".clj" || ext == ".cljc" || ext == ".cl") return "lisp";
|
||||
}
|
||||
// Shebang
|
||||
std::string ft = shebang_to_ft(first_line);
|
||||
return ft;
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
|
||||
// Extensibility API implementations
|
||||
namespace kte {
|
||||
|
||||
void HighlighterRegistry::Register(std::string_view filetype, Factory factory, bool override_existing)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
for (auto &e : registry()) {
|
||||
if (e.ft == ft) {
|
||||
if (override_existing) e.factory = std::move(factory);
|
||||
return;
|
||||
}
|
||||
}
|
||||
registry().push_back(RegEntry{ft, std::move(factory)});
|
||||
}
|
||||
|
||||
bool HighlighterRegistry::IsRegistered(std::string_view filetype)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
for (const auto &e : registry()) if (e.ft == ft) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> HighlighterRegistry::RegisteredFiletypes()
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
out.reserve(registry().size());
|
||||
for (const auto &e : registry()) out.push_back(e.ft);
|
||||
return out;
|
||||
}
|
||||
|
||||
#ifdef KTE_ENABLE_TREESITTER
|
||||
// Forward declare adapter factory
|
||||
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char* filetype,
|
||||
const void* (*get_lang)());
|
||||
|
||||
void HighlighterRegistry::RegisterTreeSitter(std::string_view filetype,
|
||||
const TSLanguage* (*get_language)())
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
Register(ft, [ft, get_language]() {
|
||||
return CreateTreeSitterHighlighter(ft.c_str(), reinterpret_cast<const void* (*)()>(get_language));
|
||||
}, /*override_existing=*/true);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,49 +0,0 @@
|
||||
// HighlighterRegistry.h - create/detect language highlighters and allow external registration
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
class HighlighterRegistry {
|
||||
public:
|
||||
using Factory = std::function<std::unique_ptr<LanguageHighlighter>()>;
|
||||
|
||||
// Create a highlighter for normalized filetype id (e.g., "cpp", "json", "markdown", "shell", "go", "python", "rust", "lisp").
|
||||
static std::unique_ptr<LanguageHighlighter> CreateFor(std::string_view filetype);
|
||||
|
||||
// Detect filetype by path extension and shebang (first line).
|
||||
// Returns normalized id or empty string if unknown.
|
||||
static std::string DetectForPath(std::string_view path, std::string_view first_line);
|
||||
|
||||
// Normalize various aliases/extensions to canonical ids.
|
||||
static std::string Normalize(std::string_view ft);
|
||||
|
||||
// Extensibility: allow external code to register highlighters at runtime.
|
||||
// The filetype key is normalized via Normalize(). If a factory is already registered for the
|
||||
// normalized key and override=false, the existing factory is kept.
|
||||
static void Register(std::string_view filetype, Factory factory, bool override_existing = true);
|
||||
|
||||
// Returns true if a factory is registered for the (normalized) filetype.
|
||||
static bool IsRegistered(std::string_view filetype);
|
||||
|
||||
// Return a list of currently registered (normalized) filetypes. Primarily for diagnostics/tests.
|
||||
static std::vector<std::string> RegisteredFiletypes();
|
||||
|
||||
#ifdef KTE_ENABLE_TREESITTER
|
||||
// Forward declaration to avoid hard dependency when disabled.
|
||||
struct TSLanguage;
|
||||
// Convenience: register a Tree-sitter-backed highlighter for a filetype.
|
||||
// The getter should return a non-null language pointer for the grammar.
|
||||
static void RegisterTreeSitter(std::string_view filetype,
|
||||
const TSLanguage* (*get_language)());
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,42 +0,0 @@
|
||||
#include "JsonHighlighter.h"
|
||||
#include "Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
|
||||
static bool is_digit(char c) { return c >= '0' && c <= '9'; }
|
||||
|
||||
void JSONHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
auto push = [&](int a, int b, TokenKind k){ if (b> a) out.push_back({a,b,k}); };
|
||||
|
||||
int i = 0;
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') { int j=i+1; while (j<n && (s[j]==' '||s[j]=='\t')) ++j; push(i,j,TokenKind::Whitespace); i=j; continue; }
|
||||
if (c == '"') {
|
||||
int j = i+1; bool esc=false; while (j < n) { char d = s[j++]; if (esc) { esc=false; continue; } if (d == '\\') { esc=true; continue; } if (d == '"') break; }
|
||||
push(i, j, TokenKind::String); i = j; continue;
|
||||
}
|
||||
if (is_digit(c) || (c=='-' && i+1<n && is_digit(s[i+1]))) {
|
||||
int j=i+1; while (j<n && (std::isdigit(static_cast<unsigned char>(s[j]))||s[j]=='.'||s[j]=='e'||s[j]=='E'||s[j]=='+'||s[j]=='-'||s[j]=='_')) ++j; push(i,j,TokenKind::Number); i=j; continue;
|
||||
}
|
||||
// booleans/null
|
||||
if (std::isalpha(static_cast<unsigned char>(c))) {
|
||||
int j=i+1; while (j<n && std::isalpha(static_cast<unsigned char>(s[j]))) ++j;
|
||||
std::string id = s.substr(i, j-i);
|
||||
if (id == "true" || id == "false" || id == "null") push(i,j,TokenKind::Constant); else push(i,j,TokenKind::Identifier);
|
||||
i=j; continue;
|
||||
}
|
||||
// punctuation
|
||||
if (c=='{'||c=='}'||c=='['||c==']'||c==','||c==':' ) { push(i,i+1,TokenKind::Punctuation); ++i; continue; }
|
||||
// fallback
|
||||
push(i,i+1,TokenKind::Default); ++i;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,43 +0,0 @@
|
||||
// LanguageHighlighter.h - interface for line-based highlighters
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "Highlight.h"
|
||||
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
|
||||
class LanguageHighlighter {
|
||||
public:
|
||||
virtual ~LanguageHighlighter() = default;
|
||||
// Produce highlight spans for a given buffer row. Implementations should append to out.
|
||||
virtual void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const = 0;
|
||||
virtual bool Stateful() const { return false; }
|
||||
};
|
||||
|
||||
// Optional extension for stateful highlighters (e.g., multi-line comments/strings).
|
||||
// Engines may detect and use this via dynamic_cast without breaking stateless impls.
|
||||
class StatefulHighlighter : public LanguageHighlighter {
|
||||
public:
|
||||
struct LineState {
|
||||
bool in_block_comment{false};
|
||||
bool in_raw_string{false};
|
||||
// For raw strings, remember the delimiter between the opening R"delim( and closing )delim"
|
||||
std::string raw_delim;
|
||||
};
|
||||
|
||||
// Highlight one line given the previous line state; return the resulting state after this line.
|
||||
// Implementations should append spans for this line to out and compute the next state.
|
||||
virtual LineState HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const = 0;
|
||||
|
||||
bool Stateful() const override { return true; }
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,41 +0,0 @@
|
||||
#include "LispHighlighter.h"
|
||||
#include "Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
|
||||
static void push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
|
||||
|
||||
LispHighlighter::LispHighlighter()
|
||||
{
|
||||
const char* kw[] = {"defun","lambda","let","let*","define","set!","if","cond","begin","quote","quasiquote","unquote","unquote-splicing","loop","do","and","or","not"};
|
||||
for (auto s: kw) kws_.insert(s);
|
||||
}
|
||||
|
||||
void LispHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
int bol = 0; while (bol<n && (s[bol]==' '||s[bol]=='\t')) ++bol;
|
||||
if (bol < n && s[bol] == ';') { push(out, bol, n, TokenKind::Comment); if (bol>0) push(out,0,bol,TokenKind::Whitespace); return; }
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c==' '||c=='\t') { int j=i+1; while (j<n && (s[j]==' '||s[j]=='\t')) ++j; push(out,i,j,TokenKind::Whitespace); i=j; continue; }
|
||||
if (c==';') { push(out,i,n,TokenKind::Comment); break; }
|
||||
if (c=='"') { int j=i+1; bool esc=false; while (j<n){ char d=s[j++]; if (esc){esc=false; continue;} if (d=='\\'){esc=true; continue;} if (d=='"') break; } push(out,i,j,TokenKind::String); i=j; continue; }
|
||||
if (std::isalpha(static_cast<unsigned char>(c)) || c=='*' || c=='-' || c=='+' || c=='/' || c=='_' ) {
|
||||
int j=i+1; while (j<n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j]=='*' || s[j]=='-' || s[j]=='+' || s[j]=='/' || s[j]=='_' || s[j]=='!')) ++j;
|
||||
std::string id=s.substr(i,j-i);
|
||||
TokenKind k = kws_.count(id) ? TokenKind::Keyword : TokenKind::Identifier;
|
||||
push(out,i,j,k); i=j; continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) { int j=i+1; while (j<n && (std::isdigit(static_cast<unsigned char>(s[j]))||s[j]=='.')) ++j; push(out,i,j,TokenKind::Number); i=j; continue; }
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) { TokenKind k=TokenKind::Punctuation; push(out,i,i+1,k); ++i; continue; }
|
||||
push(out,i,i+1,TokenKind::Default); ++i;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,88 +0,0 @@
|
||||
#include "MarkdownHighlighter.h"
|
||||
#include "Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
|
||||
static void push_span(std::vector<HighlightSpan> &out, int a, int b, TokenKind k) {
|
||||
if (b > a) out.push_back({a,b,k});
|
||||
}
|
||||
|
||||
void MarkdownHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
LineState st; // not used in stateless entry
|
||||
(void)HighlightLineStateful(buf, row, st, out);
|
||||
}
|
||||
|
||||
StatefulHighlighter::LineState MarkdownHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
StatefulHighlighter::LineState state = prev;
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return state;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
|
||||
// Reuse in_block_comment flag as "in fenced code" state.
|
||||
if (state.in_block_comment) {
|
||||
// If line contains closing fence ``` then close after it
|
||||
auto pos = s.find("```");
|
||||
if (pos == std::string::npos) {
|
||||
push_span(out, 0, n, TokenKind::String);
|
||||
state.in_block_comment = true;
|
||||
return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + 3);
|
||||
push_span(out, 0, end, TokenKind::String);
|
||||
// rest of line processed normally after fence
|
||||
int i = end;
|
||||
// whitespace
|
||||
if (i < n) push_span(out, i, n, TokenKind::Default);
|
||||
state.in_block_comment = false;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect fenced code block start at beginning (allow leading spaces)
|
||||
int bol = 0; while (bol < n && (s[bol]==' '||s[bol]=='\t')) ++bol;
|
||||
if (bol + 3 <= n && s.compare(bol, 3, "```") == 0) {
|
||||
push_span(out, bol, n, TokenKind::String);
|
||||
state.in_block_comment = true; // enter fenced mode
|
||||
return state;
|
||||
}
|
||||
|
||||
// Headings: lines starting with 1-6 '#'
|
||||
if (bol < n && s[bol] == '#') {
|
||||
int j = bol; while (j < n && s[j] == '#') ++j; // hashes
|
||||
// include following space and text as Keyword to stand out
|
||||
push_span(out, bol, n, TokenKind::Keyword);
|
||||
return state;
|
||||
}
|
||||
|
||||
// Process inline: emphasis and code spans
|
||||
int i = 0;
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == '`') {
|
||||
int j = i + 1; while (j < n && s[j] != '`') ++j; if (j < n) ++j;
|
||||
push_span(out, i, j, TokenKind::String); i = j; continue;
|
||||
}
|
||||
if (c == '*' || c == '_') {
|
||||
// bold/italic markers: treat the marker and until next same marker as Type to highlight
|
||||
char m = c; int j = i + 1; while (j < n && s[j] != m) ++j; if (j < n) ++j;
|
||||
push_span(out, i, j, TokenKind::Type); i = j; continue;
|
||||
}
|
||||
// links []() minimal: treat [text](url) as Function
|
||||
if (c == '[') {
|
||||
int j = i + 1; while (j < n && s[j] != ']') ++j; if (j < n) ++j; // include ]
|
||||
if (j < n && s[j] == '(') { while (j < n && s[j] != ')') ++j; if (j < n) ++j; }
|
||||
push_span(out, i, j, TokenKind::Function); i = j; continue;
|
||||
}
|
||||
// whitespace
|
||||
if (c == ' ' || c == '\t') { int j=i+1; while (j<n && (s[j]==' '||s[j]=='\t')) ++j; push_span(out, i, j, TokenKind::Whitespace); i=j; continue; }
|
||||
// fallback: default single char
|
||||
push_span(out, i, i+1, TokenKind::Default); ++i;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,14 +0,0 @@
|
||||
// MarkdownHighlighter.h - simple Markdown highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
class MarkdownHighlighter final : public StatefulHighlighter {
|
||||
public:
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev, std::vector<HighlightSpan> &out) const override;
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,16 +0,0 @@
|
||||
#include "NullHighlighter.h"
|
||||
#include "Buffer.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
void NullHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
if (n <= 0) return;
|
||||
out.push_back({0, n, TokenKind::Default});
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,85 +0,0 @@
|
||||
#include "PythonHighlighter.h"
|
||||
#include "Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
|
||||
static void push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
|
||||
static bool is_ident_start(char c){ return std::isalpha(static_cast<unsigned char>(c)) || c=='_'; }
|
||||
static bool is_ident_char(char c){ return std::isalnum(static_cast<unsigned char>(c)) || c=='_'; }
|
||||
|
||||
PythonHighlighter::PythonHighlighter()
|
||||
{
|
||||
const char* kw[] = {"and","as","assert","break","class","continue","def","del","elif","else","except","False","finally","for","from","global","if","import","in","is","lambda","None","nonlocal","not","or","pass","raise","return","True","try","while","with","yield"};
|
||||
for (auto s: kw) kws_.insert(s);
|
||||
}
|
||||
|
||||
void PythonHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
LineState st; (void)HighlightLineStateful(buf, row, st, out);
|
||||
}
|
||||
|
||||
StatefulHighlighter::LineState PythonHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
StatefulHighlighter::LineState state = prev;
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return state;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
|
||||
// Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
|
||||
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
|
||||
auto pos = s.find(state.raw_delim);
|
||||
if (pos == std::string::npos) {
|
||||
push(out, 0, n, TokenKind::String);
|
||||
return state; // still inside
|
||||
} else {
|
||||
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
|
||||
push(out, 0, end, TokenKind::String);
|
||||
// remainder processed normally
|
||||
s = s.substr(end);
|
||||
n = static_cast<int>(s.size());
|
||||
state.in_raw_string = false; state.raw_delim.clear();
|
||||
// Continue parsing remainder as a separate small loop
|
||||
int base = end; // original offset, but we already emitted to 'out' with base=0; following spans should be from 'end'
|
||||
// For simplicity, mark rest as Default
|
||||
if (n>0) push(out, base, base + n, TokenKind::Default);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
// Detect comment start '#', ignoring inside strings
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c==' '||c=='\t') { int j=i+1; while (j<n && (s[j]==' '||s[j]=='\t')) ++j; push(out,i,j,TokenKind::Whitespace); i=j; continue; }
|
||||
if (c=='#') { push(out,i,n,TokenKind::Comment); break; }
|
||||
// Strings: triple quotes and single-line
|
||||
if (c=='"' || c=='\'') {
|
||||
char q=c;
|
||||
// triple?
|
||||
if (i+2 < n && s[i+1]==q && s[i+2]==q) {
|
||||
std::string delim(3, q);
|
||||
int j = i+3; // search for closing triple
|
||||
auto pos = s.find(delim, static_cast<std::size_t>(j));
|
||||
if (pos == std::string::npos) {
|
||||
push(out,i,n,TokenKind::String);
|
||||
state.in_raw_string = true; state.raw_delim = delim; return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + 3);
|
||||
push(out,i,end,TokenKind::String); i=end; continue;
|
||||
}
|
||||
} else {
|
||||
int j=i+1; bool esc=false; while (j<n) { char d=s[j++]; if (esc){esc=false; continue;} if (d=='\\'){esc=true; continue;} if (d==q) break; }
|
||||
push(out,i,j,TokenKind::String); i=j; continue;
|
||||
}
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) { int j=i+1; while (j<n && (std::isalnum(static_cast<unsigned char>(s[j]))||s[j]=='.'||s[j]=='_' )) ++j; push(out,i,j,TokenKind::Number); i=j; continue; }
|
||||
if (is_ident_start(c)) { int j=i+1; while (j<n && is_ident_char(s[j])) ++j; std::string id=s.substr(i,j-i); TokenKind k=TokenKind::Identifier; if (kws_.count(id)) k=TokenKind::Keyword; push(out,i,j,k); i=j; continue; }
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) { TokenKind k=TokenKind::Operator; if (c==':'||c==','||c=='('||c==')'||c=='['||c==']') k=TokenKind::Punctuation; push(out,i,i+1,k); ++i; continue; }
|
||||
push(out,i,i+1,TokenKind::Default); ++i;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,18 +0,0 @@
|
||||
// PythonHighlighter.h - simple Python highlighter with triple-quote state
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
|
||||
class PythonHighlighter final : public StatefulHighlighter {
|
||||
public:
|
||||
PythonHighlighter();
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev, std::vector<HighlightSpan> &out) const override;
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,39 +0,0 @@
|
||||
#include "RustHighlighter.h"
|
||||
#include "Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
|
||||
static void push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
|
||||
static bool is_ident_start(char c){ return std::isalpha(static_cast<unsigned char>(c)) || c=='_'; }
|
||||
static bool is_ident_char(char c){ return std::isalnum(static_cast<unsigned char>(c)) || c=='_'; }
|
||||
|
||||
RustHighlighter::RustHighlighter()
|
||||
{
|
||||
const char* kw[] = {"as","break","const","continue","crate","else","enum","extern","false","fn","for","if","impl","in","let","loop","match","mod","move","mut","pub","ref","return","self","Self","static","struct","super","trait","true","type","unsafe","use","where","while","dyn","async","await","try"};
|
||||
for (auto s: kw) kws_.insert(s);
|
||||
const char* tp[] = {"u8","u16","u32","u64","u128","usize","i8","i16","i32","i64","i128","isize","f32","f64","bool","char","str"};
|
||||
for (auto s: tp) types_.insert(s);
|
||||
}
|
||||
|
||||
void RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c==' '||c=='\t') { int j=i+1; while (j<n && (s[j]==' '||s[j]=='\t')) ++j; push(out,i,j,TokenKind::Whitespace); i=j; continue; }
|
||||
if (c=='/' && i+1<n && s[i+1]=='/') { push(out,i,n,TokenKind::Comment); break; }
|
||||
if (c=='/' && i+1<n && s[i+1]=='*') { int j=i+2; bool closed=false; while (j+1<=n) { if (j+1<n && s[j]=='*' && s[j+1]=='/') { j+=2; closed=true; break; } ++j; } if (!closed) { push(out,i,n,TokenKind::Comment); break; } else { push(out,i,j,TokenKind::Comment); i=j; continue; } }
|
||||
if (c=='"') { int j=i+1; bool esc=false; while (j<n){ char d=s[j++]; if (esc){esc=false; continue;} if (d=='\\'){esc=true; continue;} if (d=='"') break; } push(out,i,j,TokenKind::String); i=j; continue; }
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) { int j=i+1; while (j<n && (std::isalnum(static_cast<unsigned char>(s[j]))||s[j]=='.'||s[j]=='_' )) ++j; push(out,i,j,TokenKind::Number); i=j; continue; }
|
||||
if (is_ident_start(c)) { int j=i+1; while (j<n && is_ident_char(s[j])) ++j; std::string id=s.substr(i,j-i); TokenKind k=TokenKind::Identifier; if (kws_.count(id)) k=TokenKind::Keyword; else if (types_.count(id)) k=TokenKind::Type; push(out,i,j,k); i=j; continue; }
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) { TokenKind k=TokenKind::Operator; if (c==';'||c==','||c=='('||c==')'||c=='{'||c=='}'||c=='['||c==']') k=TokenKind::Punctuation; push(out,i,i+1,k); ++i; continue; }
|
||||
push(out,i,i+1,TokenKind::Default); ++i;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,18 +0,0 @@
|
||||
// RustHighlighter.h - simple Rust highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
|
||||
class RustHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
RustHighlighter();
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
std::unordered_set<std::string> types_;
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,43 +0,0 @@
|
||||
#include "ShellHighlighter.h"
|
||||
#include "Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
|
||||
static void push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k){ if (b>a) out.push_back({a,b,k}); }
|
||||
|
||||
void ShellHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size()) return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
// if first non-space is '#', whole line is comment
|
||||
int bol = 0; while (bol < n && (s[bol]==' '||s[bol]=='\t')) ++bol;
|
||||
if (bol < n && s[bol] == '#') { push(out, bol, n, TokenKind::Comment); if (bol>0) push(out,0,bol,TokenKind::Whitespace); return; }
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') { int j=i+1; while (j<n && (s[j]==' '||s[j]=='\t')) ++j; push(out,i,j,TokenKind::Whitespace); i=j; continue; }
|
||||
if (c == '#') { push(out, i, n, TokenKind::Comment); break; }
|
||||
if (c == '\'' || c == '"') {
|
||||
char q = c; int j = i+1; bool esc=false; while (j<n) { char d=s[j++]; if (q=='"') { if (esc) {esc=false; continue;} if (d=='\\'){esc=true; continue;} if (d=='"') break; } else { if (d=='\'') break; } }
|
||||
push(out,i,j,TokenKind::String); i=j; continue;
|
||||
}
|
||||
// simple keywords
|
||||
if (std::isalpha(static_cast<unsigned char>(c))) {
|
||||
int j=i+1; while (j<n && (std::isalnum(static_cast<unsigned char>(s[j]))||s[j]=='_')) ++j; std::string id=s.substr(i,j-i);
|
||||
static const char* kws[] = {"if","then","fi","for","in","do","done","case","esac","while","function","elif","else"};
|
||||
bool kw=false; for (auto k: kws) if (id==k) { kw=true; break; }
|
||||
push(out,i,j, kw?TokenKind::Keyword:TokenKind::Identifier); i=j; continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c=='('||c==')'||c=='{'||c=='}'||c==','||c==';') k=TokenKind::Punctuation;
|
||||
push(out,i,i+1,k); ++i; continue;
|
||||
}
|
||||
push(out,i,i+1,TokenKind::Default); ++i;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
@@ -1,6 +1,12 @@
|
||||
#include <ncurses.h>
|
||||
#include <clocale>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
#ifdef __APPLE__
|
||||
#include <xlocale.h>
|
||||
#endif
|
||||
#include <langinfo.h>
|
||||
#include <cctype>
|
||||
|
||||
#include "TerminalFrontend.h"
|
||||
#include "Command.h"
|
||||
@@ -10,6 +16,35 @@
|
||||
bool
|
||||
TerminalFrontend::Init(Editor &ed)
|
||||
{
|
||||
// Enable UTF-8 locale so ncurses and the terminal handle multibyte correctly
|
||||
// This relies on the user's environment (e.g., LANG/LC_ALL) being set to a UTF-8 locale.
|
||||
// If not set, try a couple of common UTF-8 fallbacks.
|
||||
const char *loc = std::setlocale(LC_ALL, "");
|
||||
auto is_utf8_codeset = []() -> bool {
|
||||
const char *cs = nl_langinfo(CODESET);
|
||||
if (!cs)
|
||||
return false;
|
||||
std::string s(cs);
|
||||
for (auto &ch: s)
|
||||
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
|
||||
return (s.find("UTF-8") != std::string::npos) || (s.find("UTF8") != std::string::npos);
|
||||
};
|
||||
bool utf8_ok = (MB_CUR_MAX > 1) && is_utf8_codeset();
|
||||
if (!utf8_ok) {
|
||||
// Try common UTF-8 locales
|
||||
loc = std::setlocale(LC_CTYPE, "C.UTF-8");
|
||||
utf8_ok = (loc != nullptr) && (MB_CUR_MAX > 1) && is_utf8_codeset();
|
||||
if (!utf8_ok) {
|
||||
loc = std::setlocale(LC_CTYPE, "en_US.UTF-8");
|
||||
utf8_ok = (loc != nullptr) && (MB_CUR_MAX > 1) && is_utf8_codeset();
|
||||
}
|
||||
if (!utf8_ok) {
|
||||
// macOS often uses plain "UTF-8" locale identifier
|
||||
loc = std::setlocale(LC_CTYPE, "UTF-8");
|
||||
utf8_ok = (loc != nullptr) && (MB_CUR_MAX > 1) && is_utf8_codeset();
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure Control keys reach the app: disable XON/XOFF and dsusp/susp bindings (e.g., ^S/^Q, ^Y on macOS)
|
||||
{
|
||||
struct termios tio{};
|
||||
@@ -55,6 +90,9 @@ TerminalFrontend::Init(Editor &ed)
|
||||
prev_r_ = r;
|
||||
prev_c_ = c;
|
||||
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
|
||||
// Inform renderer of UTF-8 capability so it can choose proper output path
|
||||
renderer_.SetUtf8Enabled(utf8_ok);
|
||||
input_.SetUtf8Enabled(utf8_ok);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -100,4 +138,4 @@ TerminalFrontend::Shutdown()
|
||||
have_orig_tio_ = false;
|
||||
}
|
||||
endwin();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <cstdio>
|
||||
#include <cwchar>
|
||||
#include <climits>
|
||||
#include <ncurses.h>
|
||||
|
||||
#include "TerminalInputHandler.h"
|
||||
@@ -36,18 +38,48 @@ map_key_to_command(const int ch,
|
||||
MEVENT ev{};
|
||||
if (getmouse(&ev) == OK) {
|
||||
// Mouse wheel → map to MoveUp/MoveDown one line per wheel notch
|
||||
unsigned long wheel_up_mask = 0;
|
||||
unsigned long wheel_dn_mask = 0;
|
||||
#ifdef BUTTON4_PRESSED
|
||||
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
|
||||
out = {true, CommandId::MoveUp, "", 0};
|
||||
return true;
|
||||
}
|
||||
wheel_up_mask |= BUTTON4_PRESSED;
|
||||
#endif
|
||||
#ifdef BUTTON4_RELEASED
|
||||
wheel_up_mask |= BUTTON4_RELEASED;
|
||||
#endif
|
||||
#ifdef BUTTON4_CLICKED
|
||||
wheel_up_mask |= BUTTON4_CLICKED;
|
||||
#endif
|
||||
#ifdef BUTTON4_DOUBLE_CLICKED
|
||||
wheel_up_mask |= BUTTON4_DOUBLE_CLICKED;
|
||||
#endif
|
||||
#ifdef BUTTON4_TRIPLE_CLICKED
|
||||
wheel_up_mask |= BUTTON4_TRIPLE_CLICKED;
|
||||
#endif
|
||||
#ifdef BUTTON5_PRESSED
|
||||
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
|
||||
out = {true, CommandId::MoveDown, "", 0};
|
||||
wheel_dn_mask |= BUTTON5_PRESSED;
|
||||
#endif
|
||||
#ifdef BUTTON5_RELEASED
|
||||
wheel_dn_mask |= BUTTON5_RELEASED;
|
||||
#endif
|
||||
#ifdef BUTTON5_CLICKED
|
||||
wheel_dn_mask |= BUTTON5_CLICKED;
|
||||
#endif
|
||||
#ifdef BUTTON5_DOUBLE_CLICKED
|
||||
wheel_dn_mask |= BUTTON5_DOUBLE_CLICKED;
|
||||
#endif
|
||||
#ifdef BUTTON5_TRIPLE_CLICKED
|
||||
wheel_dn_mask |= BUTTON5_TRIPLE_CLICKED;
|
||||
#endif
|
||||
if (wheel_up_mask && (ev.bstate & wheel_up_mask)) {
|
||||
// Prefer viewport scrolling for wheel: page up
|
||||
out = {true, CommandId::PageUp, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (wheel_dn_mask && (ev.bstate & wheel_dn_mask)) {
|
||||
// Prefer viewport scrolling for wheel: page down
|
||||
out = {true, CommandId::PageDown, "", 0};
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
// React to left button click/press
|
||||
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
|
||||
char buf[64];
|
||||
@@ -281,6 +313,77 @@ map_key_to_command(const int ch,
|
||||
bool
|
||||
TerminalInputHandler::decode_(MappedInput &out)
|
||||
{
|
||||
#if defined(KTE_HAVE_GET_WCH)
|
||||
if (utf8_enabled_) {
|
||||
// Prefer wide-character input so we can capture Unicode code points
|
||||
wint_t wch = 0;
|
||||
int rc = get_wch(&wch);
|
||||
if (rc == ERR) {
|
||||
return false;
|
||||
}
|
||||
if (rc == KEY_CODE_YES) {
|
||||
// Function/special key; pass through existing mapper
|
||||
int sk = static_cast<int>(wch);
|
||||
bool consumed = map_key_to_command(
|
||||
sk,
|
||||
k_prefix_, esc_meta_,
|
||||
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_,
|
||||
uarg_text_,
|
||||
out);
|
||||
if (!consumed)
|
||||
return false;
|
||||
} else {
|
||||
// Regular character
|
||||
if (wch <= 0x7F) {
|
||||
// ASCII path -> reuse existing mapping (handles control, ESC, etc.)
|
||||
int ch = static_cast<int>(wch);
|
||||
bool consumed = map_key_to_command(
|
||||
ch,
|
||||
k_prefix_, esc_meta_,
|
||||
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_,
|
||||
uarg_text_,
|
||||
out);
|
||||
if (!consumed)
|
||||
return false;
|
||||
} else {
|
||||
// Non-ASCII printable -> insert UTF-8 text directly
|
||||
if (iswcntrl(static_cast<wint_t>(wch))) {
|
||||
out.hasCommand = false;
|
||||
return true;
|
||||
}
|
||||
char mb[MB_LEN_MAX];
|
||||
mbstate_t st{};
|
||||
std::size_t n = wcrtomb(mb, static_cast<wchar_t>(wch), &st);
|
||||
if (n == static_cast<std::size_t>(-1)) {
|
||||
// Fallback placeholder if encoding failed
|
||||
out.hasCommand = true;
|
||||
out.id = CommandId::InsertText;
|
||||
out.arg = "?";
|
||||
out.count = 0;
|
||||
} else {
|
||||
out.hasCommand = true;
|
||||
out.id = CommandId::InsertText;
|
||||
out.arg.assign(mb, mb + n);
|
||||
out.count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int ch = getch();
|
||||
if (ch == ERR) {
|
||||
return false; // no input
|
||||
}
|
||||
bool consumed = map_key_to_command(
|
||||
ch,
|
||||
k_prefix_, esc_meta_,
|
||||
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_, uarg_text_,
|
||||
out);
|
||||
if (!consumed)
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
// Wide-character input not available in this curses; fall back to byte-wise getch
|
||||
(void) utf8_enabled_;
|
||||
int ch = getch();
|
||||
if (ch == ERR) {
|
||||
return false; // no input
|
||||
@@ -292,6 +395,7 @@ TerminalInputHandler::decode_(MappedInput &out)
|
||||
out);
|
||||
if (!consumed)
|
||||
return false;
|
||||
#endif
|
||||
// If a command was produced and a universal argument is active, attach it and clear state
|
||||
if (out.hasCommand && uarg_active_ && out.id != CommandId::UArgStatus) {
|
||||
int count = 0;
|
||||
@@ -320,4 +424,4 @@ TerminalInputHandler::Poll(MappedInput &out)
|
||||
{
|
||||
out = {};
|
||||
return decode_(out) && out.hasCommand;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,12 @@ public:
|
||||
|
||||
bool Poll(MappedInput &out) override;
|
||||
|
||||
|
||||
void SetUtf8Enabled(bool on)
|
||||
{
|
||||
utf8_enabled_ = on;
|
||||
}
|
||||
|
||||
private:
|
||||
bool decode_(MappedInput &out);
|
||||
|
||||
@@ -30,6 +36,8 @@ private:
|
||||
bool uarg_had_digits_ = false; // whether any digits were supplied
|
||||
int uarg_value_ = 0; // current absolute value (>=0)
|
||||
std::string uarg_text_; // raw digits/minus typed for status display
|
||||
|
||||
bool utf8_enabled_ = true;
|
||||
};
|
||||
|
||||
#endif // KTE_TERMINAL_INPUT_HANDLER_H
|
||||
#endif // KTE_TERMINAL_INPUT_HANDLER_H
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdlib>
|
||||
#include <ncurses.h>
|
||||
#include <regex>
|
||||
#include <cwchar>
|
||||
#include <string>
|
||||
|
||||
#include "TerminalRenderer.h"
|
||||
@@ -42,18 +43,18 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
std::size_t coloffs = buf->Coloffs();
|
||||
|
||||
const int tabw = 8;
|
||||
// Phase 3: prefetch visible viewport highlights (current terminal area)
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
int fr = static_cast<int>(rowoffs);
|
||||
int rc = std::max(0, content_rows);
|
||||
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
||||
}
|
||||
// Phase 3: prefetch visible viewport highlights (current terminal area)
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
int fr = static_cast<int>(rowoffs);
|
||||
int rc = std::max(0, content_rows);
|
||||
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
||||
}
|
||||
|
||||
for (int r = 0; r < content_rows; ++r) {
|
||||
move(r, 0);
|
||||
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
||||
std::size_t render_col = 0;
|
||||
std::size_t src_i = 0;
|
||||
for (int r = 0; r < content_rows; ++r) {
|
||||
move(r, 0);
|
||||
std::size_t li = rowoffs + static_cast<std::size_t>(r);
|
||||
std::size_t render_col = 0;
|
||||
std::size_t src_i = 0;
|
||||
// Compute matches for this line if search highlighting is active
|
||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||
std::vector<std::pair<std::size_t, std::size_t> > ranges; // [start, end)
|
||||
@@ -105,49 +106,55 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
bool hl_on = false;
|
||||
bool cur_on = false;
|
||||
int written = 0;
|
||||
if (li < lines.size()) {
|
||||
std::string line = static_cast<std::string>(lines[li]);
|
||||
src_i = 0;
|
||||
render_col = 0;
|
||||
// Syntax highlighting: fetch per-line spans
|
||||
const kte::LineHighlight *lh_ptr = nullptr;
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||
lh_ptr = &buf->Highlighter()->GetLine(*buf, static_cast<int>(li), buf->Version());
|
||||
}
|
||||
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
|
||||
if (!lh_ptr) return kte::TokenKind::Default;
|
||||
for (const auto &sp: lh_ptr->spans) {
|
||||
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(src_index) < sp.col_end)
|
||||
return sp.kind;
|
||||
}
|
||||
return kte::TokenKind::Default;
|
||||
};
|
||||
auto apply_token_attr = [&](kte::TokenKind k) {
|
||||
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
|
||||
attrset(A_NORMAL);
|
||||
switch (k) {
|
||||
case kte::TokenKind::Keyword:
|
||||
case kte::TokenKind::Type:
|
||||
case kte::TokenKind::Constant:
|
||||
case kte::TokenKind::Function:
|
||||
attron(A_BOLD);
|
||||
break;
|
||||
case kte::TokenKind::Comment:
|
||||
attron(A_DIM);
|
||||
break;
|
||||
case kte::TokenKind::String:
|
||||
case kte::TokenKind::Char:
|
||||
case kte::TokenKind::Number:
|
||||
// standout a bit using A_UNDERLINE if available
|
||||
attron(A_UNDERLINE);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
while (written < cols) {
|
||||
char ch = ' ';
|
||||
bool from_src = false;
|
||||
if (li < lines.size()) {
|
||||
std::string line = static_cast<std::string>(lines[li]);
|
||||
src_i = 0;
|
||||
render_col = 0;
|
||||
// Syntax highlighting: fetch per-line spans
|
||||
const kte::LineHighlight *lh_ptr = nullptr;
|
||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
|
||||
HasHighlighter()) {
|
||||
lh_ptr = &buf->Highlighter()->GetLine(
|
||||
*buf, static_cast<int>(li), buf->Version());
|
||||
}
|
||||
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
|
||||
if (!lh_ptr)
|
||||
return kte::TokenKind::Default;
|
||||
for (const auto &sp: lh_ptr->spans) {
|
||||
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(
|
||||
src_index) < sp.col_end)
|
||||
return sp.kind;
|
||||
}
|
||||
return kte::TokenKind::Default;
|
||||
};
|
||||
auto apply_token_attr = [&](kte::TokenKind k) {
|
||||
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
|
||||
attrset(A_NORMAL);
|
||||
switch (k) {
|
||||
case kte::TokenKind::Keyword:
|
||||
case kte::TokenKind::Type:
|
||||
case kte::TokenKind::Constant:
|
||||
case kte::TokenKind::Function:
|
||||
attron(A_BOLD);
|
||||
break;
|
||||
case kte::TokenKind::Comment:
|
||||
attron(A_DIM);
|
||||
break;
|
||||
case kte::TokenKind::String:
|
||||
case kte::TokenKind::Char:
|
||||
case kte::TokenKind::Number:
|
||||
// standout a bit using A_UNDERLINE if available
|
||||
attron(A_UNDERLINE);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
while (written < cols) {
|
||||
// Default to space when beyond EOL
|
||||
bool from_src = false;
|
||||
int wcw = 1; // display width
|
||||
std::size_t advance_bytes = 0;
|
||||
if (src_i < line.size()) {
|
||||
unsigned char c = static_cast<unsigned char>(line[src_i]);
|
||||
if (c == '\t') {
|
||||
@@ -166,102 +173,139 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
next_tab -= to_skip;
|
||||
}
|
||||
// Now render visible spaces
|
||||
while (next_tab > 0 && written < cols) {
|
||||
bool in_hl = search_mode && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && src_i >= cur_mx
|
||||
&& src_i < cur_mend;
|
||||
// Toggle highlight attributes
|
||||
int attr = 0;
|
||||
if (in_hl)
|
||||
attr |= A_STANDOUT;
|
||||
if (in_cur)
|
||||
attr |= A_BOLD;
|
||||
if ((attr & A_STANDOUT) && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!(attr & A_STANDOUT) && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if ((attr & A_BOLD) && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!(attr & A_BOLD) && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
// Apply syntax attribute only if not in search highlight
|
||||
if (!in_hl) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(' ');
|
||||
++written;
|
||||
++render_col;
|
||||
--next_tab;
|
||||
}
|
||||
++src_i;
|
||||
continue;
|
||||
} else {
|
||||
// normal char
|
||||
if (render_col < coloffs) {
|
||||
while (next_tab > 0 && written < cols) {
|
||||
bool in_hl = search_mode && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && src_i >= cur_mx
|
||||
&& src_i < cur_mend;
|
||||
// Toggle highlight attributes
|
||||
int attr = 0;
|
||||
if (in_hl)
|
||||
attr |= A_STANDOUT;
|
||||
if (in_cur)
|
||||
attr |= A_BOLD;
|
||||
if ((attr & A_STANDOUT) && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!(attr & A_STANDOUT) && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if ((attr & A_BOLD) && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!(attr & A_BOLD) && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
// Apply syntax attribute only if not in search highlight
|
||||
if (!in_hl) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(' ');
|
||||
++written;
|
||||
++render_col;
|
||||
++src_i;
|
||||
continue;
|
||||
--next_tab;
|
||||
}
|
||||
++src_i;
|
||||
continue;
|
||||
} else {
|
||||
if (!Utf8Enabled()) {
|
||||
// ASCII fallback: treat each byte as single width
|
||||
if (render_col + 1 <= coloffs) {
|
||||
++render_col;
|
||||
++src_i;
|
||||
continue;
|
||||
}
|
||||
wcw = 1;
|
||||
advance_bytes = 1;
|
||||
from_src = true;
|
||||
} else {
|
||||
// Decode one UTF-8 codepoint
|
||||
mbstate_t st{};
|
||||
const char *p = line.data() + src_i;
|
||||
std::size_t rem = line.size() - src_i;
|
||||
wchar_t tmp_wc = 0;
|
||||
std::size_t n = mbrtowc(&tmp_wc, p, rem, &st);
|
||||
if (n == static_cast<std::size_t>(-1) || n ==
|
||||
static_cast<std::size_t>(-2) || n == 0) {
|
||||
// Invalid/incomplete -> treat as single-byte placeholder
|
||||
tmp_wc = L'?';
|
||||
n = 1;
|
||||
}
|
||||
int w = wcwidth(tmp_wc);
|
||||
if (w < 0)
|
||||
w = 1;
|
||||
// If this codepoint is scrolled off to the left, skip it
|
||||
if (render_col + static_cast<std::size_t>(w) <=
|
||||
coloffs) {
|
||||
render_col += static_cast<std::size_t>(w);
|
||||
src_i += n;
|
||||
continue;
|
||||
}
|
||||
wcw = w;
|
||||
advance_bytes = n;
|
||||
from_src = true;
|
||||
}
|
||||
ch = static_cast<char>(c);
|
||||
from_src = true;
|
||||
}
|
||||
} else {
|
||||
// beyond EOL, fill spaces
|
||||
ch = ' ';
|
||||
from_src = false;
|
||||
}
|
||||
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
|
||||
cur_mend;
|
||||
if (in_hl && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!in_hl && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (in_cur && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!in_cur && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
if (!in_hl && from_src) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
addch(static_cast<unsigned char>(ch));
|
||||
++written;
|
||||
++render_col;
|
||||
if (from_src)
|
||||
++src_i;
|
||||
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
||||
bool in_cur =
|
||||
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
|
||||
cur_mend;
|
||||
if (in_hl && !hl_on) {
|
||||
attron(A_STANDOUT);
|
||||
hl_on = true;
|
||||
}
|
||||
if (!in_hl && hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (in_cur && !cur_on) {
|
||||
attron(A_BOLD);
|
||||
cur_on = true;
|
||||
}
|
||||
if (!in_cur && cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
if (!in_hl && from_src) {
|
||||
apply_token_attr(token_at(src_i));
|
||||
}
|
||||
if (written + wcw > cols) {
|
||||
break;
|
||||
}
|
||||
if (from_src) {
|
||||
// Output original bytes for this unit (UTF-8 codepoint or ASCII byte)
|
||||
const char *cp = line.data() + (src_i);
|
||||
int out_n = Utf8Enabled() ? static_cast<int>(advance_bytes) : 1;
|
||||
addnstr(cp, out_n);
|
||||
src_i += static_cast<std::size_t>(out_n);
|
||||
} else {
|
||||
addch(' ');
|
||||
}
|
||||
written += wcw;
|
||||
render_col += wcw;
|
||||
if (src_i >= line.size() && written >= cols)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
attrset(A_NORMAL);
|
||||
clrtoeol();
|
||||
}
|
||||
if (hl_on) {
|
||||
attroff(A_STANDOUT);
|
||||
hl_on = false;
|
||||
}
|
||||
if (cur_on) {
|
||||
attroff(A_BOLD);
|
||||
cur_on = false;
|
||||
}
|
||||
attrset(A_NORMAL);
|
||||
clrtoeol();
|
||||
}
|
||||
|
||||
// Place terminal cursor at logical position accounting for tabs and coloffs
|
||||
std::size_t cy = buf->Cury();
|
||||
@@ -418,6 +462,10 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
else
|
||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
||||
right = rbuf;
|
||||
// If UTF-8 is not enabled (ASCII fallback), append a short hint
|
||||
if (!Utf8Enabled()) {
|
||||
right += " | ASCII";
|
||||
}
|
||||
}
|
||||
|
||||
// Compute placements with truncation rules: prioritize left and right; middle gets remaining
|
||||
@@ -464,4 +512,4 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,21 @@ public:
|
||||
~TerminalRenderer() override;
|
||||
|
||||
void Draw(Editor &ed) override;
|
||||
|
||||
// Enable/disable UTF-8 aware rendering (set by TerminalFrontend after locale init)
|
||||
void SetUtf8Enabled(bool on)
|
||||
{
|
||||
utf8_enabled_ = on;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] bool Utf8Enabled() const
|
||||
{
|
||||
return utf8_enabled_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool utf8_enabled_ = true;
|
||||
};
|
||||
|
||||
#endif // KTE_TERMINAL_RENDERER_H
|
||||
#endif // KTE_TERMINAL_RENDERER_H
|
||||
@@ -1,46 +0,0 @@
|
||||
#include "TreeSitterHighlighter.h"
|
||||
|
||||
#ifdef KTE_ENABLE_TREESITTER
|
||||
|
||||
#include "Buffer.h"
|
||||
#include <utility>
|
||||
|
||||
namespace kte {
|
||||
|
||||
TreeSitterHighlighter::TreeSitterHighlighter(const TSLanguage* lang, std::string filetype)
|
||||
: language_(lang), filetype_(std::move(filetype))
|
||||
{
|
||||
}
|
||||
|
||||
TreeSitterHighlighter::~TreeSitterHighlighter()
|
||||
{
|
||||
disposeParser();
|
||||
}
|
||||
|
||||
void TreeSitterHighlighter::ensureParsed(const Buffer& /*buf*/) const
|
||||
{
|
||||
// Intentionally a stub to avoid pulling the Tree-sitter API and library by default.
|
||||
// In future, when linking against tree-sitter, initialize parser_, set language_,
|
||||
// and build tree_ from the buffer contents.
|
||||
}
|
||||
|
||||
void TreeSitterHighlighter::disposeParser() const
|
||||
{
|
||||
// Stub; nothing to dispose when not actually creating parser/tree
|
||||
}
|
||||
|
||||
void TreeSitterHighlighter::HighlightLine(const Buffer &/*buf*/, int /*row*/, std::vector<HighlightSpan> &/*out*/) const
|
||||
{
|
||||
// For now, no-op. When tree-sitter is wired, map nodes to TokenKind spans per line.
|
||||
}
|
||||
|
||||
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char* filetype,
|
||||
const void* (*get_lang)())
|
||||
{
|
||||
const auto* lang = reinterpret_cast<const TSLanguage*>(get_lang ? get_lang() : nullptr);
|
||||
return std::make_unique<TreeSitterHighlighter>(lang, filetype ? std::string(filetype) : std::string());
|
||||
}
|
||||
|
||||
} // namespace kte
|
||||
|
||||
#endif // KTE_ENABLE_TREESITTER
|
||||
25626
ext/json.h
Normal file
25626
ext/json.h
Normal file
File diff suppressed because it is too large
Load Diff
185
ext/json_fwd.h
Normal file
185
ext/json_fwd.h
Normal file
@@ -0,0 +1,185 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2025 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_
|
||||
#define INCLUDE_NLOHMANN_JSON_FWD_HPP_
|
||||
|
||||
#include <cstdint> // int64_t, uint64_t
|
||||
#include <map> // map
|
||||
#include <memory> // allocator
|
||||
#include <string> // string
|
||||
#include <vector> // vector
|
||||
|
||||
// #include <nlohmann/detail/abi_macros.hpp>
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2025 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
// This file contains all macro definitions affecting or depending on the ABI
|
||||
|
||||
#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK
|
||||
#if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH)
|
||||
#if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0
|
||||
#warning "Already included a different version of the library!"
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum)
|
||||
#define NLOHMANN_JSON_VERSION_MINOR 12 // NOLINT(modernize-macro-to-enum)
|
||||
#define NLOHMANN_JSON_VERSION_PATCH 0 // NOLINT(modernize-macro-to-enum)
|
||||
|
||||
#ifndef JSON_DIAGNOSTICS
|
||||
#define JSON_DIAGNOSTICS 0
|
||||
#endif
|
||||
|
||||
#ifndef JSON_DIAGNOSTIC_POSITIONS
|
||||
#define JSON_DIAGNOSTIC_POSITIONS 0
|
||||
#endif
|
||||
|
||||
#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
#define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0
|
||||
#endif
|
||||
|
||||
#if JSON_DIAGNOSTICS
|
||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS
|
||||
#endif
|
||||
|
||||
#if JSON_DIAGNOSTIC_POSITIONS
|
||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS
|
||||
#endif
|
||||
|
||||
#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp
|
||||
#else
|
||||
#define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON
|
||||
#endif
|
||||
|
||||
#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION
|
||||
#define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0
|
||||
#endif
|
||||
|
||||
// Construct the namespace ABI tags component
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c
|
||||
#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c)
|
||||
|
||||
#define NLOHMANN_JSON_ABI_TAGS \
|
||||
NLOHMANN_JSON_ABI_TAGS_CONCAT( \
|
||||
NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \
|
||||
NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \
|
||||
NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS)
|
||||
|
||||
// Construct the namespace version component
|
||||
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \
|
||||
_v ## major ## _ ## minor ## _ ## patch
|
||||
#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \
|
||||
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch)
|
||||
|
||||
#if NLOHMANN_JSON_NAMESPACE_NO_VERSION
|
||||
#define NLOHMANN_JSON_NAMESPACE_VERSION
|
||||
#else
|
||||
#define NLOHMANN_JSON_NAMESPACE_VERSION \
|
||||
NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \
|
||||
NLOHMANN_JSON_VERSION_MINOR, \
|
||||
NLOHMANN_JSON_VERSION_PATCH)
|
||||
#endif
|
||||
|
||||
// Combine namespace components
|
||||
#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b
|
||||
#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \
|
||||
NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b)
|
||||
|
||||
#ifndef NLOHMANN_JSON_NAMESPACE
|
||||
#define NLOHMANN_JSON_NAMESPACE \
|
||||
nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \
|
||||
NLOHMANN_JSON_ABI_TAGS, \
|
||||
NLOHMANN_JSON_NAMESPACE_VERSION)
|
||||
#endif
|
||||
|
||||
#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
#define NLOHMANN_JSON_NAMESPACE_BEGIN \
|
||||
namespace nlohmann \
|
||||
{ \
|
||||
inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \
|
||||
NLOHMANN_JSON_ABI_TAGS, \
|
||||
NLOHMANN_JSON_NAMESPACE_VERSION) \
|
||||
{
|
||||
#endif
|
||||
|
||||
#ifndef NLOHMANN_JSON_NAMESPACE_END
|
||||
#define NLOHMANN_JSON_NAMESPACE_END \
|
||||
} /* namespace (inline namespace) NOLINT(readability/namespace) */ \
|
||||
} // namespace nlohmann
|
||||
#endif
|
||||
|
||||
|
||||
/*!
|
||||
@brief namespace for Niels Lohmann
|
||||
@see https://github.com/nlohmann
|
||||
@since version 1.0.0
|
||||
*/
|
||||
NLOHMANN_JSON_NAMESPACE_BEGIN
|
||||
/*!
|
||||
@brief default JSONSerializer template argument
|
||||
|
||||
This serializer ignores the template arguments and uses ADL
|
||||
([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl))
|
||||
for serialization.
|
||||
*/
|
||||
template<typename T = void, typename SFINAE = void>
|
||||
struct adl_serializer;
|
||||
|
||||
/// a class to store JSON values
|
||||
/// @sa https://json.nlohmann.me/api/basic_json/
|
||||
template<template<typename U, typename V, typename... Args> class ObjectType =
|
||||
std::map,
|
||||
template<typename U, typename... Args> class ArrayType = std::vector,
|
||||
class StringType = std::string, class BooleanType = bool,
|
||||
class NumberIntegerType = std::int64_t,
|
||||
class NumberUnsignedType = std::uint64_t,
|
||||
class NumberFloatType = double,
|
||||
template<typename U> class AllocatorType = std::allocator,
|
||||
template<typename T, typename SFINAE = void> class JSONSerializer =
|
||||
adl_serializer,
|
||||
class BinaryType = std::vector<std::uint8_t>, // cppcheck-suppress syntaxError
|
||||
class CustomBaseClass = void>
|
||||
class basic_json;
|
||||
|
||||
/// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document
|
||||
/// @sa https://json.nlohmann.me/api/json_pointer/
|
||||
template<typename RefStringType>
|
||||
class json_pointer;
|
||||
|
||||
/*!
|
||||
@brief default specialization
|
||||
@sa https://json.nlohmann.me/api/json/
|
||||
*/
|
||||
using json = basic_json<>;
|
||||
|
||||
/// @brief a minimal map-like container that preserves insertion order
|
||||
/// @sa https://json.nlohmann.me/api/ordered_map/
|
||||
template<class Key, class T, class IgnoredLess, class Allocator>
|
||||
struct ordered_map;
|
||||
|
||||
/// @brief specialization that maintains the insertion order of object keys
|
||||
/// @sa https://json.nlohmann.me/api/ordered_json/
|
||||
using ordered_json = basic_json<nlohmann::ordered_map>;
|
||||
|
||||
NLOHMANN_JSON_NAMESPACE_END
|
||||
|
||||
#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_
|
||||
2
kte-cloc
2
kte-cloc
@@ -6,4 +6,4 @@ then
|
||||
fmt_args="-fmt 3"
|
||||
fi
|
||||
|
||||
ls -1 *.cc *.h | grep -v '^Font.h$' | xargs cloc ${fmt_args}
|
||||
ls -1 *.cc *.h lsp/*.{cc,h} | grep -v '^Font.h$' | xargs cloc ${fmt_args}
|
||||
|
||||
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
|
||||
147
lsp/JsonRpcTransport.cc
Normal file
147
lsp/JsonRpcTransport.cc
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* JsonRpcTransport.cc - minimal stdio JSON-RPC framing (Content-Length)
|
||||
*/
|
||||
#include "JsonRpcTransport.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace kte::lsp {
|
||||
void
|
||||
JsonRpcTransport::connect(int inFd, int outFd)
|
||||
{
|
||||
inFd_ = inFd;
|
||||
outFd_ = outFd;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
JsonRpcTransport::send(const std::string &/*method*/, const std::string &payload)
|
||||
{
|
||||
if (outFd_ < 0)
|
||||
return;
|
||||
const std::string header = "Content-Length: " + std::to_string(payload.size()) + "\r\n\r\n";
|
||||
std::lock_guard<std::mutex> lk(writeMutex_);
|
||||
// write header
|
||||
const char *hbuf = header.data();
|
||||
size_t hleft = header.size();
|
||||
while (hleft > 0) {
|
||||
ssize_t n = ::write(outFd_, hbuf, hleft);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
hbuf += static_cast<size_t>(n);
|
||||
hleft -= static_cast<size_t>(n);
|
||||
}
|
||||
// write payload
|
||||
const char *pbuf = payload.data();
|
||||
size_t pleft = payload.size();
|
||||
while (pleft > 0) {
|
||||
ssize_t n = ::write(outFd_, pbuf, pleft);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
pbuf += static_cast<size_t>(n);
|
||||
pleft -= static_cast<size_t>(n);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
readLineCrlf(int fd, std::string &out, size_t maxLen)
|
||||
{
|
||||
out.clear();
|
||||
char ch;
|
||||
while (true) {
|
||||
ssize_t n = ::read(fd, &ch, 1);
|
||||
if (n == 0)
|
||||
return false; // EOF
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
out.push_back(ch);
|
||||
// Handle CRLF or bare LF as end-of-line
|
||||
if ((out.size() >= 2 && out[out.size() - 2] == '\r' && out[out.size() - 1] == '\n') ||
|
||||
(out.size() >= 1 && out[out.size() - 1] == '\n')) {
|
||||
return true;
|
||||
}
|
||||
if (out.size() > maxLen) {
|
||||
// sanity cap
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::optional<JsonRpcMessage>
|
||||
JsonRpcTransport::read()
|
||||
{
|
||||
if (inFd_ < 0)
|
||||
return std::nullopt;
|
||||
// Parse headers (case-insensitive), accept/ignore extras
|
||||
size_t contentLength = 0;
|
||||
while (true) {
|
||||
std::string line;
|
||||
if (!readLineCrlf(inFd_, line, kMaxHeaderLine))
|
||||
return std::nullopt;
|
||||
// Normalize end-of-line handling: consider blank line as end of headers
|
||||
if (line == "\r\n" || line == "\n" || line == "\r")
|
||||
break;
|
||||
// Trim trailing CRLF
|
||||
if (!line.empty() && (line.back() == '\n' || line.back() == '\r')) {
|
||||
while (!line.empty() && (line.back() == '\n' || line.back() == '\r'))
|
||||
line.pop_back();
|
||||
}
|
||||
// Find colon
|
||||
auto pos = line.find(':');
|
||||
if (pos == std::string::npos)
|
||||
continue;
|
||||
std::string name = line.substr(0, pos);
|
||||
std::string value = line.substr(pos + 1);
|
||||
// trim leading spaces in value
|
||||
size_t i = 0;
|
||||
while (i < value.size() && (value[i] == ' ' || value[i] == '\t'))
|
||||
++i;
|
||||
value.erase(0, i);
|
||||
// lower-case name for comparison
|
||||
for (auto &c: name)
|
||||
c = static_cast<char>(::tolower(static_cast<unsigned char>(c)));
|
||||
if (name == "content-length") {
|
||||
size_t len = static_cast<size_t>(std::strtoull(value.c_str(), nullptr, 10));
|
||||
if (len > kMaxBody) {
|
||||
return std::nullopt; // drop too-large message
|
||||
}
|
||||
contentLength = len;
|
||||
}
|
||||
// else: ignore other headers
|
||||
}
|
||||
if (contentLength == 0)
|
||||
return std::nullopt;
|
||||
std::string body;
|
||||
body.resize(contentLength);
|
||||
size_t readTotal = 0;
|
||||
while (readTotal < contentLength) {
|
||||
ssize_t n = ::read(inFd_, &body[readTotal], contentLength - readTotal);
|
||||
if (n == 0)
|
||||
return std::nullopt;
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return std::nullopt;
|
||||
}
|
||||
readTotal += static_cast<size_t>(n);
|
||||
}
|
||||
return JsonRpcMessage{std::move(body)};
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
43
lsp/JsonRpcTransport.h
Normal file
43
lsp/JsonRpcTransport.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* JsonRpcTransport.h - minimal JSON-RPC over stdio transport
|
||||
*/
|
||||
#ifndef KTE_JSON_RPC_TRANSPORT_H
|
||||
#define KTE_JSON_RPC_TRANSPORT_H
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
|
||||
namespace kte::lsp {
|
||||
struct JsonRpcMessage {
|
||||
std::string raw; // raw JSON payload (stub)
|
||||
};
|
||||
|
||||
class JsonRpcTransport {
|
||||
public:
|
||||
JsonRpcTransport() = default;
|
||||
|
||||
~JsonRpcTransport() = default;
|
||||
|
||||
// Connect this transport to file descriptors (read from inFd, write to outFd)
|
||||
void connect(int inFd, int outFd);
|
||||
|
||||
// Send a method call (request or notification)
|
||||
// 'payload' should be a complete JSON object string to send as the message body.
|
||||
void send(const std::string &method, const std::string &payload);
|
||||
|
||||
// Blocking read next message; returns nullopt on EOF or error
|
||||
std::optional<JsonRpcMessage> read();
|
||||
|
||||
private:
|
||||
int inFd_ = -1;
|
||||
int outFd_ = -1;
|
||||
std::mutex writeMutex_;
|
||||
|
||||
// Limits to keep the transport resilient
|
||||
static constexpr size_t kMaxHeaderLine = 16 * 1024; // 16 KiB per header line
|
||||
static constexpr size_t kMaxBody = 64ull * 1024ull * 1024ull; // 64 MiB body cap
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_JSON_RPC_TRANSPORT_H
|
||||
75
lsp/LspClient.h
Normal file
75
lsp/LspClient.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* LspClient.h - Core LSP client abstraction (initial stub)
|
||||
*/
|
||||
#ifndef KTE_LSP_CLIENT_H
|
||||
#define KTE_LSP_CLIENT_H
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "LspTypes.h"
|
||||
#include "Diagnostic.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
// Callback types for initial language features
|
||||
// If error is non-empty, the result may be default-constructed/empty
|
||||
using CompletionCallback = std::function<void(const CompletionList & result, const std::string & error)>;
|
||||
using HoverCallback = std::function<void(const HoverResult & result, const std::string & error)>;
|
||||
using LocationCallback = std::function<void(const std::vector<Location> & result, const std::string & error)>;
|
||||
|
||||
class LspClient {
|
||||
public:
|
||||
virtual ~LspClient() = default;
|
||||
|
||||
// Lifecycle
|
||||
virtual bool initialize(const std::string &rootPath) = 0;
|
||||
|
||||
virtual void shutdown() = 0;
|
||||
|
||||
// Document Synchronization
|
||||
virtual void didOpen(const std::string &uri, const std::string &languageId,
|
||||
int version, const std::string &text) = 0;
|
||||
|
||||
virtual void didChange(const std::string &uri, int version,
|
||||
const std::vector<TextDocumentContentChangeEvent> &changes) = 0;
|
||||
|
||||
virtual void didClose(const std::string &uri) = 0;
|
||||
|
||||
virtual void didSave(const std::string &uri) = 0;
|
||||
|
||||
// Language Features (initial)
|
||||
virtual void completion(const std::string &, Position,
|
||||
CompletionCallback) {}
|
||||
|
||||
|
||||
virtual void hover(const std::string &, Position,
|
||||
HoverCallback) {}
|
||||
|
||||
|
||||
virtual void definition(const std::string &, Position,
|
||||
LocationCallback) {}
|
||||
|
||||
|
||||
// Process Management
|
||||
virtual bool isRunning() const = 0;
|
||||
|
||||
virtual std::string getServerName() const = 0;
|
||||
|
||||
// Handlers (optional; set by manager)
|
||||
using DiagnosticsHandler = std::function<void(const std::string & uri,
|
||||
const std::vector<Diagnostic> &diagnostics
|
||||
)
|
||||
>;
|
||||
|
||||
|
||||
virtual void setDiagnosticsHandler(DiagnosticsHandler h)
|
||||
{
|
||||
(void) h;
|
||||
}
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_CLIENT_H
|
||||
736
lsp/LspManager.cc
Normal file
736
lsp/LspManager.cc
Normal file
@@ -0,0 +1,736 @@
|
||||
/*
|
||||
* LspManager.cc - central coordination of LSP servers and diagnostics
|
||||
*/
|
||||
|
||||
#include "LspManager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <utility>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <cstdarg>
|
||||
|
||||
#include "../Buffer.h"
|
||||
#include "../Editor.h"
|
||||
#include "BufferChangeTracker.h"
|
||||
#include "LspProcessClient.h"
|
||||
#include "UtfCodec.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace kte::lsp {
|
||||
static void
|
||||
lsp_debug_file(const char *fmt, ...)
|
||||
{
|
||||
FILE *f = std::fopen("/tmp/kte-lsp.log", "a");
|
||||
if (!f)
|
||||
return;
|
||||
// prepend timestamp
|
||||
std::time_t t = std::time(nullptr);
|
||||
char ts[32];
|
||||
std::strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
|
||||
std::fprintf(f, "[%s] ", ts);
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
std::vfprintf(f, fmt, ap);
|
||||
va_end(ap);
|
||||
std::fputc('\n', f);
|
||||
std::fclose(f);
|
||||
}
|
||||
|
||||
|
||||
LspManager::LspManager(Editor *editor, DiagnosticDisplay *display)
|
||||
: editor_(editor), display_(display)
|
||||
{
|
||||
// Pre-populate with sensible default server configs
|
||||
registerDefaultServers();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::registerServer(const std::string &languageId, const LspServerConfig &config)
|
||||
{
|
||||
serverConfigs_[languageId] = config;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspManager::startServerForBuffer(Buffer *buffer)
|
||||
{
|
||||
const auto lang = getLanguageId(buffer);
|
||||
if (lang.empty())
|
||||
return false;
|
||||
|
||||
if (servers_.find(lang) != servers_.end() && servers_[lang]->isRunning()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto it = serverConfigs_.find(lang);
|
||||
if (it == serverConfigs_.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto &cfg = it->second;
|
||||
// Respect autostart for automatic starts on buffer open
|
||||
if (!cfg.autostart) {
|
||||
return false;
|
||||
}
|
||||
// Allow env override of server path
|
||||
std::string command = cfg.command;
|
||||
if (lang == "cpp") {
|
||||
if (const char *p = std::getenv("KTE_LSP_CLANGD"); p && *p)
|
||||
command = p;
|
||||
} else if (lang == "go") {
|
||||
if (const char *p = std::getenv("KTE_LSP_GOPLS"); p && *p)
|
||||
command = p;
|
||||
} else if (lang == "rust") {
|
||||
if (const char *p = std::getenv("KTE_LSP_RUST_ANALYZER"); p && *p)
|
||||
command = p;
|
||||
}
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] startServerForBuffer: lang=%s cmd=%s args=%zu file=%s\n",
|
||||
lang.c_str(), command.c_str(), cfg.args.size(), buffer->Filename().c_str());
|
||||
lsp_debug_file("startServerForBuffer: lang=%s cmd=%s args=%zu file=%s",
|
||||
lang.c_str(), command.c_str(), cfg.args.size(), buffer->Filename().c_str());
|
||||
}
|
||||
auto client = std::make_unique<LspProcessClient>(command, cfg.args);
|
||||
// Wire diagnostics handler to manager
|
||||
client->setDiagnosticsHandler([this](const std::string &uri, const std::vector<Diagnostic> &diags) {
|
||||
this->handleDiagnostics(uri, diags);
|
||||
});
|
||||
// Determine workspace root using rootPatterns if set; fallback to file's parent
|
||||
std::string rootPath;
|
||||
if (!buffer->Filename().empty()) {
|
||||
rootPath = detectWorkspaceRoot(buffer->Filename(), cfg);
|
||||
if (rootPath.empty()) {
|
||||
fs::path p(buffer->Filename());
|
||||
rootPath = p.has_parent_path() ? p.parent_path().string() : std::string{};
|
||||
}
|
||||
}
|
||||
if (debug_) {
|
||||
const char *pathEnv = std::getenv("PATH");
|
||||
std::fprintf(stderr, "[kte][lsp] initializing server: rootPath=%s PATH=%s\n",
|
||||
rootPath.c_str(), pathEnv ? pathEnv : "<null>");
|
||||
lsp_debug_file("initializing server: rootPath=%s PATH=%s",
|
||||
rootPath.c_str(), pathEnv ? pathEnv : "<null>");
|
||||
}
|
||||
if (!client->initialize(rootPath)) {
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] initialize failed for lang=%s\n", lang.c_str());
|
||||
lsp_debug_file("initialize failed for lang=%s", lang.c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
servers_[lang] = std::move(client);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::stopServer(const std::string &languageId)
|
||||
{
|
||||
auto it = servers_.find(languageId);
|
||||
if (it != servers_.end()) {
|
||||
it->second->shutdown();
|
||||
servers_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::stopAllServers()
|
||||
{
|
||||
for (auto &kv: servers_) {
|
||||
kv.second->shutdown();
|
||||
}
|
||||
servers_.clear();
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspManager::startServerForLanguage(const std::string &languageId, const std::string &rootPath)
|
||||
{
|
||||
auto cfgIt = serverConfigs_.find(languageId);
|
||||
if (cfgIt == serverConfigs_.end())
|
||||
return false;
|
||||
|
||||
// If already running, nothing to do
|
||||
auto it = servers_.find(languageId);
|
||||
if (it != servers_.end() && it->second && it->second->isRunning()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto &cfg = cfgIt->second;
|
||||
std::string command = cfg.command;
|
||||
if (languageId == "cpp") {
|
||||
if (const char *p = std::getenv("KTE_LSP_CLANGD"); p && *p)
|
||||
command = p;
|
||||
} else if (languageId == "go") {
|
||||
if (const char *p = std::getenv("KTE_LSP_GOPLS"); p && *p)
|
||||
command = p;
|
||||
} else if (languageId == "rust") {
|
||||
if (const char *p = std::getenv("KTE_LSP_RUST_ANALYZER"); p && *p)
|
||||
command = p;
|
||||
}
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] startServerForLanguage: lang=%s cmd=%s args=%zu root=%s\n",
|
||||
languageId.c_str(), command.c_str(), cfg.args.size(), rootPath.c_str());
|
||||
lsp_debug_file("startServerForLanguage: lang=%s cmd=%s args=%zu root=%s",
|
||||
languageId.c_str(), command.c_str(), cfg.args.size(), rootPath.c_str());
|
||||
}
|
||||
auto client = std::make_unique<LspProcessClient>(command, cfg.args);
|
||||
client->setDiagnosticsHandler([this](const std::string &uri, const std::vector<Diagnostic> &diags) {
|
||||
this->handleDiagnostics(uri, diags);
|
||||
});
|
||||
std::string root = rootPath;
|
||||
if (!root.empty()) {
|
||||
// keep
|
||||
} else {
|
||||
// Try cwd if not provided
|
||||
root = std::string();
|
||||
}
|
||||
if (!client->initialize(root)) {
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] initialize failed for lang=%s\n", languageId.c_str());
|
||||
lsp_debug_file("initialize failed for lang=%s", languageId.c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
servers_[languageId] = std::move(client);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspManager::restartServer(const std::string &languageId, const std::string &rootPath)
|
||||
{
|
||||
stopServer(languageId);
|
||||
return startServerForLanguage(languageId, rootPath);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::onBufferOpened(Buffer *buffer)
|
||||
{
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] onBufferOpened: file=%s lang=%s\n",
|
||||
buffer->Filename().c_str(), getLanguageId(buffer).c_str());
|
||||
lsp_debug_file("onBufferOpened: file=%s lang=%s",
|
||||
buffer->Filename().c_str(), getLanguageId(buffer).c_str());
|
||||
}
|
||||
if (!startServerForBuffer(buffer)) {
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] onBufferOpened: server did not start\n");
|
||||
lsp_debug_file("onBufferOpened: server did not start");
|
||||
}
|
||||
return;
|
||||
}
|
||||
auto *client = ensureServerForLanguage(getLanguageId(buffer));
|
||||
if (!client)
|
||||
return;
|
||||
|
||||
const auto uri = getUri(buffer);
|
||||
const auto lang = getLanguageId(buffer);
|
||||
const int version = static_cast<int>(buffer->Version());
|
||||
const std::string text = buffer->FullText();
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] didOpen: uri=%s lang=%s version=%d bytes=%zu\n",
|
||||
uri.c_str(), lang.c_str(), version, text.size());
|
||||
lsp_debug_file("didOpen: uri=%s lang=%s version=%d bytes=%zu",
|
||||
uri.c_str(), lang.c_str(), version, text.size());
|
||||
}
|
||||
client->didOpen(uri, lang, version, text);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::onBufferChanged(Buffer *buffer)
|
||||
{
|
||||
auto *client = ensureServerForLanguage(getLanguageId(buffer));
|
||||
if (!client)
|
||||
return;
|
||||
const auto uri = getUri(buffer);
|
||||
int version = static_cast<int>(buffer->Version());
|
||||
|
||||
std::vector<TextDocumentContentChangeEvent> changes;
|
||||
if (auto *tracker = buffer->GetChangeTracker()) {
|
||||
changes = tracker->getChanges();
|
||||
tracker->clearChanges();
|
||||
version = tracker->getVersion();
|
||||
} else {
|
||||
// Fallback: full document change
|
||||
TextDocumentContentChangeEvent ev;
|
||||
ev.range.reset();
|
||||
ev.text = buffer->FullText();
|
||||
changes.push_back(std::move(ev));
|
||||
}
|
||||
|
||||
// Option A: convert ranges from UTF-8 (editor coords) -> UTF-16 (LSP wire)
|
||||
std::vector<TextDocumentContentChangeEvent> changes16;
|
||||
changes16.reserve(changes.size());
|
||||
// LineProvider that serves lines from this buffer by URI
|
||||
Buffer *bufForUri = buffer; // changes are for this buffer
|
||||
auto provider = [bufForUri](const std::string &/*u*/, int line) -> std::string_view {
|
||||
if (!bufForUri)
|
||||
return std::string_view();
|
||||
const auto &rows = bufForUri->Rows();
|
||||
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||
return std::string_view();
|
||||
// Materialize one line into a thread_local scratch; return view
|
||||
thread_local std::string scratch;
|
||||
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||
return std::string_view(scratch);
|
||||
};
|
||||
for (const auto &ch: changes) {
|
||||
TextDocumentContentChangeEvent out = ch;
|
||||
if (ch.range.has_value()) {
|
||||
Range r16 = toUtf16(uri, *ch.range, provider);
|
||||
if (debug_) {
|
||||
lsp_debug_file("didChange range convert: L%d C%d-%d -> L%d C%d-%d",
|
||||
ch.range->start.line, ch.range->start.character,
|
||||
ch.range->end.character,
|
||||
r16.start.line, r16.start.character, r16.end.character);
|
||||
}
|
||||
out.range = r16;
|
||||
}
|
||||
changes16.push_back(std::move(out));
|
||||
}
|
||||
client->didChange(uri, version, changes16);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::onBufferClosed(Buffer *buffer)
|
||||
{
|
||||
auto *client = ensureServerForLanguage(getLanguageId(buffer));
|
||||
if (!client)
|
||||
return;
|
||||
client->didClose(getUri(buffer));
|
||||
// Clear diagnostics for this file
|
||||
diagnosticStore_.clear(getUri(buffer));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::onBufferSaved(Buffer *buffer)
|
||||
{
|
||||
auto *client = ensureServerForLanguage(getLanguageId(buffer));
|
||||
if (!client)
|
||||
return;
|
||||
client->didSave(getUri(buffer));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::requestCompletion(Buffer *buffer, Position pos, CompletionCallback callback)
|
||||
{
|
||||
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
|
||||
const auto uri = getUri(buffer);
|
||||
// Convert position to UTF-16 using Option A provider
|
||||
auto provider = [buffer](const std::string &/*u*/, int line) -> std::string_view {
|
||||
if (!buffer)
|
||||
return std::string_view();
|
||||
const auto &rows = buffer->Rows();
|
||||
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||
return std::string_view();
|
||||
thread_local std::string scratch;
|
||||
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||
return std::string_view(scratch);
|
||||
};
|
||||
Position p16 = toUtf16(uri, pos, provider);
|
||||
if (debug_) {
|
||||
lsp_debug_file("completion pos convert: L%d C%d -> L%d C%d", pos.line, pos.character, p16.line,
|
||||
p16.character);
|
||||
}
|
||||
client->completion(uri, p16, std::move(callback));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::requestHover(Buffer *buffer, Position pos, HoverCallback callback)
|
||||
{
|
||||
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
|
||||
const auto uri = getUri(buffer);
|
||||
auto provider = [buffer](const std::string &/*u*/, int line) -> std::string_view {
|
||||
if (!buffer)
|
||||
return std::string_view();
|
||||
const auto &rows = buffer->Rows();
|
||||
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||
return std::string_view();
|
||||
thread_local std::string scratch;
|
||||
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||
return std::string_view(scratch);
|
||||
};
|
||||
Position p16 = toUtf16(uri, pos, provider);
|
||||
if (debug_) {
|
||||
lsp_debug_file("hover pos convert: L%d C%d -> L%d C%d", pos.line, pos.character, p16.line,
|
||||
p16.character);
|
||||
}
|
||||
// Wrap the callback to convert any returned range from UTF-16 (wire) -> UTF-8 (editor)
|
||||
HoverCallback wrapped = [this, uri, provider, cb = std::move(callback)](const HoverResult &res16,
|
||||
const std::string &err) {
|
||||
if (!cb)
|
||||
return;
|
||||
if (!res16.range.has_value()) {
|
||||
cb(res16, err);
|
||||
return;
|
||||
}
|
||||
HoverResult res8 = res16;
|
||||
res8.range = toUtf8(uri, *res16.range, provider);
|
||||
if (debug_) {
|
||||
const auto &r16 = *res16.range;
|
||||
const auto &r8 = *res8.range;
|
||||
lsp_debug_file("hover range convert: L%d %d-%d -> L%d %d-%d",
|
||||
r16.start.line, r16.start.character, r16.end.character,
|
||||
r8.start.line, r8.start.character, r8.end.character);
|
||||
}
|
||||
cb(res8, err);
|
||||
};
|
||||
client->hover(uri, p16, std::move(wrapped));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::requestDefinition(Buffer *buffer, Position pos, LocationCallback callback)
|
||||
{
|
||||
if (auto *client = ensureServerForLanguage(getLanguageId(buffer))) {
|
||||
const auto uri = getUri(buffer);
|
||||
auto provider = [buffer](const std::string &/*u*/, int line) -> std::string_view {
|
||||
if (!buffer)
|
||||
return std::string_view();
|
||||
const auto &rows = buffer->Rows();
|
||||
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||
return std::string_view();
|
||||
thread_local std::string scratch;
|
||||
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||
return std::string_view(scratch);
|
||||
};
|
||||
Position p16 = toUtf16(uri, pos, provider);
|
||||
if (debug_) {
|
||||
lsp_debug_file("definition pos convert: L%d C%d -> L%d C%d", pos.line, pos.character, p16.line,
|
||||
p16.character);
|
||||
}
|
||||
// Wrap callback to convert Location ranges from UTF-16 (wire) -> UTF-8 (editor)
|
||||
LocationCallback wrapped = [this, uri, provider, cb = std::move(callback)](
|
||||
const std::vector<Location> &locs16,
|
||||
const std::string &err) {
|
||||
if (!cb)
|
||||
return;
|
||||
std::vector<Location> locs8;
|
||||
locs8.reserve(locs16.size());
|
||||
for (const auto &l: locs16) {
|
||||
Location x = l;
|
||||
x.range = toUtf8(uri, l.range, provider);
|
||||
if (debug_) {
|
||||
lsp_debug_file("definition range convert: L%d %d-%d -> L%d %d-%d",
|
||||
l.range.start.line, l.range.start.character,
|
||||
l.range.end.character,
|
||||
x.range.start.line, x.range.start.character,
|
||||
x.range.end.character);
|
||||
}
|
||||
locs8.push_back(std::move(x));
|
||||
}
|
||||
cb(locs8, err);
|
||||
};
|
||||
client->definition(uri, p16, std::move(wrapped));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::handleDiagnostics(const std::string &uri, const std::vector<Diagnostic> &diagnostics)
|
||||
{
|
||||
// Convert incoming ranges from UTF-16 (wire) -> UTF-8 (editor)
|
||||
std::vector<Diagnostic> conv = diagnostics;
|
||||
Buffer *buf = findBufferByUri(uri);
|
||||
auto provider = [buf](const std::string &/*u*/, int line) -> std::string_view {
|
||||
if (!buf)
|
||||
return std::string_view();
|
||||
const auto &rows = buf->Rows();
|
||||
if (line < 0 || static_cast<size_t>(line) >= rows.size())
|
||||
return std::string_view();
|
||||
thread_local std::string scratch;
|
||||
scratch = static_cast<std::string>(rows[static_cast<size_t>(line)]);
|
||||
return std::string_view(scratch);
|
||||
};
|
||||
for (auto &d: conv) {
|
||||
Range r8 = toUtf8(uri, d.range, provider);
|
||||
if (debug_) {
|
||||
lsp_debug_file("diagnostic range convert: L%d C%d-%d -> L%d C%d-%d",
|
||||
d.range.start.line, d.range.start.character, d.range.end.character,
|
||||
r8.start.line, r8.start.character, r8.end.character);
|
||||
}
|
||||
d.range = r8;
|
||||
}
|
||||
diagnosticStore_.setDiagnostics(uri, conv);
|
||||
if (display_) {
|
||||
display_->updateDiagnostics(uri, conv);
|
||||
display_->updateStatusBar(diagnosticStore_.getErrorCount(uri), diagnosticStore_.getWarningCount(uri));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspManager::toggleAutostart(const std::string &languageId)
|
||||
{
|
||||
auto it = serverConfigs_.find(languageId);
|
||||
if (it == serverConfigs_.end())
|
||||
return false;
|
||||
it->second.autostart = !it->second.autostart;
|
||||
return it->second.autostart;
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::string>
|
||||
LspManager::configuredLanguages() const
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
out.reserve(serverConfigs_.size());
|
||||
for (const auto &kv: serverConfigs_)
|
||||
out.push_back(kv.first);
|
||||
std::sort(out.begin(), out.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::string>
|
||||
LspManager::runningLanguages() const
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
for (const auto &kv: servers_) {
|
||||
if (kv.second && kv.second->isRunning())
|
||||
out.push_back(kv.first);
|
||||
}
|
||||
std::sort(out.begin(), out.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
LspManager::getLanguageId(Buffer *buffer)
|
||||
{
|
||||
// Prefer explicit filetype if set
|
||||
const auto &ft = buffer->Filetype();
|
||||
if (!ft.empty())
|
||||
return ft;
|
||||
// Otherwise map extension
|
||||
fs::path p(buffer->Filename());
|
||||
return extToLanguageId(p.extension().string());
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
LspManager::getUri(Buffer *buffer)
|
||||
{
|
||||
const auto &path = buffer->Filename();
|
||||
if (path.empty()) {
|
||||
// Untitled buffer: use a pseudo-URI
|
||||
return std::string("untitled:") + std::to_string(reinterpret_cast<std::uintptr_t>(buffer));
|
||||
}
|
||||
fs::path p(path);
|
||||
p = fs::weakly_canonical(p);
|
||||
#ifdef _WIN32
|
||||
// rudimentary file URI; future: robust encoding
|
||||
return std::string("file:/") + p.string();
|
||||
#else
|
||||
return std::string("file://") + p.string();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
// Resolve a Buffer* by matching constructed file URI
|
||||
Buffer *
|
||||
LspManager::findBufferByUri(const std::string &uri)
|
||||
{
|
||||
if (!editor_)
|
||||
return nullptr;
|
||||
// Compare against getUri for each buffer
|
||||
auto &bufs = editor_->Buffers();
|
||||
for (auto &b: bufs) {
|
||||
if (getUri(&b) == uri)
|
||||
return &b;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
LspManager::extToLanguageId(const std::string &ext)
|
||||
{
|
||||
std::string e = ext;
|
||||
if (!e.empty() && e[0] == '.')
|
||||
e.erase(0, 1);
|
||||
std::string lower;
|
||||
lower.resize(e.size());
|
||||
std::transform(e.begin(), e.end(), lower.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
if (lower == "rs")
|
||||
return "rust";
|
||||
if (lower == "c" || lower == "cc" || lower == "cpp" || lower == "h" || lower == "hpp")
|
||||
return "cpp";
|
||||
if (lower == "go")
|
||||
return "go";
|
||||
if (lower == "py")
|
||||
return "python";
|
||||
if (lower == "js")
|
||||
return "javascript";
|
||||
if (lower == "ts")
|
||||
return "typescript";
|
||||
if (lower == "json")
|
||||
return "json";
|
||||
if (lower == "sh" || lower == "bash" || lower == "zsh")
|
||||
return "shell";
|
||||
if (lower == "md")
|
||||
return "markdown";
|
||||
return lower; // best-effort
|
||||
}
|
||||
|
||||
|
||||
LspClient *
|
||||
LspManager::ensureServerForLanguage(const std::string &languageId)
|
||||
{
|
||||
auto it = servers_.find(languageId);
|
||||
if (it != servers_.end() && it->second && it->second->isRunning()) {
|
||||
return it->second.get();
|
||||
}
|
||||
// Attempt to start from config if present
|
||||
auto cfg = serverConfigs_.find(languageId);
|
||||
if (cfg == serverConfigs_.end())
|
||||
return nullptr;
|
||||
auto client = std::make_unique<LspProcessClient>(cfg->second.command, cfg->second.args);
|
||||
client->setDiagnosticsHandler([this](const std::string &uri, const std::vector<Diagnostic> &diags) {
|
||||
this->handleDiagnostics(uri, diags);
|
||||
});
|
||||
// No specific file context here; initialize with empty or current working dir
|
||||
if (!client->initialize(""))
|
||||
return nullptr;
|
||||
auto *ret = client.get();
|
||||
servers_[languageId] = std::move(client);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspManager::registerDefaultServers()
|
||||
{
|
||||
// Import defaults and register by inferred languageId from file patterns
|
||||
for (const auto &cfg: GetDefaultServerConfigs()) {
|
||||
if (cfg.filePatterns.empty()) {
|
||||
// If no patterns, we can't infer; skip
|
||||
continue;
|
||||
}
|
||||
for (const auto &pat: cfg.filePatterns) {
|
||||
const auto lang = patternToLanguageId(pat);
|
||||
if (lang.empty())
|
||||
continue;
|
||||
// Don't overwrite if user already registered a server for this lang
|
||||
if (serverConfigs_.find(lang) == serverConfigs_.end()) {
|
||||
serverConfigs_.emplace(lang, cfg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
LspManager::patternToLanguageId(const std::string &pattern)
|
||||
{
|
||||
// Expect patterns like "*.rs", "*.cpp" etc. Extract extension and reuse extToLanguageId
|
||||
// Find last '.' in the pattern and take substring after it, stripping any trailing wildcards
|
||||
std::string ext;
|
||||
// Common case: starts with *.
|
||||
auto pos = pattern.rfind('.');
|
||||
if (pos != std::string::npos && pos + 1 < pattern.size()) {
|
||||
ext = pattern.substr(pos + 1);
|
||||
// Remove any trailing wildcard characters
|
||||
while (!ext.empty() && (ext.back() == '*' || ext.back() == '?')) {
|
||||
ext.pop_back();
|
||||
}
|
||||
} else {
|
||||
// No dot; try to treat whole pattern as extension after trimming leading '*'
|
||||
ext = pattern;
|
||||
while (!ext.empty() && (ext.front() == '*' || ext.front() == '.')) {
|
||||
ext.erase(ext.begin());
|
||||
}
|
||||
}
|
||||
if (ext.empty())
|
||||
return {};
|
||||
return extToLanguageId(ext);
|
||||
}
|
||||
|
||||
|
||||
// Detect workspace root by walking up from filePath looking for any of the
|
||||
// configured rootPatterns (simple filenames). Supports comma/semicolon-separated
|
||||
// patterns in cfg.rootPatterns.
|
||||
std::string
|
||||
LspManager::detectWorkspaceRoot(const std::string &filePath, const LspServerConfig &cfg)
|
||||
{
|
||||
if (filePath.empty())
|
||||
return {};
|
||||
fs::path start(filePath);
|
||||
fs::path dir = start.has_parent_path() ? start.parent_path() : start;
|
||||
|
||||
// Build cache key
|
||||
const std::string cacheKey = (dir.string() + "|" + cfg.rootPatterns);
|
||||
auto it = rootCache_.find(cacheKey);
|
||||
if (it != rootCache_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Split patterns by ',', ';', or ':'
|
||||
std::vector<std::string> pats;
|
||||
{
|
||||
std::string acc;
|
||||
for (char c: cfg.rootPatterns) {
|
||||
if (c == ',' || c == ';' || c == ':') {
|
||||
if (!acc.empty()) {
|
||||
pats.push_back(acc);
|
||||
acc.clear();
|
||||
}
|
||||
} else if (!std::isspace(static_cast<unsigned char>(c))) {
|
||||
acc.push_back(c);
|
||||
}
|
||||
}
|
||||
if (!acc.empty())
|
||||
pats.push_back(acc);
|
||||
}
|
||||
// If no patterns defined, cache empty and return {}
|
||||
if (pats.empty()) {
|
||||
rootCache_[cacheKey] = {};
|
||||
return {};
|
||||
}
|
||||
|
||||
fs::path cur = dir;
|
||||
while (true) {
|
||||
// Check each pattern in this directory
|
||||
for (const auto &pat: pats) {
|
||||
if (pat.empty())
|
||||
continue;
|
||||
fs::path candidate = cur / pat;
|
||||
std::error_code ec;
|
||||
bool exists = fs::exists(candidate, ec);
|
||||
if (!ec && exists) {
|
||||
rootCache_[cacheKey] = cur.string();
|
||||
return rootCache_[cacheKey];
|
||||
}
|
||||
}
|
||||
if (cur.has_parent_path()) {
|
||||
fs::path parent = cur.parent_path();
|
||||
if (parent == cur)
|
||||
break; // reached root guard
|
||||
cur = parent;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
rootCache_[cacheKey] = {};
|
||||
return {};
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
108
lsp/LspManager.h
Normal file
108
lsp/LspManager.h
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* LspManager.h - central coordination of LSP servers and diagnostics
|
||||
*/
|
||||
#ifndef KTE_LSP_MANAGER_H
|
||||
#define KTE_LSP_MANAGER_H
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
class Buffer; // fwd
|
||||
class Editor; // fwd
|
||||
|
||||
#include "DiagnosticDisplay.h"
|
||||
#include "DiagnosticStore.h"
|
||||
#include "LspClient.h"
|
||||
#include "LspServerConfig.h"
|
||||
#include "UtfCodec.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
class LspManager {
|
||||
public:
|
||||
explicit LspManager(Editor *editor, DiagnosticDisplay *display);
|
||||
|
||||
// Server management
|
||||
void registerServer(const std::string &languageId, const LspServerConfig &config);
|
||||
|
||||
bool startServerForBuffer(Buffer *buffer);
|
||||
|
||||
void stopServer(const std::string &languageId);
|
||||
|
||||
void stopAllServers();
|
||||
|
||||
// Manual lifecycle controls
|
||||
bool startServerForLanguage(const std::string &languageId, const std::string &rootPath = std::string());
|
||||
|
||||
bool restartServer(const std::string &languageId, const std::string &rootPath = std::string());
|
||||
|
||||
// Document sync (to be called by editor/buffer events)
|
||||
void onBufferOpened(Buffer *buffer);
|
||||
|
||||
void onBufferChanged(Buffer *buffer);
|
||||
|
||||
void onBufferClosed(Buffer *buffer);
|
||||
|
||||
void onBufferSaved(Buffer *buffer);
|
||||
|
||||
// Feature requests (stubs)
|
||||
void requestCompletion(Buffer *buffer, Position pos, CompletionCallback callback);
|
||||
|
||||
void requestHover(Buffer *buffer, Position pos, HoverCallback callback);
|
||||
|
||||
void requestDefinition(Buffer *buffer, Position pos, LocationCallback callback);
|
||||
|
||||
// Diagnostics (public so LspClient impls can forward results here later)
|
||||
void handleDiagnostics(const std::string &uri, const std::vector<Diagnostic> &diagnostics);
|
||||
|
||||
|
||||
void setDebugLogging(bool enabled)
|
||||
{
|
||||
debug_ = enabled;
|
||||
}
|
||||
|
||||
|
||||
// Configuration utilities
|
||||
bool toggleAutostart(const std::string &languageId);
|
||||
|
||||
std::vector<std::string> configuredLanguages() const;
|
||||
|
||||
std::vector<std::string> runningLanguages() const;
|
||||
|
||||
private:
|
||||
[[maybe_unused]] Editor *editor_{}; // non-owning
|
||||
DiagnosticDisplay *display_{}; // non-owning
|
||||
DiagnosticStore diagnosticStore_{};
|
||||
|
||||
// Key: languageId → client
|
||||
std::unordered_map<std::string, std::unique_ptr<LspClient> > servers_;
|
||||
std::unordered_map<std::string, LspServerConfig> serverConfigs_;
|
||||
|
||||
// Helpers
|
||||
static std::string getLanguageId(Buffer *buffer);
|
||||
|
||||
static std::string getUri(Buffer *buffer);
|
||||
|
||||
static std::string extToLanguageId(const std::string &ext);
|
||||
|
||||
LspClient *ensureServerForLanguage(const std::string &languageId);
|
||||
|
||||
bool debug_ = false;
|
||||
|
||||
// Configuration helpers
|
||||
void registerDefaultServers();
|
||||
|
||||
static std::string patternToLanguageId(const std::string &pattern);
|
||||
|
||||
// Workspace root detection helpers/cache
|
||||
std::string detectWorkspaceRoot(const std::string &filePath, const LspServerConfig &cfg);
|
||||
|
||||
// key = startDir + "|" + cfg.rootPatterns
|
||||
std::unordered_map<std::string, std::string> rootCache_;
|
||||
|
||||
// Resolve a buffer by its file:// (or untitled:) URI
|
||||
Buffer *findBufferByUri(const std::string &uri);
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_MANAGER_H
|
||||
948
lsp/LspProcessClient.cc
Normal file
948
lsp/LspProcessClient.cc
Normal file
@@ -0,0 +1,948 @@
|
||||
/*
|
||||
* LspProcessClient.cc - process-based LSP client (Phase 1 minimal)
|
||||
*/
|
||||
#include "LspProcessClient.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cerrno>
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <thread>
|
||||
|
||||
#include "json.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
LspProcessClient::LspProcessClient(std::string serverCommand, std::vector<std::string> serverArgs)
|
||||
: command_(std::move(serverCommand)), args_(std::move(serverArgs)), transport_(new JsonRpcTransport())
|
||||
{
|
||||
if (const char *dbg = std::getenv("KTE_LSP_DEBUG"); dbg && *dbg) {
|
||||
debug_ = true;
|
||||
}
|
||||
if (const char *to = std::getenv("KTE_LSP_REQ_TIMEOUT_MS"); to && *to) {
|
||||
char *end = nullptr;
|
||||
long long v = std::strtoll(to, &end, 10);
|
||||
if (end && *end == '\0' && v >= 0) {
|
||||
requestTimeoutMs_ = v;
|
||||
}
|
||||
}
|
||||
if (const char *mp = std::getenv("KTE_LSP_MAX_PENDING"); mp && *mp) {
|
||||
char *end = nullptr;
|
||||
long long v = std::strtoll(mp, &end, 10);
|
||||
if (end && *end == '\0' && v >= 0) {
|
||||
maxPending_ = static_cast<size_t>(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
LspProcessClient::~LspProcessClient()
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspProcessClient::spawnServerProcess()
|
||||
{
|
||||
int toChild[2]; // parent writes toChild[1] -> child's stdin
|
||||
int fromChild[2]; // child writes fromChild[1] -> parent's stdout reader
|
||||
if (pipe(toChild) != 0) {
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] pipe(toChild) failed: %s\n", std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (pipe(fromChild) != 0) {
|
||||
::close(toChild[0]);
|
||||
::close(toChild[1]);
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] pipe(fromChild) failed: %s\n", std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
// fork failed
|
||||
::close(toChild[0]);
|
||||
::close(toChild[1]);
|
||||
::close(fromChild[0]);
|
||||
::close(fromChild[1]);
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] fork failed: %s\n", std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (pid == 0) {
|
||||
// Child: set up stdio
|
||||
::dup2(toChild[0], STDIN_FILENO);
|
||||
::dup2(fromChild[1], STDOUT_FILENO);
|
||||
// Close extra fds
|
||||
::close(toChild[0]);
|
||||
::close(toChild[1]);
|
||||
::close(fromChild[0]);
|
||||
::close(fromChild[1]);
|
||||
// Build argv
|
||||
std::vector<char *> argv;
|
||||
argv.push_back(const_cast<char *>(command_.c_str()));
|
||||
for (auto &s: args_)
|
||||
argv.push_back(const_cast<char *>(s.c_str()));
|
||||
argv.push_back(nullptr);
|
||||
// Exec
|
||||
execvp(command_.c_str(), argv.data());
|
||||
// If exec fails
|
||||
// Note: in child; cannot easily log to parent. Attempt to write to stderr.
|
||||
std::fprintf(stderr, "[kte][lsp] execvp failed for '%s': %s\n", command_.c_str(), std::strerror(errno));
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
// Parent: keep ends
|
||||
childPid_ = pid;
|
||||
outFd_ = toChild[1]; // write to child's stdin
|
||||
inFd_ = fromChild[0]; // read from child's stdout
|
||||
// Close the other ends we don't use
|
||||
::close(toChild[0]);
|
||||
::close(fromChild[1]);
|
||||
// Set CLOEXEC on our fds
|
||||
fcntl(outFd_, F_SETFD, FD_CLOEXEC);
|
||||
fcntl(inFd_, F_SETFD, FD_CLOEXEC);
|
||||
if (debug_) {
|
||||
std::ostringstream oss;
|
||||
oss << command_;
|
||||
for (const auto &a: args_) {
|
||||
oss << ' ' << a;
|
||||
}
|
||||
const char *pathEnv = std::getenv("PATH");
|
||||
std::fprintf(stderr, "[kte][lsp] spawned pid=%d argv=[%s] inFd=%d outFd=%d PATH=%s\n",
|
||||
static_cast<int>(childPid_), oss.str().c_str(), inFd_, outFd_,
|
||||
pathEnv ? pathEnv : "<null>");
|
||||
}
|
||||
transport_->connect(inFd_, outFd_);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::terminateProcess()
|
||||
{
|
||||
if (outFd_ >= 0) {
|
||||
::close(outFd_);
|
||||
outFd_ = -1;
|
||||
}
|
||||
if (inFd_ >= 0) {
|
||||
::close(inFd_);
|
||||
inFd_ = -1;
|
||||
}
|
||||
if (childPid_ > 0) {
|
||||
// Try to wait non-blocking; if still running, send SIGTERM
|
||||
int status = 0;
|
||||
pid_t r = waitpid(childPid_, &status, WNOHANG);
|
||||
if (r == 0) {
|
||||
// still running
|
||||
kill(childPid_, SIGTERM);
|
||||
waitpid(childPid_, &status, 0);
|
||||
}
|
||||
childPid_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::sendInitialize(const std::string &rootPath)
|
||||
{
|
||||
int idNum = nextRequestIntId_++;
|
||||
pendingInitializeId_ = std::to_string(idNum);
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["id"] = idNum;
|
||||
j["method"] = "initialize";
|
||||
nlohmann::json params;
|
||||
params["processId"] = static_cast<int>(getpid());
|
||||
params["rootUri"] = toFileUri(rootPath);
|
||||
// Minimal client capabilities for now
|
||||
nlohmann::json caps;
|
||||
caps["textDocument"]["synchronization"]["didSave"] = true;
|
||||
params["capabilities"] = std::move(caps);
|
||||
j["params"] = std::move(params);
|
||||
transport_->send("initialize", j.dump());
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspProcessClient::initialize(const std::string &rootPath)
|
||||
{
|
||||
if (running_)
|
||||
return true;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] initialize: rootPath=%s\n", rootPath.c_str());
|
||||
if (!spawnServerProcess())
|
||||
return false;
|
||||
running_ = true;
|
||||
sendInitialize(rootPath);
|
||||
startReader();
|
||||
startTimeoutWatchdog();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::shutdown()
|
||||
{
|
||||
if (!running_)
|
||||
return;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] shutdown\n");
|
||||
// Send shutdown request then exit notification (best-effort)
|
||||
int id = nextRequestIntId_++;
|
||||
{
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["id"] = id;
|
||||
j["method"] = "shutdown";
|
||||
transport_->send("shutdown", j.dump());
|
||||
}
|
||||
{
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["method"] = "exit";
|
||||
transport_->send("exit", j.dump());
|
||||
}
|
||||
// Close pipes to unblock reader, then join thread, then ensure child is gone
|
||||
terminateProcess();
|
||||
stopReader();
|
||||
stopTimeoutWatchdog();
|
||||
// Clear any pending callbacks
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||
pending_.clear();
|
||||
pendingOrder_.clear();
|
||||
}
|
||||
running_ = false;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::didOpen(const std::string &uri, const std::string &languageId,
|
||||
int version, const std::string &text)
|
||||
{
|
||||
if (!running_)
|
||||
return;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] -> didOpen uri=%s lang=%s version=%d bytes=%zu\n",
|
||||
uri.c_str(), languageId.c_str(), version, text.size());
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["method"] = "textDocument/didOpen";
|
||||
j["params"]["textDocument"]["uri"] = uri;
|
||||
j["params"]["textDocument"]["languageId"] = languageId;
|
||||
j["params"]["textDocument"]["version"] = version;
|
||||
j["params"]["textDocument"]["text"] = text;
|
||||
transport_->send("textDocument/didOpen", j.dump());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::didChange(const std::string &uri, int version,
|
||||
const std::vector<TextDocumentContentChangeEvent> &changes)
|
||||
{
|
||||
if (!running_)
|
||||
return;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] -> didChange uri=%s version=%d changes=%zu\n",
|
||||
uri.c_str(), version, changes.size());
|
||||
// Phase 1: send full or ranged changes using proper JSON construction
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["method"] = "textDocument/didChange";
|
||||
j["params"]["textDocument"]["uri"] = uri;
|
||||
j["params"]["textDocument"]["version"] = version;
|
||||
auto &arr = j["params"]["contentChanges"];
|
||||
arr = nlohmann::json::array();
|
||||
for (const auto &ch: changes) {
|
||||
nlohmann::json c;
|
||||
if (ch.range.has_value()) {
|
||||
c["range"]["start"]["line"] = ch.range->start.line;
|
||||
c["range"]["start"]["character"] = ch.range->start.character;
|
||||
c["range"]["end"]["line"] = ch.range->end.line;
|
||||
c["range"]["end"]["character"] = ch.range->end.character;
|
||||
}
|
||||
c["text"] = ch.text;
|
||||
arr.push_back(std::move(c));
|
||||
}
|
||||
transport_->send("textDocument/didChange", j.dump());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::didClose(const std::string &uri)
|
||||
{
|
||||
if (!running_)
|
||||
return;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] -> didClose uri=%s\n", uri.c_str());
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["method"] = "textDocument/didClose";
|
||||
j["params"]["textDocument"]["uri"] = uri;
|
||||
transport_->send("textDocument/didClose", j.dump());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::didSave(const std::string &uri)
|
||||
{
|
||||
if (!running_)
|
||||
return;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] -> didSave uri=%s\n", uri.c_str());
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["method"] = "textDocument/didSave";
|
||||
j["params"]["textDocument"]["uri"] = uri;
|
||||
transport_->send("textDocument/didSave", j.dump());
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::startReader()
|
||||
{
|
||||
stopReader_ = false;
|
||||
reader_ = std::thread([this] {
|
||||
this->readerLoop();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::stopReader()
|
||||
{
|
||||
stopReader_ = true;
|
||||
if (reader_.joinable()) {
|
||||
// Waking up read() by closing inFd_ is handled in terminateProcess(); ensure it’s closed first
|
||||
// Here, best-effort join with small delay
|
||||
reader_.join();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::readerLoop()
|
||||
{
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] readerLoop start\n");
|
||||
while (!stopReader_) {
|
||||
auto msg = transport_->read();
|
||||
if (!msg.has_value()) {
|
||||
// EOF or error
|
||||
break;
|
||||
}
|
||||
handleIncoming(msg->raw);
|
||||
}
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] readerLoop end\n");
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::handleIncoming(const std::string &json)
|
||||
{
|
||||
try {
|
||||
auto j = nlohmann::json::parse(json, nullptr, false);
|
||||
if (j.is_discarded())
|
||||
return; // malformed JSON
|
||||
// Validate jsonrpc if present
|
||||
if (auto itRpc = j.find("jsonrpc"); itRpc != j.end()) {
|
||||
if (!itRpc->is_string() || *itRpc != "2.0")
|
||||
return;
|
||||
}
|
||||
|
||||
auto normalizeId = [](const nlohmann::json &idVal) -> std::string {
|
||||
if (idVal.is_string())
|
||||
return idVal.get<std::string>();
|
||||
if (idVal.is_number_integer())
|
||||
return std::to_string(idVal.get<long long>());
|
||||
return std::string();
|
||||
};
|
||||
|
||||
// Handle responses (have id and no method) or server -> client requests (have id and method)
|
||||
if (auto itId = j.find("id"); itId != j.end() && !itId->is_null()) {
|
||||
const std::string respIdStr = normalizeId(*itId);
|
||||
|
||||
// If it's a request from server, it will also have a method
|
||||
if (auto itMeth = j.find("method"); itMeth != j.end() && itMeth->is_string()) {
|
||||
const std::string method = *itMeth;
|
||||
if (method == "workspace/configuration") {
|
||||
// Respond with default empty settings array matching requested items length
|
||||
size_t n = 0;
|
||||
if (auto itParams = j.find("params");
|
||||
itParams != j.end() && itParams->is_object()) {
|
||||
if (auto itItems = itParams->find("items");
|
||||
itItems != itParams->end() && itItems->is_array()) {
|
||||
n = itItems->size();
|
||||
}
|
||||
}
|
||||
nlohmann::json resp;
|
||||
resp["jsonrpc"] = "2.0";
|
||||
// echo id type: if original was string, send string; else number
|
||||
if (itId->is_string())
|
||||
resp["id"] = *itId;
|
||||
else if (itId->is_number_integer())
|
||||
resp["id"] = *itId;
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
arr.push_back(nlohmann::json::object());
|
||||
resp["result"] = std::move(arr);
|
||||
transport_->send("response", resp.dump());
|
||||
return;
|
||||
}
|
||||
if (method == "window/showMessageRequest") {
|
||||
// Best-effort respond with null result (dismiss)
|
||||
nlohmann::json resp;
|
||||
resp["jsonrpc"] = "2.0";
|
||||
if (itId->is_string())
|
||||
resp["id"] = *itId;
|
||||
else if (itId->is_number_integer())
|
||||
resp["id"] = *itId;
|
||||
resp["result"] = nullptr;
|
||||
transport_->send("response", resp.dump());
|
||||
return;
|
||||
}
|
||||
// Unknown server request: respond with MethodNotFound
|
||||
nlohmann::json err;
|
||||
err["code"] = -32601;
|
||||
err["message"] = "Method not found";
|
||||
nlohmann::json resp;
|
||||
resp["jsonrpc"] = "2.0";
|
||||
if (itId->is_string())
|
||||
resp["id"] = *itId;
|
||||
else if (itId->is_number_integer())
|
||||
resp["id"] = *itId;
|
||||
resp["error"] = std::move(err);
|
||||
transport_->send("response", resp.dump());
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize handshake special-case
|
||||
if (!pendingInitializeId_.empty() && respIdStr == pendingInitializeId_) {
|
||||
nlohmann::json init;
|
||||
init["jsonrpc"] = "2.0";
|
||||
init["method"] = "initialized";
|
||||
init["params"] = nlohmann::json::object();
|
||||
transport_->send("initialized", init.dump());
|
||||
pendingInitializeId_.clear();
|
||||
}
|
||||
// Dispatcher lookup
|
||||
std::function < void(const nlohmann::json &, const nlohmann::json *) > cb;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||
auto it = pending_.find(respIdStr);
|
||||
if (it != pending_.end()) {
|
||||
cb = it->second.callback;
|
||||
if (it->second.orderIt != pendingOrder_.end()) {
|
||||
pendingOrder_.erase(it->second.orderIt);
|
||||
}
|
||||
pending_.erase(it);
|
||||
}
|
||||
}
|
||||
if (cb) {
|
||||
const nlohmann::json *errPtr = nullptr;
|
||||
const auto itErr = j.find("error");
|
||||
if (itErr != j.end() && itErr->is_object())
|
||||
errPtr = &(*itErr);
|
||||
nlohmann::json result;
|
||||
const auto itRes = j.find("result");
|
||||
if (itRes != j.end())
|
||||
result = *itRes; // may be null
|
||||
cb(result, errPtr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto itMethod = j.find("method");
|
||||
if (itMethod == j.end() || !itMethod->is_string())
|
||||
return;
|
||||
const std::string method = *itMethod;
|
||||
if (method == "window/logMessage") {
|
||||
if (debug_) {
|
||||
const auto itParams = j.find("params");
|
||||
if (itParams != j.end()) {
|
||||
const auto itMsg = itParams->find("message");
|
||||
if (itMsg != itParams->end() && itMsg->is_string()) {
|
||||
std::fprintf(stderr, "[kte][lsp] logMessage: %s\n",
|
||||
itMsg->get_ref<const std::string &>().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (method == "window/showMessage") {
|
||||
const auto itParams = j.find("params");
|
||||
if (debug_ &&itParams
|
||||
|
||||
|
||||
|
||||
!=
|
||||
j.end() && itParams->is_object()
|
||||
)
|
||||
{
|
||||
int typ = 0;
|
||||
std::string msg;
|
||||
if (auto itm = itParams->find("message"); itm != itParams->end() && itm->is_string())
|
||||
msg = *itm;
|
||||
if (auto ity = itParams->find("type");
|
||||
ity != itParams->end() && ity->is_number_integer())
|
||||
typ = *ity;
|
||||
std::fprintf(stderr, "[kte][lsp] showMessage(type=%d): %s\n", typ, msg.c_str());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (method != "textDocument/publishDiagnostics") {
|
||||
return;
|
||||
}
|
||||
const auto itParams = j.find("params");
|
||||
if (itParams == j.end() || !itParams->is_object())
|
||||
return;
|
||||
const auto itUri = itParams->find("uri");
|
||||
if (itUri == itParams->end() || !itUri->is_string())
|
||||
return;
|
||||
const std::string uri = *itUri;
|
||||
std::vector<Diagnostic> diags;
|
||||
const auto itDiag = itParams->find("diagnostics");
|
||||
if (itDiag != itParams->end() && itDiag->is_array()) {
|
||||
for (const auto &djson: *itDiag) {
|
||||
if (!djson.is_object())
|
||||
continue;
|
||||
Diagnostic d;
|
||||
// severity
|
||||
int sev = 3;
|
||||
if (auto itS = djson.find("severity"); itS != djson.end() && itS->is_number_integer()) {
|
||||
sev = *itS;
|
||||
}
|
||||
switch (sev) {
|
||||
case 1:
|
||||
d.severity = DiagnosticSeverity::Error;
|
||||
break;
|
||||
case 2:
|
||||
d.severity = DiagnosticSeverity::Warning;
|
||||
break;
|
||||
case 3:
|
||||
d.severity = DiagnosticSeverity::Information;
|
||||
break;
|
||||
case 4:
|
||||
d.severity = DiagnosticSeverity::Hint;
|
||||
break;
|
||||
default:
|
||||
d.severity = DiagnosticSeverity::Information;
|
||||
break;
|
||||
}
|
||||
if (auto itM = djson.find("message"); itM != djson.end() && itM->is_string()) {
|
||||
d.message = *itM;
|
||||
}
|
||||
if (auto itR = djson.find("range"); itR != djson.end() && itR->is_object()) {
|
||||
if (auto itStart = itR->find("start");
|
||||
itStart != itR->end() && itStart->is_object()) {
|
||||
if (auto itL = itStart->find("line");
|
||||
itL != itStart->end() && itL->is_number_integer()) {
|
||||
d.range.start.line = *itL;
|
||||
}
|
||||
if (auto itC = itStart->find("character");
|
||||
itC != itStart->end() && itC->is_number_integer()) {
|
||||
d.range.start.character = *itC;
|
||||
}
|
||||
}
|
||||
if (auto itEnd = itR->find("end"); itEnd != itR->end() && itEnd->is_object()) {
|
||||
if (auto itL = itEnd->find("line");
|
||||
itL != itEnd->end() && itL->is_number_integer()) {
|
||||
d.range.end.line = *itL;
|
||||
}
|
||||
if (auto itC = itEnd->find("character");
|
||||
itC != itEnd->end() && itC->is_number_integer()) {
|
||||
d.range.end.character = *itC;
|
||||
}
|
||||
}
|
||||
}
|
||||
// optional code/source
|
||||
if (auto itCode = djson.find("code"); itCode != djson.end()) {
|
||||
if (itCode->is_string())
|
||||
d.code = itCode->get<std::string>();
|
||||
else if (itCode->is_number_integer())
|
||||
d.code = std::to_string(itCode->get<int>());
|
||||
}
|
||||
if (auto itSrc = djson.find("source"); itSrc != djson.end() && itSrc->is_string()) {
|
||||
d.source = itSrc->get<std::string>();
|
||||
}
|
||||
diags.push_back(std::move(d));
|
||||
}
|
||||
}
|
||||
if (diagnosticsHandler_) {
|
||||
diagnosticsHandler_(uri, diags);
|
||||
}
|
||||
} catch (...) {
|
||||
// swallow parse errors
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
LspProcessClient::sendRequest(const std::string &method, const nlohmann::json ¶ms,
|
||||
std::function<void(const nlohmann::json & result, const nlohmann::json * errorJson)> cb)
|
||||
{
|
||||
if (!running_)
|
||||
return 0;
|
||||
int id = nextRequestIntId_++;
|
||||
nlohmann::json j;
|
||||
j["jsonrpc"] = "2.0";
|
||||
j["id"] = id;
|
||||
j["method"] = method;
|
||||
if (!params.is_null())
|
||||
j["params"] = params;
|
||||
if (debug_)
|
||||
std::fprintf(stderr, "[kte][lsp] -> request method=%s id=%d\n", method.c_str(), id);
|
||||
transport_->send(method, j.dump());
|
||||
if (cb) {
|
||||
std::function < void() > callDropped;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||
if (maxPending_ > 0 && pending_.size() >= maxPending_) {
|
||||
// Evict oldest
|
||||
if (!pendingOrder_.empty()) {
|
||||
std::string oldestId = pendingOrder_.front();
|
||||
auto it = pending_.find(oldestId);
|
||||
if (it != pending_.end()) {
|
||||
auto cbOld = it->second.callback;
|
||||
std::string methOld = it->second.method;
|
||||
if (debug_) {
|
||||
std::fprintf(
|
||||
stderr,
|
||||
"[kte][lsp] dropping oldest pending id=%s method=%s (cap=%zu)\n",
|
||||
oldestId.c_str(), methOld.c_str(), maxPending_);
|
||||
}
|
||||
// Prepare drop callback to run outside lock
|
||||
callDropped = [cbOld] {
|
||||
if (cbOld) {
|
||||
nlohmann::json err;
|
||||
err["code"] = -32001;
|
||||
err["message"] =
|
||||
"Request dropped (max pending exceeded)";
|
||||
cbOld(nlohmann::json(), &err);
|
||||
}
|
||||
};
|
||||
pending_.erase(it);
|
||||
}
|
||||
pendingOrder_.pop_front();
|
||||
}
|
||||
}
|
||||
pendingOrder_.push_back(std::to_string(id));
|
||||
auto itOrder = pendingOrder_.end();
|
||||
--itOrder;
|
||||
PendingRequest pr;
|
||||
pr.method = method;
|
||||
pr.callback = std::move(cb);
|
||||
if (requestTimeoutMs_ > 0) {
|
||||
pr.deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(
|
||||
requestTimeoutMs_);
|
||||
}
|
||||
pr.orderIt = itOrder;
|
||||
pending_[std::to_string(id)] = std::move(pr);
|
||||
}
|
||||
if (callDropped)
|
||||
callDropped();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::completion(const std::string &uri, Position pos, CompletionCallback cb)
|
||||
{
|
||||
nlohmann::json params;
|
||||
params["textDocument"]["uri"] = uri;
|
||||
params["position"]["line"] = pos.line;
|
||||
params["position"]["character"] = pos.character;
|
||||
sendRequest("textDocument/completion", params,
|
||||
[cb = std::move(cb)](const nlohmann::json &result, const nlohmann::json *error) {
|
||||
CompletionList out{};
|
||||
std::string err;
|
||||
if (error) {
|
||||
if (auto itMsg = error->find("message");
|
||||
itMsg != error->end() && itMsg->is_string())
|
||||
err = itMsg->get<std::string>();
|
||||
else
|
||||
err = "LSP error";
|
||||
} else {
|
||||
auto parseItem = [](const nlohmann::json &j) -> CompletionItem {
|
||||
CompletionItem it{};
|
||||
if (auto il = j.find("label"); il != j.end() && il->is_string())
|
||||
it.label = il->get<std::string>();
|
||||
if (auto idt = j.find("detail"); idt != j.end() && idt->is_string())
|
||||
it.detail = idt->get<std::string>();
|
||||
if (auto ins = j.find("insertText"); ins != j.end() && ins->is_string())
|
||||
it.insertText = ins->get<std::string>();
|
||||
return it;
|
||||
};
|
||||
if (result.is_array()) {
|
||||
for (const auto &ji: result) {
|
||||
if (ji.is_object())
|
||||
out.items.push_back(parseItem(ji));
|
||||
}
|
||||
} else if (result.is_object()) {
|
||||
if (auto ii = result.find("isIncomplete");
|
||||
ii != result.end() && ii->is_boolean())
|
||||
out.isIncomplete = ii->get<bool>();
|
||||
if (auto itms = result.find("items");
|
||||
itms != result.end() && itms->is_array()) {
|
||||
for (const auto &ji: *itms) {
|
||||
if (ji.is_object())
|
||||
out.items.push_back(parseItem(ji));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cb)
|
||||
cb(out, err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::hover(const std::string &uri, Position pos, HoverCallback cb)
|
||||
{
|
||||
nlohmann::json params;
|
||||
params["textDocument"]["uri"] = uri;
|
||||
params["position"]["line"] = pos.line;
|
||||
params["position"]["character"] = pos.character;
|
||||
sendRequest("textDocument/hover", params,
|
||||
[cb = std::move(cb)](const nlohmann::json &result, const nlohmann::json *error) {
|
||||
HoverResult out{};
|
||||
std::string err;
|
||||
if (error) {
|
||||
if (auto itMsg = error->find("message");
|
||||
itMsg != error->end() && itMsg->is_string())
|
||||
err = itMsg->get<std::string>();
|
||||
else
|
||||
err = "LSP error";
|
||||
} else if (!result.is_null()) {
|
||||
auto appendText = [&](const std::string &s) {
|
||||
if (!out.contents.empty())
|
||||
out.contents.push_back('\n');
|
||||
out.contents += s;
|
||||
};
|
||||
if (result.is_object()) {
|
||||
if (auto itC = result.find("contents"); itC != result.end()) {
|
||||
if (itC->is_string()) {
|
||||
appendText(itC->get<std::string>());
|
||||
} else if (itC->is_object()) {
|
||||
if (auto itV = itC->find("value");
|
||||
itV != itC->end() && itV->is_string())
|
||||
appendText(itV->get<std::string>());
|
||||
} else if (itC->is_array()) {
|
||||
for (const auto &el: *itC) {
|
||||
if (el.is_string())
|
||||
appendText(el.get<std::string>());
|
||||
else if (el.is_object()) {
|
||||
if (auto itV = el.find("value");
|
||||
itV != el.end() && itV->is_string())
|
||||
appendText(itV->get<std::string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (auto itR = result.find("range");
|
||||
itR != result.end() && itR->is_object()) {
|
||||
Range r{};
|
||||
if (auto s = itR->find("start");
|
||||
s != itR->end() && s->is_object()) {
|
||||
if (auto il = s->find("line");
|
||||
il != s->end() && il->is_number_integer())
|
||||
r.start.line = *il;
|
||||
if (auto ic = s->find("character");
|
||||
ic != s->end() && ic->is_number_integer())
|
||||
r.start.character = *ic;
|
||||
}
|
||||
if (auto e = itR->find("end"); e != itR->end() && e->is_object()) {
|
||||
if (auto il = e->find("line");
|
||||
il != e->end() && il->is_number_integer())
|
||||
r.end.line = *il;
|
||||
if (auto ic = e->find("character");
|
||||
ic != e->end() && ic->is_number_integer())
|
||||
r.end.character = *ic;
|
||||
}
|
||||
out.range = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cb)
|
||||
cb(out, err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::definition(const std::string &uri, Position pos, LocationCallback cb)
|
||||
{
|
||||
nlohmann::json params;
|
||||
params["textDocument"]["uri"] = uri;
|
||||
params["position"]["line"] = pos.line;
|
||||
params["position"]["character"] = pos.character;
|
||||
sendRequest("textDocument/definition", params,
|
||||
[cb = std::move(cb)](const nlohmann::json &result, const nlohmann::json *error) {
|
||||
std::vector<Location> out;
|
||||
std::string err;
|
||||
auto parseRange = [](const nlohmann::json &jr) -> Range {
|
||||
Range r{};
|
||||
if (!jr.is_object())
|
||||
return r;
|
||||
if (auto s = jr.find("start"); s != jr.end() && s->is_object()) {
|
||||
if (auto il = s->find("line"); il != s->end() && il->is_number_integer())
|
||||
r.start.line = *il;
|
||||
if (auto ic = s->find("character");
|
||||
ic != s->end() && ic->is_number_integer())
|
||||
r.start.character = *ic;
|
||||
}
|
||||
if (auto e = jr.find("end"); e != jr.end() && e->is_object()) {
|
||||
if (auto il = e->find("line"); il != e->end() && il->is_number_integer())
|
||||
r.end.line = *il;
|
||||
if (auto ic = e->find("character");
|
||||
ic != e->end() && e->is_number_integer())
|
||||
r.end.character = *ic;
|
||||
}
|
||||
return r;
|
||||
};
|
||||
auto pushLocObj = [&](const nlohmann::json &jo) {
|
||||
Location loc{};
|
||||
if (auto iu = jo.find("uri"); iu != jo.end() && iu->is_string())
|
||||
loc.uri = iu->get<std::string>();
|
||||
if (auto ir = jo.find("range"); ir != jo.end())
|
||||
loc.range = parseRange(*ir);
|
||||
out.push_back(std::move(loc));
|
||||
};
|
||||
if (error) {
|
||||
if (auto itMsg = error->find("message");
|
||||
itMsg != error->end() && itMsg->is_string())
|
||||
err = itMsg->get<std::string>();
|
||||
else
|
||||
err = "LSP error";
|
||||
} else if (!result.is_null()) {
|
||||
if (result.is_object()) {
|
||||
if (result.contains("uri") && result.contains("range")) {
|
||||
pushLocObj(result);
|
||||
} else if (result.contains("targetUri")) {
|
||||
Location loc{};
|
||||
if (auto tu = result.find("targetUri");
|
||||
tu != result.end() && tu->is_string())
|
||||
loc.uri = tu->get<std::string>();
|
||||
if (auto tr = result.find("targetRange"); tr != result.end())
|
||||
loc.range = parseRange(*tr);
|
||||
out.push_back(std::move(loc));
|
||||
}
|
||||
} else if (result.is_array()) {
|
||||
for (const auto &el: result) {
|
||||
if (el.is_object()) {
|
||||
if (el.contains("uri")) {
|
||||
pushLocObj(el);
|
||||
} else if (el.contains("targetUri")) {
|
||||
Location loc{};
|
||||
if (auto tu = el.find("targetUri");
|
||||
tu != el.end() && tu->is_string())
|
||||
loc.uri = tu->get<std::string>();
|
||||
if (auto tr = el.find("targetRange");
|
||||
tr != el.end())
|
||||
loc.range = parseRange(*tr);
|
||||
out.push_back(std::move(loc));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cb)
|
||||
cb(out, err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
LspProcessClient::isRunning() const
|
||||
{
|
||||
return running_;
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
LspProcessClient::getServerName() const
|
||||
{
|
||||
return command_;
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
LspProcessClient::toFileUri(const std::string &path)
|
||||
{
|
||||
if (path.empty())
|
||||
return std::string();
|
||||
#ifdef _WIN32
|
||||
return std::string("file:/") + path;
|
||||
#else
|
||||
return std::string("file://") + path;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::startTimeoutWatchdog()
|
||||
{
|
||||
stopTimeout_ = false;
|
||||
if (requestTimeoutMs_ <= 0)
|
||||
return;
|
||||
timeoutThread_ = std::thread([this] {
|
||||
while (!stopTimeout_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
struct Expired {
|
||||
std::string id;
|
||||
std::string method;
|
||||
std::function<void(const nlohmann::json &, const nlohmann::json *)> cb;
|
||||
};
|
||||
std::vector<Expired> expired;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||
for (auto it = pending_.begin(); it != pending_.end();) {
|
||||
const auto &pr = it->second;
|
||||
if (pr.deadline.time_since_epoch().count() != 0 && now >= pr.deadline) {
|
||||
expired.push_back(Expired{it->first, pr.method, pr.callback});
|
||||
if (pr.orderIt != pendingOrder_.end())
|
||||
pendingOrder_.erase(pr.orderIt);
|
||||
it = pending_.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto &kv: expired) {
|
||||
if (debug_) {
|
||||
std::fprintf(stderr, "[kte][lsp] request timeout id=%s method=%s\n",
|
||||
kv.id.c_str(), kv.method.c_str());
|
||||
}
|
||||
if (kv.cb) {
|
||||
nlohmann::json err;
|
||||
err["code"] = -32000;
|
||||
err["message"] = "Request timed out";
|
||||
kv.cb(nlohmann::json(), &err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LspProcessClient::stopTimeoutWatchdog()
|
||||
{
|
||||
stopTimeout_ = true;
|
||||
if (timeoutThread_.joinable())
|
||||
timeoutThread_.join();
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
189
lsp/LspProcessClient.h
Normal file
189
lsp/LspProcessClient.h
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* LspProcessClient.h - process-based LSP client (initial stub)
|
||||
*/
|
||||
#ifndef KTE_LSP_PROCESS_CLIENT_H
|
||||
#define KTE_LSP_PROCESS_CLIENT_H
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
#include <list>
|
||||
|
||||
#include "json.h"
|
||||
|
||||
#include "LspClient.h"
|
||||
#include "JsonRpcTransport.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
class LspProcessClient : public LspClient {
|
||||
public:
|
||||
LspProcessClient(std::string serverCommand, std::vector<std::string> serverArgs);
|
||||
|
||||
~LspProcessClient() override;
|
||||
|
||||
bool initialize(const std::string &rootPath) override;
|
||||
|
||||
void shutdown() override;
|
||||
|
||||
void didOpen(const std::string &uri, const std::string &languageId,
|
||||
int version, const std::string &text) override;
|
||||
|
||||
void didChange(const std::string &uri, int version,
|
||||
const std::vector<TextDocumentContentChangeEvent> &changes) override;
|
||||
|
||||
void didClose(const std::string &uri) override;
|
||||
|
||||
void didSave(const std::string &uri) override;
|
||||
|
||||
// Language Features (wire-up via dispatcher; minimal callbacks for now)
|
||||
void completion(const std::string &uri, Position pos,
|
||||
CompletionCallback cb) override;
|
||||
|
||||
void hover(const std::string &uri, Position pos,
|
||||
HoverCallback cb) override;
|
||||
|
||||
void definition(const std::string &uri, Position pos,
|
||||
LocationCallback cb) override;
|
||||
|
||||
bool isRunning() const override;
|
||||
|
||||
std::string getServerName() const override;
|
||||
|
||||
|
||||
void setDiagnosticsHandler(DiagnosticsHandler h) override
|
||||
{
|
||||
diagnosticsHandler_ = std::move(h);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string command_;
|
||||
std::vector<std::string> args_;
|
||||
std::unique_ptr<JsonRpcTransport> transport_;
|
||||
bool running_ = false;
|
||||
bool debug_ = false;
|
||||
int inFd_ = -1; // read from server (server stdout)
|
||||
int outFd_ = -1; // write to server (server stdin)
|
||||
pid_t childPid_ = -1;
|
||||
int nextRequestIntId_ = 1;
|
||||
std::string pendingInitializeId_{}; // echo exactly as sent (string form)
|
||||
|
||||
// Incoming processing
|
||||
std::thread reader_;
|
||||
std::atomic<bool> stopReader_{false};
|
||||
DiagnosticsHandler diagnosticsHandler_{};
|
||||
|
||||
// Simple request dispatcher: map request id -> callback
|
||||
struct PendingRequest {
|
||||
std::string method;
|
||||
// If error is present, errorJson points to it; otherwise nullptr
|
||||
std::function<void(const nlohmann::json & result, const nlohmann::json * errorJson)> callback;
|
||||
// Optional timeout
|
||||
std::chrono::steady_clock::time_point deadline{}; // epoch if no timeout
|
||||
// Order tracking for LRU eviction
|
||||
std::list<std::string>::iterator orderIt{};
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, PendingRequest> pending_;
|
||||
// Maintain insertion order (oldest at front)
|
||||
std::list<std::string> pendingOrder_;
|
||||
std::mutex pendingMutex_;
|
||||
|
||||
// Timeout/watchdog for pending requests
|
||||
std::thread timeoutThread_;
|
||||
std::atomic<bool> stopTimeout_{false};
|
||||
int64_t requestTimeoutMs_ = 0; // 0 = disabled
|
||||
size_t maxPending_ = 0; // 0 = unlimited
|
||||
|
||||
bool spawnServerProcess();
|
||||
|
||||
void terminateProcess();
|
||||
|
||||
static std::string toFileUri(const std::string &path);
|
||||
|
||||
void sendInitialize(const std::string &rootPath);
|
||||
|
||||
void startReader();
|
||||
|
||||
void stopReader();
|
||||
|
||||
void readerLoop();
|
||||
|
||||
void handleIncoming(const std::string &json);
|
||||
|
||||
// Helper to send a request with params and register a response callback
|
||||
int sendRequest(const std::string &method, const nlohmann::json ¶ms,
|
||||
std::function<void(const nlohmann::json & result, const nlohmann::json * errorJson)> cb);
|
||||
|
||||
// Start/stop timeout thread
|
||||
void startTimeoutWatchdog();
|
||||
|
||||
void stopTimeoutWatchdog();
|
||||
|
||||
public:
|
||||
// Test hook: inject a raw JSON message as if received from server
|
||||
void debugInjectMessageForTest(const std::string &raw)
|
||||
{
|
||||
handleIncoming(raw);
|
||||
}
|
||||
|
||||
|
||||
// Test hook: add a pending request entry manually
|
||||
void debugAddPendingForTest(const std::string &id, const std::string &method,
|
||||
std::function<void(const nlohmann::json & result,
|
||||
const nlohmann::json *errorJson)
|
||||
|
||||
>
|
||||
cb
|
||||
)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(pendingMutex_);
|
||||
pendingOrder_.push_back(id);
|
||||
auto it = pendingOrder_.end();
|
||||
--it;
|
||||
PendingRequest pr{method, std::move(cb), {}, it};
|
||||
pending_[id] = std::move(pr);
|
||||
}
|
||||
|
||||
|
||||
// Test hook: override timeout
|
||||
void setRequestTimeoutMsForTest(int64_t ms)
|
||||
{
|
||||
requestTimeoutMs_ = ms;
|
||||
}
|
||||
|
||||
|
||||
// Test hook: set max pending
|
||||
void setMaxPendingForTest(size_t maxPending)
|
||||
{
|
||||
maxPending_ = maxPending;
|
||||
}
|
||||
|
||||
|
||||
// Test hook: set running flag (to allow sendRequest in tests without spawning)
|
||||
void setRunningForTest(bool r)
|
||||
{
|
||||
running_ = r;
|
||||
}
|
||||
|
||||
|
||||
// Test hook: send a raw request using internal machinery
|
||||
int debugSendRequestForTest(const std::string &method, const nlohmann::json ¶ms,
|
||||
std::function<void(const nlohmann::json & result,
|
||||
const nlohmann::json *errorJson)
|
||||
|
||||
>
|
||||
cb
|
||||
)
|
||||
{
|
||||
return sendRequest(method, params, std::move(cb));
|
||||
}
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_PROCESS_CLIENT_H
|
||||
47
lsp/LspServerConfig.h
Normal file
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
|
||||
55
lsp/LspTypes.h
Normal file
55
lsp/LspTypes.h
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* LspTypes.h - minimal LSP-related data types for initial integration
|
||||
*/
|
||||
#ifndef KTE_LSP_TYPES_H
|
||||
#define KTE_LSP_TYPES_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace kte::lsp {
|
||||
// NOTE on coordinates:
|
||||
// - Internal editor coordinates use UTF-8 columns counted by Unicode scalars.
|
||||
// - LSP wire protocol uses UTF-16 code units for the `character` field.
|
||||
// Conversions are performed in higher layers via `lsp/UtfCodec.h` helpers.
|
||||
struct Position {
|
||||
int line = 0;
|
||||
int character = 0;
|
||||
};
|
||||
|
||||
struct Range {
|
||||
Position start;
|
||||
Position end;
|
||||
};
|
||||
|
||||
struct TextDocumentContentChangeEvent {
|
||||
std::optional<Range> range; // if not set, represents full document change
|
||||
std::string text; // new text for the given range
|
||||
};
|
||||
|
||||
// Minimal feature result types for phase 1
|
||||
struct CompletionItem {
|
||||
std::string label;
|
||||
std::optional<std::string> detail; // optional extra info
|
||||
std::optional<std::string> insertText; // if present, use instead of label
|
||||
};
|
||||
|
||||
struct CompletionList {
|
||||
bool isIncomplete = false;
|
||||
std::vector<CompletionItem> items;
|
||||
};
|
||||
|
||||
struct HoverResult {
|
||||
std::string contents; // concatenated plaintext/markdown for now
|
||||
std::optional<Range> range; // optional range
|
||||
};
|
||||
|
||||
struct Location {
|
||||
std::string uri;
|
||||
Range range;
|
||||
};
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_TYPES_H
|
||||
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
|
||||
155
lsp/UtfCodec.cc
Normal file
155
lsp/UtfCodec.cc
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* UtfCodec.cc - UTF-8 <-> UTF-16 code unit position conversions
|
||||
*/
|
||||
#include "UtfCodec.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace kte::lsp {
|
||||
// Decode next code point from a UTF-8 string.
|
||||
// On invalid input, consumes 1 byte and returns U+FFFD.
|
||||
// Returns: (codepoint, bytesConsumed)
|
||||
static inline std::pair<uint32_t, size_t>
|
||||
decodeUtf8(std::string_view s, size_t i)
|
||||
{
|
||||
if (i >= s.size())
|
||||
return {0, 0};
|
||||
unsigned char c0 = static_cast<unsigned char>(s[i]);
|
||||
if (c0 < 0x80) {
|
||||
return {c0, 1};
|
||||
}
|
||||
// Determine sequence length
|
||||
if ((c0 & 0xE0) == 0xC0) {
|
||||
if (i + 1 >= s.size())
|
||||
return {0xFFFD, 1};
|
||||
unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
|
||||
if ((c1 & 0xC0) != 0x80)
|
||||
return {0xFFFD, 1};
|
||||
uint32_t cp = ((c0 & 0x1F) << 6) | (c1 & 0x3F);
|
||||
// Overlong check: must be >= 0x80
|
||||
if (cp < 0x80)
|
||||
return {0xFFFD, 1};
|
||||
return {cp, 2};
|
||||
}
|
||||
if ((c0 & 0xF0) == 0xE0) {
|
||||
if (i + 2 >= s.size())
|
||||
return {0xFFFD, 1};
|
||||
unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
|
||||
unsigned char c2 = static_cast<unsigned char>(s[i + 2]);
|
||||
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80)
|
||||
return {0xFFFD, 1};
|
||||
uint32_t cp = ((c0 & 0x0F) << 12) | ((c1 & 0x3F) << 6) | (c2 & 0x3F);
|
||||
// Overlong / surrogate range check
|
||||
if (cp < 0x800 || (cp >= 0xD800 && cp <= 0xDFFF))
|
||||
return {0xFFFD, 1};
|
||||
return {cp, 3};
|
||||
}
|
||||
if ((c0 & 0xF8) == 0xF0) {
|
||||
if (i + 3 >= s.size())
|
||||
return {0xFFFD, 1};
|
||||
unsigned char c1 = static_cast<unsigned char>(s[i + 1]);
|
||||
unsigned char c2 = static_cast<unsigned char>(s[i + 2]);
|
||||
unsigned char c3 = static_cast<unsigned char>(s[i + 3]);
|
||||
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80 || (c3 & 0xC0) != 0x80)
|
||||
return {0xFFFD, 1};
|
||||
uint32_t cp = ((c0 & 0x07) << 18) | ((c1 & 0x3F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
|
||||
// Overlong / max range check
|
||||
if (cp < 0x10000 || cp > 0x10FFFF)
|
||||
return {0xFFFD, 1};
|
||||
return {cp, 4};
|
||||
}
|
||||
return {0xFFFD, 1};
|
||||
}
|
||||
|
||||
|
||||
static inline size_t
|
||||
utf16UnitsForCodepoint(uint32_t cp)
|
||||
{
|
||||
return (cp <= 0xFFFF) ? 1 : 2;
|
||||
}
|
||||
|
||||
|
||||
size_t
|
||||
utf8ColToUtf16Units(std::string_view lineUtf8, size_t utf8Col)
|
||||
{
|
||||
// Count by Unicode scalars up to utf8Col; clamp at EOL
|
||||
size_t units = 0;
|
||||
size_t col = 0;
|
||||
size_t i = 0;
|
||||
while (i < lineUtf8.size()) {
|
||||
if (col >= utf8Col)
|
||||
break;
|
||||
auto [cp, n] = decodeUtf8(lineUtf8, i);
|
||||
if (n == 0)
|
||||
break;
|
||||
units += utf16UnitsForCodepoint(cp);
|
||||
i += n;
|
||||
++col;
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
|
||||
size_t
|
||||
utf16UnitsToUtf8Col(std::string_view lineUtf8, size_t utf16Units)
|
||||
{
|
||||
// Traverse code points until consuming utf16Units (or reaching EOL)
|
||||
size_t units = 0;
|
||||
size_t col = 0;
|
||||
size_t i = 0;
|
||||
while (i < lineUtf8.size()) {
|
||||
auto [cp, n] = decodeUtf8(lineUtf8, i);
|
||||
if (n == 0)
|
||||
break;
|
||||
size_t add = utf16UnitsForCodepoint(cp);
|
||||
if (units + add > utf16Units)
|
||||
break;
|
||||
units += add;
|
||||
i += n;
|
||||
++col;
|
||||
if (units == utf16Units)
|
||||
break;
|
||||
}
|
||||
return col;
|
||||
}
|
||||
|
||||
|
||||
Position
|
||||
toUtf16(const std::string &uri, const Position &pUtf8, const LineProvider &provider)
|
||||
{
|
||||
Position out = pUtf8;
|
||||
std::string_view line = provider ? provider(uri, pUtf8.line) : std::string_view();
|
||||
out.character = static_cast<int>(utf8ColToUtf16Units(line, static_cast<size_t>(pUtf8.character)));
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Position
|
||||
toUtf8(const std::string &uri, const Position &pUtf16, const LineProvider &provider)
|
||||
{
|
||||
Position out = pUtf16;
|
||||
std::string_view line = provider ? provider(uri, pUtf16.line) : std::string_view();
|
||||
out.character = static_cast<int>(utf16UnitsToUtf8Col(line, static_cast<size_t>(pUtf16.character)));
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Range
|
||||
toUtf16(const std::string &uri, const Range &rUtf8, const LineProvider &provider)
|
||||
{
|
||||
Range r;
|
||||
r.start = toUtf16(uri, rUtf8.start, provider);
|
||||
r.end = toUtf16(uri, rUtf8.end, provider);
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
Range
|
||||
toUtf8(const std::string &uri, const Range &rUtf16, const LineProvider &provider)
|
||||
{
|
||||
Range r;
|
||||
r.start = toUtf8(uri, rUtf16.start, provider);
|
||||
r.end = toUtf8(uri, rUtf16.end, provider);
|
||||
return r;
|
||||
}
|
||||
} // namespace kte::lsp
|
||||
37
lsp/UtfCodec.h
Normal file
37
lsp/UtfCodec.h
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* UtfCodec.h - Helpers for UTF-8 <-> UTF-16 code unit position conversions
|
||||
*/
|
||||
#ifndef KTE_LSP_UTF_CODEC_H
|
||||
#define KTE_LSP_UTF_CODEC_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "LspTypes.h"
|
||||
|
||||
namespace kte::lsp {
|
||||
// Map between editor-internal UTF-8 columns (by Unicode scalar count)
|
||||
// and LSP wire UTF-16 code units (per LSP spec).
|
||||
|
||||
// Convert a UTF-8 column index (in Unicode scalars) to UTF-16 code units for a given line.
|
||||
size_t utf8ColToUtf16Units(std::string_view lineUtf8, size_t utf8Col);
|
||||
|
||||
// Convert a UTF-16 code unit count to a UTF-8 column index (in Unicode scalars) for a given line.
|
||||
size_t utf16UnitsToUtf8Col(std::string_view lineUtf8, size_t utf16Units);
|
||||
|
||||
// Line text provider to allow conversions without giving the codec direct buffer access.
|
||||
using LineProvider = std::function<std::string_view(const std::string & uri, int line)>;
|
||||
|
||||
// Convenience helpers for positions and ranges using a line provider.
|
||||
Position toUtf16(const std::string &uri, const Position &pUtf8, const LineProvider &provider);
|
||||
|
||||
Position toUtf8(const std::string &uri, const Position &pUtf16, const LineProvider &provider);
|
||||
|
||||
Range toUtf16(const std::string &uri, const Range &rUtf8, const LineProvider &provider);
|
||||
|
||||
Range toUtf8(const std::string &uri, const Range &rUtf16, const LineProvider &provider);
|
||||
} // namespace kte::lsp
|
||||
|
||||
#endif // KTE_LSP_UTF_CODEC_H
|
||||
49
main.cc
49
main.cc
@@ -12,6 +12,7 @@
|
||||
#include "Editor.h"
|
||||
#include "Frontend.h"
|
||||
#include "TerminalFrontend.h"
|
||||
#include "lsp/LspManager.h"
|
||||
|
||||
#if defined(KTE_BUILD_GUI)
|
||||
#include "GUIFrontend.h"
|
||||
@@ -28,6 +29,8 @@ PrintUsage(const char *prog)
|
||||
{
|
||||
std::cerr << "Usage: " << prog << " [OPTIONS] [files]\n"
|
||||
<< "Options:\n"
|
||||
<< " -c, --chdir DIR Change working directory before opening files\n"
|
||||
<< " -d, --debug Enable LSP debug logging\n"
|
||||
<< " -g, --gui Use GUI frontend (if built)\n"
|
||||
<< " -t, --term Use terminal (ncurses) frontend [default]\n"
|
||||
<< " -h, --help Show this help and exit\n"
|
||||
@@ -36,17 +39,25 @@ PrintUsage(const char *prog)
|
||||
|
||||
|
||||
int
|
||||
main(int argc, const char *argv[])
|
||||
main(const int argc, const char *argv[])
|
||||
{
|
||||
Editor editor;
|
||||
// Wire up LSP manager (no diagnostic UI yet; frontends may provide later)
|
||||
kte::lsp::LspManager lspMgr(&editor, nullptr);
|
||||
editor.SetLspManager(&lspMgr);
|
||||
|
||||
// CLI parsing using getopt_long
|
||||
bool req_gui = false;
|
||||
bool req_term = false;
|
||||
bool show_help = false;
|
||||
bool show_version = false;
|
||||
bool lsp_debug = false;
|
||||
|
||||
std::string nwd;
|
||||
|
||||
static struct option long_opts[] = {
|
||||
{"chdir", required_argument, nullptr, 'c'},
|
||||
{"debug", no_argument, nullptr, 'd'},
|
||||
{"gui", no_argument, nullptr, 'g'},
|
||||
{"term", no_argument, nullptr, 't'},
|
||||
{"help", no_argument, nullptr, 'h'},
|
||||
@@ -56,8 +67,14 @@ main(int argc, const char *argv[])
|
||||
|
||||
int opt;
|
||||
int long_index = 0;
|
||||
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "gthV", long_opts, &long_index)) != -1) {
|
||||
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "c:dgthV", long_opts, &long_index)) != -1) {
|
||||
switch (opt) {
|
||||
case 'c':
|
||||
nwd = optarg;
|
||||
break;
|
||||
case 'd':
|
||||
lsp_debug = true;
|
||||
break;
|
||||
case 'g':
|
||||
req_gui = true;
|
||||
break;
|
||||
@@ -90,6 +107,16 @@ main(int argc, const char *argv[])
|
||||
(void) req_term; // suppress unused warning when GUI is not compiled in
|
||||
#endif
|
||||
|
||||
// Apply LSP debug setting strictly based on -d flag
|
||||
lspMgr.setDebugLogging(lsp_debug);
|
||||
if (lsp_debug) {
|
||||
// Ensure LSP subprocess client picks up debug via environment
|
||||
::setenv("KTE_LSP_DEBUG", "1", 1);
|
||||
} else {
|
||||
// Prevent environment from enabling debug implicitly
|
||||
::unsetenv("KTE_LSP_DEBUG");
|
||||
}
|
||||
|
||||
// Determine frontend
|
||||
#if !defined(KTE_BUILD_GUI)
|
||||
if (req_gui) {
|
||||
@@ -104,11 +131,14 @@ main(int argc, const char *argv[])
|
||||
} else if (req_term) {
|
||||
use_gui = false;
|
||||
} else {
|
||||
// Default depends on build target: kge defaults to GUI, kte to terminal
|
||||
|
||||
|
||||
|
||||
// Default depends on build target: kge defaults to GUI, kte to terminal
|
||||
#if defined(KTE_DEFAULT_GUI)
|
||||
use_gui = true;
|
||||
use_gui = true;
|
||||
#else
|
||||
use_gui = false;
|
||||
use_gui = false;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
@@ -199,6 +229,13 @@ main(int argc, const char *argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(KTE_BUILD_GUI)
|
||||
if (!nwd.empty()) {
|
||||
if (chdir(nwd.c_str()) != 0) {
|
||||
std::cerr << "kge: failed to chdir to " << nwd << std::endl;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!fe->Init(editor)) {
|
||||
std::cerr << "kte: failed to initialize frontend" << std::endl;
|
||||
return 1;
|
||||
@@ -212,4 +249,4 @@ main(int argc, const char *argv[])
|
||||
fe->Shutdown();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
279
syntax/CppHighlighter.cc
Normal file
279
syntax/CppHighlighter.cc
Normal file
@@ -0,0 +1,279 @@
|
||||
#include "CppHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static bool
|
||||
is_digit(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
|
||||
CppHighlighter::CppHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue",
|
||||
"return", "goto", "struct", "class", "namespace", "using", "template", "typename",
|
||||
"public", "private", "protected", "virtual", "override", "const", "constexpr", "auto",
|
||||
"static", "inline", "operator", "new", "delete", "try", "catch", "throw", "friend",
|
||||
"enum", "union", "extern", "volatile", "mutable", "noexcept", "sizeof", "this"
|
||||
};
|
||||
for (auto s: kw)
|
||||
keywords_.insert(s);
|
||||
const char *types[] = {
|
||||
"int", "long", "short", "char", "signed", "unsigned", "float", "double", "void",
|
||||
"bool", "wchar_t", "size_t", "ptrdiff_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t",
|
||||
"int8_t", "int16_t", "int32_t", "int64_t"
|
||||
};
|
||||
for (auto s: types)
|
||||
types_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
CppHighlighter::is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
CppHighlighter::is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
CppHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
// Stateless entry simply delegates to stateful with a clean previous state
|
||||
StatefulHighlighter::LineState prev;
|
||||
(void) HighlightLineStateful(buf, row, prev, out);
|
||||
}
|
||||
|
||||
|
||||
StatefulHighlighter::LineState
|
||||
CppHighlighter::HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
StatefulHighlighter::LineState state = prev;
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return state;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
if (s.empty())
|
||||
return state;
|
||||
|
||||
auto push = [&](int a, int b, TokenKind k) {
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
};
|
||||
int n = static_cast<int>(s.size());
|
||||
int bol = 0;
|
||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||
++bol;
|
||||
int i = 0;
|
||||
|
||||
// Continue multi-line raw string from previous line
|
||||
if (state.in_raw_string) {
|
||||
std::string needle = ")" + state.raw_delim + "\"";
|
||||
auto pos = s.find(needle);
|
||||
if (pos == std::string::npos) {
|
||||
push(0, n, TokenKind::String);
|
||||
state.in_raw_string = true;
|
||||
return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + needle.size());
|
||||
push(0, end, TokenKind::String);
|
||||
i = end;
|
||||
state.in_raw_string = false;
|
||||
state.raw_delim.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Continue multi-line block comment from previous line
|
||||
if (state.in_block_comment) {
|
||||
int j = i;
|
||||
while (i + 1 < n) {
|
||||
if (s[i] == '*' && s[i + 1] == '/') {
|
||||
i += 2;
|
||||
push(j, i, TokenKind::Comment);
|
||||
state.in_block_comment = false;
|
||||
break;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
if (state.in_block_comment) {
|
||||
push(j, n, TokenKind::Comment);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
// Preprocessor at beginning of line (after leading whitespace)
|
||||
if (i == bol && c == '#') {
|
||||
push(0, n, TokenKind::Preproc);
|
||||
break;
|
||||
}
|
||||
|
||||
// Whitespace
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Line comment
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
|
||||
push(i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
|
||||
// Block comment
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||
int j = i + 2;
|
||||
bool closed = false;
|
||||
while (j + 1 <= n) {
|
||||
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||
j += 2;
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (closed) {
|
||||
push(i, j, TokenKind::Comment);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// Spill to next lines
|
||||
push(i, n, TokenKind::Comment);
|
||||
state.in_block_comment = true;
|
||||
return state;
|
||||
}
|
||||
|
||||
// Raw string start: very simple detection: R"delim(
|
||||
if (c == 'R' && i + 1 < n && s[i + 1] == '"') {
|
||||
int k = i + 2;
|
||||
std::string delim;
|
||||
while (k < n && s[k] != '(') {
|
||||
delim.push_back(s[k]);
|
||||
++k;
|
||||
}
|
||||
if (k < n && s[k] == '(') {
|
||||
int body_start = k + 1;
|
||||
std::string needle = ")" + delim + "\"";
|
||||
auto pos = s.find(needle, static_cast<std::size_t>(body_start));
|
||||
if (pos == std::string::npos) {
|
||||
push(i, n, TokenKind::String);
|
||||
state.in_raw_string = true;
|
||||
state.raw_delim = delim;
|
||||
return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + needle.size());
|
||||
push(i, end, TokenKind::String);
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// If malformed, just treat 'R' as identifier fallback
|
||||
}
|
||||
|
||||
// Regular string literal
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
push(i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Char literal
|
||||
if (c == '\'') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '\'')
|
||||
break;
|
||||
}
|
||||
push(i, j, TokenKind::Char);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Number literal (simple)
|
||||
if (is_digit(c) || (c == '.' && i + 1 < n && is_digit(s[i + 1]))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'x' ||
|
||||
s[j] == 'X' || s[j] == 'b' || s[j] == 'B' || s[j] == '_'))
|
||||
++j;
|
||||
push(i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Identifier / keyword / type
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (keywords_.count(id))
|
||||
k = TokenKind::Keyword;
|
||||
else if (types_.count(id))
|
||||
k = TokenKind::Type;
|
||||
push(i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Operators and punctuation (single char for now)
|
||||
TokenKind kind = TokenKind::Operator;
|
||||
if (std::ispunct(static_cast<unsigned char>(c)) && c != '_' && c != '#') {
|
||||
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
|
||||
']')
|
||||
kind = TokenKind::Punctuation;
|
||||
push(i, i + 1, kind);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
push(i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
} // namespace kte
|
||||
35
syntax/CppHighlighter.h
Normal file
35
syntax/CppHighlighter.h
Normal file
@@ -0,0 +1,35 @@
|
||||
// CppHighlighter.h - minimal stateless C/C++ line highlighter
|
||||
#pragma once
|
||||
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
class CppHighlighter final : public StatefulHighlighter {
|
||||
public:
|
||||
CppHighlighter();
|
||||
|
||||
~CppHighlighter() override = default;
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
LineState HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> keywords_;
|
||||
std::unordered_set<std::string> types_;
|
||||
|
||||
static bool is_ident_start(char c);
|
||||
|
||||
static bool is_ident_char(char c);
|
||||
};
|
||||
} // namespace kte
|
||||
159
syntax/ErlangHighlighter.cc
Normal file
159
syntax/ErlangHighlighter.cc
Normal file
@@ -0,0 +1,159 @@
|
||||
#include "ErlangHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_' || c == '\'';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '@' || c == ':' || c == '?';
|
||||
}
|
||||
|
||||
|
||||
ErlangHighlighter::ErlangHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"after", "begin", "case", "catch", "cond", "div", "end", "fun", "if", "let", "of",
|
||||
"receive", "when", "try", "rem", "and", "andalso", "orelse", "not", "band", "bor", "bxor",
|
||||
"bnot", "xor", "module", "export", "import", "record", "define", "undef", "include", "include_lib"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ErlangHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// comment
|
||||
if (c == '%') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
// strings
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// char literal $X
|
||||
if (c == '$') {
|
||||
int j = i + 1;
|
||||
if (j < n && s[j] == '\\' && j + 1 < n)
|
||||
j += 2;
|
||||
else if (j < n)
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Char);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// numbers
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '#' || s[j] == '.' ||
|
||||
s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// atoms/variables/identifiers (including quoted atoms)
|
||||
if (is_ident_start(c)) {
|
||||
// quoted atom: '...'
|
||||
if (c == '\'') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (d == '\'') {
|
||||
if (j < n && s[j] == '\'') {
|
||||
++j;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (d == '\\')
|
||||
esc = !esc;
|
||||
}
|
||||
push(out, i, j, TokenKind::Identifier);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
// lowercase leading -> atom/function/module; uppercase or '_' -> variable
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
// keyword check (lowercase)
|
||||
std::string lower;
|
||||
lower.reserve(id.size());
|
||||
for (char ch: id)
|
||||
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||
if (kws_.count(lower))
|
||||
k = TokenKind::Keyword;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ',' || c == ';' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c ==
|
||||
'}')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
17
syntax/ErlangHighlighter.h
Normal file
17
syntax/ErlangHighlighter.h
Normal file
@@ -0,0 +1,17 @@
|
||||
// ErlangHighlighter.h - simple Erlang highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class ErlangHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
ErlangHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
};
|
||||
} // namespace kte
|
||||
121
syntax/ForthHighlighter.cc
Normal file
121
syntax/ForthHighlighter.cc
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "ForthHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_word_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '>' || c == '<' || c == '?';
|
||||
}
|
||||
|
||||
|
||||
ForthHighlighter::ForthHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
":", ";", "if", "else", "then", "begin", "until", "while", "repeat",
|
||||
"do", "loop", "+loop", "leave", "again", "case", "of", "endof", "endcase",
|
||||
".", ".r", ".s", ".\"", ",", "cr", "emit", "type", "key",
|
||||
"+", "-", "*", "/", "mod", "/mod", "+-", "abs", "min", "max",
|
||||
"dup", "drop", "swap", "over", "rot", "-rot", "nip", "tuck", "pick", "roll",
|
||||
"and", "or", "xor", "invert", "lshift", "rshift",
|
||||
"variable", "constant", "value", "to", "create", "does>", "allot", ",",
|
||||
"cells", "cell+", "chars", "char+",
|
||||
"[", "]", "immediate",
|
||||
"s\"", ".\""
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ForthHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// backslash comment to end of line
|
||||
if (c == '\\') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
// parenthesis comment ( ... ) if at word boundary
|
||||
if (c == '(') {
|
||||
int j = i + 1;
|
||||
while (j < n && s[j] != ')')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Comment);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// strings: ." ... " and S" ... " and raw "..."
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
while (j < n && s[j] != '"')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '#'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// word/identifier
|
||||
if (std::isalpha(static_cast<unsigned char>(c)) || std::ispunct(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_word_char(s[j]))
|
||||
++j;
|
||||
std::string w = s.substr(i, j - i);
|
||||
// normalize to lowercase for keyword compare (Forth is case-insensitive typically)
|
||||
std::string lower;
|
||||
lower.reserve(w.size());
|
||||
for (char ch: w)
|
||||
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||
TokenKind k = kws_.count(lower) ? TokenKind::Keyword : TokenKind::Identifier;
|
||||
// Single-char punctuation fallback
|
||||
if (w.size() == 1 && std::ispunct(static_cast<unsigned char>(w[0])) && !kws_.count(lower)) {
|
||||
k = (w[0] == '(' || w[0] == ')' || w[0] == ',')
|
||||
? TokenKind::Punctuation
|
||||
: TokenKind::Operator;
|
||||
}
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
17
syntax/ForthHighlighter.h
Normal file
17
syntax/ForthHighlighter.h
Normal file
@@ -0,0 +1,17 @@
|
||||
// ForthHighlighter.h - simple Forth highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class ForthHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
ForthHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
};
|
||||
} // namespace kte
|
||||
157
syntax/GoHighlighter.cc
Normal file
157
syntax/GoHighlighter.cc
Normal file
@@ -0,0 +1,157 @@
|
||||
#include "GoHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
GoHighlighter::GoHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", "func",
|
||||
"go", "goto", "if", "import", "interface", "map", "package", "range", "return", "select", "struct",
|
||||
"switch", "type", "var"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
const char *tp[] = {
|
||||
"bool", "byte", "complex64", "complex128", "error", "float32", "float64", "int", "int8", "int16",
|
||||
"int32", "int64", "rune", "string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr"
|
||||
};
|
||||
for (auto s: tp)
|
||||
types_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
int bol = 0;
|
||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||
++bol;
|
||||
// line comment
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||
int j = i + 2;
|
||||
bool closed = false;
|
||||
while (j + 1 <= n) {
|
||||
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||
j += 2;
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (!closed) {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
} else {
|
||||
push(out, i, j, TokenKind::Comment);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (c == '"' || c == '`') {
|
||||
char q = c;
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
if (q == '`') {
|
||||
while (j < n && s[j] != '`')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
} else {
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'x' ||
|
||||
s[j] == 'X' || s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (kws_.count(id))
|
||||
k = TokenKind::Keyword;
|
||||
else if (types_.count(id))
|
||||
k = TokenKind::Type;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
|
||||
']')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
18
syntax/GoHighlighter.h
Normal file
18
syntax/GoHighlighter.h
Normal file
@@ -0,0 +1,18 @@
|
||||
// GoHighlighter.h - simple Go highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class GoHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
GoHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
std::unordered_set<std::string> types_;
|
||||
};
|
||||
} // namespace kte
|
||||
209
syntax/HighlighterEngine.cc
Normal file
209
syntax/HighlighterEngine.cc
Normal file
@@ -0,0 +1,209 @@
|
||||
#include "HighlighterEngine.h"
|
||||
#include "../Buffer.h"
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <thread>
|
||||
|
||||
namespace kte {
|
||||
HighlighterEngine::HighlighterEngine() = default;
|
||||
|
||||
|
||||
HighlighterEngine::~HighlighterEngine()
|
||||
{
|
||||
// stop background worker
|
||||
if (worker_running_.load()) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
worker_running_.store(false);
|
||||
has_request_ = true; // wake it up to exit
|
||||
}
|
||||
cv_.notify_one();
|
||||
if (worker_.joinable())
|
||||
worker_.join();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HighlighterEngine::SetHighlighter(std::unique_ptr<LanguageHighlighter> hl)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
hl_ = std::move(hl);
|
||||
cache_.clear();
|
||||
state_cache_.clear();
|
||||
state_last_contig_.clear();
|
||||
}
|
||||
|
||||
|
||||
const LineHighlight &
|
||||
HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mtx_);
|
||||
auto it = cache_.find(row);
|
||||
if (it != cache_.end() && it->second.version == buf_version) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Prepare destination slot to reuse its capacity and avoid allocations
|
||||
LineHighlight &slot = cache_[row];
|
||||
slot.version = buf_version;
|
||||
slot.spans.clear();
|
||||
|
||||
if (!hl_) {
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Copy shared_ptr-like raw pointer for use outside critical sections
|
||||
LanguageHighlighter *hl_ptr = hl_.get();
|
||||
bool is_stateful = dynamic_cast<StatefulHighlighter *>(hl_ptr) != nullptr;
|
||||
|
||||
if (!is_stateful) {
|
||||
// Stateless fast path: we can release the lock while computing to reduce contention
|
||||
auto &out = slot.spans;
|
||||
lock.unlock();
|
||||
hl_ptr->HighlightLine(buf, row, out);
|
||||
return cache_.at(row);
|
||||
}
|
||||
|
||||
// Stateful path: we need to walk from a known previous state. Keep lock while consulting caches,
|
||||
// but release during heavy computation.
|
||||
auto *stateful = static_cast<StatefulHighlighter *>(hl_ptr);
|
||||
|
||||
StatefulHighlighter::LineState prev_state;
|
||||
int start_row = -1;
|
||||
if (!state_cache_.empty()) {
|
||||
// linear search over map (unordered), track best candidate
|
||||
int best = -1;
|
||||
for (const auto &kv: state_cache_) {
|
||||
int r = kv.first;
|
||||
if (r <= row - 1 && kv.second.version == buf_version) {
|
||||
if (r > best)
|
||||
best = r;
|
||||
}
|
||||
}
|
||||
if (best >= 0) {
|
||||
start_row = best;
|
||||
prev_state = state_cache_.at(best).state;
|
||||
}
|
||||
}
|
||||
|
||||
// We'll compute states and the target line's spans without holding the lock for most of the work.
|
||||
// Create a local copy of prev_state and iterate rows; we will update caches under lock.
|
||||
lock.unlock();
|
||||
StatefulHighlighter::LineState cur_state = prev_state;
|
||||
for (int r = start_row + 1; r <= row; ++r) {
|
||||
std::vector<HighlightSpan> tmp;
|
||||
std::vector<HighlightSpan> &out = (r == row) ? slot.spans : tmp;
|
||||
auto next_state = stateful->HighlightLineStateful(buf, r, cur_state, out);
|
||||
// Update state cache for r
|
||||
std::lock_guard<std::mutex> gl(mtx_);
|
||||
StateEntry se;
|
||||
se.version = buf_version;
|
||||
se.state = next_state;
|
||||
state_cache_[r] = se;
|
||||
cur_state = next_state;
|
||||
}
|
||||
|
||||
// Return reference under lock to ensure slot's address stability in map
|
||||
lock.lock();
|
||||
return cache_.at(row);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HighlighterEngine::InvalidateFrom(int row)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
if (cache_.empty())
|
||||
return;
|
||||
// Simple implementation: erase all rows >= row
|
||||
for (auto it = cache_.begin(); it != cache_.end();) {
|
||||
if (it->first >= row)
|
||||
it = cache_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
if (!state_cache_.empty()) {
|
||||
for (auto it = state_cache_.begin(); it != state_cache_.end();) {
|
||||
if (it->first >= row)
|
||||
it = state_cache_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HighlighterEngine::ensure_worker_started() const
|
||||
{
|
||||
if (worker_running_.load())
|
||||
return;
|
||||
worker_running_.store(true);
|
||||
worker_ = std::thread([this]() {
|
||||
this->worker_loop();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HighlighterEngine::worker_loop() const
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mtx_);
|
||||
while (worker_running_.load()) {
|
||||
cv_.wait(lock, [this]() {
|
||||
return has_request_ || !worker_running_.load();
|
||||
});
|
||||
if (!worker_running_.load())
|
||||
break;
|
||||
WarmRequest req = pending_;
|
||||
has_request_ = false;
|
||||
// Copy locals then release lock while computing
|
||||
lock.unlock();
|
||||
if (req.buf) {
|
||||
int start = std::max(0, req.start_row);
|
||||
int end = std::max(start, req.end_row);
|
||||
for (int r = start; r <= end; ++r) {
|
||||
// Re-check version staleness quickly by peeking cache version; not strictly necessary
|
||||
// Compute line; GetLine is thread-safe
|
||||
(void) this->GetLine(*req.buf, r, req.version);
|
||||
}
|
||||
}
|
||||
lock.lock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HighlighterEngine::PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version,
|
||||
int warm_margin) const
|
||||
{
|
||||
if (row_count <= 0)
|
||||
return;
|
||||
// Synchronously compute visible rows to ensure cache hits during draw
|
||||
int start = std::max(0, first_row);
|
||||
int end = start + row_count - 1;
|
||||
int max_rows = static_cast<int>(buf.Nrows());
|
||||
if (start >= max_rows)
|
||||
return;
|
||||
if (end >= max_rows)
|
||||
end = max_rows - 1;
|
||||
|
||||
for (int r = start; r <= end; ++r) {
|
||||
(void) GetLine(buf, r, buf_version);
|
||||
}
|
||||
|
||||
// Enqueue background warm-around
|
||||
int warm_start = std::max(0, start - warm_margin);
|
||||
int warm_end = std::min(max_rows - 1, end + warm_margin);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
pending_.buf = &buf;
|
||||
pending_.version = buf_version;
|
||||
pending_.start_row = warm_start;
|
||||
pending_.end_row = warm_end;
|
||||
has_request_ = true;
|
||||
}
|
||||
ensure_worker_started();
|
||||
cv_.notify_one();
|
||||
}
|
||||
} // namespace kte
|
||||
85
syntax/HighlighterEngine.h
Normal file
85
syntax/HighlighterEngine.h
Normal file
@@ -0,0 +1,85 @@
|
||||
// HighlighterEngine.h - caching layer for per-line highlights
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
#include "../Highlight.h"
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
class HighlighterEngine {
|
||||
public:
|
||||
HighlighterEngine();
|
||||
|
||||
~HighlighterEngine();
|
||||
|
||||
void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl);
|
||||
|
||||
// Retrieve highlights for a given line and buffer version.
|
||||
// If cache is stale, recompute using the current highlighter.
|
||||
const LineHighlight &GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
|
||||
|
||||
// Invalidate cached lines from row (inclusive)
|
||||
void InvalidateFrom(int row);
|
||||
|
||||
|
||||
bool HasHighlighter() const
|
||||
{
|
||||
return static_cast<bool>(hl_);
|
||||
}
|
||||
|
||||
|
||||
// Phase 3: viewport-first prefetch and background warming
|
||||
// Compute only the visible range now, and enqueue a background warm-around task.
|
||||
// warm_margin: how many extra lines above/below to warm in the background.
|
||||
void PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version,
|
||||
int warm_margin = 200) const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<LanguageHighlighter> hl_;
|
||||
// Simple cache by row index (mutable to allow caching in const GetLine)
|
||||
mutable std::unordered_map<int, LineHighlight> cache_;
|
||||
|
||||
// For stateful highlighters, remember per-line state (state after finishing that row)
|
||||
struct StateEntry {
|
||||
std::uint64_t version{0};
|
||||
// Using the interface type; forward-declare via header
|
||||
StatefulHighlighter::LineState state;
|
||||
};
|
||||
|
||||
mutable std::unordered_map<int, StateEntry> state_cache_;
|
||||
|
||||
// Track best known contiguous state row for a given version to avoid O(n) scans
|
||||
mutable std::unordered_map<std::uint64_t, int> state_last_contig_;
|
||||
|
||||
// Thread-safety for caches and background worker state
|
||||
mutable std::mutex mtx_;
|
||||
|
||||
// Background warmer
|
||||
struct WarmRequest {
|
||||
const Buffer *buf{nullptr};
|
||||
std::uint64_t version{0};
|
||||
int start_row{0};
|
||||
int end_row{0}; // inclusive
|
||||
};
|
||||
|
||||
mutable std::condition_variable cv_;
|
||||
mutable std::thread worker_;
|
||||
mutable std::atomic<bool> worker_running_{false};
|
||||
mutable bool has_request_{false};
|
||||
mutable WarmRequest pending_{};
|
||||
|
||||
void ensure_worker_started() const;
|
||||
|
||||
void worker_loop() const;
|
||||
};
|
||||
} // namespace kte
|
||||
247
syntax/HighlighterRegistry.cc
Normal file
247
syntax/HighlighterRegistry.cc
Normal file
@@ -0,0 +1,247 @@
|
||||
#include "HighlighterRegistry.h"
|
||||
#include "CppHighlighter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
#include <cctype>
|
||||
|
||||
// Forward declare simple highlighters implemented in this project
|
||||
namespace kte {
|
||||
// Registration storage
|
||||
struct RegEntry {
|
||||
std::string ft; // normalized
|
||||
HighlighterRegistry::Factory factory;
|
||||
};
|
||||
|
||||
|
||||
static std::vector<RegEntry> &
|
||||
registry()
|
||||
{
|
||||
static std::vector<RegEntry> reg;
|
||||
return reg;
|
||||
}
|
||||
|
||||
|
||||
class JSONHighlighter;
|
||||
class MarkdownHighlighter;
|
||||
class ShellHighlighter;
|
||||
class GoHighlighter;
|
||||
class PythonHighlighter;
|
||||
class RustHighlighter;
|
||||
class LispHighlighter;
|
||||
class SqlHighlighter;
|
||||
class ErlangHighlighter;
|
||||
class ForthHighlighter;
|
||||
}
|
||||
|
||||
// Headers for the above
|
||||
#include "JsonHighlighter.h"
|
||||
#include "MarkdownHighlighter.h"
|
||||
#include "ShellHighlighter.h"
|
||||
#include "GoHighlighter.h"
|
||||
#include "PythonHighlighter.h"
|
||||
#include "RustHighlighter.h"
|
||||
#include "LispHighlighter.h"
|
||||
#include "SqlHighlighter.h"
|
||||
#include "ErlangHighlighter.h"
|
||||
#include "ForthHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
static std::string
|
||||
to_lower(std::string_view s)
|
||||
{
|
||||
std::string r(s);
|
||||
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
HighlighterRegistry::Normalize(std::string_view ft)
|
||||
{
|
||||
std::string f = to_lower(ft);
|
||||
if (f == "c" || f == "c++" || f == "cc" || f == "hpp" || f == "hh" || f == "h" || f == "cxx")
|
||||
return "cpp";
|
||||
if (f == "cpp")
|
||||
return "cpp";
|
||||
if (f == "json")
|
||||
return "json";
|
||||
if (f == "markdown" || f == "md" || f == "mkd" || f == "mdown")
|
||||
return "markdown";
|
||||
if (f == "shell" || f == "sh" || f == "bash" || f == "zsh" || f == "ksh" || f == "fish")
|
||||
return "shell";
|
||||
if (f == "go" || f == "golang")
|
||||
return "go";
|
||||
if (f == "py" || f == "python")
|
||||
return "python";
|
||||
if (f == "rs" || f == "rust")
|
||||
return "rust";
|
||||
if (f == "lisp" || f == "scheme" || f == "scm" || f == "rkt" || f == "el" || f == "clj" || f == "cljc" || f ==
|
||||
"cl")
|
||||
return "lisp";
|
||||
if (f == "sql" || f == "sqlite" || f == "sqlite3")
|
||||
return "sql";
|
||||
if (f == "erlang" || f == "erl" || f == "hrl")
|
||||
return "erlang";
|
||||
if (f == "forth" || f == "fth" || f == "4th" || f == "fs")
|
||||
return "forth";
|
||||
return f;
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<LanguageHighlighter>
|
||||
HighlighterRegistry::CreateFor(std::string_view filetype)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
// Prefer externally registered factories
|
||||
for (const auto &e: registry()) {
|
||||
if (e.ft == ft && e.factory)
|
||||
return e.factory();
|
||||
}
|
||||
if (ft == "cpp")
|
||||
return std::make_unique<CppHighlighter>();
|
||||
if (ft == "json")
|
||||
return std::make_unique<JSONHighlighter>();
|
||||
if (ft == "markdown")
|
||||
return std::make_unique<MarkdownHighlighter>();
|
||||
if (ft == "shell")
|
||||
return std::make_unique<ShellHighlighter>();
|
||||
if (ft == "go")
|
||||
return std::make_unique<GoHighlighter>();
|
||||
if (ft == "python")
|
||||
return std::make_unique<PythonHighlighter>();
|
||||
if (ft == "rust")
|
||||
return std::make_unique<RustHighlighter>();
|
||||
if (ft == "lisp")
|
||||
return std::make_unique<LispHighlighter>();
|
||||
if (ft == "sql")
|
||||
return std::make_unique<SqlHighlighter>();
|
||||
if (ft == "erlang")
|
||||
return std::make_unique<ErlangHighlighter>();
|
||||
if (ft == "forth")
|
||||
return std::make_unique<ForthHighlighter>();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
static std::string
|
||||
shebang_to_ft(std::string_view first_line)
|
||||
{
|
||||
if (first_line.size() < 2 || first_line.substr(0, 2) != "#!")
|
||||
return "";
|
||||
std::string low = to_lower(first_line);
|
||||
if (low.find("python") != std::string::npos)
|
||||
return "python";
|
||||
if (low.find("bash") != std::string::npos)
|
||||
return "shell";
|
||||
if (low.find("sh") != std::string::npos)
|
||||
return "shell";
|
||||
if (low.find("zsh") != std::string::npos)
|
||||
return "shell";
|
||||
if (low.find("fish") != std::string::npos)
|
||||
return "shell";
|
||||
if (low.find("scheme") != std::string::npos || low.find("racket") != std::string::npos || low.find("guile") !=
|
||||
std::string::npos)
|
||||
return "lisp";
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
std::string
|
||||
HighlighterRegistry::DetectForPath(std::string_view path, std::string_view first_line)
|
||||
{
|
||||
// Extension
|
||||
std::string p(path);
|
||||
std::error_code ec;
|
||||
std::string ext = std::filesystem::path(p).extension().string();
|
||||
for (auto &ch: ext)
|
||||
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
|
||||
if (!ext.empty()) {
|
||||
if (ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".cxx" || ext == ".h" || ext == ".hpp" || ext
|
||||
== ".hh")
|
||||
return "cpp";
|
||||
if (ext == ".json")
|
||||
return "json";
|
||||
if (ext == ".md" || ext == ".markdown" || ext == ".mkd")
|
||||
return "markdown";
|
||||
if (ext == ".sh" || ext == ".bash" || ext == ".zsh" || ext == ".ksh" || ext == ".fish")
|
||||
return "shell";
|
||||
if (ext == ".go")
|
||||
return "go";
|
||||
if (ext == ".py")
|
||||
return "python";
|
||||
if (ext == ".rs")
|
||||
return "rust";
|
||||
if (ext == ".lisp" || ext == ".scm" || ext == ".rkt" || ext == ".el" || ext == ".clj" || ext == ".cljc"
|
||||
|| ext == ".cl")
|
||||
return "lisp";
|
||||
if (ext == ".sql" || ext == ".sqlite")
|
||||
return "sql";
|
||||
if (ext == ".erl" || ext == ".hrl")
|
||||
return "erlang";
|
||||
if (ext == ".forth" || ext == ".fth" || ext == ".4th" || ext == ".fs")
|
||||
return "forth";
|
||||
}
|
||||
// Shebang
|
||||
std::string ft = shebang_to_ft(first_line);
|
||||
return ft;
|
||||
}
|
||||
} // namespace kte
|
||||
|
||||
// Extensibility API implementations
|
||||
namespace kte {
|
||||
void
|
||||
HighlighterRegistry::Register(std::string_view filetype, Factory factory, bool override_existing)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
for (auto &e: registry()) {
|
||||
if (e.ft == ft) {
|
||||
if (override_existing)
|
||||
e.factory = std::move(factory);
|
||||
return;
|
||||
}
|
||||
}
|
||||
registry().push_back(RegEntry{ft, std::move(factory)});
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
HighlighterRegistry::IsRegistered(std::string_view filetype)
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
for (const auto &e: registry())
|
||||
if (e.ft == ft)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::string>
|
||||
HighlighterRegistry::RegisteredFiletypes()
|
||||
{
|
||||
std::vector<std::string> out;
|
||||
out.reserve(registry().size());
|
||||
for (const auto &e: registry())
|
||||
out.push_back(e.ft);
|
||||
return out;
|
||||
}
|
||||
|
||||
#ifdef KTE_ENABLE_TREESITTER
|
||||
// Forward declare adapter factory
|
||||
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
|
||||
const void * (*get_lang)());
|
||||
|
||||
void
|
||||
HighlighterRegistry::RegisterTreeSitter(std::string_view filetype,
|
||||
const TSLanguage * (*get_language)())
|
||||
{
|
||||
std::string ft = Normalize(filetype);
|
||||
Register(ft, [ft, get_language]() {
|
||||
return CreateTreeSitterHighlighter(ft.c_str(), reinterpret_cast<const void* (*)()>(get_language));
|
||||
}, /*override_existing=*/true);
|
||||
}
|
||||
#endif
|
||||
} // namespace kte
|
||||
47
syntax/HighlighterRegistry.h
Normal file
47
syntax/HighlighterRegistry.h
Normal file
@@ -0,0 +1,47 @@
|
||||
// HighlighterRegistry.h - create/detect language highlighters and allow external registration
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
class HighlighterRegistry {
|
||||
public:
|
||||
using Factory = std::function<std::unique_ptr<LanguageHighlighter>()>;
|
||||
|
||||
// Create a highlighter for normalized filetype id (e.g., "cpp", "json", "markdown", "shell", "go", "python", "rust", "lisp").
|
||||
static std::unique_ptr<LanguageHighlighter> CreateFor(std::string_view filetype);
|
||||
|
||||
// Detect filetype by path extension and shebang (first line).
|
||||
// Returns normalized id or empty string if unknown.
|
||||
static std::string DetectForPath(std::string_view path, std::string_view first_line);
|
||||
|
||||
// Normalize various aliases/extensions to canonical ids.
|
||||
static std::string Normalize(std::string_view ft);
|
||||
|
||||
// Extensibility: allow external code to register highlighters at runtime.
|
||||
// The filetype key is normalized via Normalize(). If a factory is already registered for the
|
||||
// normalized key and override=false, the existing factory is kept.
|
||||
static void Register(std::string_view filetype, Factory factory, bool override_existing = true);
|
||||
|
||||
// Returns true if a factory is registered for the (normalized) filetype.
|
||||
static bool IsRegistered(std::string_view filetype);
|
||||
|
||||
// Return a list of currently registered (normalized) filetypes. Primarily for diagnostics/tests.
|
||||
static std::vector<std::string> RegisteredFiletypes();
|
||||
|
||||
#ifdef KTE_ENABLE_TREESITTER
|
||||
// Forward declaration to avoid hard dependency when disabled.
|
||||
struct TSLanguage;
|
||||
// Convenience: register a Tree-sitter-backed highlighter for a filetype.
|
||||
// The getter should return a non-null language pointer for the grammar.
|
||||
static void RegisterTreeSitter(std::string_view filetype,
|
||||
const TSLanguage * (*get_language)());
|
||||
#endif
|
||||
};
|
||||
} // namespace kte
|
||||
90
syntax/JsonHighlighter.cc
Normal file
90
syntax/JsonHighlighter.cc
Normal file
@@ -0,0 +1,90 @@
|
||||
#include "JsonHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static bool
|
||||
is_digit(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
JSONHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
auto push = [&](int a, int b, TokenKind k) {
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
};
|
||||
|
||||
int i = 0;
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
push(i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_digit(c) || (c == '-' && i + 1 < n && is_digit(s[i + 1]))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isdigit(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'e' ||
|
||||
s[j] == 'E' || s[j] == '+' || s[j] == '-' || s[j] == '_'))
|
||||
++j;
|
||||
push(i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// booleans/null
|
||||
if (std::isalpha(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && std::isalpha(static_cast<unsigned char>(s[j])))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
if (id == "true" || id == "false" || id == "null")
|
||||
push(i, j, TokenKind::Constant);
|
||||
else
|
||||
push(i, j, TokenKind::Identifier);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// punctuation
|
||||
if (c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == ':') {
|
||||
push(i, i + 1, TokenKind::Punctuation);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
// fallback
|
||||
push(i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
@@ -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
|
||||
51
syntax/LanguageHighlighter.h
Normal file
51
syntax/LanguageHighlighter.h
Normal file
@@ -0,0 +1,51 @@
|
||||
// LanguageHighlighter.h - interface for line-based highlighters
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "../Highlight.h"
|
||||
|
||||
class Buffer;
|
||||
|
||||
namespace kte {
|
||||
class LanguageHighlighter {
|
||||
public:
|
||||
virtual ~LanguageHighlighter() = default;
|
||||
|
||||
// Produce highlight spans for a given buffer row. Implementations should append to out.
|
||||
virtual void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const = 0;
|
||||
|
||||
|
||||
virtual bool Stateful() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Optional extension for stateful highlighters (e.g., multi-line comments/strings).
|
||||
// Engines may detect and use this via dynamic_cast without breaking stateless impls.
|
||||
class StatefulHighlighter : public LanguageHighlighter {
|
||||
public:
|
||||
struct LineState {
|
||||
bool in_block_comment{false};
|
||||
bool in_raw_string{false};
|
||||
// For raw strings, remember the delimiter between the opening R"delim( and closing )delim"
|
||||
std::string raw_delim;
|
||||
};
|
||||
|
||||
// Highlight one line given the previous line state; return the resulting state after this line.
|
||||
// Implementations should append spans for this line to out and compute the next state.
|
||||
virtual LineState HighlightLineStateful(const Buffer &buf,
|
||||
int row,
|
||||
const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const = 0;
|
||||
|
||||
|
||||
bool Stateful() const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
} // namespace kte
|
||||
107
syntax/LispHighlighter.cc
Normal file
107
syntax/LispHighlighter.cc
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "LispHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
LispHighlighter::LispHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"defun", "lambda", "let", "let*", "define", "set!", "if", "cond", "begin", "quote", "quasiquote",
|
||||
"unquote", "unquote-splicing", "loop", "do", "and", "or", "not"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
LispHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
int bol = 0;
|
||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||
++bol;
|
||||
if (bol < n && s[bol] == ';') {
|
||||
push(out, bol, n, TokenKind::Comment);
|
||||
if (bol > 0)
|
||||
push(out, 0, bol, TokenKind::Whitespace);
|
||||
return;
|
||||
}
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == ';') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isalpha(static_cast<unsigned char>(c)) || c == '*' || c == '-' || c == '+' || c == '/' || c ==
|
||||
'_') {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '*' || s[j] == '-' ||
|
||||
s[j] == '+' || s[j] == '/' || s[j] == '_' || s[j] == '!'))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = kws_.count(id) ? TokenKind::Keyword : TokenKind::Identifier;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isdigit(static_cast<unsigned char>(s[j])) || s[j] == '.'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
@@ -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
|
||||
132
syntax/MarkdownHighlighter.cc
Normal file
132
syntax/MarkdownHighlighter.cc
Normal file
@@ -0,0 +1,132 @@
|
||||
#include "MarkdownHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push_span(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
MarkdownHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
LineState st; // not used in stateless entry
|
||||
(void) HighlightLineStateful(buf, row, st, out);
|
||||
}
|
||||
|
||||
|
||||
StatefulHighlighter::LineState
|
||||
MarkdownHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
StatefulHighlighter::LineState state = prev;
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return state;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
|
||||
// Reuse in_block_comment flag as "in fenced code" state.
|
||||
if (state.in_block_comment) {
|
||||
// If line contains closing fence ``` then close after it
|
||||
auto pos = s.find("```");
|
||||
if (pos == std::string::npos) {
|
||||
push_span(out, 0, n, TokenKind::String);
|
||||
state.in_block_comment = true;
|
||||
return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + 3);
|
||||
push_span(out, 0, end, TokenKind::String);
|
||||
// rest of line processed normally after fence
|
||||
int i = end;
|
||||
// whitespace
|
||||
if (i < n)
|
||||
push_span(out, i, n, TokenKind::Default);
|
||||
state.in_block_comment = false;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect fenced code block start at beginning (allow leading spaces)
|
||||
int bol = 0;
|
||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||
++bol;
|
||||
if (bol + 3 <= n && s.compare(bol, 3, "```") == 0) {
|
||||
push_span(out, bol, n, TokenKind::String);
|
||||
state.in_block_comment = true; // enter fenced mode
|
||||
return state;
|
||||
}
|
||||
|
||||
// Headings: lines starting with 1-6 '#'
|
||||
if (bol < n && s[bol] == '#') {
|
||||
int j = bol;
|
||||
while (j < n && s[j] == '#')
|
||||
++j; // hashes
|
||||
// include following space and text as Keyword to stand out
|
||||
push_span(out, bol, n, TokenKind::Keyword);
|
||||
return state;
|
||||
}
|
||||
|
||||
// Process inline: emphasis and code spans
|
||||
int i = 0;
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == '`') {
|
||||
int j = i + 1;
|
||||
while (j < n && s[j] != '`')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
push_span(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '*' || c == '_') {
|
||||
// bold/italic markers: treat the marker and until next same marker as Type to highlight
|
||||
char m = c;
|
||||
int j = i + 1;
|
||||
while (j < n && s[j] != m)
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
push_span(out, i, j, TokenKind::Type);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// links []() minimal: treat [text](url) as Function
|
||||
if (c == '[') {
|
||||
int j = i + 1;
|
||||
while (j < n && s[j] != ']')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j; // include ]
|
||||
if (j < n && s[j] == '(') {
|
||||
while (j < n && s[j] != ')')
|
||||
++j;
|
||||
if (j < n)
|
||||
++j;
|
||||
}
|
||||
push_span(out, i, j, TokenKind::Function);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// whitespace
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push_span(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// fallback: default single char
|
||||
push_span(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
} // namespace kte
|
||||
14
syntax/MarkdownHighlighter.h
Normal file
14
syntax/MarkdownHighlighter.h
Normal file
@@ -0,0 +1,14 @@
|
||||
// MarkdownHighlighter.h - simple Markdown highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
class MarkdownHighlighter final : public StatefulHighlighter {
|
||||
public:
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const override;
|
||||
};
|
||||
} // namespace kte
|
||||
17
syntax/NullHighlighter.cc
Normal file
17
syntax/NullHighlighter.cc
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "NullHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
|
||||
namespace kte {
|
||||
void
|
||||
NullHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
if (n <= 0)
|
||||
return;
|
||||
out.push_back({0, n, TokenKind::Default});
|
||||
}
|
||||
} // namespace kte
|
||||
@@ -4,10 +4,8 @@
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
class NullHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
172
syntax/PythonHighlighter.cc
Normal file
172
syntax/PythonHighlighter.cc
Normal file
@@ -0,0 +1,172 @@
|
||||
#include "PythonHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
PythonHighlighter::PythonHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else", "except", "False",
|
||||
"finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "None", "nonlocal", "not",
|
||||
"or", "pass", "raise", "return", "True", "try", "while", "with", "yield"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
PythonHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
LineState st;
|
||||
(void) HighlightLineStateful(buf, row, st, out);
|
||||
}
|
||||
|
||||
|
||||
StatefulHighlighter::LineState
|
||||
PythonHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
StatefulHighlighter::LineState state = prev;
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return state;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
|
||||
// Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
|
||||
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
|
||||
auto pos = s.find(state.raw_delim);
|
||||
if (pos == std::string::npos) {
|
||||
push(out, 0, n, TokenKind::String);
|
||||
return state; // still inside
|
||||
} else {
|
||||
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
|
||||
push(out, 0, end, TokenKind::String);
|
||||
// remainder processed normally
|
||||
s = s.substr(end);
|
||||
n = static_cast<int>(s.size());
|
||||
state.in_raw_string = false;
|
||||
state.raw_delim.clear();
|
||||
// Continue parsing remainder as a separate small loop
|
||||
int base = end;
|
||||
// original offset, but we already emitted to 'out' with base=0; following spans should be from 'end'
|
||||
// For simplicity, mark rest as Default
|
||||
if (n > 0)
|
||||
push(out, base, base + n, TokenKind::Default);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
// Detect comment start '#', ignoring inside strings
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '#') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
// Strings: triple quotes and single-line
|
||||
if (c == '"' || c == '\'') {
|
||||
char q = c;
|
||||
// triple?
|
||||
if (i + 2 < n && s[i + 1] == q && s[i + 2] == q) {
|
||||
std::string delim(3, q);
|
||||
int j = i + 3; // search for closing triple
|
||||
auto pos = s.find(delim, static_cast<std::size_t>(j));
|
||||
if (pos == std::string::npos) {
|
||||
push(out, i, n, TokenKind::String);
|
||||
state.in_raw_string = true;
|
||||
state.raw_delim = delim;
|
||||
return state;
|
||||
} else {
|
||||
int end = static_cast<int>(pos + 3);
|
||||
push(out, i, end, TokenKind::String);
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == q)
|
||||
break;
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (kws_.count(id))
|
||||
k = TokenKind::Keyword;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ':' || c == ',' || c == '(' || c == ')' || c == '[' || c == ']')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
} // namespace kte
|
||||
20
syntax/PythonHighlighter.h
Normal file
20
syntax/PythonHighlighter.h
Normal file
@@ -0,0 +1,20 @@
|
||||
// PythonHighlighter.h - simple Python highlighter with triple-quote state
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class PythonHighlighter final : public StatefulHighlighter {
|
||||
public:
|
||||
PythonHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
|
||||
std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
};
|
||||
} // namespace kte
|
||||
145
syntax/RustHighlighter.cc
Normal file
145
syntax/RustHighlighter.cc
Normal file
@@ -0,0 +1,145 @@
|
||||
#include "RustHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
RustHighlighter::RustHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if",
|
||||
"impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self",
|
||||
"static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while", "dyn", "async",
|
||||
"await", "try"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
const char *tp[] = {
|
||||
"u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32", "f64",
|
||||
"bool", "char", "str"
|
||||
};
|
||||
for (auto s: tp)
|
||||
types_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||
int j = i + 2;
|
||||
bool closed = false;
|
||||
while (j + 1 <= n) {
|
||||
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||
j += 2;
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (!closed) {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
} else {
|
||||
push(out, i, j, TokenKind::Comment);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (c == '"') {
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (kws_.count(id))
|
||||
k = TokenKind::Keyword;
|
||||
else if (types_.count(id))
|
||||
k = TokenKind::Type;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
|
||||
']')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
18
syntax/RustHighlighter.h
Normal file
18
syntax/RustHighlighter.h
Normal file
@@ -0,0 +1,18 @@
|
||||
// RustHighlighter.h - simple Rust highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class RustHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
RustHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
std::unordered_set<std::string> types_;
|
||||
};
|
||||
} // namespace kte
|
||||
105
syntax/ShellHighlighter.cc
Normal file
105
syntax/ShellHighlighter.cc
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "ShellHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ShellHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
// if first non-space is '#', whole line is comment
|
||||
int bol = 0;
|
||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||
++bol;
|
||||
if (bol < n && s[bol] == '#') {
|
||||
push(out, bol, n, TokenKind::Comment);
|
||||
if (bol > 0)
|
||||
push(out, 0, bol, TokenKind::Whitespace);
|
||||
return;
|
||||
}
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (c == '#') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
if (c == '\'' || c == '"') {
|
||||
char q = c;
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (q == '"') {
|
||||
if (esc) {
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (d == '"')
|
||||
break;
|
||||
} else {
|
||||
if (d == '\'')
|
||||
break;
|
||||
}
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// simple keywords
|
||||
if (std::isalpha(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '_'))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
static const char *kws[] = {
|
||||
"if", "then", "fi", "for", "in", "do", "done", "case", "esac", "while", "function",
|
||||
"elif", "else"
|
||||
};
|
||||
bool kw = false;
|
||||
for (auto k: kws)
|
||||
if (id == k) {
|
||||
kw = true;
|
||||
break;
|
||||
}
|
||||
push(out, i, j, kw ? TokenKind::Keyword : TokenKind::Identifier);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == '(' || c == ')' || c == '{' || c == '}' || c == ',' || c == ';')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
@@ -4,10 +4,8 @@
|
||||
#include "LanguageHighlighter.h"
|
||||
|
||||
namespace kte {
|
||||
|
||||
class ShellHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
};
|
||||
|
||||
} // namespace kte
|
||||
} // namespace kte
|
||||
156
syntax/SqlHighlighter.cc
Normal file
156
syntax/SqlHighlighter.cc
Normal file
@@ -0,0 +1,156 @@
|
||||
#include "SqlHighlighter.h"
|
||||
#include "../Buffer.h"
|
||||
#include <cctype>
|
||||
|
||||
namespace kte {
|
||||
static void
|
||||
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
|
||||
{
|
||||
if (b > a)
|
||||
out.push_back({a, b, k});
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_start(char c)
|
||||
{
|
||||
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
is_ident_char(char c)
|
||||
{
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '$';
|
||||
}
|
||||
|
||||
|
||||
SqlHighlighter::SqlHighlighter()
|
||||
{
|
||||
const char *kw[] = {
|
||||
"select", "insert", "update", "delete", "from", "where", "group", "by", "order", "limit",
|
||||
"offset", "values", "into", "create", "table", "index", "unique", "on", "as", "and", "or",
|
||||
"not", "null", "is", "primary", "key", "constraint", "foreign", "references", "drop", "alter",
|
||||
"add", "column", "rename", "to", "if", "exists", "join", "left", "right", "inner", "outer",
|
||||
"cross", "using", "set", "distinct", "having", "union", "all", "case", "when", "then", "else",
|
||||
"end", "pragma", "transaction", "begin", "commit", "rollback", "replace"
|
||||
};
|
||||
for (auto s: kw)
|
||||
kws_.insert(s);
|
||||
|
||||
const char *types[] = {"integer", "real", "text", "blob", "numeric", "boolean", "date", "datetime"};
|
||||
for (auto s: types)
|
||||
types_.insert(s);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||
{
|
||||
const auto &rows = buf.Rows();
|
||||
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
|
||||
return;
|
||||
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
|
||||
int n = static_cast<int>(s.size());
|
||||
int i = 0;
|
||||
|
||||
while (i < n) {
|
||||
char c = s[i];
|
||||
if (c == ' ' || c == '\t') {
|
||||
int j = i + 1;
|
||||
while (j < n && (s[j] == ' ' || s[j] == '\t'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Whitespace);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
// line comments: -- ...
|
||||
if (c == '-' && i + 1 < n && s[i + 1] == '-') {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
}
|
||||
// simple block comment on same line: /* ... */
|
||||
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||
int j = i + 2;
|
||||
bool closed = false;
|
||||
while (j + 1 <= n) {
|
||||
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
|
||||
j += 2;
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (!closed) {
|
||||
push(out, i, n, TokenKind::Comment);
|
||||
break;
|
||||
} else {
|
||||
push(out, i, j, TokenKind::Comment);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// strings: '...' or "..."
|
||||
if (c == '\'' || c == '"') {
|
||||
char q = c;
|
||||
int j = i + 1;
|
||||
bool esc = false;
|
||||
while (j < n) {
|
||||
char d = s[j++];
|
||||
if (d == q) {
|
||||
// Handle doubled quote escaping for SQL single quotes
|
||||
if (q == '\'' && j < n && s[j] == '\'') {
|
||||
++j;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (d == '\\') {
|
||||
esc = !esc;
|
||||
} else {
|
||||
esc = false;
|
||||
}
|
||||
}
|
||||
push(out, i, j, TokenKind::String);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c))) {
|
||||
int j = i + 1;
|
||||
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
|
||||
++j;
|
||||
push(out, i, j, TokenKind::Number);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (is_ident_start(c)) {
|
||||
int j = i + 1;
|
||||
while (j < n && is_ident_char(s[j]))
|
||||
++j;
|
||||
std::string id = s.substr(i, j - i);
|
||||
std::string lower;
|
||||
lower.reserve(id.size());
|
||||
for (char ch: id)
|
||||
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
|
||||
TokenKind k = TokenKind::Identifier;
|
||||
if (kws_.count(lower))
|
||||
k = TokenKind::Keyword;
|
||||
else if (types_.count(lower))
|
||||
k = TokenKind::Type;
|
||||
push(out, i, j, k);
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
if (std::ispunct(static_cast<unsigned char>(c))) {
|
||||
TokenKind k = TokenKind::Operator;
|
||||
if (c == ',' || c == ';' || c == '(' || c == ')')
|
||||
k = TokenKind::Punctuation;
|
||||
push(out, i, i + 1, k);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
push(out, i, i + 1, TokenKind::Default);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
} // namespace kte
|
||||
18
syntax/SqlHighlighter.h
Normal file
18
syntax/SqlHighlighter.h
Normal file
@@ -0,0 +1,18 @@
|
||||
// SqlHighlighter.h - simple SQL/SQLite highlighter
|
||||
#pragma once
|
||||
|
||||
#include "LanguageHighlighter.h"
|
||||
#include <unordered_set>
|
||||
|
||||
namespace kte {
|
||||
class SqlHighlighter final : public LanguageHighlighter {
|
||||
public:
|
||||
SqlHighlighter();
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> kws_;
|
||||
std::unordered_set<std::string> types_;
|
||||
};
|
||||
} // namespace kte
|
||||
51
syntax/TreeSitterHighlighter.cc
Normal file
51
syntax/TreeSitterHighlighter.cc
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "../TreeSitterHighlighter.h"
|
||||
|
||||
#ifdef KTE_ENABLE_TREESITTER
|
||||
|
||||
#include "Buffer.h"
|
||||
#include <utility>
|
||||
|
||||
namespace kte {
|
||||
TreeSitterHighlighter::TreeSitterHighlighter(const TSLanguage *lang, std::string filetype)
|
||||
: language_(lang), filetype_(std::move(filetype)) {}
|
||||
|
||||
|
||||
TreeSitterHighlighter::~TreeSitterHighlighter()
|
||||
{
|
||||
disposeParser();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
TreeSitterHighlighter::ensureParsed(const Buffer & /*buf*/) const
|
||||
{
|
||||
// Intentionally a stub to avoid pulling the Tree-sitter API and library by default.
|
||||
// In future, when linking against tree-sitter, initialize parser_, set language_,
|
||||
// and build tree_ from the buffer contents.
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
TreeSitterHighlighter::disposeParser() const
|
||||
{
|
||||
// Stub; nothing to dispose when not actually creating parser/tree
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
TreeSitterHighlighter::HighlightLine(const Buffer &/*buf*/, int /*row*/, std::vector<HighlightSpan> &/*out*/) const
|
||||
{
|
||||
// For now, no-op. When tree-sitter is wired, map nodes to TokenKind spans per line.
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<LanguageHighlighter>
|
||||
CreateTreeSitterHighlighter(const char *filetype,
|
||||
const void * (*get_lang)())
|
||||
{
|
||||
const auto *lang = reinterpret_cast<const TSLanguage *>(get_lang ? get_lang() : nullptr);
|
||||
return std::make_unique < TreeSitterHighlighter > (lang, filetype ? std::string(filetype) : std::string());
|
||||
}
|
||||
} // namespace kte
|
||||
|
||||
#endif // KTE_ENABLE_TREESITTER
|
||||
@@ -17,32 +17,32 @@ struct TSTree;
|
||||
}
|
||||
|
||||
namespace kte {
|
||||
|
||||
// A minimal adapter that uses Tree-sitter to parse the whole buffer and then, for now,
|
||||
// does very limited token classification. This acts as a scaffold for future richer
|
||||
// queries. If no queries are provided, it currently produces no spans (safe fallback).
|
||||
class TreeSitterHighlighter : public LanguageHighlighter {
|
||||
public:
|
||||
explicit TreeSitterHighlighter(const TSLanguage* lang, std::string filetype);
|
||||
~TreeSitterHighlighter() override;
|
||||
explicit TreeSitterHighlighter(const TSLanguage *lang, std::string filetype);
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
~TreeSitterHighlighter() override;
|
||||
|
||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||
|
||||
private:
|
||||
const TSLanguage* language_{nullptr};
|
||||
std::string filetype_;
|
||||
// Lazy parser to avoid startup cost; mutable to allow creation in const method
|
||||
mutable TSParser* parser_{nullptr};
|
||||
mutable TSTree* tree_{nullptr};
|
||||
const TSLanguage *language_{nullptr};
|
||||
std::string filetype_;
|
||||
// Lazy parser to avoid startup cost; mutable to allow creation in const method
|
||||
mutable TSParser *parser_{nullptr};
|
||||
mutable TSTree *tree_{nullptr};
|
||||
|
||||
void ensureParsed(const Buffer& buf) const;
|
||||
void disposeParser() const;
|
||||
void ensureParsed(const Buffer &buf) const;
|
||||
|
||||
void disposeParser() const;
|
||||
};
|
||||
|
||||
// Factory used by HighlighterRegistry when registering via RegisterTreeSitter.
|
||||
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char* filetype,
|
||||
const void* (*get_lang)());
|
||||
|
||||
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
|
||||
const void * (*get_lang)());
|
||||
} // namespace kte
|
||||
|
||||
#endif // KTE_ENABLE_TREESITTER
|
||||
#endif // KTE_ENABLE_TREESITTER
|
||||
119
test_lsp_decode.cc
Normal file
119
test_lsp_decode.cc
Normal file
@@ -0,0 +1,119 @@
|
||||
// test_lsp_decode.cc - tests for LspProcessClient JSON decoding/dispatch
|
||||
#include <cassert>
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "lsp/LspProcessClient.h"
|
||||
#include "lsp/Diagnostic.h"
|
||||
|
||||
using namespace kte::lsp;
|
||||
|
||||
|
||||
int
|
||||
main()
|
||||
{
|
||||
// Create client (won't start a process for these tests)
|
||||
LspProcessClient client("/bin/echo", {});
|
||||
|
||||
// 1) Numeric id response should match string key "42"
|
||||
{
|
||||
std::atomic<bool> called{false};
|
||||
client.debugAddPendingForTest("42", "dummy",
|
||||
[&](const nlohmann::json &result, const nlohmann::json *err) {
|
||||
(void) result;
|
||||
(void) err;
|
||||
called = true;
|
||||
});
|
||||
std::string resp = "{\"jsonrpc\":\"2.0\",\"id\":42,\"result\":null}";
|
||||
client.debugInjectMessageForTest(resp);
|
||||
assert(called.load());
|
||||
}
|
||||
|
||||
// 2) String id response should resolve
|
||||
{
|
||||
std::atomic<bool> called{false};
|
||||
client.debugAddPendingForTest("abc123", "dummy",
|
||||
[&](const nlohmann::json &result, const nlohmann::json *err) {
|
||||
(void) result;
|
||||
(void) err;
|
||||
called = true;
|
||||
});
|
||||
std::string resp = "{\"jsonrpc\":\"2.0\",\"id\":\"abc123\",\"result\":{}}";
|
||||
client.debugInjectMessageForTest(resp);
|
||||
assert(called.load());
|
||||
}
|
||||
|
||||
// 3) Diagnostics notification decoding
|
||||
{
|
||||
std::atomic<bool> diagCalled{false};
|
||||
client.setDiagnosticsHandler([&](const std::string &uri, const std::vector<Diagnostic> &d) {
|
||||
assert(uri == "file:///tmp/x.rs");
|
||||
assert(!d.empty());
|
||||
diagCalled = true;
|
||||
});
|
||||
std::string notif = R"({
|
||||
"jsonrpc":"2.0",
|
||||
"method":"textDocument/publishDiagnostics",
|
||||
"params":{
|
||||
"uri":"file:///tmp/x.rs",
|
||||
"diagnostics":[{
|
||||
"range": {"start": {"line":0, "character":1}, "end": {"line":0, "character":2}},
|
||||
"severity": 1,
|
||||
"message": "oops"
|
||||
}]
|
||||
}
|
||||
})";
|
||||
client.debugInjectMessageForTest(notif);
|
||||
assert(diagCalled.load());
|
||||
}
|
||||
|
||||
// 4) ShowMessage notification should be safely handled (no crash)
|
||||
{
|
||||
std::string msg =
|
||||
"{\"jsonrpc\":\"2.0\",\"method\":\"window/showMessage\",\"params\":{\"type\":2,\"message\":\"hi\"}}";
|
||||
client.debugInjectMessageForTest(msg);
|
||||
}
|
||||
|
||||
// 5) workspace/configuration request should be responded to (no crash)
|
||||
{
|
||||
std::string req = R"({
|
||||
"jsonrpc":"2.0",
|
||||
"id": 7,
|
||||
"method":"workspace/configuration",
|
||||
"params": {"items": [{"section":"x"},{"section":"y"}]}
|
||||
})";
|
||||
client.debugInjectMessageForTest(req);
|
||||
}
|
||||
|
||||
// 6) Pending cap eviction: oldest request is dropped with -32001
|
||||
{
|
||||
LspProcessClient c2("/bin/echo", {});
|
||||
c2.setRunningForTest(true);
|
||||
c2.setMaxPendingForTest(2);
|
||||
|
||||
std::atomic<int> drops{0};
|
||||
auto make_cb = [&](const char *tag) {
|
||||
return [&, tag](const nlohmann::json &res, const nlohmann::json *err) {
|
||||
(void) res;
|
||||
if (err && err->is_object()) {
|
||||
auto it = err->find("code");
|
||||
if (it != err->end() && it->is_number_integer() && *it == -32001) {
|
||||
// Only the oldest (first) should be dropped
|
||||
if (std::string(tag) == "first")
|
||||
drops.fetch_add(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
// Enqueue 3 requests; cap=2 -> first should be dropped immediately when third is added
|
||||
c2.debugSendRequestForTest("a", nlohmann::json::object(), make_cb("first"));
|
||||
c2.debugSendRequestForTest("b", nlohmann::json::object(), make_cb("second"));
|
||||
c2.debugSendRequestForTest("c", nlohmann::json::object(), make_cb("third"));
|
||||
// Allow callbacks (none are async here, drop is invoked inline after send)
|
||||
assert(drops.load() == 1);
|
||||
}
|
||||
|
||||
std::puts("test_lsp_decode: OK");
|
||||
return 0;
|
||||
}
|
||||
76
test_transport.cc
Normal file
76
test_transport.cc
Normal file
@@ -0,0 +1,76 @@
|
||||
// test_transport.cc - transport framing tests
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "lsp/JsonRpcTransport.h"
|
||||
|
||||
using namespace kte::lsp;
|
||||
|
||||
|
||||
static void
|
||||
write_all(int fd, const void *bufv, size_t len)
|
||||
{
|
||||
const char *buf = static_cast<const char *>(bufv);
|
||||
size_t left = len;
|
||||
while (left > 0) {
|
||||
ssize_t n = ::write(fd, buf, left);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
std::perror("write");
|
||||
std::abort();
|
||||
}
|
||||
buf += static_cast<size_t>(n);
|
||||
left -= static_cast<size_t>(n);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
main()
|
||||
{
|
||||
int p[2];
|
||||
assert(pipe(p) == 0);
|
||||
int readFd = p[0];
|
||||
int writeFd = p[1];
|
||||
|
||||
JsonRpcTransport t;
|
||||
// We only need inFd for read tests; pass writeFd for completeness
|
||||
t.connect(readFd, writeFd);
|
||||
|
||||
auto sendMsg = [&](const std::string &payload, bool lowerHeader) {
|
||||
std::string header = (lowerHeader ? std::string("content-length: ") : std::string("Content-Length: ")) +
|
||||
std::to_string(payload.size()) + "\r\n\r\n";
|
||||
write_all(writeFd, header.data(), header.size());
|
||||
// Send body in two parts to exercise partial reads
|
||||
size_t mid = payload.size() / 2;
|
||||
write_all(writeFd, payload.data(), mid);
|
||||
write_all(writeFd, payload.data() + mid, payload.size() - mid);
|
||||
};
|
||||
|
||||
std::string p1 = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":null}";
|
||||
std::string p2 = "{\"jsonrpc\":\"2.0\",\"method\":\"ping\"}";
|
||||
sendMsg(p1, false);
|
||||
sendMsg(p2, true); // case-insensitive header
|
||||
|
||||
auto m1 = t.read();
|
||||
assert(m1.has_value());
|
||||
assert(m1->raw == p1);
|
||||
|
||||
auto m2 = t.read();
|
||||
assert(m2.has_value());
|
||||
assert(m2->raw == p2);
|
||||
|
||||
// Close write end to signal EOF; next read should return nullopt
|
||||
::close(writeFd);
|
||||
auto m3 = t.read();
|
||||
assert(!m3.has_value());
|
||||
|
||||
::close(readFd);
|
||||
std::puts("test_transport: OK");
|
||||
return 0;
|
||||
}
|
||||
101
test_utfcodec.cc
Normal file
101
test_utfcodec.cc
Normal file
@@ -0,0 +1,101 @@
|
||||
// test_utfcodec.cc - simple tests for UtfCodec helpers
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "lsp/UtfCodec.h"
|
||||
|
||||
using namespace kte::lsp;
|
||||
|
||||
|
||||
static std::string_view
|
||||
lp(const std::string &, int)
|
||||
{
|
||||
return std::string_view();
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
main()
|
||||
{
|
||||
// ASCII: each scalar = 1 UTF-16 unit
|
||||
{
|
||||
std::string s = "hello"; // 5 ASCII
|
||||
assert(utf8ColToUtf16Units(s, 0) == 0);
|
||||
assert(utf8ColToUtf16Units(s, 3) == 3);
|
||||
assert(utf16UnitsToUtf8Col(s, 3) == 3);
|
||||
assert(utf16UnitsToUtf8Col(s, 10) == 5); // clamp to EOL
|
||||
}
|
||||
|
||||
// BMP multibyte (e.g., ü U+00FC, α U+03B1) -> still 1 UTF-16 unit
|
||||
{
|
||||
std::string s = u8"aüαb"; // bytes: a [C3 BC] [CE B1] b
|
||||
// columns by codepoints: a(0), ü(1), α(2), b(3)
|
||||
assert(utf8ColToUtf16Units(s, 0) == 0);
|
||||
assert(utf8ColToUtf16Units(s, 1) == 1);
|
||||
assert(utf8ColToUtf16Units(s, 2) == 2);
|
||||
assert(utf8ColToUtf16Units(s, 4) == 4); // past EOL clamps to 4 units
|
||||
|
||||
assert(utf16UnitsToUtf8Col(s, 0) == 0);
|
||||
assert(utf16UnitsToUtf8Col(s, 2) == 2);
|
||||
assert(utf16UnitsToUtf8Col(s, 4) == 4);
|
||||
}
|
||||
|
||||
// Non-BMP (emoji) -> 2 UTF-16 units per code point
|
||||
{
|
||||
std::string s = u8"A😀B"; // U+1F600 between A and B
|
||||
// codepoints: A, 😀, B => utf8 columns 0..3
|
||||
// utf16 units: A(1), 😀(2), B(1) cumulative: 0,1,3,4
|
||||
assert(utf8ColToUtf16Units(s, 0) == 0);
|
||||
assert(utf8ColToUtf16Units(s, 1) == 1); // after A
|
||||
assert(utf8ColToUtf16Units(s, 2) == 3); // after 😀 (2 units)
|
||||
assert(utf8ColToUtf16Units(s, 3) == 4); // after B
|
||||
|
||||
assert(utf16UnitsToUtf8Col(s, 0) == 0);
|
||||
assert(utf16UnitsToUtf8Col(s, 1) == 1); // A
|
||||
assert(utf16UnitsToUtf8Col(s, 2) == 1); // mid-surrogate -> stays before 😀
|
||||
assert(utf16UnitsToUtf8Col(s, 3) == 2); // end of 😀
|
||||
assert(utf16UnitsToUtf8Col(s, 4) == 3); // after B
|
||||
assert(utf16UnitsToUtf8Col(s, 10) == 3); // clamp
|
||||
}
|
||||
|
||||
// Invalid UTF-8: treat invalid byte as U+FFFD (1 UTF-16 unit), consume 1 byte
|
||||
{
|
||||
std::string s;
|
||||
s.push_back('X');
|
||||
s.push_back(char(0xFF)); // invalid single byte
|
||||
s.push_back('Y');
|
||||
// Columns by codepoints as we decode: 'X', U+FFFD, 'Y'
|
||||
assert(utf8ColToUtf16Units(s, 0) == 0);
|
||||
assert(utf8ColToUtf16Units(s, 1) == 1);
|
||||
assert(utf8ColToUtf16Units(s, 2) == 2);
|
||||
assert(utf8ColToUtf16Units(s, 3) == 3);
|
||||
|
||||
assert(utf16UnitsToUtf8Col(s, 0) == 0);
|
||||
assert(utf16UnitsToUtf8Col(s, 1) == 1);
|
||||
assert(utf16UnitsToUtf8Col(s, 2) == 2);
|
||||
assert(utf16UnitsToUtf8Col(s, 3) == 3);
|
||||
}
|
||||
|
||||
// Position/Range helpers with a simple provider
|
||||
{
|
||||
std::string lines[] = {u8"A😀B"};
|
||||
LineProvider provider = [&](const std::string &, int line) -> std::string_view {
|
||||
return (line == 0) ? std::string_view(lines[0]) : std::string_view();
|
||||
};
|
||||
Position p8{0, 2}; // after 😀 in utf8 columns
|
||||
Position p16 = toUtf16("file:///x", p8, provider);
|
||||
assert(p16.line == 0 && p16.character == 3);
|
||||
|
||||
Position back = toUtf8("file:///x", p16, provider);
|
||||
assert(back.line == 0 && back.character == 2);
|
||||
|
||||
Range r8{{0, 1}, {0, 3}}; // A|😀|B end
|
||||
Range r16 = toUtf16("file:///x", r8, provider);
|
||||
assert(r16.start.character == 1 && r16.end.character == 4);
|
||||
}
|
||||
|
||||
std::puts("test_utfcodec: OK");
|
||||
return 0;
|
||||
}
|
||||
177
themes/EInk.h
Normal file
177
themes/EInk.h
Normal file
@@ -0,0 +1,177 @@
|
||||
// themes/EInk.h — Monochrome e-ink inspired ImGui themes (header-only)
|
||||
#pragma once
|
||||
#include "imgui.h"
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||
|
||||
static void
|
||||
ApplyEInkImGuiTheme()
|
||||
{
|
||||
// E-Ink grayscale palette (light background)
|
||||
const ImVec4 paper = RGBA(0xF2F2EE); // light paper
|
||||
const ImVec4 bg1 = RGBA(0xE6E6E2);
|
||||
const ImVec4 bg2 = RGBA(0xDADAD5);
|
||||
const ImVec4 bg3 = RGBA(0xCFCFCA);
|
||||
const ImVec4 ink = RGBA(0x111111); // primary text (near black)
|
||||
const ImVec4 dim = RGBA(0x666666); // disabled text
|
||||
const ImVec4 border = RGBA(0xB8B8B3);
|
||||
const ImVec4 accent = RGBA(0x222222); // controls/active
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 0.0f;
|
||||
style.FrameRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.GrabRounding = 0.0f;
|
||||
style.TabRounding = 0.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = ink;
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(dim.x, dim.y, dim.z, 1.0f);
|
||||
colors[ImGuiCol_WindowBg] = paper;
|
||||
colors[ImGuiCol_ChildBg] = paper;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = border;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = paper;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
|
||||
colors[ImGuiCol_CheckMark] = accent;
|
||||
colors[ImGuiCol_SliderGrab] = accent;
|
||||
colors[ImGuiCol_SliderGrabActive] = ink;
|
||||
colors[ImGuiCol_Button] = bg3;
|
||||
colors[ImGuiCol_ButtonHovered] = bg2;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
colors[ImGuiCol_Header] = bg3;
|
||||
colors[ImGuiCol_HeaderHovered] = bg2;
|
||||
colors[ImGuiCol_HeaderActive] = bg2;
|
||||
colors[ImGuiCol_Separator] = border;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg2;
|
||||
colors[ImGuiCol_SeparatorActive] = accent;
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.12f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(accent.x, accent.y, accent.z, 0.50f);
|
||||
colors[ImGuiCol_ResizeGripActive] = ink;
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = bg1;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(accent.x, accent.y, accent.z, 0.30f);
|
||||
colors[ImGuiCol_DragDropTarget] = accent;
|
||||
colors[ImGuiCol_NavHighlight] = accent;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_PlotLines] = accent;
|
||||
colors[ImGuiCol_PlotLinesHovered] = ink;
|
||||
colors[ImGuiCol_PlotHistogram] = accent;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = ink;
|
||||
}
|
||||
|
||||
|
||||
static inline void
|
||||
ApplyEInkDarkImGuiTheme()
|
||||
{
|
||||
// E-Ink dark variant (dark background, light ink)
|
||||
const ImVec4 paper = RGBA(0x1A1A1A);
|
||||
const ImVec4 bg1 = RGBA(0x222222);
|
||||
const ImVec4 bg2 = RGBA(0x2B2B2B);
|
||||
const ImVec4 bg3 = RGBA(0x343434);
|
||||
const ImVec4 ink = RGBA(0xEDEDEA);
|
||||
const ImVec4 dim = RGBA(0xB5B5B3);
|
||||
const ImVec4 border = RGBA(0x444444);
|
||||
const ImVec4 accent = RGBA(0xDDDDDD);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 0.0f;
|
||||
style.FrameRounding = 0.0f;
|
||||
style.PopupRounding = 0.0f;
|
||||
style.GrabRounding = 0.0f;
|
||||
style.TabRounding = 0.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = ink;
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(dim.x, dim.y, dim.z, 1.0f);
|
||||
colors[ImGuiCol_WindowBg] = paper;
|
||||
colors[ImGuiCol_ChildBg] = paper;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = border;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = paper;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
|
||||
colors[ImGuiCol_CheckMark] = accent;
|
||||
colors[ImGuiCol_SliderGrab] = accent;
|
||||
colors[ImGuiCol_SliderGrabActive] = ink;
|
||||
colors[ImGuiCol_Button] = bg3;
|
||||
colors[ImGuiCol_ButtonHovered] = bg2;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
colors[ImGuiCol_Header] = bg3;
|
||||
colors[ImGuiCol_HeaderHovered] = bg2;
|
||||
colors[ImGuiCol_HeaderActive] = bg2;
|
||||
colors[ImGuiCol_Separator] = border;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg2;
|
||||
colors[ImGuiCol_SeparatorActive] = accent;
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.12f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(accent.x, accent.y, accent.z, 0.50f);
|
||||
colors[ImGuiCol_ResizeGripActive] = ink;
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = bg1;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(accent.x, accent.y, accent.z, 0.30f);
|
||||
colors[ImGuiCol_DragDropTarget] = accent;
|
||||
colors[ImGuiCol_NavHighlight] = accent;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_PlotLines] = accent;
|
||||
colors[ImGuiCol_PlotLinesHovered] = ink;
|
||||
colors[ImGuiCol_PlotHistogram] = accent;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = ink;
|
||||
}
|
||||
204
themes/Gruvbox.h
Normal file
204
themes/Gruvbox.h
Normal file
@@ -0,0 +1,204 @@
|
||||
// themes/Gruvbox.h — Gruvbox Dark/Light (medium) ImGui themes (header-only)
|
||||
#pragma once
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||
|
||||
static void
|
||||
ApplyGruvboxDarkMediumTheme()
|
||||
{
|
||||
// Gruvbox (dark, medium) palette
|
||||
const ImVec4 bg0 = RGBA(0x282828); // dark0
|
||||
const ImVec4 bg1 = RGBA(0x3C3836); // dark1
|
||||
const ImVec4 bg2 = RGBA(0x504945); // dark2
|
||||
const ImVec4 bg3 = RGBA(0x665C54); // dark3
|
||||
const ImVec4 fg1 = RGBA(0xEBDBB2); // light1
|
||||
const ImVec4 fg0 = RGBA(0xFBF1C7); // light0
|
||||
// accent colors
|
||||
const ImVec4 yellow = RGBA(0xFABD2F);
|
||||
const ImVec4 blue = RGBA(0x83A598);
|
||||
const ImVec4 aqua = RGBA(0x8EC07C);
|
||||
const ImVec4 orange = RGBA(0xFE8019);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 4.0f;
|
||||
style.FrameRounding = 3.0f;
|
||||
style.PopupRounding = 4.0f;
|
||||
style.GrabRounding = 3.0f;
|
||||
style.TabRounding = 4.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = fg1;
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(fg1.x, fg1.y, fg1.z, 0.55f);
|
||||
colors[ImGuiCol_WindowBg] = bg0;
|
||||
colors[ImGuiCol_ChildBg] = bg0;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = bg2;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = bg0;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_CheckMark] = aqua;
|
||||
colors[ImGuiCol_SliderGrab] = aqua;
|
||||
colors[ImGuiCol_SliderGrabActive] = blue;
|
||||
|
||||
colors[ImGuiCol_Button] = bg3;
|
||||
colors[ImGuiCol_ButtonHovered] = bg2;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_Header] = bg3;
|
||||
colors[ImGuiCol_HeaderHovered] = bg2;
|
||||
colors[ImGuiCol_HeaderActive] = bg2;
|
||||
|
||||
colors[ImGuiCol_Separator] = bg2;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg1;
|
||||
colors[ImGuiCol_SeparatorActive] = blue;
|
||||
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(fg0.x, fg0.y, fg0.z, 0.12f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(aqua.x, aqua.y, aqua.z, 0.67f);
|
||||
colors[ImGuiCol_ResizeGripActive] = blue;
|
||||
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = bg1;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
|
||||
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(orange.x, orange.y, orange.z, 0.30f);
|
||||
colors[ImGuiCol_DragDropTarget] = orange;
|
||||
colors[ImGuiCol_NavHighlight] = orange;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(fg0.x, fg0.y, fg0.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_PlotLines] = aqua;
|
||||
colors[ImGuiCol_PlotLinesHovered] = blue;
|
||||
colors[ImGuiCol_PlotHistogram] = yellow;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = orange;
|
||||
}
|
||||
|
||||
|
||||
static inline void
|
||||
ApplyGruvboxLightMediumTheme()
|
||||
{
|
||||
// Gruvbox (light, medium) palette
|
||||
const ImVec4 bg0 = RGBA(0xFBF1C7); // light0
|
||||
const ImVec4 bg1 = RGBA(0xEBDBB2); // light1
|
||||
const ImVec4 bg2 = RGBA(0xD5C4A1); // light2
|
||||
const ImVec4 bg3 = RGBA(0xBDAE93); // light3
|
||||
const ImVec4 fg1 = RGBA(0x3C3836); // dark1
|
||||
const ImVec4 fg0 = RGBA(0x282828); // dark0
|
||||
// accents
|
||||
const ImVec4 yellow = RGBA(0xB57614);
|
||||
const ImVec4 blue = RGBA(0x076678);
|
||||
const ImVec4 aqua = RGBA(0x427B58);
|
||||
const ImVec4 orange = RGBA(0xAF3A03);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 4.0f;
|
||||
style.FrameRounding = 3.0f;
|
||||
style.PopupRounding = 4.0f;
|
||||
style.GrabRounding = 3.0f;
|
||||
style.TabRounding = 4.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
colors[ImGuiCol_Text] = fg1;
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(fg1.x, fg1.y, fg1.z, 0.55f);
|
||||
colors[ImGuiCol_WindowBg] = bg0;
|
||||
colors[ImGuiCol_ChildBg] = bg0;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = bg2;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
|
||||
colors[ImGuiCol_FrameBg] = bg2;
|
||||
colors[ImGuiCol_FrameBgHovered] = bg3;
|
||||
colors[ImGuiCol_FrameBgActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_TitleBg] = bg1;
|
||||
colors[ImGuiCol_TitleBgActive] = bg2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = bg1;
|
||||
|
||||
colors[ImGuiCol_MenuBarBg] = bg1;
|
||||
colors[ImGuiCol_ScrollbarBg] = bg0;
|
||||
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_CheckMark] = aqua;
|
||||
colors[ImGuiCol_SliderGrab] = aqua;
|
||||
colors[ImGuiCol_SliderGrabActive] = blue;
|
||||
|
||||
colors[ImGuiCol_Button] = bg3;
|
||||
colors[ImGuiCol_ButtonHovered] = bg2;
|
||||
colors[ImGuiCol_ButtonActive] = bg1;
|
||||
|
||||
colors[ImGuiCol_Header] = bg3;
|
||||
colors[ImGuiCol_HeaderHovered] = bg2;
|
||||
colors[ImGuiCol_HeaderActive] = bg2;
|
||||
|
||||
colors[ImGuiCol_Separator] = bg2;
|
||||
colors[ImGuiCol_SeparatorHovered] = bg1;
|
||||
colors[ImGuiCol_SeparatorActive] = blue;
|
||||
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(fg0.x, fg0.y, fg0.z, 0.12f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(aqua.x, aqua.y, aqua.z, 0.67f);
|
||||
colors[ImGuiCol_ResizeGripActive] = blue;
|
||||
|
||||
colors[ImGuiCol_Tab] = bg2;
|
||||
colors[ImGuiCol_TabHovered] = bg1;
|
||||
colors[ImGuiCol_TabActive] = bg3;
|
||||
colors[ImGuiCol_TabUnfocused] = bg2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = bg3;
|
||||
|
||||
colors[ImGuiCol_TableHeaderBg] = bg2;
|
||||
colors[ImGuiCol_TableBorderStrong] = bg1;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
|
||||
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(orange.x, orange.y, orange.z, 0.30f);
|
||||
colors[ImGuiCol_DragDropTarget] = orange;
|
||||
colors[ImGuiCol_NavHighlight] = orange;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(fg0.x, fg0.y, fg0.z, 0.70f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
|
||||
colors[ImGuiCol_PlotLines] = aqua;
|
||||
colors[ImGuiCol_PlotLinesHovered] = blue;
|
||||
colors[ImGuiCol_PlotHistogram] = yellow;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = orange;
|
||||
}
|
||||
111
themes/Nord.h
Normal file
111
themes/Nord.h
Normal file
@@ -0,0 +1,111 @@
|
||||
// themes/Nord.h — Nord-inspired ImGui theme (header-only)
|
||||
#pragma once
|
||||
#include "ThemeHelpers.h"
|
||||
|
||||
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
|
||||
|
||||
static void
|
||||
ApplyNordImGuiTheme()
|
||||
{
|
||||
// Nord palette
|
||||
const ImVec4 nord0 = RGBA(0x2E3440); // darkest bg
|
||||
const ImVec4 nord1 = RGBA(0x3B4252);
|
||||
const ImVec4 nord2 = RGBA(0x434C5E);
|
||||
const ImVec4 nord3 = RGBA(0x4C566A);
|
||||
const ImVec4 nord4 = RGBA(0xD8DEE9);
|
||||
const ImVec4 nord6 = RGBA(0xECEFF4); // lightest
|
||||
const ImVec4 nord8 = RGBA(0x88C0D0); // cyan
|
||||
const ImVec4 nord9 = RGBA(0x81A1C1); // blue
|
||||
const ImVec4 nord10 = RGBA(0x5E81AC); // blue dark
|
||||
const ImVec4 nord12 = RGBA(0xD08770); // orange
|
||||
const ImVec4 nord13 = RGBA(0xEBCB8B); // yellow
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
|
||||
// Base style tweaks to suit Nord aesthetics
|
||||
style.WindowPadding = ImVec2(8.0f, 8.0f);
|
||||
style.FramePadding = ImVec2(6.0f, 4.0f);
|
||||
style.CellPadding = ImVec2(6.0f, 4.0f);
|
||||
style.ItemSpacing = ImVec2(6.0f, 6.0f);
|
||||
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
|
||||
style.ScrollbarSize = 14.0f;
|
||||
style.GrabMinSize = 10.0f;
|
||||
style.WindowRounding = 4.0f;
|
||||
style.FrameRounding = 3.0f;
|
||||
style.PopupRounding = 4.0f;
|
||||
style.GrabRounding = 3.0f;
|
||||
style.TabRounding = 4.0f;
|
||||
style.WindowBorderSize = 1.0f;
|
||||
style.FrameBorderSize = 1.0f;
|
||||
|
||||
ImVec4 *colors = style.Colors;
|
||||
|
||||
colors[ImGuiCol_Text] = nord4; // primary text
|
||||
colors[ImGuiCol_TextDisabled] = ImVec4(nord4.x, nord4.y, nord4.z, 0.55f);
|
||||
colors[ImGuiCol_WindowBg] = nord0;
|
||||
colors[ImGuiCol_ChildBg] = nord0;
|
||||
colors[ImGuiCol_PopupBg] = ImVec4(nord1.x, nord1.y, nord1.z, 0.98f);
|
||||
colors[ImGuiCol_Border] = nord2;
|
||||
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
|
||||
|
||||
colors[ImGuiCol_FrameBg] = nord2;
|
||||
colors[ImGuiCol_FrameBgHovered] = nord3;
|
||||
colors[ImGuiCol_FrameBgActive] = nord1;
|
||||
|
||||
colors[ImGuiCol_TitleBg] = nord1;
|
||||
colors[ImGuiCol_TitleBgActive] = nord2;
|
||||
colors[ImGuiCol_TitleBgCollapsed] = nord1;
|
||||
|
||||
colors[ImGuiCol_MenuBarBg] = nord1;
|
||||
colors[ImGuiCol_ScrollbarBg] = nord10;
|
||||
colors[ImGuiCol_ScrollbarGrab] = nord3;
|
||||
colors[ImGuiCol_ScrollbarGrabHovered] = nord2;
|
||||
colors[ImGuiCol_ScrollbarGrabActive] = nord1;
|
||||
|
||||
colors[ImGuiCol_CheckMark] = nord8;
|
||||
colors[ImGuiCol_SliderGrab] = nord8;
|
||||
colors[ImGuiCol_SliderGrabActive] = nord9;
|
||||
|
||||
colors[ImGuiCol_Button] = nord3;
|
||||
colors[ImGuiCol_ButtonHovered] = nord2;
|
||||
colors[ImGuiCol_ButtonActive] = nord1;
|
||||
|
||||
colors[ImGuiCol_Header] = nord3;
|
||||
colors[ImGuiCol_HeaderHovered] = nord10;
|
||||
colors[ImGuiCol_HeaderActive] = nord10;
|
||||
|
||||
colors[ImGuiCol_Separator] = nord2;
|
||||
colors[ImGuiCol_SeparatorHovered] = nord10;
|
||||
colors[ImGuiCol_SeparatorActive] = nord9;
|
||||
|
||||
colors[ImGuiCol_ResizeGrip] = ImVec4(nord6.x, nord6.y, nord6.z, 0.12f);
|
||||
colors[ImGuiCol_ResizeGripHovered] = ImVec4(nord8.x, nord8.y, nord8.z, 0.67f);
|
||||
colors[ImGuiCol_ResizeGripActive] = nord9;
|
||||
|
||||
colors[ImGuiCol_Tab] = nord2;
|
||||
colors[ImGuiCol_TabHovered] = nord10;
|
||||
colors[ImGuiCol_TabActive] = nord3;
|
||||
colors[ImGuiCol_TabUnfocused] = nord2;
|
||||
colors[ImGuiCol_TabUnfocusedActive] = nord3;
|
||||
|
||||
// Docking colors omitted for compatibility
|
||||
|
||||
colors[ImGuiCol_TableHeaderBg] = nord2;
|
||||
colors[ImGuiCol_TableBorderStrong] = nord1;
|
||||
colors[ImGuiCol_TableBorderLight] = ImVec4(nord1.x, nord1.y, nord1.z, 0.6f);
|
||||
colors[ImGuiCol_TableRowBg] = ImVec4(nord1.x, nord1.y, nord1.z, 0.2f);
|
||||
colors[ImGuiCol_TableRowBgAlt] = ImVec4(nord1.x, nord1.y, nord1.z, 0.35f);
|
||||
|
||||
colors[ImGuiCol_TextSelectedBg] = ImVec4(nord8.x, nord8.y, nord8.z, 0.35f);
|
||||
colors[ImGuiCol_DragDropTarget] = nord13;
|
||||
colors[ImGuiCol_NavHighlight] = nord9;
|
||||
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(nord6.x, nord6.y, nord6.z, 0.7f);
|
||||
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
|
||||
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
|
||||
|
||||
// Plots
|
||||
colors[ImGuiCol_PlotLines] = nord8;
|
||||
colors[ImGuiCol_PlotLinesHovered] = nord9;
|
||||
colors[ImGuiCol_PlotHistogram] = nord13;
|
||||
colors[ImGuiCol_PlotHistogramHovered] = nord12;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user