mirror of https://github.com/helix-editor/helix
Compare commits
6 Commits
0773531942
...
7f21e6b3ab
Author | SHA1 | Date |
---|---|---|
|
7f21e6b3ab | |
|
fed3edcab7 | |
|
4099465632 | |
|
9100bce9aa | |
|
f5dc8245ea | |
|
e02d7683c6 |
|
@ -1552,6 +1552,7 @@ dependencies = [
|
|||
"helix-event",
|
||||
"helix-loader",
|
||||
"helix-lsp",
|
||||
"helix-parsec",
|
||||
"helix-stdx",
|
||||
"helix-tui",
|
||||
"helix-vcs",
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
| `:write-buffer-close`, `:wbc` | Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt) |
|
||||
| `:write-buffer-close!`, `:wbc!` | Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt) |
|
||||
| `:new`, `:n` | Create a new scratch buffer. |
|
||||
| `:goto-mark` | Go to the selection saved in a register. Register can be provided as argument or selected register else ^ will be used |
|
||||
| `:register-mark` | Save current selection into a register. Register can be provided as argument or selected register else ^ will be used |
|
||||
| `:format`, `:fmt` | Format the file using an external formatter or language server. |
|
||||
| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-16 for number of spaces.) |
|
||||
| `:line-ending` | Set the document's default line ending. Options: crlf, lf. |
|
||||
|
|
|
@ -12,10 +12,11 @@ use crate::{
|
|||
tree_sitter::Node,
|
||||
Assoc, ChangeSet, RopeSlice,
|
||||
};
|
||||
use helix_parsec::{seq, take_until, Parser};
|
||||
use helix_stdx::range::is_subset;
|
||||
use helix_stdx::rope::{self, RopeSliceExt};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::{borrow::Cow, iter, slice};
|
||||
use std::{borrow::Cow, fmt::Display, iter, slice};
|
||||
|
||||
/// A single selection range.
|
||||
///
|
||||
|
@ -392,6 +393,34 @@ impl Range {
|
|||
}
|
||||
}
|
||||
|
||||
impl Display for Range {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "({},{})", self.anchor, self.head)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Range {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let parser = seq!(
|
||||
"(",
|
||||
take_until(|c| c == ','),
|
||||
",",
|
||||
take_until(|c| c == ')'),
|
||||
")"
|
||||
);
|
||||
match parser.parse(value) {
|
||||
Ok((_tail, (_, anchor, _, head, _))) => Ok(Self {
|
||||
anchor: anchor.parse::<usize>().map_err(|e| e.to_string())?,
|
||||
head: head.parse::<usize>().map_err(|e| e.to_string())?,
|
||||
old_visual_position: None,
|
||||
}),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(usize, usize)> for Range {
|
||||
fn from((anchor, head): (usize, usize)) -> Self {
|
||||
Self {
|
||||
|
@ -884,6 +913,54 @@ mod test {
|
|||
use super::*;
|
||||
use crate::Rope;
|
||||
|
||||
#[test]
|
||||
fn parse_range() -> Result<(), String> {
|
||||
// sometimes we want Ok, sometimes we want Err, but we never want a panic
|
||||
assert_eq!(
|
||||
Range::try_from("(0,28)"),
|
||||
Ok(Range {
|
||||
anchor: 0,
|
||||
head: 28,
|
||||
old_visual_position: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
Range::try_from("(3456789,123456789)"),
|
||||
Ok(Range {
|
||||
anchor: 3456789,
|
||||
head: 123456789,
|
||||
old_visual_position: None
|
||||
})
|
||||
);
|
||||
assert_eq!(Range::try_from("(,)"), Err("(,)".to_string()));
|
||||
assert_eq!(
|
||||
Range::try_from("(asdf,asdf)"),
|
||||
Err("invalid digit found in string".to_string())
|
||||
);
|
||||
assert_eq!(Range::try_from("()"), Err("()".to_string()));
|
||||
assert_eq!(
|
||||
Range::try_from("(-4,ALSK)"),
|
||||
Err("invalid digit found in string".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
Range::try_from("(⦡⓼␀⍆ⴉ├⺶⍄⾨,⦡⓼␀⍆ⴉ├⺶⍄⾨)"),
|
||||
Err("invalid digit found in string".to_string())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_range() {
|
||||
assert_eq!(
|
||||
Range {
|
||||
anchor: 72,
|
||||
head: 28,
|
||||
old_visual_position: None,
|
||||
}
|
||||
.to_string(),
|
||||
"(72,28)".to_string(),
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_new_empty() {
|
||||
|
|
|
@ -135,7 +135,9 @@ pub trait RopeSliceExt<'a>: Sized {
|
|||
/// let graphemes: Vec<_> = text.graphemes().collect();
|
||||
/// assert_eq!(graphemes.as_slice(), &["😶🌫️", "🏴☠️", "🖼️"]);
|
||||
/// ```
|
||||
fn graphemes(self) -> RopeGraphemes<'a>;
|
||||
fn graphemes(self) -> RopeGraphemes<'a> {
|
||||
self.graphemes_at(0)
|
||||
}
|
||||
/// Returns an iterator over the grapheme clusters in the slice, reversed.
|
||||
///
|
||||
/// The returned iterator starts at the end of the slice and ends at the beginning of the
|
||||
|
@ -150,7 +152,127 @@ pub trait RopeSliceExt<'a>: Sized {
|
|||
/// let graphemes: Vec<_> = text.graphemes_rev().collect();
|
||||
/// assert_eq!(graphemes.as_slice(), &["🖼️", "🏴☠️", "😶🌫️"]);
|
||||
/// ```
|
||||
fn graphemes_rev(self) -> RevRopeGraphemes<'a>;
|
||||
fn graphemes_rev(self) -> RopeGraphemes<'a>;
|
||||
/// Returns an iterator over the grapheme clusters in the slice at the given byte index.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ropey::Rope;
|
||||
/// # use helix_stdx::rope::RopeSliceExt;
|
||||
/// let text = Rope::from_str("😶🌫️🏴☠️🖼️");
|
||||
/// // 14 is the byte index of the pirate flag's starting cluster boundary.
|
||||
/// let graphemes: Vec<_> = text.slice(..).graphemes_at(14).collect();
|
||||
/// assert_eq!(graphemes.as_slice(), &["🏴☠️", "🖼️"]);
|
||||
/// // 27 is the byte index of the pirate flag's ending cluster boundary.
|
||||
/// let graphemes: Vec<_> = text.slice(..).graphemes_at(27).reversed().collect();
|
||||
/// assert_eq!(graphemes.as_slice(), &["🏴☠️", "😶🌫️"]);
|
||||
/// ```
|
||||
fn graphemes_at(self, byte_idx: usize) -> RopeGraphemes<'a>;
|
||||
/// Returns an iterator over the grapheme clusters in a rope and the byte index where each
|
||||
/// grapheme cluster starts.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ropey::Rope;
|
||||
/// # use helix_stdx::rope::RopeSliceExt;
|
||||
/// let text = Rope::from_str("😶🌫️🏴☠️🖼️");
|
||||
/// let slice = text.slice(..);
|
||||
/// let graphemes: Vec<_> = slice.grapheme_indices_at(0).collect();
|
||||
/// assert_eq!(
|
||||
/// graphemes.as_slice(),
|
||||
/// &[(0, "😶🌫️".into()), (14, "🏴☠️".into()), (27, "🖼️".into())]
|
||||
/// );
|
||||
/// let graphemes: Vec<_> = slice.grapheme_indices_at(slice.len_bytes()).reversed().collect();
|
||||
/// assert_eq!(
|
||||
/// graphemes.as_slice(),
|
||||
/// &[(27, "🖼️".into()), (14, "🏴☠️".into()), (0, "😶🌫️".into())]
|
||||
/// );
|
||||
/// ```
|
||||
fn grapheme_indices_at(self, byte_idx: usize) -> RopeGraphemeIndices<'a>;
|
||||
/// Finds the byte index of the next grapheme boundary after `byte_idx`.
|
||||
///
|
||||
/// If the byte index lies on the last grapheme cluster in the slice then this function
|
||||
/// returns `RopeSlice::len_bytes`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ropey::Rope;
|
||||
/// # use helix_stdx::rope::RopeSliceExt;
|
||||
/// let text = Rope::from_str("😶🌫️🏴☠️🖼️");
|
||||
/// let slice = text.slice(..);
|
||||
/// let mut byte_idx = 0;
|
||||
/// assert_eq!(slice.graphemes_at(byte_idx).next(), Some("😶🌫️".into()));
|
||||
/// byte_idx = slice.next_grapheme_boundary(byte_idx);
|
||||
/// assert_eq!(slice.graphemes_at(byte_idx).next(), Some("🏴☠️".into()));
|
||||
///
|
||||
/// // If `byte_idx` does not lie on a character or grapheme boundary then this function is
|
||||
/// // functionally the same as `ceil_grapheme_boundary`.
|
||||
/// assert_eq!(slice.next_grapheme_boundary(byte_idx - 1), byte_idx);
|
||||
/// assert_eq!(slice.next_grapheme_boundary(byte_idx - 2), byte_idx);
|
||||
/// assert_eq!(slice.next_grapheme_boundary(byte_idx + 1), slice.next_grapheme_boundary(byte_idx));
|
||||
/// assert_eq!(slice.next_grapheme_boundary(byte_idx + 2), slice.next_grapheme_boundary(byte_idx));
|
||||
///
|
||||
/// byte_idx = slice.next_grapheme_boundary(byte_idx);
|
||||
/// assert_eq!(slice.graphemes_at(byte_idx).next(), Some("🖼️".into()));
|
||||
/// byte_idx = slice.next_grapheme_boundary(byte_idx);
|
||||
/// assert_eq!(slice.graphemes_at(byte_idx).next(), None);
|
||||
/// assert_eq!(byte_idx, slice.len_bytes());
|
||||
/// ```
|
||||
fn next_grapheme_boundary(self, byte_idx: usize) -> usize {
|
||||
self.nth_next_grapheme_boundary(byte_idx, 1)
|
||||
}
|
||||
/// Finds the byte index of the `n`th grapheme cluster after the given `byte_idx`.
|
||||
///
|
||||
/// If there are fewer than `n` grapheme clusters after `byte_idx` in the rope then this
|
||||
/// function returns `RopeSlice::len_bytes`.
|
||||
///
|
||||
/// This is functionally equivalent to calling `next_grapheme_boundary` `n` times but is more
|
||||
/// efficient.
|
||||
fn nth_next_grapheme_boundary(self, byte_idx: usize, n: usize) -> usize;
|
||||
/// Finds the byte index of the previous grapheme boundary before `byte_idx`.
|
||||
///
|
||||
/// If the byte index lies on the first grapheme cluster in the slice then this function
|
||||
/// returns zero.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ropey::Rope;
|
||||
/// # use helix_stdx::rope::RopeSliceExt;
|
||||
/// let text = Rope::from_str("😶🌫️🏴☠️🖼️");
|
||||
/// let slice = text.slice(..);
|
||||
/// let mut byte_idx = text.len_bytes();
|
||||
/// assert_eq!(slice.graphemes_at(byte_idx).prev(), Some("🖼️".into()));
|
||||
/// byte_idx = slice.prev_grapheme_boundary(byte_idx);
|
||||
/// assert_eq!(slice.graphemes_at(byte_idx).prev(), Some("🏴☠️".into()));
|
||||
///
|
||||
/// // If `byte_idx` does not lie on a character or grapheme boundary then this function is
|
||||
/// // functionally the same as `floor_grapheme_boundary`.
|
||||
/// assert_eq!(slice.prev_grapheme_boundary(byte_idx + 1), byte_idx);
|
||||
/// assert_eq!(slice.prev_grapheme_boundary(byte_idx + 2), byte_idx);
|
||||
/// assert_eq!(slice.prev_grapheme_boundary(byte_idx - 1), slice.prev_grapheme_boundary(byte_idx));
|
||||
/// assert_eq!(slice.prev_grapheme_boundary(byte_idx - 2), slice.prev_grapheme_boundary(byte_idx));
|
||||
///
|
||||
/// byte_idx = slice.prev_grapheme_boundary(byte_idx);
|
||||
/// assert_eq!(slice.graphemes_at(byte_idx).prev(), Some("😶🌫️".into()));
|
||||
/// byte_idx = slice.prev_grapheme_boundary(byte_idx);
|
||||
/// assert_eq!(slice.graphemes_at(byte_idx).prev(), None);
|
||||
/// assert_eq!(byte_idx, 0);
|
||||
/// ```
|
||||
fn prev_grapheme_boundary(self, byte_idx: usize) -> usize {
|
||||
self.nth_prev_grapheme_boundary(byte_idx, 1)
|
||||
}
|
||||
/// Finds the byte index of the `n`th grapheme cluster before the given `byte_idx`.
|
||||
///
|
||||
/// If there are fewer than `n` grapheme clusters before `byte_idx` in the rope then this
|
||||
/// function returns zero.
|
||||
///
|
||||
/// This is functionally equivalent to calling `prev_grapheme_boundary` `n` times but is more
|
||||
/// efficient.
|
||||
fn nth_prev_grapheme_boundary(self, byte_idx: usize, n: usize) -> usize;
|
||||
}
|
||||
|
||||
impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
|
||||
|
@ -335,31 +457,111 @@ impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn graphemes(self) -> RopeGraphemes<'a> {
|
||||
let mut chunks = self.chunks();
|
||||
let first_chunk = chunks.next().unwrap_or("");
|
||||
fn graphemes_rev(self) -> RopeGraphemes<'a> {
|
||||
self.graphemes_at(self.len_bytes()).reversed()
|
||||
}
|
||||
|
||||
fn graphemes_at(self, byte_idx: usize) -> RopeGraphemes<'a> {
|
||||
// Bounds check
|
||||
assert!(byte_idx <= self.len_bytes());
|
||||
|
||||
let (mut chunks, chunk_byte_idx, _, _) = self.chunks_at_byte(byte_idx);
|
||||
let current_chunk = chunks.next().unwrap_or("");
|
||||
|
||||
RopeGraphemes {
|
||||
text: self,
|
||||
chunks,
|
||||
cur_chunk: first_chunk,
|
||||
cur_chunk_start: 0,
|
||||
cursor: GraphemeCursor::new(0, self.len_bytes(), true),
|
||||
current_chunk,
|
||||
chunk_byte_idx,
|
||||
cursor: GraphemeCursor::new(byte_idx, self.len_bytes(), true),
|
||||
is_reversed: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn graphemes_rev(self) -> RevRopeGraphemes<'a> {
|
||||
let (mut chunks, mut cur_chunk_start, _, _) = self.chunks_at_byte(self.len_bytes());
|
||||
chunks.reverse();
|
||||
let first_chunk = chunks.next().unwrap_or("");
|
||||
cur_chunk_start -= first_chunk.len();
|
||||
RevRopeGraphemes {
|
||||
text: self,
|
||||
chunks,
|
||||
cur_chunk: first_chunk,
|
||||
cur_chunk_start,
|
||||
cursor: GraphemeCursor::new(self.len_bytes(), self.len_bytes(), true),
|
||||
fn grapheme_indices_at(self, byte_idx: usize) -> RopeGraphemeIndices<'a> {
|
||||
// Bounds check
|
||||
assert!(byte_idx <= self.len_bytes());
|
||||
RopeGraphemeIndices {
|
||||
front_offset: byte_idx,
|
||||
iter: self.graphemes_at(byte_idx),
|
||||
is_reversed: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn nth_next_grapheme_boundary(self, mut byte_idx: usize, n: usize) -> usize {
|
||||
// Bounds check
|
||||
assert!(byte_idx <= self.len_bytes());
|
||||
|
||||
byte_idx = self.floor_char_boundary(byte_idx);
|
||||
|
||||
// Get the chunk with our byte index in it.
|
||||
let (mut chunk, mut chunk_byte_idx, _, _) = self.chunk_at_byte(byte_idx);
|
||||
|
||||
// Set up the grapheme cursor.
|
||||
let mut gc = GraphemeCursor::new(byte_idx, self.len_bytes(), true);
|
||||
|
||||
// Find the nth next grapheme cluster boundary.
|
||||
for _ in 0..n {
|
||||
loop {
|
||||
match gc.next_boundary(chunk, chunk_byte_idx) {
|
||||
Ok(None) => return self.len_bytes(),
|
||||
Ok(Some(boundary)) => {
|
||||
byte_idx = boundary;
|
||||
break;
|
||||
}
|
||||
Err(GraphemeIncomplete::NextChunk) => {
|
||||
chunk_byte_idx += chunk.len();
|
||||
let (a, _, _, _) = self.chunk_at_byte(chunk_byte_idx);
|
||||
chunk = a;
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(n)) => {
|
||||
let ctx_chunk = self.chunk_at_byte(n - 1).0;
|
||||
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
byte_idx
|
||||
}
|
||||
|
||||
fn nth_prev_grapheme_boundary(self, mut byte_idx: usize, n: usize) -> usize {
|
||||
// Bounds check
|
||||
assert!(byte_idx <= self.len_bytes());
|
||||
|
||||
byte_idx = self.ceil_char_boundary(byte_idx);
|
||||
|
||||
// Get the chunk with our byte index in it.
|
||||
let (mut chunk, mut chunk_byte_idx, _, _) = self.chunk_at_byte(byte_idx);
|
||||
|
||||
// Set up the grapheme cursor.
|
||||
let mut gc = GraphemeCursor::new(byte_idx, self.len_bytes(), true);
|
||||
|
||||
for _ in 0..n {
|
||||
loop {
|
||||
match gc.prev_boundary(chunk, chunk_byte_idx) {
|
||||
Ok(None) => return 0,
|
||||
Ok(Some(boundary)) => {
|
||||
byte_idx = boundary;
|
||||
break;
|
||||
}
|
||||
Err(GraphemeIncomplete::PrevChunk) => {
|
||||
let (a, b, _, _) = self.chunk_at_byte(chunk_byte_idx - 1);
|
||||
chunk = a;
|
||||
chunk_byte_idx = b;
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(n)) => {
|
||||
let ctx_chunk = self.chunk_at_byte(n - 1).0;
|
||||
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
byte_idx
|
||||
}
|
||||
}
|
||||
|
||||
// copied from std
|
||||
|
@ -370,13 +572,19 @@ const fn is_utf8_char_boundary(b: u8) -> bool {
|
|||
}
|
||||
|
||||
/// An iterator over the graphemes of a `RopeSlice`.
|
||||
///
|
||||
/// This iterator is cursor-like: rather than implementing DoubleEndedIterator it can be reversed
|
||||
/// like a cursor. This style matches `Bytes` and `Chars` iterator types in Ropey and is more
|
||||
/// natural and useful for wrapping `GraphemeCursor`.
|
||||
#[derive(Clone)]
|
||||
pub struct RopeGraphemes<'a> {
|
||||
text: RopeSlice<'a>,
|
||||
chunks: Chunks<'a>,
|
||||
cur_chunk: &'a str,
|
||||
cur_chunk_start: usize,
|
||||
current_chunk: &'a str,
|
||||
/// Byte index of the start of the current chunk.
|
||||
chunk_byte_idx: usize,
|
||||
cursor: GraphemeCursor,
|
||||
is_reversed: bool,
|
||||
}
|
||||
|
||||
impl fmt::Debug for RopeGraphemes<'_> {
|
||||
|
@ -384,112 +592,178 @@ impl fmt::Debug for RopeGraphemes<'_> {
|
|||
f.debug_struct("RopeGraphemes")
|
||||
.field("text", &self.text)
|
||||
.field("chunks", &self.chunks)
|
||||
.field("cur_chunk", &self.cur_chunk)
|
||||
.field("cur_chunk_start", &self.cur_chunk_start)
|
||||
.field("current_chunk", &self.current_chunk)
|
||||
.field("chunk_byte_idx", &self.chunk_byte_idx)
|
||||
// .field("cursor", &self.cursor)
|
||||
.field("is_reversed", &self.is_reversed)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RopeGraphemes<'a> {
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> Option<RopeSlice<'a>> {
|
||||
if self.is_reversed {
|
||||
self.prev_impl()
|
||||
} else {
|
||||
self.next_impl()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev(&mut self) -> Option<RopeSlice<'a>> {
|
||||
if self.is_reversed {
|
||||
self.next_impl()
|
||||
} else {
|
||||
self.prev_impl()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reverse(&mut self) {
|
||||
self.is_reversed = !self.is_reversed;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn reversed(mut self) -> Self {
|
||||
self.reverse();
|
||||
self
|
||||
}
|
||||
|
||||
fn next_impl(&mut self) -> Option<RopeSlice<'a>> {
|
||||
let a = self.cursor.cur_cursor();
|
||||
let b;
|
||||
loop {
|
||||
match self
|
||||
.cursor
|
||||
.next_boundary(self.current_chunk, self.chunk_byte_idx)
|
||||
{
|
||||
Ok(None) => return None,
|
||||
Ok(Some(boundary)) => {
|
||||
b = boundary;
|
||||
break;
|
||||
}
|
||||
Err(GraphemeIncomplete::NextChunk) => {
|
||||
self.chunk_byte_idx += self.current_chunk.len();
|
||||
self.current_chunk = self.chunks.next().unwrap_or("");
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(idx)) => {
|
||||
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
|
||||
self.cursor.provide_context(chunk, byte_idx);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if a < self.chunk_byte_idx {
|
||||
Some(self.text.byte_slice(a..b))
|
||||
} else {
|
||||
let a2 = a - self.chunk_byte_idx;
|
||||
let b2 = b - self.chunk_byte_idx;
|
||||
Some((&self.current_chunk[a2..b2]).into())
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_impl(&mut self) -> Option<RopeSlice<'a>> {
|
||||
let a = self.cursor.cur_cursor();
|
||||
let b;
|
||||
loop {
|
||||
match self
|
||||
.cursor
|
||||
.prev_boundary(self.current_chunk, self.chunk_byte_idx)
|
||||
{
|
||||
Ok(None) => return None,
|
||||
Ok(Some(boundary)) => {
|
||||
b = boundary;
|
||||
break;
|
||||
}
|
||||
Err(GraphemeIncomplete::PrevChunk) => {
|
||||
self.current_chunk = self.chunks.prev().unwrap_or("");
|
||||
self.chunk_byte_idx -= self.current_chunk.len();
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(idx)) => {
|
||||
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
|
||||
self.cursor.provide_context(chunk, byte_idx);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if a >= self.chunk_byte_idx + self.current_chunk.len() {
|
||||
Some(self.text.byte_slice(b..a))
|
||||
} else {
|
||||
let a2 = a - self.chunk_byte_idx;
|
||||
let b2 = b - self.chunk_byte_idx;
|
||||
Some((&self.current_chunk[b2..a2]).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RopeGraphemes<'a> {
|
||||
type Item = RopeSlice<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let a = self.cursor.cur_cursor();
|
||||
let b;
|
||||
loop {
|
||||
match self
|
||||
.cursor
|
||||
.next_boundary(self.cur_chunk, self.cur_chunk_start)
|
||||
{
|
||||
Ok(None) => {
|
||||
return None;
|
||||
}
|
||||
Ok(Some(n)) => {
|
||||
b = n;
|
||||
break;
|
||||
}
|
||||
Err(GraphemeIncomplete::NextChunk) => {
|
||||
self.cur_chunk_start += self.cur_chunk.len();
|
||||
self.cur_chunk = self.chunks.next().unwrap_or("");
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(idx)) => {
|
||||
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
|
||||
self.cursor.provide_context(chunk, byte_idx);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
RopeGraphemes::next(self)
|
||||
}
|
||||
}
|
||||
|
||||
if a < self.cur_chunk_start {
|
||||
Some(self.text.byte_slice(a..b))
|
||||
/// An iterator over the grapheme clusters in a rope and the byte indices where each grapheme
|
||||
/// cluster starts.
|
||||
///
|
||||
/// This iterator wraps `RopeGraphemes` and is also cursor-like. Use `reverse` or `reversed` to
|
||||
/// toggle the direction of the iterator. See [RopeGraphemes].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RopeGraphemeIndices<'a> {
|
||||
front_offset: usize,
|
||||
iter: RopeGraphemes<'a>,
|
||||
is_reversed: bool,
|
||||
}
|
||||
|
||||
impl<'a> RopeGraphemeIndices<'a> {
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> Option<(usize, RopeSlice<'a>)> {
|
||||
if self.is_reversed {
|
||||
self.prev_impl()
|
||||
} else {
|
||||
let a2 = a - self.cur_chunk_start;
|
||||
let b2 = b - self.cur_chunk_start;
|
||||
Some((&self.cur_chunk[a2..b2]).into())
|
||||
self.next_impl()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev(&mut self) -> Option<(usize, RopeSlice<'a>)> {
|
||||
if self.is_reversed {
|
||||
self.next_impl()
|
||||
} else {
|
||||
self.prev_impl()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reverse(&mut self) {
|
||||
self.is_reversed = !self.is_reversed;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn reversed(mut self) -> Self {
|
||||
self.reverse();
|
||||
self
|
||||
}
|
||||
|
||||
fn next_impl(&mut self) -> Option<(usize, RopeSlice<'a>)> {
|
||||
let slice = self.iter.next()?;
|
||||
let idx = self.front_offset;
|
||||
self.front_offset += slice.len_bytes();
|
||||
Some((idx, slice))
|
||||
}
|
||||
|
||||
fn prev_impl(&mut self) -> Option<(usize, RopeSlice<'a>)> {
|
||||
let slice = self.iter.prev()?;
|
||||
self.front_offset -= slice.len_bytes();
|
||||
Some((self.front_offset, slice))
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the graphemes of a `RopeSlice` in reverse.
|
||||
#[derive(Clone)]
|
||||
pub struct RevRopeGraphemes<'a> {
|
||||
text: RopeSlice<'a>,
|
||||
chunks: Chunks<'a>,
|
||||
cur_chunk: &'a str,
|
||||
cur_chunk_start: usize,
|
||||
cursor: GraphemeCursor,
|
||||
}
|
||||
|
||||
impl fmt::Debug for RevRopeGraphemes<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RevRopeGraphemes")
|
||||
.field("text", &self.text)
|
||||
.field("chunks", &self.chunks)
|
||||
.field("cur_chunk", &self.cur_chunk)
|
||||
.field("cur_chunk_start", &self.cur_chunk_start)
|
||||
// .field("cursor", &self.cursor)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RevRopeGraphemes<'a> {
|
||||
type Item = RopeSlice<'a>;
|
||||
impl<'a> Iterator for RopeGraphemeIndices<'a> {
|
||||
type Item = (usize, RopeSlice<'a>);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let a = self.cursor.cur_cursor();
|
||||
let b;
|
||||
loop {
|
||||
match self
|
||||
.cursor
|
||||
.prev_boundary(self.cur_chunk, self.cur_chunk_start)
|
||||
{
|
||||
Ok(None) => {
|
||||
return None;
|
||||
}
|
||||
Ok(Some(n)) => {
|
||||
b = n;
|
||||
break;
|
||||
}
|
||||
Err(GraphemeIncomplete::PrevChunk) => {
|
||||
self.cur_chunk = self.chunks.next().unwrap_or("");
|
||||
self.cur_chunk_start -= self.cur_chunk.len();
|
||||
}
|
||||
Err(GraphemeIncomplete::PreContext(idx)) => {
|
||||
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
|
||||
self.cursor.provide_context(chunk, byte_idx);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if a >= self.cur_chunk_start + self.cur_chunk.len() {
|
||||
Some(self.text.byte_slice(b..a))
|
||||
} else {
|
||||
let a2 = a - self.cur_chunk_start;
|
||||
let b2 = b - self.cur_chunk_start;
|
||||
Some((&self.cur_chunk[b2..a2]).into())
|
||||
}
|
||||
RopeGraphemeIndices::next(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,16 +49,32 @@ helix-lsp = { path = "../helix-lsp" }
|
|||
helix-dap = { path = "../helix-dap" }
|
||||
helix-vcs = { path = "../helix-vcs" }
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
helix-parsec = { path = "../helix-parsec" }
|
||||
|
||||
anyhow = "1"
|
||||
once_cell = "1.21"
|
||||
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
|
||||
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
|
||||
tokio = { version = "1", features = [
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"io-util",
|
||||
"io-std",
|
||||
"time",
|
||||
"process",
|
||||
"macros",
|
||||
"fs",
|
||||
"parking_lot",
|
||||
] }
|
||||
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = [
|
||||
"crossterm",
|
||||
] }
|
||||
crossterm = { version = "0.28", features = ["event-stream"] }
|
||||
signal-hook = "0.3"
|
||||
tokio-stream = "0.1"
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
futures-util = { version = "0.3", features = [
|
||||
"std",
|
||||
"async-await",
|
||||
], default-features = false }
|
||||
arc-swap = { version = "1.7.1" }
|
||||
termini = "1"
|
||||
indexmap = "2.9"
|
||||
|
@ -96,7 +112,11 @@ signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
|
|||
libc = "0.2.172"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc"] }
|
||||
crossterm = { version = "0.28", features = [
|
||||
"event-stream",
|
||||
"use-dev-tty",
|
||||
"libc",
|
||||
] }
|
||||
|
||||
[build-dependencies]
|
||||
helix-loader = { path = "../helix-loader" }
|
||||
|
|
|
@ -5,6 +5,7 @@ pub(crate) mod typed;
|
|||
pub use dap::*;
|
||||
use futures_util::FutureExt;
|
||||
use helix_event::status;
|
||||
use helix_parsec::{seq, take_until, Parser};
|
||||
use helix_stdx::{
|
||||
path::{self, find_paths},
|
||||
rope::{self, RopeSliceExt},
|
||||
|
@ -47,6 +48,7 @@ use helix_view::{
|
|||
info::Info,
|
||||
input::KeyEvent,
|
||||
keyboard::KeyCode,
|
||||
register::RegisterValues,
|
||||
theme::Style,
|
||||
tree,
|
||||
view::View,
|
||||
|
@ -74,6 +76,7 @@ use std::{
|
|||
future::Future,
|
||||
io::Read,
|
||||
num::NonZeroUsize,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use std::{
|
||||
|
@ -6635,6 +6638,10 @@ fn extend_to_word(cx: &mut Context) {
|
|||
jump_to_word(cx, Movement::Extend)
|
||||
}
|
||||
|
||||
fn read_from_register(editor: &mut Editor, reg: char) -> Option<RegisterValues> {
|
||||
editor.registers.read(reg, &*editor)
|
||||
}
|
||||
|
||||
fn jump_to_label(cx: &mut Context, labels: Vec<Range>, behaviour: Movement) {
|
||||
let doc = doc!(cx.editor);
|
||||
let alphabet = &cx.editor.config().jump_label_alphabet;
|
||||
|
|
|
@ -479,6 +479,155 @@ fn new_file(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> an
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn register_mark(
|
||||
cx: &mut compositor::Context,
|
||||
args: Args,
|
||||
event: PromptEvent,
|
||||
) -> anyhow::Result<()> {
|
||||
if event != PromptEvent::Validate {
|
||||
return Ok(());
|
||||
};
|
||||
let register_name: char = args
|
||||
.first()
|
||||
.map_or_else(|| cx.editor.selected_register, |s| s.chars().next())
|
||||
.unwrap_or('^');
|
||||
|
||||
let (view, doc) = current!(cx.editor);
|
||||
|
||||
let ranges_str = doc
|
||||
.selection(view.id)
|
||||
.ranges()
|
||||
.iter()
|
||||
.map(|r| r.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// we have to take because of cell
|
||||
let history = doc.history.take();
|
||||
let current_history_point = history.current_revision();
|
||||
doc.history.replace(history);
|
||||
|
||||
// doc_id so we know which doc to switch to
|
||||
// current_history_point so we can apply changes
|
||||
// to our selection when we restore it.
|
||||
// the rest of the elements are just the stringified ranges
|
||||
let mut register_val = vec![
|
||||
format!("{}", doc.id()),
|
||||
format!("{}", current_history_point),
|
||||
];
|
||||
register_val.extend(ranges_str);
|
||||
|
||||
cx.editor.registers.write(register_name, register_val)?;
|
||||
|
||||
cx.editor
|
||||
.set_status(format!("Saved selection bookmark to [{}]", register_name));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_mark_register_contents(
|
||||
registers_vals: Option<RegisterValues>,
|
||||
) -> anyhow::Result<(DocumentId, usize, Selection)> {
|
||||
match registers_vals {
|
||||
Some(rv) => {
|
||||
let mut rv_iter = rv.into_iter();
|
||||
|
||||
let Some(doc_id) = rv_iter
|
||||
.next()
|
||||
.map(|c| c.into_owned())
|
||||
.and_then(|s| s.try_into().ok())
|
||||
else {
|
||||
return Err(anyhow!("Register did not contain valid document id"));
|
||||
};
|
||||
let Some(history_rev) = rv_iter
|
||||
.next()
|
||||
.map(|c| c.into_owned())
|
||||
.and_then(|s| s.parse().ok())
|
||||
else {
|
||||
return Err(anyhow!("Register did not contain valid revision number"));
|
||||
};
|
||||
|
||||
let Ok(ranges) = rv_iter
|
||||
.map(|tup| {
|
||||
let s = tup.into_owned();
|
||||
let range_parser = seq!(
|
||||
"(",
|
||||
take_until(|c| c == ','),
|
||||
",",
|
||||
take_until(|c| c == ')'),
|
||||
")"
|
||||
);
|
||||
let Ok((_tail, (_lparen, anchor_str, _comma, head_str, _rparen))) =
|
||||
range_parser.parse(&s)
|
||||
else {
|
||||
return Err(format!("Could not parse range from string: {}", s));
|
||||
};
|
||||
|
||||
let Ok(anchor) = <usize as FromStr>::from_str(anchor_str) else {
|
||||
return Err(format!("Could not parse range from string: {}", s));
|
||||
};
|
||||
let Ok(head) = <usize as FromStr>::from_str(head_str) else {
|
||||
return Err(format!("Could not parse range from string: {}", s));
|
||||
};
|
||||
|
||||
Ok(Range {
|
||||
anchor,
|
||||
head,
|
||||
old_visual_position: None,
|
||||
})
|
||||
})
|
||||
// reverse the iterators so the first range will end up as the primary when we push them
|
||||
.rev()
|
||||
.collect::<Result<Vec<Range>, String>>()
|
||||
else {
|
||||
return Err(anyhow!("Some ranges in the register failed to parse!"));
|
||||
};
|
||||
|
||||
let mut ranges_iter = ranges.into_iter();
|
||||
|
||||
let last_range = ranges_iter.next().unwrap(); // safe since there is always at least one range
|
||||
let mut selection = Selection::from(last_range);
|
||||
for r in ranges_iter {
|
||||
selection = selection.push(r);
|
||||
}
|
||||
|
||||
Ok((doc_id, history_rev, selection))
|
||||
}
|
||||
None => Err(anyhow!("Register was empty")),
|
||||
}
|
||||
}
|
||||
|
||||
fn goto_mark(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
|
||||
if event != PromptEvent::Validate {
|
||||
return Ok(());
|
||||
};
|
||||
let register_name: char = args
|
||||
.first()
|
||||
.map_or_else(|| cx.editor.selected_register, |s| s.chars().next())
|
||||
.unwrap_or('^');
|
||||
|
||||
let scrolloff = cx.editor.config().scrolloff;
|
||||
// use some helper functions to avoid making the borrow checker angry
|
||||
let registers_vals = read_from_register(cx.editor, register_name);
|
||||
let (doc_id, history_rev, mut selection) = parse_mark_register_contents(registers_vals)?;
|
||||
|
||||
cx.editor.switch(doc_id, Action::Replace);
|
||||
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let history = doc.history.take();
|
||||
let revisions_to_apply = history.changes_since(history_rev);
|
||||
doc.history.replace(history);
|
||||
|
||||
selection = match revisions_to_apply {
|
||||
Some(t) => selection.map(t.changes()),
|
||||
None => selection,
|
||||
};
|
||||
|
||||
doc.set_selection(view.id, selection);
|
||||
|
||||
view.ensure_cursor_in_view(doc, scrolloff);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
|
||||
if event != PromptEvent::Validate {
|
||||
return Ok(());
|
||||
|
@ -2758,6 +2907,28 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
|||
..Signature::DEFAULT
|
||||
},
|
||||
},
|
||||
TypableCommand {
|
||||
name: "goto-mark",
|
||||
aliases: &[],
|
||||
doc: "Go to the selection saved in a register. Register can be provided as argument or selected register else ^ will be used",
|
||||
fun: goto_mark,
|
||||
completer: CommandCompleter::positional(&[completers::register]),
|
||||
signature: Signature {
|
||||
positionals: (0, Some(1)),
|
||||
..Signature::DEFAULT
|
||||
},
|
||||
},
|
||||
TypableCommand {
|
||||
name: "register-mark",
|
||||
aliases: &[],
|
||||
doc: "Save current selection into a register. Register can be provided as argument or selected register else ^ will be used",
|
||||
fun: register_mark,
|
||||
completer: CommandCompleter::positional(&[completers::register]),
|
||||
signature: Signature {
|
||||
positionals: (0, Some(1)),
|
||||
..Signature::DEFAULT
|
||||
},
|
||||
},
|
||||
TypableCommand {
|
||||
name: "format",
|
||||
aliases: &["fmt"],
|
||||
|
|
|
@ -336,6 +336,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
|||
|
||||
"C-a" => increment,
|
||||
"C-x" => decrement,
|
||||
|
||||
});
|
||||
let mut select = normal.clone();
|
||||
select.merge_nodes(keymap!({ "Select mode"
|
||||
|
|
|
@ -65,6 +65,43 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn bookmark() -> anyhow::Result<()> {
|
||||
// add a mark and then immediately paste it out
|
||||
test((
|
||||
indoc! {"\
|
||||
#[|Lorem]#
|
||||
ipsum
|
||||
#(|Lorem)#
|
||||
ipsum
|
||||
#(|Lorem)#
|
||||
ipsum
|
||||
#(|Lorem)#
|
||||
ipsum
|
||||
#(|Lorem)#
|
||||
ipsum"
|
||||
},
|
||||
// make a mark, make changes to the doc, colapse selection by going to end of doc
|
||||
// then resore mark and see the selection is still good
|
||||
":register-mark<space>1<ret>casdf<esc>ge:goto-mark<space>1<ret>",
|
||||
indoc! {"\
|
||||
#[|asdf]#
|
||||
ipsum
|
||||
#(|asdf)#
|
||||
ipsum
|
||||
#(|asdf)#
|
||||
ipsum
|
||||
#(|asdf)#
|
||||
ipsum
|
||||
#(|asdf)#
|
||||
ipsum"
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn surround_by_character() -> anyhow::Result<()> {
|
||||
// Only pairs matching the passed character count
|
||||
|
|
|
@ -19,7 +19,7 @@ pub mod theme;
|
|||
pub mod tree;
|
||||
pub mod view;
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::num::{NonZeroUsize, ParseIntError};
|
||||
|
||||
// uses NonZeroUsize so Option<DocumentId> use a byte rather than two
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
|
@ -32,6 +32,21 @@ impl Default for DocumentId {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for DocumentId {
|
||||
type Error = ParseIntError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
Ok(Self(value.parse::<NonZeroUsize>()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for DocumentId {
|
||||
type Error = ParseIntError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
Ok(Self(value.parse::<NonZeroUsize>()?))
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for DocumentId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}", self.0))
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
] @punctuation
|
||||
|
||||
(string_value) @string
|
||||
((color_value) "#") @string.special
|
||||
(color_value "#" @string.special)
|
||||
(color_value) @string.special
|
||||
|
||||
(integer_value) @constant.numeric.integer
|
||||
|
|
Loading…
Reference in New Issue