Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18ce005ec0 | ||
|
|
22f7573361 | ||
|
|
96e82cc6dd | ||
|
|
480f1390c7 | ||
|
|
3126a5e523 | ||
|
|
b60a8dc491 | ||
|
|
056c9af38e | ||
|
|
6413e14455 | ||
|
|
d1a45581bf | ||
|
|
81a5c25071 | ||
|
|
5667a6d7bd | ||
|
|
99c4bb2066 | ||
|
|
953fee97d7 | ||
|
|
d7e35727f1 | ||
|
|
23f04e4357 | ||
|
|
0585edad9e | ||
|
|
8712ea673d | ||
|
|
3148e16cf8 | ||
|
|
34eaa72033 | ||
|
|
f49f1698f4 | ||
|
|
f4b3188069 |
@@ -231,9 +231,12 @@ Buffer::Buffer(const Buffer &other)
|
|||||||
mark_set_ = other.mark_set_;
|
mark_set_ = other.mark_set_;
|
||||||
mark_curx_ = other.mark_curx_;
|
mark_curx_ = other.mark_curx_;
|
||||||
mark_cury_ = other.mark_cury_;
|
mark_cury_ = other.mark_cury_;
|
||||||
// Copy syntax/highlighting flags
|
// Copy edit mode + syntax/highlighting flags
|
||||||
|
edit_mode_ = other.edit_mode_;
|
||||||
|
edit_mode_detected_ = other.edit_mode_detected_;
|
||||||
version_ = other.version_;
|
version_ = other.version_;
|
||||||
syntax_enabled_ = other.syntax_enabled_;
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
syntax_user_override_ = other.syntax_user_override_;
|
||||||
filetype_ = other.filetype_;
|
filetype_ = other.filetype_;
|
||||||
// Fresh undo system for the copy
|
// Fresh undo system for the copy
|
||||||
undo_tree_ = std::make_unique<UndoTree>();
|
undo_tree_ = std::make_unique<UndoTree>();
|
||||||
@@ -281,8 +284,11 @@ Buffer::operator=(const Buffer &other)
|
|||||||
mark_set_ = other.mark_set_;
|
mark_set_ = other.mark_set_;
|
||||||
mark_curx_ = other.mark_curx_;
|
mark_curx_ = other.mark_curx_;
|
||||||
mark_cury_ = other.mark_cury_;
|
mark_cury_ = other.mark_cury_;
|
||||||
|
edit_mode_ = other.edit_mode_;
|
||||||
|
edit_mode_detected_ = other.edit_mode_detected_;
|
||||||
version_ = other.version_;
|
version_ = other.version_;
|
||||||
syntax_enabled_ = other.syntax_enabled_;
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
syntax_user_override_ = other.syntax_user_override_;
|
||||||
filetype_ = other.filetype_;
|
filetype_ = other.filetype_;
|
||||||
// Recreate undo system for this instance
|
// Recreate undo system for this instance
|
||||||
undo_tree_ = std::make_unique<UndoTree>();
|
undo_tree_ = std::make_unique<UndoTree>();
|
||||||
@@ -323,16 +329,28 @@ Buffer::Buffer(Buffer &&other) noexcept
|
|||||||
mark_set_(other.mark_set_),
|
mark_set_(other.mark_set_),
|
||||||
mark_curx_(other.mark_curx_),
|
mark_curx_(other.mark_curx_),
|
||||||
mark_cury_(other.mark_cury_),
|
mark_cury_(other.mark_cury_),
|
||||||
|
visual_line_active_(other.visual_line_active_),
|
||||||
|
visual_line_anchor_y_(other.visual_line_anchor_y_),
|
||||||
|
visual_line_active_y_(other.visual_line_active_y_),
|
||||||
undo_tree_(std::move(other.undo_tree_)),
|
undo_tree_(std::move(other.undo_tree_)),
|
||||||
undo_sys_(std::move(other.undo_sys_))
|
undo_sys_(std::move(other.undo_sys_))
|
||||||
{
|
{
|
||||||
// Move syntax/highlighting state
|
// Move edit mode + syntax/highlighting state
|
||||||
|
edit_mode_ = other.edit_mode_;
|
||||||
|
edit_mode_detected_ = other.edit_mode_detected_;
|
||||||
version_ = other.version_;
|
version_ = other.version_;
|
||||||
syntax_enabled_ = other.syntax_enabled_;
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
syntax_user_override_ = other.syntax_user_override_;
|
||||||
filetype_ = std::move(other.filetype_);
|
filetype_ = std::move(other.filetype_);
|
||||||
highlighter_ = std::move(other.highlighter_);
|
highlighter_ = std::move(other.highlighter_);
|
||||||
content_ = std::move(other.content_);
|
content_ = std::move(other.content_);
|
||||||
rows_cache_dirty_ = other.rows_cache_dirty_;
|
rows_cache_dirty_ = other.rows_cache_dirty_;
|
||||||
|
on_disk_identity_ = other.on_disk_identity_;
|
||||||
|
// Non-owning: the recorder object itself is owned by SwapManager and outlives
|
||||||
|
// this move. The caller (Editor) is responsible for calling SwapManager::Rehome()
|
||||||
|
// so the journal's Buffer* key follows this object to its new address.
|
||||||
|
swap_rec_ = other.swap_rec_;
|
||||||
|
other.swap_rec_ = nullptr;
|
||||||
// Update UndoSystem's buffer reference to point to this object
|
// Update UndoSystem's buffer reference to point to this object
|
||||||
if (undo_sys_) {
|
if (undo_sys_) {
|
||||||
undo_sys_->UpdateBufferReference(*this);
|
undo_sys_->UpdateBufferReference(*this);
|
||||||
@@ -361,16 +379,28 @@ Buffer::operator=(Buffer &&other) noexcept
|
|||||||
mark_set_ = other.mark_set_;
|
mark_set_ = other.mark_set_;
|
||||||
mark_curx_ = other.mark_curx_;
|
mark_curx_ = other.mark_curx_;
|
||||||
mark_cury_ = other.mark_cury_;
|
mark_cury_ = other.mark_cury_;
|
||||||
|
visual_line_active_ = other.visual_line_active_;
|
||||||
|
visual_line_anchor_y_ = other.visual_line_anchor_y_;
|
||||||
|
visual_line_active_y_ = other.visual_line_active_y_;
|
||||||
undo_tree_ = std::move(other.undo_tree_);
|
undo_tree_ = std::move(other.undo_tree_);
|
||||||
undo_sys_ = std::move(other.undo_sys_);
|
undo_sys_ = std::move(other.undo_sys_);
|
||||||
|
|
||||||
// Move syntax/highlighting state
|
// Move edit mode + syntax/highlighting state
|
||||||
|
edit_mode_ = other.edit_mode_;
|
||||||
|
edit_mode_detected_ = other.edit_mode_detected_;
|
||||||
version_ = other.version_;
|
version_ = other.version_;
|
||||||
syntax_enabled_ = other.syntax_enabled_;
|
syntax_enabled_ = other.syntax_enabled_;
|
||||||
|
syntax_user_override_ = other.syntax_user_override_;
|
||||||
filetype_ = std::move(other.filetype_);
|
filetype_ = std::move(other.filetype_);
|
||||||
highlighter_ = std::move(other.highlighter_);
|
highlighter_ = std::move(other.highlighter_);
|
||||||
content_ = std::move(other.content_);
|
content_ = std::move(other.content_);
|
||||||
rows_cache_dirty_ = other.rows_cache_dirty_;
|
rows_cache_dirty_ = other.rows_cache_dirty_;
|
||||||
|
on_disk_identity_ = other.on_disk_identity_;
|
||||||
|
// Non-owning: the recorder object itself is owned by SwapManager and outlives
|
||||||
|
// this move. The caller (Editor) is responsible for calling SwapManager::Rehome()
|
||||||
|
// so the journal's Buffer* key follows this object to its new address.
|
||||||
|
swap_rec_ = other.swap_rec_;
|
||||||
|
other.swap_rec_ = nullptr;
|
||||||
// Update UndoSystem's buffer reference to point to this object
|
// Update UndoSystem's buffer reference to point to this object
|
||||||
if (undo_sys_) {
|
if (undo_sys_) {
|
||||||
undo_sys_->UpdateBufferReference(*this);
|
undo_sys_->UpdateBufferReference(*this);
|
||||||
|
|||||||
@@ -35,9 +35,12 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
@@ -48,6 +51,26 @@
|
|||||||
#include "Highlight.h"
|
#include "Highlight.h"
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
|
// Edit mode determines which font class is used for a buffer.
|
||||||
|
enum class EditMode { Code, Writing };
|
||||||
|
|
||||||
|
// Detect edit mode from a filename's extension.
|
||||||
|
inline EditMode
|
||||||
|
DetectEditMode(const std::string &filename)
|
||||||
|
{
|
||||||
|
std::string ext = std::filesystem::path(filename).extension().string();
|
||||||
|
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::tolower(c));
|
||||||
|
});
|
||||||
|
static const std::unordered_set<std::string> writing_exts = {
|
||||||
|
".txt", ".md", ".markdown", ".rst", ".org",
|
||||||
|
".tex", ".adoc", ".asciidoc",
|
||||||
|
};
|
||||||
|
if (writing_exts.count(ext))
|
||||||
|
return EditMode::Writing;
|
||||||
|
return EditMode::Code;
|
||||||
|
}
|
||||||
|
|
||||||
// Forward declaration for swap journal integration
|
// Forward declaration for swap journal integration
|
||||||
namespace kte {
|
namespace kte {
|
||||||
class SwapRecorder;
|
class SwapRecorder;
|
||||||
@@ -484,6 +507,35 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Edit mode (code vs writing)
|
||||||
|
[[nodiscard]] EditMode GetEditMode() const
|
||||||
|
{
|
||||||
|
return edit_mode_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SetEditMode(EditMode m)
|
||||||
|
{
|
||||||
|
edit_mode_ = m;
|
||||||
|
edit_mode_detected_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ToggleEditMode()
|
||||||
|
{
|
||||||
|
edit_mode_ = (edit_mode_ == EditMode::Code)
|
||||||
|
? EditMode::Writing
|
||||||
|
: EditMode::Code;
|
||||||
|
edit_mode_detected_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] bool EditModeDetected() const
|
||||||
|
{
|
||||||
|
return edit_mode_detected_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void SetSyntaxEnabled(bool on)
|
void SetSyntaxEnabled(bool on)
|
||||||
{
|
{
|
||||||
syntax_enabled_ = on;
|
syntax_enabled_ = on;
|
||||||
@@ -496,6 +548,25 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Marks that the user explicitly set syntax state via a command (:syntax
|
||||||
|
// on/off, :set filetype=...), as opposed to it being auto-applied from
|
||||||
|
// GUIConfig. Frontends that re-apply the config-driven default every
|
||||||
|
// frame (e.g. ImGuiFrontend::apply_syntax_to_buffer) must check this
|
||||||
|
// first, mirroring EditModeDetected()'s "don't stomp a manual toggle"
|
||||||
|
// pattern - otherwise a manual :syntax off is silently undone on the very
|
||||||
|
// next frame.
|
||||||
|
void SetSyntaxUserOverride(bool on)
|
||||||
|
{
|
||||||
|
syntax_user_override_ = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] bool SyntaxUserOverride() const
|
||||||
|
{
|
||||||
|
return syntax_user_override_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void SetFiletype(const std::string &ft)
|
void SetFiletype(const std::string &ft)
|
||||||
{
|
{
|
||||||
filetype_ = ft;
|
filetype_ = ft;
|
||||||
@@ -614,9 +685,14 @@ private:
|
|||||||
std::unique_ptr<struct UndoTree> undo_tree_;
|
std::unique_ptr<struct UndoTree> undo_tree_;
|
||||||
std::unique_ptr<UndoSystem> undo_sys_;
|
std::unique_ptr<UndoSystem> undo_sys_;
|
||||||
|
|
||||||
|
// Edit mode (code vs writing)
|
||||||
|
EditMode edit_mode_ = EditMode::Code;
|
||||||
|
bool edit_mode_detected_ = false; // true after initial auto-detection
|
||||||
|
|
||||||
// Syntax/highlighting state
|
// Syntax/highlighting state
|
||||||
std::uint64_t version_ = 0; // increment on edits
|
std::uint64_t version_ = 0; // increment on edits
|
||||||
bool syntax_enabled_ = true;
|
bool syntax_enabled_ = true;
|
||||||
|
bool syntax_user_override_ = false; // true once user explicitly set syntax state via a command
|
||||||
std::string filetype_;
|
std::string filetype_;
|
||||||
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
||||||
// Non-owning pointer to swap recorder managed by Editor/SwapManager
|
// Non-owning pointer to swap recorder managed by Editor/SwapManager
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**kte** (Kyle's Text Editor) is a C++20 text editor with a terminal-first design (ncurses) and optional GUI frontends (ImGui via SDL2/OpenGL/Freetype, or Qt6). It uses a WordStar/VDE-style command model. The terminal editor is `kte`; the GUI editor is `kge`.
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configure (from project root, build dir is "build")
|
||||||
|
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_GUI=ON -DBUILD_TESTS=ON
|
||||||
|
|
||||||
|
# Build everything
|
||||||
|
cmake --build build
|
||||||
|
|
||||||
|
# Build specific targets
|
||||||
|
cmake --build build --target kte # terminal editor
|
||||||
|
cmake --build build --target kge # GUI editor (requires -DBUILD_GUI=ON)
|
||||||
|
cmake --build build --target kte_tests # test suite
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
cmake --build build --target kte_tests && ./build/kte_tests
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no single-test runner; the test binary runs all tests. Tests use a minimal custom framework in `tests/Test.h` with `TEST()`, `ASSERT_EQ()`, `ASSERT_TRUE()`, `EXPECT_TRUE()` macros.
|
||||||
|
|
||||||
|
### Key CMake Options
|
||||||
|
|
||||||
|
| Flag | Default | Purpose |
|
||||||
|
|------|---------|---------|
|
||||||
|
| `BUILD_GUI` | ON | Build `kge` (ImGui GUI) |
|
||||||
|
| `KTE_USE_QT` | OFF | Use Qt6 instead of ImGui for GUI |
|
||||||
|
| `BUILD_TESTS` | ON | Build test suite |
|
||||||
|
| `ENABLE_ASAN` | OFF | AddressSanitizer |
|
||||||
|
| `KTE_STATIC_LINK` | OFF | Static linking (Linux only) |
|
||||||
|
| `KTE_ENABLE_TREESITTER` | OFF | Tree-sitter syntax highlighting |
|
||||||
|
|
||||||
|
### Nix
|
||||||
|
|
||||||
|
`flake.nix` provides devshells: `default` (ImGui+debug tools), `terminal`, `qt`.
|
||||||
|
|
||||||
|
### Docker (cross-platform Linux testing)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t kte-linux . && docker run --rm -v "$(pwd):/kte" kte-linux
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Three-layer design with strict frontend independence:
|
||||||
|
|
||||||
|
```
|
||||||
|
Frontend Layer (Terminal / ImGui / Qt / Test)
|
||||||
|
InputHandler.h, Renderer.h, Frontend.h interfaces
|
||||||
|
↓
|
||||||
|
Command Layer
|
||||||
|
CommandId enum → CommandRegistry → handler functions in Command.cc
|
||||||
|
↓
|
||||||
|
Core Model Layer
|
||||||
|
Editor → Buffer → PieceTable
|
||||||
|
UndoSystem (tree-based, records at PieceTable level)
|
||||||
|
SwapManager (crash recovery journal per buffer)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
- **PieceTable** (`PieceTable.h/.cc`) - Text storage. Lazy materialization; most ops work on the piece list directly. Line index and materialization caches must be invalidated on content changes.
|
||||||
|
- **Buffer** (`Buffer.h/.cc`) - Wraps PieceTable. Prefer `GetLineView(row)` (zero-copy) or `GetLineString(row)` over `Rows()` (legacy, materializes all lines). All text mutations must go through PieceTable API (`insert_text`, `delete_text`) to ensure undo and swap recording work.
|
||||||
|
- **Editor** (`Editor.h/.cc`) - Top-level state container. Primarily getters/setters; editing logic lives in commands.
|
||||||
|
- **Command** (`Command.h/.cc`) - 120+ editing commands. This is the main place to add new editing operations. Register via `CommandRegistry::Register()` in `InstallDefaultCommands()`.
|
||||||
|
- **UndoSystem/UndoTree/UndoNode** - Tree-based undo with branching. Group related ops with `buf.Undo()->BeginGroup()` / `EndGroup()`.
|
||||||
|
- **Swap** (`Swap.h/.cc`) - Append-only crash recovery journal. Uses circuit breaker pattern for resilience. Files in `~/.local/state/kte/`.
|
||||||
|
- **Syntax highlighting** (`syntax/`) - Pluggable per-language highlighters registered in `HighlighterRegistry`. Per-line caching with buffer version tracking.
|
||||||
|
|
||||||
|
### Frontend Implementations
|
||||||
|
|
||||||
|
Each frontend implements three interfaces (`Frontend.h`, `InputHandler.h`, `Renderer.h`):
|
||||||
|
- **Terminal**: ncurses-based (always built)
|
||||||
|
- **ImGui**: SDL2+OpenGL+Freetype (built with `-DBUILD_GUI=ON`)
|
||||||
|
- **Qt**: Qt6 (built with `-DBUILD_GUI=ON -DKTE_USE_QT=ON`)
|
||||||
|
- **Test**: Programmatic frontend for testing (always built, no UI deps)
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- **C++20**, compiled with `-Wall -Wextra -Werror -pedantic`
|
||||||
|
- **Clang** uses `-stdlib=libc++`
|
||||||
|
- **Naming**: PascalCase for classes/methods, snake_case for variables, trailing underscore for private members (e.g., `pieces_`)
|
||||||
|
- **Indentation**: Tabs
|
||||||
|
- **Error handling**: Fallible ops use `bool func(args..., std::string &err)` pattern. Always clear `err` at start, capture `errno` immediately after syscall failure. Use EINTR-safe wrappers from `SyscallWrappers.h` instead of raw syscalls.
|
||||||
|
- **ErrorHandler**: Centralized logging to `~/.local/state/kte/error.log` with severity levels (Info/Warning/Error/Critical).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Tests live in `tests/test_*.cc`. Use `TestFrontend`/`TestInputHandler`/`TestRenderer` for integration tests that exercise the full Editor+Buffer+Command stack without UI dependencies.
|
||||||
|
|
||||||
|
Key test files by area:
|
||||||
|
- PieceTable: `test_piece_table.cc`
|
||||||
|
- Buffer I/O: `test_buffer_io.cc`
|
||||||
|
- Commands: `test_command_semantics.cc`
|
||||||
|
- Search/replace: `test_search.cc`, `test_search_replace_flow.cc`
|
||||||
|
- Undo: `test_undo.cc`
|
||||||
|
- Swap (crash recovery): `test_swap_*.cc` (7 files)
|
||||||
|
- Reflow: `test_reflow_paragraph.cc`, `test_reflow_indented_bullets.cc`
|
||||||
|
- Integration: `test_daily_workflows.cc`
|
||||||
|
|
||||||
|
## Important Caveats
|
||||||
|
|
||||||
|
- `Buffer::Rows()` is legacy; use `GetLineView()` / `GetLineString()` in new code
|
||||||
|
- `GetLineView()` returns a `string_view` valid only until next buffer modification
|
||||||
|
- After editing ops, call `ensure_cursor_visible()` to update viewport
|
||||||
|
- All source files are in the project root (no `src/` directory); tests are in `tests/`; syntax highlighters in `syntax/`; themes in `themes/`; embedded fonts in `fonts/`
|
||||||
|
- External deps: `ext/imgui/` (Dear ImGui), `ext/tomlplusplus/` (TOML parser)
|
||||||
|
- GUI config: `~/.config/kte/kge.toml` (TOML preferred over legacy INI)
|
||||||
+36
-3
@@ -4,7 +4,7 @@ project(kte)
|
|||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(KTE_VERSION "1.8.0")
|
set(KTE_VERSION "1.12.0")
|
||||||
|
|
||||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||||
@@ -14,7 +14,7 @@ set(BUILD_TESTS ON CACHE BOOL "Enable building test programs.")
|
|||||||
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
||||||
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
|
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
|
||||||
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
|
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
|
||||||
option(KTE_STATIC_LINK "Enable static linking on Linux" ON)
|
option(KTE_STATIC_LINK "Enable static linking on Linux" OFF)
|
||||||
|
|
||||||
# Optionally enable AddressSanitizer (ASan)
|
# Optionally enable AddressSanitizer (ASan)
|
||||||
option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF)
|
option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF)
|
||||||
@@ -51,6 +51,7 @@ else ()
|
|||||||
)
|
)
|
||||||
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
||||||
add_compile_options("-stdlib=libc++")
|
add_compile_options("-stdlib=libc++")
|
||||||
|
add_link_options("-stdlib=libc++")
|
||||||
else ()
|
else ()
|
||||||
# nothing special for gcc at the moment
|
# nothing special for gcc at the moment
|
||||||
endif ()
|
endif ()
|
||||||
@@ -70,8 +71,34 @@ endif ()
|
|||||||
# NCurses for terminal mode
|
# NCurses for terminal mode
|
||||||
set(CURSES_NEED_NCURSES TRUE)
|
set(CURSES_NEED_NCURSES TRUE)
|
||||||
set(CURSES_NEED_WIDE TRUE)
|
set(CURSES_NEED_WIDE TRUE)
|
||||||
|
|
||||||
|
# macOS ships only a narrow-char ncurses in the SDK, and Homebrew's wide-char
|
||||||
|
# build is keg-only, so FindCurses can't see it without a hint.
|
||||||
|
if (APPLE)
|
||||||
|
find_program(BREW_EXECUTABLE brew)
|
||||||
|
if (BREW_EXECUTABLE)
|
||||||
|
execute_process(COMMAND "${BREW_EXECUTABLE}" --prefix ncurses
|
||||||
|
OUTPUT_VARIABLE HOMEBREW_NCURSES_PREFIX
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
ERROR_QUIET)
|
||||||
|
if (HOMEBREW_NCURSES_PREFIX AND IS_DIRECTORY "${HOMEBREW_NCURSES_PREFIX}")
|
||||||
|
message(STATUS "Using Homebrew ncursesw: ${HOMEBREW_NCURSES_PREFIX}")
|
||||||
|
list(PREPEND CMAKE_PREFIX_PATH "${HOMEBREW_NCURSES_PREFIX}")
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
find_package(Curses REQUIRED)
|
find_package(Curses REQUIRED)
|
||||||
include_directories(${CURSES_INCLUDE_DIR})
|
|
||||||
|
# FindCurses exports CURSES_INCLUDE_DIRS; the singular CURSES_INCLUDE_DIR was
|
||||||
|
# never set, so this silently expanded to nothing and the compiler fell back
|
||||||
|
# to whatever curses header happened to be on the default include path.
|
||||||
|
include_directories(${CURSES_INCLUDE_DIRS})
|
||||||
|
|
||||||
|
# The wide-char entry points we rely on (get_wch, etc.) are declared only when
|
||||||
|
# NCURSES_WIDECHAR is set; the header defaults it to 0 unless a feature-test
|
||||||
|
# macro says otherwise. We always link the wide library, so ask for them.
|
||||||
|
add_compile_definitions(NCURSES_WIDECHAR=1)
|
||||||
|
|
||||||
# On Alpine Linux, CMake's FindCurses looks in wrong paths
|
# On Alpine Linux, CMake's FindCurses looks in wrong paths
|
||||||
# Manually find the correct ncurses library
|
# Manually find the correct ncurses library
|
||||||
@@ -205,6 +232,8 @@ set(FONT_HEADERS
|
|||||||
fonts/FontList.h
|
fonts/FontList.h
|
||||||
fonts/B612Mono.h
|
fonts/B612Mono.h
|
||||||
fonts/BrassMono.h
|
fonts/BrassMono.h
|
||||||
|
fonts/CrimsonPro.h
|
||||||
|
fonts/ETBook.h
|
||||||
fonts/BrassMonoCode.h
|
fonts/BrassMonoCode.h
|
||||||
fonts/FiraCode.h
|
fonts/FiraCode.h
|
||||||
fonts/Go.h
|
fonts/Go.h
|
||||||
@@ -216,6 +245,7 @@ set(FONT_HEADERS
|
|||||||
fonts/IosevkaExtended.h
|
fonts/IosevkaExtended.h
|
||||||
fonts/ShareTech.h
|
fonts/ShareTech.h
|
||||||
fonts/SpaceMono.h
|
fonts/SpaceMono.h
|
||||||
|
fonts/Spectral.h
|
||||||
fonts/Syne.h
|
fonts/Syne.h
|
||||||
fonts/Triplicate.h
|
fonts/Triplicate.h
|
||||||
fonts/Unispace.h
|
fonts/Unispace.h
|
||||||
@@ -327,6 +357,7 @@ if (BUILD_TESTS)
|
|||||||
tests/test_swap_edge_cases.cc
|
tests/test_swap_edge_cases.cc
|
||||||
tests/test_swap_recovery_prompt.cc
|
tests/test_swap_recovery_prompt.cc
|
||||||
tests/test_swap_cleanup.cc
|
tests/test_swap_cleanup.cc
|
||||||
|
tests/test_swap_cleanup2.cc
|
||||||
tests/test_swap_git_editor.cc
|
tests/test_swap_git_editor.cc
|
||||||
tests/test_piece_table.cc
|
tests/test_piece_table.cc
|
||||||
tests/test_search.cc
|
tests/test_search.cc
|
||||||
@@ -339,6 +370,8 @@ if (BUILD_TESTS)
|
|||||||
tests/test_migration_coverage.cc
|
tests/test_migration_coverage.cc
|
||||||
tests/test_smart_newline.cc
|
tests/test_smart_newline.cc
|
||||||
tests/test_reflow_undo.cc
|
tests/test_reflow_undo.cc
|
||||||
|
tests/test_syntax_highlighting.cc
|
||||||
|
tests/test_paste_split.cc
|
||||||
|
|
||||||
# minimal engine sources required by Buffer
|
# minimal engine sources required by Buffer
|
||||||
PieceTable.cc
|
PieceTable.cc
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# kge Configuration
|
||||||
|
|
||||||
|
kge loads configuration from `~/.config/kte/kge.toml`. If no TOML file is
|
||||||
|
found, it falls back to the legacy `kge.ini` format.
|
||||||
|
|
||||||
|
## TOML Format
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[window]
|
||||||
|
fullscreen = false
|
||||||
|
columns = 80
|
||||||
|
rows = 42
|
||||||
|
|
||||||
|
[font]
|
||||||
|
# Default font and size
|
||||||
|
name = "default"
|
||||||
|
size = 18.0
|
||||||
|
# Font used in code mode (monospace)
|
||||||
|
code = "default"
|
||||||
|
# Font used in writing mode (proportional)
|
||||||
|
writing = "crimsonpro"
|
||||||
|
|
||||||
|
[appearance]
|
||||||
|
theme = "nord"
|
||||||
|
# "dark" or "light" for themes with variants
|
||||||
|
background = "dark"
|
||||||
|
|
||||||
|
[editor]
|
||||||
|
syntax = true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sections
|
||||||
|
|
||||||
|
### `[window]`
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
|--------------|------|---------|---------------------------------|
|
||||||
|
| `fullscreen` | bool | false | Start in fullscreen mode |
|
||||||
|
| `columns` | int | 80 | Initial window width in columns |
|
||||||
|
| `rows` | int | 42 | Initial window height in rows |
|
||||||
|
|
||||||
|
### `[font]`
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
|-----------|--------|--------------|------------------------------------------|
|
||||||
|
| `name` | string | "default" | Default font loaded at startup |
|
||||||
|
| `size` | float | 18.0 | Font size in pixels |
|
||||||
|
| `code` | string | "default" | Font for code mode (monospace) |
|
||||||
|
| `writing` | string | "crimsonpro" | Font for writing mode (proportional) |
|
||||||
|
|
||||||
|
### `[appearance]`
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
|--------------|--------|---------|-----------------------------------------|
|
||||||
|
| `theme` | string | "nord" | Color theme |
|
||||||
|
| `background` | string | "dark" | Background mode: "dark" or "light" |
|
||||||
|
|
||||||
|
### `[editor]`
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
|----------|------|---------|------------------------------|
|
||||||
|
| `syntax` | bool | true | Enable syntax highlighting |
|
||||||
|
|
||||||
|
## Edit Modes
|
||||||
|
|
||||||
|
kge has two edit modes that control which font is used:
|
||||||
|
|
||||||
|
- **code** — Uses the monospace font (`font.code`). Default for source files.
|
||||||
|
- **writing** — Uses the proportional font (`font.writing`). Auto-detected
|
||||||
|
for `.txt`, `.md`, `.markdown`, `.rst`, `.org`, `.tex`, `.adoc`, and
|
||||||
|
`.asciidoc` files.
|
||||||
|
|
||||||
|
Toggle with `C-k m` or `: mode [code|writing]`.
|
||||||
|
|
||||||
|
## Available Fonts
|
||||||
|
|
||||||
|
### Monospace
|
||||||
|
|
||||||
|
b612, berkeley, berkeley-bold, brassmono, brassmono-bold, brassmonocode,
|
||||||
|
brassmonocode-bold, fira, go, ibm, idealist, inconsolata, inconsolataex,
|
||||||
|
iosevka, iosevkaex, sharetech, space, syne, triplicate, unispace
|
||||||
|
|
||||||
|
### Proportional (Serif)
|
||||||
|
|
||||||
|
crimsonpro, etbook, spectral
|
||||||
|
|
||||||
|
## Available Themes
|
||||||
|
|
||||||
|
amber, eink, everforest, gruvbox, kanagawa-paper, lcars, leuchtturm, nord,
|
||||||
|
old-book, orbital, plan9, solarized, tufte, weyland-yutani, zenburn
|
||||||
|
|
||||||
|
Themes with light/dark variants: eink, gruvbox, leuchtturm, old-book,
|
||||||
|
solarized. Set `background = "light"` or use `: background light`.
|
||||||
|
|
||||||
|
## Migrating from kge.ini
|
||||||
|
|
||||||
|
If you have an existing `kge.ini`, kge will still read it but prints a
|
||||||
|
notice to stderr suggesting migration. To migrate, create `kge.toml` in the
|
||||||
|
same directory (`~/.config/kte/`) using the format above. The TOML file
|
||||||
|
takes priority when both exist.
|
||||||
|
|
||||||
|
The INI keys map to TOML as follows:
|
||||||
|
|
||||||
|
| INI key | TOML equivalent |
|
||||||
|
|---------------|--------------------------|
|
||||||
|
| `fullscreen` | `window.fullscreen` |
|
||||||
|
| `columns` | `window.columns` |
|
||||||
|
| `rows` | `window.rows` |
|
||||||
|
| `font` | `font.name` |
|
||||||
|
| `font_size` | `font.size` |
|
||||||
|
| `theme` | `appearance.theme` |
|
||||||
|
| `background` | `appearance.background` |
|
||||||
|
| `syntax` | `editor.syntax` |
|
||||||
|
|
||||||
|
New keys `font.code` and `font.writing` have no INI equivalent (the INI
|
||||||
|
parser accepts `code_font` and `writing_font` if needed).
|
||||||
+242
-45
@@ -159,6 +159,27 @@ ensure_at_least_one_line(Buffer &buf)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RAII helper: brackets a multi-edit command so its individual undo nodes
|
||||||
|
// undo/redo as a single atomic step, per the project's undo-grouping convention.
|
||||||
|
struct UndoGroupGuard {
|
||||||
|
UndoSystem *u;
|
||||||
|
|
||||||
|
|
||||||
|
explicit UndoGroupGuard(UndoSystem *u_) : u(u_)
|
||||||
|
{
|
||||||
|
if (u)
|
||||||
|
u->BeginGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
~UndoGroupGuard()
|
||||||
|
{
|
||||||
|
if (u)
|
||||||
|
u->EndGroup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Determine if a command mutates the buffer contents (text edits)
|
// Determine if a command mutates the buffer contents (text edits)
|
||||||
static bool
|
static bool
|
||||||
is_mutating_command(CommandId id)
|
is_mutating_command(CommandId id)
|
||||||
@@ -264,8 +285,12 @@ extract_region_text(const Buffer &buf, std::size_t sx, std::size_t sy, std::size
|
|||||||
|
|
||||||
|
|
||||||
// Helper: delete region and leave cursor at start (sx,sy). Adjust lines appropriately.
|
// Helper: delete region and leave cursor at start (sx,sy). Adjust lines appropriately.
|
||||||
|
// If `u` is non-null, each underlying mutation is recorded as its own undo node;
|
||||||
|
// callers that want the whole region-delete to undo/redo as one step should
|
||||||
|
// wrap the call in an UndoGroupGuard.
|
||||||
static void
|
static void
|
||||||
delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::size_t ey)
|
delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::size_t ey,
|
||||||
|
UndoSystem *u = nullptr)
|
||||||
{
|
{
|
||||||
std::size_t nrows = buf.Nrows();
|
std::size_t nrows = buf.Nrows();
|
||||||
if (nrows == 0)
|
if (nrows == 0)
|
||||||
@@ -282,7 +307,14 @@ delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::
|
|||||||
std::size_t xe = std::min(ex, line.size());
|
std::size_t xe = std::min(ex, line.size());
|
||||||
if (xe < xs)
|
if (xe < xs)
|
||||||
std::swap(xs, xe);
|
std::swap(xs, xe);
|
||||||
|
std::string deleted = std::string(line.substr(xs, xe - xs));
|
||||||
buf.delete_text(static_cast<int>(sy), static_cast<int>(xs), xe - xs);
|
buf.delete_text(static_cast<int>(sy), static_cast<int>(xs), xe - xs);
|
||||||
|
if (u && !deleted.empty()) {
|
||||||
|
buf.SetCursor(xs, sy);
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
|
u->Append(std::string_view(deleted));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Multi-line: delete from (sx,sy) to (ex,ey)
|
// Multi-line: delete from (sx,sy) to (ex,ey)
|
||||||
// Strategy:
|
// Strategy:
|
||||||
@@ -303,11 +335,25 @@ delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::
|
|||||||
|
|
||||||
// Delete tail of first line (from xs to end)
|
// Delete tail of first line (from xs to end)
|
||||||
if (xs < first_line_len) {
|
if (xs < first_line_len) {
|
||||||
|
std::string tail = std::string(rows[sy].substr(xs));
|
||||||
buf.delete_text(static_cast<int>(sy), static_cast<int>(xs), first_line_len - xs);
|
buf.delete_text(static_cast<int>(sy), static_cast<int>(xs), first_line_len - xs);
|
||||||
|
if (u && !tail.empty()) {
|
||||||
|
buf.SetCursor(xs, sy);
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
|
u->Append(std::string_view(tail));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete lines from ey down to sy+1 (reverse order to preserve indices)
|
// Delete lines from ey down to sy+1 (reverse order to preserve indices)
|
||||||
for (std::size_t i = ey; i > sy; --i) {
|
for (std::size_t i = ey; i > sy; --i) {
|
||||||
|
if (u) {
|
||||||
|
std::string row_text = static_cast<std::string>(buf.Rows()[i]);
|
||||||
|
buf.SetCursor(0, i);
|
||||||
|
u->Begin(UndoType::DeleteRow);
|
||||||
|
u->Append(std::string_view(row_text));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
buf.delete_row(static_cast<int>(i));
|
buf.delete_row(static_cast<int>(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +363,12 @@ delete_region(Buffer &buf, std::size_t sx, std::size_t sy, std::size_t ex, std::
|
|||||||
const auto &rows_after = buf.Rows();
|
const auto &rows_after = buf.Rows();
|
||||||
std::size_t line_len = rows_after[sy].size();
|
std::size_t line_len = rows_after[sy].size();
|
||||||
buf.insert_text(static_cast<int>(sy), static_cast<int>(line_len), suffix);
|
buf.insert_text(static_cast<int>(sy), static_cast<int>(line_len), suffix);
|
||||||
|
if (u) {
|
||||||
|
buf.SetCursor(line_len, sy);
|
||||||
|
u->Begin(UndoType::Insert);
|
||||||
|
u->Append(std::string_view(suffix));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buf.SetCursor(sx, sy);
|
buf.SetCursor(sx, sy);
|
||||||
@@ -752,6 +804,8 @@ cmd_save_and_quit(CommandContext &ctx)
|
|||||||
if (buf->IsFileBacked()) {
|
if (buf->IsFileBacked()) {
|
||||||
if (buf->Save(err)) {
|
if (buf->Save(err)) {
|
||||||
buf->SetDirty(false);
|
buf->SetDirty(false);
|
||||||
|
if (auto *sm = ctx.editor.Swap())
|
||||||
|
sm->ResetJournal(*buf);
|
||||||
} else {
|
} else {
|
||||||
ctx.editor.SetStatus(err);
|
ctx.editor.SetStatus(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -759,6 +813,8 @@ cmd_save_and_quit(CommandContext &ctx)
|
|||||||
} else if (!buf->Filename().empty()) {
|
} else if (!buf->Filename().empty()) {
|
||||||
if (buf->SaveAs(buf->Filename(), err)) {
|
if (buf->SaveAs(buf->Filename(), err)) {
|
||||||
buf->SetDirty(false);
|
buf->SetDirty(false);
|
||||||
|
if (auto *sm = ctx.editor.Swap())
|
||||||
|
sm->ResetJournal(*buf);
|
||||||
} else {
|
} else {
|
||||||
ctx.editor.SetStatus(err);
|
ctx.editor.SetStatus(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -904,6 +960,10 @@ cmd_unknown_esc_command(CommandContext &ctx)
|
|||||||
static void
|
static void
|
||||||
apply_filetype(Buffer &buf, const std::string &ft)
|
apply_filetype(Buffer &buf, const std::string &ft)
|
||||||
{
|
{
|
||||||
|
// Only reachable from explicit user commands (:syntax on, :set
|
||||||
|
// filetype=...) - mark so frontends that re-apply config-driven syntax
|
||||||
|
// defaults every frame (e.g. ImGuiFrontend) don't stomp this choice.
|
||||||
|
buf.SetSyntaxUserOverride(true);
|
||||||
buf.EnsureHighlighter();
|
buf.EnsureHighlighter();
|
||||||
auto *eng = buf.Highlighter();
|
auto *eng = buf.Highlighter();
|
||||||
if (!eng)
|
if (!eng)
|
||||||
@@ -973,6 +1033,7 @@ cmd_syntax(CommandContext &ctx)
|
|||||||
};
|
};
|
||||||
trim(arg);
|
trim(arg);
|
||||||
if (arg == "on") {
|
if (arg == "on") {
|
||||||
|
b->SetSyntaxUserOverride(true);
|
||||||
b->SetSyntaxEnabled(true);
|
b->SetSyntaxEnabled(true);
|
||||||
// If no highlighter but filetype is cpp by extension, set it
|
// If no highlighter but filetype is cpp by extension, set it
|
||||||
if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
|
if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
|
||||||
@@ -980,6 +1041,7 @@ cmd_syntax(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
ctx.editor.SetStatus("syntax: on");
|
ctx.editor.SetStatus("syntax: on");
|
||||||
} else if (arg == "off") {
|
} else if (arg == "off") {
|
||||||
|
b->SetSyntaxUserOverride(true);
|
||||||
b->SetSyntaxEnabled(false);
|
b->SetSyntaxEnabled(false);
|
||||||
ctx.editor.SetStatus("syntax: off");
|
ctx.editor.SetStatus("syntax: off");
|
||||||
} else if (arg == "reload") {
|
} else if (arg == "reload") {
|
||||||
@@ -1331,6 +1393,43 @@ cmd_font_set_size(CommandContext &ctx)
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// Toggle edit mode (code/writing) for current buffer
|
||||||
|
static bool
|
||||||
|
cmd_toggle_edit_mode(const CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *b = ctx.editor.CurrentBuffer();
|
||||||
|
if (!b)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::string arg = ctx.arg;
|
||||||
|
std::transform(arg.begin(), arg.end(), arg.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::tolower(c));
|
||||||
|
});
|
||||||
|
// Trim whitespace
|
||||||
|
auto start = arg.find_first_not_of(" \t");
|
||||||
|
if (start != std::string::npos)
|
||||||
|
arg = arg.substr(start);
|
||||||
|
auto end = arg.find_last_not_of(" \t");
|
||||||
|
if (end != std::string::npos)
|
||||||
|
arg = arg.substr(0, end + 1);
|
||||||
|
|
||||||
|
if (arg == "code") {
|
||||||
|
b->SetEditMode(EditMode::Code);
|
||||||
|
} else if (arg == "writing") {
|
||||||
|
b->SetEditMode(EditMode::Writing);
|
||||||
|
} else {
|
||||||
|
b->ToggleEditMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writing mode disables syntax highlighting; code mode re-enables it.
|
||||||
|
b->SetSyntaxEnabled(b->GetEditMode() == EditMode::Code);
|
||||||
|
|
||||||
|
const char *mode_str = (b->GetEditMode() == EditMode::Writing) ? "writing" : "code";
|
||||||
|
ctx.editor.SetStatus(std::string("Mode: ") + mode_str);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Background set command (GUI, ImGui-only for now)
|
// Background set command (GUI, ImGui-only for now)
|
||||||
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
||||||
static bool
|
static bool
|
||||||
@@ -1353,6 +1452,10 @@ cmd_background_set(const CommandContext &ctx)
|
|||||||
std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) {
|
std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) {
|
||||||
return (char) std::tolower(c);
|
return (char) std::tolower(c);
|
||||||
});
|
});
|
||||||
|
if (mode.empty()) {
|
||||||
|
ctx.editor.SetStatus(std::string("Background: ") + kte::BackgroundModeName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (mode != "light" && mode != "dark") {
|
if (mode != "light" && mode != "dark") {
|
||||||
ctx.editor.SetStatus("background: expected 'light' or 'dark'");
|
ctx.editor.SetStatus("background: expected 'light' or 'dark'");
|
||||||
return true;
|
return true;
|
||||||
@@ -1880,15 +1983,15 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
if (cmd == "font") {
|
if (cmd == "font") {
|
||||||
#if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
|
|
||||||
// Complete against installed font families (case-insensitive prefix)
|
|
||||||
std::vector<std::string> cands;
|
std::vector<std::string> cands;
|
||||||
QStringList fams = QFontDatabase::families();
|
|
||||||
std::string apfx_lower = argprefix;
|
std::string apfx_lower = argprefix;
|
||||||
std::transform(apfx_lower.begin(), apfx_lower.end(), apfx_lower.begin(),
|
std::transform(apfx_lower.begin(), apfx_lower.end(), apfx_lower.begin(),
|
||||||
[](unsigned char c) {
|
[](unsigned char c) {
|
||||||
return (char) std::tolower(c);
|
return (char) std::tolower(c);
|
||||||
});
|
});
|
||||||
|
#if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
|
||||||
|
// Qt: complete against system font families
|
||||||
|
QStringList fams = QFontDatabase::families();
|
||||||
for (const auto &fam: fams) {
|
for (const auto &fam: fams) {
|
||||||
std::string n = fam.toStdString();
|
std::string n = fam.toStdString();
|
||||||
std::string nlower = n;
|
std::string nlower = n;
|
||||||
@@ -1899,6 +2002,13 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
if (apfx_lower.empty() || nlower.rfind(apfx_lower, 0) == 0)
|
if (apfx_lower.empty() || nlower.rfind(apfx_lower, 0) == 0)
|
||||||
cands.push_back(n);
|
cands.push_back(n);
|
||||||
}
|
}
|
||||||
|
#elif defined(KTE_BUILD_GUI)
|
||||||
|
// ImGui: complete against embedded font registry
|
||||||
|
for (const auto &n : kte::Fonts::FontRegistry::Instance().FontNames()) {
|
||||||
|
if (apfx_lower.empty() || n.rfind(apfx_lower, 0) == 0)
|
||||||
|
cands.push_back(n);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (cands.empty()) {
|
if (cands.empty()) {
|
||||||
// no change
|
// no change
|
||||||
} else if (cands.size() == 1) {
|
} else if (cands.size() == 1) {
|
||||||
@@ -1919,9 +2029,19 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
||||||
return true;
|
return true;
|
||||||
#else
|
}
|
||||||
(void) argprefix;
|
if (cmd == "mode") {
|
||||||
#endif
|
std::vector<std::string> modes = {"code", "writing"};
|
||||||
|
std::vector<std::string> cands;
|
||||||
|
for (const auto &m : modes) {
|
||||||
|
if (argprefix.empty() || m.rfind(argprefix, 0) == 0)
|
||||||
|
cands.push_back(m);
|
||||||
|
}
|
||||||
|
if (cands.size() == 1) {
|
||||||
|
ctx.editor.SetPromptText(cmd + std::string(" ") + cands[0]);
|
||||||
|
}
|
||||||
|
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
// default: no special arg completion
|
// default: no special arg completion
|
||||||
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
||||||
@@ -2447,12 +2567,10 @@ cmd_newline(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
pos = p + with.size();
|
pos = p + with.size();
|
||||||
} else {
|
} else {
|
||||||
// When replacing with empty, continue after the deletion point to avoid re-matching
|
// Replacing with empty leaves nothing inserted at the deletion
|
||||||
|
// point, so resume scanning from `p` itself (not p+1) to catch
|
||||||
|
// adjacent/overlapping matches, e.g. "aaaa" -> "" replacing "aa".
|
||||||
pos = p;
|
pos = p;
|
||||||
if (pos < static_cast<std::size_t>(buf->Rows()[y].size()))
|
|
||||||
++pos;
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
++total;
|
++total;
|
||||||
}
|
}
|
||||||
@@ -2568,6 +2686,10 @@ cmd_newline(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus(err);
|
ctx.editor.SetStatus(err);
|
||||||
} else {
|
} else {
|
||||||
buf->SetDirty(false);
|
buf->SetDirty(false);
|
||||||
|
if (auto *sm = ctx.editor.Swap()) {
|
||||||
|
sm->NotifyFilenameChanged(*buf);
|
||||||
|
sm->ResetJournal(*buf);
|
||||||
|
}
|
||||||
ctx.editor.SetStatus("Saved as " + value);
|
ctx.editor.SetStatus("Saved as " + value);
|
||||||
if (auto *u = buf->Undo())
|
if (auto *u = buf->Undo())
|
||||||
u->mark_saved();
|
u->mark_saved();
|
||||||
@@ -2857,14 +2979,28 @@ cmd_newline(CommandContext &ctx)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
std::size_t changed = 0;
|
std::size_t changed = 0;
|
||||||
|
UndoSystem *ru = buf->Undo();
|
||||||
|
UndoGroupGuard rguard(ru);
|
||||||
// Iterate by index to allow modifications via PieceTable helpers
|
// Iterate by index to allow modifications via PieceTable helpers
|
||||||
for (std::size_t y = 0; y < buf->Rows().size(); ++y) {
|
for (std::size_t y = 0; y < buf->Rows().size(); ++y) {
|
||||||
std::string before = static_cast<std::string>(buf->Rows()[y]);
|
std::string before = static_cast<std::string>(buf->Rows()[y]);
|
||||||
std::string after = std::regex_replace(before, rx, repl);
|
std::string after = std::regex_replace(before, rx, repl);
|
||||||
if (after != before) {
|
if (after != before) {
|
||||||
// Replace entire line y with 'after' using PieceTable ops
|
// Replace entire line y with 'after' using PieceTable ops
|
||||||
|
if (ru) {
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
ru->Begin(UndoType::DeleteRow);
|
||||||
|
ru->Append(std::string_view(before));
|
||||||
|
ru->commit();
|
||||||
|
}
|
||||||
buf->delete_row(static_cast<int>(y));
|
buf->delete_row(static_cast<int>(y));
|
||||||
buf->insert_row(static_cast<int>(y), std::string_view(after));
|
buf->insert_row(static_cast<int>(y), std::string_view(after));
|
||||||
|
if (ru) {
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
ru->Begin(UndoType::InsertRow);
|
||||||
|
ru->Append(std::string_view(after));
|
||||||
|
ru->commit();
|
||||||
|
}
|
||||||
++changed;
|
++changed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2937,18 +3073,28 @@ cmd_newline(CommandContext &ctx)
|
|||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
UndoSystem *u = buf->Undo();
|
||||||
|
if (u && repeat > 1)
|
||||||
|
u->BeginGroup();
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
|
// Sync the buffer's cursor to the split point before Begin(), which
|
||||||
|
// records its node's row/col from the buffer's *current* cursor. Undo
|
||||||
|
// must reverse this exact split, not wherever the cursor ends up after
|
||||||
|
// the whole loop finishes.
|
||||||
|
buf->SetCursor(x, y);
|
||||||
|
if (u) {
|
||||||
|
u->Begin(UndoType::Newline);
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
buf->split_line(static_cast<int>(y), static_cast<int>(x));
|
buf->split_line(static_cast<int>(y), static_cast<int>(x));
|
||||||
// Move to start of next line
|
// Move to start of next line
|
||||||
y += 1;
|
y += 1;
|
||||||
x = 0;
|
x = 0;
|
||||||
}
|
}
|
||||||
|
if (u && repeat > 1)
|
||||||
|
u->EndGroup();
|
||||||
buf->SetCursor(x, y);
|
buf->SetCursor(x, y);
|
||||||
buf->SetDirty(true);
|
buf->SetDirty(true);
|
||||||
if (auto *u = buf->Undo()) {
|
|
||||||
u->Begin(UndoType::Newline);
|
|
||||||
u->commit();
|
|
||||||
}
|
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3132,7 +3278,9 @@ cmd_backspace(CommandContext &ctx)
|
|||||||
x = prev_len;
|
x = prev_len;
|
||||||
buf->SetCursor(x, y);
|
buf->SetCursor(x, y);
|
||||||
if (u) {
|
if (u) {
|
||||||
u->Begin(UndoType::Newline);
|
// Forward action here is a join, not a split: JoinLines has the
|
||||||
|
// correct (inverted) apply() semantics, unlike Newline.
|
||||||
|
u->Begin(UndoType::JoinLines);
|
||||||
u->commit();
|
u->commit();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -3211,7 +3359,9 @@ cmd_delete_char(CommandContext &ctx)
|
|||||||
} else if (y + 1 < rows_view.size()) {
|
} else if (y + 1 < rows_view.size()) {
|
||||||
buf->join_lines(static_cast<int>(y));
|
buf->join_lines(static_cast<int>(y));
|
||||||
if (u) {
|
if (u) {
|
||||||
u->Begin(UndoType::Newline);
|
// Forward action here is a join, not a split: JoinLines has the
|
||||||
|
// correct (inverted) apply() semantics, unlike Newline.
|
||||||
|
u->Begin(UndoType::JoinLines);
|
||||||
u->commit();
|
u->commit();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -3284,18 +3434,32 @@ cmd_kill_to_eol(CommandContext &ctx)
|
|||||||
std::size_t x = buf->Curx();
|
std::size_t x = buf->Curx();
|
||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
std::string killed_total;
|
std::string killed_total;
|
||||||
|
UndoSystem *u = buf->Undo();
|
||||||
|
UndoGroupGuard guard(u);
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
const auto &rows_view = buf->Rows();
|
const auto &rows_view = buf->Rows();
|
||||||
if (y >= rows_view.size())
|
if (y >= rows_view.size())
|
||||||
break;
|
break;
|
||||||
if (x < rows_view[y].size()) {
|
if (x < rows_view[y].size()) {
|
||||||
// delete from cursor to end of line
|
// delete from cursor to end of line
|
||||||
killed_total += rows_view[y].substr(x);
|
std::string seg = static_cast<std::string>(rows_view[y].substr(x));
|
||||||
|
killed_total += seg;
|
||||||
std::size_t len = rows_view[y].size() - x;
|
std::size_t len = rows_view[y].size() - x;
|
||||||
buf->delete_text(static_cast<int>(y), static_cast<int>(x), len);
|
buf->delete_text(static_cast<int>(y), static_cast<int>(x), len);
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(x, y);
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
|
u->Append(std::string_view(seg));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
} else if (y + 1 < rows_view.size()) {
|
} else if (y + 1 < rows_view.size()) {
|
||||||
// at EOL: delete the newline (join with next line)
|
// at EOL: delete the newline (join with next line)
|
||||||
killed_total += "\n";
|
killed_total += "\n";
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(x, y);
|
||||||
|
u->Begin(UndoType::JoinLines);
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
buf->join_lines(static_cast<int>(y));
|
buf->join_lines(static_cast<int>(y));
|
||||||
} else {
|
} else {
|
||||||
// nothing to delete
|
// nothing to delete
|
||||||
@@ -3329,20 +3493,37 @@ cmd_kill_line(CommandContext &ctx)
|
|||||||
(void) x; // cursor x will be reset to 0
|
(void) x; // cursor x will be reset to 0
|
||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
std::string killed_total;
|
std::string killed_total;
|
||||||
|
UndoSystem *u = buf->Undo();
|
||||||
|
UndoGroupGuard guard(u);
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
const auto &rows_view = buf->Rows();
|
const auto &rows_view = buf->Rows();
|
||||||
if (rows_view.empty())
|
if (rows_view.empty())
|
||||||
break;
|
break;
|
||||||
if (rows_view.size() == 1) {
|
if (rows_view.size() == 1) {
|
||||||
// last remaining line: clear its contents
|
// last remaining line: clear its contents
|
||||||
killed_total += static_cast<std::string>(rows_view[0]);
|
std::string content = static_cast<std::string>(rows_view[0]);
|
||||||
if (!rows_view[0].empty())
|
killed_total += content;
|
||||||
buf->delete_text(0, 0, rows_view[0].size());
|
if (!content.empty()) {
|
||||||
|
buf->delete_text(0, 0, content.size());
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(0, 0);
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
|
u->Append(std::string_view(content));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
y = 0;
|
y = 0;
|
||||||
} else if (y < rows_view.size()) {
|
} else if (y < rows_view.size()) {
|
||||||
// erase current line; keep y pointing at the next line
|
// erase current line; keep y pointing at the next line
|
||||||
killed_total += static_cast<std::string>(rows_view[y]);
|
std::string content = static_cast<std::string>(rows_view[y]);
|
||||||
|
killed_total += content;
|
||||||
killed_total += "\n";
|
killed_total += "\n";
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
u->Begin(UndoType::DeleteRow);
|
||||||
|
u->Append(std::string_view(content));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
buf->delete_row(static_cast<int>(y));
|
buf->delete_row(static_cast<int>(y));
|
||||||
const auto &rows_after = buf->Rows();
|
const auto &rows_after = buf->Rows();
|
||||||
if (y >= rows_after.size()) {
|
if (y >= rows_after.size()) {
|
||||||
@@ -3556,7 +3737,10 @@ cmd_kill_region(CommandContext &ctx)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::string text = extract_region_text(*buf, sx, sy, ex, ey);
|
std::string text = extract_region_text(*buf, sx, sy, ex, ey);
|
||||||
delete_region(*buf, sx, sy, ex, ey);
|
{
|
||||||
|
UndoGroupGuard guard(buf->Undo());
|
||||||
|
delete_region(*buf, sx, sy, ex, ey, buf->Undo());
|
||||||
|
}
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
if (!text.empty()) {
|
if (!text.empty()) {
|
||||||
if (ctx.editor.KillChain())
|
if (ctx.editor.KillChain())
|
||||||
@@ -4168,6 +4352,7 @@ cmd_delete_word_prev(CommandContext &ctx)
|
|||||||
std::size_t x = buf->Curx();
|
std::size_t x = buf->Curx();
|
||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
std::string killed_total;
|
std::string killed_total;
|
||||||
|
UndoGroupGuard guard(buf->Undo());
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
if (y >= rows.size()) {
|
if (y >= rows.size()) {
|
||||||
y = rows.empty() ? 0 : rows.size() - 1;
|
y = rows.empty() ? 0 : rows.size() - 1;
|
||||||
@@ -4209,7 +4394,7 @@ cmd_delete_word_prev(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
// Now delete from (x, y) to (start_x, start_y) using PieceTable
|
// Now delete from (x, y) to (start_x, start_y) using PieceTable
|
||||||
std::string deleted = extract_region_text(*buf, x, y, start_x, start_y);
|
std::string deleted = extract_region_text(*buf, x, y, start_x, start_y);
|
||||||
delete_region(*buf, x, y, start_x, start_y);
|
delete_region(*buf, x, y, start_x, start_y, buf->Undo());
|
||||||
// Prepend to killed_total (since we're deleting backwards)
|
// Prepend to killed_total (since we're deleting backwards)
|
||||||
killed_total = deleted + killed_total;
|
killed_total = deleted + killed_total;
|
||||||
}
|
}
|
||||||
@@ -4241,6 +4426,7 @@ cmd_delete_word_next(CommandContext &ctx)
|
|||||||
std::size_t x = buf->Curx();
|
std::size_t x = buf->Curx();
|
||||||
int repeat = ctx.count > 0 ? ctx.count : 1;
|
int repeat = ctx.count > 0 ? ctx.count : 1;
|
||||||
std::string killed_total;
|
std::string killed_total;
|
||||||
|
UndoGroupGuard guard(buf->Undo());
|
||||||
for (int i = 0; i < repeat; ++i) {
|
for (int i = 0; i < repeat; ++i) {
|
||||||
if (y >= rows.size())
|
if (y >= rows.size())
|
||||||
break;
|
break;
|
||||||
@@ -4280,7 +4466,7 @@ cmd_delete_word_next(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
// Now delete from (start_x, start_y) to (x, y) using PieceTable
|
// Now delete from (start_x, start_y) to (x, y) using PieceTable
|
||||||
std::string deleted = extract_region_text(*buf, start_x, start_y, x, y);
|
std::string deleted = extract_region_text(*buf, start_x, start_y, x, y);
|
||||||
delete_region(*buf, start_x, start_y, x, y);
|
delete_region(*buf, start_x, start_y, x, y, buf->Undo());
|
||||||
y = start_y;
|
y = start_y;
|
||||||
x = start_x;
|
x = start_x;
|
||||||
killed_total += deleted;
|
killed_total += deleted;
|
||||||
@@ -4314,8 +4500,16 @@ cmd_indent_region(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus("No region to indent");
|
ctx.editor.SetStatus("No region to indent");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
UndoSystem *u = buf->Undo();
|
||||||
|
UndoGroupGuard guard(u);
|
||||||
for (std::size_t y = sy; y <= ey && y < buf->Rows().size(); ++y) {
|
for (std::size_t y = sy; y <= ey && y < buf->Rows().size(); ++y) {
|
||||||
buf->insert_text(static_cast<int>(y), 0, std::string_view("\t"));
|
buf->insert_text(static_cast<int>(y), 0, std::string_view("\t"));
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
u->Begin(UndoType::Insert);
|
||||||
|
u->Append('\t');
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buf->SetDirty(true);
|
buf->SetDirty(true);
|
||||||
buf->ClearMark();
|
buf->ClearMark();
|
||||||
@@ -4339,6 +4533,8 @@ cmd_unindent_region(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus("No region to unindent");
|
ctx.editor.SetStatus("No region to unindent");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
UndoSystem *u = buf->Undo();
|
||||||
|
UndoGroupGuard guard(u);
|
||||||
for (std::size_t y = sy; y <= ey && y < buf->Rows().size(); ++y) {
|
for (std::size_t y = sy; y <= ey && y < buf->Rows().size(); ++y) {
|
||||||
const auto &rows_view = buf->Rows();
|
const auto &rows_view = buf->Rows();
|
||||||
if (y >= rows_view.size())
|
if (y >= rows_view.size())
|
||||||
@@ -4347,13 +4543,26 @@ cmd_unindent_region(CommandContext &ctx)
|
|||||||
if (!line.empty()) {
|
if (!line.empty()) {
|
||||||
if (line[0] == '\t') {
|
if (line[0] == '\t') {
|
||||||
buf->delete_text(static_cast<int>(y), 0, 1);
|
buf->delete_text(static_cast<int>(y), 0, 1);
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
|
u->Append('\t');
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
} else if (line[0] == ' ') {
|
} else if (line[0] == ' ') {
|
||||||
std::size_t spaces = 0;
|
std::size_t spaces = 0;
|
||||||
while (spaces < line.size() && spaces < 8 && line[spaces] == ' ') {
|
while (spaces < line.size() && spaces < 8 && line[spaces] == ' ') {
|
||||||
++spaces;
|
++spaces;
|
||||||
}
|
}
|
||||||
if (spaces > 0)
|
if (spaces > 0) {
|
||||||
buf->delete_text(static_cast<int>(y), 0, spaces);
|
buf->delete_text(static_cast<int>(y), 0, spaces);
|
||||||
|
if (u) {
|
||||||
|
buf->SetCursor(0, y);
|
||||||
|
u->Begin(UndoType::Delete);
|
||||||
|
u->Append(line.substr(0, spaces));
|
||||||
|
u->commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4370,25 +4579,8 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
if (!buf)
|
if (!buf)
|
||||||
return false;
|
return false;
|
||||||
struct GroupGuard {
|
|
||||||
UndoSystem *u;
|
|
||||||
|
|
||||||
|
|
||||||
explicit GroupGuard(UndoSystem *u_) : u(u_)
|
|
||||||
{
|
|
||||||
if (u)
|
|
||||||
u->BeginGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
~GroupGuard()
|
|
||||||
{
|
|
||||||
if (u)
|
|
||||||
u->EndGroup();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Reflow performs a multi-edit transformation; make it a single standalone undo/redo step.
|
// Reflow performs a multi-edit transformation; make it a single standalone undo/redo step.
|
||||||
GroupGuard guard(buf->Undo());
|
UndoGroupGuard guard(buf->Undo());
|
||||||
if (auto *u = buf->Undo())
|
if (auto *u = buf->Undo())
|
||||||
u->commit();
|
u->commit();
|
||||||
ensure_at_least_one_line(*buf);
|
ensure_at_least_one_line(*buf);
|
||||||
@@ -5013,6 +5205,11 @@ InstallDefaultCommands()
|
|||||||
CommandId::NewWindow, "new-window", "Open a new editor window (GUI only)", cmd_new_window,
|
CommandId::NewWindow, "new-window", "Open a new editor window (GUI only)", cmd_new_window,
|
||||||
false, false
|
false, false
|
||||||
});
|
});
|
||||||
|
// Edit mode toggle (public)
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::ToggleEditMode, "mode", "Toggle or set edit mode: code|writing",
|
||||||
|
cmd_toggle_edit_mode, true, false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,12 @@ enum class CommandId {
|
|||||||
CenterOnCursor, // center the viewport on the current cursor line (C-k k)
|
CenterOnCursor, // center the viewport on the current cursor line (C-k k)
|
||||||
// GUI: open a new editor window sharing the same buffer list
|
// GUI: open a new editor window sharing the same buffer list
|
||||||
NewWindow,
|
NewWindow,
|
||||||
|
// GUI: font size controls
|
||||||
|
FontZoomIn,
|
||||||
|
FontZoomOut,
|
||||||
|
FontZoomReset,
|
||||||
|
// Edit mode (code/writing)
|
||||||
|
ToggleEditMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -69,20 +69,22 @@ Editor::SetStatus(const std::string &message)
|
|||||||
Buffer *
|
Buffer *
|
||||||
Editor::CurrentBuffer()
|
Editor::CurrentBuffer()
|
||||||
{
|
{
|
||||||
if (buffers_.empty() || curbuf_ >= buffers_.size()) {
|
auto &bufs = Buffers();
|
||||||
|
if (bufs.empty() || curbuf_ >= bufs.size()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return &buffers_[curbuf_];
|
return &bufs[curbuf_];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const Buffer *
|
const Buffer *
|
||||||
Editor::CurrentBuffer() const
|
Editor::CurrentBuffer() const
|
||||||
{
|
{
|
||||||
if (buffers_.empty() || curbuf_ >= buffers_.size()) {
|
const auto &bufs = Buffers();
|
||||||
|
if (bufs.empty() || curbuf_ >= bufs.size()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return &buffers_[curbuf_];
|
return &bufs[curbuf_];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -117,8 +119,9 @@ Editor::DisplayNameFor(const Buffer &buf) const
|
|||||||
|
|
||||||
// Prepare list of other buffer paths
|
// Prepare list of other buffer paths
|
||||||
std::vector<std::vector<std::filesystem::path> > others;
|
std::vector<std::vector<std::filesystem::path> > others;
|
||||||
others.reserve(buffers_.size());
|
const auto &bufs = Buffers();
|
||||||
for (const auto &b: buffers_) {
|
others.reserve(bufs.size());
|
||||||
|
for (const auto &b: bufs) {
|
||||||
if (&b == &buf)
|
if (&b == &buf)
|
||||||
continue;
|
continue;
|
||||||
if (b.Filename().empty())
|
if (b.Filename().empty())
|
||||||
@@ -161,41 +164,69 @@ Editor::DisplayNameFor(const Buffer &buf) const
|
|||||||
std::size_t
|
std::size_t
|
||||||
Editor::AddBuffer(const Buffer &buf)
|
Editor::AddBuffer(const Buffer &buf)
|
||||||
{
|
{
|
||||||
buffers_.push_back(buf);
|
auto &bufs = Buffers();
|
||||||
// Attach swap recorder
|
// push_back may reallocate the vector's storage, moving every existing
|
||||||
|
// Buffer to a new address. Drain any in-flight swap records first so the
|
||||||
|
// writer thread never dereferences an address that's about to move, then
|
||||||
|
// rehome each attached buffer that actually moved.
|
||||||
|
if (swap_ && !bufs.empty())
|
||||||
|
swap_->Flush();
|
||||||
|
std::vector<Buffer *> old_addrs;
|
||||||
|
old_addrs.reserve(bufs.size());
|
||||||
|
for (auto &b: bufs)
|
||||||
|
old_addrs.push_back(&b);
|
||||||
|
bufs.push_back(buf);
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
swap_->Attach(&buffers_.back());
|
for (std::size_t i = 0; i < old_addrs.size(); ++i) {
|
||||||
buffers_.back().SetSwapRecorder(swap_->RecorderFor(&buffers_.back()));
|
Buffer *new_addr = &bufs[i];
|
||||||
|
if (new_addr != old_addrs[i])
|
||||||
|
bufs[i].SetSwapRecorder(swap_->Rehome(old_addrs[i], new_addr));
|
||||||
}
|
}
|
||||||
if (buffers_.size() == 1) {
|
swap_->Attach(&bufs.back());
|
||||||
|
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
||||||
|
}
|
||||||
|
if (bufs.size() == 1) {
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
return buffers_.size() - 1;
|
return bufs.size() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::size_t
|
std::size_t
|
||||||
Editor::AddBuffer(Buffer &&buf)
|
Editor::AddBuffer(Buffer &&buf)
|
||||||
{
|
{
|
||||||
buffers_.push_back(std::move(buf));
|
auto &bufs = Buffers();
|
||||||
|
if (swap_ && !bufs.empty())
|
||||||
|
swap_->Flush();
|
||||||
|
std::vector<Buffer *> old_addrs;
|
||||||
|
old_addrs.reserve(bufs.size());
|
||||||
|
for (auto &b: bufs)
|
||||||
|
old_addrs.push_back(&b);
|
||||||
|
bufs.push_back(std::move(buf));
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
swap_->Attach(&buffers_.back());
|
for (std::size_t i = 0; i < old_addrs.size(); ++i) {
|
||||||
buffers_.back().SetSwapRecorder(swap_->RecorderFor(&buffers_.back()));
|
Buffer *new_addr = &bufs[i];
|
||||||
|
if (new_addr != old_addrs[i])
|
||||||
|
bufs[i].SetSwapRecorder(swap_->Rehome(old_addrs[i], new_addr));
|
||||||
}
|
}
|
||||||
if (buffers_.size() == 1) {
|
swap_->Attach(&bufs.back());
|
||||||
|
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
||||||
|
}
|
||||||
|
if (bufs.size() == 1) {
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
return buffers_.size() - 1;
|
return bufs.size() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
Editor::OpenFile(const std::string &path, std::string &err)
|
Editor::OpenFile(const std::string &path, std::string &err)
|
||||||
{
|
{
|
||||||
// If there is exactly one unnamed, empty, clean buffer, reuse it instead
|
// If the current buffer is an unnamed, empty, clean scratch buffer, reuse
|
||||||
// of creating a new one.
|
// it instead of creating a new one.
|
||||||
if (buffers_.size() == 1) {
|
auto &bufs_ref = Buffers();
|
||||||
Buffer &cur = buffers_[curbuf_];
|
if (!bufs_ref.empty() && curbuf_ < bufs_ref.size()) {
|
||||||
|
Buffer &cur = bufs_ref[curbuf_];
|
||||||
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
||||||
const bool clean = !cur.Dirty();
|
const bool clean = !cur.Dirty();
|
||||||
const std::size_t nrows = cur.Nrows();
|
const std::size_t nrows = cur.Nrows();
|
||||||
@@ -268,7 +299,7 @@ Editor::OpenFile(const std::string &path, std::string &err)
|
|||||||
// Add as a new buffer and switch to it
|
// Add as a new buffer and switch to it
|
||||||
std::size_t idx = AddBuffer(std::move(b));
|
std::size_t idx = AddBuffer(std::move(b));
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
swap_->NotifyFilenameChanged(buffers_[idx]);
|
swap_->NotifyFilenameChanged(Buffers()[idx]);
|
||||||
}
|
}
|
||||||
SwitchTo(idx);
|
SwitchTo(idx);
|
||||||
// Defensive: ensure any active prompt is closed after a successful open
|
// Defensive: ensure any active prompt is closed after a successful open
|
||||||
@@ -446,12 +477,13 @@ Editor::ProcessPendingOpens()
|
|||||||
bool
|
bool
|
||||||
Editor::SwitchTo(std::size_t index)
|
Editor::SwitchTo(std::size_t index)
|
||||||
{
|
{
|
||||||
if (index >= buffers_.size()) {
|
auto &bufs = Buffers();
|
||||||
|
if (index >= bufs.size()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
curbuf_ = index;
|
curbuf_ = index;
|
||||||
// Robustness: ensure a valid highlighter is installed when switching buffers
|
// Robustness: ensure a valid highlighter is installed when switching buffers
|
||||||
Buffer &b = buffers_[curbuf_];
|
Buffer &b = bufs[curbuf_];
|
||||||
if (b.SyntaxEnabled()) {
|
if (b.SyntaxEnabled()) {
|
||||||
b.EnsureHighlighter();
|
b.EnsureHighlighter();
|
||||||
if (auto *eng = b.Highlighter()) {
|
if (auto *eng = b.Highlighter()) {
|
||||||
@@ -478,21 +510,36 @@ Editor::SwitchTo(std::size_t index)
|
|||||||
bool
|
bool
|
||||||
Editor::CloseBuffer(std::size_t index)
|
Editor::CloseBuffer(std::size_t index)
|
||||||
{
|
{
|
||||||
if (index >= buffers_.size()) {
|
auto &bufs = Buffers();
|
||||||
|
if (index >= bufs.size()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
// Always remove swap file when closing a buffer on normal exit.
|
// Always remove swap file when closing a buffer on normal exit.
|
||||||
// Swap files are for crash recovery; on clean close, we don't need them.
|
// Swap files are for crash recovery; on clean close, we don't need them.
|
||||||
// This prevents stale swap files from accumulating (e.g., when used as git editor).
|
// This prevents stale swap files from accumulating (e.g., when used as git editor).
|
||||||
swap_->Detach(&buffers_[index], true);
|
swap_->Detach(&bufs[index], true);
|
||||||
buffers_[index].SetSwapRecorder(nullptr);
|
bufs[index].SetSwapRecorder(nullptr);
|
||||||
|
// Drain in-flight records before the erase-shift below moves other
|
||||||
|
// buffers to new addresses (vector::erase never reallocates, but it does
|
||||||
|
// move-assign each trailing buffer into the previous slot).
|
||||||
|
swap_->Flush();
|
||||||
}
|
}
|
||||||
buffers_.erase(buffers_.begin() + static_cast<std::ptrdiff_t>(index));
|
bufs.erase(bufs.begin() + static_cast<std::ptrdiff_t>(index));
|
||||||
if (buffers_.empty()) {
|
if (swap_) {
|
||||||
|
// erase() shifts every buffer after `index` down by one slot in-place
|
||||||
|
// (no reallocation), so the buffer now at slot i used to live at slot
|
||||||
|
// i+1 (same underlying storage, since data() doesn't move on erase).
|
||||||
|
for (std::size_t i = index; i < bufs.size(); ++i) {
|
||||||
|
Buffer *new_addr = &bufs[i];
|
||||||
|
Buffer *old_addr = new_addr + 1;
|
||||||
|
bufs[i].SetSwapRecorder(swap_->Rehome(old_addr, new_addr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bufs.empty()) {
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
} else if (curbuf_ >= buffers_.size()) {
|
} else if (curbuf_ >= bufs.size()) {
|
||||||
curbuf_ = buffers_.size() - 1;
|
curbuf_ = bufs.size() - 1;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -516,7 +563,12 @@ Editor::Reset()
|
|||||||
// Reset close-confirm/save state
|
// Reset close-confirm/save state
|
||||||
close_confirm_pending_ = false;
|
close_confirm_pending_ = false;
|
||||||
close_after_save_ = false;
|
close_after_save_ = false;
|
||||||
buffers_.clear();
|
auto &bufs = Buffers();
|
||||||
|
if (swap_) {
|
||||||
|
for (auto &buf : bufs)
|
||||||
|
swap_->Detach(&buf, true);
|
||||||
|
}
|
||||||
|
bufs.clear();
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -521,7 +521,7 @@ public:
|
|||||||
// Buffers
|
// Buffers
|
||||||
[[nodiscard]] std::size_t BufferCount() const
|
[[nodiscard]] std::size_t BufferCount() const
|
||||||
{
|
{
|
||||||
return buffers_.size();
|
return Buffers().size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -531,6 +531,19 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Clamp curbuf_ to valid range. Call when the shared buffer list may
|
||||||
|
// have been modified by another editor (e.g., buffer closed in another window).
|
||||||
|
void ValidateBufferIndex()
|
||||||
|
{
|
||||||
|
const auto &bufs = Buffers();
|
||||||
|
if (bufs.empty()) {
|
||||||
|
curbuf_ = 0;
|
||||||
|
} else if (curbuf_ >= bufs.size()) {
|
||||||
|
curbuf_ = bufs.size() - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Buffer *CurrentBuffer();
|
Buffer *CurrentBuffer();
|
||||||
|
|
||||||
const Buffer *CurrentBuffer() const;
|
const Buffer *CurrentBuffer() const;
|
||||||
|
|||||||
+130
-9
@@ -3,9 +3,29 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
#include "GUIConfig.h"
|
#include "GUIConfig.h"
|
||||||
|
|
||||||
|
// toml++ for TOML config parsing
|
||||||
|
#if defined(__clang__)
|
||||||
|
# pragma clang diagnostic push
|
||||||
|
# pragma clang diagnostic ignored "-Weverything"
|
||||||
|
#elif defined(__GNUC__)
|
||||||
|
# pragma GCC diagnostic push
|
||||||
|
# pragma GCC diagnostic ignored "-Wall"
|
||||||
|
# pragma GCC diagnostic ignored "-Wextra"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "ext/tomlplusplus/toml.hpp"
|
||||||
|
|
||||||
|
#if defined(__clang__)
|
||||||
|
# pragma clang diagnostic pop
|
||||||
|
#elif defined(__GNUC__)
|
||||||
|
# pragma GCC diagnostic pop
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
trim(std::string &s)
|
trim(std::string &s)
|
||||||
@@ -19,37 +39,124 @@ trim(std::string &s)
|
|||||||
|
|
||||||
|
|
||||||
static std::string
|
static std::string
|
||||||
default_config_path()
|
config_dir()
|
||||||
{
|
{
|
||||||
const char *home = std::getenv("HOME");
|
const char *home = std::getenv("HOME");
|
||||||
if (!home || !*home)
|
if (!home || !*home)
|
||||||
return {};
|
return {};
|
||||||
std::string path(home);
|
return std::string(home) + "/.config/kte";
|
||||||
path += "/.config/kte/kge.ini";
|
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
GUIConfig
|
GUIConfig
|
||||||
GUIConfig::Load()
|
GUIConfig::Load()
|
||||||
{
|
{
|
||||||
GUIConfig cfg; // defaults already set
|
GUIConfig cfg;
|
||||||
const std::string path = default_config_path();
|
std::string dir = config_dir();
|
||||||
|
if (dir.empty())
|
||||||
|
return cfg;
|
||||||
|
|
||||||
if (!path.empty()) {
|
// Try TOML first
|
||||||
cfg.LoadFromFile(path);
|
std::string toml_path = dir + "/kge.toml";
|
||||||
|
if (cfg.LoadFromTOML(toml_path))
|
||||||
|
return cfg;
|
||||||
|
|
||||||
|
// Fall back to legacy INI
|
||||||
|
std::string ini_path = dir + "/kge.ini";
|
||||||
|
if (cfg.LoadFromINI(ini_path)) {
|
||||||
|
std::cerr << "kge: loaded legacy kge.ini; consider migrating to kge.toml\n";
|
||||||
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
GUIConfig::LoadFromFile(const std::string &path)
|
GUIConfig::LoadFromTOML(const std::string &path)
|
||||||
|
{
|
||||||
|
if (!std::filesystem::exists(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
toml::table tbl;
|
||||||
|
try {
|
||||||
|
tbl = toml::parse_file(path);
|
||||||
|
} catch (const toml::parse_error &err) {
|
||||||
|
std::cerr << "kge: TOML parse error in " << path << ": " << err.what() << "\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [window]
|
||||||
|
if (auto win = tbl["window"].as_table()) {
|
||||||
|
if (auto v = (*win)["fullscreen"].value<bool>())
|
||||||
|
fullscreen = *v;
|
||||||
|
if (auto v = (*win)["columns"].value<int64_t>()) {
|
||||||
|
if (*v > 0) columns = static_cast<int>(*v);
|
||||||
|
}
|
||||||
|
if (auto v = (*win)["rows"].value<int64_t>()) {
|
||||||
|
if (*v > 0) rows = static_cast<int>(*v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [font]
|
||||||
|
bool explicit_code_font = false;
|
||||||
|
bool explicit_writing_font = false;
|
||||||
|
if (auto sec = tbl["font"].as_table()) {
|
||||||
|
if (auto v = (*sec)["name"].value<std::string>())
|
||||||
|
font = *v;
|
||||||
|
if (auto v = (*sec)["size"].value<double>()) {
|
||||||
|
if (*v > 0.0) font_size = static_cast<float>(*v);
|
||||||
|
}
|
||||||
|
if (auto v = (*sec)["code"].value<std::string>()) {
|
||||||
|
code_font = *v;
|
||||||
|
explicit_code_font = true;
|
||||||
|
}
|
||||||
|
if (auto v = (*sec)["writing"].value<std::string>()) {
|
||||||
|
writing_font = *v;
|
||||||
|
explicit_writing_font = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [appearance]
|
||||||
|
if (auto sec = tbl["appearance"].as_table()) {
|
||||||
|
if (auto v = (*sec)["theme"].value<std::string>())
|
||||||
|
theme = *v;
|
||||||
|
if (auto v = (*sec)["background"].value<std::string>()) {
|
||||||
|
std::string bg = *v;
|
||||||
|
std::transform(bg.begin(), bg.end(), bg.begin(), [](unsigned char c) {
|
||||||
|
return (char) std::tolower(c);
|
||||||
|
});
|
||||||
|
if (bg == "light" || bg == "dark")
|
||||||
|
background = bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [editor]
|
||||||
|
if (auto sec = tbl["editor"].as_table()) {
|
||||||
|
if (auto v = (*sec)["syntax"].value<bool>())
|
||||||
|
syntax = *v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default code_font to the main font if not explicitly set
|
||||||
|
if (!explicit_code_font)
|
||||||
|
code_font = font;
|
||||||
|
if (!explicit_writing_font && writing_font == "crimsonpro" && font != "default")
|
||||||
|
writing_font = font;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
GUIConfig::LoadFromINI(const std::string &path)
|
||||||
{
|
{
|
||||||
std::ifstream in(path);
|
std::ifstream in(path);
|
||||||
if (!in.good())
|
if (!in.good())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
bool explicit_code_font = false;
|
||||||
|
bool explicit_writing_font = false;
|
||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
while (std::getline(in, line)) {
|
while (std::getline(in, line)) {
|
||||||
// Remove comments starting with '#' or ';'
|
// Remove comments starting with '#' or ';'
|
||||||
@@ -104,6 +211,12 @@ GUIConfig::LoadFromFile(const std::string &path)
|
|||||||
}
|
}
|
||||||
} else if (key == "font") {
|
} else if (key == "font") {
|
||||||
font = val;
|
font = val;
|
||||||
|
} else if (key == "code_font") {
|
||||||
|
code_font = val;
|
||||||
|
explicit_code_font = true;
|
||||||
|
} else if (key == "writing_font") {
|
||||||
|
writing_font = val;
|
||||||
|
explicit_writing_font = true;
|
||||||
} else if (key == "theme") {
|
} else if (key == "theme") {
|
||||||
theme = val;
|
theme = val;
|
||||||
} else if (key == "background" || key == "bg") {
|
} else if (key == "background" || key == "bg") {
|
||||||
@@ -126,5 +239,13 @@ GUIConfig::LoadFromFile(const std::string &path)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If code_font was not explicitly set, default it to the main font
|
||||||
|
// so that the edit-mode font switcher doesn't immediately switch away
|
||||||
|
// from the font loaded during Init.
|
||||||
|
if (!explicit_code_font)
|
||||||
|
code_font = font;
|
||||||
|
if (!explicit_writing_font && writing_font == "crimsonpro" && font != "default")
|
||||||
|
writing_font = font;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -1,5 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* GUIConfig - loads simple GUI configuration from $HOME/.config/kte/kge.ini
|
* GUIConfig - loads GUI configuration from $HOME/.config/kte/kge.toml
|
||||||
|
*
|
||||||
|
* Falls back to legacy kge.ini if no TOML config is found.
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -22,12 +24,18 @@ public:
|
|||||||
std::string background = "dark";
|
std::string background = "dark";
|
||||||
|
|
||||||
// Default syntax highlighting state for GUI (kge): on/off
|
// Default syntax highlighting state for GUI (kge): on/off
|
||||||
// Accepts: on/off/true/false/yes/no/1/0 in the ini file.
|
bool syntax = true;
|
||||||
bool syntax = true; // default: enabled
|
|
||||||
|
|
||||||
// Load from default path: $HOME/.config/kte/kge.ini
|
// Per-mode font defaults
|
||||||
|
std::string code_font = "default";
|
||||||
|
std::string writing_font = "crimsonpro";
|
||||||
|
|
||||||
|
// Load from default paths: try kge.toml first, fall back to kge.ini
|
||||||
static GUIConfig Load();
|
static GUIConfig Load();
|
||||||
|
|
||||||
// Load from explicit path. Returns true if file existed and was parsed.
|
// Load from explicit TOML path. Returns true if file existed and was parsed.
|
||||||
bool LoadFromFile(const std::string &path);
|
bool LoadFromTOML(const std::string &path);
|
||||||
|
|
||||||
|
// Load from explicit INI path (legacy). Returns true if file existed and was parsed.
|
||||||
|
bool LoadFromINI(const std::string &path);
|
||||||
};
|
};
|
||||||
|
|||||||
+187
-19
@@ -312,7 +312,7 @@ namespace kte {
|
|||||||
enum class BackgroundMode { Light, Dark };
|
enum class BackgroundMode { Light, Dark };
|
||||||
|
|
||||||
// Global background mode; default to Dark to match prior defaults
|
// Global background mode; default to Dark to match prior defaults
|
||||||
static inline auto gBackgroundMode = BackgroundMode::Dark;
|
inline auto gBackgroundMode = BackgroundMode::Dark;
|
||||||
|
|
||||||
// Basic theme identifier (kept minimal; some ids are aliases)
|
// Basic theme identifier (kept minimal; some ids are aliases)
|
||||||
enum class ThemeId {
|
enum class ThemeId {
|
||||||
@@ -330,11 +330,13 @@ enum class ThemeId {
|
|||||||
Amber = 10,
|
Amber = 10,
|
||||||
WeylandYutani = 11,
|
WeylandYutani = 11,
|
||||||
Orbital = 12,
|
Orbital = 12,
|
||||||
|
Tufte = 13,
|
||||||
|
Leuchtturm = 14,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Current theme tracking
|
// Current theme tracking
|
||||||
static inline auto gCurrentTheme = ThemeId::Nord;
|
inline auto gCurrentTheme = ThemeId::Nord;
|
||||||
static inline std::size_t gCurrentThemeIndex = 6; // Nord index
|
inline std::size_t gCurrentThemeIndex = 7; // Nord index
|
||||||
|
|
||||||
// Forward declarations for helpers used below
|
// Forward declarations for helpers used below
|
||||||
static size_t ThemeIndexFromId(ThemeId id);
|
static size_t ThemeIndexFromId(ThemeId id);
|
||||||
@@ -372,11 +374,13 @@ BackgroundModeName()
|
|||||||
#include "themes/Everforest.h"
|
#include "themes/Everforest.h"
|
||||||
#include "themes/KanagawaPaper.h"
|
#include "themes/KanagawaPaper.h"
|
||||||
#include "themes/LCARS.h"
|
#include "themes/LCARS.h"
|
||||||
|
#include "themes/Leuchtturm.h"
|
||||||
#include "themes/OldBook.h"
|
#include "themes/OldBook.h"
|
||||||
#include "themes/Amber.h"
|
#include "themes/Amber.h"
|
||||||
#include "themes/WeylandYutani.h"
|
#include "themes/WeylandYutani.h"
|
||||||
#include "themes/Zenburn.h"
|
#include "themes/Zenburn.h"
|
||||||
#include "themes/Orbital.h"
|
#include "themes/Orbital.h"
|
||||||
|
#include "themes/Tufte.h"
|
||||||
|
|
||||||
|
|
||||||
// Theme abstraction and registry (generalized theme system)
|
// Theme abstraction and registry (generalized theme system)
|
||||||
@@ -409,6 +413,28 @@ struct LCARSTheme final : Theme {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct LeuchtturmTheme final : Theme {
|
||||||
|
[[nodiscard]] const char *Name() const override
|
||||||
|
{
|
||||||
|
return "leuchtturm";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Apply() const override
|
||||||
|
{
|
||||||
|
if (gBackgroundMode == BackgroundMode::Dark)
|
||||||
|
ApplyLeuchtturmDarkTheme();
|
||||||
|
else
|
||||||
|
ApplyLeuchtturmLightTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThemeId Id() override
|
||||||
|
{
|
||||||
|
return ThemeId::Leuchtturm;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
struct EverforestTheme final : Theme {
|
struct EverforestTheme final : Theme {
|
||||||
[[nodiscard]] const char *Name() const override
|
[[nodiscard]] const char *Name() const override
|
||||||
{
|
{
|
||||||
@@ -488,6 +514,28 @@ struct OrbitalTheme final : Theme {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct TufteTheme final : Theme {
|
||||||
|
[[nodiscard]] const char *Name() const override
|
||||||
|
{
|
||||||
|
return "tufte";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Apply() const override
|
||||||
|
{
|
||||||
|
if (gBackgroundMode == BackgroundMode::Dark)
|
||||||
|
ApplyTufteDarkTheme();
|
||||||
|
else
|
||||||
|
ApplyTufteLightTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThemeId Id() override
|
||||||
|
{
|
||||||
|
return ThemeId::Tufte;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
struct ZenburnTheme final : Theme {
|
struct ZenburnTheme final : Theme {
|
||||||
[[nodiscard]] const char *Name() const override
|
[[nodiscard]] const char *Name() const override
|
||||||
{
|
{
|
||||||
@@ -657,18 +705,20 @@ ThemeRegistry()
|
|||||||
static std::vector<std::unique_ptr<Theme> > reg;
|
static std::vector<std::unique_ptr<Theme> > reg;
|
||||||
if (reg.empty()) {
|
if (reg.empty()) {
|
||||||
// Alphabetical by canonical name:
|
// Alphabetical by canonical name:
|
||||||
// amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, orbital, plan9, solarized, weyland-yutani, zenburn
|
// amber, eink, everforest, gruvbox, kanagawa-paper, lcars, leuchtturm, nord, old-book, orbital, plan9, solarized, tufte, weyland-yutani, zenburn
|
||||||
reg.emplace_back(std::make_unique<detail::AmberTheme>());
|
reg.emplace_back(std::make_unique<detail::AmberTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::EInkTheme>());
|
reg.emplace_back(std::make_unique<detail::EInkTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::EverforestTheme>());
|
reg.emplace_back(std::make_unique<detail::EverforestTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::GruvboxTheme>());
|
reg.emplace_back(std::make_unique<detail::GruvboxTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::KanagawaPaperTheme>());
|
reg.emplace_back(std::make_unique<detail::KanagawaPaperTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::LCARSTheme>());
|
reg.emplace_back(std::make_unique<detail::LCARSTheme>());
|
||||||
|
reg.emplace_back(std::make_unique<detail::LeuchtturmTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::NordTheme>());
|
reg.emplace_back(std::make_unique<detail::NordTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::OldBookTheme>());
|
reg.emplace_back(std::make_unique<detail::OldBookTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::OrbitalTheme>());
|
reg.emplace_back(std::make_unique<detail::OrbitalTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::Plan9Theme>());
|
reg.emplace_back(std::make_unique<detail::Plan9Theme>());
|
||||||
reg.emplace_back(std::make_unique<detail::SolarizedTheme>());
|
reg.emplace_back(std::make_unique<detail::SolarizedTheme>());
|
||||||
|
reg.emplace_back(std::make_unique<detail::TufteTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::WeylandYutaniTheme>());
|
reg.emplace_back(std::make_unique<detail::WeylandYutaniTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::ZenburnTheme>());
|
reg.emplace_back(std::make_unique<detail::ZenburnTheme>());
|
||||||
}
|
}
|
||||||
@@ -845,20 +895,24 @@ ThemeIndexFromId(const ThemeId id)
|
|||||||
return 4;
|
return 4;
|
||||||
case ThemeId::LCARS:
|
case ThemeId::LCARS:
|
||||||
return 5;
|
return 5;
|
||||||
case ThemeId::Nord:
|
case ThemeId::Leuchtturm:
|
||||||
return 6;
|
return 6;
|
||||||
case ThemeId::OldBook:
|
case ThemeId::Nord:
|
||||||
return 7;
|
return 7;
|
||||||
case ThemeId::Orbital:
|
case ThemeId::OldBook:
|
||||||
return 8;
|
return 8;
|
||||||
case ThemeId::Plan9:
|
case ThemeId::Orbital:
|
||||||
return 9;
|
return 9;
|
||||||
case ThemeId::Solarized:
|
case ThemeId::Plan9:
|
||||||
return 10;
|
return 10;
|
||||||
case ThemeId::WeylandYutani:
|
case ThemeId::Solarized:
|
||||||
return 11;
|
return 11;
|
||||||
case ThemeId::Zenburn:
|
case ThemeId::Tufte:
|
||||||
return 12;
|
return 12;
|
||||||
|
case ThemeId::WeylandYutani:
|
||||||
|
return 13;
|
||||||
|
case ThemeId::Zenburn:
|
||||||
|
return 14;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -882,30 +936,144 @@ ThemeIdFromIndex(const size_t idx)
|
|||||||
case 5:
|
case 5:
|
||||||
return ThemeId::LCARS;
|
return ThemeId::LCARS;
|
||||||
case 6:
|
case 6:
|
||||||
return ThemeId::Nord;
|
return ThemeId::Leuchtturm;
|
||||||
case 7:
|
case 7:
|
||||||
return ThemeId::OldBook;
|
return ThemeId::Nord;
|
||||||
case 8:
|
case 8:
|
||||||
return ThemeId::Orbital;
|
return ThemeId::OldBook;
|
||||||
case 9:
|
case 9:
|
||||||
return ThemeId::Plan9;
|
return ThemeId::Orbital;
|
||||||
case 10:
|
case 10:
|
||||||
return ThemeId::Solarized;
|
return ThemeId::Plan9;
|
||||||
case 11:
|
case 11:
|
||||||
return ThemeId::WeylandYutani;
|
return ThemeId::Solarized;
|
||||||
case 12:
|
case 12:
|
||||||
|
return ThemeId::Tufte;
|
||||||
|
case 13:
|
||||||
|
return ThemeId::WeylandYutani;
|
||||||
|
case 14:
|
||||||
return ThemeId::Zenburn;
|
return ThemeId::Zenburn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Syntax palette (v1): map TokenKind to ink color per current theme/background ---
|
// --- Syntax palette (v1): map TokenKind to ink color per current theme/background ---
|
||||||
|
|
||||||
|
// Tufte palette: high-contrast, restrained color. Body text is true black on
|
||||||
|
// cream; only keywords and links get subtle color to avoid a "christmas tree."
|
||||||
|
static ImVec4
|
||||||
|
SyntaxInkTufte(const TokenKind k, const bool dark)
|
||||||
|
{
|
||||||
|
const ImVec4 ink = dark ? RGBA(0xEAE6DE) : RGBA(0x111111); // body text
|
||||||
|
const ImVec4 dim = dark ? RGBA(0x8A8680) : RGBA(0x555555); // comments
|
||||||
|
const ImVec4 red = dark ? RGBA(0xD06060) : RGBA(0x8B0000); // keywords/preproc
|
||||||
|
const ImVec4 navy = dark ? RGBA(0x7098C0) : RGBA(0x1A3A5C); // functions/links
|
||||||
|
const ImVec4 grn = dark ? RGBA(0x8AAA6E) : RGBA(0x2E5E2E); // strings
|
||||||
|
switch (k) {
|
||||||
|
case TokenKind::Keyword:
|
||||||
|
case TokenKind::Preproc:
|
||||||
|
return red;
|
||||||
|
case TokenKind::String:
|
||||||
|
case TokenKind::Char:
|
||||||
|
return grn;
|
||||||
|
case TokenKind::Comment:
|
||||||
|
return dim;
|
||||||
|
case TokenKind::Function:
|
||||||
|
return navy;
|
||||||
|
case TokenKind::Number:
|
||||||
|
case TokenKind::Constant:
|
||||||
|
return dark ? RGBA(0xC8A85A) : RGBA(0x6B4C00);
|
||||||
|
case TokenKind::Type:
|
||||||
|
return dark ? RGBA(0xBBAA90) : RGBA(0x333333);
|
||||||
|
case TokenKind::Error:
|
||||||
|
return dark ? RGBA(0xD06060) : RGBA(0xCC0000);
|
||||||
|
default:
|
||||||
|
return ink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Leuchtturm palette: blue-black fountain pen ink with brass and bronze accents.
|
||||||
|
// Body text is ink-colored; accents drawn from the pen metals.
|
||||||
|
static ImVec4
|
||||||
|
SyntaxInkLeuchtturm(const TokenKind k, const bool dark)
|
||||||
|
{
|
||||||
|
const ImVec4 ink = dark ? RGBA(0xE5DDD0) : RGBA(0x040720); // fountain pen ink
|
||||||
|
const ImVec4 dim = dark ? RGBA(0x7A7060) : RGBA(0x6A6558); // comments
|
||||||
|
const ImVec4 brass = dark ? RGBA(0xB8A060) : RGBA(0x504518); // patinated brass
|
||||||
|
const ImVec4 bronze= dark ? RGBA(0xC08050) : RGBA(0x5C3010); // dark bronze
|
||||||
|
const ImVec4 navy = dark ? RGBA(0x8898B0) : RGBA(0x1C2E4A); // deep navy
|
||||||
|
switch (k) {
|
||||||
|
case TokenKind::Keyword:
|
||||||
|
case TokenKind::Preproc:
|
||||||
|
return brass;
|
||||||
|
case TokenKind::String:
|
||||||
|
case TokenKind::Char:
|
||||||
|
return bronze;
|
||||||
|
case TokenKind::Comment:
|
||||||
|
return dim;
|
||||||
|
case TokenKind::Function:
|
||||||
|
return navy;
|
||||||
|
case TokenKind::Number:
|
||||||
|
case TokenKind::Constant:
|
||||||
|
return dark ? RGBA(0xA89060) : RGBA(0x483C10);
|
||||||
|
case TokenKind::Type:
|
||||||
|
return dark ? RGBA(0xC0B898) : RGBA(0x222238);
|
||||||
|
case TokenKind::Error:
|
||||||
|
return dark ? RGBA(0xD06060) : RGBA(0xA02020);
|
||||||
|
default:
|
||||||
|
return ink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Everforest: warm forest palette on dark green-gray (bg 0x2B3339).
|
||||||
|
// Default comment color (0x616E88) is too dim; boost it and tune others.
|
||||||
|
static ImVec4
|
||||||
|
SyntaxInkEverforest(const TokenKind k)
|
||||||
|
{
|
||||||
|
switch (k) {
|
||||||
|
case TokenKind::Keyword:
|
||||||
|
return RGBA(0xE67E80); // everforest red
|
||||||
|
case TokenKind::Type:
|
||||||
|
return RGBA(0xD699B6); // everforest purple
|
||||||
|
case TokenKind::String:
|
||||||
|
case TokenKind::Char:
|
||||||
|
return RGBA(0xA7C080); // everforest green
|
||||||
|
case TokenKind::Comment:
|
||||||
|
return RGBA(0x859289); // boosted from 0x616E88 for contrast
|
||||||
|
case TokenKind::Number:
|
||||||
|
case TokenKind::Constant:
|
||||||
|
return RGBA(0xD8A657); // everforest yellow/orange
|
||||||
|
case TokenKind::Preproc:
|
||||||
|
return RGBA(0xE69875); // everforest orange
|
||||||
|
case TokenKind::Function:
|
||||||
|
return RGBA(0x83C092); // everforest aqua
|
||||||
|
case TokenKind::Operator:
|
||||||
|
case TokenKind::Punctuation:
|
||||||
|
return RGBA(0xD3C6AA); // everforest fg
|
||||||
|
case TokenKind::Error:
|
||||||
|
return RGBA(0xE67E80);
|
||||||
|
default:
|
||||||
|
return RGBA(0xD3C6AA); // everforest fg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[[maybe_unused]] static ImVec4
|
[[maybe_unused]] static ImVec4
|
||||||
SyntaxInk(const TokenKind k)
|
SyntaxInk(const TokenKind k)
|
||||||
{
|
{
|
||||||
// Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults
|
|
||||||
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
|
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
|
||||||
// Base text
|
|
||||||
|
// Per-theme syntax palettes
|
||||||
|
if (gCurrentTheme == ThemeId::Tufte)
|
||||||
|
return SyntaxInkTufte(k, dark);
|
||||||
|
if (gCurrentTheme == ThemeId::Leuchtturm)
|
||||||
|
return SyntaxInkLeuchtturm(k, dark);
|
||||||
|
if (gCurrentTheme == ThemeId::Everforest)
|
||||||
|
return SyntaxInkEverforest(k);
|
||||||
|
|
||||||
|
// Default palettes tuned for Nord-ish themes
|
||||||
const ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
|
const ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case TokenKind::Keyword:
|
case TokenKind::Keyword:
|
||||||
|
|||||||
+18
-5
@@ -41,6 +41,7 @@ HelpText::Text()
|
|||||||
" C-k j Jump to mark\n"
|
" C-k j Jump to mark\n"
|
||||||
" C-k k Center viewport on cursor\n"
|
" C-k k Center viewport on cursor\n"
|
||||||
" C-k l Reload buffer from disk\n"
|
" C-k l Reload buffer from disk\n"
|
||||||
|
" C-k m Toggle edit mode (code/writing)\n"
|
||||||
" C-k n Previous buffer\n"
|
" C-k n Previous buffer\n"
|
||||||
" C-k o Change working directory (prompt)\n"
|
" C-k o Change working directory (prompt)\n"
|
||||||
" C-k p Next buffer\n"
|
" C-k p Next buffer\n"
|
||||||
@@ -82,12 +83,24 @@ HelpText::Text()
|
|||||||
"\n"
|
"\n"
|
||||||
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
|
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"GUI appearance (command prompt):\n"
|
"Edit modes:\n"
|
||||||
" : theme NAME Set GUI theme (amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, plan9, solarized, weyland-yutani, zenburn)\n"
|
" code Monospace font (default for source files)\n"
|
||||||
" : background MODE Set background: light | dark (affects eink, gruvbox, old-book, solarized)\n"
|
" writing Proportional font (auto for .txt, .md, .rst, .org, .tex)\n"
|
||||||
|
" C-k m or : mode [code|writing] to toggle\n"
|
||||||
"\n"
|
"\n"
|
||||||
"GUI config file options:\n"
|
"GUI commands (command prompt):\n"
|
||||||
" font_size=NUM Set font size in pixels (default: 16; e.g., font_size=18)\n"
|
" : theme NAME Set theme (amber, eink, everforest, gruvbox,\n"
|
||||||
|
" kanagawa-paper, lcars, leuchtturm, nord, old-book,\n"
|
||||||
|
" orbital, plan9, solarized, tufte, weyland-yutani,\n"
|
||||||
|
" zenburn)\n"
|
||||||
|
" : background MODE Background: light | dark\n"
|
||||||
|
" : font NAME Set font (tab completes)\n"
|
||||||
|
" : font-size NUM Set font size in pixels\n"
|
||||||
|
" : mode [code|writing] Toggle or set edit mode\n"
|
||||||
|
"\n"
|
||||||
|
"Configuration:\n"
|
||||||
|
" Config file: ~/.config/kte/kge.toml (see CONFIG.md)\n"
|
||||||
|
" Legacy kge.ini is also supported.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"GUI window management:\n"
|
"GUI window management:\n"
|
||||||
" Cmd+N (macOS) Open a new editor window sharing the same buffers\n"
|
" Cmd+N (macOS) Open a new editor window sharing the same buffers\n"
|
||||||
|
|||||||
+216
-76
@@ -30,7 +30,7 @@
|
|||||||
static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
|
static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers shared between Init and OpenNewWindow_
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -38,7 +38,20 @@ apply_syntax_to_buffer(Buffer *b, const GUIConfig &cfg)
|
|||||||
{
|
{
|
||||||
if (!b)
|
if (!b)
|
||||||
return;
|
return;
|
||||||
if (cfg.syntax) {
|
|
||||||
|
// Auto-detect edit mode from file extension once per buffer so that
|
||||||
|
// manual toggles (C-k m / : mode) are not overridden every frame.
|
||||||
|
if (!b->EditModeDetected() && !b->Filename().empty())
|
||||||
|
b->SetEditMode(DetectEditMode(b->Filename()));
|
||||||
|
|
||||||
|
// If the user explicitly set syntax state via a command (:syntax on/off,
|
||||||
|
// :set filetype=...), leave it alone - otherwise this runs every frame
|
||||||
|
// and silently undoes a manual ":syntax off" on the very next frame.
|
||||||
|
if (b->SyntaxUserOverride())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Writing mode disables syntax; otherwise follow the global config.
|
||||||
|
if (cfg.syntax && b->GetEditMode() != EditMode::Writing) {
|
||||||
b->SetSyntaxEnabled(true);
|
b->SetSyntaxEnabled(true);
|
||||||
b->EnsureHighlighter();
|
b->EnsureHighlighter();
|
||||||
if (auto *eng = b->Highlighter()) {
|
if (auto *eng = b->Highlighter()) {
|
||||||
@@ -71,7 +84,9 @@ static void
|
|||||||
update_editor_dimensions(Editor &ed, float disp_w, float disp_h)
|
update_editor_dimensions(Editor &ed, float disp_w, float disp_h)
|
||||||
{
|
{
|
||||||
float row_h = ImGui::GetTextLineHeightWithSpacing();
|
float row_h = ImGui::GetTextLineHeightWithSpacing();
|
||||||
float ch_w = ImGui::CalcTextSize("M").x;
|
// Use average character width rather than "M" (the widest character)
|
||||||
|
// so that column count is reasonable for proportional fonts too.
|
||||||
|
float ch_w = ImGui::CalcTextSize("abcdefghijklmnopqrstuvwxyz").x / 26.0f;
|
||||||
if (row_h <= 0.0f)
|
if (row_h <= 0.0f)
|
||||||
row_h = 16.0f;
|
row_h = 16.0f;
|
||||||
if (ch_w <= 0.0f)
|
if (ch_w <= 0.0f)
|
||||||
@@ -96,6 +111,63 @@ update_editor_dimensions(Editor &ed, float disp_w, float disp_h)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SetupImGuiStyle_ — apply theme, fonts, and flags to the current ImGui context
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void
|
||||||
|
GUIFrontend::SetupImGuiStyle_()
|
||||||
|
{
|
||||||
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
|
|
||||||
|
// Disable imgui.ini for secondary windows (primary sets its own path in Init)
|
||||||
|
io.IniFilename = nullptr;
|
||||||
|
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
||||||
|
ImGui::StyleColorsDark();
|
||||||
|
|
||||||
|
if (config_.background == "light")
|
||||||
|
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||||
|
else
|
||||||
|
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||||
|
kte::ApplyThemeByName(config_.theme);
|
||||||
|
|
||||||
|
// Load fonts into this context's font atlas.
|
||||||
|
// Font registry is global and already populated by Init; just load into this atlas.
|
||||||
|
if (!kte::Fonts::FontRegistry::Instance().LoadFont(config_.font, (float) config_.font_size)) {
|
||||||
|
LoadGuiFont_(nullptr, (float) config_.font_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Destroy a single window's ImGui context + SDL/GL resources
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void
|
||||||
|
GUIFrontend::DestroyWindowResources_(WindowState &ws)
|
||||||
|
{
|
||||||
|
if (ws.imgui_ctx) {
|
||||||
|
// Must activate this window's GL context before shutting down the
|
||||||
|
// OpenGL3 backend, otherwise it deletes another context's resources.
|
||||||
|
if (ws.window && ws.gl_ctx)
|
||||||
|
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
|
||||||
|
ImGui::SetCurrentContext(ws.imgui_ctx);
|
||||||
|
ImGui_ImplOpenGL3_Shutdown();
|
||||||
|
ImGui_ImplSDL2_Shutdown();
|
||||||
|
ImGui::DestroyContext(ws.imgui_ctx);
|
||||||
|
ws.imgui_ctx = nullptr;
|
||||||
|
}
|
||||||
|
if (ws.gl_ctx) {
|
||||||
|
SDL_GL_DeleteContext(ws.gl_ctx);
|
||||||
|
ws.gl_ctx = nullptr;
|
||||||
|
}
|
||||||
|
if (ws.window) {
|
||||||
|
SDL_DestroyWindow(ws.window);
|
||||||
|
ws.window = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
||||||
{
|
{
|
||||||
@@ -172,8 +244,9 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
|||||||
SDL_GL_MakeCurrent(win, gl_ctx);
|
SDL_GL_MakeCurrent(win, gl_ctx);
|
||||||
SDL_GL_SetSwapInterval(1); // vsync
|
SDL_GL_SetSwapInterval(1); // vsync
|
||||||
|
|
||||||
|
// Create primary ImGui context
|
||||||
IMGUI_CHECKVERSION();
|
IMGUI_CHECKVERSION();
|
||||||
ImGui::CreateContext();
|
ImGuiContext *imgui_ctx = ImGui::CreateContext();
|
||||||
ImGuiIO &io = ImGui::GetIO();
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
|
|
||||||
// Set custom ini filename path to ~/.config/kte/imgui.ini
|
// Set custom ini filename path to ~/.config/kte/imgui.ini
|
||||||
@@ -239,6 +312,7 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
|||||||
auto ws = std::make_unique<WindowState>();
|
auto ws = std::make_unique<WindowState>();
|
||||||
ws->window = win;
|
ws->window = win;
|
||||||
ws->gl_ctx = gl_ctx;
|
ws->gl_ctx = gl_ctx;
|
||||||
|
ws->imgui_ctx = imgui_ctx;
|
||||||
ws->width = init_w;
|
ws->width = init_w;
|
||||||
ws->height = init_h;
|
ws->height = init_h;
|
||||||
// The primary window's editor IS the editor passed in from main; we don't
|
// The primary window's editor IS the editor passed in from main; we don't
|
||||||
@@ -255,8 +329,6 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
|||||||
bool
|
bool
|
||||||
GUIFrontend::OpenNewWindow_(Editor &primary)
|
GUIFrontend::OpenNewWindow_(Editor &primary)
|
||||||
{
|
{
|
||||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
|
||||||
|
|
||||||
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
||||||
int w = windows_[0]->width;
|
int w = windows_[0]->width;
|
||||||
int h = windows_[0]->height;
|
int h = windows_[0]->height;
|
||||||
@@ -277,25 +349,48 @@ GUIFrontend::OpenNewWindow_(Editor &primary)
|
|||||||
SDL_GL_MakeCurrent(win, gl_ctx);
|
SDL_GL_MakeCurrent(win, gl_ctx);
|
||||||
SDL_GL_SetSwapInterval(1);
|
SDL_GL_SetSwapInterval(1);
|
||||||
|
|
||||||
// Secondary windows share the ImGui context already created in Init.
|
// Each window gets its own ImGui context — ImGui requires exactly one
|
||||||
// We need to init the SDL2/OpenGL backends for this new window.
|
// NewFrame/Render cycle per context per frame.
|
||||||
// ImGui_ImplSDL2 supports multiple windows via SDL_GetWindowID checks.
|
ImGuiContext *imgui_ctx = ImGui::CreateContext();
|
||||||
ImGui_ImplOpenGL3_Init(kGlslVersion);
|
ImGui::SetCurrentContext(imgui_ctx);
|
||||||
|
|
||||||
|
SetupImGuiStyle_();
|
||||||
|
|
||||||
|
if (!ImGui_ImplSDL2_InitForOpenGL(win, gl_ctx)) {
|
||||||
|
ImGui::DestroyContext(imgui_ctx);
|
||||||
|
SDL_GL_DeleteContext(gl_ctx);
|
||||||
|
SDL_DestroyWindow(win);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ImGui_ImplOpenGL3_Init(kGlslVersion)) {
|
||||||
|
ImGui_ImplSDL2_Shutdown();
|
||||||
|
ImGui::DestroyContext(imgui_ctx);
|
||||||
|
SDL_GL_DeleteContext(gl_ctx);
|
||||||
|
SDL_DestroyWindow(win);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
auto ws = std::make_unique<WindowState>();
|
auto ws = std::make_unique<WindowState>();
|
||||||
ws->window = win;
|
ws->window = win;
|
||||||
ws->gl_ctx = gl_ctx;
|
ws->gl_ctx = gl_ctx;
|
||||||
|
ws->imgui_ctx = imgui_ctx;
|
||||||
ws->width = w;
|
ws->width = w;
|
||||||
ws->height = h;
|
ws->height = h;
|
||||||
|
|
||||||
// Secondary editor shares the primary's buffer list
|
// Secondary editor shares the primary's buffer list
|
||||||
ws->editor.SetSharedBuffers(&primary.Buffers());
|
ws->editor.SetSharedBuffers(&primary.Buffers());
|
||||||
ws->editor.SetDimensions(primary.Rows(), primary.Cols());
|
ws->editor.SetDimensions(primary.Rows(), primary.Cols());
|
||||||
|
|
||||||
|
// Open a new untitled buffer and switch to it in the new window.
|
||||||
|
ws->editor.AddBuffer(Buffer());
|
||||||
|
ws->editor.SwitchTo(ws->editor.BufferCount() - 1);
|
||||||
|
|
||||||
ws->input.Attach(&ws->editor);
|
ws->input.Attach(&ws->editor);
|
||||||
|
|
||||||
windows_.push_back(std::move(ws));
|
windows_.push_back(std::move(ws));
|
||||||
|
|
||||||
// Restore primary GL context as current
|
// Restore primary context
|
||||||
|
ImGui::SetCurrentContext(windows_[0]->imgui_ctx);
|
||||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -305,10 +400,10 @@ void
|
|||||||
GUIFrontend::Step(Editor &ed, bool &running)
|
GUIFrontend::Step(Editor &ed, bool &running)
|
||||||
{
|
{
|
||||||
// --- Event processing ---
|
// --- Event processing ---
|
||||||
|
// SDL events carry a window ID. Route each event to the correct window's
|
||||||
|
// ImGui context (for ImGui_ImplSDL2_ProcessEvent) and input handler.
|
||||||
SDL_Event e;
|
SDL_Event e;
|
||||||
while (SDL_PollEvent(&e)) {
|
while (SDL_PollEvent(&e)) {
|
||||||
ImGui_ImplSDL2_ProcessEvent(&e);
|
|
||||||
|
|
||||||
// Determine which window this event belongs to
|
// Determine which window this event belongs to
|
||||||
Uint32 event_win_id = 0;
|
Uint32 event_win_id = 0;
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
@@ -329,6 +424,9 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
case SDL_MOUSEWHEEL:
|
case SDL_MOUSEWHEEL:
|
||||||
event_win_id = e.wheel.windowID;
|
event_win_id = e.wheel.windowID;
|
||||||
break;
|
break;
|
||||||
|
case SDL_MOUSEMOTION:
|
||||||
|
event_win_id = e.motion.windowID;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -338,43 +436,57 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the target window and route the event to its ImGui context
|
||||||
|
WindowState *target = nullptr;
|
||||||
|
std::size_t target_idx = 0;
|
||||||
|
if (event_win_id != 0) {
|
||||||
|
for (std::size_t i = 0; i < windows_.size(); ++i) {
|
||||||
|
if (SDL_GetWindowID(windows_[i]->window) == event_win_id) {
|
||||||
|
target = windows_[i].get();
|
||||||
|
target_idx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target && target->imgui_ctx) {
|
||||||
|
// Set this window's ImGui context so ImGui_ImplSDL2_ProcessEvent
|
||||||
|
// updates the correct IO state.
|
||||||
|
ImGui::SetCurrentContext(target->imgui_ctx);
|
||||||
|
ImGui_ImplSDL2_ProcessEvent(&e);
|
||||||
|
}
|
||||||
|
|
||||||
if (e.type == SDL_WINDOWEVENT) {
|
if (e.type == SDL_WINDOWEVENT) {
|
||||||
if (e.window.event == SDL_WINDOWEVENT_CLOSE) {
|
if (e.window.event == SDL_WINDOWEVENT_CLOSE) {
|
||||||
// Mark the window as dead; primary window close = quit
|
if (target) {
|
||||||
for (std::size_t i = 0; i < windows_.size(); ++i) {
|
if (target_idx == 0) {
|
||||||
if (SDL_GetWindowID(windows_[i]->window) == e.window.windowID) {
|
|
||||||
if (i == 0) {
|
|
||||||
running = false;
|
running = false;
|
||||||
} else {
|
} else {
|
||||||
windows_[i]->alive = false;
|
target->alive = false;
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
} else if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||||
for (auto &ws: windows_) {
|
if (target) {
|
||||||
if (SDL_GetWindowID(ws->window) == e.window.windowID) {
|
target->width = e.window.data1;
|
||||||
ws->width = e.window.data1;
|
target->height = e.window.data2;
|
||||||
ws->height = e.window.data2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route input events to the correct window's input handler
|
// Route input events to the correct window's input handler
|
||||||
if (event_win_id != 0) {
|
if (target) {
|
||||||
// Primary window (index 0) uses the external editor &ed
|
Editor &tgt_ed = (target_idx == 0) ? ed : target->editor;
|
||||||
if (windows_.size() > 0 &&
|
if (tgt_ed.FilePickerVisible()) {
|
||||||
SDL_GetWindowID(windows_[0]->window) == event_win_id) {
|
// Modal: don't let keystrokes fall through as edit commands
|
||||||
windows_[0]->input.ProcessSDLEvent(e);
|
// to the buffer underneath. Escape closes the picker;
|
||||||
|
// everything else (navigation, filtering, double-click) is
|
||||||
|
// handled by the picker's own ImGui widgets via
|
||||||
|
// ImGui_ImplSDL2_ProcessEvent above.
|
||||||
|
if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE) {
|
||||||
|
tgt_ed.SetFilePickerVisible(false);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for (std::size_t i = 1; i < windows_.size(); ++i) {
|
target->input.ProcessSDLEvent(e);
|
||||||
if (SDL_GetWindowID(windows_[i]->window) == event_win_id) {
|
|
||||||
windows_[i]->input.ProcessSDLEvent(e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,18 +494,24 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
if (!running)
|
if (!running)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// --- Apply pending font change ---
|
// --- Apply pending font change (to all contexts) ---
|
||||||
{
|
{
|
||||||
std::string fname;
|
std::string fname;
|
||||||
float fsize = 0.0f;
|
float fsize = 0.0f;
|
||||||
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(fname, fsize)) {
|
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(fname, fsize)) {
|
||||||
if (!fname.empty() && fsize > 0.0f) {
|
if (!fname.empty() && fsize > 0.0f) {
|
||||||
|
for (auto &ws : windows_) {
|
||||||
|
if (!ws->alive || !ws->imgui_ctx)
|
||||||
|
continue;
|
||||||
|
ImGui::SetCurrentContext(ws->imgui_ctx);
|
||||||
|
SDL_GL_MakeCurrent(ws->window, ws->gl_ctx);
|
||||||
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
|
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
|
||||||
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||||
ImGui_ImplOpenGL3_CreateFontsTexture();
|
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Step each window ---
|
// --- Step each window ---
|
||||||
// We iterate by index because OpenNewWindow_ may append to windows_.
|
// We iterate by index because OpenNewWindow_ may append to windows_.
|
||||||
@@ -404,7 +522,12 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
|
|
||||||
Editor &wed = (wi == 0) ? ed : ws.editor;
|
Editor &wed = (wi == 0) ? ed : ws.editor;
|
||||||
|
|
||||||
|
// Shared buffer list may have been modified by another window.
|
||||||
|
wed.ValidateBufferIndex();
|
||||||
|
|
||||||
|
// Activate this window's GL and ImGui contexts
|
||||||
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
|
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
|
||||||
|
ImGui::SetCurrentContext(ws.imgui_ctx);
|
||||||
|
|
||||||
// Start a new ImGui frame
|
// Start a new ImGui frame
|
||||||
ImGui_ImplOpenGL3_NewFrame();
|
ImGui_ImplOpenGL3_NewFrame();
|
||||||
@@ -422,6 +545,9 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
// Allow deferred opens
|
// Allow deferred opens
|
||||||
wed.ProcessPendingOpens();
|
wed.ProcessPendingOpens();
|
||||||
|
|
||||||
|
// Ensure newly opened buffers get syntax + edit mode detection
|
||||||
|
apply_syntax_to_buffer(wed.CurrentBuffer(), config_);
|
||||||
|
|
||||||
// Drain input queue
|
// Drain input queue
|
||||||
for (;;) {
|
for (;;) {
|
||||||
MappedInput mi;
|
MappedInput mi;
|
||||||
@@ -431,6 +557,21 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
if (mi.id == CommandId::NewWindow) {
|
if (mi.id == CommandId::NewWindow) {
|
||||||
// Open a new window; handled after this loop
|
// Open a new window; handled after this loop
|
||||||
wed.SetNewWindowRequested(true);
|
wed.SetNewWindowRequested(true);
|
||||||
|
} else if (mi.id == CommandId::FontZoomIn ||
|
||||||
|
mi.id == CommandId::FontZoomOut ||
|
||||||
|
mi.id == CommandId::FontZoomReset) {
|
||||||
|
auto &fr = kte::Fonts::FontRegistry::Instance();
|
||||||
|
float cur = fr.CurrentFontSize();
|
||||||
|
if (cur <= 0.0f) cur = config_.font_size;
|
||||||
|
float next = cur;
|
||||||
|
if (mi.id == CommandId::FontZoomIn)
|
||||||
|
next = std::min(cur + 2.0f, 72.0f);
|
||||||
|
else if (mi.id == CommandId::FontZoomOut)
|
||||||
|
next = std::max(cur - 2.0f, 8.0f);
|
||||||
|
else
|
||||||
|
next = config_.font_size; // reset to config default
|
||||||
|
if (next != cur)
|
||||||
|
fr.RequestLoadFont(fr.CurrentFontName(), next);
|
||||||
} else {
|
} else {
|
||||||
const std::string before = wed.KillRingHead();
|
const std::string before = wed.KillRingHead();
|
||||||
Execute(wed, mi.id, mi.arg, mi.count);
|
Execute(wed, mi.id, mi.arg, mi.count);
|
||||||
@@ -442,16 +583,27 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle new-window request
|
|
||||||
if (wed.NewWindowRequested()) {
|
|
||||||
wed.SetNewWindowRequested(false);
|
|
||||||
OpenNewWindow_(ed); // always share primary editor's buffers
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wi == 0 && wed.QuitRequested()) {
|
if (wi == 0 && wed.QuitRequested()) {
|
||||||
running = false;
|
running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Switch font based on current buffer's edit mode (deferred to next frame)
|
||||||
|
{
|
||||||
|
Buffer *cur = wed.CurrentBuffer();
|
||||||
|
if (cur) {
|
||||||
|
auto &fr = kte::Fonts::FontRegistry::Instance();
|
||||||
|
const std::string &expected =
|
||||||
|
(cur->GetEditMode() == EditMode::Writing)
|
||||||
|
? config_.writing_font
|
||||||
|
: config_.code_font;
|
||||||
|
if (fr.CurrentFontName() != expected && fr.HasFont(expected)) {
|
||||||
|
float sz = fr.CurrentFontSize();
|
||||||
|
if (sz <= 0.0f) sz = config_.font_size;
|
||||||
|
fr.RequestLoadFont(expected, sz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw
|
// Draw
|
||||||
ws.renderer.Draw(wed);
|
ws.renderer.Draw(wed);
|
||||||
|
|
||||||
@@ -466,52 +618,40 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
SDL_GL_SwapWindow(ws.window);
|
SDL_GL_SwapWindow(ws.window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle deferred new-window requests (must happen outside the render loop
|
||||||
|
// to avoid corrupting an in-progress ImGui frame).
|
||||||
|
for (std::size_t wi = 0; wi < windows_.size(); ++wi) {
|
||||||
|
Editor &wed = (wi == 0) ? ed : windows_[wi]->editor;
|
||||||
|
if (wed.NewWindowRequested()) {
|
||||||
|
wed.SetNewWindowRequested(false);
|
||||||
|
OpenNewWindow_(ed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove dead secondary windows
|
// Remove dead secondary windows
|
||||||
for (auto it = windows_.begin() + 1; it != windows_.end();) {
|
for (auto it = windows_.begin() + 1; it != windows_.end();) {
|
||||||
if (!(*it)->alive) {
|
if (!(*it)->alive) {
|
||||||
SDL_GL_MakeCurrent((*it)->window, (*it)->gl_ctx);
|
DestroyWindowResources_(**it);
|
||||||
ImGui_ImplOpenGL3_Shutdown();
|
|
||||||
SDL_GL_DeleteContext((*it)->gl_ctx);
|
|
||||||
SDL_DestroyWindow((*it)->window);
|
|
||||||
it = windows_.erase(it);
|
it = windows_.erase(it);
|
||||||
// Restore primary context
|
|
||||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore primary context
|
||||||
|
if (!windows_.empty()) {
|
||||||
|
ImGui::SetCurrentContext(windows_[0]->imgui_ctx);
|
||||||
|
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
GUIFrontend::Shutdown()
|
GUIFrontend::Shutdown()
|
||||||
{
|
{
|
||||||
// Destroy secondary windows first
|
// Destroy all windows (secondary first, then primary)
|
||||||
for (std::size_t i = 1; i < windows_.size(); ++i) {
|
for (auto it = windows_.rbegin(); it != windows_.rend(); ++it) {
|
||||||
SDL_GL_MakeCurrent(windows_[i]->window, windows_[i]->gl_ctx);
|
DestroyWindowResources_(**it);
|
||||||
ImGui_ImplOpenGL3_Shutdown();
|
|
||||||
SDL_GL_DeleteContext(windows_[i]->gl_ctx);
|
|
||||||
SDL_DestroyWindow(windows_[i]->window);
|
|
||||||
}
|
|
||||||
windows_.resize(std::min(windows_.size(), std::size_t(1)));
|
|
||||||
|
|
||||||
// Destroy primary window
|
|
||||||
if (!windows_.empty()) {
|
|
||||||
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
|
||||||
}
|
|
||||||
ImGui_ImplOpenGL3_Shutdown();
|
|
||||||
ImGui_ImplSDL2_Shutdown();
|
|
||||||
ImGui::DestroyContext();
|
|
||||||
|
|
||||||
if (!windows_.empty()) {
|
|
||||||
if (windows_[0]->gl_ctx) {
|
|
||||||
SDL_GL_DeleteContext(windows_[0]->gl_ctx);
|
|
||||||
windows_[0]->gl_ctx = nullptr;
|
|
||||||
}
|
|
||||||
if (windows_[0]->window) {
|
|
||||||
SDL_DestroyWindow(windows_[0]->window);
|
|
||||||
windows_[0]->window = nullptr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
windows_.clear();
|
windows_.clear();
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
|
|||||||
+8
-1
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
|
|
||||||
struct SDL_Window;
|
struct SDL_Window;
|
||||||
|
struct ImGuiContext;
|
||||||
typedef void *SDL_GLContext;
|
typedef void *SDL_GLContext;
|
||||||
|
|
||||||
class GUIFrontend final : public Frontend {
|
class GUIFrontend final : public Frontend {
|
||||||
@@ -28,10 +29,13 @@ public:
|
|||||||
void Shutdown() override;
|
void Shutdown() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Per-window state
|
// Per-window state — each window owns its own ImGui context so that
|
||||||
|
// NewFrame/Render cycles are fully independent (ImGui requires exactly
|
||||||
|
// one NewFrame per Render per context).
|
||||||
struct WindowState {
|
struct WindowState {
|
||||||
SDL_Window *window = nullptr;
|
SDL_Window *window = nullptr;
|
||||||
SDL_GLContext gl_ctx = nullptr;
|
SDL_GLContext gl_ctx = nullptr;
|
||||||
|
ImGuiContext *imgui_ctx = nullptr;
|
||||||
ImGuiInputHandler input{};
|
ImGuiInputHandler input{};
|
||||||
ImGuiRenderer renderer{};
|
ImGuiRenderer renderer{};
|
||||||
Editor editor{};
|
Editor editor{};
|
||||||
@@ -44,6 +48,9 @@ private:
|
|||||||
// Returns false if window creation fails.
|
// Returns false if window creation fails.
|
||||||
bool OpenNewWindow_(Editor &primary);
|
bool OpenNewWindow_(Editor &primary);
|
||||||
|
|
||||||
|
// Initialize fonts and theme for a given ImGui context (must be current).
|
||||||
|
void SetupImGuiStyle_();
|
||||||
|
static void DestroyWindowResources_(WindowState &ws);
|
||||||
static bool LoadGuiFont_(const char *path, float size_px);
|
static bool LoadGuiFont_(const char *path, float size_px);
|
||||||
|
|
||||||
GUIConfig config_{};
|
GUIConfig config_{};
|
||||||
|
|||||||
+47
-30
@@ -8,6 +8,20 @@
|
|||||||
#include "ImGuiInputHandler.h"
|
#include "ImGuiInputHandler.h"
|
||||||
#include "KKeymap.h"
|
#include "KKeymap.h"
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
|
#include "PasteSplit.h"
|
||||||
|
|
||||||
|
// Verbose k-prefix suffix logging for debugging macOS/SDL key translation
|
||||||
|
// issues. Default to off; enable by defining IMGUI_IH_DEBUG=1 at compile
|
||||||
|
// time. Mirrors QtInputHandler.cc's QT_IH_DEBUG gate.
|
||||||
|
#ifndef IMGUI_IH_DEBUG
|
||||||
|
#define IMGUI_IH_DEBUG 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if IMGUI_IH_DEBUG
|
||||||
|
#define IH_LOGF(...) do { std::fprintf(stderr, __VA_ARGS__); std::fflush(stderr); } while (0)
|
||||||
|
#else
|
||||||
|
#define IH_LOGF(...) ((void) 0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
@@ -182,18 +196,19 @@ map_key(const SDL_Keycode key,
|
|||||||
k_ctrl_pending = false;
|
k_ctrl_pending = false;
|
||||||
CommandId id;
|
CommandId id;
|
||||||
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
||||||
|
#if IMGUI_IH_DEBUG
|
||||||
// Diagnostics for u/U
|
// Diagnostics for u/U
|
||||||
if (lower == 'u') {
|
if (lower == 'u') {
|
||||||
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
||||||
? static_cast<char>(ascii_key)
|
? static_cast<char>(ascii_key)
|
||||||
: '?';
|
: '?';
|
||||||
std::fprintf(stderr,
|
IH_LOGF(
|
||||||
"[kge] k-prefix suffix: sym=%d mods=0x%x ascii=%d '%c' ctrl2=%d pass_ctrl=%d mapped=%d id=%d\n",
|
"[kge] k-prefix suffix: sym=%d mods=0x%x ascii=%d '%c' ctrl2=%d pass_ctrl=%d mapped=%d id=%d\n",
|
||||||
static_cast<int>(key), static_cast<unsigned int>(mod), ascii_key, disp,
|
static_cast<int>(key), static_cast<unsigned int>(mod), ascii_key, disp,
|
||||||
ctrl2 ? 1 : 0, pass_ctrl ? 1 : 0, mapped ? 1 : 0,
|
ctrl2 ? 1 : 0, pass_ctrl ? 1 : 0, mapped ? 1 : 0,
|
||||||
mapped ? static_cast<int>(id) : -1);
|
mapped ? static_cast<int>(id) : -1);
|
||||||
std::fflush(stderr);
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
out = {true, id, "", 0};
|
out = {true, id, "", 0};
|
||||||
if (ed)
|
if (ed)
|
||||||
@@ -349,6 +364,26 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Font zoom: Cmd+=/Cmd+-/Cmd+0 (macOS) or Ctrl+=/Ctrl+-/Ctrl+0
|
||||||
|
if ((mods & (KMOD_CTRL | KMOD_GUI)) && !(mods & KMOD_SHIFT)) {
|
||||||
|
bool is_zoom = true;
|
||||||
|
CommandId zoom_cmd = CommandId::FontZoomIn;
|
||||||
|
if (key == SDLK_EQUALS || key == SDLK_PLUS)
|
||||||
|
zoom_cmd = CommandId::FontZoomIn;
|
||||||
|
else if (key == SDLK_MINUS)
|
||||||
|
zoom_cmd = CommandId::FontZoomOut;
|
||||||
|
else if (key == SDLK_0)
|
||||||
|
zoom_cmd = CommandId::FontZoomReset;
|
||||||
|
else
|
||||||
|
is_zoom = false;
|
||||||
|
if (is_zoom) {
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, zoom_cmd, std::string(), 0});
|
||||||
|
suppress_text_input_once_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Paste: Ctrl+V (Windows/Linux) or Cmd+V (macOS)
|
// Handle Paste: Ctrl+V (Windows/Linux) or Cmd+V (macOS)
|
||||||
// Note: SDL defines letter keycodes in lowercase only (e.g., SDLK_v). Shift does not change keycode.
|
// Note: SDL defines letter keycodes in lowercase only (e.g., SDLK_v). Shift does not change keycode.
|
||||||
if ((mods & (KMOD_CTRL | KMOD_GUI)) && (key == SDLK_v)) {
|
if ((mods & (KMOD_CTRL | KMOD_GUI)) && (key == SDLK_v)) {
|
||||||
@@ -356,32 +391,12 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
if (clip) {
|
if (clip) {
|
||||||
std::string text(clip);
|
std::string text(clip);
|
||||||
SDL_free(clip);
|
SDL_free(clip);
|
||||||
// Split on '\n' and enqueue as InsertText/Newline commands
|
// Turn line breaks (\n, \r\n, or bare \r) into Newline
|
||||||
|
// commands and the rest into InsertText; InsertText itself
|
||||||
|
// rejects any embedded '\r'/'\n'.
|
||||||
std::lock_guard<std::mutex> lk(mu_);
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
std::size_t start = 0;
|
for (const auto &cmd : SplitPasteIntoCommands(text))
|
||||||
while (start <= text.size()) {
|
q_.push(cmd);
|
||||||
std::size_t pos = text.find('\n', start);
|
|
||||||
std::string_view segment;
|
|
||||||
bool has_nl = (pos != std::string::npos);
|
|
||||||
if (has_nl) {
|
|
||||||
segment = std::string_view(text).substr(start, pos - start);
|
|
||||||
} else {
|
|
||||||
segment = std::string_view(text).substr(start);
|
|
||||||
}
|
|
||||||
if (!segment.empty()) {
|
|
||||||
MappedInput ins{
|
|
||||||
true, CommandId::InsertText, std::string(segment), 0
|
|
||||||
};
|
|
||||||
q_.push(ins);
|
|
||||||
}
|
|
||||||
if (has_nl) {
|
|
||||||
MappedInput nl{true, CommandId::Newline, std::string(), 0};
|
|
||||||
q_.push(nl);
|
|
||||||
start = pos + 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Suppress the corresponding TEXTINPUT that may follow
|
// Suppress the corresponding TEXTINPUT that may follow
|
||||||
suppress_text_input_once_ = true;
|
suppress_text_input_once_ = true;
|
||||||
return true; // consumed
|
return true; // consumed
|
||||||
@@ -504,15 +519,17 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
bool pass_ctrl = k_ctrl_pending_;
|
bool pass_ctrl = k_ctrl_pending_;
|
||||||
k_ctrl_pending_ = false;
|
k_ctrl_pending_ = false;
|
||||||
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
||||||
|
#if IMGUI_IH_DEBUG
|
||||||
// Diagnostics: log any k-prefix TEXTINPUT suffix mapping
|
// Diagnostics: log any k-prefix TEXTINPUT suffix mapping
|
||||||
|
{
|
||||||
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
||||||
? static_cast<char>(ascii_key)
|
? static_cast<char>(ascii_key)
|
||||||
: '?';
|
: '?';
|
||||||
std::fprintf(stderr,
|
IH_LOGF("[kge] k-prefix TEXTINPUT suffix: ascii=%d '%c' mapped=%d id=%d\n",
|
||||||
"[kge] k-prefix TEXTINPUT suffix: ascii=%d '%c' mapped=%d id=%d\n",
|
|
||||||
ascii_key, disp, mapped ? 1 : 0,
|
ascii_key, disp, mapped ? 1 : 0,
|
||||||
mapped ? static_cast<int>(id) : -1);
|
mapped ? static_cast<int>(id) : -1);
|
||||||
std::fflush(stderr);
|
}
|
||||||
|
#endif
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
mi = {true, id, "", 0};
|
mi = {true, id, "", 0};
|
||||||
if (ed_)
|
if (ed_)
|
||||||
|
|||||||
+218
-147
@@ -76,23 +76,18 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
// Two-way sync between Buffer::Rowoffs and ImGui scroll position:
|
// Two-way sync between Buffer::Rowoffs and ImGui scroll position:
|
||||||
// - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it.
|
// - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it.
|
||||||
// - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view.
|
// - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view.
|
||||||
static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
|
|
||||||
static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs
|
|
||||||
|
|
||||||
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
||||||
const long buf_coloffs = static_cast<long>(buf->Coloffs());
|
const long buf_coloffs = static_cast<long>(buf->Coloffs());
|
||||||
|
|
||||||
// Detect programmatic change (e.g., page_down command changed rowoffs)
|
// Detect programmatic change (e.g., page_down command changed rowoffs)
|
||||||
// Use SetNextWindowScroll BEFORE BeginChild to set initial scroll position
|
// Use SetNextWindowScroll BEFORE BeginChild to set initial scroll position
|
||||||
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
|
if (prev_buf_rowoffs_ >= 0 && buf_rowoffs != prev_buf_rowoffs_) {
|
||||||
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
||||||
ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y));
|
ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y));
|
||||||
}
|
}
|
||||||
if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
|
// Horizontal scroll is handled purely in pixel space (see
|
||||||
float target_x = static_cast<float>(buf_coloffs) * space_w;
|
// cursor-visibility block after the line loop) so we don't
|
||||||
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
// convert the character-based coloffs to an ImGui scroll here.
|
||||||
ImGui::SetNextWindowScroll(ImVec2(target_x, target_y));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reserve space for status bar at bottom.
|
// Reserve space for status bar at bottom.
|
||||||
// We calculate a height that is an exact multiple of the line height
|
// We calculate a height that is an exact multiple of the line height
|
||||||
@@ -111,44 +106,92 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
ImVec2 child_window_pos = ImGui::GetWindowPos();
|
ImVec2 child_window_pos = ImGui::GetWindowPos();
|
||||||
float scroll_y = ImGui::GetScrollY();
|
float scroll_y = ImGui::GetScrollY();
|
||||||
float scroll_x = ImGui::GetScrollX();
|
float scroll_x = ImGui::GetScrollX();
|
||||||
std::size_t rowoffs = 0; // we render from the first line; scrolling is handled by ImGui
|
|
||||||
|
|
||||||
// Synchronize buffer offsets from ImGui scroll if user scrolled manually
|
// Synchronize buffer offsets from ImGui scroll if user scrolled manually
|
||||||
bool forced_scroll = false;
|
bool forced_scroll = false;
|
||||||
{
|
{
|
||||||
static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
|
|
||||||
static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels
|
|
||||||
|
|
||||||
const long scroll_top = static_cast<long>(scroll_y / row_h);
|
const long scroll_top = static_cast<long>(scroll_y / row_h);
|
||||||
const long scroll_left = static_cast<long>(scroll_x / space_w);
|
|
||||||
|
|
||||||
// Check if rowoffs was programmatically changed this frame
|
// Check if rowoffs was programmatically changed this frame
|
||||||
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
|
if (prev_buf_rowoffs_ >= 0 && buf_rowoffs != prev_buf_rowoffs_) {
|
||||||
forced_scroll = true;
|
forced_scroll = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user scrolled (not programmatic), update buffer offsets accordingly
|
// If user scrolled vertically (not programmatic), update buffer row offset
|
||||||
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y && !forced_scroll) {
|
if (prev_scroll_y_ >= 0.0f && scroll_y != prev_scroll_y_ && !forced_scroll) {
|
||||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||||
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
||||||
mbuf->Coloffs());
|
mbuf->Coloffs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x && !forced_scroll) {
|
// Horizontal scroll is pixel-based and managed by the cursor
|
||||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
// visibility block below; we don't sync it back to coloffs.
|
||||||
mbuf->SetOffsets(mbuf->Rowoffs(),
|
|
||||||
static_cast<std::size_t>(std::max(0L, scroll_left)));
|
// Update trackers for next frame
|
||||||
|
prev_scroll_y_ = scroll_y;
|
||||||
|
prev_scroll_x_ = scroll_x;
|
||||||
|
}
|
||||||
|
prev_buf_rowoffs_ = buf_rowoffs;
|
||||||
|
prev_buf_coloffs_ = buf_coloffs;
|
||||||
|
|
||||||
|
// Max-line-width cache: reset when the buffer or font changes. We only
|
||||||
|
// measure visible lines per frame and maintain a running max, so the
|
||||||
|
// scrollbar may be slightly conservative on first view of a file until
|
||||||
|
// the user scrolls, but idle CPU drops dramatically on large files.
|
||||||
|
// Edits bump the buffer version but we intentionally do NOT reset the
|
||||||
|
// max on version changes — keeping a conservative (possibly too-wide)
|
||||||
|
// scrollbar is preferable to per-keystroke jitter.
|
||||||
|
{
|
||||||
|
ImFont *cur_font = ImGui::GetFont();
|
||||||
|
float cur_fsize = ImGui::GetFontSize();
|
||||||
|
if (buf != max_width_buf_
|
||||||
|
|| cur_font != max_width_font_
|
||||||
|
|| cur_fsize != max_width_font_size_) {
|
||||||
|
max_width_buf_ = buf;
|
||||||
|
max_width_font_ = cur_font;
|
||||||
|
max_width_font_size_ = cur_fsize;
|
||||||
|
max_width_px_ = 0.0f;
|
||||||
|
}
|
||||||
|
max_width_version_ = buf->Version();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hoist the search-regex compilation out of the per-line loop. Compiling
|
||||||
|
// std::regex per line per frame was a large source of idle CPU on macOS.
|
||||||
|
const bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
||||||
|
const bool regex_mode = search_mode && ed.PromptActive() && (
|
||||||
|
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
||||||
|
ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind);
|
||||||
|
std::regex search_rx;
|
||||||
|
bool search_rx_valid = false;
|
||||||
|
if (regex_mode) {
|
||||||
|
try {
|
||||||
|
search_rx = std::regex(ed.SearchQuery());
|
||||||
|
search_rx_valid = true;
|
||||||
|
} catch (const std::regex_error &) {
|
||||||
|
search_rx_valid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update trackers for next frame
|
// Compute the visible row range and skip rendering work for off-screen
|
||||||
prev_scroll_y = scroll_y;
|
// lines. ImGui clips drawing, but string allocation, tab expansion,
|
||||||
prev_scroll_x = scroll_x;
|
// syntax-highlight lookups, and width measurement are not free.
|
||||||
|
const std::size_t total_rows = lines.size();
|
||||||
|
std::size_t first_vis = 0;
|
||||||
|
std::size_t last_vis = total_rows; // exclusive
|
||||||
|
if (row_h > 0.0f && total_rows > 0) {
|
||||||
|
const long margin = 4; // render a few extra rows above/below for smoother scrolling
|
||||||
|
long fv = static_cast<long>(std::floor(scroll_y / row_h)) - margin;
|
||||||
|
long lv = static_cast<long>(
|
||||||
|
std::ceil((scroll_y + child_h_plan) / row_h)) + margin;
|
||||||
|
if (fv < 0) fv = 0;
|
||||||
|
if (lv < 0) lv = 0;
|
||||||
|
if (static_cast<std::size_t>(fv) > total_rows)
|
||||||
|
fv = static_cast<long>(total_rows);
|
||||||
|
if (static_cast<std::size_t>(lv) > total_rows)
|
||||||
|
lv = static_cast<long>(total_rows);
|
||||||
|
first_vis = static_cast<std::size_t>(fv);
|
||||||
|
last_vis = static_cast<std::size_t>(lv);
|
||||||
}
|
}
|
||||||
prev_buf_rowoffs = buf_rowoffs;
|
|
||||||
prev_buf_coloffs = buf_coloffs;
|
|
||||||
// Cache current horizontal offset in rendered columns for click handling
|
|
||||||
const std::size_t coloffs_now = buf->Coloffs();
|
|
||||||
|
|
||||||
// Mark selection state (mark -> cursor), in source coordinates
|
// Mark selection state (mark -> cursor), in source coordinates
|
||||||
bool sel_active = false;
|
bool sel_active = false;
|
||||||
@@ -169,7 +212,7 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
const std::size_t vsel_sy = vsel_active ? buf->VisualLineStartY() : 0;
|
const std::size_t vsel_sy = vsel_active ? buf->VisualLineStartY() : 0;
|
||||||
const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 0;
|
const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 0;
|
||||||
|
|
||||||
static bool mouse_selecting = false;
|
// (mouse_selecting__ is a member variable)
|
||||||
auto mouse_pos_to_buf = [&]() -> std::pair<std::size_t, std::size_t> {
|
auto mouse_pos_to_buf = [&]() -> std::pair<std::size_t, std::size_t> {
|
||||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||||
// Convert mouse pos to buffer row
|
// Convert mouse pos to buffer row
|
||||||
@@ -181,29 +224,54 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
if (by >= lines.size())
|
if (by >= lines.size())
|
||||||
by = lines.empty() ? 0 : (lines.size() - 1);
|
by = lines.empty() ? 0 : (lines.size() - 1);
|
||||||
|
|
||||||
// Convert mouse pos to rendered x
|
if (lines.empty())
|
||||||
|
return {0, 0};
|
||||||
|
|
||||||
|
// Expand tabs for the clicked line
|
||||||
|
std::string line_clicked = static_cast<std::string>(lines[by]);
|
||||||
|
const std::size_t tabw = 8;
|
||||||
|
std::string click_expanded;
|
||||||
|
click_expanded.reserve(line_clicked.size() + 16);
|
||||||
|
std::size_t click_rx = 0;
|
||||||
|
// Map: source column -> expanded column
|
||||||
|
std::vector<std::size_t> src_to_exp;
|
||||||
|
src_to_exp.reserve(line_clicked.size() + 1);
|
||||||
|
for (std::size_t ci = 0; ci < line_clicked.size(); ++ci) {
|
||||||
|
src_to_exp.push_back(click_rx);
|
||||||
|
if (line_clicked[ci] == '\t') {
|
||||||
|
std::size_t adv = (tabw - (click_rx % tabw));
|
||||||
|
click_expanded.append(adv, ' ');
|
||||||
|
click_rx += adv;
|
||||||
|
} else {
|
||||||
|
click_expanded.push_back(line_clicked[ci]);
|
||||||
|
click_rx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
src_to_exp.push_back(click_rx); // past-end position
|
||||||
|
|
||||||
|
// Pixel x relative to the line start (accounting for scroll)
|
||||||
float visual_x = mp.x - child_window_pos.x;
|
float visual_x = mp.x - child_window_pos.x;
|
||||||
if (visual_x < 0.0f)
|
if (visual_x < 0.0f)
|
||||||
visual_x = 0.0f;
|
visual_x = 0.0f;
|
||||||
std::size_t clicked_rx = static_cast<std::size_t>(visual_x / space_w) + coloffs_now;
|
// Add scroll offset in pixels
|
||||||
|
visual_x += scroll_x;
|
||||||
|
|
||||||
// Convert rendered column to source column
|
// Find the source column whose expanded position is closest
|
||||||
if (lines.empty())
|
// to the click pixel, using actual text measurement.
|
||||||
return {0, 0};
|
|
||||||
std::string line_clicked = static_cast<std::string>(lines[by]);
|
|
||||||
const std::size_t tabw = 8;
|
|
||||||
std::size_t rx = 0;
|
|
||||||
std::size_t best_col = 0;
|
std::size_t best_col = 0;
|
||||||
float best_dist = std::numeric_limits<float>::infinity();
|
float best_dist = std::numeric_limits<float>::infinity();
|
||||||
float clicked_rx_f = static_cast<float>(clicked_rx);
|
for (std::size_t ci = 0; ci <= line_clicked.size(); ++ci) {
|
||||||
for (std::size_t i = 0; i <= line_clicked.size(); ++i) {
|
std::size_t exp_col = src_to_exp[ci];
|
||||||
float dist = std::fabs(clicked_rx_f - static_cast<float>(rx));
|
float px = 0.0f;
|
||||||
|
if (exp_col > 0 && !click_expanded.empty()) {
|
||||||
|
std::size_t end = std::min(click_expanded.size(), exp_col);
|
||||||
|
px = ImGui::CalcTextSize(click_expanded.c_str(),
|
||||||
|
click_expanded.c_str() + end).x;
|
||||||
|
}
|
||||||
|
float dist = std::fabs(visual_x - px);
|
||||||
if (dist < best_dist) {
|
if (dist < best_dist) {
|
||||||
best_dist = dist;
|
best_dist = dist;
|
||||||
best_col = i;
|
best_col = ci;
|
||||||
}
|
|
||||||
if (i < line_clicked.size()) {
|
|
||||||
rx += (line_clicked[i] == '\t') ? (tabw - (rx % tabw)) : 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {by, best_col};
|
return {by, best_col};
|
||||||
@@ -211,7 +279,7 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
|
|
||||||
// Mouse-driven selection: set mark on double-click or drag, update cursor on any press/drag
|
// Mouse-driven selection: set mark on double-click or drag, update cursor on any press/drag
|
||||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||||
mouse_selecting = true;
|
mouse_selecting_ = true;
|
||||||
auto [by, bx] = mouse_pos_to_buf();
|
auto [by, bx] = mouse_pos_to_buf();
|
||||||
char tmp[64];
|
char tmp[64];
|
||||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
||||||
@@ -225,7 +293,7 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mouse_selecting && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
if (mouse_selecting_ && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||||
auto [by, bx] = mouse_pos_to_buf();
|
auto [by, bx] = mouse_pos_to_buf();
|
||||||
// If we are dragging (mouse moved while down), ensure mark is set to start selection
|
// If we are dragging (mouse moved while down), ensure mark is set to start selection
|
||||||
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 1.0f)) {
|
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 1.0f)) {
|
||||||
@@ -242,30 +310,56 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||||
}
|
}
|
||||||
if (mouse_selecting && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
if (mouse_selecting_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||||
mouse_selecting = false;
|
mouse_selecting_ = false;
|
||||||
}
|
}
|
||||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
// Advance the cursor Y so the first visible line draws at its correct
|
||||||
|
// scroll position. Skipped rows simply leave the layout cursor untouched.
|
||||||
|
if (first_vis > 0) {
|
||||||
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() +
|
||||||
|
static_cast<float>(first_vis) * row_h);
|
||||||
|
}
|
||||||
|
for (std::size_t i = first_vis; i < last_vis; ++i) {
|
||||||
// Capture the screen position before drawing the line
|
// Capture the screen position before drawing the line
|
||||||
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
ImVec2 line_pos = ImGui::GetCursorScreenPos();
|
||||||
std::string line = static_cast<std::string>(lines[i]);
|
std::string line = static_cast<std::string>(lines[i]);
|
||||||
|
|
||||||
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
|
// Expand tabs to spaces with width=8
|
||||||
const std::size_t tabw = 8;
|
const std::size_t tabw = 8;
|
||||||
std::string expanded;
|
std::string expanded;
|
||||||
expanded.reserve(line.size() + 16);
|
expanded.reserve(line.size() + 16);
|
||||||
std::size_t rx_abs_draw = 0; // rendered column for drawing
|
std::size_t rx_abs_draw = 0;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: convert a rendered column position to an absolute
|
||||||
|
// pixel x offset from the start of the line. ImGui's scroll
|
||||||
|
// handles viewport clipping so we measure from column 0.
|
||||||
|
auto rx_to_px = [&](std::size_t rx_col) -> float {
|
||||||
|
std::size_t end = std::min(expanded.size(), rx_col);
|
||||||
|
if (end == 0)
|
||||||
|
return 0.0f;
|
||||||
|
return ImGui::CalcTextSize(expanded.c_str(),
|
||||||
|
expanded.c_str() + end).x;
|
||||||
|
};
|
||||||
|
|
||||||
// Compute search highlight ranges for this line in source indices
|
// Compute search highlight ranges for this line in source indices
|
||||||
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
|
||||||
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
|
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
|
||||||
if (search_mode) {
|
if (search_mode) {
|
||||||
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring
|
// In regex mode, reuse the compiled regex hoisted above the loop.
|
||||||
if (ed.PromptActive() && (
|
if (regex_mode) {
|
||||||
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
|
if (search_rx_valid) {
|
||||||
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
|
||||||
try {
|
try {
|
||||||
std::regex rx(ed.SearchQuery());
|
for (auto it = std::sregex_iterator(line.begin(), line.end(), search_rx);
|
||||||
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
|
|
||||||
it != std::sregex_iterator(); ++it) {
|
it != std::sregex_iterator(); ++it) {
|
||||||
const auto &m = *it;
|
const auto &m = *it;
|
||||||
std::size_t sx = static_cast<std::size_t>(m.position());
|
std::size_t sx = static_cast<std::size_t>(m.position());
|
||||||
@@ -275,6 +369,7 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
} catch (const std::regex_error &) {
|
} catch (const std::regex_error &) {
|
||||||
// ignore invalid patterns here; status line already shows the error
|
// ignore invalid patterns here; status line already shows the error
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const std::string &q = ed.SearchQuery();
|
const std::string &q = ed.SearchQuery();
|
||||||
std::size_t pos = 0;
|
std::size_t pos = 0;
|
||||||
@@ -306,13 +401,8 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
std::size_t sx = rg.first, ex = rg.second;
|
std::size_t sx = rg.first, ex = rg.second;
|
||||||
std::size_t rx_start = src_to_rx(sx);
|
std::size_t rx_start = src_to_rx(sx);
|
||||||
std::size_t rx_end = src_to_rx(ex);
|
std::size_t rx_end = src_to_rx(ex);
|
||||||
// Apply horizontal scroll offset
|
ImVec2 p0 = ImVec2(line_pos.x + rx_to_px(rx_start), line_pos.y);
|
||||||
if (rx_end <= coloffs_now)
|
ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
|
||||||
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);
|
line_pos.y + line_h);
|
||||||
// Choose color: current match stronger
|
// Choose color: current match stronger
|
||||||
bool is_current = has_current && sx == cur_x && ex == cur_end;
|
bool is_current = has_current && sx == cur_x && ex == cur_end;
|
||||||
@@ -349,20 +439,14 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
if (line_has) {
|
if (line_has) {
|
||||||
std::size_t rx_start = src_to_rx(sx);
|
std::size_t rx_start = src_to_rx(sx);
|
||||||
std::size_t rx_end = src_to_rx(ex);
|
std::size_t rx_end = src_to_rx(ex);
|
||||||
if (rx_end > coloffs_now) {
|
ImVec2 p0 = ImVec2(line_pos.x + rx_to_px(rx_start),
|
||||||
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);
|
line_pos.y);
|
||||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
|
||||||
line_pos.y + line_h);
|
line_pos.y + line_h);
|
||||||
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (vsel_active && i >= vsel_sy && i <= vsel_ey) {
|
if (vsel_active && i >= vsel_sy && i <= vsel_ey) {
|
||||||
// Visual-line (multi-cursor) mode: highlight only the per-line cursor spot.
|
// Visual-line (multi-cursor) mode: highlight only the per-line cursor spot.
|
||||||
const std::size_t spot_sx = std::min(buf->Curx(), line.size());
|
const std::size_t spot_sx = std::min(buf->Curx(), line.size());
|
||||||
@@ -374,32 +458,13 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
// EOL spot: draw a 1-cell highlight just past the last character.
|
// EOL spot: draw a 1-cell highlight just past the last character.
|
||||||
rx_end = rx_start + 1;
|
rx_end = rx_start + 1;
|
||||||
}
|
}
|
||||||
if (rx_end > coloffs_now) {
|
ImVec2 p0 = ImVec2(line_pos.x + rx_to_px(rx_start),
|
||||||
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);
|
line_pos.y);
|
||||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
|
||||||
line_pos.y + line_h);
|
line_pos.y + line_h);
|
||||||
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw syntax-colored runs (text above background highlights)
|
// Draw syntax-colored runs (text above background highlights)
|
||||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
||||||
kte::LineHighlight lh = buf->Highlighter()->GetLine(
|
kte::LineHighlight lh = buf->Highlighter()->GetLine(
|
||||||
@@ -442,19 +507,14 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
for (const auto &sp: spans) {
|
for (const auto &sp: spans) {
|
||||||
std::size_t rx_s = src_to_rx_full(sp.s);
|
std::size_t rx_s = src_to_rx_full(sp.s);
|
||||||
std::size_t rx_e = src_to_rx_full(sp.e);
|
std::size_t rx_e = src_to_rx_full(sp.e);
|
||||||
if (rx_e <= coloffs_now)
|
std::size_t draw_start = rx_s;
|
||||||
continue; // fully left of viewport
|
|
||||||
// Clamp to visible portion and expanded length
|
|
||||||
std::size_t draw_start = (rx_s > coloffs_now) ? rx_s : coloffs_now;
|
|
||||||
if (draw_start >= expanded.size())
|
if (draw_start >= expanded.size())
|
||||||
continue; // fully right of expanded text
|
continue;
|
||||||
std::size_t draw_end = std::min<std::size_t>(rx_e, expanded.size());
|
std::size_t draw_end = std::min<std::size_t>(rx_e, expanded.size());
|
||||||
if (draw_end <= draw_start)
|
if (draw_end <= draw_start)
|
||||||
continue;
|
continue;
|
||||||
// Screen position is relative to coloffs_now
|
|
||||||
std::size_t screen_x = draw_start - coloffs_now;
|
|
||||||
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.k));
|
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.k));
|
||||||
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(screen_x) * space_w,
|
ImVec2 p = ImVec2(line_pos.x + rx_to_px(draw_start),
|
||||||
line_pos.y);
|
line_pos.y);
|
||||||
ImGui::GetWindowDrawList()->AddText(
|
ImGui::GetWindowDrawList()->AddText(
|
||||||
p, col, expanded.c_str() + draw_start, expanded.c_str() + draw_end);
|
p, col, expanded.c_str() + draw_start, expanded.c_str() + draw_end);
|
||||||
@@ -463,49 +523,49 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
// Use row_h (with spacing) to match click calculation and ensure consistent line positions.
|
// Use row_h (with spacing) to match click calculation and ensure consistent line positions.
|
||||||
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
||||||
} else {
|
} else {
|
||||||
// No syntax: draw as one run, accounting for horizontal scroll offset
|
// No syntax: draw the full line; ImGui scroll handles clipping.
|
||||||
if (coloffs_now < expanded.size()) {
|
if (!expanded.empty()) {
|
||||||
ImVec2 p = ImVec2(line_pos.x, line_pos.y);
|
ImVec2 p = ImVec2(line_pos.x, line_pos.y);
|
||||||
ImGui::GetWindowDrawList()->AddText(
|
ImGui::GetWindowDrawList()->AddText(
|
||||||
p, ImGui::GetColorU32(ImGuiCol_Text),
|
p, ImGui::GetColorU32(ImGuiCol_Text),
|
||||||
expanded.c_str() + coloffs_now);
|
expanded.c_str());
|
||||||
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
|
||||||
} else {
|
|
||||||
// Line is fully scrolled out of view horizontally
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
|
||||||
}
|
}
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw a visible cursor indicator on the current line
|
// Draw a visible cursor indicator on the current line
|
||||||
if (i == cy) {
|
if (i == cy) {
|
||||||
// Compute rendered X (rx) from source column with tab expansion
|
std::size_t rx_abs = src_to_rx(cx);
|
||||||
std::size_t rx_abs = 0;
|
float cursor_px = rx_to_px(rx_abs);
|
||||||
for (std::size_t k = 0; k < std::min(cx, line.size()); ++k) {
|
|
||||||
if (line[k] == '\t')
|
|
||||||
rx_abs += (tabw - (rx_abs % tabw));
|
|
||||||
else
|
|
||||||
rx_abs += 1;
|
|
||||||
}
|
|
||||||
// Convert to viewport x by subtracting horizontal col offset
|
|
||||||
std::size_t rx_viewport = (rx_abs > coloffs_now) ? (rx_abs - coloffs_now) : 0;
|
|
||||||
// For proportional fonts (Linux GUI), avoid accumulating drift by computing
|
|
||||||
// the exact pixel width of the expanded substring up to the cursor.
|
|
||||||
// expanded contains the line with tabs expanded to spaces and is what we draw.
|
|
||||||
float cursor_px = 0.0f;
|
|
||||||
if (rx_viewport > 0 && coloffs_now < expanded.size()) {
|
|
||||||
std::size_t start = coloffs_now;
|
|
||||||
std::size_t end = std::min(expanded.size(), start + rx_viewport);
|
|
||||||
// Measure substring width in pixels
|
|
||||||
ImVec2 sz = ImGui::CalcTextSize(expanded.c_str() + start,
|
|
||||||
expanded.c_str() + end);
|
|
||||||
cursor_px = sz.x;
|
|
||||||
}
|
|
||||||
ImVec2 p0 = ImVec2(line_pos.x + cursor_px, line_pos.y);
|
ImVec2 p0 = ImVec2(line_pos.x + cursor_px, line_pos.y);
|
||||||
ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h);
|
ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h);
|
||||||
ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight
|
ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track widest line for content width reporting. We only measure
|
||||||
|
// visible lines and fold the result into a monotonic max cached on
|
||||||
|
// the renderer (see max_width_* members). Off-screen lines are not
|
||||||
|
// measured per frame; if the user scrolls or edits, the cache is
|
||||||
|
// refreshed accordingly.
|
||||||
|
if (!expanded.empty()) {
|
||||||
|
float line_w = ImGui::CalcTextSize(expanded.c_str()).x;
|
||||||
|
if (line_w > max_width_px_)
|
||||||
|
max_width_px_ = line_w;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// After the visible-range loop, advance the layout cursor to the end of
|
||||||
|
// the (virtual) content so ImGui sees the correct total size. Vertical
|
||||||
|
// height comes from total_rows; horizontal width comes from the cached
|
||||||
|
// max line width. A Dummy at the final position records both.
|
||||||
|
if (total_rows > last_vis) {
|
||||||
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() +
|
||||||
|
static_cast<float>(total_rows - last_vis) * row_h);
|
||||||
|
}
|
||||||
|
if (max_width_px_ > 0.0f) {
|
||||||
|
ImGui::SetCursorPosX(max_width_px_);
|
||||||
|
}
|
||||||
|
ImGui::Dummy(ImVec2(0, 0));
|
||||||
// Synchronize cursor and scrolling after rendering all lines so content size is known.
|
// Synchronize cursor and scrolling after rendering all lines so content size is known.
|
||||||
{
|
{
|
||||||
float child_h_actual = ImGui::GetWindowHeight();
|
float child_h_actual = ImGui::GetWindowHeight();
|
||||||
@@ -545,29 +605,40 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
last_row = first_row + vis_rows - 1;
|
last_row = first_row + vis_rows - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal scroll: ensure cursor column is visible
|
// Horizontal scroll: ensure cursor is visible (pixel-based for proportional fonts)
|
||||||
long vis_cols = static_cast<long>(std::round(child_w_actual / space_w));
|
float cursor_px_abs = 0.0f;
|
||||||
if (vis_cols < 1)
|
|
||||||
vis_cols = 1;
|
|
||||||
long first_col = static_cast<long>(scroll_x_now / space_w);
|
|
||||||
long last_col = first_col + vis_cols - 1;
|
|
||||||
|
|
||||||
std::size_t cursor_rx = 0;
|
|
||||||
if (cy < lines.size()) {
|
if (cy < lines.size()) {
|
||||||
std::string cur_line = static_cast<std::string>(lines[cy]);
|
std::string cur_line = static_cast<std::string>(lines[cy]);
|
||||||
const std::size_t tabw = 8;
|
const std::size_t tabw = 8;
|
||||||
for (std::size_t i = 0; i < cx && i < cur_line.size(); ++i) {
|
// Expand tabs for cursor line to measure pixel position
|
||||||
if (cur_line[i] == '\t') {
|
std::string cur_expanded;
|
||||||
cursor_rx += tabw - (cursor_rx % tabw);
|
cur_expanded.reserve(cur_line.size() + 16);
|
||||||
|
std::size_t cur_rx = 0;
|
||||||
|
for (std::size_t ci = 0; ci < cur_line.size(); ++ci) {
|
||||||
|
if (cur_line[ci] == '\t') {
|
||||||
|
std::size_t adv = tabw - (cur_rx % tabw);
|
||||||
|
cur_expanded.append(adv, ' ');
|
||||||
|
cur_rx += adv;
|
||||||
} else {
|
} else {
|
||||||
|
cur_expanded.push_back(cur_line[ci]);
|
||||||
|
cur_rx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Compute rendered column of cursor
|
||||||
|
std::size_t cursor_rx = 0;
|
||||||
|
for (std::size_t ci = 0; ci < cx && ci < cur_line.size(); ++ci) {
|
||||||
|
if (cur_line[ci] == '\t')
|
||||||
|
cursor_rx += tabw - (cursor_rx % tabw);
|
||||||
|
else
|
||||||
cursor_rx += 1;
|
cursor_rx += 1;
|
||||||
}
|
}
|
||||||
|
std::size_t exp_end = std::min(cur_expanded.size(), cursor_rx);
|
||||||
|
if (exp_end > 0)
|
||||||
|
cursor_px_abs = ImGui::CalcTextSize(cur_expanded.c_str(),
|
||||||
|
cur_expanded.c_str() + exp_end).x;
|
||||||
}
|
}
|
||||||
}
|
if (cursor_px_abs < scroll_x_now || cursor_px_abs > scroll_x_now + child_w_actual) {
|
||||||
long cxr = static_cast<long>(cursor_rx);
|
float target_x = cursor_px_abs - (child_w_actual / 2.0f);
|
||||||
if (cxr < first_col || cxr > last_col) {
|
|
||||||
float target_x = static_cast<float>(cxr) * space_w;
|
|
||||||
target_x -= (child_w_actual / 2.0f);
|
|
||||||
if (target_x < 0.f)
|
if (target_x < 0.f)
|
||||||
target_x = 0.f;
|
target_x = 0.f;
|
||||||
float max_x = ImGui::GetScrollMaxX();
|
float max_x = ImGui::GetScrollMaxX();
|
||||||
|
|||||||
@@ -2,8 +2,13 @@
|
|||||||
* ImGuiRenderer - ImGui-based renderer for GUI mode
|
* ImGuiRenderer - ImGui-based renderer for GUI mode
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
#include "Renderer.h"
|
#include "Renderer.h"
|
||||||
|
|
||||||
|
struct ImFont;
|
||||||
|
class Buffer;
|
||||||
|
|
||||||
class ImGuiRenderer final : public Renderer {
|
class ImGuiRenderer final : public Renderer {
|
||||||
public:
|
public:
|
||||||
ImGuiRenderer() = default;
|
ImGuiRenderer() = default;
|
||||||
@@ -11,4 +16,22 @@ public:
|
|||||||
~ImGuiRenderer() override = default;
|
~ImGuiRenderer() override = default;
|
||||||
|
|
||||||
void Draw(Editor &ed) override;
|
void Draw(Editor &ed) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Per-window scroll tracking for two-way sync between Buffer offsets and ImGui scroll.
|
||||||
|
// These must be per-instance (not static) so each window maintains independent state.
|
||||||
|
long prev_buf_rowoffs_ = -1;
|
||||||
|
long prev_buf_coloffs_ = -1;
|
||||||
|
float prev_scroll_y_ = -1.0f;
|
||||||
|
float prev_scroll_x_ = -1.0f;
|
||||||
|
bool mouse_selecting_ = false;
|
||||||
|
|
||||||
|
// Max-line-width cache for the horizontal scrollbar. Measuring every line
|
||||||
|
// every frame is prohibitively expensive on large files; we only update the
|
||||||
|
// running max from visible lines and reset when buffer/version/font changes.
|
||||||
|
const Buffer *max_width_buf_ = nullptr;
|
||||||
|
std::uint64_t max_width_version_ = 0;
|
||||||
|
ImFont *max_width_font_ = nullptr;
|
||||||
|
float max_width_font_size_ = 0.0f;
|
||||||
|
float max_width_px_ = 0.0f;
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-5
@@ -1,6 +1,4 @@
|
|||||||
#include <iostream>
|
|
||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
#include <ostream>
|
|
||||||
|
|
||||||
#include "KKeymap.h"
|
#include "KKeymap.h"
|
||||||
|
|
||||||
@@ -40,6 +38,15 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
out = CommandId::ToggleReadOnly; // C-k ' (toggle read-only)
|
out = CommandId::ToggleReadOnly; // C-k ' (toggle read-only)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (ascii_key == 'E') {
|
||||||
|
// Explicitly rejected and kept distinct from 'e' (OpenFileStart): the
|
||||||
|
// switch below operates on the lowercased key, so it can't otherwise
|
||||||
|
// tell 'E' and 'e' apart. Return false and let the caller show its
|
||||||
|
// normal "unknown k-command" status-line message - curses already
|
||||||
|
// owns the terminal here, so writing to stderr would corrupt the
|
||||||
|
// screen.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
switch (k_lower) {
|
switch (k_lower) {
|
||||||
case 'a':
|
case 'a':
|
||||||
@@ -66,9 +73,6 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
case 'e':
|
case 'e':
|
||||||
out = CommandId::OpenFileStart;
|
out = CommandId::OpenFileStart;
|
||||||
return true;
|
return true;
|
||||||
case 'E':
|
|
||||||
std::cerr << "E is not a valid command" << std::endl;
|
|
||||||
return false;
|
|
||||||
case 'f':
|
case 'f':
|
||||||
out = CommandId::FlushKillRing;
|
out = CommandId::FlushKillRing;
|
||||||
return true;
|
return true;
|
||||||
@@ -84,6 +88,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
case 'l':
|
case 'l':
|
||||||
out = CommandId::ReloadBuffer;
|
out = CommandId::ReloadBuffer;
|
||||||
return true;
|
return true;
|
||||||
|
case 'm':
|
||||||
|
out = CommandId::ToggleEditMode;
|
||||||
|
return true;
|
||||||
case 'n':
|
case 'n':
|
||||||
out = CommandId::BufferPrev;
|
out = CommandId::BufferPrev;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* PasteSplit.h - split pasted/clipboard text into editor commands
|
||||||
|
*
|
||||||
|
* Pure logic (no SDL/GUI dependencies) so it can be unit tested and shared
|
||||||
|
* across frontends. Line breaks in the pasted text must become Newline
|
||||||
|
* commands; InsertText itself rejects embedded '\r'/'\n' (see Command.cc).
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "InputHandler.h"
|
||||||
|
|
||||||
|
|
||||||
|
// Translate a block of pasted text into a sequence of editor commands.
|
||||||
|
//
|
||||||
|
// Any of "\n", "\r\n", or a bare "\r" is treated as a single line break and
|
||||||
|
// emitted as a Newline command; the text between breaks becomes InsertText
|
||||||
|
// commands. Empty text segments are skipped, but line breaks are always
|
||||||
|
// emitted so blank lines round-trip correctly.
|
||||||
|
inline std::vector<MappedInput> SplitPasteIntoCommands(const std::string &text)
|
||||||
|
{
|
||||||
|
std::vector<MappedInput> out;
|
||||||
|
std::string segment;
|
||||||
|
|
||||||
|
auto flush_segment = [&]() {
|
||||||
|
if (!segment.empty()) {
|
||||||
|
out.push_back(MappedInput{true, CommandId::InsertText, segment, 0});
|
||||||
|
segment.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < text.size(); ++i) {
|
||||||
|
const char c = text[i];
|
||||||
|
if (c == '\n' || c == '\r') {
|
||||||
|
flush_segment();
|
||||||
|
out.push_back(MappedInput{true, CommandId::Newline, std::string(), 0});
|
||||||
|
// Collapse a "\r\n" pair into one line break.
|
||||||
|
if (c == '\r' && i + 1 < text.size() && text[i + 1] == '\n')
|
||||||
|
++i;
|
||||||
|
} else {
|
||||||
|
segment.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flush_segment();
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -268,6 +268,43 @@ SwapManager::RecorderFor(Buffer *buf)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SwapRecorder *
|
||||||
|
SwapManager::Rehome(Buffer *old_addr, Buffer *new_addr)
|
||||||
|
{
|
||||||
|
if (!old_addr || !new_addr || old_addr == new_addr)
|
||||||
|
return nullptr;
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
SwapRecorder *result = nullptr;
|
||||||
|
|
||||||
|
auto jit = journals_.find(old_addr);
|
||||||
|
if (jit != journals_.end()) {
|
||||||
|
JournalCtx ctx = std::move(jit->second);
|
||||||
|
journals_.erase(jit);
|
||||||
|
journals_[new_addr] = std::move(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto rit = recorders_.find(old_addr);
|
||||||
|
if (rit != recorders_.end()) {
|
||||||
|
recorders_.erase(rit);
|
||||||
|
// BufferRecorder binds a Buffer& at construction, so it can't be
|
||||||
|
// repointed in place; rebuild it against the buffer's new address.
|
||||||
|
auto rec = std::make_unique<BufferRecorder>(*this, *new_addr);
|
||||||
|
result = rec.get();
|
||||||
|
recorders_[new_addr] = std::move(rec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defensive: any record still queued (not yet drained by the writer thread)
|
||||||
|
// for the old address must follow the buffer to its new location. Callers
|
||||||
|
// are expected to Flush() before rehoming so this should normally be a no-op.
|
||||||
|
for (auto &p: queue_) {
|
||||||
|
if (p.buf == old_addr)
|
||||||
|
p.buf = new_addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
SwapManager::Attach(Buffer *buf)
|
SwapManager::Attach(Buffer *buf)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -94,6 +94,15 @@ public:
|
|||||||
// Detach(buf) or SwapManager destruction.
|
// Detach(buf) or SwapManager destruction.
|
||||||
SwapRecorder *RecorderFor(Buffer *buf);
|
SwapRecorder *RecorderFor(Buffer *buf);
|
||||||
|
|
||||||
|
// Re-key an attached buffer's journal/recorder entries after its Buffer object
|
||||||
|
// has moved to a new address (e.g. std::vector<Buffer> reallocation/erase-shift).
|
||||||
|
// Callers must ensure no swap records for old_addr are in flight (see Flush())
|
||||||
|
// before calling this, and must not call it with an address that isn't
|
||||||
|
// currently attached. Returns the recorder for new_addr (nullptr if old_addr
|
||||||
|
// wasn't attached); the caller is responsible for calling
|
||||||
|
// new_buf->SetSwapRecorder() with the result.
|
||||||
|
SwapRecorder *Rehome(Buffer *old_addr, Buffer *new_addr);
|
||||||
|
|
||||||
// Notify that the buffer's filename changed (e.g., SaveAs)
|
// Notify that the buffer's filename changed (e.g., SaveAs)
|
||||||
void NotifyFilenameChanged(Buffer &buf);
|
void NotifyFilenameChanged(Buffer &buf);
|
||||||
|
|
||||||
|
|||||||
+45
-10
@@ -1,4 +1,6 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <climits>
|
||||||
|
#include <cwchar>
|
||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
|
|
||||||
#include "TerminalInputHandler.h"
|
#include "TerminalInputHandler.h"
|
||||||
@@ -12,6 +14,22 @@ CTRL(char c)
|
|||||||
{
|
{
|
||||||
return c & 0x1F;
|
return c & 0x1F;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Encode a single wide character in the process locale's multibyte encoding
|
||||||
|
// (UTF-8, given main.cc's setlocale(LC_ALL, "")). Returns false if the
|
||||||
|
// codepoint can't be represented, leaving `out` untouched.
|
||||||
|
bool
|
||||||
|
wchar_to_mb(wchar_t wc, std::string &out)
|
||||||
|
{
|
||||||
|
std::mbstate_t state{};
|
||||||
|
char buf[MB_LEN_MAX];
|
||||||
|
std::size_t n = std::wcrtomb(buf, wc, &state);
|
||||||
|
if (n == static_cast<std::size_t>(-1))
|
||||||
|
return false;
|
||||||
|
out.assign(buf, n);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TerminalInputHandler::TerminalInputHandler() = default;
|
TerminalInputHandler::TerminalInputHandler() = default;
|
||||||
@@ -21,6 +39,7 @@ TerminalInputHandler::~TerminalInputHandler() = default;
|
|||||||
|
|
||||||
static bool
|
static bool
|
||||||
map_key_to_command(const int ch,
|
map_key_to_command(const int ch,
|
||||||
|
const bool is_keycode,
|
||||||
bool &k_prefix,
|
bool &k_prefix,
|
||||||
bool &esc_meta,
|
bool &esc_meta,
|
||||||
bool &k_ctrl_pending,
|
bool &k_ctrl_pending,
|
||||||
@@ -28,9 +47,13 @@ map_key_to_command(const int ch,
|
|||||||
Editor *ed,
|
Editor *ed,
|
||||||
MappedInput &out)
|
MappedInput &out)
|
||||||
{
|
{
|
||||||
// Handle special keys from ncurses
|
// Handle special keys from ncurses. These are only meaningful when
|
||||||
|
// get_wch() reported KEY_CODE_YES: a regular (possibly non-ASCII) wide
|
||||||
|
// character can numerically collide with a KEY_* constant otherwise
|
||||||
|
// (e.g. U+0107 'ć' equals KEY_BACKSPACE's value), which would wrongly
|
||||||
|
// swallow it as a special key instead of inserting it.
|
||||||
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
|
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
|
||||||
switch (ch) {
|
switch (is_keycode ? ch : -1) {
|
||||||
case KEY_ENTER:
|
case KEY_ENTER:
|
||||||
// Some terminals send KEY_ENTER distinct from '\n'/'\r'
|
// Some terminals send KEY_ENTER distinct from '\n'/'\r'
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
@@ -259,7 +282,7 @@ map_key_to_command(const int ch,
|
|||||||
esc_meta = false;
|
esc_meta = false;
|
||||||
int ascii_key = ch;
|
int ascii_key = ch;
|
||||||
// Handle ESC + BACKSPACE (meta-backspace, Alt-Backspace)
|
// Handle ESC + BACKSPACE (meta-backspace, Alt-Backspace)
|
||||||
if (ch == KEY_BACKSPACE || ch == 127 || ch == CTRL('H')) {
|
if ((is_keycode && ch == KEY_BACKSPACE) || ch == 127 || ch == CTRL('H')) {
|
||||||
ascii_key = KEY_BACKSPACE; // normalized value for lookup
|
ascii_key = KEY_BACKSPACE; // normalized value for lookup
|
||||||
} else if (ch == ',') {
|
} else if (ch == ',') {
|
||||||
// Some terminals emit ',' when Shift state is lost after ESC; treat as '<'
|
// Some terminals emit ',' when Shift state is lost after ESC; treat as '<'
|
||||||
@@ -281,7 +304,7 @@ map_key_to_command(const int ch,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Backspace in ncurses can be KEY_BACKSPACE or 127
|
// Backspace in ncurses can be KEY_BACKSPACE or 127
|
||||||
if (ch == KEY_BACKSPACE || ch == 127 || ch == CTRL('H')) {
|
if ((is_keycode && ch == KEY_BACKSPACE) || ch == 127 || ch == CTRL('H')) {
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
k_ctrl_pending = false;
|
k_ctrl_pending = false;
|
||||||
out = {true, CommandId::Backspace, "", 0};
|
out = {true, CommandId::Backspace, "", 0};
|
||||||
@@ -297,11 +320,20 @@ map_key_to_command(const int ch,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Printable ASCII
|
// Printable character: ASCII, or (for a regular decoded wide character,
|
||||||
if (ch >= 0x20 && ch <= 0x7E) {
|
// not a keycode) any other printable Unicode codepoint - e.g. accented
|
||||||
|
// Latin, Cyrillic, CJK, etc.
|
||||||
|
if (!is_keycode && ch >= 0x20 && ch != 0x7F) {
|
||||||
|
std::string mb;
|
||||||
|
if (ch <= 0x7E) {
|
||||||
|
mb.assign(1, static_cast<char>(ch));
|
||||||
|
} else if (!wchar_to_mb(static_cast<wchar_t>(ch), mb)) {
|
||||||
|
out.hasCommand = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
out.hasCommand = true;
|
out.hasCommand = true;
|
||||||
out.id = CommandId::InsertText;
|
out.id = CommandId::InsertText;
|
||||||
out.arg.assign(1, static_cast<char>(ch));
|
out.arg = mb;
|
||||||
out.count = 0;
|
out.count = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -314,12 +346,15 @@ map_key_to_command(const int ch,
|
|||||||
bool
|
bool
|
||||||
TerminalInputHandler::decode_(MappedInput &out)
|
TerminalInputHandler::decode_(MappedInput &out)
|
||||||
{
|
{
|
||||||
int ch = getch();
|
wint_t wch;
|
||||||
if (ch == ERR) {
|
int ret = get_wch(&wch);
|
||||||
|
if (ret == ERR) {
|
||||||
return false; // no input
|
return false; // no input
|
||||||
}
|
}
|
||||||
|
const bool is_keycode = (ret == KEY_CODE_YES);
|
||||||
|
const int ch = static_cast<int>(wch);
|
||||||
bool consumed = map_key_to_command(
|
bool consumed = map_key_to_command(
|
||||||
ch,
|
ch, is_keycode,
|
||||||
k_prefix_, esc_meta_,
|
k_prefix_, esc_meta_,
|
||||||
k_ctrl_pending_,
|
k_ctrl_pending_,
|
||||||
mouse_selecting_,
|
mouse_selecting_,
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ enum class UndoType : std::uint8_t {
|
|||||||
Newline,
|
Newline,
|
||||||
DeleteRow,
|
DeleteRow,
|
||||||
InsertRow,
|
InsertRow,
|
||||||
|
// Inverse of Newline: forward = join_lines(row) [removes the newline at the
|
||||||
|
// end of `row`], backward = split_line(row, col) [recreates the original
|
||||||
|
// two lines]. Used by backspace-at-col-0 and delete-at-eol, whose forward
|
||||||
|
// action is a join, not a split — Newline's apply() semantics are the
|
||||||
|
// wrong direction for those.
|
||||||
|
JoinLines,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UndoNode {
|
struct UndoNode {
|
||||||
|
|||||||
+14
-1
@@ -37,7 +37,7 @@ UndoSystem::Begin(UndoType type)
|
|||||||
|
|
||||||
// Some operations should always be standalone undo steps.
|
// Some operations should always be standalone undo steps.
|
||||||
const bool always_standalone = (type == UndoType::Newline || type == UndoType::DeleteRow || type ==
|
const bool always_standalone = (type == UndoType::Newline || type == UndoType::DeleteRow || type ==
|
||||||
UndoType::InsertRow);
|
UndoType::InsertRow || type == UndoType::JoinLines);
|
||||||
if (always_standalone) {
|
if (always_standalone) {
|
||||||
commit();
|
commit();
|
||||||
}
|
}
|
||||||
@@ -77,6 +77,7 @@ UndoSystem::Begin(UndoType type)
|
|||||||
case UndoType::Newline:
|
case UndoType::Newline:
|
||||||
case UndoType::DeleteRow:
|
case UndoType::DeleteRow:
|
||||||
case UndoType::InsertRow:
|
case UndoType::InsertRow:
|
||||||
|
case UndoType::JoinLines:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,6 +326,16 @@ UndoSystem::apply(const UndoNode *node, int direction)
|
|||||||
buf_->SetCursor(0, static_cast<std::size_t>(node->row));
|
buf_->SetCursor(0, static_cast<std::size_t>(node->row));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case UndoType::JoinLines:
|
||||||
|
// Mirror image of Newline: forward removes a newline, backward restores it.
|
||||||
|
if (direction > 0) {
|
||||||
|
buf_->join_lines(node->row);
|
||||||
|
buf_->SetCursor(static_cast<std::size_t>(node->col), static_cast<std::size_t>(node->row));
|
||||||
|
} else {
|
||||||
|
buf_->split_line(node->row, node->col);
|
||||||
|
buf_->SetCursor(0, static_cast<std::size_t>(node->row + 1));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,6 +435,8 @@ UndoSystem::type_str(UndoType t)
|
|||||||
return "DeleteRow";
|
return "DeleteRow";
|
||||||
case UndoType::InsertRow:
|
case UndoType::InsertRow:
|
||||||
return "InsertRow";
|
return "InsertRow";
|
||||||
|
case UndoType::JoinLines:
|
||||||
|
return "JoinLines";
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-2
@@ -1,3 +1,24 @@
|
|||||||
// Placeholder translation unit for UndoTree struct definition.
|
// Undo logic is implemented in UndoSystem; this file only owns node lifetime.
|
||||||
// Undo logic is implemented in UndoSystem.
|
|
||||||
#include "UndoTree.h"
|
#include "UndoTree.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void
|
||||||
|
free_node_graph(UndoNode *node)
|
||||||
|
{
|
||||||
|
// Walk the sibling (redo-branch) list; for each node, recursively free its
|
||||||
|
// child subtree first, then the node itself.
|
||||||
|
while (node) {
|
||||||
|
UndoNode *next = node->next;
|
||||||
|
free_node_graph(node->child);
|
||||||
|
delete node;
|
||||||
|
node = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
UndoTree::~UndoTree()
|
||||||
|
{
|
||||||
|
free_node_graph(root);
|
||||||
|
delete pending;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,4 +7,11 @@ struct UndoTree {
|
|||||||
UndoNode *current = nullptr; // current state of buffer
|
UndoNode *current = nullptr; // current state of buffer
|
||||||
UndoNode *saved = nullptr; // points to node matching last save (for dirty flag)
|
UndoNode *saved = nullptr; // points to node matching last save (for dirty flag)
|
||||||
UndoNode *pending = nullptr; // in-progress batch (detached)
|
UndoNode *pending = nullptr; // in-progress batch (detached)
|
||||||
|
|
||||||
|
// Frees the entire node graph (root's subtree/branches plus any detached
|
||||||
|
// pending node). `current`/`saved` are aliases into that same graph, not
|
||||||
|
// separately owned. Without this, closing/reloading/resetting a buffer
|
||||||
|
// (which replaces or destroys its UndoTree) leaks every UndoNode ever
|
||||||
|
// created for it.
|
||||||
|
~UndoTree();
|
||||||
};
|
};
|
||||||
|
|||||||
+44
-13
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
pkgs ? import <nixpkgs> {},
|
lib,
|
||||||
lib ? pkgs.lib,
|
|
||||||
stdenv,
|
stdenv,
|
||||||
cmake,
|
cmake,
|
||||||
ncurses,
|
ncurses,
|
||||||
@@ -10,9 +9,10 @@
|
|||||||
kdePackages,
|
kdePackages,
|
||||||
qt6Packages ? kdePackages.qt6Packages,
|
qt6Packages ? kdePackages.qt6Packages,
|
||||||
installShellFiles,
|
installShellFiles,
|
||||||
|
copyDesktopItems,
|
||||||
|
makeDesktopItem,
|
||||||
graphical ? false,
|
graphical ? false,
|
||||||
graphical-qt ? false,
|
graphical-qt ? false,
|
||||||
...
|
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cmakeContent = builtins.readFile ./CMakeLists.txt;
|
cmakeContent = builtins.readFile ./CMakeLists.txt;
|
||||||
@@ -23,25 +23,29 @@ let
|
|||||||
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
|
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
|
||||||
in
|
in
|
||||||
stdenv.mkDerivation {
|
stdenv.mkDerivation {
|
||||||
pname = "kte";
|
pname = if graphical then (if graphical-qt then "kge-qt" else "kge") else "kte";
|
||||||
inherit version;
|
inherit version;
|
||||||
|
|
||||||
src = lib.cleanSource ./.;
|
src = lib.cleanSource ./.;
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
cmake
|
cmake
|
||||||
ncurses
|
|
||||||
installShellFiles
|
installShellFiles
|
||||||
]
|
] ++ lib.optionals graphical [
|
||||||
++ lib.optionals graphical [
|
copyDesktopItems
|
||||||
|
] ++ lib.optionals graphical-qt [
|
||||||
|
qt6Packages.wrapQtAppsHook
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
ncurses
|
||||||
|
] ++ lib.optionals graphical [
|
||||||
SDL2
|
SDL2
|
||||||
libGL
|
libGL
|
||||||
xorg.libX11
|
xorg.libX11
|
||||||
]
|
] ++ lib.optionals graphical-qt [
|
||||||
++ lib.optionals graphical-qt [
|
|
||||||
kdePackages.qt6ct
|
kdePackages.qt6ct
|
||||||
qt6Packages.qtbase
|
qt6Packages.qtbase
|
||||||
qt6Packages.wrapQtAppsHook
|
|
||||||
];
|
];
|
||||||
|
|
||||||
cmakeFlags = [
|
cmakeFlags = [
|
||||||
@@ -51,6 +55,30 @@ stdenv.mkDerivation {
|
|||||||
"-DKTE_STATIC_LINK=OFF"
|
"-DKTE_STATIC_LINK=OFF"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
desktopItems = lib.optionals graphical [
|
||||||
|
(makeDesktopItem {
|
||||||
|
name = "kge";
|
||||||
|
desktopName = "kge";
|
||||||
|
genericName = "Text Editor";
|
||||||
|
comment = "kyle's graphical text editor";
|
||||||
|
exec = if graphical-qt then "kge-qt %F" else "kge %F";
|
||||||
|
icon = "kge";
|
||||||
|
terminal = false;
|
||||||
|
categories = [ "Utility" "TextEditor" "Development" ];
|
||||||
|
mimeTypes = [
|
||||||
|
"text/plain"
|
||||||
|
"text/x-c"
|
||||||
|
"text/x-c++"
|
||||||
|
"text/x-python"
|
||||||
|
"text/x-go"
|
||||||
|
"text/x-rust"
|
||||||
|
"application/json"
|
||||||
|
"text/markdown"
|
||||||
|
"text/x-shellscript"
|
||||||
|
];
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
|
|
||||||
@@ -59,14 +87,11 @@ stdenv.mkDerivation {
|
|||||||
installManPage ../docs/kte.1
|
installManPage ../docs/kte.1
|
||||||
|
|
||||||
${lib.optionalString graphical ''
|
${lib.optionalString graphical ''
|
||||||
mkdir -p $out/bin
|
|
||||||
|
|
||||||
${if graphical-qt then ''
|
${if graphical-qt then ''
|
||||||
cp kge $out/bin/kge-qt
|
cp kge $out/bin/kge-qt
|
||||||
'' else ''
|
'' else ''
|
||||||
cp kge $out/bin/kge
|
cp kge $out/bin/kge
|
||||||
''}
|
''}
|
||||||
|
|
||||||
installManPage ../docs/kge.1
|
installManPage ../docs/kge.1
|
||||||
|
|
||||||
mkdir -p $out/share/icons/hicolor/256x256/apps
|
mkdir -p $out/share/icons/hicolor/256x256/apps
|
||||||
@@ -75,4 +100,10 @@ stdenv.mkDerivation {
|
|||||||
|
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "kyle's text editor" + lib.optionalString graphical " (graphical)";
|
||||||
|
platforms = lib.platforms.unix;
|
||||||
|
mainProgram = if graphical then (if graphical-qt then "kge-qt" else "kge") else "kte";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-4
@@ -23,29 +23,34 @@ Current themes (alphabetically):
|
|||||||
- **gruvbox** — Retro groove color scheme (light/dark variants)
|
- **gruvbox** — Retro groove color scheme (light/dark variants)
|
||||||
- **kanagawa-paper** — Inspired by traditional Japanese art
|
- **kanagawa-paper** — Inspired by traditional Japanese art
|
||||||
- **lcars** — Star Trek LCARS interface style
|
- **lcars** — Star Trek LCARS interface style
|
||||||
|
- **leuchtturm** — Modern, clean theme (light/dark variants)
|
||||||
- **nord** — Arctic, north-bluish color palette
|
- **nord** — Arctic, north-bluish color palette
|
||||||
- **old-book** — Sepia-toned vintage book aesthetic (light/dark
|
- **old-book** — Sepia-toned vintage book aesthetic (light/dark
|
||||||
variants)
|
variants)
|
||||||
- **orbital** — Space-themed dark palette
|
- **orbital** — Space-themed dark palette
|
||||||
- **plan9** — Minimalist Plan 9 from Bell Labs inspired
|
- **plan9** — Minimalist Plan 9 from Bell Labs inspired
|
||||||
- **solarized** — Ethan Schoonover's Solarized (light/dark variants)
|
- **solarized** — Ethan Schoonover's Solarized (light/dark variants)
|
||||||
|
- **tufte** — Edward Tufte-inspired minimalist theme (light/dark variants)
|
||||||
- **weyland-yutani** — Alien franchise corporate aesthetic
|
- **weyland-yutani** — Alien franchise corporate aesthetic
|
||||||
- **zenburn** — Low-contrast, easy-on-the-eyes theme
|
- **zenburn** — Low-contrast, easy-on-the-eyes theme
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Themes are configured via `$HOME/.config/kte/kge.ini`:
|
Themes are configured via `$HOME/.config/kte/kge.toml`:
|
||||||
|
|
||||||
```ini
|
```toml
|
||||||
theme = nord
|
[appearance]
|
||||||
background = dark
|
theme = "nord"
|
||||||
|
background = "dark"
|
||||||
```
|
```
|
||||||
|
|
||||||
- `theme` — The theme name (e.g., "nord", "gruvbox", "solarized")
|
- `theme` — The theme name (e.g., "nord", "gruvbox", "solarized")
|
||||||
- `background` — Either "dark" or "light" (for themes supporting both
|
- `background` — Either "dark" or "light" (for themes supporting both
|
||||||
variants)
|
variants)
|
||||||
|
|
||||||
|
Legacy `kge.ini` format is also supported (see CONFIG.md).
|
||||||
|
|
||||||
Themes can also be switched at runtime using the `:theme <name>`
|
Themes can also be switched at runtime using the `:theme <name>`
|
||||||
command.
|
command.
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,7 @@
|
|||||||
|
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
outputs =
|
outputs = { self, nixpkgs, ... }:
|
||||||
inputs@{ self, nixpkgs, ... }:
|
|
||||||
let
|
let
|
||||||
eachSystem = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
|
eachSystem = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
|
||||||
pkgsFor = system: import nixpkgs { inherit system; };
|
pkgsFor = system: import nixpkgs { inherit system; };
|
||||||
@@ -17,5 +16,27 @@
|
|||||||
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; graphical-qt = false; };
|
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; graphical-qt = false; };
|
||||||
qt = (pkgsFor system).callPackage ./default.nix { graphical = true; graphical-qt = true; };
|
qt = (pkgsFor system).callPackage ./default.nix { graphical = true; graphical-qt = true; };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
devShells = eachSystem (system:
|
||||||
|
let pkgs = pkgsFor system;
|
||||||
|
in {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
inputsFrom = [ self.packages.${system}.kge ];
|
||||||
|
packages = with pkgs; [ gdb valgrind ];
|
||||||
|
};
|
||||||
|
terminal = pkgs.mkShell {
|
||||||
|
inputsFrom = [ self.packages.${system}.kte ];
|
||||||
|
};
|
||||||
|
qt = pkgs.mkShell {
|
||||||
|
inputsFrom = [ self.packages.${system}.qt ];
|
||||||
|
packages = with pkgs; [ gdb valgrind ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
overlays.default = final: prev: {
|
||||||
|
kte = self.packages.${final.system}.kte;
|
||||||
|
kge = self.packages.${final.system}.kge;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+1768
File diff suppressed because it is too large
Load Diff
+1203
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@
|
|||||||
#include "BerkeleyMono.h"
|
#include "BerkeleyMono.h"
|
||||||
#include "BrassMono.h"
|
#include "BrassMono.h"
|
||||||
#include "BrassMonoCode.h"
|
#include "BrassMonoCode.h"
|
||||||
|
#include "CrimsonPro.h"
|
||||||
|
#include "ETBook.h"
|
||||||
#include "FiraCode.h"
|
#include "FiraCode.h"
|
||||||
#include "Go.h"
|
#include "Go.h"
|
||||||
#include "IBMPlexMono.h"
|
#include "IBMPlexMono.h"
|
||||||
@@ -13,6 +15,7 @@
|
|||||||
#include "IosevkaExtended.h"
|
#include "IosevkaExtended.h"
|
||||||
#include "ShareTech.h"
|
#include "ShareTech.h"
|
||||||
#include "SpaceMono.h"
|
#include "SpaceMono.h"
|
||||||
|
#include "Spectral.h"
|
||||||
#include "Syne.h"
|
#include "Syne.h"
|
||||||
#include "Triplicate.h"
|
#include "Triplicate.h"
|
||||||
#include "Unispace.h"
|
#include "Unispace.h"
|
||||||
|
|||||||
@@ -45,6 +45,16 @@ InstallDefaultFonts()
|
|||||||
BrassMonoCode::DefaultFontBoldCompressedData,
|
BrassMonoCode::DefaultFontBoldCompressedData,
|
||||||
BrassMonoCode::DefaultFontBoldCompressedSize
|
BrassMonoCode::DefaultFontBoldCompressedSize
|
||||||
));
|
));
|
||||||
|
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||||
|
"crimsonpro",
|
||||||
|
CrimsonPro::DefaultFontRegularCompressedData,
|
||||||
|
CrimsonPro::DefaultFontRegularCompressedSize
|
||||||
|
));
|
||||||
|
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||||
|
"etbook",
|
||||||
|
ETBook::DefaultFontRegularCompressedData,
|
||||||
|
ETBook::DefaultFontRegularCompressedSize
|
||||||
|
));
|
||||||
FontRegistry::Instance().Register(std::make_unique<Font>(
|
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||||
"fira",
|
"fira",
|
||||||
FiraCode::DefaultFontRegularCompressedData,
|
FiraCode::DefaultFontRegularCompressedData,
|
||||||
@@ -95,6 +105,11 @@ InstallDefaultFonts()
|
|||||||
SpaceMono::DefaultFontRegularCompressedData,
|
SpaceMono::DefaultFontRegularCompressedData,
|
||||||
SpaceMono::DefaultFontRegularCompressedSize
|
SpaceMono::DefaultFontRegularCompressedSize
|
||||||
));
|
));
|
||||||
|
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||||
|
"spectral",
|
||||||
|
Spectral::DefaultFontRegularCompressedData,
|
||||||
|
Spectral::DefaultFontRegularCompressedSize
|
||||||
|
));
|
||||||
FontRegistry::Instance().Register(std::make_unique<Font>(
|
FontRegistry::Instance().Register(std::make_unique<Font>(
|
||||||
"syne",
|
"syne",
|
||||||
Syne::DefaultFontRegularCompressedData,
|
Syne::DefaultFontRegularCompressedData,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "Font.h"
|
#include "Font.h"
|
||||||
|
|
||||||
@@ -87,6 +89,19 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Return all registered font names (sorted)
|
||||||
|
std::vector<std::string> FontNames() const
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
std::vector<std::string> names;
|
||||||
|
names.reserve(fonts_.size());
|
||||||
|
for (const auto &[name, _] : fonts_)
|
||||||
|
names.push_back(name);
|
||||||
|
std::sort(names.begin(), names.end());
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Current font name/size as last successfully loaded via LoadFont()
|
// Current font name/size as last successfully loaded via LoadFont()
|
||||||
std::string CurrentFontName() const
|
std::string CurrentFontName() const
|
||||||
{
|
{
|
||||||
|
|||||||
+3227
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
|||||||
|
# kge configuration
|
||||||
|
# Place at ~/.config/kte/kge.toml
|
||||||
|
|
||||||
|
[window]
|
||||||
|
fullscreen = false
|
||||||
|
columns = 80
|
||||||
|
rows = 42
|
||||||
|
|
||||||
|
[font]
|
||||||
|
# Default font and size
|
||||||
|
name = "default"
|
||||||
|
size = 18.0
|
||||||
|
# Font used in code mode (monospace)
|
||||||
|
code = "default"
|
||||||
|
# Font used in writing mode (proportional) — for .txt, .md, .rst, .org, .tex, etc.
|
||||||
|
writing = "crimsonpro"
|
||||||
|
|
||||||
|
[appearance]
|
||||||
|
theme = "nord"
|
||||||
|
# "dark" or "light" for themes with variants
|
||||||
|
background = "dark"
|
||||||
|
|
||||||
|
[editor]
|
||||||
|
syntax = true
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <random>
|
#include <random>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
@@ -255,7 +256,18 @@ main(int argc, char *argv[])
|
|||||||
// Fall through: not a +number, treat as filename starting with '+'
|
// Fall through: not a +number, treat as filename starting with '+'
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string path = arg;
|
// Resolve to absolute path now, before any
|
||||||
|
// chdir (macOS GUI changes CWD to HOME before
|
||||||
|
// deferred opens are processed).
|
||||||
|
std::string path = arg;
|
||||||
|
try {
|
||||||
|
std::filesystem::path p(path);
|
||||||
|
if (p.is_relative()) {
|
||||||
|
path = std::filesystem::absolute(p).string();
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
// Fall through with original path
|
||||||
|
}
|
||||||
editor.RequestOpenFile(path, pending_line);
|
editor.RequestOpenFile(path, pending_line);
|
||||||
pending_line = 0; // consumed (if set)
|
pending_line = 0; // consumed (if set)
|
||||||
}
|
}
|
||||||
@@ -280,6 +292,20 @@ main(int argc, char *argv[])
|
|||||||
fe = std::make_unique<TerminalFrontend>();
|
fe = std::make_unique<TerminalFrontend>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guarantee Shutdown() runs on every exit path from here on (normal
|
||||||
|
// return, early return, or exception unwinding e.g. from Init/Execute/
|
||||||
|
// Step) so a crash never leaves the terminal in raw/cbreak mode.
|
||||||
|
struct FrontendShutdownGuard {
|
||||||
|
Frontend *fe;
|
||||||
|
|
||||||
|
|
||||||
|
~FrontendShutdownGuard()
|
||||||
|
{
|
||||||
|
if (fe)
|
||||||
|
fe->Shutdown();
|
||||||
|
}
|
||||||
|
} shutdown_guard{fe.get()};
|
||||||
|
|
||||||
#if defined(KTE_BUILD_GUI) && defined(__APPLE__)
|
#if defined(KTE_BUILD_GUI) && defined(__APPLE__)
|
||||||
if (use_gui) {
|
if (use_gui) {
|
||||||
/* likely using the .app, so need to cd */
|
/* likely using the .app, so need to cd */
|
||||||
@@ -308,8 +334,6 @@ main(int argc, char *argv[])
|
|||||||
fe->Step(editor, running);
|
fe->Step(editor, running);
|
||||||
}
|
}
|
||||||
|
|
||||||
fe->Shutdown();
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
std::string msg = std::string("Unhandled exception: ") + e.what();
|
std::string msg = std::string("Unhandled exception: ") + e.what();
|
||||||
|
|||||||
+15
-17
@@ -15,20 +15,18 @@ sha256sum kge.app.zip
|
|||||||
open .
|
open .
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
mkdir -p cmake-build-release-qt
|
# Qt build disabled — ImGui frontend is the primary GUI.
|
||||||
cmake -S . -B cmake-build-release-qt -DBUILD_GUI=ON -DKTE_USE_QT=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF
|
# mkdir -p cmake-build-release-qt
|
||||||
|
# cmake -S . -B cmake-build-release-qt -DBUILD_GUI=ON -DKTE_USE_QT=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF
|
||||||
cd cmake-build-release-qt
|
#
|
||||||
make clean
|
# cd cmake-build-release-qt
|
||||||
rm -fr kge.app* kge-qt.app*
|
# make clean
|
||||||
make
|
# rm -fr kge.app* kge-qt.app*
|
||||||
mv -f kge.app kge-qt.app
|
# make
|
||||||
# Use the same Qt's macdeployqt as used for building; ensure it overwrites in-bundle paths
|
# mv -f kge.app kge-qt.app
|
||||||
macdeployqt kge-qt.app -always-overwrite -verbose=3
|
# macdeployqt kge-qt.app -always-overwrite -verbose=3
|
||||||
|
# cmake -DAPP_BUNDLE="$(pwd)/kge-qt.app" -P "${PWD%/*}/cmake/fix_bundle.cmake"
|
||||||
# Run CMake BundleUtilities fixup to internalize non-Qt dylibs and rewrite install names
|
# zip -r kge-qt.app.zip kge-qt.app
|
||||||
cmake -DAPP_BUNDLE="$(pwd)/kge-qt.app" -P "${PWD%/*}/cmake/fix_bundle.cmake"
|
# sha256sum kge-qt.app.zip
|
||||||
zip -r kge-qt.app.zip kge-qt.app
|
# open .
|
||||||
sha256sum kge-qt.app.zip
|
# cd ..
|
||||||
open .
|
|
||||||
cd ..
|
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ fi
|
|||||||
|
|
||||||
git tag "${KTE_VERSION}"
|
git tag "${KTE_VERSION}"
|
||||||
git push && git push --tags
|
git push && git push --tags
|
||||||
|
git push github && git push github --tags
|
||||||
|
|
||||||
( ./make-app-release )
|
( ./make-app-release )
|
||||||
+35
-2
@@ -46,14 +46,45 @@ GoHighlighter::GoHighlighter()
|
|||||||
void
|
void
|
||||||
GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
{
|
{
|
||||||
|
StatefulHighlighter::LineState prev;
|
||||||
|
(void) HighlightLineStateful(buf, row, prev, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
StatefulHighlighter::LineState
|
||||||
|
GoHighlighter::HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
StatefulHighlighter::LineState state = prev;
|
||||||
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
||||||
return;
|
return state;
|
||||||
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
||||||
int n = static_cast<int>(s.size());
|
int n = static_cast<int>(s.size());
|
||||||
int i = 0;
|
int i = 0;
|
||||||
int bol = 0;
|
int bol = 0;
|
||||||
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
|
||||||
++bol;
|
++bol;
|
||||||
|
|
||||||
|
// Continue a multi-line block comment from the previous line.
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
int j = i;
|
||||||
|
while (i + 1 < n) {
|
||||||
|
if (s[i] == '*' && s[i + 1] == '/') {
|
||||||
|
i += 2;
|
||||||
|
push(out, j, i, TokenKind::Comment);
|
||||||
|
state.in_block_comment = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
push(out, j, n, TokenKind::Comment);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// line comment
|
// line comment
|
||||||
while (i < n) {
|
while (i < n) {
|
||||||
char c = s[i];
|
char c = s[i];
|
||||||
@@ -82,7 +113,8 @@ GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSp
|
|||||||
}
|
}
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
push(out, i, n, TokenKind::Comment);
|
push(out, i, n, TokenKind::Comment);
|
||||||
break;
|
state.in_block_comment = true;
|
||||||
|
return state;
|
||||||
} else {
|
} else {
|
||||||
push(out, i, j, TokenKind::Comment);
|
push(out, i, j, TokenKind::Comment);
|
||||||
i = j;
|
i = j;
|
||||||
@@ -152,5 +184,6 @@ GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSp
|
|||||||
push(out, i, i + 1, TokenKind::Default);
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
@@ -5,12 +5,17 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace kte {
|
namespace kte {
|
||||||
class GoHighlighter final : public LanguageHighlighter {
|
class GoHighlighter final : public StatefulHighlighter {
|
||||||
public:
|
public:
|
||||||
GoHighlighter();
|
GoHighlighter();
|
||||||
|
|
||||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
LineState HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unordered_set<std::string> kws_;
|
std::unordered_set<std::string> kws_;
|
||||||
std::unordered_set<std::string> types_;
|
std::unordered_set<std::string> types_;
|
||||||
|
|||||||
@@ -74,7 +74,22 @@ HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version
|
|||||||
|
|
||||||
StatefulHighlighter::LineState prev_state;
|
StatefulHighlighter::LineState prev_state;
|
||||||
int start_row = -1;
|
int start_row = -1;
|
||||||
if (!state_cache_.empty()) {
|
|
||||||
|
// Fast path: state_last_contig_ tracks the highest row we've cached state
|
||||||
|
// for, per version. If that row is already below our target, it's
|
||||||
|
// necessarily the best anchor the O(n) scan below would have found, so we
|
||||||
|
// can skip the scan entirely. Re-validated against state_cache_ before
|
||||||
|
// use since InvalidateFrom()/SetHighlighter() may have raced/cleared it.
|
||||||
|
auto contig_it = state_last_contig_.find(buf_version);
|
||||||
|
if (contig_it != state_last_contig_.end() && contig_it->second < row) {
|
||||||
|
auto sc_it = state_cache_.find(contig_it->second);
|
||||||
|
if (sc_it != state_cache_.end() && sc_it->second.version == buf_version) {
|
||||||
|
start_row = contig_it->second;
|
||||||
|
prev_state = sc_it->second.state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start_row < 0 && !state_cache_.empty()) {
|
||||||
// linear search over map (unordered), track best candidate
|
// linear search over map (unordered), track best candidate
|
||||||
int best = -1;
|
int best = -1;
|
||||||
for (const auto &kv: state_cache_) {
|
for (const auto &kv: state_cache_) {
|
||||||
@@ -109,6 +124,9 @@ HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version
|
|||||||
se.state = next_state;
|
se.state = next_state;
|
||||||
state_cache_[r] = se;
|
state_cache_[r] = se;
|
||||||
cur_state = next_state;
|
cur_state = next_state;
|
||||||
|
int &contig = state_last_contig_[buf_version];
|
||||||
|
if (r > contig)
|
||||||
|
contig = r;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store in cache and return by value
|
// Store in cache and return by value
|
||||||
@@ -139,6 +157,14 @@ HighlighterEngine::InvalidateFrom(int row)
|
|||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A version's tracked contiguous-state row is no longer valid once any
|
||||||
|
// row at or above it has been evicted from state_cache_ above.
|
||||||
|
for (auto it = state_last_contig_.begin(); it != state_last_contig_.end();) {
|
||||||
|
if (it->second >= row)
|
||||||
|
it = state_last_contig_.erase(it);
|
||||||
|
else
|
||||||
|
++it;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -55,31 +55,26 @@ PythonHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineS
|
|||||||
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
||||||
int n = static_cast<int>(s.size());
|
int n = static_cast<int>(s.size());
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
// Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
|
// Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
|
||||||
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
|
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
|
||||||
auto pos = s.find(state.raw_delim);
|
auto pos = s.find(state.raw_delim);
|
||||||
if (pos == std::string::npos) {
|
if (pos == std::string::npos) {
|
||||||
push(out, 0, n, TokenKind::String);
|
push(out, 0, n, TokenKind::String);
|
||||||
return state; // still inside
|
return state; // still inside
|
||||||
} else {
|
}
|
||||||
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
|
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
|
||||||
push(out, 0, end, TokenKind::String);
|
push(out, 0, end, TokenKind::String);
|
||||||
// remainder processed normally
|
|
||||||
s = s.substr(end);
|
|
||||||
n = static_cast<int>(s.size());
|
|
||||||
state.in_raw_string = false;
|
state.in_raw_string = false;
|
||||||
state.raw_delim.clear();
|
state.raw_delim.clear();
|
||||||
// Continue parsing remainder as a separate small loop
|
// Resume the normal tokenizer at the closing delimiter's end, on the
|
||||||
int base = end;
|
// same (unmodified) `s`/`n`, so anything after it - including a new
|
||||||
// original offset, but we already emitted to 'out' with base=0; following spans should be from 'end'
|
// triple-quoted string opening on this same line - is re-scanned
|
||||||
// For simplicity, mark rest as Default
|
// instead of being dumped into a single opaque Default span.
|
||||||
if (n > 0)
|
i = end;
|
||||||
push(out, base, base + n, TokenKind::Default);
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
// Detect comment start '#', ignoring inside strings
|
// Detect comment start '#', ignoring inside strings
|
||||||
while (i < n) {
|
while (i < n) {
|
||||||
char c = s[i];
|
char c = s[i];
|
||||||
|
|||||||
@@ -47,11 +47,42 @@ RustHighlighter::RustHighlighter()
|
|||||||
void
|
void
|
||||||
RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
{
|
{
|
||||||
|
StatefulHighlighter::LineState prev;
|
||||||
|
(void) HighlightLineStateful(buf, row, prev, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
StatefulHighlighter::LineState
|
||||||
|
RustHighlighter::HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
StatefulHighlighter::LineState state = prev;
|
||||||
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
||||||
return;
|
return state;
|
||||||
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
||||||
int n = static_cast<int>(s.size());
|
int n = static_cast<int>(s.size());
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
|
// Continue a multi-line block comment from the previous line.
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
int j = i;
|
||||||
|
while (i + 1 < n) {
|
||||||
|
if (s[i] == '*' && s[i + 1] == '/') {
|
||||||
|
i += 2;
|
||||||
|
push(out, j, i, TokenKind::Comment);
|
||||||
|
state.in_block_comment = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
push(out, j, n, TokenKind::Comment);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (i < n) {
|
while (i < n) {
|
||||||
char c = s[i];
|
char c = s[i];
|
||||||
if (c == ' ' || c == '\t') {
|
if (c == ' ' || c == '\t') {
|
||||||
@@ -79,7 +110,8 @@ RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<Highlight
|
|||||||
}
|
}
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
push(out, i, n, TokenKind::Comment);
|
push(out, i, n, TokenKind::Comment);
|
||||||
break;
|
state.in_block_comment = true;
|
||||||
|
return state;
|
||||||
} else {
|
} else {
|
||||||
push(out, i, j, TokenKind::Comment);
|
push(out, i, j, TokenKind::Comment);
|
||||||
i = j;
|
i = j;
|
||||||
@@ -140,5 +172,6 @@ RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<Highlight
|
|||||||
push(out, i, i + 1, TokenKind::Default);
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
@@ -5,12 +5,17 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace kte {
|
namespace kte {
|
||||||
class RustHighlighter final : public LanguageHighlighter {
|
class RustHighlighter final : public StatefulHighlighter {
|
||||||
public:
|
public:
|
||||||
RustHighlighter();
|
RustHighlighter();
|
||||||
|
|
||||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
LineState HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unordered_set<std::string> kws_;
|
std::unordered_set<std::string> kws_;
|
||||||
std::unordered_set<std::string> types_;
|
std::unordered_set<std::string> types_;
|
||||||
|
|||||||
@@ -47,12 +47,42 @@ SqlHighlighter::SqlHighlighter()
|
|||||||
void
|
void
|
||||||
SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
|
||||||
{
|
{
|
||||||
|
StatefulHighlighter::LineState prev;
|
||||||
|
(void) HighlightLineStateful(buf, row, prev, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
StatefulHighlighter::LineState
|
||||||
|
SqlHighlighter::HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const
|
||||||
|
{
|
||||||
|
StatefulHighlighter::LineState state = prev;
|
||||||
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
if (row < 0 || static_cast<std::size_t>(row) >= buf.Nrows())
|
||||||
return;
|
return state;
|
||||||
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
std::string s = buf.GetLineString(static_cast<std::size_t>(row));
|
||||||
int n = static_cast<int>(s.size());
|
int n = static_cast<int>(s.size());
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
|
// Continue a multi-line block comment from the previous line.
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
int j = i;
|
||||||
|
while (i + 1 < n) {
|
||||||
|
if (s[i] == '*' && s[i + 1] == '/') {
|
||||||
|
i += 2;
|
||||||
|
push(out, j, i, TokenKind::Comment);
|
||||||
|
state.in_block_comment = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
if (state.in_block_comment) {
|
||||||
|
push(out, j, n, TokenKind::Comment);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (i < n) {
|
while (i < n) {
|
||||||
char c = s[i];
|
char c = s[i];
|
||||||
if (c == ' ' || c == '\t') {
|
if (c == ' ' || c == '\t') {
|
||||||
@@ -68,7 +98,7 @@ SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightS
|
|||||||
push(out, i, n, TokenKind::Comment);
|
push(out, i, n, TokenKind::Comment);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// simple block comment on same line: /* ... */
|
// block comment: /* ... */ (may span multiple lines)
|
||||||
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
|
||||||
int j = i + 2;
|
int j = i + 2;
|
||||||
bool closed = false;
|
bool closed = false;
|
||||||
@@ -82,7 +112,8 @@ SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightS
|
|||||||
}
|
}
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
push(out, i, n, TokenKind::Comment);
|
push(out, i, n, TokenKind::Comment);
|
||||||
break;
|
state.in_block_comment = true;
|
||||||
|
return state;
|
||||||
} else {
|
} else {
|
||||||
push(out, i, j, TokenKind::Comment);
|
push(out, i, j, TokenKind::Comment);
|
||||||
i = j;
|
i = j;
|
||||||
@@ -151,5 +182,6 @@ SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightS
|
|||||||
push(out, i, i + 1, TokenKind::Default);
|
push(out, i, i + 1, TokenKind::Default);
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
@@ -5,12 +5,17 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace kte {
|
namespace kte {
|
||||||
class SqlHighlighter final : public LanguageHighlighter {
|
class SqlHighlighter final : public StatefulHighlighter {
|
||||||
public:
|
public:
|
||||||
SqlHighlighter();
|
SqlHighlighter();
|
||||||
|
|
||||||
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
|
LineState HighlightLineStateful(const Buffer &buf,
|
||||||
|
int row,
|
||||||
|
const LineState &prev,
|
||||||
|
std::vector<HighlightSpan> &out) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unordered_set<std::string> kws_;
|
std::unordered_set<std::string> kws_;
|
||||||
std::unordered_set<std::string> types_;
|
std::unordered_set<std::string> types_;
|
||||||
|
|||||||
@@ -108,3 +108,26 @@ TEST(CommandSemantics_CopyRegion_And_KillRegion)
|
|||||||
ASSERT_EQ(b.MarkSet(), false);
|
ASSERT_EQ(b.MarkSet(), false);
|
||||||
ASSERT_EQ(h.Text(), std::string("hello "));
|
ASSERT_EQ(h.Text(), std::string("hello "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST(CommandSemantics_Syntax_OnOff_SetsUserOverride)
|
||||||
|
{
|
||||||
|
TestHarness h;
|
||||||
|
Editor &ed = h.EditorRef();
|
||||||
|
Buffer &b = h.Buf();
|
||||||
|
|
||||||
|
// Before any explicit :syntax command, nothing has overridden the
|
||||||
|
// frontend's config-driven default.
|
||||||
|
ASSERT_EQ(b.SyntaxUserOverride(), false);
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Syntax, "off"));
|
||||||
|
ASSERT_EQ(b.SyntaxEnabled(), false);
|
||||||
|
// This flag is what a frontend's per-frame "apply config default" pass
|
||||||
|
// must check before re-enabling syntax, or a manual :syntax off gets
|
||||||
|
// silently undone on the next frame.
|
||||||
|
ASSERT_EQ(b.SyntaxUserOverride(), true);
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Syntax, "on"));
|
||||||
|
ASSERT_EQ(b.SyntaxEnabled(), true);
|
||||||
|
ASSERT_EQ(b.SyntaxUserOverride(), true);
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,21 @@ TEST(KKeymap_KPrefix_CanonicalChords)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST(KKeymap_KPrefix_UppercaseE_RejectedAndDistinctFromLowercaseE)
|
||||||
|
{
|
||||||
|
CommandId id{};
|
||||||
|
|
||||||
|
// 'e' is a real binding (OpenFileStart).
|
||||||
|
ASSERT_TRUE(KLookupKCommand('e', false, id));
|
||||||
|
ASSERT_EQ(id, CommandId::OpenFileStart);
|
||||||
|
|
||||||
|
// 'E' must be rejected, not silently aliased to 'e' via case-insensitive
|
||||||
|
// lookup (the switch normalizes to lowercase, so this has to be special-
|
||||||
|
// cased before it).
|
||||||
|
ASSERT_EQ(KLookupKCommand('E', false, id), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(KKeymap_CtrlChords_CanonicalChords)
|
TEST(KKeymap_CtrlChords_CanonicalChords)
|
||||||
{
|
{
|
||||||
CommandId id{};
|
CommandId id{};
|
||||||
|
|||||||
@@ -414,17 +414,21 @@ TEST (Migration_EmptyBufferCheck_Pattern)
|
|||||||
|
|
||||||
TEST (Migration_SyntaxHighlighter_Pattern)
|
TEST (Migration_SyntaxHighlighter_Pattern)
|
||||||
{
|
{
|
||||||
// Test the pattern used in syntax highlighters
|
// Test the pattern used in syntax highlighters: loop Nrows(), fetch each
|
||||||
|
// line via GetLineString(row). Assert the pattern actually returns the
|
||||||
|
// right content, not just that it doesn't crash - see syntax highlighter
|
||||||
|
// correctness tests (test_syntax_highlighting.cc) for tokenization coverage.
|
||||||
Buffer buf;
|
Buffer buf;
|
||||||
buf.insert_text(0, 0, std::string("int main() {\n return 0;\n}"));
|
buf.insert_text(0, 0, std::string("int main() {\n return 0;\n}"));
|
||||||
|
|
||||||
|
ASSERT_EQ(buf.Nrows(), static_cast<std::size_t>(3));
|
||||||
|
const std::vector<std::string> expected = {"int main() {", " return 0;", "}"};
|
||||||
for (std::size_t row = 0; row < buf.Nrows(); ++row) {
|
for (std::size_t row = 0; row < buf.Nrows(); ++row) {
|
||||||
// This is the pattern used in all migrated highlighters
|
|
||||||
if (row >= buf.Nrows()) {
|
if (row >= buf.Nrows()) {
|
||||||
break; // Should never happen
|
break; // Should never happen
|
||||||
}
|
}
|
||||||
std::string line = buf.GetLineString(row);
|
std::string line = buf.GetLineString(row);
|
||||||
// Successfully accessed line - size() is always valid for std::string
|
ASSERT_EQ(line, expected[row]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// Tests for SplitPasteIntoCommands: pasted text must turn embedded line
|
||||||
|
// breaks into Newline commands so InsertText never receives a '\r' or '\n'.
|
||||||
|
#include "Test.h"
|
||||||
|
|
||||||
|
#include "PasteSplit.h"
|
||||||
|
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Assert that no InsertText command carries an embedded newline/carriage
|
||||||
|
// return, mirroring the rejection in Command.cc's InsertText handler.
|
||||||
|
void
|
||||||
|
assert_no_newlines_in_inserts(const std::vector<MappedInput> &cmds)
|
||||||
|
{
|
||||||
|
for (const auto &c : cmds) {
|
||||||
|
if (c.id == CommandId::InsertText) {
|
||||||
|
ASSERT_TRUE(c.arg.find('\n') == std::string::npos);
|
||||||
|
ASSERT_TRUE(c.arg.find('\r') == std::string::npos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
TEST(PasteSplit_PlainTextNoNewline)
|
||||||
|
{
|
||||||
|
auto cmds = SplitPasteIntoCommands("hello world");
|
||||||
|
ASSERT_EQ(cmds.size(), 1u);
|
||||||
|
ASSERT_TRUE(cmds[0].id == CommandId::InsertText);
|
||||||
|
ASSERT_TRUE(cmds[0].arg == "hello world");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST(PasteSplit_UnixNewlines)
|
||||||
|
{
|
||||||
|
auto cmds = SplitPasteIntoCommands("a\nb");
|
||||||
|
assert_no_newlines_in_inserts(cmds);
|
||||||
|
ASSERT_EQ(cmds.size(), 3u);
|
||||||
|
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "a");
|
||||||
|
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
|
||||||
|
ASSERT_TRUE(cmds[2].id == CommandId::InsertText && cmds[2].arg == "b");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Regression: CRLF clipboards left a '\r' in the InsertText segment, which
|
||||||
|
// InsertText rejected with "InsertText arg must not contain newlines".
|
||||||
|
TEST(PasteSplit_WindowsCRLF)
|
||||||
|
{
|
||||||
|
auto cmds = SplitPasteIntoCommands("a\r\nb");
|
||||||
|
assert_no_newlines_in_inserts(cmds);
|
||||||
|
ASSERT_EQ(cmds.size(), 3u);
|
||||||
|
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "a");
|
||||||
|
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
|
||||||
|
ASSERT_TRUE(cmds[2].id == CommandId::InsertText && cmds[2].arg == "b");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Regression: a bare '\r' (classic Mac / some macOS apps) must also break lines.
|
||||||
|
TEST(PasteSplit_BareCR)
|
||||||
|
{
|
||||||
|
auto cmds = SplitPasteIntoCommands("a\rb");
|
||||||
|
assert_no_newlines_in_inserts(cmds);
|
||||||
|
ASSERT_EQ(cmds.size(), 3u);
|
||||||
|
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "a");
|
||||||
|
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
|
||||||
|
ASSERT_TRUE(cmds[2].id == CommandId::InsertText && cmds[2].arg == "b");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST(PasteSplit_BlankLinesPreserved)
|
||||||
|
{
|
||||||
|
// Two line breaks between "a" and "b" => one empty line.
|
||||||
|
auto cmds = SplitPasteIntoCommands("a\n\nb");
|
||||||
|
assert_no_newlines_in_inserts(cmds);
|
||||||
|
ASSERT_EQ(cmds.size(), 4u);
|
||||||
|
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "a");
|
||||||
|
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
|
||||||
|
ASSERT_TRUE(cmds[2].id == CommandId::Newline);
|
||||||
|
ASSERT_TRUE(cmds[3].id == CommandId::InsertText && cmds[3].arg == "b");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST(PasteSplit_TrailingNewline)
|
||||||
|
{
|
||||||
|
auto cmds = SplitPasteIntoCommands("abc\n");
|
||||||
|
assert_no_newlines_in_inserts(cmds);
|
||||||
|
ASSERT_EQ(cmds.size(), 2u);
|
||||||
|
ASSERT_TRUE(cmds[0].id == CommandId::InsertText && cmds[0].arg == "abc");
|
||||||
|
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST(PasteSplit_MultilineCRLF)
|
||||||
|
{
|
||||||
|
auto cmds = SplitPasteIntoCommands("line1\r\nline2\r\nline3");
|
||||||
|
assert_no_newlines_in_inserts(cmds);
|
||||||
|
ASSERT_EQ(cmds.size(), 5u);
|
||||||
|
ASSERT_TRUE(cmds[0].arg == "line1");
|
||||||
|
ASSERT_TRUE(cmds[1].id == CommandId::Newline);
|
||||||
|
ASSERT_TRUE(cmds[2].arg == "line2");
|
||||||
|
ASSERT_TRUE(cmds[3].id == CommandId::Newline);
|
||||||
|
ASSERT_TRUE(cmds[4].arg == "line3");
|
||||||
|
}
|
||||||
@@ -101,6 +101,36 @@ TEST(SearchFlow_SearchReplace_EmptyFind_DoesNotMutateBuffer_And_ClearsState)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST(SearchFlow_SearchReplace_EmptyWith_ReplacesAdjacentOverlappingMatches)
|
||||||
|
{
|
||||||
|
TestHarness h;
|
||||||
|
Editor &ed = h.EditorRef();
|
||||||
|
Buffer &b = h.Buf();
|
||||||
|
|
||||||
|
// "aaaa" with "aa" -> "" must remove both non-overlapping occurrences, not
|
||||||
|
// just the first: after deleting the match at column 0, the next "aa" now
|
||||||
|
// sits at column 0 too (not column 1), so the scan must resume at the
|
||||||
|
// deletion point rather than one character past it.
|
||||||
|
b.insert_text(0, 0, "aaaa\n");
|
||||||
|
b.SetCursor(0, 0);
|
||||||
|
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::SearchReplace));
|
||||||
|
ASSERT_TRUE(ed.PromptActive());
|
||||||
|
ASSERT_EQ(ed.CurrentPromptKind(), Editor::PromptKind::ReplaceFind);
|
||||||
|
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::InsertText, "aa"));
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::Newline));
|
||||||
|
ASSERT_TRUE(ed.PromptActive());
|
||||||
|
ASSERT_EQ(ed.CurrentPromptKind(), Editor::PromptKind::ReplaceWith);
|
||||||
|
|
||||||
|
// Leave the replacement empty and accept.
|
||||||
|
ASSERT_TRUE(h.Exec(CommandId::Newline));
|
||||||
|
|
||||||
|
ASSERT_TRUE(!ed.PromptActive());
|
||||||
|
ASSERT_EQ(std::string(b.Rows()[0]), std::string(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(SearchFlow_RegexFind_InvalidPattern_FailsSafely_And_ClearsStateOnEnter)
|
TEST(SearchFlow_RegexFind_InvalidPattern_FailsSafely_And_ClearsStateOnEnter)
|
||||||
{
|
{
|
||||||
TestHarness h;
|
TestHarness h;
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#include "Test.h"
|
||||||
|
|
||||||
|
#include "Command.h"
|
||||||
|
#include "Editor.h"
|
||||||
|
|
||||||
|
#include "tests/TestHarness.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
|
||||||
|
static void
|
||||||
|
write_file_bytes(const std::string &path, const std::string &bytes)
|
||||||
|
{
|
||||||
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||||
|
out.write(bytes.data(), (std::streamsize) bytes.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RAII helper to set XDG_STATE_HOME for the duration of a test and clean up.
|
||||||
|
struct XdgStateGuard {
|
||||||
|
fs::path root;
|
||||||
|
std::string old_xdg;
|
||||||
|
bool had_old;
|
||||||
|
|
||||||
|
explicit XdgStateGuard(const std::string &suffix)
|
||||||
|
{
|
||||||
|
root = fs::temp_directory_path() /
|
||||||
|
(std::string("kte_ut_xdg_") + suffix + "_" + std::to_string((int) ::getpid()));
|
||||||
|
fs::remove_all(root);
|
||||||
|
fs::create_directories(root);
|
||||||
|
|
||||||
|
const char *p = std::getenv("XDG_STATE_HOME");
|
||||||
|
had_old = (p != nullptr);
|
||||||
|
if (p)
|
||||||
|
old_xdg = p;
|
||||||
|
setenv("XDG_STATE_HOME", root.string().c_str(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
~XdgStateGuard()
|
||||||
|
{
|
||||||
|
if (had_old)
|
||||||
|
setenv("XDG_STATE_HOME", old_xdg.c_str(), 1);
|
||||||
|
else
|
||||||
|
unsetenv("XDG_STATE_HOME");
|
||||||
|
fs::remove_all(root);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
TEST(SwapCleanup_SaveAndQuit)
|
||||||
|
{
|
||||||
|
ktet::InstallDefaultCommandsOnce();
|
||||||
|
XdgStateGuard xdg("save_quit");
|
||||||
|
|
||||||
|
const std::string path = (xdg.root / "work" / "file.txt").string();
|
||||||
|
fs::create_directories(xdg.root / "work");
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
ed.AddBuffer(Buffer());
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(ed.OpenFile(path, err));
|
||||||
|
Buffer *b = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(b != nullptr);
|
||||||
|
|
||||||
|
// Edit to create swap file
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::MoveFileStart));
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "Z"));
|
||||||
|
ASSERT_TRUE(b->Dirty());
|
||||||
|
|
||||||
|
ed.Swap()->Flush(b);
|
||||||
|
const std::string swp = kte::SwapManager::ComputeSwapPathForTests(*b);
|
||||||
|
ASSERT_TRUE(fs::exists(swp));
|
||||||
|
|
||||||
|
// Save-and-quit should clean up the swap file
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::SaveAndQuit));
|
||||||
|
ed.Swap()->Flush(b);
|
||||||
|
ASSERT_TRUE(!fs::exists(swp));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
std::remove(path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST(SwapCleanup_EditorReset)
|
||||||
|
{
|
||||||
|
ktet::InstallDefaultCommandsOnce();
|
||||||
|
XdgStateGuard xdg("editor_reset");
|
||||||
|
|
||||||
|
const std::string path = (xdg.root / "work" / "file.txt").string();
|
||||||
|
fs::create_directories(xdg.root / "work");
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
ed.AddBuffer(Buffer());
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(ed.OpenFile(path, err));
|
||||||
|
Buffer *b = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(b != nullptr);
|
||||||
|
|
||||||
|
// Edit to create swap file
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::MoveFileStart));
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "W"));
|
||||||
|
ASSERT_TRUE(b->Dirty());
|
||||||
|
|
||||||
|
ed.Swap()->Flush(b);
|
||||||
|
const std::string swp = kte::SwapManager::ComputeSwapPathForTests(*b);
|
||||||
|
ASSERT_TRUE(fs::exists(swp));
|
||||||
|
|
||||||
|
// Reset (simulates clean editor exit) should remove swap files
|
||||||
|
ed.Reset();
|
||||||
|
ASSERT_TRUE(!fs::exists(swp));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
std::remove(path.c_str());
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// test_syntax_highlighting.cc - tokenization correctness for LanguageHighlighter
|
||||||
|
// implementations, focused on multi-line state propagation (block comments,
|
||||||
|
// triple-quoted strings) that stateless-looking per-line scans easily get wrong.
|
||||||
|
#include "Test.h"
|
||||||
|
#include "Buffer.h"
|
||||||
|
#include "Highlight.h"
|
||||||
|
#include "syntax/LanguageHighlighter.h"
|
||||||
|
#include "syntax/GoHighlighter.h"
|
||||||
|
#include "syntax/RustHighlighter.h"
|
||||||
|
#include "syntax/SqlHighlighter.h"
|
||||||
|
#include "syntax/PythonHighlighter.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using kte::HighlightSpan;
|
||||||
|
using kte::StatefulHighlighter;
|
||||||
|
using kte::TokenKind;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool
|
||||||
|
has_kind(const std::vector<HighlightSpan> &spans, TokenKind k)
|
||||||
|
{
|
||||||
|
for (const auto &sp: spans) {
|
||||||
|
if (sp.kind == k)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Syntax_Go_MultiLineBlockComment_PropagatesAcrossLines)
|
||||||
|
{
|
||||||
|
Buffer b;
|
||||||
|
b.replace_all_bytes("/* start\nmiddle line\nend */\nfunc foo() {}\n");
|
||||||
|
|
||||||
|
kte::GoHighlighter hl;
|
||||||
|
StatefulHighlighter::LineState state;
|
||||||
|
std::vector<HighlightSpan> spans;
|
||||||
|
|
||||||
|
// Line 0 opens an unclosed block comment; state must carry forward.
|
||||||
|
state = hl.HighlightLineStateful(b, 0, state, spans);
|
||||||
|
ASSERT_TRUE(state.in_block_comment);
|
||||||
|
|
||||||
|
// Line 1 is entirely inside the comment; the whole line must be Comment,
|
||||||
|
// not re-tokenized as code (this is the bug: without state propagation
|
||||||
|
// "middle" and "line" would come back as identifiers).
|
||||||
|
spans.clear();
|
||||||
|
state = hl.HighlightLineStateful(b, 1, state, spans);
|
||||||
|
ASSERT_TRUE(state.in_block_comment);
|
||||||
|
ASSERT_TRUE(!spans.empty());
|
||||||
|
for (const auto &sp: spans) {
|
||||||
|
ASSERT_TRUE(sp.kind == TokenKind::Comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line 2 closes the comment.
|
||||||
|
spans.clear();
|
||||||
|
state = hl.HighlightLineStateful(b, 2, state, spans);
|
||||||
|
ASSERT_TRUE(!state.in_block_comment);
|
||||||
|
ASSERT_TRUE(has_kind(spans, TokenKind::Comment));
|
||||||
|
|
||||||
|
// Line 3 is ordinary code again: "func" must be a Keyword, not a Comment.
|
||||||
|
spans.clear();
|
||||||
|
state = hl.HighlightLineStateful(b, 3, state, spans);
|
||||||
|
ASSERT_TRUE(!state.in_block_comment);
|
||||||
|
ASSERT_TRUE(has_kind(spans, TokenKind::Keyword));
|
||||||
|
ASSERT_TRUE(!has_kind(spans, TokenKind::Comment));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Syntax_Rust_MultiLineBlockComment_PropagatesAcrossLines)
|
||||||
|
{
|
||||||
|
Buffer b;
|
||||||
|
b.replace_all_bytes("/* start\nmiddle line\nend */\nfn foo() {}\n");
|
||||||
|
|
||||||
|
kte::RustHighlighter hl;
|
||||||
|
StatefulHighlighter::LineState state;
|
||||||
|
std::vector<HighlightSpan> spans;
|
||||||
|
|
||||||
|
state = hl.HighlightLineStateful(b, 0, state, spans);
|
||||||
|
ASSERT_TRUE(state.in_block_comment);
|
||||||
|
|
||||||
|
spans.clear();
|
||||||
|
state = hl.HighlightLineStateful(b, 1, state, spans);
|
||||||
|
ASSERT_TRUE(state.in_block_comment);
|
||||||
|
for (const auto &sp: spans) {
|
||||||
|
ASSERT_TRUE(sp.kind == TokenKind::Comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
spans.clear();
|
||||||
|
state = hl.HighlightLineStateful(b, 2, state, spans);
|
||||||
|
ASSERT_TRUE(!state.in_block_comment);
|
||||||
|
|
||||||
|
spans.clear();
|
||||||
|
state = hl.HighlightLineStateful(b, 3, state, spans);
|
||||||
|
ASSERT_TRUE(has_kind(spans, TokenKind::Keyword));
|
||||||
|
ASSERT_TRUE(!has_kind(spans, TokenKind::Comment));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Syntax_Sql_MultiLineBlockComment_PropagatesAcrossLines)
|
||||||
|
{
|
||||||
|
Buffer b;
|
||||||
|
b.replace_all_bytes("/* start\nmiddle line\nend */\nSELECT 1;\n");
|
||||||
|
|
||||||
|
kte::SqlHighlighter hl;
|
||||||
|
StatefulHighlighter::LineState state;
|
||||||
|
std::vector<HighlightSpan> spans;
|
||||||
|
|
||||||
|
state = hl.HighlightLineStateful(b, 0, state, spans);
|
||||||
|
ASSERT_TRUE(state.in_block_comment);
|
||||||
|
|
||||||
|
spans.clear();
|
||||||
|
state = hl.HighlightLineStateful(b, 1, state, spans);
|
||||||
|
ASSERT_TRUE(state.in_block_comment);
|
||||||
|
for (const auto &sp: spans) {
|
||||||
|
ASSERT_TRUE(sp.kind == TokenKind::Comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
spans.clear();
|
||||||
|
state = hl.HighlightLineStateful(b, 2, state, spans);
|
||||||
|
ASSERT_TRUE(!state.in_block_comment);
|
||||||
|
|
||||||
|
spans.clear();
|
||||||
|
state = hl.HighlightLineStateful(b, 3, state, spans);
|
||||||
|
ASSERT_TRUE(has_kind(spans, TokenKind::Keyword));
|
||||||
|
ASSERT_TRUE(!has_kind(spans, TokenKind::Comment));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Syntax_Python_TripleQuote_ClosesAndReopensOnSameLine)
|
||||||
|
{
|
||||||
|
Buffer b;
|
||||||
|
// Line 0 opens a triple-quoted string that stays open.
|
||||||
|
// Line 1 closes the first string ("end'''") and, on the SAME line, opens a
|
||||||
|
// second triple-quoted string ("'''start of") that stays open into line 2.
|
||||||
|
b.replace_all_bytes("x = '''abc\nend''' + '''start of\nnext string'''\n");
|
||||||
|
|
||||||
|
kte::PythonHighlighter hl;
|
||||||
|
StatefulHighlighter::LineState state;
|
||||||
|
std::vector<HighlightSpan> spans;
|
||||||
|
|
||||||
|
state = hl.HighlightLineStateful(b, 0, state, spans);
|
||||||
|
ASSERT_TRUE(state.in_raw_string);
|
||||||
|
ASSERT_EQ(state.raw_delim, std::string("'''"));
|
||||||
|
|
||||||
|
spans.clear();
|
||||||
|
state = hl.HighlightLineStateful(b, 1, state, spans);
|
||||||
|
// The bug: without re-scanning the remainder after the closing ''' the
|
||||||
|
// second opening ''' on this line is never noticed, so state.in_raw_string
|
||||||
|
// would incorrectly come back false here.
|
||||||
|
ASSERT_TRUE(state.in_raw_string);
|
||||||
|
ASSERT_EQ(state.raw_delim, std::string("'''"));
|
||||||
|
|
||||||
|
// Line 2 is inside the second string; it must be highlighted as String,
|
||||||
|
// not as ordinary code.
|
||||||
|
spans.clear();
|
||||||
|
state = hl.HighlightLineStateful(b, 2, state, spans);
|
||||||
|
ASSERT_TRUE(!spans.empty());
|
||||||
|
bool all_string = true;
|
||||||
|
for (const auto &sp: spans) {
|
||||||
|
if (sp.kind != TokenKind::String)
|
||||||
|
all_string = false;
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(all_string);
|
||||||
|
}
|
||||||
@@ -1198,6 +1198,312 @@ TEST (Undo_Command_RedoCountSelectsBranch)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Undo_Command_Newline_UndoRejoinsCorrectLines)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
Buffer *buf = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(buf != nullptr);
|
||||||
|
|
||||||
|
buf->replace_all_bytes("abcdef\nghijkl");
|
||||||
|
buf->SetCursor(3, 0);
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Newline));
|
||||||
|
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(3));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("def"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("ghijkl"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||||
|
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(2));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abcdef"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("ghijkl"));
|
||||||
|
|
||||||
|
validate_undo_tree(*buf->Undo());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Undo_Command_Backspace_JoinUndoRedo)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
Buffer *buf = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(buf != nullptr);
|
||||||
|
|
||||||
|
buf->replace_all_bytes("abc\ndef");
|
||||||
|
buf->SetCursor(0, 1);
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Backspace));
|
||||||
|
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(1));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abcdef"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||||
|
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(2));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("def"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||||
|
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(1));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abcdef"));
|
||||||
|
|
||||||
|
validate_undo_tree(*buf->Undo());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Undo_Command_DeleteChar_JoinUndoRedo)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
Buffer *buf = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(buf != nullptr);
|
||||||
|
|
||||||
|
buf->replace_all_bytes("abc\ndef");
|
||||||
|
buf->SetCursor(3, 0);
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::DeleteChar));
|
||||||
|
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(1));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abcdef"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||||
|
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(2));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("def"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||||
|
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(1));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abcdef"));
|
||||||
|
|
||||||
|
validate_undo_tree(*buf->Undo());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Undo_Command_RegexReplaceAll_UndoRedo)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
Buffer *buf = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(buf != nullptr);
|
||||||
|
|
||||||
|
buf->replace_all_bytes("foo one\nfoo two\nbar three");
|
||||||
|
buf->SetCursor(0, 0);
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::RegexpReplace));
|
||||||
|
ASSERT_TRUE(ed.PromptActive());
|
||||||
|
ed.SetPromptText("foo");
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Newline));
|
||||||
|
ASSERT_TRUE(ed.PromptActive());
|
||||||
|
ed.SetPromptText("baz");
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Newline));
|
||||||
|
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("baz one"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("baz two"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("bar three"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("foo one"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("foo two"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("bar three"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("baz one"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("baz two"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("bar three"));
|
||||||
|
|
||||||
|
validate_undo_tree(*buf->Undo());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Undo_Command_IndentUnindentRegion_UndoRedo)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
Buffer *buf = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(buf != nullptr);
|
||||||
|
|
||||||
|
buf->replace_all_bytes("one\ntwo\nthree");
|
||||||
|
buf->SetMark(0, 0);
|
||||||
|
buf->SetCursor(0, 2);
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::IndentRegion));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("\tone"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("\ttwo"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("\tthree"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("one"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("two"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("three"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("\tone"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("\ttwo"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("\tthree"));
|
||||||
|
|
||||||
|
buf->SetMark(0, 0);
|
||||||
|
buf->SetCursor(0, 2);
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::UnindentRegion));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("one"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("two"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("three"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("\tone"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("\ttwo"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("\tthree"));
|
||||||
|
|
||||||
|
validate_undo_tree(*buf->Undo());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Undo_Command_KillToEol_UndoRedo)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
Buffer *buf = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(buf != nullptr);
|
||||||
|
|
||||||
|
buf->replace_all_bytes("abcdef\nghijkl");
|
||||||
|
buf->SetCursor(3, 0);
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::KillToEOL));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||||
|
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(2));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abcdef"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("ghijkl"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||||
|
|
||||||
|
validate_undo_tree(*buf->Undo());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Undo_Command_KillLine_UndoRedo)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
Buffer *buf = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(buf != nullptr);
|
||||||
|
|
||||||
|
buf->replace_all_bytes("abc\ndef\nghi");
|
||||||
|
buf->SetCursor(0, 1);
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::KillLine));
|
||||||
|
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(2));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("ghi"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||||
|
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(3));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("def"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[2]), std::string("ghi"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||||
|
ASSERT_EQ(buf->Nrows(), static_cast<std::size_t>(2));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc"));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[1]), std::string("ghi"));
|
||||||
|
|
||||||
|
validate_undo_tree(*buf->Undo());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Undo_Command_KillRegion_UndoRedo)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
Buffer *buf = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(buf != nullptr);
|
||||||
|
|
||||||
|
buf->replace_all_bytes("abc def ghi");
|
||||||
|
buf->SetMark(4, 0);
|
||||||
|
buf->SetCursor(8, 0);
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::KillRegion));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc ghi"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc def ghi"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Redo));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc ghi"));
|
||||||
|
|
||||||
|
validate_undo_tree(*buf->Undo());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (Undo_Command_DeleteWordPrevNext_UndoRedo)
|
||||||
|
{
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
ed.AddBuffer(std::move(b));
|
||||||
|
Buffer *buf = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(buf != nullptr);
|
||||||
|
|
||||||
|
buf->replace_all_bytes("abc def ghi");
|
||||||
|
buf->SetCursor(8, 0);
|
||||||
|
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::DeleteWordPrev));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc ghi"));
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc def ghi"));
|
||||||
|
|
||||||
|
buf->SetCursor(4, 0);
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::DeleteWordNext));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc ghi"));
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::Undo));
|
||||||
|
ASSERT_EQ(std::string(buf->Rows()[0]), std::string("abc def ghi"));
|
||||||
|
|
||||||
|
validate_undo_tree(*buf->Undo());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST (Undo_InsertRow_UndoDeletesRow)
|
TEST (Undo_InsertRow_UndoDeletesRow)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
// themes/Leuchtturm.h — Fountain pen on cream paper, brass and leather (header-only)
|
||||||
|
// Inspired by Kaweco Brass/Bronze Sport pens on Leuchtturm1917 notebook paper.
|
||||||
|
// Light: warm cream paper with blue-black fountain pen ink.
|
||||||
|
// Dark: leather case and patinated metal.
|
||||||
|
#pragma once
|
||||||
|
#include "ThemeHelpers.h"
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
ApplyLeuchtturmLightTheme()
|
||||||
|
{
|
||||||
|
// Notebook paper and fountain pen ink
|
||||||
|
const ImVec4 paper = RGBA(0xF2ECDF); // Leuchtturm cream paper
|
||||||
|
const ImVec4 bg1 = RGBA(0xE8E2D5); // slightly darker cream
|
||||||
|
const ImVec4 bg2 = RGBA(0xDDD7CA); // UI elements
|
||||||
|
const ImVec4 bg3 = RGBA(0xD1CBBD); // hover/active
|
||||||
|
const ImVec4 ink = RGBA(0x040720); // blue-black fountain pen ink
|
||||||
|
const ImVec4 dim = RGBA(0x7A756A); // faded text (like printed headers)
|
||||||
|
const ImVec4 border = RGBA(0xCCC6B4); // faint ruled lines
|
||||||
|
|
||||||
|
// Metal accents from the pens
|
||||||
|
const ImVec4 brass = RGBA(0x6B5E2A); // dark patinated brass
|
||||||
|
const ImVec4 brown = RGBA(0x5C3D28); // leather/bronze
|
||||||
|
|
||||||
|
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 = 12.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 = 0.0f;
|
||||||
|
|
||||||
|
ImVec4 *colors = style.Colors;
|
||||||
|
colors[ImGuiCol_Text] = ink;
|
||||||
|
colors[ImGuiCol_TextDisabled] = dim;
|
||||||
|
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] = border;
|
||||||
|
|
||||||
|
colors[ImGuiCol_CheckMark] = ink;
|
||||||
|
colors[ImGuiCol_SliderGrab] = ink;
|
||||||
|
colors[ImGuiCol_SliderGrabActive] = brass;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Button] = bg2;
|
||||||
|
colors[ImGuiCol_ButtonHovered] = bg3;
|
||||||
|
colors[ImGuiCol_ButtonActive] = bg1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Header] = bg2;
|
||||||
|
colors[ImGuiCol_HeaderHovered] = bg3;
|
||||||
|
colors[ImGuiCol_HeaderActive] = bg3;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Separator] = border;
|
||||||
|
colors[ImGuiCol_SeparatorHovered] = bg3;
|
||||||
|
colors[ImGuiCol_SeparatorActive] = brass;
|
||||||
|
|
||||||
|
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
|
||||||
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(brass.x, brass.y, brass.z, 0.50f);
|
||||||
|
colors[ImGuiCol_ResizeGripActive] = brass;
|
||||||
|
|
||||||
|
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] = border;
|
||||||
|
colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f);
|
||||||
|
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f);
|
||||||
|
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f);
|
||||||
|
|
||||||
|
colors[ImGuiCol_TextSelectedBg] = ImVec4(brass.x, brass.y, brass.z, 0.18f);
|
||||||
|
colors[ImGuiCol_DragDropTarget] = brass;
|
||||||
|
colors[ImGuiCol_NavHighlight] = brass;
|
||||||
|
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||||
|
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f);
|
||||||
|
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f);
|
||||||
|
colors[ImGuiCol_PlotLines] = brown;
|
||||||
|
colors[ImGuiCol_PlotLinesHovered] = brass;
|
||||||
|
colors[ImGuiCol_PlotHistogram] = brown;
|
||||||
|
colors[ImGuiCol_PlotHistogramHovered] = brass;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Dark variant — leather pen case with warm metal and cream accents
|
||||||
|
static inline void
|
||||||
|
ApplyLeuchtturmDarkTheme()
|
||||||
|
{
|
||||||
|
const ImVec4 bg0 = RGBA(0x1C1610); // dark leather
|
||||||
|
const ImVec4 bg1 = RGBA(0x251E16); // slightly lighter
|
||||||
|
const ImVec4 bg2 = RGBA(0x30281E); // UI elements
|
||||||
|
const ImVec4 bg3 = RGBA(0x3E3428); // hover/active
|
||||||
|
const ImVec4 ink = RGBA(0xE5DDD0); // warm cream text
|
||||||
|
const ImVec4 dim = RGBA(0x978E7C); // secondary text
|
||||||
|
const ImVec4 border = RGBA(0x4A3E30); // subtle borders
|
||||||
|
|
||||||
|
const ImVec4 brass = RGBA(0xB8A060); // polished brass
|
||||||
|
const ImVec4 brown = RGBA(0x8B6848); // bronze pen
|
||||||
|
|
||||||
|
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 = 12.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 = 0.0f;
|
||||||
|
|
||||||
|
ImVec4 *colors = style.Colors;
|
||||||
|
colors[ImGuiCol_Text] = ink;
|
||||||
|
colors[ImGuiCol_TextDisabled] = dim;
|
||||||
|
colors[ImGuiCol_WindowBg] = bg0;
|
||||||
|
colors[ImGuiCol_ChildBg] = bg0;
|
||||||
|
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] = bg0;
|
||||||
|
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabHovered] = border;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabActive] = dim;
|
||||||
|
|
||||||
|
colors[ImGuiCol_CheckMark] = brass;
|
||||||
|
colors[ImGuiCol_SliderGrab] = brass;
|
||||||
|
colors[ImGuiCol_SliderGrabActive] = brown;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Button] = bg2;
|
||||||
|
colors[ImGuiCol_ButtonHovered] = bg3;
|
||||||
|
colors[ImGuiCol_ButtonActive] = bg1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Header] = bg2;
|
||||||
|
colors[ImGuiCol_HeaderHovered] = bg3;
|
||||||
|
colors[ImGuiCol_HeaderActive] = bg3;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Separator] = border;
|
||||||
|
colors[ImGuiCol_SeparatorHovered] = bg3;
|
||||||
|
colors[ImGuiCol_SeparatorActive] = brass;
|
||||||
|
|
||||||
|
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
|
||||||
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(brass.x, brass.y, brass.z, 0.50f);
|
||||||
|
colors[ImGuiCol_ResizeGripActive] = brass;
|
||||||
|
|
||||||
|
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] = border;
|
||||||
|
colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f);
|
||||||
|
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f);
|
||||||
|
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f);
|
||||||
|
|
||||||
|
colors[ImGuiCol_TextSelectedBg] = ImVec4(brass.x, brass.y, brass.z, 0.22f);
|
||||||
|
colors[ImGuiCol_DragDropTarget] = brass;
|
||||||
|
colors[ImGuiCol_NavHighlight] = brass;
|
||||||
|
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||||
|
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f);
|
||||||
|
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f);
|
||||||
|
colors[ImGuiCol_PlotLines] = brass;
|
||||||
|
colors[ImGuiCol_PlotLinesHovered] = brown;
|
||||||
|
colors[ImGuiCol_PlotHistogram] = brass;
|
||||||
|
colors[ImGuiCol_PlotHistogramHovered] = brown;
|
||||||
|
}
|
||||||
+203
@@ -0,0 +1,203 @@
|
|||||||
|
// themes/Tufte.h — Edward Tufte inspired ImGui theme (header-only)
|
||||||
|
// Warm cream paper, dark ink, minimal chrome, restrained accent colors.
|
||||||
|
#pragma once
|
||||||
|
#include "ThemeHelpers.h"
|
||||||
|
|
||||||
|
// Light variant (primary — Tufte's books are fundamentally light)
|
||||||
|
static inline void
|
||||||
|
ApplyTufteLightTheme()
|
||||||
|
{
|
||||||
|
// Tufte palette: warm cream paper with near-black ink
|
||||||
|
const ImVec4 paper = RGBA(0xFFFFF8); // Tufte's signature warm white
|
||||||
|
const ImVec4 bg1 = RGBA(0xF4F0E8); // slightly darker cream
|
||||||
|
const ImVec4 bg2 = RGBA(0xEAE6DE); // UI elements
|
||||||
|
const ImVec4 bg3 = RGBA(0xDDD9D1); // hover/active
|
||||||
|
const ImVec4 ink = RGBA(0x111111); // near-black text
|
||||||
|
const ImVec4 dim = RGBA(0x6B6B6B); // disabled/secondary text
|
||||||
|
const ImVec4 border = RGBA(0xD0CCC4); // subtle borders
|
||||||
|
|
||||||
|
// Tufte uses color sparingly: muted red for emphasis, navy for links
|
||||||
|
const ImVec4 red = RGBA(0xA00000); // restrained dark red
|
||||||
|
const ImVec4 blue = RGBA(0x1F3F6F); // dark navy
|
||||||
|
|
||||||
|
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 = 12.0f;
|
||||||
|
style.GrabMinSize = 10.0f;
|
||||||
|
style.WindowRounding = 0.0f; // sharp edges — typographic, not app-like
|
||||||
|
style.FrameRounding = 0.0f;
|
||||||
|
style.PopupRounding = 0.0f;
|
||||||
|
style.GrabRounding = 0.0f;
|
||||||
|
style.TabRounding = 0.0f;
|
||||||
|
style.WindowBorderSize = 1.0f;
|
||||||
|
style.FrameBorderSize = 0.0f; // minimal frame borders
|
||||||
|
|
||||||
|
ImVec4 *colors = style.Colors;
|
||||||
|
colors[ImGuiCol_Text] = ink;
|
||||||
|
colors[ImGuiCol_TextDisabled] = dim;
|
||||||
|
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] = border;
|
||||||
|
|
||||||
|
colors[ImGuiCol_CheckMark] = ink;
|
||||||
|
colors[ImGuiCol_SliderGrab] = ink;
|
||||||
|
colors[ImGuiCol_SliderGrabActive] = blue;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Button] = bg2;
|
||||||
|
colors[ImGuiCol_ButtonHovered] = bg3;
|
||||||
|
colors[ImGuiCol_ButtonActive] = bg1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Header] = bg2;
|
||||||
|
colors[ImGuiCol_HeaderHovered] = bg3;
|
||||||
|
colors[ImGuiCol_HeaderActive] = bg3;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Separator] = border;
|
||||||
|
colors[ImGuiCol_SeparatorHovered] = bg3;
|
||||||
|
colors[ImGuiCol_SeparatorActive] = red;
|
||||||
|
|
||||||
|
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
|
||||||
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(red.x, red.y, red.z, 0.50f);
|
||||||
|
colors[ImGuiCol_ResizeGripActive] = red;
|
||||||
|
|
||||||
|
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] = border;
|
||||||
|
colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f);
|
||||||
|
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f);
|
||||||
|
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f);
|
||||||
|
|
||||||
|
colors[ImGuiCol_TextSelectedBg] = ImVec4(red.x, red.y, red.z, 0.15f);
|
||||||
|
colors[ImGuiCol_DragDropTarget] = red;
|
||||||
|
colors[ImGuiCol_NavHighlight] = red;
|
||||||
|
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||||
|
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f);
|
||||||
|
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f);
|
||||||
|
colors[ImGuiCol_PlotLines] = blue;
|
||||||
|
colors[ImGuiCol_PlotLinesHovered] = red;
|
||||||
|
colors[ImGuiCol_PlotHistogram] = blue;
|
||||||
|
colors[ImGuiCol_PlotHistogramHovered] = red;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Dark variant — warm charcoal with cream ink, same restrained accents
|
||||||
|
static inline void
|
||||||
|
ApplyTufteDarkTheme()
|
||||||
|
{
|
||||||
|
const ImVec4 bg0 = RGBA(0x1C1B19); // warm near-black
|
||||||
|
const ImVec4 bg1 = RGBA(0x252420); // slightly lighter
|
||||||
|
const ImVec4 bg2 = RGBA(0x302F2A); // UI elements
|
||||||
|
const ImVec4 bg3 = RGBA(0x3D3C36); // hover/active
|
||||||
|
const ImVec4 ink = RGBA(0xEAE6DE); // cream text (inverted paper)
|
||||||
|
const ImVec4 dim = RGBA(0x9A9690); // disabled text
|
||||||
|
const ImVec4 border = RGBA(0x4A4840); // subtle borders
|
||||||
|
|
||||||
|
const ImVec4 red = RGBA(0xD06060); // warmer red for dark bg
|
||||||
|
const ImVec4 blue = RGBA(0x7098C0); // lighter navy for dark bg
|
||||||
|
|
||||||
|
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 = 12.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 = 0.0f;
|
||||||
|
|
||||||
|
ImVec4 *colors = style.Colors;
|
||||||
|
colors[ImGuiCol_Text] = ink;
|
||||||
|
colors[ImGuiCol_TextDisabled] = dim;
|
||||||
|
colors[ImGuiCol_WindowBg] = bg0;
|
||||||
|
colors[ImGuiCol_ChildBg] = bg0;
|
||||||
|
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] = bg0;
|
||||||
|
colors[ImGuiCol_ScrollbarGrab] = bg3;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabHovered] = border;
|
||||||
|
colors[ImGuiCol_ScrollbarGrabActive] = dim;
|
||||||
|
|
||||||
|
colors[ImGuiCol_CheckMark] = ink;
|
||||||
|
colors[ImGuiCol_SliderGrab] = ink;
|
||||||
|
colors[ImGuiCol_SliderGrabActive] = blue;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Button] = bg2;
|
||||||
|
colors[ImGuiCol_ButtonHovered] = bg3;
|
||||||
|
colors[ImGuiCol_ButtonActive] = bg1;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Header] = bg2;
|
||||||
|
colors[ImGuiCol_HeaderHovered] = bg3;
|
||||||
|
colors[ImGuiCol_HeaderActive] = bg3;
|
||||||
|
|
||||||
|
colors[ImGuiCol_Separator] = border;
|
||||||
|
colors[ImGuiCol_SeparatorHovered] = bg3;
|
||||||
|
colors[ImGuiCol_SeparatorActive] = red;
|
||||||
|
|
||||||
|
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
|
||||||
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(red.x, red.y, red.z, 0.50f);
|
||||||
|
colors[ImGuiCol_ResizeGripActive] = red;
|
||||||
|
|
||||||
|
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] = border;
|
||||||
|
colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f);
|
||||||
|
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f);
|
||||||
|
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f);
|
||||||
|
|
||||||
|
colors[ImGuiCol_TextSelectedBg] = ImVec4(red.x, red.y, red.z, 0.20f);
|
||||||
|
colors[ImGuiCol_DragDropTarget] = red;
|
||||||
|
colors[ImGuiCol_NavHighlight] = red;
|
||||||
|
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
|
||||||
|
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f);
|
||||||
|
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f);
|
||||||
|
colors[ImGuiCol_PlotLines] = blue;
|
||||||
|
colors[ImGuiCol_PlotLinesHovered] = red;
|
||||||
|
colors[ImGuiCol_PlotHistogram] = blue;
|
||||||
|
colors[ImGuiCol_PlotHistogramHovered] = red;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user