summaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/commands.rs693
-rw-r--r--helix-term/src/ui/editor.rs76
2 files changed, 436 insertions, 333 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 63b91942..fbeae5ff 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -322,124 +322,152 @@ impl PartialEq for Command {
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| {
- movement::move_horizontally(text, range, Direction::Backward, count, Movement::Move)
- });
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ movement::move_horizontally(
+ doc.text().slice(..),
+ range,
+ Direction::Backward,
+ count,
+ Movement::Move,
+ )
+ }),
+ );
}
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| {
- movement::move_horizontally(text, range, Direction::Forward, count, Movement::Move)
- });
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ movement::move_horizontally(
+ doc.text().slice(..),
+ range,
+ Direction::Forward,
+ count,
+ Movement::Move,
+ )
+ }),
+ );
}
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| {
- movement::move_vertically(text, range, Direction::Backward, count, Movement::Move)
- });
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ movement::move_vertically(
+ doc.text().slice(..),
+ range,
+ Direction::Backward,
+ count,
+ Movement::Move,
+ )
+ }),
+ );
}
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| {
- movement::move_vertically(text, range, Direction::Forward, count, Movement::Move)
- });
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ movement::move_vertically(
+ doc.text().slice(..),
+ range,
+ Direction::Forward,
+ count,
+ Movement::Move,
+ )
+ }),
+ );
}
fn goto_line_end(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ let text = doc.text();
+ let line = text.char_to_line(range.head);
- 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 pos = graphemes::nth_prev_grapheme_boundary(text.slice(..), pos, 1);
- let pos = range.head.max(pos).max(text.line_to_char(line));
-
- Range::new(
- match doc.mode {
- Mode::Normal | Mode::Insert => pos,
- Mode::Select => range.anchor,
- },
- pos,
- )
- });
+ 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));
- doc.set_selection(view.id, selection);
+ Range::new(
+ match doc.mode {
+ Mode::Normal | Mode::Insert => pos,
+ Mode::Select => range.anchor,
+ },
+ pos,
+ )
+ }),
+ );
}
fn goto_line_end_newline(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- 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);
- Range::new(pos, pos)
- });
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ let text = doc.text();
+ let line = text.char_to_line(range.head);
- doc.set_selection(view.id, selection);
+ let pos = line_end_char_index(&text.slice(..), line);
+ Range::new(pos, pos)
+ }),
+ );
}
fn goto_line_start(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ let text = doc.text();
+ let line = text.char_to_line(range.head);
- let selection = doc.selection(view.id).transform(|range| {
- let text = doc.text();
- let line = text.char_to_line(range.head);
-
- // 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,
- )
- });
-
- doc.set_selection(view.id, selection);
-}
-
-fn goto_first_nonwhitespace(cx: &mut Context) {
- let (view, doc) = current!(cx.editor);
-
- let selection = doc.selection(view.id).transform(|range| {
- let text = doc.text();
- let line_idx = text.char_to_line(range.head);
-
- if let Some(pos) = find_first_non_whitespace_char(text.line(line_idx)) {
- let pos = pos + text.line_to_char(line_idx);
+ // adjust to start of the line
+ let pos = text.line_to_char(line);
Range::new(
match doc.mode {
- Mode::Normal => pos,
+ Mode::Normal | Mode::Insert => pos,
Mode::Select => range.anchor,
- Mode::Insert => unreachable!(),
},
pos,
)
- } else {
- range
- }
- });
+ }),
+ );
+}
- doc.set_selection(view.id, selection);
+fn goto_first_nonwhitespace(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ let text = doc.text();
+ let line_idx = text.char_to_line(range.head);
+
+ 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 | Mode::Insert => pos,
+ Mode::Select => range.anchor,
+ },
+ pos,
+ )
+ } else {
+ range
+ }
+ }),
+ );
}
fn goto_window(cx: &mut Context, align: Align) {
@@ -480,73 +508,68 @@ fn goto_window_bottom(cx: &mut Context) {
fn move_next_word_start(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| movement::move_next_word_start(text, range, count));
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id)
+ .clone()
+ .transform(|range| movement::move_next_word_start(doc.text().slice(..), range, count)),
+ );
}
fn move_prev_word_start(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| movement::move_prev_word_start(text, range, count));
-
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id)
+ .clone()
+ .transform(|range| movement::move_prev_word_start(doc.text().slice(..), range, count)),
+ );
}
fn move_next_word_end(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| movement::move_next_word_end(text, range, count));
-
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id)
+ .clone()
+ .transform(|range| movement::move_next_word_end(doc.text().slice(..), range, count)),
+ );
}
fn move_next_long_word_start(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| movement::move_next_long_word_start(text, range, count));
-
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ movement::move_next_long_word_start(doc.text().slice(..), range, count)
+ }),
+ );
}
fn move_prev_long_word_start(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| movement::move_prev_long_word_start(text, range, count));
-
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ movement::move_prev_long_word_start(doc.text().slice(..), range, count)
+ }),
+ );
}
fn move_next_long_word_end(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| movement::move_next_long_word_end(text, range, count));
-
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ movement::move_next_long_word_end(doc.text().slice(..), range, count)
+ }),
+ );
}
fn goto_file_start(cx: &mut Context) {
@@ -566,42 +589,40 @@ fn goto_file_end(cx: &mut Context) {
fn extend_next_word_start(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 word = movement::move_next_word_start(text, range, count);
- let pos = word.head;
- Range::new(range.anchor, pos)
- });
-
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ let word = movement::move_next_word_start(doc.text().slice(..), range, count);
+ let pos = word.head;
+ Range::new(range.anchor, pos)
+ }),
+ );
}
fn extend_prev_word_start(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 word = movement::move_prev_word_start(text, range, count);
- let pos = word.head;
- Range::new(range.anchor, pos)
- });
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ let word = movement::move_prev_word_start(doc.text().slice(..), range, count);
+ let pos = word.head;
+ Range::new(range.anchor, pos)
+ }),
+ );
}
fn extend_next_word_end(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 word = movement::move_next_word_end(text, range, count);
- let pos = word.head;
- Range::new(range.anchor, pos)
- });
-
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ let word = movement::move_next_word_end(doc.text().slice(..), range, count);
+ let pos = word.head;
+ Range::new(range.anchor, pos)
+ }),
+ );
}
#[inline]
@@ -644,21 +665,24 @@ 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
- })
- });
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ search_fn(doc.text().slice(..), 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
+ },
+ )
+ }),
+ );
})
}
@@ -752,24 +776,30 @@ 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 text = doc.text().slice(..);
+ let selection = doc.selection(view.id).clone().min_width_1(text);
- (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);
@@ -842,41 +872,69 @@ fn half_page_down(cx: &mut Context) {
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| {
- movement::move_horizontally(text, range, Direction::Backward, count, Movement::Extend)
- });
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ movement::move_horizontally(
+ doc.text().slice(..),
+ range,
+ Direction::Backward,
+ count,
+ Movement::Extend,
+ )
+ }),
+ );
}
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| {
- movement::move_horizontally(text, range, Direction::Forward, count, Movement::Extend)
- });
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ movement::move_horizontally(
+ doc.text().slice(..),
+ range,
+ Direction::Forward,
+ count,
+ Movement::Extend,
+ )
+ }),
+ );
}
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| {
- movement::move_vertically(text, range, Direction::Backward, count, Movement::Extend)
- });
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ movement::move_vertically(
+ doc.text().slice(..),
+ range,
+ Direction::Backward,
+ count,
+ Movement::Extend,
+ )
+ }),
+ );
}
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| {
- movement::move_vertically(text, range, Direction::Forward, count, Movement::Extend)
- });
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ movement::move_vertically(
+ doc.text().slice(..),
+ range,
+ Direction::Forward,
+ count,
+ Movement::Extend,
+ )
+ }),
+ );
}
fn select_all(cx: &mut Context) {
@@ -1025,41 +1083,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();
+ 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);
- doc.set_selection(view.id, selection);
+ 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).clone().min_width_1(text);
+ // 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);
}
@@ -1087,20 +1140,24 @@ 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));
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id)
+ .clone()
+ .transform(|range| Range::new(range.head, range.head)),
+ );
}
fn flip_selections(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
- let selection = doc
- .selection(view.id)
- .transform(|range| Range::new(range.head, range.anchor));
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id)
+ .clone()
+ .transform(|range| Range::new(range.head, range.anchor)),
+ );
}
fn enter_insert_mode(doc: &mut Document) {
@@ -1112,10 +1169,12 @@ fn insert_mode(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
enter_insert_mode(doc);
- let selection = doc
- .selection(view.id)
- .transform(|range| Range::new(range.to(), range.from()));
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id)
+ .clone()
+ .transform(|range| Range::new(range.to(), range.from())),
+ );
}
// inserts at the end of each selection
@@ -1124,15 +1183,14 @@ fn append_mode(cx: &mut Context) {
enter_insert_mode(doc);
doc.restore_cursor = true;
- 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::next_grapheme_boundary(text, range.to()), // to() + next char
+ graphemes::next_grapheme_boundary(doc.text().slice(..), range.to()), // to() + next char
)
});
- let end = text.len_chars();
+ let end = doc.text().len_chars();
if selection.iter().any(|range| range.head == end) {
let transaction = Transaction::change(
@@ -1508,11 +1566,13 @@ mod cmd {
match cx.editor.clipboard_provider.get_contents() {
Ok(contents) => {
+ let selection = doc
+ .selection(view.id)
+ .clone()
+ .min_width_1(doc.text().slice(..));
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);
@@ -1983,13 +2043,15 @@ 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);
- Range::new(pos, pos)
- });
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ let text = &doc.text().slice(..);
+ let line = text.char_to_line(range.head);
+ let pos = line_end_char_index(text, line);
+ Range::new(pos, pos)
+ }),
+ );
}
/// Sometimes when applying formatting changes we want to mark the buffer as unmodified, for
@@ -2119,14 +2181,15 @@ 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| {
- Range::new(
- range.from(),
- graphemes::prev_grapheme_boundary(text, range.to()),
- )
- });
- doc.set_selection(view.id, selection);
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ Range::new(
+ range.from(),
+ graphemes::prev_grapheme_boundary(doc.text().slice(..), range.to()),
+ )
+ }),
+ );
doc.restore_cursor = false;
}
@@ -2149,6 +2212,24 @@ fn goto_last_accessed_file(cx: &mut Context) {
}
fn select_mode(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+
+ // Make sure all selections are at least 1-wide.
+ // (With the exception of being in an empty document, of course.)
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ if range.is_empty() && range.head == doc.text().len_chars() {
+ Range::new(
+ graphemes::prev_grapheme_boundary(doc.text().slice(..), range.anchor),
+ range.head,
+ )
+ } else {
+ range.min_width_1(doc.text().slice(..))
+ }
+ }),
+ );
+
doc_mut!(cx.editor).mode = Mode::Select;
}
@@ -2161,7 +2242,7 @@ fn goto_prehook(cx: &mut Context) -> bool {
push_jump(cx.editor);
let (view, doc) = current!(cx.editor);
- let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(2));
+ let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(1));
let pos = doc.text().line_to_char(line_idx);
doc.set_selection(view.id, Selection::point(pos));
true
@@ -2706,11 +2787,13 @@ pub mod insert {
pub fn delete_word_backward(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| movement::move_prev_word_start(text, range, count));
- doc.set_selection(view.id, selection);
+
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ movement::move_prev_word_start(doc.text().slice(..), range, count)
+ }),
+ );
delete_selection(cx)
}
}
@@ -2829,17 +2912,18 @@ fn paste_impl(
let mut values = values.iter().cloned().map(Tendril::from).chain(repeat);
let text = doc.text();
+ let selection = doc.selection(view.id).clone().min_width_1(text.slice(..));
- 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) => text.line_to_char(text.char_to_line(range.to())),
// 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()))
});
@@ -2879,12 +2963,17 @@ 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)
+ .clone()
+ .min_width_1(doc.text().slice(..));
+ 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);
@@ -2897,12 +2986,13 @@ fn replace_selections_with_clipboard_impl(editor: &mut Editor) {
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)
+ .clone()
+ .min_width_1(doc.text().slice(..));
+ 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);
@@ -3510,20 +3600,21 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
} = event
{
let (view, doc) = current!(cx.editor);
- let text = doc.text().slice(..);
- let selection = doc.selection(view.id).transform(|range| {
- match ch {
- 'w' => textobject::textobject_word(text, range, objtype, count),
- // TODO: cancel new ranges if inconsistent surround matches across lines
- ch if !ch.is_ascii_alphanumeric() => {
- textobject::textobject_surround(text, range, objtype, ch, count)
+ doc.set_selection(
+ view.id,
+ doc.selection(view.id).clone().transform(|range| {
+ let text = doc.text().slice(..);
+ match ch {
+ 'w' => textobject::textobject_word(text, range, objtype, count),
+ // TODO: cancel new ranges if inconsistent surround matches across lines
+ ch if !ch.is_ascii_alphanumeric() => {
+ textobject::textobject_surround(text, range, objtype, ch, count)
+ }
+ _ => range,
}
- _ => range,
- }
- });
-
- doc.set_selection(view.id, selection);
+ }),
+ );
}
})
}
@@ -3537,17 +3628,13 @@ fn surround_add(cx: &mut Context) {
{
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id);
+ let selection = doc.selection(view.id).clone().min_width_1(text);
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());
@@ -3573,9 +3660,9 @@ fn surround_replace(cx: &mut Context) {
{
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id);
+ let selection = doc.selection(view.id).clone().min_width_1(text);
- let change_pos = match surround::get_surround_pos(text, selection, from, count)
+ let change_pos = match surround::get_surround_pos(text, &selection, from, count)
{
Some(c) => c,
None => return,
@@ -3607,9 +3694,9 @@ fn surround_delete(cx: &mut Context) {
{
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
- let selection = doc.selection(view.id);
+ let selection = doc.selection(view.id).clone().min_width_1(text);
- let change_pos = match surround::get_surround_pos(text, selection, ch, count) {
+ let change_pos = match surround::get_surround_pos(text, &selection, ch, count) {
Some(c) => c,
None => return,
};
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index ef13004c..d374d9b6 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},
LineEnding, Position, Range,
};
@@ -140,8 +140,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,
@@ -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,24 +184,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));
}
}
@@ -237,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};
@@ -306,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(
@@ -323,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,
);
@@ -341,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)
};
@@ -350,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,
);