aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
authorBlaž Hrastnik2021-07-29 09:43:20 +0000
committerGitHub2021-07-29 09:43:20 +0000
commit05d20e196f81c8b71c2aecaf46f5d443d6b6b582 (patch)
tree0642d43c12f16ac3c68c19602c64fdea8108cc97 /helix-term/src
parent8a2fa692f26f5bff5861151f395304837f5d93ec (diff)
parente4d41d06e3b52863d35ce3703f78cc8e0807c504 (diff)
Merge pull request #376 from cessen/great_line_ending_and_cursor_range_cleanup
The Great Line Ending & Cursor Range Cleanup
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/commands.rs621
-rw-r--r--helix-term/src/ui/completion.rs20
-rw-r--r--helix-term/src/ui/editor.rs103
3 files changed, 441 insertions, 303 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 32eb3f00..f6df26ba 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -1,9 +1,6 @@
use helix_core::{
comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
- line_ending::{
- get_line_ending_of_str, line_end_char_index, rope_end_without_line_ending,
- str_is_line_ending,
- },
+ line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
match_brackets,
movement::{self, Direction},
object, pos_at_coords,
@@ -110,7 +107,10 @@ enum Align {
}
fn align_view(doc: &Document, view: &mut View, align: Align) {
- let pos = doc.selection(view.id).cursor();
+ let pos = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos);
let relative = match align {
@@ -340,7 +340,8 @@ fn move_char_left(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+
+ let selection = doc.selection(view.id).clone().transform(|range| {
movement::move_horizontally(text, range, Direction::Backward, count, Movement::Move)
});
doc.set_selection(view.id, selection);
@@ -350,7 +351,8 @@ fn move_char_right(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+
+ let selection = doc.selection(view.id).clone().transform(|range| {
movement::move_horizontally(text, range, Direction::Forward, count, Movement::Move)
});
doc.set_selection(view.id, selection);
@@ -360,7 +362,8 @@ fn move_line_up(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+
+ let selection = doc.selection(view.id).clone().transform(|range| {
movement::move_vertically(text, range, Direction::Backward, count, Movement::Move)
});
doc.set_selection(view.id, selection);
@@ -370,7 +373,8 @@ fn move_line_down(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+
+ let selection = doc.selection(view.id).clone().transform(|range| {
movement::move_vertically(text, range, Direction::Forward, count, Movement::Move)
});
doc.set_selection(view.id, selection);
@@ -378,84 +382,61 @@ fn move_line_down(cx: &mut Context) {
fn goto_line_end(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
- let text = doc.text();
- let line = text.char_to_line(range.head);
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ let line = range.cursor_line(text);
+ let line_start = text.line_to_char(line);
- let pos = line_end_char_index(&text.slice(..), line);
- let pos = graphemes::nth_prev_grapheme_boundary(text.slice(..), pos, 1);
- let pos = range.head.max(pos).max(text.line_to_char(line));
+ let pos = graphemes::prev_grapheme_boundary(text, line_end_char_index(&text, line))
+ .max(line_start);
- Range::new(
- match doc.mode {
- Mode::Normal | Mode::Insert => pos,
- Mode::Select => range.anchor,
- },
- pos,
- )
+ range.put_cursor(text, pos, doc.mode == Mode::Select)
});
-
doc.set_selection(view.id, selection);
}
fn goto_line_end_newline(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
- let text = doc.text();
- let line = text.char_to_line(range.head);
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ let line = range.cursor_line(text);
+ let pos = line_end_char_index(&text, line);
- let pos = line_end_char_index(&text.slice(..), line);
- Range::new(pos, pos)
+ range.put_cursor(text, pos, doc.mode == Mode::Select)
});
-
doc.set_selection(view.id, selection);
}
fn goto_line_start(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
- let text = doc.text();
- let line = text.char_to_line(range.head);
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ let line = range.cursor_line(text);
// adjust to start of the line
let pos = text.line_to_char(line);
- Range::new(
- match doc.mode {
- Mode::Normal => range.anchor,
- Mode::Select | Mode::Insert => pos,
- },
- pos,
- )
+ range.put_cursor(text, pos, doc.mode == Mode::Select)
});
-
doc.set_selection(view.id, selection);
}
fn goto_first_nonwhitespace(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
- let text = doc.text();
- let line_idx = text.char_to_line(range.head);
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ let line = range.cursor_line(text);
- if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) {
- let pos = pos + text.line_to_char(line_idx);
- Range::new(
- match doc.mode {
- Mode::Normal => pos,
- Mode::Select => range.anchor,
- Mode::Insert => unreachable!(),
- },
- pos,
- )
+ if let Some(pos) = find_first_non_whitespace_char(text.line(line)) {
+ let pos = pos + text.line_to_char(line);
+ range.put_cursor(text, pos, doc.mode == Mode::Select)
} else {
range
}
});
-
doc.set_selection(view.id, selection);
}
@@ -501,8 +482,8 @@ fn move_next_word_start(cx: &mut Context) {
let selection = doc
.selection(view.id)
+ .clone()
.transform(|range| movement::move_next_word_start(text, range, count));
-
doc.set_selection(view.id, selection);
}
@@ -513,8 +494,8 @@ fn move_prev_word_start(cx: &mut Context) {
let selection = doc
.selection(view.id)
+ .clone()
.transform(|range| movement::move_prev_word_start(text, range, count));
-
doc.set_selection(view.id, selection);
}
@@ -525,8 +506,8 @@ fn move_next_word_end(cx: &mut Context) {
let selection = doc
.selection(view.id)
+ .clone()
.transform(|range| movement::move_next_word_end(text, range, count));
-
doc.set_selection(view.id, selection);
}
@@ -537,8 +518,8 @@ fn move_next_long_word_start(cx: &mut Context) {
let selection = doc
.selection(view.id)
+ .clone()
.transform(|range| movement::move_next_long_word_start(text, range, count));
-
doc.set_selection(view.id, selection);
}
@@ -549,8 +530,8 @@ fn move_prev_long_word_start(cx: &mut Context) {
let selection = doc
.selection(view.id)
+ .clone()
.transform(|range| movement::move_prev_long_word_start(text, range, count));
-
doc.set_selection(view.id, selection);
}
@@ -561,8 +542,8 @@ fn move_next_long_word_end(cx: &mut Context) {
let selection = doc
.selection(view.id)
+ .clone()
.transform(|range| movement::move_next_long_word_end(text, range, count));
-
doc.set_selection(view.id, selection);
}
@@ -579,9 +560,7 @@ fn goto_file_start(cx: &mut Context) {
fn goto_file_end(cx: &mut Context) {
push_jump(cx.editor);
let (view, doc) = current!(cx.editor);
- let text = doc.text();
- let last_line = text.line_to_char(text.len_lines().saturating_sub(2));
- doc.set_selection(view.id, Selection::point(last_line));
+ doc.set_selection(view.id, Selection::point(doc.text().len_chars()));
}
fn extend_next_word_start(cx: &mut Context) {
@@ -589,12 +568,11 @@ fn extend_next_word_start(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+ let selection = doc.selection(view.id).clone().transform(|range| {
let word = movement::move_next_word_start(text, range, count);
- let pos = word.head;
- Range::new(range.anchor, pos)
+ let pos = word.cursor(text);
+ range.put_cursor(text, pos, true)
});
-
doc.set_selection(view.id, selection);
}
@@ -603,10 +581,10 @@ fn extend_prev_word_start(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+ let selection = doc.selection(view.id).clone().transform(|range| {
let word = movement::move_prev_word_start(text, range, count);
- let pos = word.head;
- Range::new(range.anchor, pos)
+ let pos = word.cursor(text);
+ range.put_cursor(text, pos, true)
});
doc.set_selection(view.id, selection);
}
@@ -616,12 +594,11 @@ fn extend_next_word_end(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+ let selection = doc.selection(view.id).clone().transform(|range| {
let word = movement::move_next_word_end(text, range, count);
- let pos = word.head;
- Range::new(range.anchor, pos)
+ let pos = word.cursor(text);
+ range.put_cursor(text, pos, true)
});
-
doc.set_selection(view.id, selection);
}
@@ -667,26 +644,56 @@ where
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
- search_fn(text, ch, range.head, count, inclusive).map_or(range, |pos| {
- if extend {
- Range::new(range.anchor, pos)
- } else {
- // select
- Range::new(range.head, pos)
- }
- // or (pos, pos) to move to found val
- })
- });
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ // TODO: use `Range::cursor()` here instead. However, that works in terms of
+ // graphemes, whereas this function doesn't yet. So we're doing the same logic
+ // here, but just in terms of chars instead.
+ let search_start_pos = if range.anchor < range.head {
+ range.head - 1
+ } else {
+ range.head
+ };
+ search_fn(text, ch, search_start_pos, count, inclusive)
+ .map_or(range, |pos| range.put_cursor(text, pos, extend))
+ });
doc.set_selection(view.id, selection);
})
}
+fn find_next_char_impl(
+ text: RopeSlice,
+ ch: char,
+ pos: usize,
+ n: usize,
+ inclusive: bool,
+) -> Option<usize> {
+ let pos = (pos + 1).min(text.len_chars());
+ if inclusive {
+ search::find_nth_next(text, ch, pos, n)
+ } else {
+ search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1))
+ }
+}
+
+fn find_prev_char_impl(
+ text: RopeSlice,
+ ch: char,
+ pos: usize,
+ n: usize,
+ inclusive: bool,
+) -> Option<usize> {
+ if inclusive {
+ search::find_nth_prev(text, ch, pos, n)
+ } else {
+ search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars()))
+ }
+}
+
fn find_till_char(cx: &mut Context) {
find_char_impl(
cx,
- search::find_nth_next,
+ find_next_char_impl,
false, /* inclusive */
false, /* extend */
)
@@ -695,7 +702,7 @@ fn find_till_char(cx: &mut Context) {
fn find_next_char(cx: &mut Context) {
find_char_impl(
cx,
- search::find_nth_next,
+ find_next_char_impl,
true, /* inclusive */
false, /* extend */
)
@@ -704,7 +711,7 @@ fn find_next_char(cx: &mut Context) {
fn extend_till_char(cx: &mut Context) {
find_char_impl(
cx,
- search::find_nth_next,
+ find_next_char_impl,
false, /* inclusive */
true, /* extend */
)
@@ -713,7 +720,7 @@ fn extend_till_char(cx: &mut Context) {
fn extend_next_char(cx: &mut Context) {
find_char_impl(
cx,
- search::find_nth_next,
+ find_next_char_impl,
true, /* inclusive */
true, /* extend */
)
@@ -722,7 +729,7 @@ fn extend_next_char(cx: &mut Context) {
fn till_prev_char(cx: &mut Context) {
find_char_impl(
cx,
- search::find_nth_prev,
+ find_prev_char_impl,
false, /* inclusive */
false, /* extend */
)
@@ -731,7 +738,7 @@ fn till_prev_char(cx: &mut Context) {
fn find_prev_char(cx: &mut Context) {
find_char_impl(
cx,
- search::find_nth_prev,
+ find_prev_char_impl,
true, /* inclusive */
false, /* extend */
)
@@ -740,7 +747,7 @@ fn find_prev_char(cx: &mut Context) {
fn extend_till_prev_char(cx: &mut Context) {
find_char_impl(
cx,
- search::find_nth_prev,
+ find_prev_char_impl,
false, /* inclusive */
true, /* extend */
)
@@ -749,7 +756,7 @@ fn extend_till_prev_char(cx: &mut Context) {
fn extend_prev_char(cx: &mut Context) {
find_char_impl(
cx,
- search::find_nth_prev,
+ find_prev_char_impl,
true, /* inclusive */
true, /* extend */
)
@@ -773,24 +780,29 @@ fn replace(cx: &mut Context) {
_ => None,
};
- if let Some(ch) = ch {
- let transaction =
- Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
- let max_to = rope_end_without_line_ending(&doc.text().slice(..));
- let to = std::cmp::min(max_to, range.to() + 1);
- let text: String = RopeGraphemes::new(doc.text().slice(range.from()..to))
- .map(|g| {
- let cow: Cow<str> = g.into();
- if str_is_line_ending(&cow) {
- cow
- } else {
- ch.into()
- }
- })
- .collect();
+ let selection = doc.selection(view.id);
- (range.from(), to, Some(text.into()))
- });
+ if let Some(ch) = ch {
+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
+ if !range.is_empty() {
+ let text: String =
+ RopeGraphemes::new(doc.text().slice(range.from()..range.to()))
+ .map(|g| {
+ let cow: Cow<str> = g.into();
+ if str_is_line_ending(&cow) {
+ cow
+ } else {
+ ch.into()
+ }
+ })
+ .collect();
+
+ (range.from(), range.to(), Some(text.into()))
+ } else {
+ // No change.
+ (range.from(), range.to(), None)
+ }
+ });
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
@@ -800,24 +812,24 @@ fn replace(cx: &mut Context) {
fn switch_case(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let transaction =
- Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
- let text: Tendril = range
- .fragment(doc.text().slice(..))
- .chars()
- .flat_map(|ch| {
- if ch.is_lowercase() {
- ch.to_uppercase().collect()
- } else if ch.is_uppercase() {
- ch.to_lowercase().collect()
- } else {
- vec![ch]
- }
- })
- .collect();
+ let selection = doc.selection(view.id);
+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
+ let text: Tendril = range
+ .fragment(doc.text().slice(..))
+ .chars()
+ .flat_map(|ch| {
+ if ch.is_lowercase() {
+ ch.to_uppercase().collect()
+ } else if ch.is_uppercase() {
+ ch.to_lowercase().collect()
+ } else {
+ vec![ch]
+ }
+ })
+ .collect();
- (range.from(), range.to() + 1, Some(text))
- });
+ (range.from(), range.to(), Some(text))
+ });
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
@@ -825,12 +837,12 @@ fn switch_case(cx: &mut Context) {
fn switch_to_uppercase(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let transaction =
- Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
- let text: Tendril = range.fragment(doc.text().slice(..)).to_uppercase().into();
+ let selection = doc.selection(view.id);
+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
+ let text: Tendril = range.fragment(doc.text().slice(..)).to_uppercase().into();
- (range.from(), range.to() + 1, Some(text))
- });
+ (range.from(), range.to(), Some(text))
+ });
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
@@ -838,12 +850,12 @@ fn switch_to_uppercase(cx: &mut Context) {
fn switch_to_lowercase(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let transaction =
- Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
- let text: Tendril = range.fragment(doc.text().slice(..)).to_lowercase().into();
+ let selection = doc.selection(view.id);
+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
+ let text: Tendril = range.fragment(doc.text().slice(..)).to_lowercase().into();
- (range.from(), range.to() + 1, Some(text))
- });
+ (range.from(), range.to(), Some(text))
+ });
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
@@ -852,7 +864,12 @@ fn switch_to_lowercase(cx: &mut Context) {
fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
use Direction::*;
let (view, doc) = current!(cx.editor);
- let cursor = coords_at_pos(doc.text().slice(..), doc.selection(view.id).cursor());
+ let cursor = coords_at_pos(
+ doc.text().slice(..),
+ doc.selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..)),
+ );
let doc_last_line = doc.text().len_lines() - 1;
let last_line = view.last_line(doc);
@@ -881,7 +898,7 @@ fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
.min(last_line.saturating_sub(scrolloff));
let text = doc.text().slice(..);
- let pos = pos_at_coords(text, Position::new(line, cursor.col)); // this func will properly truncate to line end
+ let pos = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end
// TODO: only manipulate main selection
doc.set_selection(view.id, Selection::point(pos));
@@ -915,7 +932,8 @@ fn extend_char_left(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+
+ let selection = doc.selection(view.id).clone().transform(|range| {
movement::move_horizontally(text, range, Direction::Backward, count, Movement::Extend)
});
doc.set_selection(view.id, selection);
@@ -925,7 +943,8 @@ fn extend_char_right(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+
+ let selection = doc.selection(view.id).clone().transform(|range| {
movement::move_horizontally(text, range, Direction::Forward, count, Movement::Extend)
});
doc.set_selection(view.id, selection);
@@ -935,7 +954,8 @@ fn extend_line_up(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+
+ let selection = doc.selection(view.id).clone().transform(|range| {
movement::move_vertically(text, range, Direction::Backward, count, Movement::Extend)
});
doc.set_selection(view.id, selection);
@@ -945,7 +965,8 @@ fn extend_line_down(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+
+ let selection = doc.selection(view.id).clone().transform(|range| {
movement::move_vertically(text, range, Direction::Forward, count, Movement::Extend)
});
doc.set_selection(view.id, selection);
@@ -954,7 +975,7 @@ fn extend_line_down(cx: &mut Context) {
fn select_all(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let end = rope_end_without_line_ending(&doc.text().slice(..));
+ let end = doc.text().len_chars();
doc.set_selection(view.id, Selection::single(0, end))
}
@@ -992,9 +1013,14 @@ fn split_selection_on_newline(cx: &mut Context) {
}
fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) {
- let text = doc.text();
+ let text = doc.text().slice(..);
let selection = doc.selection(view.id);
- let start = text.char_to_byte(selection.cursor());
+
+ // Get the right side of the primary block cursor.
+ let start = text.char_to_byte(graphemes::next_grapheme_boundary(
+ text,
+ selection.primary().cursor(text),
+ ));
// use find_at to find the next match after the cursor, loop around the end
// Careful, `Regex` uses `bytes` as offsets, not character indices!
@@ -1011,12 +1037,10 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege
return;
}
- let head = end - 1;
-
let selection = if extend {
- selection.clone().push(Range::new(start, head))
+ selection.clone().push(Range::new(start, end))
} else {
- Selection::single(start, head)
+ Selection::single(start, end)
};
doc.set_selection(view.id, selection);
@@ -1079,16 +1103,15 @@ fn extend_line(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
- let pos = doc.selection(view.id).primary();
let text = doc.text();
+ let range = doc.selection(view.id).primary();
- let line_start = text.char_to_line(pos.anchor);
- let start = text.line_to_char(line_start);
- let line_end = text.char_to_line(pos.head);
- let mut end = line_end_char_index(&text.slice(..), line_end + count.saturating_sub(1));
+ let (start_line, end_line) = range.line_range(text.slice(..));
+ let start = text.line_to_char(start_line);
+ let mut end = text.line_to_char((end_line + count).min(text.len_lines()));
- if pos.anchor == start && pos.head == end && line_end < (text.len_lines() - 2) {
- end = line_end_char_index(&text.slice(..), line_end + 1);
+ if range.from() == start && range.to() == end {
+ end = text.line_to_char((end_line + count + 1).min(text.len_lines()));
}
doc.set_selection(view.id, Selection::single(start, end));
@@ -1097,41 +1120,36 @@ fn extend_line(cx: &mut Context) {
fn extend_to_line_bounds(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let text = doc.text();
- let selection = doc.selection(view.id).transform(|range| {
- let start = text.line_to_char(text.char_to_line(range.from()));
- let end = text
- .line_to_char(text.char_to_line(range.to()) + 1)
- .saturating_sub(1);
-
- if range.anchor < range.head {
- Range::new(start, end)
- } else {
- Range::new(end, start)
- }
- });
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ let text = doc.text();
- doc.set_selection(view.id, selection);
+ let (start_line, end_line) = range.line_range(text.slice(..));
+ let start = text.line_to_char(start_line);
+ let end = text.line_to_char((end_line + 1).min(text.len_lines()));
+
+ if range.anchor <= range.head {
+ Range::new(start, end)
+ } else {
+ Range::new(end, start)
+ }
+ }),
+ );
}
fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
- // first yank the selection
- let values: Vec<String> = doc
- .selection(view_id)
- .fragments(doc.text().slice(..))
- .map(Cow::into_owned)
- .collect();
+ let text = doc.text().slice(..);
+ let selection = doc.selection(view_id);
+ // first yank the selection
+ let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
reg.write(values);
// then delete
- let transaction =
- Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
- let alltext = doc.text().slice(..);
- let max_to = rope_end_without_line_ending(&alltext);
- let to = std::cmp::min(max_to, range.to() + 1);
- (range.from(), to, None)
- });
+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
+ (range.from(), range.to(), None)
+ });
doc.apply(&transaction, view_id);
}
@@ -1159,19 +1177,22 @@ fn change_selection(cx: &mut Context) {
fn collapse_selection(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let selection = doc
- .selection(view.id)
- .transform(|range| Range::new(range.head, range.head));
+ let text = doc.text().slice(..);
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ let pos = range.cursor(text);
+ Range::new(pos, pos)
+ });
doc.set_selection(view.id, selection);
}
fn flip_selections(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
+
let selection = doc
.selection(view.id)
+ .clone()
.transform(|range| Range::new(range.head, range.anchor));
-
doc.set_selection(view.id, selection);
}
@@ -1186,6 +1207,7 @@ fn insert_mode(cx: &mut Context) {
let selection = doc
.selection(view.id)
+ .clone()
.transform(|range| Range::new(range.to(), range.from()));
doc.set_selection(view.id, selection);
}
@@ -1195,18 +1217,13 @@ fn append_mode(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
enter_insert_mode(doc);
doc.restore_cursor = true;
-
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
- Range::new(
- range.from(),
- graphemes::next_grapheme_boundary(text, range.to()), // to() + next char
- )
- });
+ // Make sure there's room at the end of the document if the last
+ // selection butts up against it.
let end = text.len_chars();
-
- if selection.iter().any(|range| range.head == end) {
+ let last_range = doc.selection(view.id).iter().last().unwrap();
+ if !last_range.is_empty() && last_range.head == end {
let transaction = Transaction::change(
doc.text(),
std::array::IntoIter::new([(end, end, Some(doc.line_ending.as_str().into()))]),
@@ -1214,6 +1231,12 @@ fn append_mode(cx: &mut Context) {
doc.apply(&transaction, view.id);
}
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ Range::new(
+ range.from(),
+ graphemes::next_grapheme_boundary(doc.text().slice(..), range.to()),
+ )
+ });
doc.set_selection(view.id, selection);
}
@@ -1636,11 +1659,10 @@ mod cmd {
match cx.editor.clipboard_provider.get_contents() {
Ok(contents) => {
+ let selection = doc.selection(view.id);
let transaction =
- Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
- let max_to = rope_end_without_line_ending(&doc.text().slice(..));
- let to = std::cmp::min(max_to, range.to() + 1);
- (range.from(), to, Some(contents.as_str().into()))
+ Transaction::change_by_selection(doc.text(), &selection, |range| {
+ (range.from(), range.to(), Some(contents.as_str().into()))
});
doc.apply(&transaction, view.id);
@@ -2236,10 +2258,10 @@ fn append_to_line(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
enter_insert_mode(doc);
- let selection = doc.selection(view.id).transform(|range| {
- let text = doc.text();
- let line = text.char_to_line(range.head);
- let pos = line_end_char_index(&text.slice(..), line);
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ let text = doc.text().slice(..);
+ let line = range.cursor_line(text);
+ let pos = line_end_char_index(&text, line);
Range::new(pos, pos)
});
doc.set_selection(view.id, selection);
@@ -2299,7 +2321,7 @@ fn open(cx: &mut Context, open: Open) {
let mut offs = 0;
let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
- let line = text.char_to_line(range.head);
+ let line = range.cursor_line(text);
let line = match open {
// adjust position to the end of the line (next line - 1)
@@ -2373,7 +2395,7 @@ fn normal_mode(cx: &mut Context) {
// if leaving append mode, move cursor back by 1
if doc.restore_cursor {
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+ let selection = doc.selection(view.id).clone().transform(|range| {
Range::new(
range.from(),
graphemes::prev_grapheme_boundary(text, range.to()),
@@ -2413,6 +2435,23 @@ fn goto_last_accessed_file(cx: &mut Context) {
}
fn select_mode(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+
+ // Make sure end-of-document selections are also 1-width.
+ // (With the exception of being in an empty document, of course.)
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ if range.is_empty() && range.head == text.len_chars() {
+ Range::new(
+ graphemes::prev_grapheme_boundary(text, range.anchor),
+ range.head,
+ )
+ } else {
+ range
+ }
+ });
+ doc.set_selection(view.id, selection);
+
doc_mut!(cx.editor).mode = Mode::Select;
}
@@ -2485,7 +2524,13 @@ fn goto_definition(cx: &mut Context) {
let offset_encoding = language_server.offset_encoding();
- let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding);
+ let pos = pos_to_lsp_pos(
+ doc.text(),
+ doc.selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..)),
+ offset_encoding,
+ );
// TODO: handle fails
let future = language_server.goto_definition(doc.identifier(), pos, None);
@@ -2522,7 +2567,13 @@ fn goto_type_definition(cx: &mut Context) {
let offset_encoding = language_server.offset_encoding();
- let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding);
+ let pos = pos_to_lsp_pos(
+ doc.text(),
+ doc.selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..)),
+ offset_encoding,
+ );
// TODO: handle fails
let future = language_server.goto_type_definition(doc.identifier(), pos, None);
@@ -2559,7 +2610,13 @@ fn goto_implementation(cx: &mut Context) {
let offset_encoding = language_server.offset_encoding();
- let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding);
+ let pos = pos_to_lsp_pos(
+ doc.text(),
+ doc.selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..)),
+ offset_encoding,
+ );
// TODO: handle fails
let future = language_server.goto_implementation(doc.identifier(), pos, None);
@@ -2596,7 +2653,13 @@ fn goto_reference(cx: &mut Context) {
let offset_encoding = language_server.offset_encoding();
- let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding);
+ let pos = pos_to_lsp_pos(
+ doc.text(),
+ doc.selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..)),
+ offset_encoding,
+ );
// TODO: handle fails
let future = language_server.goto_reference(doc.identifier(), pos, None);
@@ -2655,7 +2718,10 @@ fn goto_next_diag(cx: &mut Context) {
let editor = &mut cx.editor;
let (view, doc) = current!(editor);
- let cursor_pos = doc.selection(view.id).cursor();
+ let cursor_pos = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
let diag = if let Some(diag) = doc
.diagnostics()
.iter()
@@ -2676,7 +2742,10 @@ fn goto_prev_diag(cx: &mut Context) {
let editor = &mut cx.editor;
let (view, doc) = current!(editor);
- let cursor_pos = doc.selection(view.id).cursor();
+ let cursor_pos = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
let diag = if let Some(diag) = doc
.diagnostics()
.iter()
@@ -2704,7 +2773,9 @@ fn signature_help(cx: &mut Context) {
let pos = pos_to_lsp_pos(
doc.text(),
- doc.selection(view.id).cursor(),
+ doc.selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..)),
language_server.offset_encoding(),
);
@@ -2827,11 +2898,11 @@ pub mod insert {
let (view, doc) = current!(cx.editor);
let text = doc.text();
- let selection = doc.selection(view.id);
+ let selection = doc.selection(view.id).clone().cursors(text.slice(..));
// run through insert hooks, stopping on the first one that returns Some(t)
for hook in HOOKS {
- if let Some(transaction) = hook(text, selection, c) {
+ if let Some(transaction) = hook(text, &selection, c) {
doc.apply(&transaction, view.id);
break;
}
@@ -2851,7 +2922,11 @@ pub mod insert {
// indent by one to reach 4 spaces).
let indent = Tendril::from(doc.indent_unit());
- let transaction = Transaction::insert(doc.text(), doc.selection(view.id), indent);
+ let transaction = Transaction::insert(
+ doc.text(),
+ &doc.selection(view.id).clone().cursors(doc.text().slice(..)),
+ indent,
+ );
doc.apply(&transaction, view.id);
}
@@ -2860,13 +2935,13 @@ pub mod insert {
let text = doc.text().slice(..);
let contents = doc.text();
- let selection = doc.selection(view.id);
+ let selection = doc.selection(view.id).clone().cursors(text);
let mut ranges = SmallVec::with_capacity(selection.len());
// TODO: this is annoying, but we need to do it to properly calculate pos after edits
let mut offs = 0;
- let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
+ let mut transaction = Transaction::change_by_selection(contents, &selection, |range| {
let pos = range.head;
let prev = if pos == 0 {
@@ -2929,9 +3004,10 @@ pub mod insert {
let text = doc.text().slice(..);
let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
+ let pos = range.cursor(text);
(
- graphemes::nth_prev_grapheme_boundary(text, range.head, count),
- range.head,
+ graphemes::nth_prev_grapheme_boundary(text, pos, count),
+ pos,
None,
)
});
@@ -2944,9 +3020,10 @@ pub mod insert {
let text = doc.text().slice(..);
let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
+ let pos = range.cursor(text);
(
- range.head,
- graphemes::nth_next_grapheme_boundary(text, range.head, count),
+ pos,
+ graphemes::nth_next_grapheme_boundary(text, pos, count),
None,
)
});
@@ -2957,8 +3034,10 @@ pub mod insert {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
+
let selection = doc
.selection(view.id)
+ .clone()
.transform(|range| movement::move_prev_word_start(text, range, count));
doc.set_selection(view.id, selection);
delete_selection(cx)
@@ -2986,9 +3065,11 @@ fn redo(cx: &mut Context) {
fn yank(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+
let values: Vec<String> = doc
.selection(view.id)
- .fragments(doc.text().slice(..))
+ .fragments(text)
.map(Cow::into_owned)
.collect();
@@ -3007,10 +3088,11 @@ fn yank(cx: &mut Context) {
fn yank_joined_to_clipboard_impl(editor: &mut Editor, separator: &str) -> anyhow::Result<()> {
let (view, doc) = current!(editor);
+ let text = doc.text().slice(..);
let values: Vec<String> = doc
.selection(view.id)
- .fragments(doc.text().slice(..))
+ .fragments(text)
.map(Cow::into_owned)
.collect();
@@ -3038,11 +3120,9 @@ fn yank_joined_to_clipboard(cx: &mut Context) {
fn yank_main_selection_to_clipboard_impl(editor: &mut Editor) -> anyhow::Result<()> {
let (view, doc) = current!(editor);
+ let text = doc.text().slice(..);
- let value = doc
- .selection(view.id)
- .primary()
- .fragment(doc.text().slice(..));
+ let value = doc.selection(view.id).primary().fragment(text);
if let Err(e) = editor.clipboard_provider.set_contents(value.into_owned()) {
bail!("Couldn't set system clipboard content: {:?}", e);
@@ -3083,17 +3163,21 @@ fn paste_impl(
let mut values = values.iter().cloned().map(Tendril::from).chain(repeat);
let text = doc.text();
+ let selection = doc.selection(view.id);
- let transaction = Transaction::change_by_selection(text, doc.selection(view.id), |range| {
+ let transaction = Transaction::change_by_selection(text, selection, |range| {
let pos = match (action, linewise) {
// paste linewise before
(Paste::Before, true) => text.line_to_char(text.char_to_line(range.from())),
// paste linewise after
- (Paste::After, true) => text.line_to_char(text.char_to_line(range.to()) + 1),
+ (Paste::After, true) => {
+ let line = range.line_range(text.slice(..)).1;
+ text.line_to_char((line + 1).min(text.len_lines()))
+ }
// paste insert
(Paste::Before, false) => range.from(),
// paste append
- (Paste::After, false) => range.to() + 1,
+ (Paste::After, false) => range.to(),
};
(pos, pos, Some(values.next().unwrap()))
});
@@ -3134,12 +3218,14 @@ fn replace_with_yanked(cx: &mut Context) {
if let Some(values) = registers.read(reg_name) {
if let Some(yank) = values.first() {
- let transaction =
- Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
- let max_to = rope_end_without_line_ending(&doc.text().slice(..));
- let to = std::cmp::min(max_to, range.to() + 1);
- (range.from(), to, Some(yank.as_str().into()))
- });
+ let selection = doc.selection(view.id);
+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
+ if !range.is_empty() {
+ (range.from(), range.to(), Some(yank.as_str().into()))
+ } else {
+ (range.from(), range.to(), None)
+ }
+ });
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
@@ -3152,12 +3238,10 @@ fn replace_selections_with_clipboard_impl(editor: &mut Editor) -> anyhow::Result
match editor.clipboard_provider.get_contents() {
Ok(contents) => {
- let transaction =
- Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
- let max_to = rope_end_without_line_ending(&doc.text().slice(..));
- let to = std::cmp::min(max_to, range.to() + 1);
- (range.from(), to, Some(contents.as_str().into()))
- });
+ let selection = doc.selection(view.id);
+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
+ (range.from(), range.to(), Some(contents.as_str().into()))
+ });
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
@@ -3204,8 +3288,7 @@ fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
// Get all line numbers
for range in doc.selection(view_id) {
- let start = doc.text().char_to_line(range.from());
- let end = doc.text().char_to_line(range.to());
+ let (start, end) = range.line_range(doc.text().slice(..));
for line in start..=end {
lines.push(line)
@@ -3333,10 +3416,9 @@ fn join_selections(cx: &mut Context) {
let fragment = Tendril::from(" ");
for selection in doc.selection(view.id) {
- let start = text.char_to_line(selection.from());
- let mut end = text.char_to_line(selection.to());
+ let (start, mut end) = selection.line_range(slice);
if start == end {
- end += 1
+ end = (end + 1).min(text.len_lines() - 1);
}
let lines = start..end;
@@ -3433,13 +3515,17 @@ fn completion(cx: &mut Context) {
};
let offset_encoding = language_server.offset_encoding();
+ let cursor = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
- let pos = pos_to_lsp_pos(doc.text(), doc.selection(view.id).cursor(), offset_encoding);
+ let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
// TODO: handle fails
let future = language_server.completion(doc.identifier(), pos, None);
- let trigger_offset = doc.selection(view.id).cursor();
+ let trigger_offset = cursor;
cx.callback(
future,
@@ -3489,7 +3575,9 @@ fn hover(cx: &mut Context) {
let pos = pos_to_lsp_pos(
doc.text(),
- doc.selection(view.id).cursor(),
+ doc.selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..)),
language_server.offset_encoding(),
);
@@ -3555,7 +3643,10 @@ fn match_brackets(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
if let Some(syntax) = doc.syntax() {
- let pos = doc.selection(view.id).cursor();
+ let pos = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) {
let selection = Selection::point(pos);
doc.set_selection(view.id, selection);
@@ -3655,7 +3746,10 @@ fn align_view_bottom(cx: &mut Context) {
fn align_view_middle(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let pos = doc.selection(view.id).cursor();
+ let pos = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
let pos = coords_at_pos(doc.text().slice(..), pos);
const OFFSET: usize = 7; // gutters
@@ -3687,7 +3781,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
+ let selection = doc.selection(view.id).clone().transform(|range| {
match ch {
'w' => textobject::textobject_word(text, range, objtype, count),
// TODO: cancel new ranges if inconsistent surround matches across lines
@@ -3697,7 +3791,6 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
_ => range,
}
});
-
doc.set_selection(view.id, selection);
}
})
@@ -3707,18 +3800,13 @@ fn surround_add(cx: &mut Context) {
cx.on_next_key(move |cx, event| {
if let Some(ch) = event.char() {
let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
let selection = doc.selection(view.id);
let (open, close) = surround::get_pair(ch);
let mut changes = Vec::new();
for range in selection.iter() {
- let from = range.from();
- let max_to = rope_end_without_line_ending(&text);
- let to = std::cmp::min(range.to() + 1, max_to);
-
- changes.push((from, from, Some(Tendril::from_char(open))));
- changes.push((to, to, Some(Tendril::from_char(close))));
+ changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
+ changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));
}
let transaction = Transaction::change(doc.text(), changes.into_iter());
@@ -3748,8 +3836,11 @@ fn surround_replace(cx: &mut Context) {
let transaction = Transaction::change(
doc.text(),
change_pos.iter().enumerate().map(|(i, &pos)| {
- let ch = if i % 2 == 0 { open } else { close };
- (pos, pos + 1, Some(Tendril::from_char(ch)))
+ if i % 2 == 0 {
+ (pos, pos + 1, Some(Tendril::from_char(open)))
+ } else {
+ (pos.saturating_sub(1), pos, Some(Tendril::from_char(close)))
+ }
}),
);
doc.apply(&transaction, view.id);
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index be6db42c..2725d53d 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -86,7 +86,10 @@ impl Completion {
let item = item.unwrap();
// if more text was entered, remove it
- let cursor = doc.selection(view.id).cursor();
+ let cursor = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
if trigger_offset < cursor {
let remove = Transaction::change(
doc.text(),
@@ -109,7 +112,10 @@ impl Completion {
)
} else {
let text = item.insert_text.as_ref().unwrap_or(&item.label);
- let cursor = doc.selection(view.id).cursor();
+ let cursor = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
Transaction::change(
doc.text(),
vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
@@ -155,7 +161,10 @@ impl Completion {
// TODO: hooks should get processed immediately so maybe do it after select!(), before
// looping?
- let cursor = doc.selection(view.id).cursor();
+ let cursor = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
if self.trigger_offset <= cursor {
let fragment = doc.text().slice(self.trigger_offset..cursor);
let text = Cow::from(fragment);
@@ -212,7 +221,10 @@ impl Component for Completion {
.language()
.and_then(|scope| scope.strip_prefix("source."))
.unwrap_or("");
- let cursor_pos = doc.selection(view.id).cursor();
+ let cursor_pos = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- view.first_line) as u16;
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 99b49309..d5c907b8 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_grapheme_boundary},
+ graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary},
syntax::{self, HighlightEvent},
unicode::segmentation::UnicodeSegmentation,
unicode::width::UnicodeWidthStr,
@@ -166,8 +166,8 @@ impl EditorView {
let highlights = highlights.into_iter().map(|event| match event.unwrap() {
// convert byte offsets to char offset
HighlightEvent::Source { start, end } => {
- let start = ensure_grapheme_boundary(text, text.byte_to_char(start));
- let end = ensure_grapheme_boundary(text, text.byte_to_char(end));
+ let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start));
+ let end = ensure_grapheme_boundary_next(text, text.byte_to_char(end));
HighlightEvent::Source { start, end }
}
event => event,
@@ -191,21 +191,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)
@@ -213,24 +210,23 @@ impl EditorView {
(cursor_scope, selection_scope)
};
- let cursor_end = next_grapheme_boundary(text, range.head); // Used in every case below.
-
- if range.head == range.anchor {
- spans.push((cursor_scope, range.head..cursor_end));
+ // 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..cursor_end));
- spans.push((
- selection_scope,
- cursor_end..next_grapheme_boundary(text, range.anchor),
- ));
+ 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));
+ // 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));
}
}
@@ -263,7 +259,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};
@@ -332,7 +331,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 + 1)).enumerate() {
use helix_core::diagnostic::Severity;
if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
surface.set_stringn(
@@ -349,11 +352,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,
);
@@ -367,19 +376,34 @@ 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)
};
let selection = doc.selection(view.id);
for selection in selection.iter().filter(|range| range.overlaps(&screen)) {
- let head = view.screen_coords_at_pos(doc, text, selection.head);
+ let head = view.screen_coords_at_pos(
+ doc,
+ text,
+ if selection.head > selection.anchor {
+ selection.head - 1
+ } else {
+ 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,
);
@@ -387,7 +411,10 @@ impl EditorView {
// TODO: set cursor position for IME
if let Some(syntax) = doc.syntax() {
use helix_core::match_brackets;
- let pos = doc.selection(view.id).cursor();
+ let pos = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
let pos = match_brackets::find(syntax, doc.text(), pos)
.and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
@@ -432,7 +459,10 @@ impl EditorView {
widgets::{Paragraph, Widget},
};
- let cursor = doc.selection(view.id).cursor();
+ let cursor = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
@@ -544,7 +574,12 @@ impl EditorView {
// _ => "indent:ERROR",
// };
let position_info = {
- let pos = coords_at_pos(doc.text().slice(..), doc.selection(view.id).cursor());
+ let pos = coords_at_pos(
+ doc.text().slice(..),
+ doc.selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..)),
+ );
format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing
};