Code to erase character ranges

This commit is contained in:
Kovid Goyal 2022-10-06 21:14:28 +05:30
parent 5e5cae8391
commit eff239a195
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 216 additions and 45 deletions

View File

@ -16,8 +16,8 @@ func (self *Readline) text_upto_cursor_pos() string {
buf := strings.Builder{}
buf.Grow(1024)
for i, line := range self.lines {
if i == self.cursor_line {
buf.WriteString(line[:self.cursor_pos_in_line])
if i == self.cursor.Y {
buf.WriteString(line[:self.cursor.X])
break
} else {
buf.WriteString(line)
@ -31,10 +31,10 @@ func (self *Readline) text_after_cursor_pos() string {
buf := strings.Builder{}
buf.Grow(1024)
for i, line := range self.lines {
if i == self.cursor_line {
buf.WriteString(line[self.cursor_pos_in_line:])
if i == self.cursor.Y {
buf.WriteString(line[self.cursor.X:])
buf.WriteString("\n")
} else if i > self.cursor_line {
} else if i > self.cursor.Y {
buf.WriteString(line)
buf.WriteString("\n")
}
@ -50,30 +50,30 @@ func (self *Readline) all_text() string {
func (self *Readline) add_text(text string) {
new_lines := make([]string, 0, len(self.lines)+4)
new_lines = append(new_lines, self.lines[:self.cursor_line]...)
new_lines = append(new_lines, self.lines[:self.cursor.Y]...)
var lines_after []string
if len(self.lines) > self.cursor_line+1 {
lines_after = self.lines[self.cursor_line+1:]
if len(self.lines) > self.cursor.Y+1 {
lines_after = self.lines[self.cursor.Y+1:]
}
has_trailing_newline := strings.HasSuffix(text, "\n")
add_line_break := func(line string) {
new_lines = append(new_lines, line)
self.cursor_pos_in_line = len(line)
self.cursor_line += 1
self.cursor.X = len(line)
self.cursor.Y += 1
}
cline := self.lines[self.cursor_line]
before_first_line := cline[:self.cursor_pos_in_line]
cline := self.lines[self.cursor.Y]
before_first_line := cline[:self.cursor.X]
after_first_line := ""
if self.cursor_pos_in_line < len(cline) {
after_first_line = cline[self.cursor_pos_in_line:]
if self.cursor.X < len(cline) {
after_first_line = cline[self.cursor.X:]
}
for i, line := range utils.Splitlines(text) {
if i > 0 {
add_line_break(line)
} else {
line := before_first_line + line
self.cursor_pos_in_line = len(line)
self.cursor.X = len(line)
new_lines = append(new_lines, line)
}
}
@ -95,27 +95,27 @@ func (self *Readline) add_text(text string) {
func (self *Readline) move_cursor_left(amt uint, traverse_line_breaks bool) uint {
var amt_moved uint
for ; amt > 0; amt -= 1 {
if self.cursor_pos_in_line == 0 {
if !traverse_line_breaks || self.cursor_line == 0 {
if self.cursor.X == 0 {
if !traverse_line_breaks || self.cursor.Y == 0 {
return amt_moved
}
self.cursor_line -= 1
self.cursor_pos_in_line = len(self.lines[self.cursor_pos_in_line])
self.cursor.Y -= 1
self.cursor.X = len(self.lines[self.cursor.Y])
amt_moved += 1
continue
}
// This is an extremely inefficient algorithm but it does not matter since
// lines are not large.
line := self.lines[self.cursor_line]
runes := []rune(line[:self.cursor_pos_in_line])
orig_width := wcswidth.Stringwidth(line[:self.cursor_pos_in_line])
line := self.lines[self.cursor.Y]
runes := []rune(line[:self.cursor.X])
orig_width := wcswidth.Stringwidth(line[:self.cursor.X])
current_width := orig_width
for current_width == orig_width && len(runes) > 0 {
runes = runes[:len(runes)-1]
s := string(runes)
current_width = wcswidth.Stringwidth(s)
}
self.cursor_pos_in_line = len(string(runes))
self.cursor.X = len(string(runes))
amt_moved += 1
}
return amt_moved
@ -124,21 +124,21 @@ func (self *Readline) move_cursor_left(amt uint, traverse_line_breaks bool) uint
func (self *Readline) move_cursor_right(amt uint, traverse_line_breaks bool) uint {
var amt_moved uint
for ; amt > 0; amt -= 1 {
line := self.lines[self.cursor_line]
if self.cursor_pos_in_line >= len(line) {
if !traverse_line_breaks || self.cursor_line == len(self.lines)-1 {
line := self.lines[self.cursor.Y]
if self.cursor.X >= len(line) {
if !traverse_line_breaks || self.cursor.Y == len(self.lines)-1 {
return amt_moved
}
self.cursor_line += 1
self.cursor_pos_in_line = 0
self.cursor.Y += 1
self.cursor.X = 0
amt_moved += 1
continue
}
// This is an extremely inefficient algorithm but it does not matter since
// lines are not large.
before_runes := []rune(line[:self.cursor_pos_in_line])
after_runes := []rune(line[self.cursor_pos_in_line:])
orig_width := wcswidth.Stringwidth(line[:self.cursor_pos_in_line])
before_runes := []rune(line[:self.cursor.X])
after_runes := []rune(line[self.cursor.X:])
orig_width := wcswidth.Stringwidth(line[:self.cursor.X])
current_width := orig_width
for current_width == orig_width && len(after_runes) > 0 {
before_runes = append(before_runes, after_runes[0])
@ -155,8 +155,106 @@ func (self *Readline) move_cursor_right(amt uint, traverse_line_breaks bool) uin
after_runes = after_runes[1:]
before_runes = q
}
self.cursor_pos_in_line = len(string(before_runes))
self.cursor.X = len(string(before_runes))
amt_moved += 1
}
return amt_moved
}
func (self *Readline) move_to_start_of_line() bool {
if self.cursor.X > 0 {
self.cursor.X = 0
return true
}
return false
}
func (self *Readline) move_to_end_of_line() bool {
line := self.lines[self.cursor.Y]
if self.cursor.X >= len(line) {
return false
}
self.cursor.X = len(line)
return true
}
func (self *Readline) move_to_start() bool {
if self.cursor.Y == 0 && self.cursor.X == 0 {
return false
}
self.cursor.Y = 0
self.move_to_start_of_line()
return true
}
func (self *Readline) move_to_end() bool {
line := self.lines[self.cursor.Y]
if self.cursor.Y == len(self.lines)-1 && self.cursor.X >= len(line) {
return false
}
self.cursor.Y = len(self.lines) - 1
self.move_to_end_of_line()
return true
}
func (self *Readline) erase_between(start, end Position) {
if end.Less(start) {
start, end = end, start
}
if start.Y == end.Y {
line := self.lines[start.Y]
self.lines[start.Y] = line[:start.X] + line[end.X:]
if self.cursor.Y == start.Y && self.cursor.X >= start.X {
if self.cursor.X < end.X {
self.cursor.X = start.X
} else {
self.cursor.X -= end.X - start.X
}
}
return
}
lines := make([]string, 0, len(self.lines))
for i, line := range self.lines {
if i < start.Y || i > end.Y {
lines = append(lines, line)
} else if i == start.Y {
lines = append(lines, line[:start.X])
if self.cursor.Y == i && self.cursor.X > start.X {
self.cursor.X = start.X
}
} else if i == end.Y {
lines[len(lines)-1] += line[end.X:]
if i == self.cursor.Y {
self.cursor.Y = start.Y
if self.cursor.X < end.X {
self.cursor.X = start.X
} else {
self.cursor.X -= end.X - start.X
}
}
} else if i == self.cursor.Y {
self.cursor = start
}
}
self.lines = lines
}
func (self *Readline) erase_chars_before_cursor(amt uint, traverse_line_breaks bool) uint {
pos := self.cursor
num := self.move_cursor_left(amt, traverse_line_breaks)
if num == 0 {
return num
}
self.erase_between(self.cursor, pos)
return num
}
func (self *Readline) erase_chars_after_cursor(amt uint, traverse_line_breaks bool) uint {
pos := self.cursor
num := self.move_cursor_right(amt, traverse_line_breaks)
if num == 0 {
return num
}
self.erase_between(pos, self.cursor)
return num
}

View File

@ -43,15 +43,15 @@ func TestAddText(t *testing.T) {
dt("test", nil, "test", "", "test")
dt("1234\n", nil, "1234\n", "", "1234\n")
dt("abcd", func(rl *Readline) {
rl.cursor_pos_in_line = 2
rl.cursor.X = 2
rl.add_text("12")
}, "ab12", "cd", "ab12cd")
dt("abcd", func(rl *Readline) {
rl.cursor_pos_in_line = 2
rl.cursor.X = 2
rl.add_text("12\n34")
}, "ab12\n34", "cd", "ab12\n34cd")
dt("abcd\nxyz", func(rl *Readline) {
rl.cursor_pos_in_line = 2
rl.cursor.X = 2
rl.add_text("12\n34")
}, "abcd\nxy12\n34", "z", "abcd\nxy12\n34z")
}
@ -88,8 +88,8 @@ func TestCursorMovement(t *testing.T) {
}, "one", "à")
right := func(rl *Readline, amt uint, moved_amt uint, traverse_line_breaks bool) {
rl.cursor_line = 0
rl.cursor_pos_in_line = 0
rl.cursor.Y = 0
rl.cursor.X = 0
actual := rl.move_cursor_right(amt, traverse_line_breaks)
if actual != moved_amt {
t.Fatalf("Failed to move cursor by %#v\nactual != expected: %#v != %#v", amt, actual, moved_amt)
@ -111,3 +111,69 @@ func TestCursorMovement(t *testing.T) {
right(rl, 1, 1, false)
}, "à", "b")
}
func TestEraseChars(t *testing.T) {
dt := test_func(t)
backspace := func(rl *Readline, amt uint, erased_amt uint, traverse_line_breaks bool) {
actual := rl.erase_chars_before_cursor(amt, traverse_line_breaks)
if actual != erased_amt {
t.Fatalf("Failed to move cursor by %#v\nactual != expected: %d != %d", amt, actual, erased_amt)
}
}
dt("one\ntwo", func(rl *Readline) {
backspace(rl, 2, 2, false)
}, "one\nt", "")
dt("one\ntwo", func(rl *Readline) {
rl.cursor.X = 1
backspace(rl, 2, 1, false)
}, "one\n", "wo")
dt("one\ntwo", func(rl *Readline) {
rl.cursor.X = 1
backspace(rl, 2, 2, true)
}, "one", "wo")
dt("a😀", func(rl *Readline) {
backspace(rl, 1, 1, false)
}, "a", "")
dt("bà", func(rl *Readline) {
backspace(rl, 1, 1, false)
}, "b", "")
del := func(rl *Readline, amt uint, erased_amt uint, traverse_line_breaks bool) {
rl.cursor.Y = 0
rl.cursor.X = 0
actual := rl.erase_chars_after_cursor(amt, traverse_line_breaks)
if actual != erased_amt {
t.Fatalf("Failed to move cursor by %#v\nactual != expected: %d != %d", amt, actual, erased_amt)
}
}
dt("one\ntwo", func(rl *Readline) {
del(rl, 2, 2, false)
}, "", "e\ntwo")
dt("😀a", func(rl *Readline) {
del(rl, 1, 1, false)
}, "", "a")
dt("àb", func(rl *Readline) {
del(rl, 1, 1, false)
}, "", "b")
dt("one\ntwo\nthree", func(rl *Readline) {
rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2})
}, "oree", "")
dt("one\ntwo\nthree", func(rl *Readline) {
rl.cursor.X = 1
rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2})
}, "o", "ree")
dt("one\ntwo\nthree", func(rl *Readline) {
rl.cursor = Position{X: 1, Y: 1}
rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2})
}, "o", "ree")
dt("one\ntwo\nthree", func(rl *Readline) {
rl.cursor = Position{X: 1, Y: 0}
rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2})
}, "o", "ree")
dt("one\ntwo\nthree", func(rl *Readline) {
rl.cursor = Position{X: 0, Y: 0}
rl.erase_between(Position{X: 1}, Position{X: 2, Y: 2})
}, "", "oree")
}

View File

@ -21,6 +21,15 @@ type RlInit struct {
DontMarkPrompts bool
}
type Position struct {
X int
Y int
}
func (self Position) Less(other Position) bool {
return self.Y < other.Y || (self.Y == other.Y && self.X < other.X)
}
type Readline struct {
prompt string
prompt_len int
@ -29,14 +38,12 @@ type Readline struct {
mark_prompts bool
loop *loop.Loop
// The number of lines after the initial line
// The number of lines after the initial line on the screen
cursor_y int
// Input lines
lines []string
// The line the cursor is at currently
cursor_line int
// The offset into the text of the cursor line
cursor_pos_in_line int
// The cursor position in the text
cursor Position
}
func New(loop *loop.Loop, r RlInit) *Readline {

View File

@ -46,13 +46,13 @@ func (self *Readline) redraw() {
p = self.continuation_prompt
}
num_lines := self.write_line_with_prompt(line, p, int(screen_size.WidthCells))
if i == self.cursor_line {
if i == self.cursor.Y {
line_with_cursor = y
}
y += num_lines
}
self.loop.MoveCursorVertically(-y + line_with_cursor)
line := self.lines[self.cursor_line]
line_with_cursor += self.move_cursor_to_text_position(wcswidth.Stringwidth(line[:self.cursor_pos_in_line]), int(screen_size.WidthCells))
line := self.lines[self.cursor.Y]
line_with_cursor += self.move_cursor_to_text_position(wcswidth.Stringwidth(line[:self.cursor.X]), int(screen_size.WidthCells))
self.cursor_y = line_with_cursor
}