work on linewise logic
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
use std::{ops::{Range, RangeBounds, RangeInclusive}, string::Drain};
|
use std::{ops::{Range, RangeBounds, RangeInclusive}, string::Drain};
|
||||||
|
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use super::{term::Layout, vicmd::{Direction, Motion, MotionBehavior, MotionCmd, RegisterName, To, Verb, ViCmd, Word}};
|
use super::{term::Layout, vicmd::{Anchor, Dest, Direction, Motion, MotionBehavior, MotionCmd, RegisterName, To, Verb, ViCmd, Word}};
|
||||||
use crate::{libsh::error::ShResult, prelude::*};
|
use crate::{libsh::error::ShResult, prelude::*};
|
||||||
|
|
||||||
#[derive(PartialEq,Eq,Debug,Clone,Copy)]
|
#[derive(PartialEq,Eq,Debug,Clone,Copy)]
|
||||||
@@ -90,6 +91,10 @@ pub enum MotionKind {
|
|||||||
Onto(usize), // Absolute position, operations include the position but motions exclude it (wtf vim)
|
Onto(usize), // Absolute position, operations include the position but motions exclude it (wtf vim)
|
||||||
Inclusive((usize,usize)), // Range, inclusive
|
Inclusive((usize,usize)), // Range, inclusive
|
||||||
Exclusive((usize,usize)), // Range, exclusive
|
Exclusive((usize,usize)), // Range, exclusive
|
||||||
|
|
||||||
|
// Used for linewise operations like 'dj', left is the selected range, right is the cursor's new position
|
||||||
|
InclusiveWithTarget((usize,usize),usize),
|
||||||
|
ExclusiveWithTarget((usize,usize),usize),
|
||||||
Null
|
Null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,10 +343,20 @@ impl LineBuf {
|
|||||||
})?;
|
})?;
|
||||||
self.buffer.get(start_index..end_index)
|
self.buffer.get(start_index..end_index)
|
||||||
}
|
}
|
||||||
|
pub fn slice_inclusive(&mut self, range: RangeInclusive<usize>) -> Option<&str> {
|
||||||
|
self.update_graphemes_lazy();
|
||||||
|
let start_index = self.grapheme_indices().get(*range.start()).copied()?;
|
||||||
|
let end_index = self.grapheme_indices().get(*range.end()).copied().or_else(|| {
|
||||||
|
if *range.end() == self.grapheme_indices().len() {
|
||||||
|
Some(self.buffer.len())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
self.buffer.get(start_index..end_index)
|
||||||
|
}
|
||||||
pub fn slice_to(&mut self, end: usize) -> Option<&str> {
|
pub fn slice_to(&mut self, end: usize) -> Option<&str> {
|
||||||
self.update_graphemes_lazy();
|
self.update_graphemes_lazy();
|
||||||
flog!(DEBUG,end);
|
|
||||||
flog!(DEBUG,self.grapheme_indices().len());
|
|
||||||
let grapheme_index = self.grapheme_indices().get(end).copied().or_else(|| {
|
let grapheme_index = self.grapheme_indices().get(end).copied().or_else(|| {
|
||||||
if end == self.grapheme_indices().len() {
|
if end == self.grapheme_indices().len() {
|
||||||
Some(self.buffer.len())
|
Some(self.buffer.len())
|
||||||
@@ -364,6 +379,9 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
pub fn drain(&mut self, start: usize, end: usize) -> String {
|
pub fn drain(&mut self, start: usize, end: usize) -> String {
|
||||||
let drained = if end == self.grapheme_indices().len() {
|
let drained = if end == self.grapheme_indices().len() {
|
||||||
|
if start == self.grapheme_indices().len() {
|
||||||
|
return String::new()
|
||||||
|
}
|
||||||
let start = self.grapheme_indices()[start];
|
let start = self.grapheme_indices()[start];
|
||||||
self.buffer.drain(start..).collect()
|
self.buffer.drain(start..).collect()
|
||||||
} else {
|
} else {
|
||||||
@@ -383,8 +401,11 @@ impl LineBuf {
|
|||||||
self.update_graphemes();
|
self.update_graphemes();
|
||||||
}
|
}
|
||||||
pub fn insert_at_cursor(&mut self, ch: char) {
|
pub fn insert_at_cursor(&mut self, ch: char) {
|
||||||
let cursor_pos = self.cursor_byte_pos();
|
self.insert_at(self.cursor.get(), ch);
|
||||||
self.buffer.insert(cursor_pos, ch);
|
}
|
||||||
|
pub fn insert_at(&mut self, pos: usize, ch: char) {
|
||||||
|
let pos = self.index_byte_pos(pos);
|
||||||
|
self.buffer.insert(pos, ch);
|
||||||
self.update_graphemes();
|
self.update_graphemes();
|
||||||
}
|
}
|
||||||
pub fn set_buffer(&mut self, buffer: String) {
|
pub fn set_buffer(&mut self, buffer: String) {
|
||||||
@@ -407,55 +428,48 @@ impl LineBuf {
|
|||||||
self.last_selection = self.select_range.take();
|
self.last_selection = self.select_range.take();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rfind_newlines(&mut self, n: usize) -> usize {
|
pub fn rfind_newlines(&mut self, n: usize) -> (usize,bool) {
|
||||||
self.rfind_newlines_from(self.cursor.get(), n)
|
self.rfind_newlines_from(self.cursor.get(), n)
|
||||||
}
|
}
|
||||||
pub fn find_newlines(&mut self, n: usize) -> usize {
|
pub fn find_newlines(&mut self, n: usize) -> (usize,bool) {
|
||||||
self.find_newlines_from(self.cursor.get(), n)
|
self.find_newlines_from(self.cursor.get(), n)
|
||||||
}
|
}
|
||||||
pub fn rfind_newlines_from(&mut self, start_pos: usize, n: usize) -> usize {
|
pub fn find_newlines_in_direction(&mut self, start_pos: usize, n: usize, dir: Direction) -> (usize, bool) {
|
||||||
let Some(slice) = self.slice_to(start_pos) else {
|
if n == 0 {
|
||||||
return 0
|
return (start_pos,true)
|
||||||
|
}
|
||||||
|
let mut indices_iter = self.directional_indices_iter_from(start_pos, dir);
|
||||||
|
let default = match dir {
|
||||||
|
Direction::Backward => 0,
|
||||||
|
Direction::Forward => self.cursor.max
|
||||||
};
|
};
|
||||||
|
let mut result;
|
||||||
let mut offset = slice.len();
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
for (i, b) in slice.bytes().rev().enumerate() {
|
// Special case: newline at start_pos
|
||||||
if b == b'\n' {
|
if self.grapheme_at(start_pos) == Some("\n") {
|
||||||
|
count += 1;
|
||||||
|
indices_iter.next();
|
||||||
|
if n == 1 {
|
||||||
|
return (start_pos,true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(i) = indices_iter.find(|i| self.grapheme_at(*i) == Some("\n")) {
|
||||||
|
result = i;
|
||||||
count += 1;
|
count += 1;
|
||||||
if count == n {
|
if count == n {
|
||||||
offset = slice.len() - i - 1;
|
return (result, true);
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let byte_pos = if count == n {
|
(default, false)
|
||||||
offset // move to *after* the newline
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
self.find_index_for(byte_pos).unwrap_or(0)
|
|
||||||
}
|
}
|
||||||
pub fn find_newlines_from(&mut self, start_pos: usize, n: usize) -> usize {
|
pub fn rfind_newlines_from(&mut self, start_pos: usize, n: usize) -> (usize, bool) {
|
||||||
let Some(slice) = self.slice_from(start_pos) else {
|
self.find_newlines_in_direction(start_pos, n, Direction::Backward)
|
||||||
return self.cursor.max
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut count = 0;
|
|
||||||
for (i, b) in slice.bytes().enumerate() {
|
|
||||||
if b == b'\n' {
|
|
||||||
count += 1;
|
|
||||||
if count == n {
|
|
||||||
let byte_pos = self.index_byte_pos(start_pos) + i;
|
|
||||||
return self.find_index_for(byte_pos).unwrap_or(self.cursor.max);
|
|
||||||
}
|
}
|
||||||
}
|
pub fn find_newlines_from(&mut self, start_pos: usize, n: usize) -> (usize, bool) {
|
||||||
}
|
self.find_newlines_in_direction(start_pos, n, Direction::Forward)
|
||||||
|
|
||||||
self.cursor.max
|
|
||||||
}
|
}
|
||||||
pub fn find_index_for(&self, byte_pos: usize) -> Option<usize> {
|
pub fn find_index_for(&self, byte_pos: usize) -> Option<usize> {
|
||||||
self.grapheme_indices()
|
self.grapheme_indices()
|
||||||
@@ -463,14 +477,14 @@ impl LineBuf {
|
|||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
pub fn start_of_cursor_line(&mut self) -> usize {
|
pub fn start_of_cursor_line(&mut self) -> usize {
|
||||||
let mut pos = self.rfind_newlines(1);
|
let (mut pos,_) = self.rfind_newlines(1);
|
||||||
if pos != 0 {
|
if self.grapheme_at(pos) == Some("\n") || pos != 0 {
|
||||||
pos += 1; // Don't include the newline itself
|
pos += 1; // Don't include the newline itself
|
||||||
}
|
}
|
||||||
pos
|
pos
|
||||||
}
|
}
|
||||||
pub fn end_of_cursor_line(&mut self) -> usize {
|
pub fn end_of_cursor_line(&mut self) -> usize {
|
||||||
self.find_newlines(1)
|
self.find_newlines(1).0
|
||||||
}
|
}
|
||||||
pub fn this_line(&mut self) -> (usize,usize) {
|
pub fn this_line(&mut self) -> (usize,usize) {
|
||||||
(
|
(
|
||||||
@@ -478,36 +492,89 @@ impl LineBuf {
|
|||||||
self.end_of_cursor_line()
|
self.end_of_cursor_line()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pub fn prev_line(&mut self) -> Option<(usize,usize)> {
|
pub fn nth_prev_line(&mut self, n: usize) -> Option<(usize,usize)> {
|
||||||
if self.start_of_cursor_line() == 0 {
|
if self.start_of_cursor_line() == 0 {
|
||||||
return None
|
return None
|
||||||
}
|
}
|
||||||
let mut start = self.rfind_newlines(2);
|
|
||||||
if start != 0 {
|
let (start,_) = self.select_lines_up(n);
|
||||||
start += 1;
|
let slice = self.slice_from_cursor()?;
|
||||||
}
|
let end = slice.find('\n').unwrap_or(self.cursor.max);
|
||||||
let end = self.find_newlines_from(start, 1);
|
|
||||||
Some((start,end))
|
Some((start,end))
|
||||||
}
|
}
|
||||||
pub fn next_line(&mut self) -> Option<(usize,usize)> {
|
pub fn nth_next_line(&mut self, n: usize) -> Option<(usize, usize)> {
|
||||||
if self.end_of_cursor_line() == self.cursor.max {
|
if self.end_of_cursor_line() == self.cursor.max {
|
||||||
return None;
|
return None
|
||||||
}
|
}
|
||||||
let end = self.find_newlines(2);
|
|
||||||
let start = self.rfind_newlines_from(end, 1) + 1;
|
let (_,end) = self.select_lines_down(n);
|
||||||
|
let end_clamped = ClampedUsize::new(end, self.cursor.max, /*exclusive:*/ true);
|
||||||
|
let slice = self.slice_to(end_clamped.get())?;
|
||||||
|
let start = slice.rfind('\n').unwrap_or(0);
|
||||||
Some((start,end))
|
Some((start,end))
|
||||||
}
|
}
|
||||||
pub fn select_lines_backward(&mut self, n: usize) -> (usize,usize) {
|
/// Include the leading newline, if any
|
||||||
let mut start = self.rfind_newlines(n);
|
pub fn prev_line_with_leading_newline(&mut self) -> Option<(usize,usize)> {
|
||||||
if start != 0 {
|
let (mut start,end) = self.nth_prev_line(1)?;
|
||||||
start += 1;
|
start = start.saturating_sub(1);
|
||||||
|
Some((start,end))
|
||||||
|
}
|
||||||
|
/// Include the trailing newline, if any
|
||||||
|
pub fn prev_line_with_trailing_newline(&mut self) -> Option<(usize,usize)> {
|
||||||
|
let (start,mut end) = self.nth_prev_line(1)?;
|
||||||
|
end = (end + 1).min(self.cursor.max);
|
||||||
|
Some((start,end))
|
||||||
|
}
|
||||||
|
/// Include the leading newline, if any
|
||||||
|
pub fn next_line_with_leading_newline(&mut self) -> Option<(usize,usize)> {
|
||||||
|
let (mut start,end) = self.nth_next_line(1)?;
|
||||||
|
start = start.saturating_sub(1);
|
||||||
|
Some((start,end))
|
||||||
|
}
|
||||||
|
/// Include the trailing newline, if any
|
||||||
|
pub fn next_line_with_trailing_newline(&mut self) -> Option<(usize,usize)> {
|
||||||
|
let (start,mut end) = self.nth_next_line(1)?;
|
||||||
|
end = (end + 1).min(self.cursor.max);
|
||||||
|
Some((start,end))
|
||||||
|
}
|
||||||
|
pub fn select_lines_up(&mut self, n: usize) -> (usize,usize) {
|
||||||
|
let (mut start,end) = self.this_line();
|
||||||
|
if start == 0 {
|
||||||
|
return (start,end)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..n {
|
||||||
|
let slice = self.slice_to(start - 1).unwrap();
|
||||||
|
if let Some(prev_nl) = slice.rfind('\n') {
|
||||||
|
start = self.find_index_for(prev_nl).unwrap();
|
||||||
|
} else {
|
||||||
|
start = 0;
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let end = self.end_of_cursor_line();
|
|
||||||
(start,end)
|
(start,end)
|
||||||
}
|
}
|
||||||
pub fn select_lines_forward(&mut self, n: usize) -> (usize,usize) {
|
pub fn select_lines_down(&mut self, n: usize) -> (usize,usize) {
|
||||||
let start = self.start_of_cursor_line();
|
let (start,mut end) = self.this_line();
|
||||||
let end = self.find_newlines(n);
|
if end == self.cursor.max {
|
||||||
|
return (start,end)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..=n {
|
||||||
|
let next_ln_start = end + 1;
|
||||||
|
if next_ln_start >= self.cursor.max {
|
||||||
|
end = self.cursor.max;
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let slice = self.slice_from(next_ln_start).unwrap();
|
||||||
|
if let Some(next_nl) = slice.find('\n') {
|
||||||
|
end = self.find_index_for(next_nl).unwrap();
|
||||||
|
} else {
|
||||||
|
end = self.cursor.max;
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(start,end)
|
(start,end)
|
||||||
}
|
}
|
||||||
pub fn handle_edit(&mut self, old: String, new: String, curs_pos: usize) {
|
pub fn handle_edit(&mut self, old: String, new: String, curs_pos: usize) {
|
||||||
@@ -587,6 +654,8 @@ impl LineBuf {
|
|||||||
///
|
///
|
||||||
/// Finding the start of a word in the forward direction, and finding the end of a word in the backward direction
|
/// Finding the start of a word in the forward direction, and finding the end of a word in the backward direction
|
||||||
/// are logically the same operation, if you use a reversed iterator for the backward motion.
|
/// are logically the same operation, if you use a reversed iterator for the backward motion.
|
||||||
|
///
|
||||||
|
/// Tied with 'end_of_word_forward_or_start_of_word_backward_from()' for the longest method name I have ever written
|
||||||
pub fn start_of_word_forward_or_end_of_word_backward_from(&mut self, mut pos: usize, word: Word, dir: Direction) -> usize {
|
pub fn start_of_word_forward_or_end_of_word_backward_from(&mut self, mut pos: usize, word: Word, dir: Direction) -> usize {
|
||||||
let default = match dir {
|
let default = match dir {
|
||||||
Direction::Backward => 0,
|
Direction::Backward => 0,
|
||||||
@@ -600,20 +669,16 @@ impl LineBuf {
|
|||||||
return default
|
return default
|
||||||
};
|
};
|
||||||
let on_boundary = self.grapheme_at(*next).is_none_or(is_whitespace);
|
let on_boundary = self.grapheme_at(*next).is_none_or(is_whitespace);
|
||||||
flog!(DEBUG,on_boundary);
|
|
||||||
flog!(DEBUG,pos);
|
|
||||||
if on_boundary {
|
if on_boundary {
|
||||||
let Some(idx) = indices_iter.next() else { return default };
|
let Some(idx) = indices_iter.next() else { return default };
|
||||||
pos = idx;
|
pos = idx;
|
||||||
}
|
}
|
||||||
flog!(DEBUG,pos);
|
|
||||||
|
|
||||||
// Check current grapheme
|
// Check current grapheme
|
||||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||||
return default
|
return default
|
||||||
};
|
};
|
||||||
let on_whitespace = is_whitespace(&cur_char);
|
let on_whitespace = is_whitespace(&cur_char);
|
||||||
flog!(DEBUG,on_whitespace);
|
|
||||||
|
|
||||||
// Find the next whitespace
|
// Find the next whitespace
|
||||||
if !on_whitespace {
|
if !on_whitespace {
|
||||||
@@ -630,7 +695,6 @@ impl LineBuf {
|
|||||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default };
|
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default };
|
||||||
let Some(next_idx) = indices_iter.peek() else { return default };
|
let Some(next_idx) = indices_iter.peek() else { return default };
|
||||||
let on_boundary = !is_whitespace(&cur_char) && self.grapheme_at(*next_idx).is_none_or(|c| is_other_class_or_is_ws(c, &cur_char));
|
let on_boundary = !is_whitespace(&cur_char) && self.grapheme_at(*next_idx).is_none_or(|c| is_other_class_or_is_ws(c, &cur_char));
|
||||||
flog!(DEBUG,on_boundary);
|
|
||||||
if on_boundary {
|
if on_boundary {
|
||||||
pos = *next_idx
|
pos = *next_idx
|
||||||
}
|
}
|
||||||
@@ -639,7 +703,6 @@ impl LineBuf {
|
|||||||
return default
|
return default
|
||||||
};
|
};
|
||||||
let on_whitespace = is_whitespace(&cur_char);
|
let on_whitespace = is_whitespace(&cur_char);
|
||||||
flog!(DEBUG,on_whitespace);
|
|
||||||
|
|
||||||
// Advance until hitting whitespace or a different character class
|
// Advance until hitting whitespace or a different character class
|
||||||
if !on_whitespace {
|
if !on_whitespace {
|
||||||
@@ -665,7 +728,6 @@ impl LineBuf {
|
|||||||
.is_some_and(|c| !is_whitespace(c))
|
.is_some_and(|c| !is_whitespace(c))
|
||||||
}
|
}
|
||||||
).unwrap_or(default);
|
).unwrap_or(default);
|
||||||
flog!(DEBUG,self.grapheme_at(non_ws_pos));
|
|
||||||
non_ws_pos
|
non_ws_pos
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -765,6 +827,30 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn grapheme_index_for_display_col(&self, line: &str, target_col: usize) -> usize {
|
||||||
|
let mut col = 0;
|
||||||
|
for (grapheme_index, g) in line.graphemes(true).enumerate() {
|
||||||
|
let w = g.width();
|
||||||
|
if col + w > target_col {
|
||||||
|
return grapheme_index;
|
||||||
|
}
|
||||||
|
col += w;
|
||||||
|
}
|
||||||
|
// If we reach here, the target_col is past end of line
|
||||||
|
line.graphemes(true).count()
|
||||||
|
}
|
||||||
|
pub fn cursor_col(&mut self) -> usize {
|
||||||
|
let start = self.start_of_cursor_line();
|
||||||
|
let end = self.cursor.get();
|
||||||
|
let Some(slice) = self.slice_inclusive(start..=end) else {
|
||||||
|
return start
|
||||||
|
};
|
||||||
|
|
||||||
|
slice
|
||||||
|
.graphemes(true)
|
||||||
|
.map(|g| g.width())
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
pub fn rfind_from<F: Fn(&str) -> bool>(&mut self, pos: usize, op: F) -> usize {
|
pub fn rfind_from<F: Fn(&str) -> bool>(&mut self, pos: usize, op: F) -> usize {
|
||||||
let Some(slice) = self.slice_to(pos) else {
|
let Some(slice) = self.slice_to(pos) else {
|
||||||
return self.grapheme_indices().len()
|
return self.grapheme_indices().len()
|
||||||
@@ -827,12 +913,12 @@ impl LineBuf {
|
|||||||
let eval = match motion {
|
let eval = match motion {
|
||||||
MotionCmd(count,Motion::WholeLine) => {
|
MotionCmd(count,Motion::WholeLine) => {
|
||||||
let start = self.start_of_cursor_line();
|
let start = self.start_of_cursor_line();
|
||||||
let end = self.find_newlines(count);
|
let end = self.find_newlines(count).0;
|
||||||
MotionKind::Inclusive((start,end))
|
MotionKind::Inclusive((start,end))
|
||||||
}
|
}
|
||||||
MotionCmd(count,Motion::WordMotion(to, word, dir)) => {
|
MotionCmd(count,Motion::WordMotion(to, word, dir)) => {
|
||||||
let pos = self.dispatch_word_motion(count, to, word, dir);
|
let pos = self.dispatch_word_motion(count, to, word, dir);
|
||||||
let mut pos = ClampedUsize::new(pos,self.cursor.max,false);
|
let pos = ClampedUsize::new(pos,self.cursor.max,false);
|
||||||
// End-based operations must include the last character
|
// End-based operations must include the last character
|
||||||
// But the cursor must also stop just before it when moving
|
// But the cursor must also stop just before it when moving
|
||||||
// So we have to do some weird shit to reconcile this behavior
|
// So we have to do some weird shit to reconcile this behavior
|
||||||
@@ -880,7 +966,6 @@ impl LineBuf {
|
|||||||
while let Some(idx) = indices.next() {
|
while let Some(idx) = indices.next() {
|
||||||
let grapheme = self.grapheme_at(idx).unwrap();
|
let grapheme = self.grapheme_at(idx).unwrap();
|
||||||
if !is_whitespace(grapheme) {
|
if !is_whitespace(grapheme) {
|
||||||
flog!(DEBUG,grapheme);
|
|
||||||
first_graphical = Some(idx);
|
first_graphical = Some(idx);
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -895,18 +980,98 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
MotionCmd(_,Motion::BeginningOfLine) => MotionKind::On(self.start_of_cursor_line()),
|
MotionCmd(_,Motion::BeginningOfLine) => MotionKind::On(self.start_of_cursor_line()),
|
||||||
MotionCmd(count,Motion::EndOfLine) => {
|
MotionCmd(count,Motion::EndOfLine) => {
|
||||||
let pos = self.find_newlines(count);
|
let pos = self.find_newlines(count).0;
|
||||||
MotionKind::On(pos)
|
MotionKind::On(pos)
|
||||||
}
|
}
|
||||||
MotionCmd(count,Motion::CharSearch(direction, dest, ch)) => todo!(),
|
MotionCmd(count,Motion::CharSearch(direction, dest, ch)) => {
|
||||||
|
let ch_str = &format!("{ch}");
|
||||||
|
let mut pos = self.cursor;
|
||||||
|
for _ in 0..count {
|
||||||
|
let mut indices_iter = self.directional_indices_iter_from(pos.get(), direction);
|
||||||
|
|
||||||
|
let Some(ch_pos) = indices_iter.position(|i| {
|
||||||
|
self.grapheme_at(i) == Some(ch_str)
|
||||||
|
}) else {
|
||||||
|
return MotionKind::Null
|
||||||
|
};
|
||||||
|
match direction {
|
||||||
|
Direction::Forward => pos.add(ch_pos + 1),
|
||||||
|
Direction::Backward => pos.sub(ch_pos.saturating_sub(1)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if dest == Dest::Before {
|
||||||
|
match direction {
|
||||||
|
Direction::Forward => pos.sub(1),
|
||||||
|
Direction::Backward => pos.add(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MotionKind::Onto(pos.get())
|
||||||
|
}
|
||||||
MotionCmd(count,Motion::BackwardChar) => MotionKind::On(self.cursor.ret_sub(1)),
|
MotionCmd(count,Motion::BackwardChar) => MotionKind::On(self.cursor.ret_sub(1)),
|
||||||
MotionCmd(count,Motion::ForwardChar) => MotionKind::On(self.cursor.ret_add_inclusive(1)),
|
MotionCmd(count,Motion::ForwardChar) => MotionKind::On(self.cursor.ret_add_inclusive(1)),
|
||||||
MotionCmd(count,Motion::LineUp) => todo!(),
|
MotionCmd(count,Motion::LineDown) |
|
||||||
MotionCmd(count,Motion::LineUpCharwise) => todo!(),
|
MotionCmd(count,Motion::LineUp) => {
|
||||||
|
let Some((start,end)) = (match motion.1 {
|
||||||
|
Motion::LineUp => self.nth_prev_line(1),
|
||||||
|
Motion::LineDown => self.nth_next_line(1),
|
||||||
|
_ => unreachable!()
|
||||||
|
}) else {
|
||||||
|
flog!(WARN, "failed to find target line");
|
||||||
|
return MotionKind::Null
|
||||||
|
};
|
||||||
|
flog!(DEBUG, self.slice(start..end));
|
||||||
|
|
||||||
|
let target_col = if let Some(col) = self.saved_col {
|
||||||
|
col
|
||||||
|
} else {
|
||||||
|
let col = self.cursor_col();
|
||||||
|
self.saved_col = Some(col);
|
||||||
|
col
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
|
||||||
|
return MotionKind::Null
|
||||||
|
};
|
||||||
|
let target_pos = start + self.grapheme_index_for_display_col(&line, target_col);
|
||||||
|
|
||||||
|
let (start,end) = match motion.1 {
|
||||||
|
Motion::LineUp => (start,self.end_of_cursor_line()),
|
||||||
|
Motion::LineDown => (self.start_of_cursor_line(),end),
|
||||||
|
_ => unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
MotionKind::InclusiveWithTarget((start,end),target_pos)
|
||||||
|
}
|
||||||
|
MotionCmd(count,Motion::LineDownCharwise) |
|
||||||
|
MotionCmd(count,Motion::LineUpCharwise) => {
|
||||||
|
let Some((start,end)) = (match motion.1 {
|
||||||
|
Motion::LineUpCharwise => self.nth_prev_line(1),
|
||||||
|
Motion::LineDownCharwise => self.nth_next_line(1),
|
||||||
|
_ => unreachable!()
|
||||||
|
}) else {
|
||||||
|
return MotionKind::Null
|
||||||
|
};
|
||||||
|
flog!(DEBUG,start,end);
|
||||||
|
flog!(DEBUG, self.slice(start..end));
|
||||||
|
|
||||||
|
let target_col = if let Some(col) = self.saved_col {
|
||||||
|
col
|
||||||
|
} else {
|
||||||
|
let col = self.cursor_col();
|
||||||
|
self.saved_col = Some(col);
|
||||||
|
col
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
|
||||||
|
return MotionKind::Null
|
||||||
|
};
|
||||||
|
let target_pos = start + self.grapheme_index_for_display_col(&line, target_col);
|
||||||
|
|
||||||
|
MotionKind::On(target_pos)
|
||||||
|
}
|
||||||
MotionCmd(count,Motion::ScreenLineUp) => todo!(),
|
MotionCmd(count,Motion::ScreenLineUp) => todo!(),
|
||||||
MotionCmd(count,Motion::ScreenLineUpCharwise) => todo!(),
|
MotionCmd(count,Motion::ScreenLineUpCharwise) => todo!(),
|
||||||
MotionCmd(count,Motion::LineDown) => todo!(),
|
|
||||||
MotionCmd(count,Motion::LineDownCharwise) => todo!(),
|
|
||||||
MotionCmd(count,Motion::ScreenLineDown) => todo!(),
|
MotionCmd(count,Motion::ScreenLineDown) => todo!(),
|
||||||
MotionCmd(count,Motion::ScreenLineDownCharwise) => todo!(),
|
MotionCmd(count,Motion::ScreenLineDownCharwise) => todo!(),
|
||||||
MotionCmd(count,Motion::BeginningOfScreenLine) => todo!(),
|
MotionCmd(count,Motion::BeginningOfScreenLine) => todo!(),
|
||||||
@@ -984,7 +1149,9 @@ impl LineBuf {
|
|||||||
std::cmp::Ordering::Equal => { /* Do nothing */ }
|
std::cmp::Ordering::Equal => { /* Do nothing */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
MotionKind::InclusiveWithTarget((_,_),start) |
|
||||||
MotionKind::Inclusive((start,_)) |
|
MotionKind::Inclusive((start,_)) |
|
||||||
|
MotionKind::ExclusiveWithTarget((_,_),start) |
|
||||||
MotionKind::Exclusive((start,_)) => {
|
MotionKind::Exclusive((start,_)) => {
|
||||||
self.cursor.set(start)
|
self.cursor.set(start)
|
||||||
}
|
}
|
||||||
@@ -1016,11 +1183,13 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
ordered(self.cursor.get(), pos)
|
ordered(self.cursor.get(), pos)
|
||||||
}
|
}
|
||||||
|
MotionKind::InclusiveWithTarget((start,end),_) |
|
||||||
MotionKind::Inclusive((start,end)) => {
|
MotionKind::Inclusive((start,end)) => {
|
||||||
let (start, mut end) = ordered(*start, *end);
|
let (start, mut end) = ordered(*start, *end);
|
||||||
end = ClampedUsize::new(end, self.cursor.max, false).ret_add(1);
|
end = ClampedUsize::new(end, self.cursor.max, false).ret_add(1);
|
||||||
(start,end)
|
(start,end)
|
||||||
}
|
}
|
||||||
|
MotionKind::ExclusiveWithTarget((start,end),_) |
|
||||||
MotionKind::Exclusive((start,end)) => ordered(*start, *end),
|
MotionKind::Exclusive((start,end)) => ordered(*start, *end),
|
||||||
MotionKind::Null => return None
|
MotionKind::Null => return None
|
||||||
};
|
};
|
||||||
@@ -1034,7 +1203,6 @@ impl LineBuf {
|
|||||||
let Some((start,end)) = self.range_from_motion(&motion) else {
|
let Some((start,end)) = self.range_from_motion(&motion) else {
|
||||||
return Ok(())
|
return Ok(())
|
||||||
};
|
};
|
||||||
flog!(DEBUG,start,end);
|
|
||||||
let register_text = if verb == Verb::Yank {
|
let register_text = if verb == Verb::Yank {
|
||||||
self.slice(start..end)
|
self.slice(start..end)
|
||||||
.map(|c| c.to_string())
|
.map(|c| c.to_string())
|
||||||
@@ -1043,19 +1211,19 @@ impl LineBuf {
|
|||||||
self.drain(start, end)
|
self.drain(start, end)
|
||||||
};
|
};
|
||||||
register.write_to_register(register_text);
|
register.write_to_register(register_text);
|
||||||
self.cursor.set(start);
|
match motion {
|
||||||
|
MotionKind::ExclusiveWithTarget((_,_),pos) |
|
||||||
|
MotionKind::InclusiveWithTarget((_,_),pos) => self.cursor.set(pos),
|
||||||
|
_ => self.cursor.set(start),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Verb::Rot13 => {
|
Verb::Rot13 => {
|
||||||
flog!(DEBUG,motion);
|
|
||||||
let Some((start,end)) = self.range_from_motion(&motion) else {
|
let Some((start,end)) = self.range_from_motion(&motion) else {
|
||||||
return Ok(())
|
return Ok(())
|
||||||
};
|
};
|
||||||
flog!(DEBUG,start,end);
|
|
||||||
let slice = self.slice(start..end)
|
let slice = self.slice(start..end)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
flog!(DEBUG,slice);
|
|
||||||
let rot13 = rot13(slice);
|
let rot13 = rot13(slice);
|
||||||
flog!(DEBUG,rot13);
|
|
||||||
self.buffer.replace_range(start..end, &rot13);
|
self.buffer.replace_range(start..end, &rot13);
|
||||||
self.cursor.set(start);
|
self.cursor.set(start);
|
||||||
}
|
}
|
||||||
@@ -1090,7 +1258,15 @@ impl LineBuf {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Verb::InsertModeLineBreak(anchor) => todo!(),
|
Verb::InsertModeLineBreak(anchor) => {
|
||||||
|
let end = self.end_of_cursor_line();
|
||||||
|
self.insert_at(end,'\n');
|
||||||
|
self.cursor.set(end);
|
||||||
|
match anchor {
|
||||||
|
Anchor::After => self.cursor.add(2),
|
||||||
|
Anchor::Before => { /* Do nothing */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Verb::ReplaceMode |
|
Verb::ReplaceMode |
|
||||||
Verb::InsertMode |
|
Verb::InsertMode |
|
||||||
|
|||||||
@@ -33,36 +33,41 @@ pub struct FernVi {
|
|||||||
|
|
||||||
impl Readline for FernVi {
|
impl Readline for FernVi {
|
||||||
fn readline(&mut self) -> ShResult<String> {
|
fn readline(&mut self) -> ShResult<String> {
|
||||||
self.editor = LineBuf::new().with_initial("The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the a", 1004);
|
self.editor = LineBuf::new().with_initial("\nThe quick brown fox jumps over\n the lazy dogThe quick\nbrown fox jumps over the a", 1004);
|
||||||
let raw_mode = self.reader.raw_mode(); // Restores termios state on drop
|
let raw_mode_guard = self.reader.raw_mode(); // Restores termios state on drop
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let new_layout = self.get_layout();
|
let new_layout = self.get_layout();
|
||||||
if let Some(layout) = self.old_layout.as_ref() {
|
if let Some(layout) = self.old_layout.as_ref() {
|
||||||
flog!(DEBUG, "clearing???");
|
|
||||||
self.writer.clear_rows(layout)?;
|
self.writer.clear_rows(layout)?;
|
||||||
}
|
}
|
||||||
raw_mode.disable_for(|| self.print_line(new_layout))?;
|
raw_mode_guard.disable_for(|| self.print_line(new_layout))?;
|
||||||
let key = self.reader.read_key()?;
|
let key = self.reader.read_key()?;
|
||||||
flog!(DEBUG, key);
|
flog!(DEBUG, key);
|
||||||
|
|
||||||
let Some(cmd) = self.mode.handle_key(key) else {
|
let Some(mut cmd) = self.mode.handle_key(key) else {
|
||||||
continue
|
continue
|
||||||
};
|
};
|
||||||
|
cmd.alter_line_motion_if_no_verb();
|
||||||
|
|
||||||
|
if cmd.should_submit() {
|
||||||
|
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
|
||||||
|
return Ok(std::mem::take(&mut self.editor.buffer))
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
|
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
|
||||||
if self.editor.buffer.is_empty() {
|
if self.editor.buffer.is_empty() {
|
||||||
std::mem::drop(raw_mode);
|
std::mem::drop(raw_mode_guard);
|
||||||
sh_quit(0);
|
sh_quit(0);
|
||||||
} else {
|
} else {
|
||||||
self.editor.buffer.clear();
|
self.editor.buffer.clear();
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
flog!(DEBUG,cmd);
|
||||||
|
|
||||||
self.exec_cmd(cmd)?;
|
self.exec_cmd(cmd)?;
|
||||||
|
|
||||||
flog!(DEBUG,self.editor.buffer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,7 +291,6 @@ impl TermReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn peek_byte(&mut self) -> std::io::Result<u8> {
|
pub fn peek_byte(&mut self) -> std::io::Result<u8> {
|
||||||
flog!(DEBUG,"filling buffer");
|
|
||||||
let buf = self.buffer.fill_buf()?;
|
let buf = self.buffer.fill_buf()?;
|
||||||
if buf.is_empty() {
|
if buf.is_empty() {
|
||||||
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "EOF"))
|
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "EOF"))
|
||||||
@@ -491,7 +490,6 @@ impl LineWriter {
|
|||||||
self.buffer.push_str("\x1b[2K\x1b[A");
|
self.buffer.push_str("\x1b[2K\x1b[A");
|
||||||
}
|
}
|
||||||
self.buffer.push_str("\x1b[2K");
|
self.buffer.push_str("\x1b[2K");
|
||||||
flog!(DEBUG, self.buffer);
|
|
||||||
write_all(self.out,self.buffer.as_str())?;
|
write_all(self.out,self.buffer.as_str())?;
|
||||||
self.buffer.clear();
|
self.buffer.clear();
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -560,7 +558,7 @@ impl LineWriter {
|
|||||||
self.buffer.push_str(prompt);
|
self.buffer.push_str(prompt);
|
||||||
self.buffer.push_str(line.as_str());
|
self.buffer.push_str(line.as_str());
|
||||||
|
|
||||||
if end.col == 0 && end.row > 0 {
|
if end.col == 0 && end.row > 0 && !self.buffer.ends_with('\n') {
|
||||||
// The line has wrapped. We need to use our own line break.
|
// The line has wrapped. We need to use our own line break.
|
||||||
self.buffer.push('\n');
|
self.buffer.push('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,26 @@ impl ViCmd {
|
|||||||
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
|
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
|
||||||
}
|
}
|
||||||
pub fn is_line_motion(&self) -> bool {
|
pub fn is_line_motion(&self) -> bool {
|
||||||
self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::LineUp | Motion::LineDown))
|
self.motion.as_ref().is_some_and(|m| {
|
||||||
|
matches!(m.1,
|
||||||
|
Motion::LineUp |
|
||||||
|
Motion::LineDown |
|
||||||
|
Motion::LineUpCharwise |
|
||||||
|
Motion::LineDownCharwise
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise
|
||||||
|
pub fn alter_line_motion_if_no_verb(&mut self) {
|
||||||
|
if self.is_line_motion() && self.verb.is_none() {
|
||||||
|
if let Some(motion) = self.motion.as_mut() {
|
||||||
|
match motion.1 {
|
||||||
|
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
|
||||||
|
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn is_mode_transition(&self) -> bool {
|
pub fn is_mode_transition(&self) -> bool {
|
||||||
self.verb.as_ref().is_some_and(|v| {
|
self.verb.as_ref().is_some_and(|v| {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::prompt::readline::{linebuf::LineBuf, vimode::{ViInsert, ViMode, ViNor
|
|||||||
|
|
||||||
use super::super::*;
|
use super::super::*;
|
||||||
|
|
||||||
|
|
||||||
fn assert_normal_cmd(cmd: &str, start: &str, cursor: usize, expected_buf: &str, expected_cursor: usize) {
|
fn assert_normal_cmd(cmd: &str, start: &str, cursor: usize, expected_buf: &str, expected_cursor: usize) {
|
||||||
let cmd = ViNormal::new()
|
let cmd = ViNormal::new()
|
||||||
.cmds_from_raw(cmd)
|
.cmds_from_raw(cmd)
|
||||||
@@ -103,18 +104,60 @@ fn linebuf_this_line() {
|
|||||||
fn linebuf_prev_line() {
|
fn linebuf_prev_line() {
|
||||||
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
|
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
|
||||||
let mut buf = LineBuf::new().with_initial(initial, 57);
|
let mut buf = LineBuf::new().with_initial(initial, 57);
|
||||||
let (start,end) = buf.prev_line().unwrap();
|
let (start,end) = buf.nth_prev_line(1).unwrap();
|
||||||
assert_eq!(buf.slice(start..end), Some("This is the second line"))
|
assert_eq!(buf.slice(start..end), Some("This is the second line"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linebuf_prev_line_first_line_is_empty() {
|
||||||
|
let initial = "\nThis is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
|
||||||
|
let mut buf = LineBuf::new().with_initial(initial, 36);
|
||||||
|
let (start,end) = buf.nth_prev_line(1).unwrap();
|
||||||
|
assert_eq!(buf.slice(start..end), Some("This is the first line"))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn linebuf_next_line() {
|
fn linebuf_next_line() {
|
||||||
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
|
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
|
||||||
let mut buf = LineBuf::new().with_initial(initial, 57);
|
let mut buf = LineBuf::new().with_initial(initial, 57);
|
||||||
let (start,end) = buf.next_line().unwrap();
|
let (start,end) = buf.nth_next_line(1).unwrap();
|
||||||
assert_eq!(buf.slice(start..end), Some("This is the fourth line"))
|
assert_eq!(buf.slice(start..end), Some("This is the fourth line"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linebuf_next_line_last_line_is_empty() {
|
||||||
|
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line\n";
|
||||||
|
let mut buf = LineBuf::new().with_initial(initial, 57);
|
||||||
|
let (start,end) = buf.nth_next_line(1).unwrap();
|
||||||
|
assert_eq!(buf.slice(start..end), Some("This is the fourth line"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linebuf_next_line_several_trailing_newlines() {
|
||||||
|
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line\n\n\n\n";
|
||||||
|
let mut buf = LineBuf::new().with_initial(initial, 81);
|
||||||
|
let (start,end) = buf.nth_next_line(1).unwrap();
|
||||||
|
assert_eq!(buf.slice(start..end), Some(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linebuf_next_line_only_newlines() {
|
||||||
|
let initial = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
|
||||||
|
let mut buf = LineBuf::new().with_initial(initial, 7);
|
||||||
|
let (start,end) = buf.nth_next_line(1).unwrap();
|
||||||
|
assert_eq!(start, 8);
|
||||||
|
assert_eq!(buf.slice(start..end), Some(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linebuf_prev_line_only_newlines() {
|
||||||
|
let initial = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
|
||||||
|
let mut buf = LineBuf::new().with_initial(initial, 7);
|
||||||
|
let (start,end) = buf.nth_prev_line(1).unwrap();
|
||||||
|
assert_eq!(start, 6);
|
||||||
|
assert_eq!(buf.slice(start..end), Some(""))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn linebuf_cursor_motion() {
|
fn linebuf_cursor_motion() {
|
||||||
let mut buf = LineBuf::new().with_initial("Thé quíck 🦊 bröwn fóx jumpś óver the 💤 lázy dóg 🐶", 0);
|
let mut buf = LineBuf::new().with_initial("Thé quíck 🦊 bröwn fóx jumpś óver the 💤 lázy dóg 🐶", 0);
|
||||||
@@ -233,3 +276,16 @@ fn editor_delete_end_unicode_word() {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn editor_delete_line_up() {
|
||||||
|
assert_normal_cmd(
|
||||||
|
"dk",
|
||||||
|
LOREM_IPSUM,
|
||||||
|
237,
|
||||||
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||||
|
126,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user