summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Vegdahl2021-07-02 06:36:09 +0000
committerNathan Vegdahl2021-07-02 06:36:09 +0000
commit22dca3b111513f4dc0852acf0bfb0229a8661904 (patch)
tree214add658560426d12d94ccb71929050fb720487
parent230248bbc3e453bab339cb865990c3fa2f518311 (diff)
Allow last line in file to lack a line break character.
-rw-r--r--helix-core/src/movement.rs16
-rw-r--r--helix-core/src/syntax.rs18
-rw-r--r--helix-term/src/ui/editor.rs67
-rw-r--r--helix-view/src/document.rs14
4 files changed, 70 insertions, 45 deletions
diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs
index acc95e7e..b810876c 100644
--- a/helix-core/src/movement.rs
+++ b/helix-core/src/movement.rs
@@ -65,7 +65,7 @@ pub fn move_vertically(
Direction::Backward => row.saturating_sub(count),
Direction::Forward => std::cmp::min(
row.saturating_add(count),
- slice.len_lines().saturating_sub(2),
+ slice.len_lines().saturating_sub(1),
),
};
@@ -402,12 +402,13 @@ mod test {
let moves_and_expected_coordinates = IntoIter::new([
((Direction::Forward, 1usize), (1, 0)),
((Direction::Forward, 2usize), (3, 0)),
+ ((Direction::Forward, 1usize), (4, 0)),
((Direction::Backward, 999usize), (0, 0)),
- ((Direction::Forward, 3usize), (3, 0)),
- ((Direction::Forward, 0usize), (3, 0)),
- ((Direction::Backward, 0usize), (3, 0)),
- ((Direction::Forward, 5), (4, 0)),
- ((Direction::Forward, 999usize), (4, 0)),
+ ((Direction::Forward, 4usize), (4, 0)),
+ ((Direction::Forward, 0usize), (4, 0)),
+ ((Direction::Backward, 0usize), (4, 0)),
+ ((Direction::Forward, 5), (5, 0)),
+ ((Direction::Forward, 999usize), (5, 0)),
]);
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
@@ -439,7 +440,8 @@ mod test {
((Axis::V, Direction::Forward, 1usize), (3, 8)),
// Behaviour is preserved even through long jumps
((Axis::V, Direction::Backward, 999usize), (0, 8)),
- ((Axis::V, Direction::Forward, 999usize), (4, 8)),
+ ((Axis::V, Direction::Forward, 4usize), (4, 8)),
+ ((Axis::V, Direction::Forward, 999usize), (5, 0)),
]);
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index 9dbb2c03..d4379a8e 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -1758,10 +1758,20 @@ impl<I: Iterator<Item = HighlightEvent>> Iterator for Merge<I> {
self.next_event = self.iter.next();
Some(event)
}
- // can happen if deleting and cursor at EOF, and diagnostic reaches past the end
- (None, Some((_, _))) => {
- self.next_span = None;
- None
+ // Can happen if cursor at EOF and/or diagnostic reaches past the end.
+ // We need to actually emit events for the cursor-at-EOF situation,
+ // even though the range is past the end of the text. This needs to be
+ // handled appropriately by the drawing code by not assuming that
+ // all `Source` events point to valid indices in the rope.
+ (None, Some((span, range))) => {
+ let event = HighlightStart(Highlight(*span));
+ self.queue.push(HighlightEnd);
+ self.queue.push(Source {
+ start: range.start,
+ end: range.end,
+ });
+ self.next_span = self.spans.next();
+ Some(event)
}
(None, None) => None,
e => unreachable!("{:?}", e),
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 138456ca..dab654ad 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -8,7 +8,7 @@ use crate::{
use helix_core::{
coords_at_pos,
- graphemes::ensure_grapheme_boundary_next,
+ graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary},
syntax::{self, HighlightEvent},
LineEnding, Position, Range,
};
@@ -165,21 +165,18 @@ impl EditorView {
}
.unwrap_or(base_cursor_scope);
- let primary_selection_scope = theme
- .find_scope_index("ui.selection.primary")
- .unwrap_or(selection_scope);
-
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
- // inject selections as highlight scopes
- let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
-
// TODO: primary + insert mode patching:
// (ui.cursor.primary).patch(mode).unwrap_or(cursor)
-
let primary_cursor_scope = theme
.find_scope_index("ui.cursor.primary")
.unwrap_or(cursor_scope);
+ let primary_selection_scope = theme
+ .find_scope_index("ui.selection.primary")
+ .unwrap_or(selection_scope);
+ // inject selections as highlight scopes
+ let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
for (i, range) in selections.iter().enumerate() {
let (cursor_scope, selection_scope) = if i == primary_idx {
(primary_cursor_scope, primary_selection_scope)
@@ -187,19 +184,23 @@ impl EditorView {
(cursor_scope, selection_scope)
};
- if range.head == range.anchor {
+ // Special-case: cursor at end of the rope.
+ if range.head == range.anchor && range.head == text.len_chars() {
spans.push((cursor_scope, range.head..range.head + 1));
continue;
}
- let reverse = range.head < range.anchor;
-
- if reverse {
- spans.push((cursor_scope, range.head..range.head + 1));
- spans.push((selection_scope, range.head + 1..range.anchor + 1));
+ let range = range.min_width_1(text);
+ if range.head > range.anchor {
+ // Standard case.
+ let cursor_start = prev_grapheme_boundary(text, range.head);
+ spans.push((selection_scope, range.anchor..cursor_start));
+ spans.push((cursor_scope, cursor_start..range.head));
} else {
- spans.push((selection_scope, range.anchor..range.head));
- spans.push((cursor_scope, range.head..range.head + 1));
+ // Reverse case.
+ let cursor_end = next_grapheme_boundary(text, range.head);
+ spans.push((cursor_scope, range.head..cursor_end));
+ spans.push((selection_scope, cursor_end..range.anchor));
}
}
@@ -232,7 +233,10 @@ impl EditorView {
spans.pop();
}
HighlightEvent::Source { start, end } => {
- let text = text.slice(start..end);
+ // `unwrap_or_else` part is for off-the-end indices of
+ // the rope, to allow cursor highlighting at the end
+ // of the rope.
+ let text = text.get_slice(start..end).unwrap_or_else(|| " ".into());
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
@@ -301,7 +305,11 @@ impl EditorView {
let info: Style = theme.get("info");
let hint: Style = theme.get("hint");
- for (i, line) in (view.first_line..last_line).enumerate() {
+ // Whether to draw the line number for the last line of the
+ // document or not. We only draw it if it's not an empty line.
+ let draw_last = text.line_to_byte(last_line) < text.len_bytes();
+
+ for (i, line) in (view.first_line..=last_line).enumerate() {
use helix_core::diagnostic::Severity;
if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
surface.set_stringn(
@@ -318,11 +326,17 @@ impl EditorView {
);
}
- // line numbers having selections are rendered differently
+ // Line numbers having selections are rendered
+ // differently, further below.
+ let line_number_text = if line == last_line && !draw_last {
+ " ~".into()
+ } else {
+ format!("{:>5}", line + 1)
+ };
surface.set_stringn(
viewport.x + 1 - OFFSET,
viewport.y + i as u16,
- format!("{:>5}", line + 1),
+ line_number_text,
5,
linenr,
);
@@ -336,7 +350,7 @@ impl EditorView {
if is_focused {
let screen = {
let start = text.line_to_char(view.first_line);
- let end = text.line_to_char(last_line + 1);
+ let end = text.line_to_char(last_line + 1) + 1; // +1 for cursor at end of text.
Range::new(start, end)
};
@@ -345,10 +359,17 @@ impl EditorView {
for selection in selection.iter().filter(|range| range.overlaps(&screen)) {
let head = view.screen_coords_at_pos(doc, text, selection.head);
if let Some(head) = head {
+ // Draw line number for selected lines.
+ let line_number = view.first_line + head.row;
+ let line_number_text = if line_number == last_line && !draw_last {
+ " ~".into()
+ } else {
+ format!("{:>5}", line_number + 1)
+ };
surface.set_stringn(
viewport.x + 1 - OFFSET,
viewport.y + head.row as u16,
- format!("{:>5}", view.first_line + head.row + 1),
+ line_number_text,
5,
linenr_select,
);
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 0f1f3a8f..9b609429 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -448,15 +448,7 @@ impl Document {
}
let mut file = std::fs::File::open(&path).context(format!("unable to open {:?}", path))?;
- let (mut rope, encoding) = from_reader(&mut file, encoding)?;
-
- // search for line endings
- let line_ending = auto_detect_line_ending(&rope).unwrap_or(DEFAULT_LINE_ENDING);
-
- // add missing newline at the end of file
- if rope.len_bytes() == 0 || !char_is_line_ending(rope.char(rope.len_chars() - 1)) {
- rope.insert(rope.len_chars(), line_ending.as_str());
- }
+ let (rope, encoding) = from_reader(&mut file, encoding)?;
let mut doc = Self::from(rope, Some(encoding));
@@ -466,9 +458,9 @@ impl Document {
doc.detect_language(theme, loader);
}
- // Detect indentation style and set line ending.
+ // Detect indentation style and line ending.
doc.detect_indent_style();
- doc.line_ending = line_ending;
+ doc.line_ending = auto_detect_line_ending(&doc.text).unwrap_or(DEFAULT_LINE_ENDING);
Ok(doc)
}