diff --git a/src/tui.rs b/src/tui.rs index 5412096..db61754 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -968,6 +968,11 @@ fn render_editor(frame: &mut Frame, area: Rect, editor: &Editor, focused: bool) }; let block = Block::default().borders(Borders::ALL).title(title); + // Line numbers occupy a fixed-width left gutter. Width grows with the + // largest line number so the body never shifts as lines are added. + let gutter_width = line_number_width(editor.buffer.len()); + let gutter_style = Style::default().fg(Color::DarkGray); + if focused { // Highlight the character under the cursor with explicit bg + fg // colours (not `REVERSED`) so the cursor cell is filled even when @@ -981,24 +986,55 @@ fn render_editor(frame: &mut Frame, area: Rect, editor: &Editor, focused: bool) .iter() .enumerate() .map(|(r, line)| { - if r == cr { + let body = if r == cr { editor_cursor_line(line, cc, cursor_style) } else { Line::from(line.clone()) - } + }; + prepend_line_number(body, r + 1, gutter_width, gutter_style) }) .collect(); let p = Paragraph::new(lines).block(block).wrap(Wrap { trim: false }); frame.render_widget(p, area); } else { - let p = Paragraph::new(editor.text()) - .block(block) - .wrap(Wrap { trim: false }); + let lines: Vec = editor + .buffer + .iter() + .enumerate() + .map(|(r, line)| { + prepend_line_number( + Line::from(line.clone()), + r + 1, + gutter_width, + gutter_style, + ) + }) + .collect(); + let p = Paragraph::new(lines).block(block).wrap(Wrap { trim: false }); frame.render_widget(p, area); } } +fn line_number_width(line_count: usize) -> usize { + let max = line_count.max(1); + let mut digits = 1; + let mut n = max; + while n >= 10 { + n /= 10; + digits += 1; + } + digits +} + +fn prepend_line_number(line: Line<'static>, number: usize, width: usize, style: Style) -> Line<'static> { + let gutter = format!("{:>width$} │ ", number, width = width); + let mut spans = Vec::with_capacity(line.spans.len() + 1); + spans.push(Span::styled(gutter, style)); + spans.extend(line.spans); + Line::from(spans) +} + /// Build the cursor-bearing line as three spans: text before, the /// highlighted character at the cursor, text after. If the cursor sits /// past the end of the line (or on a whitespace cell), the highlight