W79: MC parity — editor gutter removed, margin+char info, viewer Enter/F3

MC has no line number gutter in editor. Removed:
- Gutter rendering in editor/render.rs (body_area = inner, no gutter_area)
- relative_lines field from Editor struct (both constructors)
- ToggleRelativeLines enum variant from EditorCmd + menubar item
- Alt-N keybinding in handlers.rs + help overlay entry
- Updated 3 column-offset tests (gutter_chars was 3 chars wide)

Editor additions (MC parity):
- Right-margin indicator: vertical '│' at word_wrap_line_length (default 72)
- Character info in status bar: '0x41 065 A' format

Viewer additions (MC parity):
- Enter key bound to cursor-down
- F3 key bound to quit
- Viewer ruler removed (never existed in MC)

1433 tests passing, zero warnings.
This commit is contained in:
2026-07-08 18:00:32 +03:00
parent 89d7a27081
commit 0b9831f93d
7 changed files with 24 additions and 96 deletions
+5 -1
View File
@@ -230,7 +230,7 @@ All previously deferred items are now resolved:
- Removed column ruler (Alt-R/ToggleRuler) — CK_Ruler is an MC editor-only
feature, not present in MC viewer at all.
**W79b Editor:** Two MC parity improvements:
**W79b Editor:** Three MC parity improvements:
- Right-margin indicator: draws a vertical `│` at the word-wrap column
(default 72) in every visible body row when word-wrap is active.
Uses `word_wrap_line_length: usize` field (new in Editor struct,
@@ -239,6 +239,10 @@ All previously deferred items are now resolved:
displayable char) at cursor position between Bytes and mode tag,
mirroring MC's `editdraw.c` status-line char display. Falls back
to `.` for non-printable characters.
- Removed line number gutter — MC editor has no gutter or line numbers.
This was a TLC-only addition incompatible with MC parity. Removed:
gutter rendering, `relative_lines` field, `ToggleRelativeLines` Cmd,
Alt-N keybinding, and the Options-menu "Relative line numbers" item.
**Documented intentional divergences:**
- Viewer encoding: TLC is UTF-8 native; MC's codepage display does not
@@ -287,15 +287,6 @@ impl Editor {
});
return Some(EditorResult::Running);
}
0x6E => {
self.relative_lines = !self.relative_lines;
self.message = Some(if self.relative_lines {
"Line numbers: ON".to_string()
} else {
"Line numbers: OFF".to_string()
});
return Some(EditorResult::Running);
}
0x74 => {
let new_width = self.cycle_tab_width();
self.message = Some(format!("Tab width: {new_width}"));
@@ -75,8 +75,6 @@ pub enum EditorCmd {
ToggleWordWrap,
/// Toggle syntax highlighting (Ctrl-S).
ToggleSyntax,
/// Toggle relative line numbers (Alt-N).
ToggleRelativeLines,
/// Edit the user menu file (MC `CK_EditUserMenu`).
EditUserMenu,
/// Alt-P — reformat the paragraph at the cursor.
@@ -277,7 +275,6 @@ impl EditorMenuBar {
MenuItem::item("Show whitespace", 'w', EditorCmd::ToggleShowWhitespace),
MenuItem::item("Word wrap", 'p', EditorCmd::ToggleWordWrap),
MenuItem::item("Syntax highlight", 'h', EditorCmd::ToggleSyntax),
MenuItem::item("Relative line numbers", 'r', EditorCmd::ToggleRelativeLines),
MenuItem::separator(),
MenuItem::item("Insert/Overwrite", 'i', EditorCmd::ToggleOverwrite),
MenuItem::item("Spell check", 's', EditorCmd::SpellCheck),
+11 -26
View File
@@ -20,7 +20,7 @@
//!
//! All rendering goes through [`Editor::render`], which writes to a
//! ratatui `Frame` at the given `Rect`. The view is a simple
//! top-line/left-column scroller with line numbers in the gutter.
//! top-line/left-column scroller.
//!
//! All key handling goes through [`Editor::handle_key`], which
//! dispatches to [`Editor::handle_key_normal`],
@@ -188,9 +188,6 @@ pub struct Editor {
history_popup_selected: Option<usize>,
/// Visual shape of the editing cursor (Block/Bar/Underline).
cursor_shape: CursorShape,
/// When true, the gutter shows each non-cursor line's distance
/// from the cursor line instead of its absolute number.
relative_lines: bool,
/// When false, syntax highlighting is disabled even if a
/// highlighter exists. Toggled by Ctrl-S (MC parity).
syntax_enabled: bool,
@@ -219,8 +216,7 @@ pub struct Editor {
last_macro: Vec<macros::NamedKey>,
/// Code-fold state. `Ctrl-F1` toggles a fold at the cursor
/// line; the renderer hides lines covered by collapsed folds
/// and shows a `+` marker in the gutter on the fold's start
/// line.
/// and shows a `+` marker on the fold's start line.
folds: folding::FoldSet,
/// Tag-stack: positions saved before each `Ctrl-]` jump so
/// `Ctrl-T` can pop back. Each entry is `(byte_offset, line)`.
@@ -299,7 +295,6 @@ impl Editor {
search: SearchState::new(),
history_popup_selected: None,
cursor_shape: CursorShape::default(),
relative_lines: false,
syntax_enabled: true,
auto_indent: true,
show_whitespace: false,
@@ -352,7 +347,6 @@ impl Editor {
search: SearchState::new(),
history_popup_selected: None,
cursor_shape: CursorShape::default(),
relative_lines: false,
syntax_enabled: true,
auto_indent: true,
show_whitespace: false,
@@ -1433,22 +1427,13 @@ impl Editor {
"Syntax: OFF".to_string()
});
}
EditorCmd::ToggleRelativeLines => {
self.relative_lines = !self.relative_lines;
self.message = Some(if self.relative_lines {
"Relative line numbers: ON".to_string()
} else {
"Relative line numbers: OFF".to_string()
});
}
EditorCmd::Settings => {
let ai = if self.auto_indent { "ON" } else { "OFF" };
let ww = if self.word_wrap { "ON" } else { "OFF" };
let ws = if self.show_whitespace { "ON" } else { "OFF" };
let sy = if self.syntax_enabled { "ON" } else { "OFF" };
let rl = if self.relative_lines { "ON" } else { "OFF" };
self.message = Some(format!(
"Settings: auto-indent={ai} wrap={ww} whitespace={ws} syntax={sy} relative-lines={rl} (Alt-A/Alt-W/Alt-E/Ctrl-S toggle)"
"Settings: auto-indent={ai} wrap={ww} whitespace={ws} syntax={sy} (Alt-A/Alt-W/Alt-E/Ctrl-S toggle)"
));
}
EditorCmd::New => {
@@ -2895,11 +2880,11 @@ mod tests {
})
.unwrap();
let buffer = terminal.backend().buffer();
assert_eq!(buffer.cell((4, 1)).expect("a cell").symbol(), "a");
assert_eq!(buffer.cell((5, 1)).expect("space cell").symbol(), "·");
assert_eq!(buffer.cell((6, 1)).expect("tab cell").symbol(), "");
assert_eq!(buffer.cell((7, 1)).expect("b cell").symbol(), "b");
assert_eq!(buffer.cell((8, 1)).expect("caret cell").symbol(), "^");
assert_eq!(buffer.cell((1, 1)).expect("a cell").symbol(), "a");
assert_eq!(buffer.cell((2, 1)).expect("space cell").symbol(), "·");
assert_eq!(buffer.cell((3, 1)).expect("tab cell").symbol(), "");
assert_eq!(buffer.cell((4, 1)).expect("b cell").symbol(), "b");
assert_eq!(buffer.cell((5, 1)).expect("caret cell").symbol(), "^");
}
#[test]
@@ -2928,7 +2913,7 @@ mod tests {
})
})
.expect("must find 'b' from beta");
let cell = buffer.cell((4, beta_y)).expect("beta line cell");
let cell = buffer.cell((1, beta_y)).expect("beta line cell");
assert_eq!(cell.symbol(), "b");
assert_eq!(cell.fg, bookmarkfound.fg);
assert_eq!(cell.bg, bookmarkfound.bg);
@@ -3214,8 +3199,8 @@ mod tests {
fg: theme.marked_fg,
bg: theme.marked_bg,
});
let f_cell = buffer.cell((4, 1)).expect("'f' cell");
let n_cell = buffer.cell((5, 1)).expect("'n' cell");
let f_cell = buffer.cell((1, 1)).expect("'f' cell");
let n_cell = buffer.cell((2, 1)).expect("'n' cell");
assert_eq!(f_cell.symbol(), "f");
assert_eq!(n_cell.symbol(), "n");
assert_eq!(f_cell.bg, marked_pair.bg, "selected 'f' cell bg");
@@ -2,7 +2,7 @@
//!
//! [`Editor::render`] writes the editor into a ratatui `Frame` at
//! the given `Rect`. The view is a simple top-line/left-column
//! scroller with line numbers in the gutter. The status line shows
//! scroller. The status line shows
//! position, modification state, and active mode.
use ratatui::layout::{Constraint, Direction, Layout, Rect};
@@ -21,7 +21,7 @@ use super::Editor;
impl Editor {
/// Render the editor into a ratatui frame at the given area.
///
/// `theme` supplies the title, gutter, body, prompt-overlay, and
/// `theme` supplies the title, body, prompt-overlay, and
/// status-line colours so the editor follows the active skin.
pub(crate) fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
// When the F9 menu bar is open, reserve the top row for it
@@ -89,7 +89,7 @@ impl Editor {
};
let marked_fg = editor_marked.map(|p| p.fg).unwrap_or(theme.marked_fg);
let marked_bg = editor_marked.map(|p| p.bg).unwrap_or(theme.marked_bg);
let linestate_fg = editor_linestate.map(|p| p.fg).unwrap_or(theme.cursor_fg);
let cur_line_fg = editor_linestate.map(|p| p.fg).unwrap_or(theme.cursor_fg);
let frame_fg = editor_frameactive.map(|p| p.fg).unwrap_or(theme.title_fg);
let margin_fg = editor_rightmargin.map(|p| p.fg).unwrap_or(frame_fg);
let margin_bg = editor_rightmargin.map(|p| p.bg).unwrap_or(body_bg);
@@ -119,23 +119,7 @@ impl Editor {
frame.render_widget(block, editor_area);
let line_count = self.buffer.line_count();
let gutter_chars = line_count.max(1).to_string().len().max(3) as u16;
let body_area = if gutter_chars < inner.width {
Rect::new(
inner.x + gutter_chars,
inner.y,
inner.width - gutter_chars,
inner.height,
)
} else {
inner
};
let gutter_area = Rect::new(
inner.x,
inner.y,
gutter_chars.min(inner.width),
inner.height,
);
let body_area = inner;
self.view.ensure_cursor_visible(
&self.buffer,
@@ -205,7 +189,7 @@ impl Editor {
Style::default().fg(bookmark_fg).bg(bookmark_bg)
}
} else if cursor_line == line_idx {
Style::default().fg(linestate_fg).bg(cur_line_bg)
Style::default().fg(cur_line_fg).bg(cur_line_bg)
} else {
Style::default().fg(body_fg).bg(body_bg)
};
@@ -319,38 +303,6 @@ impl Editor {
}
frame.render_widget(Paragraph::new(body_lines), body_area);
if gutter_chars < inner.width {
let gutter_lines: Vec<Line> = visible_wrapped
.iter()
.copied()
.map(|(line_idx, wrap_row)| {
if line_idx >= line_count {
return Line::from(Span::styled(
"~",
Style::default().fg(frame_fg).bg(body_bg),
));
}
let text = if wrap_row > 0 {
" ".repeat(gutter_chars as usize)
} else {
let num = if self.relative_lines {
(line_idx as i64 - cursor_line as i64).unsigned_abs()
} else {
line_idx as u64 + 1
};
format!("{:>w$}", num, w = gutter_chars as usize)
};
let style = if line_idx == cursor_line {
Style::default().fg(linestate_fg).bg(cur_line_bg)
} else {
Style::default().fg(frame_fg).bg(body_bg)
};
Line::from(Span::styled(text, style))
})
.collect();
frame.render_widget(Paragraph::new(gutter_lines), gutter_area);
}
if body_area.width > 0 {
let margin_area = Rect::new(
body_area.x + body_area.width - 1,
@@ -712,7 +664,6 @@ impl Editor {
]),
("View", &[
("Ctrl-S", "Toggle syntax highlight"),
("Alt-N", "Toggle line numbers"),
("Ctrl-F1", "Toggle code fold"),
("Alt-R", "Redo"),
("Alt-Tab", "Word completion"),
@@ -1114,7 +1065,7 @@ fn visual_width(ch: char, col: usize, tab_width: usize) -> usize {
/// mid-word at the current column.
///
/// Tabs are counted as their visual width (next `tab_width` boundary).
/// Empty lines always count as 1 visual row (so the gutter shows `~`).
/// Empty lines always count as 1 visual row.
fn count_wrapped_rows(line_bytes: &[u8], body_width: usize, tab_width: usize) -> usize {
if body_width == 0 {
return 1;