···284284 // Right-justified 3-digit label, drawn flush to gutter (no extra left/right padding)
285285 let _ = fb.print_text(&label, area.x, gutter_y, 14, true, 1, true);
286286287287- // Text slice
288288- let mut line = self.rope.line(line_idx).to_string();
289289- if line.ends_with('\n') {
290290- line.pop();
287287+ // Text slice (track newline separately to handle selection over EOL/empty lines)
288288+ let mut src_line = self.rope.line(line_idx).to_string();
289289+ let had_nl = src_line.ends_with('\n');
290290+ if had_nl {
291291+ src_line.pop();
291292 }
293293+ let line = src_line;
292294 let start = self.scroll_col.min(line.chars().count());
293295 let mut iter = line.chars().skip(start);
294296 let vis: String = iter.by_ref().take(cols_vis).collect();
···313315 let s = ch.to_string();
314316 // TIC default text color (no syntax) is white (12), 6px tall
315317 let _ = fb.print_text(&s, cell_x, cell_y, 12, true, 1, true);
318318+ }
319319+ }
320320+ // Extra selection cell for newline (EOL) when selected
321321+ if had_nl {
322322+ if let Some((sel_start, sel_end)) = sel {
323323+ let display_len = line.chars().count();
324324+ let nl_idx = line_char_start + display_len; // index of '\n' in rope for this line
325325+ if sel_start <= nl_idx && sel_end > nl_idx {
326326+ let nl_col = i32::try_from(display_len).unwrap_or(0);
327327+ let col_vis = nl_col - self.scroll_col as i32;
328328+ if col_vis >= 0 && col_vis < cols_vis as i32 {
329329+ let cell_x = area.x + gutter_w + gap + col_vis * 6;
330330+ let cell_y = gutter_y;
331331+ fb.rect(cell_x, cell_y, 7, 7, 0);
332332+ fb.rect(cell_x - 1, cell_y - 1, 7, 7, 14);
333333+ }
334334+ }
316335 }
317336 }
318337 }
···11+use std::cell::RefCell;
22+use std::rc::Rc;
33+44+use tic80_rust::editor::code::{Area, CodeBuffer};
55+use tic80_rust::gfx::framebuffer::Framebuffer;
66+77+#[test]
88+fn selection_covers_newline_cell_on_empty_line() {
99+ // Line 1 is empty between a and b
1010+ let text = "a\n\nb\n";
1111+ let mut cb = CodeBuffer::from_text(text);
1212+ // Select from end of first line through the empty line
1313+ cb.caret_line = 0;
1414+ cb.caret_col = 1; // after 'a'
1515+ cb.start_selection();
1616+ cb.caret_line = 2;
1717+ cb.caret_col = 0; // before 'b'
1818+1919+ let fb_rc = Rc::new(RefCell::new(Framebuffer::new()));
2020+ let mut fb = fb_rc.borrow_mut();
2121+ let area = Area { x: 0, y: 7, w: 240, h: 40 };
2222+ cb.draw(&mut fb, area);
2323+2424+ // Expect a selection cell drawn on the empty line at column 0 (gutter=18 + gap=1)
2525+ let gutter = 18i32;
2626+ let gap = 1i32;
2727+ let x = gutter + gap;
2828+ let y = area.y + 7; // second row (empty line)
2929+ let c = fb.pix(x, y, None).unwrap_or(0);
3030+ assert_ne!(c, 0, "expected selection cell on empty line for newline");
3131+}