From 6413e1445558ee2c62a3d44d70992363c877c9c6 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 14 Apr 2026 15:03:25 -0700 Subject: [PATCH] Fix writing mode: prevent per-frame override and disable syntax highlighting apply_syntax_to_buffer() was called every frame and unconditionally reset edit mode from the file extension, making it impossible to toggle out of writing mode for .txt/.md files. Add edit_mode_detected_ flag to Buffer so auto-detection runs once per buffer. Writing mode now also disables syntax highlighting as intended. Propagate edit_mode_ through Buffer copy/move ops. Co-Authored-By: Claude Opus 4.6 (1M context) --- Buffer.cc | 54 ++++++++++++---------- Buffer.h | 9 ++++ CLAUDE.md | 115 +++++++++++++++++++++++++++++++++++++++++++++++ Command.cc | 3 ++ ImGuiFrontend.cc | 8 ++-- 5 files changed, 163 insertions(+), 26 deletions(-) create mode 100644 CLAUDE.md diff --git a/Buffer.cc b/Buffer.cc index 3f706e4..f2c12c9 100644 --- a/Buffer.cc +++ b/Buffer.cc @@ -231,10 +231,12 @@ Buffer::Buffer(const Buffer &other) mark_set_ = other.mark_set_; mark_curx_ = other.mark_curx_; mark_cury_ = other.mark_cury_; - // Copy syntax/highlighting flags - version_ = other.version_; - syntax_enabled_ = other.syntax_enabled_; - filetype_ = other.filetype_; + // Copy edit mode + syntax/highlighting flags + edit_mode_ = other.edit_mode_; + edit_mode_detected_ = other.edit_mode_detected_; + version_ = other.version_; + syntax_enabled_ = other.syntax_enabled_; + filetype_ = other.filetype_; // Fresh undo system for the copy undo_tree_ = std::make_unique(); undo_sys_ = std::make_unique(*this, *undo_tree_); @@ -279,11 +281,13 @@ Buffer::operator=(const Buffer &other) dirty_ = other.dirty_; read_only_ = other.read_only_; mark_set_ = other.mark_set_; - mark_curx_ = other.mark_curx_; - mark_cury_ = other.mark_cury_; - version_ = other.version_; - syntax_enabled_ = other.syntax_enabled_; - filetype_ = other.filetype_; + mark_curx_ = other.mark_curx_; + mark_cury_ = other.mark_cury_; + edit_mode_ = other.edit_mode_; + edit_mode_detected_ = other.edit_mode_detected_; + version_ = other.version_; + syntax_enabled_ = other.syntax_enabled_; + filetype_ = other.filetype_; // Recreate undo system for this instance undo_tree_ = std::make_unique(); undo_sys_ = std::make_unique(*this, *undo_tree_); @@ -326,13 +330,15 @@ Buffer::Buffer(Buffer &&other) noexcept undo_tree_(std::move(other.undo_tree_)), undo_sys_(std::move(other.undo_sys_)) { - // Move syntax/highlighting state - version_ = other.version_; - syntax_enabled_ = other.syntax_enabled_; - filetype_ = std::move(other.filetype_); - highlighter_ = std::move(other.highlighter_); - content_ = std::move(other.content_); - rows_cache_dirty_ = other.rows_cache_dirty_; + // Move edit mode + syntax/highlighting state + edit_mode_ = other.edit_mode_; + edit_mode_detected_ = other.edit_mode_detected_; + version_ = other.version_; + syntax_enabled_ = other.syntax_enabled_; + filetype_ = std::move(other.filetype_); + highlighter_ = std::move(other.highlighter_); + content_ = std::move(other.content_); + rows_cache_dirty_ = other.rows_cache_dirty_; // Update UndoSystem's buffer reference to point to this object if (undo_sys_) { undo_sys_->UpdateBufferReference(*this); @@ -364,13 +370,15 @@ Buffer::operator=(Buffer &&other) noexcept undo_tree_ = std::move(other.undo_tree_); undo_sys_ = std::move(other.undo_sys_); - // Move syntax/highlighting state - version_ = other.version_; - syntax_enabled_ = other.syntax_enabled_; - filetype_ = std::move(other.filetype_); - highlighter_ = std::move(other.highlighter_); - content_ = std::move(other.content_); - rows_cache_dirty_ = other.rows_cache_dirty_; + // Move edit mode + syntax/highlighting state + edit_mode_ = other.edit_mode_; + edit_mode_detected_ = other.edit_mode_detected_; + version_ = other.version_; + syntax_enabled_ = other.syntax_enabled_; + filetype_ = std::move(other.filetype_); + highlighter_ = std::move(other.highlighter_); + content_ = std::move(other.content_); + rows_cache_dirty_ = other.rows_cache_dirty_; // Update UndoSystem's buffer reference to point to this object if (undo_sys_) { undo_sys_->UpdateBufferReference(*this); diff --git a/Buffer.h b/Buffer.h index 7b545ec..13d12a4 100644 --- a/Buffer.h +++ b/Buffer.h @@ -517,6 +517,7 @@ public: void SetEditMode(EditMode m) { edit_mode_ = m; + edit_mode_detected_ = true; } @@ -525,6 +526,13 @@ public: edit_mode_ = (edit_mode_ == EditMode::Code) ? EditMode::Writing : EditMode::Code; + edit_mode_detected_ = true; + } + + + [[nodiscard]] bool EditModeDetected() const + { + return edit_mode_detected_; } @@ -660,6 +668,7 @@ private: // Edit mode (code vs writing) EditMode edit_mode_ = EditMode::Code; + bool edit_mode_detected_ = false; // true after initial auto-detection // Syntax/highlighting state std::uint64_t version_ = 0; // increment on edits diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0f5a0b6 --- /dev/null +++ b/CLAUDE.md @@ -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) diff --git a/Command.cc b/Command.cc index 1cf5957..67f479e 100644 --- a/Command.cc +++ b/Command.cc @@ -1363,6 +1363,9 @@ cmd_toggle_edit_mode(const CommandContext &ctx) 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; diff --git a/ImGuiFrontend.cc b/ImGuiFrontend.cc index bf27620..d0b34fc 100644 --- a/ImGuiFrontend.cc +++ b/ImGuiFrontend.cc @@ -39,11 +39,13 @@ apply_syntax_to_buffer(Buffer *b, const GUIConfig &cfg) if (!b) return; - // Auto-detect edit mode from file extension - if (!b->Filename().empty()) + // 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 (cfg.syntax) { + // Writing mode disables syntax; otherwise follow the global config. + if (cfg.syntax && b->GetEditMode() != EditMode::Writing) { b->SetSyntaxEnabled(true); b->EnsureHighlighter(); if (auto *eng = b->Highlighter()) {