diff --git a/text/text.go b/text/text.go new file mode 100644 index 0000000..fbe6e34 --- /dev/null +++ b/text/text.go @@ -0,0 +1,248 @@ +package text + +import "unicode" + +// Dot represents a cursor or text selection. It has a start and end position, +// referring to where the user began and ended the selection respectively. +type Dot struct { Start, End int } + +// EmptyDot returns a zero-width dot at the specified position. +func EmptyDot (position int) Dot { + return Dot { position, position } +} + +// Canon places the lesser value at the start, and the greater value at the end. +// Note that a canonized dot does not in all cases correspond directly to the +// original, because there is a semantic value to the start and end positions. +func (dot Dot) Canon () Dot { + if dot.Start > dot.End { + return Dot { dot.End, dot.Start } + } else { + return dot + } +} + +// Empty returns whether or not the +func (dot Dot) Empty () bool { + return dot.Start == dot.End +} + +// Add shifts the dot to the right by the specified amount. +func (dot Dot) Add (delta int) Dot { + return Dot { + dot.Start + delta, + dot.End + delta, + } +} + +// Sub shifts the dot to the left by the specified amount. +func (dot Dot) Sub (delta int) Dot { + return Dot { + dot.Start - delta, + dot.End - delta, + } +} + +// Constrain constrains the dot's start and end from zero to length (inclusive). +func (dot Dot) Constrain (length int) Dot { + if dot.Start < 0 { dot.Start = 0 } + if dot.Start > length { dot.Start = length } + if dot.End < 0 { dot.End = 0 } + if dot.End > length { dot.End = length } + return dot +} + +// Width returns how many runes the dot spans. +func (dot Dot) Width () int { + dot = dot.Canon() + return dot.End - dot.Start +} + +// Slice returns the subset of text that the dot covers. +func (dot Dot) Slice (text []rune) []rune { + dot = dot.Canon().Constrain(len(text)) + return text[dot.Start:dot.End] +} + +// WordToLeft returns how far away to the left the next word boundary is from a +// given position. +func WordToLeft (text []rune, position int) (length int) { + if position < 1 { return } + if position > len(text) { position = len(text) } + + index := position - 1 + for index >= 0 && unicode.IsSpace(text[index]) { + length ++ + index -- + } + for index >= 0 && !unicode.IsSpace(text[index]) { + length ++ + index -- + } + return +} + +// WordToRight returns how far away to the right the next word boundary is from +// a given position. +func WordToRight (text []rune, position int) (length int) { + if position < 0 { return } + if position > len(text) { position = len(text) } + + index := position + for index < len(text) && unicode.IsSpace(text[index]) { + length ++ + index ++ + } + for index < len(text) && !unicode.IsSpace(text[index]) { + length ++ + index ++ + } + return +} + +// WordAround returns a dot that surrounds the word at the specified position. +func WordAround (text []rune, position int) (around Dot) { + return Dot { + position - WordToLeft(text, position), + position + WordToRight(text, position), + } +} + +// Backspace deletes the rune to the left of the dot. If word is true, it +// deletes up until the next word boundary on the left. If the dot is non-empty, +// it deletes the text inside of the dot. +func Backspace (text []rune, dot Dot, word bool) (result []rune, moved Dot) { + dot = dot.Constrain(len(text)) + if dot.Empty() { + distance := 1 + if word { + distance = WordToLeft(text, dot.End) + } + result = append ( + result, + text[:dot.Sub(distance).Constrain(len(text)).End]...) + result = append(result, text[dot.End:]...) + moved = EmptyDot(dot.Sub(distance).Start) + return + } else { + return Delete(text, dot, word) + } +} + +// Delete deletes the rune to the right of the dot. If word is true, it deletes +// up until the next word boundary on the right. If the dot is non-empty, it +// deletes the text inside of the dot. +func Delete (text []rune, dot Dot, word bool) (result []rune, moved Dot) { + dot = dot.Constrain(len(text)) + if dot.Empty() { + distance := 1 + if word { + distance = WordToRight(text, dot.End) + } + result = append(result, text[:dot.End]...) + result = append ( + result, + text[dot.Add(distance).Constrain(len(text)).End:]...) + moved = dot + return + } else { + dot = dot.Canon() + result = append(result, text[:dot.Start]...) + result = append(result, text[dot.End:]...) + moved = EmptyDot(dot.Start) + return + } +} + +// Lift removes the section of text inside of the dot, and returns a copy of it. +func Lift (text []rune, dot Dot) (result []rune, moved Dot, lifted []rune) { + dot = dot.Constrain(len(text)) + if dot.Empty() { + moved = dot + return + } + + dot = dot.Canon() + lifted = make([]rune, dot.Width()) + copy(lifted, dot.Slice(text)) + result = append(result, text[:dot.Start]...) + result = append(result, text[dot.End:]...) + moved = EmptyDot(dot.Start) + return +} + +// Type inserts one of more runes into the text at the dot position. If the dot +// is non-empty, it replaces the text inside of the dot with the new runes. +func Type (text []rune, dot Dot, characters ...rune) (result []rune, moved Dot) { + dot = dot.Constrain(len(text)) + if dot.Empty() { + result = append(result, text[:dot.End]...) + result = append(result, characters...) + if dot.End < len(text) { + result = append(result, text[dot.End:]...) + } + moved = EmptyDot(dot.Add(len(characters)).End) + return + } else { + dot = dot.Canon() + result = append(result, text[:dot.Start]...) + result = append(result, characters...) + result = append(result, text[dot.End:]...) + moved = EmptyDot(dot.Add(len(characters)).Start) + return + } +} + +// MoveLeft moves the dot left one rune. If word is true, it moves the dot to +// the next word boundary on the left. +func MoveLeft (text []rune, dot Dot, word bool) (moved Dot) { + dot = dot.Canon().Constrain(len(text)) + distance := 0 + if dot.Empty() { + distance = 1 + } + if word { + distance = WordToLeft(text, dot.Start) + } + moved = EmptyDot(dot.Sub(distance).Start) + return +} + +// MoveRight moves the dot right one rune. If word is true, it moves the dot to +// the next word boundary on the right. +func MoveRight (text []rune, dot Dot, word bool) (moved Dot) { + dot = dot.Canon().Constrain(len(text)) + distance := 0 + if dot.Empty() { + distance = 1 + } + if word { + distance = WordToRight(text, dot.End) + } + moved = EmptyDot(dot.Add(distance).End) + return +} + +// SelectLeft moves the end of the dot left one rune. If word is true, it moves +// the end of the dot to the next word boundary on the left. +func SelectLeft (text []rune, dot Dot, word bool) (moved Dot) { + dot = dot.Constrain(len(text)) + distance := 1 + if word { + distance = WordToLeft(text, dot.End) + } + dot.End -= distance + return dot +} + +// SelectRight moves the end of the dot right one rune. If word is true, it +// moves the end of the dot to the next word boundary on the right. +func SelectRight (text []rune, dot Dot, word bool) (moved Dot) { + dot = dot.Constrain(len(text)) + distance := 1 + if word { + distance = WordToRight(text, dot.End) + } + dot.End += distance + return dot +}