on master help sign in
Allow last line in file to lack a line break character.
22dca3bNathan Vegdahl
helix-core/src/movement.rs+9−7
@@ -65,7 +65,7 @@ pub fn move_vertically(
65 Direction::Backward => row.saturating_sub(count),
65 Direction::Backward => row.saturating_sub(count),
66 Direction::Forward => std::cmp::min(
66 Direction::Forward => std::cmp::min(
67 row.saturating_add(count),
67 row.saturating_add(count),
68− slice.len_lines().saturating_sub(2),
68+ slice.len_lines().saturating_sub(1),
69 ),
69 ),
70 };
70 };
71
71
@@ -402,12 +402,13 @@ mod test {
402 let moves_and_expected_coordinates = IntoIter::new([
402 let moves_and_expected_coordinates = IntoIter::new([
403 ((Direction::Forward, 1usize), (1, 0)),
403 ((Direction::Forward, 1usize), (1, 0)),
404 ((Direction::Forward, 2usize), (3, 0)),
404 ((Direction::Forward, 2usize), (3, 0)),
405+ ((Direction::Forward, 1usize), (4, 0)),
405 ((Direction::Backward, 999usize), (0, 0)),
406 ((Direction::Backward, 999usize), (0, 0)),
406− ((Direction::Forward, 3usize), (3, 0)),
407+ ((Direction::Forward, 4usize), (4, 0)),
407− ((Direction::Forward, 0usize), (3, 0)),
408+ ((Direction::Forward, 0usize), (4, 0)),
408− ((Direction::Backward, 0usize), (3, 0)),
409+ ((Direction::Backward, 0usize), (4, 0)),
409− ((Direction::Forward, 5), (4, 0)),
410+ ((Direction::Forward, 5), (5, 0)),
410− ((Direction::Forward, 999usize), (4, 0)),
411+ ((Direction::Forward, 999usize), (5, 0)),
411 ]);
412 ]);
412
413
413 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
414 for ((direction, amount), coordinates) in moves_and_expected_coordinates {
@@ -439,7 +440,8 @@ mod test {
439 ((Axis::V, Direction::Forward, 1usize), (3, 8)),
440 ((Axis::V, Direction::Forward, 1usize), (3, 8)),
440 // Behaviour is preserved even through long jumps
441 // Behaviour is preserved even through long jumps
441 ((Axis::V, Direction::Backward, 999usize), (0, 8)),
442 ((Axis::V, Direction::Backward, 999usize), (0, 8)),
442− ((Axis::V, Direction::Forward, 999usize), (4, 8)),
443+ ((Axis::V, Direction::Forward, 4usize), (4, 8)),
444+ ((Axis::V, Direction::Forward, 999usize), (5, 0)),
443 ]);
445 ]);
444
446
445 for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
447 for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
helix-core/src/syntax.rs+14−4
@@ -1758,10 +1758,20 @@ impl<I: Iterator<Item = HighlightEvent>> Iterator for Merge<I> {
1758 self.next_event = self.iter.next();
1758 self.next_event = self.iter.next();
1759 Some(event)
1759 Some(event)
1760 }
1760 }
1761− // can happen if deleting and cursor at EOF, and diagnostic reaches past the end
1761+ // Can happen if cursor at EOF and/or diagnostic reaches past the end.
1762+ // We need to actually emit events for the cursor-at-EOF situation,
1763+ // even though the range is past the end of the text. This needs to be
1764+ // handled appropriately by the drawing code by not assuming that
1765+ // all `Source` events point to valid indices in the rope.
1762− (None, Some((_, _))) => {
1766+ (None, Some((span, range))) => {
1767+ let event = HighlightStart(Highlight(*span));
1768+ self.queue.push(HighlightEnd);
1769+ self.queue.push(Source {
1770+ start: range.start,
1771+ end: range.end,
1772+ });
1763− self.next_span = None;
1773+ self.next_span = self.spans.next();
1764− None
1774+ Some(event)
1765 }
1775 }
1766 (None, None) => None,
1776 (None, None) => None,
1767 e => unreachable!("{:?}", e),
1777 e => unreachable!("{:?}", e),
helix-term/src/ui/editor.rs+44−23
@@ -8,7 +8,7 @@ use crate::{
8
8
9use helix_core::{
9use helix_core::{
10 coords_at_pos,
10 coords_at_pos,
11− graphemes::ensure_grapheme_boundary_next,
11+ graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary},
12 syntax::{self, HighlightEvent},
12 syntax::{self, HighlightEvent},
13 LineEnding, Position, Range,
13 LineEnding, Position, Range,
14};
14};
@@ -165,21 +165,18 @@ impl EditorView {
165 }
165 }
166 .unwrap_or(base_cursor_scope);
166 .unwrap_or(base_cursor_scope);
167
167
168− let primary_selection_scope = theme
169− .find_scope_index("ui.selection.primary")
170− .unwrap_or(selection_scope);
171−
172 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
168 let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
173− // inject selections as highlight scopes
174− let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
175−
176 // TODO: primary + insert mode patching:
169 // TODO: primary + insert mode patching:
177 // (ui.cursor.primary).patch(mode).unwrap_or(cursor)
170 // (ui.cursor.primary).patch(mode).unwrap_or(cursor)
178−
179 let primary_cursor_scope = theme
171 let primary_cursor_scope = theme
180 .find_scope_index("ui.cursor.primary")
172 .find_scope_index("ui.cursor.primary")
181 .unwrap_or(cursor_scope);
173 .unwrap_or(cursor_scope);
174+ let primary_selection_scope = theme
175+ .find_scope_index("ui.selection.primary")
176+ .unwrap_or(selection_scope);
182
177
178+ // inject selections as highlight scopes
179+ let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
183 for (i, range) in selections.iter().enumerate() {
180 for (i, range) in selections.iter().enumerate() {
184 let (cursor_scope, selection_scope) = if i == primary_idx {
181 let (cursor_scope, selection_scope) = if i == primary_idx {
185 (primary_cursor_scope, primary_selection_scope)
182 (primary_cursor_scope, primary_selection_scope)
@@ -187,19 +184,23 @@ impl EditorView {
187 (cursor_scope, selection_scope)
184 (cursor_scope, selection_scope)
188 };
185 };
189
186
187+ // Special-case: cursor at end of the rope.
190− if range.head == range.anchor {
188+ if range.head == range.anchor && range.head == text.len_chars() {
191 spans.push((cursor_scope, range.head..range.head + 1));
189 spans.push((cursor_scope, range.head..range.head + 1));
192 continue;
190 continue;
193 }
191 }
194
192
195− let reverse = range.head < range.anchor;
193+ let range = range.min_width_1(text);
196−
194+ if range.head > range.anchor {
197− if reverse {
195+ // Standard case.
198− spans.push((cursor_scope, range.head..range.head + 1));
196+ let cursor_start = prev_grapheme_boundary(text, range.head);
199− spans.push((selection_scope, range.head + 1..range.anchor + 1));
197+ spans.push((selection_scope, range.anchor..cursor_start));
198+ spans.push((cursor_scope, cursor_start..range.head));
200 } else {
199 } else {
200+ // Reverse case.
201+ let cursor_end = next_grapheme_boundary(text, range.head);
202+ spans.push((cursor_scope, range.head..cursor_end));
201− spans.push((selection_scope, range.anchor..range.head));
203+ spans.push((selection_scope, cursor_end..range.anchor));
202− spans.push((cursor_scope, range.head..range.head + 1));
203 }
204 }
204 }
205 }
205
206
@@ -232,7 +233,10 @@ impl EditorView {
232 spans.pop();
233 spans.pop();
233 }
234 }
234 HighlightEvent::Source { start, end } => {
235 HighlightEvent::Source { start, end } => {
236+ // `unwrap_or_else` part is for off-the-end indices of
237+ // the rope, to allow cursor highlighting at the end
238+ // of the rope.
235− let text = text.slice(start..end);
239+ let text = text.get_slice(start..end).unwrap_or_else(|| " ".into());
236
240
237 use helix_core::graphemes::{grapheme_width, RopeGraphemes};
241 use helix_core::graphemes::{grapheme_width, RopeGraphemes};
238
242
@@ -301,7 +305,11 @@ impl EditorView {
301 let info: Style = theme.get("info");
305 let info: Style = theme.get("info");
302 let hint: Style = theme.get("hint");
306 let hint: Style = theme.get("hint");
303
307
308+ // Whether to draw the line number for the last line of the
309+ // document or not. We only draw it if it's not an empty line.
310+ let draw_last = text.line_to_byte(last_line) < text.len_bytes();
311+
304− for (i, line) in (view.first_line..last_line).enumerate() {
312+ for (i, line) in (view.first_line..=last_line).enumerate() {
305 use helix_core::diagnostic::Severity;
313 use helix_core::diagnostic::Severity;
306 if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
314 if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
307 surface.set_stringn(
315 surface.set_stringn(
@@ -318,11 +326,17 @@ impl EditorView {
318 );
326 );
319 }
327 }
320
328
321− // line numbers having selections are rendered differently
329+ // Line numbers having selections are rendered
330+ // differently, further below.
331+ let line_number_text = if line == last_line && !draw_last {
332+ " ~".into()
333+ } else {
334+ format!("{:>5}", line + 1)
335+ };
322 surface.set_stringn(
336 surface.set_stringn(
323 viewport.x + 1 - OFFSET,
337 viewport.x + 1 - OFFSET,
324 viewport.y + i as u16,
338 viewport.y + i as u16,
325− format!("{:>5}", line + 1),
339+ line_number_text,
326 5,
340 5,
327 linenr,
341 linenr,
328 );
342 );
@@ -336,7 +350,7 @@ impl EditorView {
336 if is_focused {
350 if is_focused {
337 let screen = {
351 let screen = {
338 let start = text.line_to_char(view.first_line);
352 let start = text.line_to_char(view.first_line);
339− let end = text.line_to_char(last_line + 1);
353+ let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text.
340 Range::new(start, end)
354 Range::new(start, end)
341 };
355 };
342
356
@@ -345,10 +359,17 @@ impl EditorView {
345 for selection in selection.iter().filter(|range| range.overlaps(&screen)) {
359 for selection in selection.iter().filter(|range| range.overlaps(&screen)) {
346 let head = view.screen_coords_at_pos(doc, text, selection.head);
360 let head = view.screen_coords_at_pos(doc, text, selection.head);
347 if let Some(head) = head {
361 if let Some(head) = head {
362+ // Draw line number for selected lines.
363+ let line_number = view.first_line + head.row;
364+ let line_number_text = if line_number == last_line && !draw_last {
365+ " ~".into()
366+ } else {
367+ format!("{:>5}", line_number + 1)
368+ };
348 surface.set_stringn(
369 surface.set_stringn(
349 viewport.x + 1 - OFFSET,
370 viewport.x + 1 - OFFSET,
350 viewport.y + head.row as u16,
371 viewport.y + head.row as u16,
351− format!("{:>5}", view.first_line + head.row + 1),
372+ line_number_text,
352 5,
373 5,
353 linenr_select,
374 linenr_select,
354 );
375 );
helix-view/src/document.rs+3−11
@@ -448,15 +448,7 @@ impl Document {
448 }
448 }
449
449
450 let mut file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
450 let mut file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
451− let (mut rope, encoding) = from_reader(&mut file, encoding)?;
451+ let (rope, encoding) = from_reader(&mut file, encoding)?;
452−
453− // search for line endings
454− let line_ending = auto_detect_line_ending(&rope).unwrap_or(DEFAULT_LINE_ENDING);
455−
456− // add missing newline at the end of file
457− if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) {
458− rope.insert(rope.len_chars(), line_ending.as_str());
459− }
460
452
461 let mut doc = Self::from(rope, Some(encoding));
453 let mut doc = Self::from(rope, Some(encoding));
462
454
@@ -466,9 +458,9 @@ impl Document {
466 doc.detect_language(theme, loader);
458 doc.detect_language(theme, loader);
467 }
459 }
468
460
469− // Detect indentation style and set line ending.
461+ // Detect indentation style and line ending.
470 doc.detect_indent_style();
462 doc.detect_indent_style();
471− doc.line_ending = line_ending;
463+ doc.line_ending = auto_detect_line_ending(&doc.text).unwrap_or(DEFAULT_LINE_ENDING);
472
464
473 Ok(doc)
465 Ok(doc)
474 }
466 }