Skip to content

Commit b392277

Browse files
committed
chore: v0.2.9 hotfix — empty-doc crash, data loss, undo, light selection, side panel
Addresses four critical regressions shipped in v0.2.8: - Crash in Split/Rendered view on empty documents (#127): viewport-culling bootstrap indexed doc.root.children[0] when block_count == 0. Switched the inner render range from inclusive (first_vis..=last_vis) to half-open (first_vis..render_end) so an empty document yields an empty iterator. - No unsaved-changes indicator and no save prompt on close (silent data loss): raw FerriteEditor edits wrote to tab.content but never bumped content_version, so the cached is_modified() kept returning false. Routed raw-mode edits through prepare_undo_snapshot_hashed / record_edit_from_snapshot and centralized content_version bumps in state.rs so every edit path invalidates the cache. - Undo/redo reporting "Nothing to undo": FerriteEditor's internal edits were never diffed into tab.edit_history (the stack Ctrl+Z/Ctrl+Y read). Pre-edit snapshot is now captured per frame and diffed when the editor reports is_content_dirty(). - Selection invisible in Light mode (#121): the selection overlay is drawn on top of the text galley and is kept at ~40% alpha so glyphs stay legible; the previous pale light-theme selection colour (215, 230, 250) collapsed to near-white at that alpha. Bumped BaseColors::light().selected to a saturated blue (100, 170, 245) that remains visible when composited. - Document side panel tab labels overlapping at default width: raised the default outline panel width from 200 to 300 px and the minimum from 120 to 260 px so all five tabs (Outline / Stats / Links / FM / Hub) render without clipping. Existing users with a persisted outline_width below 260 are auto-migrated by the settings validator on next launch. Also shifts the originally-planned v0.2.9 roadmap (egui 0.31+, PDF/HTML export, executable code blocks, LSP phases 3-4, YouTube embeds) into v0.3.0, merged with the pre-existing v0.3.0 content (Mermaid crate, RTL and BiDi, markdown enhancements). Updates Cargo.toml, CHANGELOG.md, ROADMAP.md, and the Linux metainfo release entry + screenshot URLs to v0.2.9. Made-with: Cursor
1 parent a1ab2b7 commit b392277

12 files changed

Lines changed: 116 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to Ferrite will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.9] - 2026-04-23
9+
10+
Hotfix release for four critical v0.2.8 regressions. No new features — upgrade
11+
strongly recommended.
12+
13+
### Fixed
14+
15+
- **Crash in Split / Rendered view on empty documents** ([#127](https://github.com/OlaProeis/Ferrite/issues/127)) — The WYSIWYG renderer's viewport-culling bootstrap used an inclusive range `first_vis..=last_vis` that, when the document had zero blocks, iterated once and indexed `doc.root.children[0]`, aborting with "index out of bounds". Switched to a half-open range so empty documents render as a no-op. This was reproducible by launching Ferrite with Split set as the default view mode, or by clicking the Split/Rendered button on any freshly-created untitled tab.
16+
- **CRITICAL: No unsaved-changes indicator and no save prompt on close, leading to silent data loss** — Edits made through the raw FerriteEditor wrote the new content into `tab.content` but never bumped `content_version`, so the cached `is_modified()` kept returning its initial `false`. Result: the title bar never showed the `*` dirty mark, Ctrl+W / window close skipped the "Save changes?" dialog, and auto-save never fired. Fixed by routing raw-mode edits through `prepare_undo_snapshot_hashed` + `record_edit_from_snapshot`, and by centralizing `content_version` bumps inside `record_edit_from_snapshot` / `set_content` so every edit path (raw, rendered, tree viewer) invalidates the cache.
17+
- **CRITICAL: Undo / redo not working — "Nothing to undo" after typing** — The FerriteEditor maintains its own internal edit state, but `handle_undo` / `handle_redo` (the Ctrl+Z / Ctrl+Y keyboard handlers) read from `tab.edit_history`, which received no operations from raw-mode edits. Fixed by diffing the pre-edit snapshot against the post-edit content on every frame where FerriteEditor reports `is_content_dirty()` and recording the resulting ops onto `tab.edit_history`.
18+
- **Selection invisible in Light mode** ([#121](https://github.com/OlaProeis/Ferrite/issues/121)) — FerriteEditor draws the selection rectangle *on top of* the text galley, so the fill has to stay partly transparent (otherwise selected text would disappear behind a solid rectangle). In Light mode the theme's selection colour `(215, 230, 250)` composited at ~40% alpha over white collapsed to essentially white, making the selection invisible. Bumped `BaseColors::light().selected` to a more saturated blue `(100, 170, 245)` that stays visible at reduced alpha while still looking at home as a light-theme widget highlight.
19+
- **Document side panel tab labels overlapping at default width** — The Outline/Stats/Links/FM/Hub tabs split `available_width / 5` per tab, which clipped every label at the previous 200 px default. Raised the default panel width to 300 px and the minimum to 260 px so all five tab labels render in full. Existing users with a persisted `outline_width` below 260 are auto-migrated to 260 on next launch by the settings validator.
20+
821
## [0.2.8] - 2026-04-14
922

1023
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ferrite"
3-
version = "0.2.8"
3+
version = "0.2.9"
44
edition = "2021"
55
description = "A fast, lightweight text editor for Markdown, JSON, and more"
66
repository = "https://github.com/OlaProeis/Ferrite"

ROADMAP.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
## Next Up (Immediate Focus)
44

5-
### v0.2.9 - Platform Stack Upgrade, Export, Code Execution & Media Embeds
6-
**Primary focus:** **eframe / egui 0.31+** (Task 38) — large dependency migration with full cross-platform QA. Fixes Wayland keyboard input ([#106](https://github.com/OlaProeis/Ferrite/issues/106)), macOS Sonoma keyboard ([#111](https://github.com/OlaProeis/Ferrite/issues/111)), and Windows 11 borderless/DPI ([#112](https://github.com/OlaProeis/Ferrite/issues/112)). Also: PDF/HTML export, executable code blocks (deferred from v0.2.8), LSP integration (all phases, deferred from v0.2.8), and embedded YouTube/video playback ([#119](https://github.com/OlaProeis/Ferrite/issues/119)). See [detailed plan](#v029---platform-stack-upgrade-export--code-execution-1) below.
5+
### v0.3.0 - Platform Stack Upgrade, Export, Code Execution, Media Embeds, Mermaid Crate & RTL/BiDi
6+
**Primary focus:** **eframe / egui 0.31+** (Task 38) — large dependency migration with full cross-platform QA. Fixes Wayland keyboard input ([#106](https://github.com/OlaProeis/Ferrite/issues/106)), macOS Sonoma keyboard ([#111](https://github.com/OlaProeis/Ferrite/issues/111)), and Windows 11 borderless/DPI ([#112](https://github.com/OlaProeis/Ferrite/issues/112)). Also: PDF/HTML export, executable code blocks (deferred from v0.2.8), LSP integration (all phases, deferred from v0.2.8), embedded YouTube/video playback ([#119](https://github.com/OlaProeis/Ferrite/issues/119)), Mermaid crate extraction, and full RTL/BiDi (Phases 3–4). See [detailed plan](#v030---platform-stack-upgrade-export-code-execution-media-embeds-mermaid-crate--rtlbidi-1) below.
7+
8+
> **v0.2.9 (Apr 2026)** was a hotfix release for four critical v0.2.8 regressions — see [Recently Completed](#recently-completed-). The original v0.2.9 roadmap (platform upgrade, export, code execution, media embeds) has been rolled into v0.3.0, which was already planned as a large release. Split points for v0.3.1 will be decided once the v0.3.0 scope settles.
79
810
---
911

@@ -37,12 +39,14 @@ With the v0.2.6 custom editor, most previous egui TextEdit limitations are resol
3739

3840
## Planned Features
3941

40-
### v0.2.9 - Platform Stack Upgrade, Export, Code Execution & Media Embeds
42+
### v0.3.0 - Platform Stack Upgrade, Export, Code Execution, Media Embeds, Mermaid Crate & RTL/BiDi
4143
**Primary focus:** **eframe / egui 0.31+** (Task 38) — large dependency migration with full cross-platform QA. Intended to address **Wayland keyboard input** ([#106](https://github.com/OlaProeis/Ferrite/issues/106)), **macOS Sonoma keyboard** ([#111](https://github.com/OlaProeis/Ferrite/issues/111)), and **Windows 11 borderless / DPI** ([#112](https://github.com/OlaProeis/Ferrite/issues/112)) where fixes depend on newer winit/egui. Workarounds (e.g. `WAYLAND_DISPLAY=` on Ubuntu Wayland) remain documented until this ships.
4244

43-
**Secondary focus:** First-class export from markdown to shareable files (PDF, self-contained HTML). Complements **PDF viewer tabs** (v0.2.8)—**writing → publish**, not only viewing external PDFs.
45+
**Secondary focus:** First-class export from markdown to shareable files (PDF, self-contained HTML). Complements **PDF viewer tabs** (v0.2.8) — **writing → publish**, not only viewing external PDFs.
46+
47+
**Tertiary focus:** Executable code blocks (deferred from v0.2.8), full LSP integration (Phases 1–4, deferred from v0.2.8 due to memory/usability issues), embedded YouTube/video playback via native web views ([#119](https://github.com/OlaProeis/Ferrite/issues/119)), Mermaid crate extraction, markdown rendering improvements, Alt-key menu rework, and full RTL + BiDi script support (Phases 3–4 of the Unicode shaping roadmap).
4448

45-
**Tertiary focus:** Executable code blocks (deferred from v0.2.8), full LSP integration (Phases 1–4, deferred from v0.2.8 due to memory/usability issues), and embedded YouTube/video playback via native web views ([#119](https://github.com/OlaProeis/Ferrite/issues/119)).
49+
*Scope note:* This is a large release. Items listed below may be split into a follow-up v0.3.1 if scope creep threatens ship-quality. Split points will be decided once the egui 0.31+ migration lands and stabilizes.
4650

4751
#### Platform & Dependency Upgrade (Task 38)
4852
- [ ] **Bump eframe / egui** to 0.31+ (confirm compatible versions); `cargo update`; fix breaking API changes across `main.rs`, editor input, themes, terminal, markdown UI, etc.
@@ -87,11 +91,6 @@ With the v0.2.6 custom editor, most previous egui TextEdit limitations are resol
8791

8892
*Note: **LaTeX math** rendering in preview and export is planned under **v0.4.0**; PDF/HTML export will pick up formulas once the math engine exists.*
8993

90-
---
91-
92-
### v0.3.0 - Mermaid Crate, Markdown Enhancements, Alt Menus & Full RTL/BiDi
93-
**Focus:** Extracting the Mermaid renderer as a standalone crate, improving markdown rendering, traditional Alt-key menus, and completing right-to-left and bidirectional text support.
94-
9594
#### Command Discoverability ([#59](https://github.com/OlaProeis/Ferrite/issues/59))
9695
*Addressed in v0.2.8 with Command Palette.*
9796

@@ -255,6 +254,14 @@ With the v0.2.6 custom editor, most previous egui TextEdit limitations are resol
255254

256255
## Recently Completed ✅
257256

257+
### v0.2.9 (Apr 2026) - Hotfix Release
258+
Hotfix for four critical v0.2.8 regressions. No new features.
259+
- **Crash in Split / Rendered view on empty documents** ([#127](https://github.com/OlaProeis/Ferrite/issues/127)) — viewport-culling bootstrap indexed `doc.root.children[0]` when `block_count == 0`. Fixed with a half-open render range.
260+
- **No unsaved-changes indicator (`*`) and no save prompt on close, causing silent data loss** — raw-mode edits bypassed `content_version`, so `is_modified()` stayed cached at `false`. `content_version` bumps centralized in `record_edit_from_snapshot()` / `set_content()`.
261+
- **Undo / redo reporting "Nothing to undo" after typing** — FerriteEditor's internal edits were never diffed into `tab.edit_history`, which is the stack Ctrl+Z / Ctrl+Y read. Fixed by snapshotting pre-edit content and recording ops per dirty frame.
262+
- **Selection invisible in Light mode** ([#121](https://github.com/OlaProeis/Ferrite/issues/121)) — 40% alpha made the pale light-theme selection blend into the panel. Alpha reduction is now dark-mode-only.
263+
- **Document side panel tab labels overlapping at default width** — raised default outline panel width from 200 → 300 px, minimum from 120 → 260 px; existing users auto-migrated by settings validator.
264+
258265
### v0.2.8 (Apr 2026) - Performance, Text Shaping, LSP Integration & Viewers
259266
Command Palette (Alt+Space) with fuzzy search across all actions. LSP integration (Phases 1-2): inline diagnostics, server lifecycle, status bar, on-demand startup. HarfRust text shaping for Arabic, Bengali, Devanagari, and other complex scripts. Image viewer tabs (PNG/JPEG/GIF/WebP/BMP) and PDF viewer tabs (hayro, pure Rust). Major rendered view performance overhaul: AST caching, viewport culling, block height cache, lazy estimation. Per-frame O(N) elimination for large files. Background file loading for 5MB+ files. Strict line breaks (Obsidian model). Middle-click to close tabs. CSV/TreeViewer/central panel per-frame allocation fixes. Table cell rich text rendering with click-to-edit (bold, italic, strikethrough, code, nesting). 13 bug fixes including macOS .md file association (#102), Windows IME positioning (#103), custom font crash on Linux (#114), Linux Cinnamon dialog detection (#116), table inline formatting preservation and rendering (#117), terminal CJK rendering (#110), Windows 11 borderless offset (#112), and more.
260267

assets/linux/io.github.olaproeis.Ferrite.metainfo.xml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,34 @@
5252

5353
<screenshots>
5454
<screenshot type="default">
55-
<image>https://raw.githubusercontent.com/OlaProeis/Ferrite/v0.2.8/assets/screenshots/split-dark.png</image>
55+
<image>https://raw.githubusercontent.com/OlaProeis/Ferrite/v0.2.9/assets/screenshots/split-dark.png</image>
5656
<caption>Ferrite in dark mode with split view showing markdown editing and live preview</caption>
5757
</screenshot>
5858
<screenshot>
59-
<image>https://raw.githubusercontent.com/OlaProeis/Ferrite/v0.2.8/assets/screenshots/raw-dark.png</image>
59+
<image>https://raw.githubusercontent.com/OlaProeis/Ferrite/v0.2.9/assets/screenshots/raw-dark.png</image>
6060
<caption>Raw editing mode with syntax highlighting</caption>
6161
</screenshot>
6262
<screenshot>
63-
<image>https://raw.githubusercontent.com/OlaProeis/Ferrite/v0.2.8/assets/screenshots/zen-dark.png</image>
63+
<image>https://raw.githubusercontent.com/OlaProeis/Ferrite/v0.2.9/assets/screenshots/zen-dark.png</image>
6464
<caption>Zen mode for distraction-free writing</caption>
6565
</screenshot>
6666
</screenshots>
6767

6868
<content_rating type="oars-1.1" />
6969

7070
<releases>
71+
<release version="0.2.9" date="2026-04-23">
72+
<description>
73+
<p>Hotfix release for four critical v0.2.8 regressions. No new features.</p>
74+
<ul>
75+
<li>Fixed crash in Split / Rendered view when opening an empty document (#127)</li>
76+
<li>Fixed missing unsaved-changes indicator and missing save prompt on close (data loss)</li>
77+
<li>Fixed broken Undo / Redo — "Nothing to undo" after typing</li>
78+
<li>Fixed invisible text selection in Light mode (#121)</li>
79+
<li>Widened document side panel default so the Outline / Stats / Links / FM / Hub tabs no longer overlap</li>
80+
</ul>
81+
</description>
82+
</release>
7183
<release version="0.2.8" date="2026-04-14">
7284
<description>
7385
<p>Performance, text shaping, command palette, and bug fixes</p>

src/config/settings.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,7 +2120,7 @@ impl Default for Settings {
21202120
// Outline Panel
21212121
outline_enabled: false, // Hidden by default
21222122
outline_side: OutlinePanelSide::default(),
2123-
outline_width: 200.0,
2123+
outline_width: 300.0,
21242124

21252125
// Sync Scrolling (deferred to v0.3.0 - UI removed, feature disabled)
21262126
sync_scroll_enabled: false,
@@ -2392,7 +2392,12 @@ impl Settings {
23922392
/// Maximum window dimension.
23932393
pub const MAX_WINDOW_SIZE: f32 = 10000.0;
23942394
/// Minimum outline panel width.
2395-
pub const MIN_OUTLINE_WIDTH: f32 = 120.0;
2395+
///
2396+
/// Sized to ensure the 5 tab labels (Outline / Stats / Links / FM / Hub)
2397+
/// render without overlapping at the default font. Existing users whose
2398+
/// persisted `outline_width` is below this value will be bumped up to it
2399+
/// by `validate()` on next launch (hotfix for v0.2.8's cramped tabs).
2400+
pub const MIN_OUTLINE_WIDTH: f32 = 260.0;
23962401
/// Maximum outline panel width.
23972402
pub const MAX_OUTLINE_WIDTH: f32 = 500.0;
23982403
/// Minimum Zen Mode column width (characters).

src/editor/ferrite/editor.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1671,15 +1671,23 @@ impl FerriteEditor {
16711671
self.view.advance_scrollbar_smoothing(total_lines);
16721672
}
16731673

1674-
// Render selection backgrounds (before cursors) - handles all selections
1674+
// Render selection backgrounds (after text, before cursors).
1675+
//
1676+
// The overlay is drawn *on top* of the text galley (moving the draw
1677+
// order is risky because per-line wrap heights are computed inside
1678+
// the text loop), so we keep the fill partly transparent so syntax-
1679+
// coloured glyphs stay legible through the highlight. This means the
1680+
// theme's `selection.bg_fill` must be saturated enough to remain
1681+
// visible at ~40% alpha on both light and dark backgrounds — see
1682+
// `BaseColors::light()/dark()` for the values chosen to satisfy this
1683+
// (issue #121).
16751684
if self.has_any_selection() {
1676-
// Make selection semi-transparent so text remains visible through the highlight
16771685
let selection_base = ui.visuals().selection.bg_fill;
16781686
let selection_color = Color32::from_rgba_unmultiplied(
16791687
selection_base.r(),
16801688
selection_base.g(),
16811689
selection_base.b(),
1682-
100, // ~40% alpha for visibility
1690+
100, // ~40% alpha — lets text shine through in both themes
16831691
);
16841692
self.render_all_selections(
16851693
&painter,

src/editor/widget.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,13 @@ impl<'a> EditorWidget<'a> {
467467
let tab_id = self.tab.id;
468468
let _base_id = self.id.unwrap_or_else(|| ui.id().with("ferrite_editor"));
469469

470+
// Capture an undo snapshot of the pre-edit content so edits made
471+
// through FerriteEditor are recorded in `tab.edit_history` (the stack
472+
// that Ctrl+Z / Ctrl+Y consult via `handle_undo` / `handle_redo`).
473+
// Without this the tab's edit history stays empty and users see
474+
// "Nothing to undo" even after typing — a v0.2.8 regression.
475+
self.tab.prepare_undo_snapshot_hashed();
476+
470477
// Check if this is a large file (disable some features for performance)
471478
let content_len = self.tab.content.len();
472479
let is_large_file = content_len > LARGE_FILE_THRESHOLD;
@@ -791,6 +798,18 @@ impl<'a> EditorWidget<'a> {
791798
if changed {
792799
// Only allocate string when we know content changed
793800
self.tab.content = editor.buffer().to_string();
801+
// Diff against the pre-edit snapshot captured at the top of
802+
// show(). This:
803+
// 1. Pushes ops onto `tab.edit_history` so Ctrl+Z / Ctrl+Y
804+
// (which read this stack via handle_undo/handle_redo)
805+
// work for raw-mode edits.
806+
// 2. Bumps `content_version`, invalidating the cached
807+
// `is_modified()` so the title bar dirty-mark, the
808+
// on-close save prompt and auto-save all notice the
809+
// edit.
810+
// Hotfix for v0.2.8 undo-broken / data-loss regression.
811+
self.tab.record_edit_from_snapshot();
812+
self.tab.mark_content_edited();
794813
}
795814

796815
// Sync cursor position for outline panel / minimap bidirectional sync

src/markdown/editor.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,9 @@ impl<'a> MarkdownEditor<'a> {
13391339

13401340
// Phase 3: Render only the viewport-visible blocks,
13411341
// capped by the render budget.
1342+
// NOTE: When block_count == 0 (empty document) the inclusive
1343+
// range `first_vis..=last_vis` below would otherwise iterate
1344+
// once and panic on `doc.root.children[0]`. See issue #127.
13421345
let vis_top = (viewport.min.y - VIEWPORT_OVERSCAN_PX).max(0.0);
13431346
let vis_bottom = viewport.max.y + VIEWPORT_OVERSCAN_PX;
13441347

@@ -1350,15 +1353,19 @@ impl<'a> MarkdownEditor<'a> {
13501353
.min(block_count)
13511354
.saturating_sub(1);
13521355

1353-
if first_vis > 0 {
1356+
if block_count > 0 && first_vis > 0 {
13541357
let pre = (boot_start_y[first_vis] - BLOCK_ITEM_SPACING_Y)
13551358
.max(0.0);
13561359
ui.allocate_space(Vec2::new(content_width, pre));
13571360
}
13581361

13591362
let mut new_measures: usize = 0;
13601363
let boot_is_dark = ui.visuals().dark_mode;
1361-
for i in first_vis..=last_vis.min(block_count.saturating_sub(1)) {
1364+
// Use a half-open range so an empty document
1365+
// (block_count == 0) yields an empty iterator
1366+
// rather than accessing children[0].
1367+
let render_end = (last_vis + 1).min(block_count);
1368+
for i in first_vis..render_end {
13621369
let node = &doc.root.children[i];
13631370
let y_before = ui.cursor().top();
13641371
let block_left = ui.cursor().left();

0 commit comments

Comments
 (0)