├── AUTHORS ├── re1 ├── testdata │ ├── re2-search.txt.bz2 │ └── re2-exhaustive.txt.bz2 ├── escape.go ├── escape_test.go ├── rope.go ├── union.go ├── union_test.go ├── re2_test.go ├── rope_test.go ├── re1_test.go └── re1.go ├── ui ├── testdata │ ├── TestDraw_golden.png │ ├── TestBlank_golden.png │ ├── TestCursorBlink_golden.png │ ├── TestDrawEmptyText_golden.png │ ├── TestDrawCursorMidLine_golden.png │ ├── TestDrawCursorAtEndOfLastLine_golden.png │ └── TestDrawCursorOnLineAfterLastLine_golden.png ├── highlighter.go ├── config.go ├── row.go ├── sheet_test.go ├── cmd.go ├── cmd_test.go ├── win.go ├── sheet.go ├── col.go └── text_box.go ├── README.md ├── syntax ├── syntax.go ├── dirsyntax │ └── dirsyntax.go ├── regexp.go ├── gosyntax │ └── gosyntax.go └── regexp_test.go ├── .travis.yml ├── clipboard ├── clipboard_test.go └── clipboard.go ├── LICENSE ├── gok.sh ├── text ├── text.go └── text_test.go ├── rope ├── index.go ├── index_test.go ├── rope_quick_test.go ├── rope.go └── rope_test.go ├── main.go └── edit └── edit.go /AUTHORS: -------------------------------------------------------------------------------- 1 | Ethan Burns 2 | -------------------------------------------------------------------------------- /re1/testdata/re2-search.txt.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eaburns/T/HEAD/re1/testdata/re2-search.txt.bz2 -------------------------------------------------------------------------------- /ui/testdata/TestDraw_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eaburns/T/HEAD/ui/testdata/TestDraw_golden.png -------------------------------------------------------------------------------- /ui/testdata/TestBlank_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eaburns/T/HEAD/ui/testdata/TestBlank_golden.png -------------------------------------------------------------------------------- /re1/testdata/re2-exhaustive.txt.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eaburns/T/HEAD/re1/testdata/re2-exhaustive.txt.bz2 -------------------------------------------------------------------------------- /ui/testdata/TestCursorBlink_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eaburns/T/HEAD/ui/testdata/TestCursorBlink_golden.png -------------------------------------------------------------------------------- /ui/testdata/TestDrawEmptyText_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eaburns/T/HEAD/ui/testdata/TestDrawEmptyText_golden.png -------------------------------------------------------------------------------- /ui/testdata/TestDrawCursorMidLine_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eaburns/T/HEAD/ui/testdata/TestDrawCursorMidLine_golden.png -------------------------------------------------------------------------------- /ui/testdata/TestDrawCursorAtEndOfLastLine_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eaburns/T/HEAD/ui/testdata/TestDrawCursorAtEndOfLastLine_golden.png -------------------------------------------------------------------------------- /ui/testdata/TestDrawCursorOnLineAfterLastLine_golden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eaburns/T/HEAD/ui/testdata/TestDrawCursorOnLineAfterLastLine_golden.png -------------------------------------------------------------------------------- /re1/escape.go: -------------------------------------------------------------------------------- 1 | package re1 2 | 3 | import "strings" 4 | 5 | // Escape returns the argument with any meta-characters escaped. 6 | func Escape(t string) string { 7 | var s strings.Builder 8 | for _, r := range t { 9 | if strings.ContainsRune(`|*+?.^$()[]\`, r) { 10 | s.WriteRune('\\') 11 | } 12 | s.WriteRune(r) 13 | } 14 | return s.String() 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/eaburns/T.svg?branch=master)](https://travis-ci.org/eaburns/T) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/eaburns/T)](https://goreportcard.com/report/github.com/eaburns/T) 3 | 4 | T is an Acme-like editor. It's still a work-in-progress. See github.com/eaburns/T_old for the previous, partial implementation. This is a complete re-write of that in an attempt to greatly simplify. -------------------------------------------------------------------------------- /syntax/syntax.go: -------------------------------------------------------------------------------- 1 | package syntax 2 | 3 | import ( 4 | "github.com/eaburns/T/rope" 5 | "github.com/eaburns/T/text" 6 | ) 7 | 8 | // A Highlight is a style applied to an addressed string of text. 9 | type Highlight struct { 10 | // At is the addressed string. 11 | At [2]int64 12 | // Style is the style to apply to the string. 13 | text.Style 14 | } 15 | 16 | // Tokenizer returns tokens from a rope. 17 | type Tokenizer interface { 18 | // NextToken next token or false. 19 | NextToken(rope.Rope) (Highlight, bool) 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 1.11 4 | 5 | notifications: 6 | email: false 7 | 8 | env: 9 | - PATH=$HOME/gopath/bin:$PATH 10 | - QUICK_TEST_SEED=0 11 | 12 | install: 13 | - sudo apt-get install libegl1-mesa-dev libgles2-mesa-dev libx11-dev 14 | - go get github.com/golang/lint/golint 15 | - go get github.com/fzipp/gocyclo 16 | - go get github.com/gordonklaus/ineffassign 17 | - go get github.com/client9/misspell/cmd/misspell 18 | - go get -t -v ./... && go build -v ./... 19 | 20 | script: 21 | - ./gok.sh 22 | -------------------------------------------------------------------------------- /re1/escape_test.go: -------------------------------------------------------------------------------- 1 | package re1 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestEscape(t *testing.T) { 10 | const meta = `|*+?.^$()[]\` 11 | str := meta + "abc" + meta + "abc" 12 | re, residual, err := New(Escape(str), Opts{}) 13 | if err != nil || residual != "" { 14 | t.Fatalf("New(%q)=_,%q,%v, want _,\"\",nil", str, residual, err) 15 | } 16 | want := []int64{0, int64(len(str)), 0 /* id */} 17 | if got := re.Find(strings.NewReader(str)); !reflect.DeepEqual(got, want) { 18 | t.Errorf("got=%v, want=%v\n", got, want) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /clipboard/clipboard_test.go: -------------------------------------------------------------------------------- 1 | package clipboard 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/eaburns/T/rope" 7 | ) 8 | 9 | func TestNewMem(t *testing.T) { 10 | const ( 11 | text0 = "Hello, World!" 12 | text1 = "Hello, 世界" 13 | ) 14 | c := NewMem() 15 | if got, err := c.Fetch(); err != nil || got.String() != "" { 16 | t.Errorf("Fetch()=%q, %v want %q,nil", got, err, "") 17 | } 18 | c.Store(rope.New(text0)) 19 | if got, err := c.Fetch(); err != nil || got.String() != text0 { 20 | t.Errorf("Fetch()=%q, %v want %q,nil", got, err, text0) 21 | } 22 | c.Store(rope.New(text1)) 23 | if got, err := c.Fetch(); err != nil || got.String() != text1 { 24 | t.Errorf("Fetch()=%q, %v want %q,nil", got, err, text1) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /syntax/dirsyntax/dirsyntax.go: -------------------------------------------------------------------------------- 1 | // Package dirsyntax implements a syntax.Highlighter for directory entries. 2 | package dirsyntax 3 | 4 | import ( 5 | "image/color" 6 | 7 | "github.com/eaburns/T/syntax" 8 | "github.com/eaburns/T/text" 9 | ) 10 | 11 | // NewTokenizer returns a new syntax highlighter tokenizer for directory entires. 12 | func NewTokenizer(dpi float32) syntax.Tokenizer { 13 | tok, err := syntax.NewRegexpTokenizer( 14 | syntax.Regexp{ 15 | Regexp: `.*/$`, 16 | Style: text.Style{ 17 | FG: color.RGBA{R: 0x2F, G: 0x6F, B: 0x89, A: 0xFF}, 18 | }, 19 | }, 20 | syntax.Regexp{ 21 | Regexp: `(^|.+/)\..*`, 22 | Style: text.Style{ 23 | FG: color.RGBA{R: 0x70, G: 0x70, B: 0x70, A: 0xFF}, 24 | }, 25 | }, 26 | ) 27 | if err != nil { 28 | panic(err.Error()) 29 | } 30 | return tok 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 The T Authors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /gok.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | o=$(mktemp tmp.XXXXXXXXXX) 3 | 4 | fail() { 5 | echo Failed 6 | cat $o | grep -v deprecated 7 | rm $o 8 | exit 1 9 | } 10 | 11 | trap fail INT TERM 12 | 13 | echo gofmt 14 | gofmt -s -l $(find . -name '*.go') > $o 2>&1 15 | test $(wc -l $o | awk '{ print $1 }') = "0" || fail 16 | 17 | echo govet 18 | go vet ./... > $o 2>&1 || fail 19 | 20 | echo ineffassign 21 | ineffassign . > $o 2>&1 || fail 22 | 23 | echo misspell 24 | misspell . > $o 2>&1 || fail 25 | 26 | echo gocyclo 27 | gocyclo -over 15 .\ 28 | | grep -v 'main Main text/main.go'\ 29 | | grep -v 'main Main ui/main.go'\ 30 | | grep -v 'ui execCmd ui/cmd.go'\ 31 | | grep -v 'edit runEditTest edit/edit_test.go'\ 32 | > $o 2>&1 33 | e=$(mktemp tmp.XXXXXXXXXX) 34 | touch $e 35 | diff $o $e > /dev/null || { rm $e; fail; } 36 | rm $e 37 | 38 | echo go test 39 | go test -test.timeout=60s ./... > $o 2>&1 || fail 40 | 41 | echo golint 42 | golint ./... \ 43 | > $o 2>&1 44 | # Silly: diff the grepped golint output with empty. 45 | # If it's non-empty, error, otherwise succeed. 46 | e=$(mktemp tmp.XXXXXXXXXX) 47 | touch $e 48 | diff $o $e > /dev/null || { rm $e; fail; } 49 | rm $e 50 | 51 | rm $o 52 | -------------------------------------------------------------------------------- /text/text.go: -------------------------------------------------------------------------------- 1 | // Package text has text styles. 2 | package text 3 | 4 | import ( 5 | "image/color" 6 | 7 | "github.com/golang/freetype/truetype" 8 | "golang.org/x/image/font" 9 | "golang.org/x/image/font/gofont/gomedium" 10 | ) 11 | 12 | // A Style describes the color, font, and size of text. 13 | type Style struct { 14 | // FG and BG are the foreground and background colors of the text. 15 | FG, BG color.Color 16 | // Face is the font face, describing the font and size. 17 | font.Face 18 | } 19 | 20 | // Merge returns other with any nil fields 21 | // replaced by the corresponding field of sty. 22 | func (sty Style) Merge(other Style) Style { 23 | if other.FG == nil { 24 | other.FG = sty.FG 25 | } 26 | if other.BG == nil { 27 | other.BG = sty.BG 28 | } 29 | if other.Face == nil { 30 | other.Face = sty.Face 31 | } 32 | return other 33 | } 34 | 35 | // Face returns a font.Face for a TTF of a given size at a given DPI. 36 | func Face(ttf []byte, dpi float32, sizePt int) font.Face { 37 | f, err := truetype.Parse(gomedium.TTF) 38 | if err != nil { 39 | panic(err.Error()) 40 | } 41 | return truetype.NewFace(f, &truetype.Options{ 42 | Size: float64(sizePt), 43 | DPI: float64(dpi * (72.0 / 96.0)), 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /rope/index.go: -------------------------------------------------------------------------------- 1 | package rope 2 | 3 | // IndexFunc returns the byte index 4 | // of the first rune in the rope 5 | // for which a function returns true. 6 | // If the function never returns true, 7 | // IndexFunc returns -1. 8 | func IndexFunc(ro Rope, f func(r rune) bool) int64 { 9 | var i int64 10 | rr := NewReader(ro) 11 | for { 12 | r, w, err := rr.ReadRune() 13 | if err != nil { 14 | return -1 15 | } 16 | if f(r) { 17 | return i 18 | } 19 | i += int64(w) 20 | } 21 | } 22 | 23 | // IndexRune returns the byte index 24 | // of the first occurrence of r in the rope. 25 | // If the rope does not contain r, 26 | // IndexRune returns -1. 27 | func IndexRune(ro Rope, r rune) int64 { 28 | return IndexFunc(ro, func(x rune) bool { return x == r }) 29 | } 30 | 31 | // LastIndexFunc returns the byte index 32 | // of the last rune in the rope 33 | // for which a function returns true. 34 | // If the function never returns true, 35 | // IndexFunc returns -1. 36 | // 37 | // LastIndexFunc traverses the rope from end to beginning. 38 | func LastIndexFunc(ro Rope, f func(r rune) bool) int64 { 39 | i := ro.Len() 40 | rr := NewReverseReader(ro) 41 | for { 42 | r, w, err := rr.ReadRune() 43 | if err != nil { 44 | return -1 45 | } 46 | i -= int64(w) 47 | if f(r) { 48 | return i 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /syntax/regexp.go: -------------------------------------------------------------------------------- 1 | package syntax 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/eaburns/T/re1" 7 | "github.com/eaburns/T/rope" 8 | "github.com/eaburns/T/text" 9 | ) 10 | 11 | type reTokenizer struct { 12 | regexps []Regexp 13 | re *re1.Regexp 14 | } 15 | 16 | // A Regexp describes a syntactic element using a regular expression. 17 | type Regexp struct { 18 | // Regexp is the re1 regular expression to match the element. 19 | Regexp string 20 | // Group is the numbered capture group of the element text. 21 | Group int 22 | Style text.Style 23 | } 24 | 25 | // NewRegexpTokenizer returns a new Tokenizer 26 | // defined by a set of re1 regular expressions. 27 | func NewRegexpTokenizer(regexps ...Regexp) (Tokenizer, error) { 28 | var rs []*re1.Regexp 29 | for i, regexp := range regexps { 30 | switch r, residual, err := re1.New(regexp.Regexp, re1.Opts{ID: i}); { 31 | case err != nil: 32 | return nil, err 33 | case residual != "": 34 | return nil, errors.New("expected end-of-input, got " + residual) 35 | default: 36 | rs = append(rs, r) 37 | } 38 | } 39 | re := re1.Union(rs...) 40 | if re == nil { 41 | return nil, errors.New("no regexps") 42 | } 43 | return &reTokenizer{regexps: regexps, re: re}, nil 44 | } 45 | 46 | func (t *reTokenizer) NextToken(txt rope.Rope) (Highlight, bool) { 47 | ms := t.re.FindInRope(txt, 0, txt.Len()) 48 | if ms == nil { 49 | return Highlight{}, false 50 | } 51 | i := int(ms[len(ms)-1]) 52 | j := 2 * t.regexps[i].Group 53 | h := Highlight{ 54 | At: [2]int64{ms[j], ms[j+1]}, 55 | Style: t.regexps[i].Style, 56 | } 57 | return h, true 58 | } 59 | -------------------------------------------------------------------------------- /re1/rope.go: -------------------------------------------------------------------------------- 1 | package re1 2 | 3 | import ( 4 | "github.com/eaburns/T/rope" 5 | ) 6 | 7 | // FindInRope returns the left-most, longest match of a regulax expression 8 | // between byte offsets s (inclusive) and e (exclusive) in a rope. 9 | func (re *Regexp) FindInRope(ro rope.Rope, s, e int64) []int64 { 10 | sl := rope.Slice(ro, s, ro.Len()) 11 | rr := rope.NewReader(sl) 12 | v := newVM(re, rr) 13 | v.c = prevRune(ro, s) 14 | v.at, v.lim = s, e 15 | return run(v) 16 | } 17 | 18 | func prevRune(ro rope.Rope, i int64) rune { 19 | sl := rope.Slice(ro, 0, i) 20 | rr := rope.NewReverseReader(sl) 21 | r, _, err := rr.ReadRune() 22 | if err != nil { 23 | return eof 24 | } 25 | return r 26 | } 27 | 28 | // FindReverseInRope returns the right-most, longest match 29 | // of a reverse-compiled regulax expression 30 | // between byte offsets s (inclusive) and e (exclusive) in a rope. 31 | // 32 | // The receiver is assumed to be compiled for a reverse match. 33 | func (re *Regexp) FindReverseInRope(ro rope.Rope, s, e int64) []int64 { 34 | sl := rope.Slice(ro, 0, e) 35 | rr := rope.NewReverseReader(sl) 36 | v := newVM(re, rr) 37 | v.c = nextRune(ro, e) 38 | v.lim = e - s 39 | ms := run(v) 40 | // Only reverse to len(ms)-1, because the last is the regexp ID. 41 | for i := 0; i < len(ms)-1; i += 2 { 42 | if ms[i] >= 0 { 43 | ms[i], ms[i+1] = e-ms[i+1], e-ms[i] 44 | } 45 | } 46 | return ms 47 | } 48 | 49 | func nextRune(ro rope.Rope, i int64) rune { 50 | sl := rope.Slice(ro, i, ro.Len()) 51 | rr := rope.NewReader(sl) 52 | r, _, err := rr.ReadRune() 53 | if err != nil { 54 | return eof 55 | } 56 | return r 57 | } 58 | -------------------------------------------------------------------------------- /ui/highlighter.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "time" 7 | 8 | "github.com/eaburns/T/edit" 9 | "github.com/eaburns/T/rope" 10 | "github.com/eaburns/T/syntax" 11 | ) 12 | 13 | type highlighter struct { 14 | syntax.Tokenizer 15 | } 16 | 17 | func (h *highlighter) Update(hi []syntax.Highlight, diffs edit.Diffs, txt rope.Rope) (res []syntax.Highlight) { 18 | t0 := time.Now() 19 | defer func() { 20 | dur := time.Since(t0) 21 | if dur > time.Second { 22 | fmt.Println(dur, len(res)) 23 | } 24 | }() 25 | if len(diffs) == 0 { 26 | hi = update(h.Tokenizer, hi, nil, txt) 27 | return hi 28 | } 29 | for _, diff := range diffs { 30 | var tail []syntax.Highlight 31 | if len(hi) > 0 { 32 | i := sort.Search(len(hi), func(i int) bool { 33 | return hi[i].At[1] > diff.At[0] 34 | }) 35 | hi, tail = hi[:i:i], hi[i:] 36 | } 37 | for i := range tail { 38 | tail[i].At = diff.Update(tail[i].At) 39 | } 40 | for len(tail) > 0 && tail[0].At[0] == tail[0].At[1] { 41 | tail = tail[1:] 42 | } 43 | hi = update(h.Tokenizer, hi, tail, txt) 44 | } 45 | return hi 46 | } 47 | 48 | func update(tok syntax.Tokenizer, hi, tail []syntax.Highlight, txt rope.Rope) []syntax.Highlight { 49 | var at int64 50 | if len(hi) > 0 { 51 | at = hi[len(hi)-1].At[1] 52 | } 53 | for { 54 | h, ok := tok.NextToken(rope.Slice(txt, at, txt.Len())) 55 | if !ok { 56 | return append(hi, tail...) 57 | } 58 | h.At[0] += at 59 | h.At[1] += at 60 | if len(tail) > 0 && tail[0] == h { 61 | return append(hi, tail...) 62 | } 63 | for len(tail) > 0 && tail[0].At[0] < h.At[1] { 64 | tail = tail[1:] 65 | } 66 | at = h.At[1] 67 | hi = append(hi, h) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /rope/index_test.go: -------------------------------------------------------------------------------- 1 | package rope 2 | 3 | import "testing" 4 | 5 | func TestIndexFunc(t *testing.T) { 6 | tests := []struct { 7 | str string 8 | r rune 9 | at int64 10 | }{ 11 | {"", 'x', -1}, 12 | {"abc", 'a', 0}, 13 | {"abc", 'b', 1}, 14 | {"abc", 'c', 2}, 15 | {"abc", 'd', -1}, 16 | {"☺x", 'x', int64(len("☺"))}, 17 | {"abcabc", 'b', 1}, 18 | } 19 | for _, test := range tests { 20 | ro := New(test.str) 21 | got := IndexFunc(ro, func(r rune) bool { return r == test.r }) 22 | if got != test.at { 23 | t.Errorf("IndexFunc(%q, =%q)=%d, want %d", 24 | test.str, test.r, got, test.at) 25 | } 26 | } 27 | } 28 | 29 | func TestIndexRune(t *testing.T) { 30 | tests := []struct { 31 | str string 32 | r rune 33 | at int64 34 | }{ 35 | {"", 'x', -1}, 36 | {"abc", 'a', 0}, 37 | {"abc", 'b', 1}, 38 | {"abc", 'c', 2}, 39 | {"abc", 'd', -1}, 40 | {"☺x", 'x', int64(len("☺"))}, 41 | {"abcabc", 'b', 1}, 42 | } 43 | for _, test := range tests { 44 | ro := New(test.str) 45 | got := IndexRune(ro, test.r) 46 | if got != test.at { 47 | t.Errorf("IndexFunc(%q, =%q)=%d, want %d", 48 | test.str, test.r, got, test.at) 49 | } 50 | } 51 | } 52 | 53 | func TestLastIndexFunc(t *testing.T) { 54 | tests := []struct { 55 | str string 56 | r rune 57 | at int64 58 | }{ 59 | {"", 'x', -1}, 60 | {"abc", 'a', 0}, 61 | {"abc", 'b', 1}, 62 | {"abc", 'c', 2}, 63 | {"abc", 'd', -1}, 64 | {"☺x", 'x', int64(len("☺"))}, 65 | {"abcabc", 'b', 4}, 66 | } 67 | for _, test := range tests { 68 | ro := New(test.str) 69 | got := LastIndexFunc(ro, func(r rune) bool { return r == test.r }) 70 | if got != test.at { 71 | t.Errorf("LastIndexFunc(%q, =%q)=%d, want %d", 72 | test.str, test.r, got, test.at) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /syntax/gosyntax/gosyntax.go: -------------------------------------------------------------------------------- 1 | // Package gosyntax implements a syntax.Highlighter for Go syntax. 2 | package gosyntax 3 | 4 | import ( 5 | "image/color" 6 | 7 | "github.com/eaburns/T/syntax" 8 | "github.com/eaburns/T/text" 9 | "golang.org/x/image/font/gofont/gomedium" 10 | ) 11 | 12 | // NewTokenizer returns a new syntax highlighter tokenizer for Go. 13 | func NewTokenizer(dpi float32) syntax.Tokenizer { 14 | const ( 15 | blockComment = `/[*]([^*]|[*][^/])*[*]/` 16 | lineComment = `//.*` 17 | interpString = `("([^"\n]|\\["\n])*([^\\\n]|\\\n)")|""` 18 | rawString = "`[^`]*`" 19 | ) 20 | tok, err := syntax.NewRegexpTokenizer( 21 | syntax.Regexp{ 22 | // TODO: Go highlighting for keywords doesn't handle non-ASCII word boundaries. 23 | // Ideally, instead of suing [^a-ZA-Z0-9_] for word bourdaries, re1 would have a unicode-aware \b. 24 | Regexp: `(^|[^a-zA-Z0-9_])(break|default|func|interface|select|case|defer|go|map|struct|chan|else|goto|package|switch|const|fallthrough|if|range|type|continue|for|import|return|var)([^a-zA-Z0-9_]|$)`, 25 | Group: 2, 26 | Style: text.Style{ 27 | Face: text.Face(gomedium.TTF, dpi, 11 /* pt */), 28 | }, 29 | }, 30 | syntax.Regexp{ 31 | Regexp: "(" + blockComment + ")|(" + lineComment + ")", 32 | Style: text.Style{ 33 | FG: color.RGBA{R: 0x70, G: 0x70, B: 0x70, A: 0xFF}, 34 | }, 35 | }, 36 | syntax.Regexp{ 37 | Regexp: "(" + interpString + ")|(" + rawString + ")", 38 | Style: text.Style{ 39 | FG: color.RGBA{R: 0x2F, G: 0x6F, B: 0x89, A: 0xFF}, 40 | }, 41 | }, 42 | syntax.Regexp{ 43 | // TODO: Go highlighting for runes doesn't handle the \000, \xFF, \uFFFF, or \UFFFFFFFF forms. 44 | Regexp: `'[^']'|'\\t'|'\\n'|'\\\\'|'\\''`, 45 | Style: text.Style{ 46 | FG: color.RGBA{R: 0x2F, G: 0x6F, B: 0x89, A: 0xFF}, 47 | }, 48 | }, 49 | ) 50 | if err != nil { 51 | panic(err.Error()) 52 | } 53 | return tok 54 | } 55 | -------------------------------------------------------------------------------- /clipboard/clipboard.go: -------------------------------------------------------------------------------- 1 | // Package clipboard provides access to a copy/paste clipboard. 2 | // If available it uses the system clipboard, 3 | // but if unavailable it falls back to a simple, memory buffer. 4 | // 5 | // It is a wrapper on top of github.com/atotto/clipboard. 6 | package clipboard 7 | 8 | import ( 9 | "sync" 10 | 11 | "github.com/atotto/clipboard" 12 | "github.com/eaburns/T/rope" 13 | ) 14 | 15 | // A Clipboard provides means to store and fetch text. 16 | // Implementations should support concurrent access. 17 | type Clipboard interface { 18 | // Store stores the text ot the clipboard. 19 | Store(rope.Rope) error 20 | // Fetch returns the text from the clipboard. 21 | Fetch() (rope.Rope, error) 22 | } 23 | 24 | // New returns a new clipboard. 25 | // 26 | // If the system clipboard is available, 27 | // then the returned Clipboard uses the system clipboard. 28 | // 29 | // If the system clipboard is unavailable, 30 | // then a empty, memory-based clipboard is returned. 31 | func New() Clipboard { 32 | if clipboard.Unsupported { 33 | return NewMem() 34 | } 35 | return sysClipboard{} 36 | } 37 | 38 | // NewMem returns a new, empty, memory-based clipboard. 39 | // This method is useful for tests. 40 | func NewMem() Clipboard { 41 | return &memClipboard{text: rope.Empty()} 42 | } 43 | 44 | type sysClipboard struct{} 45 | 46 | func (sysClipboard) Store(r rope.Rope) error { 47 | return clipboard.WriteAll(r.String()) 48 | } 49 | 50 | func (sysClipboard) Fetch() (rope.Rope, error) { 51 | str, err := clipboard.ReadAll() 52 | if err != nil { 53 | return nil, err 54 | } 55 | return rope.New(str), nil 56 | } 57 | 58 | type memClipboard struct { 59 | text rope.Rope 60 | mu sync.Mutex 61 | } 62 | 63 | func (m *memClipboard) Store(r rope.Rope) error { 64 | m.mu.Lock() 65 | m.text = r 66 | m.mu.Unlock() 67 | return nil 68 | } 69 | 70 | func (m *memClipboard) Fetch() (rope.Rope, error) { 71 | m.mu.Lock() 72 | r := m.text 73 | m.mu.Unlock() 74 | return r, nil 75 | } 76 | -------------------------------------------------------------------------------- /ui/config.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "image/color" 5 | 6 | "github.com/eaburns/T/syntax" 7 | "github.com/eaburns/T/syntax/dirsyntax" 8 | "github.com/eaburns/T/syntax/gosyntax" 9 | "github.com/golang/freetype/truetype" 10 | "golang.org/x/image/font/gofont/goregular" 11 | ) 12 | 13 | const ( 14 | // framePx is the pixel-width of the lines 15 | // drawn between columns and rows. 16 | framePx = 1 17 | 18 | // textPadPx is the pixel-width of the padding 19 | // between the left and right side of a text box 20 | // and its text. 21 | textPadPx = 7 22 | 23 | // cursorWidthPx is the pixel-width of the cursor. 24 | cursorWidthPx = 4 25 | 26 | // colText is the default column background text. 27 | colText = "Del NewCol NewRow\n" 28 | 29 | // tagText is the default tag text. 30 | tagText = " Del Cut Paste" 31 | ) 32 | 33 | var ( 34 | // defaultFont is the default font. 35 | defaultFont, _ = truetype.Parse(goregular.TTF) 36 | 37 | // defaultFontSize is the default font size in points. 38 | defaultFontSize = 11 39 | 40 | // fg is the text foreground color. 41 | fg = color.RGBA{R: 0x10, G: 0x28, B: 0x34, A: 0xFF} 42 | 43 | // frameBG is the lines drawn between columns and rows. 44 | frameBG = fg 45 | 46 | // colBG is the column background color. 47 | colBG = color.White 48 | 49 | // tagBG is the tag background color. 50 | tagBG = color.RGBA{R: 0xCF, G: 0xE0, B: 0xF7, A: 0xFF} 51 | 52 | // bodyBG is a body background color. 53 | bodyBG = color.RGBA{R: 0xFA, G: 0xF0, B: 0xE6, A: 0xFF} 54 | 55 | // hiBG1, hiBG2, and hiBG2 are the background colors 56 | // of 1-, 2-, and 3-click highlighted text. 57 | hiBG1 = color.RGBA{R: 0xDF, G: 0xC6, B: 0xDF, A: 0xFF} 58 | hiBG2 = color.RGBA{R: 0xF6, G: 0xC3, B: 0xC6, A: 0xFF} 59 | hiBG3 = color.RGBA{R: 0xD0, G: 0xEA, B: 0xC8, A: 0xFF} 60 | 61 | // syntaxHighlighting maps file regular (using regexp package syntax) 62 | // to functions from dpi to the Highlighter for that file. 63 | syntaxHighlighting = []struct { 64 | regexp string 65 | tok func(float32) syntax.Tokenizer 66 | }{ 67 | {`.*\.go$`, gosyntax.NewTokenizer}, 68 | {`.*/$`, dirsyntax.NewTokenizer}, 69 | } 70 | ) 71 | -------------------------------------------------------------------------------- /re1/union.go: -------------------------------------------------------------------------------- 1 | package re1 2 | 3 | // Union returns a single regular expression that matches the union of a set of regular expressions. 4 | // The Union of no regexps is nil. 5 | // 6 | // The last element of the slice returned by a call to Find will be the ID (Opts.ID) of the component expression that matched. 7 | // 8 | // The capture groups are numbered with respect to their corresponding numbers for the matched component regexp. 9 | // For example, Union("(a)bc", "(d)ef") will return a match for "a" as capture group 1 if component expression "(a)bc" matches. 10 | // However it will return a match for "d" as capture group 1 if component expression "(d)ef" matches. 11 | func Union(res ...*Regexp) *Regexp { 12 | switch len(res) { 13 | case 0: 14 | return nil 15 | case 1: 16 | return res[0] 17 | } 18 | 19 | left := new(Regexp) 20 | *left = *res[0] 21 | left.prog = append([]instr{}, left.prog...) 22 | left.class = append([][][2]rune{}, left.class...) 23 | // We use non-capturing group syntax here 24 | // even though it's not actually supported by re1. 25 | // But source is only used for debugging, 26 | // and using a capturing group would be incorrectly, 27 | // because the capture numbers would be wrong. 28 | left.source = "(?:" + left.source + ")" 29 | for _, right := range res[1:] { 30 | left = union2(left, right) 31 | left.source += "|(?:" + right.source + ")" 32 | } 33 | return left 34 | } 35 | 36 | // union2 is like catProg, but 37 | // it doesn't re-number capture groups, 38 | // it doesn't reverse, and 39 | // it sets the capture count to the max, not the sum. 40 | func union2(left, right *Regexp) *Regexp { 41 | prog := make([]instr, 0, 2+len(left.prog)+len(right.prog)) 42 | prog = append(prog, instr{op: fork, arg: len(left.prog) + 2}) 43 | prog = append(prog, left.prog...) 44 | left.prog = append(prog, instr{op: jmp, arg: len(right.prog) + 1}) 45 | for _, instr := range right.prog { 46 | if instr.op == class || instr.op == nclass { 47 | instr.arg += len(left.class) 48 | } 49 | left.prog = append(left.prog, instr) 50 | } 51 | left.class = append(left.class, right.class...) 52 | if right.ncap > left.ncap { 53 | left.ncap = right.ncap 54 | } 55 | return left 56 | } 57 | -------------------------------------------------------------------------------- /text/text_test.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | 8 | "golang.org/x/image/font" 9 | "golang.org/x/image/math/fixed" 10 | ) 11 | 12 | func TestStyleMerge(t *testing.T) { 13 | face1, face2 := testFace{1}, testFace{2} 14 | tests := []struct { 15 | a, b Style 16 | want Style 17 | }{ 18 | { 19 | a: Style{}, 20 | b: Style{}, 21 | want: Style{}, 22 | }, 23 | { 24 | a: Style{FG: color.White}, 25 | b: Style{FG: color.Black}, 26 | want: Style{FG: color.Black}, 27 | }, 28 | { 29 | a: Style{FG: color.White}, 30 | b: Style{BG: color.Black}, 31 | want: Style{FG: color.White, BG: color.Black}, 32 | }, 33 | { 34 | a: Style{FG: color.White}, 35 | b: Style{Face: face1}, 36 | want: Style{FG: color.White, Face: face1}, 37 | }, 38 | { 39 | a: Style{BG: color.White}, 40 | b: Style{BG: color.Black}, 41 | want: Style{BG: color.Black}, 42 | }, 43 | { 44 | a: Style{BG: color.White}, 45 | b: Style{FG: color.Black}, 46 | want: Style{FG: color.Black, BG: color.White}, 47 | }, 48 | { 49 | a: Style{BG: color.White}, 50 | b: Style{Face: face1}, 51 | want: Style{BG: color.White, Face: face1}, 52 | }, 53 | { 54 | a: Style{Face: face1}, 55 | b: Style{Face: face2}, 56 | want: Style{Face: face2}, 57 | }, 58 | { 59 | a: Style{Face: face1}, 60 | b: Style{FG: color.White}, 61 | want: Style{FG: color.White, Face: face1}, 62 | }, 63 | { 64 | a: Style{Face: face1}, 65 | b: Style{BG: color.Black}, 66 | want: Style{BG: color.Black, Face: face1}, 67 | }, 68 | { 69 | a: Style{FG: color.White, BG: color.Black, Face: face1}, 70 | b: Style{FG: color.Black, BG: color.White, Face: face2}, 71 | want: Style{FG: color.Black, BG: color.White, Face: face2}, 72 | }, 73 | } 74 | for _, test := range tests { 75 | got := test.a.Merge(test.b) 76 | if got != test.want { 77 | t.Errorf("(%v).merge(%v)=%v, want %v", 78 | test.a, test.b, got, test.want) 79 | } 80 | } 81 | } 82 | 83 | type testFace struct{ int } 84 | 85 | func (testFace) Close() error { panic("unimplemented") } 86 | func (testFace) Glyph(fixed.Point26_6, rune) (image.Rectangle, image.Image, image.Point, fixed.Int26_6, bool) { 87 | panic("unimplemented") 88 | } 89 | func (testFace) GlyphBounds(rune) (fixed.Rectangle26_6, fixed.Int26_6, bool) { panic("unimplemented") } 90 | func (testFace) GlyphAdvance(rune) (fixed.Int26_6, bool) { panic("unimplemented") } 91 | func (testFace) Kern(rune, rune) fixed.Int26_6 { panic("unimplemented") } 92 | func (testFace) Metrics() font.Metrics { panic("unimplemented") } 93 | -------------------------------------------------------------------------------- /syntax/regexp_test.go: -------------------------------------------------------------------------------- 1 | package syntax 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | 7 | "github.com/eaburns/T/rope" 8 | "github.com/eaburns/T/text" 9 | ) 10 | 11 | func TestRegexpNextToken(t *testing.T) { 12 | style0 := text.Style{FG: color.White} 13 | style1 := text.Style{FG: color.Black} 14 | tests := []struct { 15 | name string 16 | res []Regexp 17 | text string 18 | want Highlight 19 | wantOK bool 20 | }{ 21 | { 22 | name: "no match", 23 | res: []Regexp{ 24 | { 25 | Regexp: "abc", 26 | Style: style0, 27 | }, 28 | { 29 | Regexp: "def", 30 | Style: style1, 31 | }, 32 | }, 33 | text: "xyz", 34 | wantOK: false, 35 | }, 36 | { 37 | name: "first match", 38 | res: []Regexp{ 39 | { 40 | Regexp: "abc", 41 | Style: style0, 42 | }, 43 | { 44 | Regexp: "def", 45 | Style: style1, 46 | }, 47 | }, 48 | text: "abc", 49 | want: Highlight{At: [2]int64{0, 3}, Style: style0}, 50 | wantOK: true, 51 | }, 52 | { 53 | name: "second match", 54 | res: []Regexp{ 55 | { 56 | Regexp: "abc", 57 | Style: style0, 58 | }, 59 | { 60 | Regexp: "def", 61 | Style: style1, 62 | }, 63 | }, 64 | text: "def", 65 | want: Highlight{At: [2]int64{0, 3}, Style: style1}, 66 | wantOK: true, 67 | }, 68 | { 69 | name: "skip prefix match", 70 | res: []Regexp{ 71 | { 72 | Regexp: "abc", 73 | Style: style0, 74 | }, 75 | { 76 | Regexp: "def", 77 | Style: style1, 78 | }, 79 | }, 80 | text: "XXXXdef", 81 | want: Highlight{At: [2]int64{4, 7}, Style: style1}, 82 | wantOK: true, 83 | }, 84 | { 85 | name: "sub-group match", 86 | res: []Regexp{ 87 | { 88 | Regexp: "a(b+)c", 89 | Group: 1, 90 | Style: style0, 91 | }, 92 | { 93 | Regexp: "def", 94 | Style: style1, 95 | }, 96 | }, 97 | text: "XXXXabbbc", 98 | want: Highlight{At: [2]int64{5, 8}, Style: style0}, 99 | wantOK: true, 100 | }, 101 | } 102 | for _, test := range tests { 103 | test := test 104 | t.Logf("%s\n", test.name) 105 | t.Run(test.name, func(t *testing.T) { 106 | tok, err := NewRegexpTokenizer(test.res...) 107 | if err != nil { 108 | t.Fatalf("NewRegexpTokenizer(...)=_,%v, want nil", err) 109 | } 110 | got, ok := tok.NextToken(rope.New(test.text)) 111 | if got != test.want || ok != test.wantOK { 112 | t.Errorf("NextToken(%q)=%v,%v, want %v,%v", 113 | test.text, got, ok, test.want, test.wantOK) 114 | } 115 | }) 116 | } 117 | } 118 | 119 | func TestNewRegexpTokenizerError(t *testing.T) { 120 | _, err := NewRegexpTokenizer() 121 | if err == nil { 122 | t.Error("NewRegexpTokenizer()=_,nil, wanted an error") 123 | } 124 | _, err = NewRegexpTokenizer(Regexp{Regexp: "(abc)\nx"}) 125 | if err == nil { 126 | t.Error("NewRegexpTokenizer(\"|\")=_,nil, wanted an error") 127 | } 128 | _, err = NewRegexpTokenizer(Regexp{Regexp: "|" /* malformed | */}) 129 | if err == nil { 130 | t.Error("NewRegexpTokenizer(\"|\")=_,nil, wanted an error") 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /ui/row.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | ) 7 | 8 | // The Row interface is implemented by UI elements 9 | // that sit in a column, draw, and react to user input events. 10 | // 11 | // All corrdinates below are relative to the row, 12 | // with 0,0 in the upper left. 13 | type Row interface { 14 | // Draw draws the element to the image. 15 | // If dirty is true the element should redraw itself in its entirity. 16 | // If dirty is false, the element need only redraw 17 | // parts that have changed since the last call to Draw. 18 | Draw(dirty bool, img draw.Image) 19 | 20 | // Focus handles a focus state change. 21 | // The focus is either true (in focus) or false (out of focus). 22 | Focus(focus bool) 23 | 24 | // Resize handles a resize event. 25 | Resize(size image.Point) 26 | 27 | // Tick returns whether the row need be redrawn. 28 | // It is intended to be called at regular intervals 29 | // in order to drive asynchronous events. 30 | Tick() bool 31 | 32 | // Move handles mouse cursor moving events. 33 | Move(pt image.Point) 34 | 35 | // Click handles mouse button events. 36 | // 37 | // The absolute value of the argument indicates the mouse button. 38 | // A positive value indicates the button was pressed. 39 | // A negative value indicates the button was released. 40 | // 41 | // The first return value is the button ultimately pressed 42 | // (this can differ from the argument button, for example, 43 | // if modifier keys are being held). 44 | // If the button is < 0, the second return value is the clicked address. 45 | Click(pt image.Point, button int) (int, [2]int64) 46 | 47 | // Wheel handles mouse wheel events. 48 | // -y is roll up. 49 | // +y is roll down. 50 | // -x is roll left. 51 | // +x is roll right. 52 | Wheel(pt image.Point, x, y int) 53 | 54 | // Dir handles keyboard directional events. 55 | // 56 | // These events are generated by the arrow keys, 57 | // page up and down keys, and the home and end keys. 58 | // Exactly one of x or y must be non-zero. 59 | // 60 | // If the absolute value is 1, then it is treated as an arrow key 61 | // in the corresponding direction (x-horizontal, y-vertical, 62 | // negative-left/up, positive-right/down). 63 | // If the absolute value is math.MinInt16, it is treated as a home event. 64 | // If the absolute value is math.MathInt16, it is end. 65 | // Otherwise, if the value for y is non-zero it is page up/down. 66 | // Other non-zero values for x are currently ignored. 67 | // 68 | // Dir only handles key press events, not key releases. 69 | Dir(x, y int) 70 | 71 | // Mod handles modifier key state change events. 72 | // 73 | // The absolute value of the argument indicates the modifier key. 74 | // A positive value indicates the key was pressed. 75 | // A negative value indicates the key was released. 76 | Mod(m int) 77 | 78 | // Rune handles typing events. 79 | // 80 | // The argument is a rune indicating the glyph typed 81 | // after interpretation by any system-dependent 82 | // keyboard/layout mapping. 83 | // For example, if the 'a' key is pressed 84 | // while the shift key is held, 85 | // the argument would be the letter 'A'. 86 | // 87 | // If the rune is positive, the event is a key press, 88 | // if negative, a key release. 89 | Rune(r rune) 90 | } 91 | -------------------------------------------------------------------------------- /re1/union_test.go: -------------------------------------------------------------------------------- 1 | package re1 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestUnion(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | res []string 13 | text string 14 | want []int64 15 | }{ 16 | { 17 | name: "one regexp mismatch", 18 | res: []string{"abc"}, 19 | text: "abx", 20 | want: nil, 21 | }, 22 | { 23 | name: "one regexp match", 24 | res: []string{"abc"}, 25 | text: "abc", 26 | want: []int64{0, 3, 0}, 27 | }, 28 | { 29 | name: "one regexp submatch", 30 | res: []string{"((a)(b)(c))"}, 31 | text: "abc", 32 | want: []int64{0, 3, 0, 3, 0, 1, 1, 2, 2, 3, 0}, 33 | }, 34 | { 35 | name: "two regexp mismatch", 36 | res: []string{"a+bc", "d+ef"}, 37 | text: "xxxxx", 38 | want: nil, 39 | }, 40 | { 41 | name: "two regexp first match", 42 | res: []string{"a+bc", "d+ef"}, 43 | text: "aaaaabc", 44 | want: []int64{0, 7, 0}, 45 | }, 46 | { 47 | name: "two regexp first match", 48 | res: []string{"a+bc", "d+ef"}, 49 | text: "aaaaabc", 50 | want: []int64{0, 7, 0}, 51 | }, 52 | { 53 | name: "two regexp first submatch", 54 | res: []string{"(a+)bc", "(d+)ef"}, 55 | text: "aaaaabc", 56 | want: []int64{0, 7, 0, 5, 0}, 57 | }, 58 | { 59 | name: "two regexp second match", 60 | res: []string{"a+bc", "d+ef"}, 61 | text: "dddddef", 62 | want: []int64{0, 7, 1}, 63 | }, 64 | { 65 | name: "two regexp second submatch", 66 | res: []string{"(a+)bc", "(d+)ef"}, 67 | text: "dddddef", 68 | want: []int64{0, 7, 0, 5, 1}, 69 | }, 70 | { 71 | name: "left fewer than right sub-expressions", 72 | res: []string{"a(b+)c", "(d)(e)(f)"}, 73 | text: "abbbbbc", 74 | // (d)(e)(f) has three sub-expressions, 75 | // but a(b+)c was the matching expr, and it only has one, 76 | // so only its one is used. 77 | want: []int64{0, 7, 1, 6, -1, -1, -1, -1, 0}, 78 | }, 79 | { 80 | name: "right fewer than right sub-expressions", 81 | res: []string{"(a)(b)(c)", "d(e+)f"}, 82 | text: "deeeeef", 83 | // (a)(b)(c) has three sub-expressions, 84 | // but d(e+)f was the matching expr, and it only has one, 85 | // so only its one is used. 86 | want: []int64{0, 7, 1, 6, -1, -1, -1, -1, 1}, 87 | }, 88 | { 89 | name: "char class left matches", 90 | res: []string{"[a-z]+", "[0-9]+"}, 91 | text: "abcdefg", 92 | want: []int64{0, 7, 0}, 93 | }, 94 | { 95 | name: "char class right matches", 96 | res: []string{"[a-z]+", "[0-9]+"}, 97 | text: "0123456", 98 | want: []int64{0, 7, 1}, 99 | }, 100 | } 101 | for _, test := range tests { 102 | test := test 103 | t.Run(test.name, func(t *testing.T) { 104 | var res []*Regexp 105 | var inputs []string 106 | for i, r := range test.res { 107 | re, residue, err := New(r, Opts{ID: i}) 108 | if residue != "" || err != nil { 109 | t.Fatalf("New(%q, %d)=_,%q,%v, want _, \"\", nil", 110 | r, i, residue, err) 111 | } 112 | res = append(res, re) 113 | inputs = append(inputs, r) 114 | } 115 | u := Union(res...) 116 | t.Logf("Union(%q)=%q\n", inputs, u.source) 117 | got := u.Find(strings.NewReader(test.text)) 118 | if !reflect.DeepEqual(got, test.want) { 119 | t.Errorf("Union(%q).Find(%q)=%v, want %v", 120 | inputs, test.text, got, test.want) 121 | } 122 | }) 123 | } 124 | } 125 | 126 | func TestUnionNil(t *testing.T) { 127 | if x := Union(); x != nil { 128 | t.Errorf("Union()=%v, want nil", x) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /ui/sheet_test.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/eaburns/T/rope" 10 | ) 11 | 12 | func TestTitle(t *testing.T) { 13 | tests := []struct { 14 | text string 15 | title string 16 | }{ 17 | {text: "", title: ""}, 18 | {text: " ", title: ""}, 19 | {text: " not title", title: ""}, 20 | {text: "''", title: ""}, 21 | {text: "'", title: ""}, 22 | {text: "Hello |", title: "Hello"}, 23 | {text: "/home/test/src/T |", title: "/home/test/src/T"}, 24 | {text: "/home/test/src/T", title: "/home/test/src/T"}, 25 | {text: `'Hello, World!'`, title: "Hello, World!"}, 26 | {text: `'Hello, World!`, title: "Hello, World!"}, // no ' terminator 27 | {text: `'\\'`, title: `\`}, 28 | {text: `'\''`, title: `'`}, 29 | } 30 | 31 | for _, test := range tests { 32 | s := NewSheet(testWin, "") 33 | s.tag.text = rope.New(test.text) 34 | title := s.Title() 35 | if title != test.title { 36 | t.Errorf("got %q, want %q", title, test.title) 37 | } 38 | } 39 | } 40 | 41 | func TestSetTitle(t *testing.T) { 42 | tests := []struct { 43 | text string 44 | title string 45 | want string 46 | }{ 47 | {text: "", title: "", want: ""}, 48 | {text: "/User/T/src/github.com/T", title: "", want: ""}, 49 | {text: "/User/T/src/github.com/T | Del", title: "", want: " | Del"}, 50 | {text: ` | Del`, title: "", want: " | Del"}, 51 | {text: `'' | Del`, title: "", want: " | Del"}, 52 | {text: "", title: "", want: ""}, 53 | {text: "``", title: "", want: ""}, 54 | {text: "'Hello, World' | Del", title: "", want: " | Del"}, 55 | {text: "'Hello, World'| Del", title: "", want: " | Del"}, 56 | 57 | {text: " | Del", title: "/path/to/file", want: "/path/to/file | Del"}, 58 | {text: " | Del", title: "Hello, World!", want: "'Hello, World!' | Del"}, 59 | {text: "'xyz'| Del", title: "1 2 3", want: "'1 2 3' | Del"}, 60 | { 61 | text: "/path/to/file | Del", 62 | title: `'Hello, World!'`, 63 | want: `'\'Hello, World!\'' | Del`, 64 | }, 65 | { 66 | text: "/path/to/file | Del", 67 | title: `'Hello,\World!'`, 68 | want: `'\'Hello,\\World!\'' | Del`, 69 | }, 70 | } 71 | 72 | for _, test := range tests { 73 | s := NewSheet(testWin, "") 74 | s.tag.text = rope.New(test.text) 75 | s.SetTitle(test.title) 76 | if got := s.tag.text.String(); got != test.want { 77 | t.Errorf("(%q).SetTitle(%q) = %q, want %q", 78 | test.text, test.title, got, test.want) 79 | } 80 | } 81 | } 82 | 83 | func TestSheetGet_TextFile(t *testing.T) { 84 | dir := tmpdir() 85 | defer os.RemoveAll(dir) 86 | path := filepath.Join(dir, "file") 87 | 88 | const text = "Hello, World!" 89 | write(path, text) 90 | 91 | sh := NewSheet(testWin, path) 92 | if err := sh.Get(); err != nil { 93 | t.Fatalf("Get()=%v, want nil", err) 94 | } 95 | if s := sh.body.text.String(); s != text { 96 | t.Errorf("body text is %q, want %q", s, text) 97 | } 98 | } 99 | 100 | func TestSheetGet_Dir(t *testing.T) { 101 | dir := tmpdir() 102 | defer os.RemoveAll(dir) 103 | 104 | touch(dir, "c") 105 | touch(dir, "b") 106 | touch(dir, "a") 107 | mkSubDir(dir, "3") 108 | mkSubDir(dir, "2") 109 | mkSubDir(dir, "1") 110 | 111 | sh := NewSheet(testWin, dir) 112 | if err := sh.Get(); err != nil { 113 | t.Fatalf("Get()=%v, want nil", err) 114 | } 115 | const text = "1/\n2/\n3/\na\nb\nc\n" 116 | if s := sh.body.text.String(); s != text { 117 | t.Errorf("body text is %q, want %q", s, text) 118 | } 119 | } 120 | 121 | func TestSheetPut(t *testing.T) { 122 | dir := tmpdir() 123 | defer os.RemoveAll(dir) 124 | path := filepath.Join(dir, "file") 125 | 126 | const text = "Hello, World!" 127 | sh := NewSheet(testWin, path) 128 | sh.SetText(rope.New(text)) 129 | if err := sh.Put(); err != nil { 130 | t.Fatalf("Put()=%v, want nil", err) 131 | } 132 | if s := read(path); s != text { 133 | t.Errorf("read(%q)=%q, want %q", path, s, text) 134 | } 135 | } 136 | 137 | func read(path string) string { 138 | d, err := ioutil.ReadFile(path) 139 | if err != nil { 140 | panic(err) 141 | } 142 | return string(d) 143 | } 144 | 145 | func write(path, text string) { 146 | if err := ioutil.WriteFile(path, []byte(text), os.ModePerm); err != nil { 147 | panic(err) 148 | } 149 | } 150 | 151 | func touch(dir, file string) { 152 | f, err := os.Create(filepath.Join(dir, file)) 153 | if err != nil { 154 | panic(err) 155 | } 156 | if err := f.Close(); err != nil { 157 | panic(err) 158 | } 159 | } 160 | 161 | func tmpdir() string { 162 | dir, err := ioutil.TempDir("", "T_test_") 163 | if err != nil { 164 | panic(err) 165 | } 166 | return dir 167 | } 168 | 169 | func mkSubDir(dir, subdir string) { 170 | if err := os.Mkdir(filepath.Join(dir, subdir), os.ModePerm); err != nil { 171 | panic(err) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /re1/re2_test.go: -------------------------------------------------------------------------------- 1 | package re1 2 | 3 | // These run tests from the RE2 test suite, 4 | // filtering out syntax re1 doesn't support. 5 | // We ignore the match info in the RE2 test suite, 6 | // because it's for single-line mode and first match. 7 | // re1 is always multi-line mode and longest match. 8 | // Instead, we test against the Go regexp package with 9 | // both multi-line mode and longest matching enabled. 10 | // We ignore substring matches; 11 | // regexp and RE2 compute different submatches than re1. 12 | 13 | import ( 14 | "bufio" 15 | "compress/bzip2" 16 | "os" 17 | "reflect" 18 | "regexp" 19 | "strconv" 20 | "strings" 21 | "testing" 22 | 23 | "github.com/eaburns/T/rope" 24 | ) 25 | 26 | func TestRE2Search(t *testing.T) { 27 | runRE2Tests(t, "testdata/re2-search.txt.bz2") 28 | } 29 | 30 | func TestRE2Exhaustive(t *testing.T) { 31 | runRE2Tests(t, "testdata/re2-exhaustive.txt.bz2") 32 | } 33 | 34 | func runRE2Tests(t *testing.T, path string) { 35 | t.Helper() 36 | 37 | f, err := os.Open(path) 38 | if err != nil { 39 | t.Fatal(err.Error()) 40 | } 41 | defer f.Close() 42 | 43 | var strs []string 44 | var inStrings bool 45 | scanner := bufio.NewScanner(bzip2.NewReader(f)) 46 | for scanner.Scan() { 47 | switch line := strings.TrimSpace(scanner.Text()); { 48 | case line == "strings": 49 | strs = strs[:0] 50 | inStrings = true 51 | case line == "regexps": 52 | inStrings = false 53 | case inStrings: 54 | strs = append(strs, mustUnquote(line)) 55 | default: 56 | reStr, err := strconv.Unquote(line) 57 | if err != nil { 58 | // Ignore re2 match info; it's neither multi-line more nor longest. 59 | // Instead we compare to the go regexp directly. 60 | continue 61 | } 62 | if unsupported(reStr) { 63 | continue 64 | } 65 | runRE2TestCase(t, reStr, strs) 66 | } 67 | } 68 | if err := scanner.Err(); err != nil { 69 | t.Fatalf(err.Error()) 70 | } 71 | } 72 | 73 | func runRE2TestCase(t *testing.T, reStr string, strs []string) { 74 | reStr = strings.Replace(reStr, "(?:", "(", -1) 75 | goRegexp := regexp.MustCompile("(?m:" + reStr + ")") 76 | goRegexp.Longest() 77 | r1Regexp, residue, err := New(reStr, Opts{}) 78 | if residue != "" || err != nil { 79 | t.Errorf("New(%q, '/')=_,%q,%v", reStr, residue, err) 80 | return 81 | } 82 | for _, str := range strs { 83 | want := match64(goRegexp, str) 84 | got := r1Regexp.Find(strings.NewReader(str)) 85 | // We only consider the full match, 86 | // because we disagree with regexp and re2 87 | // on what the submatches are. 88 | if got != nil { 89 | got = got[:2] 90 | } 91 | if !reflect.DeepEqual(want, got) { 92 | t.Errorf("%q: Find(%q)=%v, want %v", reStr, str, got, want) 93 | } 94 | 95 | ro := rope.New(str) 96 | got = r1Regexp.FindInRope(ro, 0, ro.Len()) 97 | if got != nil { 98 | got = got[:2] 99 | } 100 | if !reflect.DeepEqual(want, got) { 101 | t.Errorf("%q: FindInRope(%q)=%v, want %v", reStr, str, got, want) 102 | } 103 | } 104 | 105 | } 106 | 107 | func match64(re *regexp.Regexp, str string) []int64 { 108 | var ms []int64 109 | for _, m := range re.FindStringIndex(str) { 110 | ms = append(ms, int64(m)) 111 | } 112 | return ms 113 | } 114 | 115 | var ( 116 | exclude = []string{ 117 | // We don't support flags. 118 | `(?i`, 119 | `(?m`, 120 | `(?s`, 121 | `(?u`, 122 | 123 | // We don't support non-greedy repetition. 124 | `*?`, 125 | `+?`, 126 | `??`, 127 | 128 | // We don't support [[:space:]] and friends. 129 | `[[`, 130 | 131 | // We don't support these Perl character classes. 132 | `\A`, 133 | `\B`, 134 | `\C`, 135 | `\D`, 136 | `\P`, 137 | `\S`, 138 | `\W`, 139 | `\a`, 140 | `\b`, 141 | `\d`, 142 | `\f`, 143 | `\p`, 144 | `\r`, 145 | `\s`, 146 | `\S`, 147 | `\v`, 148 | `\w`, 149 | `\x`, 150 | `\z`, 151 | 152 | // We have a less-permissive charclass grammar. 153 | `[]a]`, 154 | `[-a]`, 155 | `[a-]`, 156 | `[^-a]`, 157 | `[a-b-c]`, 158 | 159 | // We don't support octal. 160 | `\608`, 161 | `\01`, 162 | `\018`, 163 | } 164 | 165 | octal = regexp.MustCompile(`[\][0-7][0-7][0-7]`) 166 | repN = regexp.MustCompile(`\{[0-9,]+\}`) 167 | ) 168 | 169 | func unsupported(re string) bool { 170 | for _, x := range exclude { 171 | if strings.Contains(re, x) { 172 | return true 173 | } 174 | } 175 | return octal.MatchString(re) || repN.MatchString(re) 176 | } 177 | 178 | func parseMatches(line string) [][]int64 { 179 | var matches [][]int64 180 | for _, match := range strings.Split(line, ";") { 181 | var ms []int64 182 | if match == "-" { 183 | matches = append(matches, ms) 184 | continue 185 | } 186 | for _, subMatch := range strings.Split(match, " ") { 187 | if subMatch == "-" { 188 | ms = append(ms, -1, -1) 189 | continue 190 | } 191 | fs := strings.Split(subMatch, "-") 192 | if len(fs) != 2 { 193 | panic("bad submatch range") 194 | } 195 | ms = append(ms, atoi(fs[0]), atoi(fs[1])) 196 | } 197 | matches = append(matches, ms) 198 | } 199 | return matches 200 | } 201 | 202 | func atoi(str string) int64 { 203 | i, err := strconv.Atoi(str) 204 | if err != nil { 205 | panic("bad int " + err.Error()) 206 | } 207 | return int64(i) 208 | } 209 | 210 | func mustUnquote(line string) string { 211 | line, err := strconv.Unquote(line) 212 | if err != nil { 213 | panic(err.Error()) 214 | } 215 | return line 216 | } 217 | -------------------------------------------------------------------------------- /rope/rope_quick_test.go: -------------------------------------------------------------------------------- 1 | package rope 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "testing" 10 | "testing/quick" 11 | "time" 12 | ) 13 | 14 | var quickConfig *quick.Config 15 | 16 | func TestMain(m *testing.M) { 17 | seed := time.Now().Unix() 18 | if s, err := strconv.ParseInt(os.Getenv("QUICK_TEST_SEED"), 10, 64); err == nil { 19 | seed = s 20 | } 21 | fmt.Println("seed", seed) 22 | quickConfig = &quick.Config{ 23 | MaxCount: 1000, 24 | Rand: rand.New(rand.NewSource(seed)), 25 | } 26 | os.Exit(m.Run()) 27 | } 28 | 29 | func TestQuickAppend(t *testing.T) { 30 | err := quick.CheckEqual( 31 | func(ss [][]byte) string { 32 | var accum strings.Builder 33 | for _, s := range ss { 34 | accum.Write(s) 35 | } 36 | return accum.String() 37 | }, 38 | func(ss [][]byte) string { 39 | accum := Empty() 40 | for _, s := range ss { 41 | accum = Append(accum, New(string(s))) 42 | } 43 | return accum.String() 44 | }, 45 | quickConfig) 46 | if err != nil { 47 | t.Error(err) 48 | } 49 | } 50 | 51 | func TestQuickSplit(t *testing.T) { 52 | err := quick.CheckEqual( 53 | func(ss [][]byte, i int) [2]string { 54 | var accum strings.Builder 55 | for _, s := range ss { 56 | accum.Write(s) 57 | } 58 | str := accum.String() 59 | i = randLen(i, len(str)) 60 | return [2]string{str[:i], str[i:]} 61 | }, 62 | func(ss [][]byte, i int) [2]string { 63 | accum := Empty() 64 | for _, s := range ss { 65 | accum = Append(accum, New(string(s))) 66 | } 67 | i = randLen(i, int(accum.Len())) 68 | l, r := Split(accum, int64(i)) 69 | return [2]string{l.String(), r.String()} 70 | }, 71 | quickConfig) 72 | if err != nil { 73 | t.Error(err) 74 | } 75 | } 76 | 77 | func TestQuickDelete(t *testing.T) { 78 | err := quick.CheckEqual( 79 | func(ss [][]byte, start, n int) string { 80 | var accum strings.Builder 81 | for _, s := range ss { 82 | accum.Write(s) 83 | } 84 | str := accum.String() 85 | start = randLen(start, len(str)) 86 | n = randLen(n, len(str)-start) 87 | return str[:start] + str[start+n:] 88 | }, 89 | func(ss [][]byte, start, n int) string { 90 | accum := Empty() 91 | for _, s := range ss { 92 | accum = Append(accum, New(string(s))) 93 | } 94 | start = randLen(start, int(accum.Len())) 95 | n = randLen(n, int(accum.Len())-start) 96 | return Delete(accum, int64(start), int64(n)).String() 97 | }, 98 | quickConfig) 99 | if err != nil { 100 | t.Error(err) 101 | } 102 | } 103 | 104 | func TestQuickInsert(t *testing.T) { 105 | err := quick.CheckEqual( 106 | func(ss [][]byte, ins []byte, i int) string { 107 | var accum strings.Builder 108 | for _, s := range ss { 109 | accum.Write(s) 110 | } 111 | str := accum.String() 112 | i = randLen(i, len(str)) 113 | return str[:i] + string(ins) + str[i:] 114 | }, 115 | func(ss [][]byte, ins []byte, i int) string { 116 | accum := Empty() 117 | for _, s := range ss { 118 | accum = Append(accum, New(string(s))) 119 | } 120 | i = randLen(i, int(accum.Len())) 121 | return Insert(accum, int64(i), New(string(ins))).String() 122 | }, 123 | quickConfig) 124 | if err != nil { 125 | t.Error(err) 126 | } 127 | } 128 | 129 | func TestQuickSlice(t *testing.T) { 130 | err := quick.CheckEqual( 131 | func(ss [][]byte, start, n int) string { 132 | var accum strings.Builder 133 | for _, s := range ss { 134 | accum.Write(s) 135 | } 136 | str := accum.String() 137 | start = randLen(start, len(str)) 138 | n = randLen(n, len(str)-start) 139 | return str[start : start+n] 140 | }, 141 | func(ss [][]byte, start, n int) string { 142 | accum := Empty() 143 | for _, s := range ss { 144 | accum = Append(accum, New(string(s))) 145 | } 146 | start = randLen(start, int(accum.Len())) 147 | n = randLen(n, int(accum.Len())-start) 148 | return Slice(accum, int64(start), int64(start+n)).String() 149 | }, 150 | quickConfig) 151 | if err != nil { 152 | t.Error(err) 153 | } 154 | } 155 | 156 | func TestQuickReadRune(t *testing.T) { 157 | err := quick.CheckEqual( 158 | func(ss []string, start, n int) string { 159 | var accum strings.Builder 160 | for _, s := range ss { 161 | accum.WriteString(s) 162 | } 163 | return accum.String() 164 | }, 165 | func(ss []string, start, n int) string { 166 | accum := Empty() 167 | for _, s := range ss { 168 | accum = Append(accum, New(s)) 169 | } 170 | str, err := readAllRune(NewReader(accum)) 171 | if err != nil { 172 | panic(err.Error()) 173 | } 174 | return str 175 | }, 176 | quickConfig) 177 | if err != nil { 178 | t.Error(err) 179 | } 180 | } 181 | 182 | func TestQuickReverseReadRune(t *testing.T) { 183 | err := quick.CheckEqual( 184 | func(ss []string, start, n int) string { 185 | var accum strings.Builder 186 | for _, s := range ss { 187 | accum.WriteString(s) 188 | } 189 | return reverseRunes(accum.String()) 190 | }, 191 | func(ss []string, start, n int) string { 192 | accum := Empty() 193 | for _, s := range ss { 194 | accum = Append(accum, New(s)) 195 | } 196 | str, err := readAllRune(NewReverseReader(accum)) 197 | if err != nil { 198 | panic(err.Error()) 199 | } 200 | return str 201 | }, 202 | quickConfig) 203 | if err != nil { 204 | t.Error(err) 205 | } 206 | } 207 | 208 | func randLen(i, max int) int { 209 | if max == 0 { 210 | return 0 211 | } 212 | if i < 0 { 213 | i = -i 214 | } 215 | return i % max 216 | } 217 | -------------------------------------------------------------------------------- /ui/cmd.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | "sync" 10 | "unicode" 11 | "unicode/utf8" 12 | 13 | "github.com/eaburns/T/edit" 14 | "github.com/eaburns/T/re1" 15 | "github.com/eaburns/T/rope" 16 | ) 17 | 18 | // execCmd handles 2-click text. 19 | // c is non-nil 20 | // s may be nil 21 | func execCmd(c *Col, s *Sheet, text string) error { 22 | switch cmd, _ := splitCmd(text); cmd { 23 | case "Del": 24 | if s == nil { 25 | c.win.Del(c) 26 | return nil 27 | } 28 | for _, r := range c.rows { 29 | if getSheet(r) == s { 30 | c.Del(r) 31 | } 32 | } 33 | 34 | case "NewCol": 35 | c.win.Add() 36 | 37 | case "NewRow": 38 | c.Add(NewSheet(c.win, "")) 39 | 40 | case "Get": 41 | if s != nil { 42 | return s.Get() 43 | } 44 | 45 | case "Put": 46 | if s != nil { 47 | return s.Put() 48 | } 49 | 50 | case "Copy": 51 | if s != nil { 52 | return s.body.Copy() 53 | } 54 | 55 | case "Cut": 56 | if s != nil { 57 | return s.body.Cut() 58 | } 59 | 60 | case "Paste": 61 | if s != nil { 62 | return s.body.Paste() 63 | } 64 | 65 | default: 66 | if text == "" { 67 | return nil 68 | } 69 | if isDir, err := openDir(c, s, text); isDir { 70 | return err 71 | } 72 | go func() { 73 | if err := shellCmd(c.win, text); err != nil { 74 | c.win.OutputString(err.Error()) 75 | } 76 | }() 77 | return nil 78 | } 79 | return nil 80 | } 81 | 82 | func shellCmd(w *Win, text string) error { 83 | // TODO: set 2-click shell command CWD to the sheet's directory. 84 | // If executed from outside of a sheet, then don't set it specifically. 85 | cmd := exec.Command("sh", "-c", text) 86 | stderr, err := cmd.StderrPipe() 87 | if err != nil { 88 | return err 89 | } 90 | stdout, err := cmd.StdoutPipe() 91 | if err != nil { 92 | stderr.Close() 93 | return err 94 | } 95 | if err := cmd.Start(); err != nil { 96 | stderr.Close() 97 | stdout.Close() 98 | return err 99 | } 100 | var wg sync.WaitGroup 101 | wg.Add(2) 102 | go pipeOutput(&wg, w, stdout) 103 | go pipeOutput(&wg, w, stderr) 104 | wg.Wait() 105 | return cmd.Wait() 106 | } 107 | 108 | func pipeOutput(wg *sync.WaitGroup, w *Win, pipe io.Reader) { 109 | defer wg.Done() 110 | var buf [4096]byte 111 | for { 112 | n, err := pipe.Read(buf[:]) 113 | if n > 0 { 114 | w.OutputBytes(buf[:n]) 115 | } 116 | if err != nil { 117 | return 118 | } 119 | } 120 | } 121 | 122 | func lookText(c *Col, s *Sheet, text string) error { 123 | if text == "" { 124 | return nil 125 | } 126 | 127 | path, err := abs(s, text) 128 | if err != nil { 129 | setLook(c, s, text) 130 | return nil 131 | } 132 | 133 | if focusSheet(c.win, path) { 134 | return nil 135 | } 136 | if focusSheet(c.win, ensureTrailingSlash(path)) { 137 | return nil 138 | } 139 | 140 | f, err := os.Open(path) 141 | if err != nil { 142 | setLook(c, s, text) 143 | return nil 144 | } 145 | defer f.Close() 146 | s = NewSheet(c.win, path) 147 | if err := get(s, f); err != nil { 148 | return err 149 | } 150 | c.Add(s) 151 | return nil 152 | } 153 | 154 | func focusSheet(w *Win, title string) bool { 155 | for _, c := range w.cols { 156 | for _, r := range c.rows { 157 | s, ok := r.(*Sheet) 158 | if !ok || s.Title() != title { 159 | continue 160 | } 161 | setWinFocus(w, c) 162 | setColFocus(c, s) 163 | return true 164 | } 165 | } 166 | return false 167 | } 168 | 169 | func setLook(c *Col, s *Sheet, text string) { 170 | // TODO: 3-clicking a non-file should highlight matches in the sheet. 171 | } 172 | 173 | func openDir(c *Col, s *Sheet, path string) (bool, error) { 174 | var err error 175 | if path, err = abs(s, path); err != nil { 176 | return false, nil 177 | } 178 | 179 | f, err := os.Open(path) 180 | if err != nil { 181 | return false, nil 182 | } 183 | defer f.Close() 184 | if st, err := f.Stat(); err != nil || !st.IsDir() { 185 | return st.IsDir(), err 186 | } 187 | 188 | if s != nil { 189 | title := s.Title() 190 | if r, _ := utf8.DecodeLastRuneInString(title); r == os.PathSeparator { 191 | if rel, err := filepath.Rel(title, path); err == nil { 192 | return true, addDir(s, f, rel) 193 | } 194 | } 195 | } 196 | 197 | s = NewSheet(c.win, path) 198 | if err := get(s, f); err != nil { 199 | return true, err 200 | } 201 | c.Add(s) 202 | return true, nil 203 | } 204 | 205 | func addDir(s *Sheet, f *os.File, rel string) error { 206 | r, err := readFromDir(rel, f) 207 | if err != nil { 208 | return err 209 | } 210 | rel = ensureTrailingSlash(rel) 211 | 212 | at := s.body.text.Len() 213 | if addr, ok := findDir(s.body.text, rel); ok { 214 | at = addr[1] 215 | } else { 216 | r = rope.Append(rope.New(rel+"\n"), r) 217 | } 218 | s.body.Change(edit.Diffs{{At: [2]int64{at, at}, Text: r}}) 219 | showAddr(s.body, at) 220 | return nil 221 | } 222 | 223 | func findDir(r rope.Rope, dir string) ([2]int64, bool) { 224 | match := re1.Escape(dir) 225 | match = strings.Replace(match, "/", `\/`, -1) 226 | addr, err := edit.Addr([2]int64{0, r.Len()}, "/^"+match+"$\\n", r) 227 | if err != nil { 228 | return [2]int64{}, false 229 | } 230 | return addr, true 231 | } 232 | 233 | func abs(s *Sheet, path string) (string, error) { 234 | if filepath.IsAbs(path) { 235 | return path, nil 236 | } 237 | if s != nil { 238 | return filepath.Join(filepath.Dir(s.Title()), path), nil 239 | } 240 | return filepath.Abs(path) 241 | } 242 | 243 | func splitCmd(exec string) (string, string) { 244 | exec = strings.TrimSpace(exec) 245 | i := strings.IndexFunc(exec, unicode.IsSpace) 246 | if i < 0 { 247 | return exec, "" 248 | } 249 | return exec[:i], strings.TrimSpace(exec[i:]) 250 | } 251 | -------------------------------------------------------------------------------- /ui/cmd_test.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/eaburns/T/rope" 9 | ) 10 | 11 | func TestCmd_empty(t *testing.T) { 12 | var ( 13 | w = newTestWin() 14 | c = w.cols[0] 15 | ) 16 | if err := execCmd(c, nil, ""); err != nil { 17 | t.Fatalf("execCmd failed: %v", err) 18 | } 19 | if len(c.rows) > 1 { 20 | t.Errorf("len(c.rows)=%d, wanted %d", len(c.rows), 1) 21 | } 22 | } 23 | 24 | func TestCmd_openDir(t *testing.T) { 25 | dir := tmpdir() 26 | defer os.RemoveAll(dir) 27 | touch(dir, "a") 28 | touch(dir, "b") 29 | touch(dir, "c") 30 | mkSubDir(dir, "1") 31 | mkSubDir(dir, "2") 32 | mkSubDir(dir, "3") 33 | 34 | const title = "/Users/testuser/some_non_directory_file.go" 35 | var ( 36 | w = newTestWin() 37 | c = w.cols[0] 38 | s = NewSheet(w, title) 39 | ) 40 | c.Add(s) 41 | 42 | if err := execCmd(c, s, dir); err != nil { 43 | t.Fatalf("execCmd(.., [%q], %q) failed with %v", title, dir, err) 44 | } 45 | 46 | var dirSheet *Sheet 47 | for _, row := range c.rows { 48 | if s, ok := row.(*Sheet); ok && s.Title() == ensureTrailingSlash(dir) { 49 | dirSheet = s 50 | break 51 | } 52 | } 53 | 54 | if dirSheet == nil { 55 | t.Fatalf("no new sheet") 56 | } 57 | 58 | const want = "1/\n2/\n3/\na\nb\nc\n" 59 | if s := dirSheet.body.text.String(); s != want { 60 | t.Errorf("body is %q, want %q\n", s, want) 61 | } 62 | } 63 | 64 | func TestCmd_addDir(t *testing.T) { 65 | dir := tmpdir() 66 | defer os.RemoveAll(dir) 67 | mkSubDir(dir, "sub") 68 | sub := filepath.Join(dir, "sub") 69 | touch(sub, "a") 70 | touch(sub, "b") 71 | touch(sub, "c") 72 | mkSubDir(sub, "1") 73 | mkSubDir(sub, "2") 74 | mkSubDir(sub, "3") 75 | 76 | tests := []struct { 77 | name string 78 | title string 79 | body string 80 | exec string 81 | want string 82 | }{ 83 | { 84 | name: "empty body", 85 | title: dir, 86 | body: "", 87 | exec: sub, 88 | want: `sub/ 89 | sub/1/ 90 | sub/2/ 91 | sub/3/ 92 | sub/a 93 | sub/b 94 | sub/c 95 | `, 96 | }, 97 | { 98 | name: "append to body", 99 | title: dir, 100 | body: `α/ 101 | β/ 102 | ξ/ 103 | `, 104 | exec: sub, 105 | want: `α/ 106 | β/ 107 | ξ/ 108 | sub/ 109 | sub/1/ 110 | sub/2/ 111 | sub/3/ 112 | sub/a 113 | sub/b 114 | sub/c 115 | `, 116 | }, 117 | { 118 | name: "insert into body", 119 | title: dir, 120 | body: `α/ 121 | β/ 122 | sub/ 123 | ξ/ 124 | `, 125 | exec: sub, 126 | want: `α/ 127 | β/ 128 | sub/ 129 | sub/1/ 130 | sub/2/ 131 | sub/3/ 132 | sub/a 133 | sub/b 134 | sub/c 135 | ξ/ 136 | `, 137 | }, 138 | { 139 | name: "rel-path", 140 | title: dir, 141 | body: `α/ 142 | β/ 143 | sub/ 144 | ξ/ 145 | `, 146 | exec: "sub", 147 | want: `α/ 148 | β/ 149 | sub/ 150 | sub/1/ 151 | sub/2/ 152 | sub/3/ 153 | sub/a 154 | sub/b 155 | sub/c 156 | ξ/ 157 | `, 158 | }, 159 | } 160 | 161 | for _, test := range tests { 162 | test := test 163 | t.Run(test.name, func(t *testing.T) { 164 | var ( 165 | w = newTestWin() 166 | c = w.cols[0] 167 | s = NewSheet(w, ensureTrailingSlash(test.title)) 168 | ) 169 | s.SetText(rope.New(test.body)) 170 | c.Add(s) 171 | if err := execCmd(c, s, test.exec); err != nil { 172 | t.Fatalf("execCmd(.., [%q], %q) failed with %v", test.title, test.exec, err) 173 | } 174 | if str := s.body.text.String(); str != test.want { 175 | t.Errorf("body=%q, want %q", str, test.want) 176 | } 177 | }) 178 | } 179 | } 180 | 181 | func TestLook_empty(t *testing.T) { 182 | var ( 183 | w = newTestWin() 184 | c = w.cols[0] 185 | ) 186 | if err := lookText(c, nil, ""); err != nil { 187 | t.Fatalf("execCmd failed: %v", err) 188 | } 189 | if len(c.rows) > 1 { 190 | t.Errorf("len(c.rows)=%d, wanted %d", len(c.rows), 1) 191 | } 192 | } 193 | 194 | func TestLook_newSheetAbsolute(t *testing.T) { 195 | dir := tmpdir() 196 | defer os.RemoveAll(dir) 197 | const text = "Hello, World!" 198 | path := filepath.Join(dir, "a") 199 | write(path, text) 200 | 201 | var ( 202 | w = newTestWin() 203 | c = w.cols[0] 204 | ) 205 | 206 | if err := lookText(c, nil, path); err != nil { 207 | t.Fatalf("lookText failed: %v", err) 208 | } 209 | 210 | if len(c.rows) != 2 { 211 | t.Fatalf("%d rows, wanted 2", len(c.rows)) 212 | } 213 | s, ok := c.rows[1].(*Sheet) 214 | if !ok { 215 | t.Fatalf("row[1] is type %T, wanted *Sheet", c.rows[1]) 216 | } 217 | if s.Title() != path { 218 | t.Errorf("sheet title is %q, wanted %q", s.Title(), path) 219 | } 220 | if txt := s.body.text.String(); txt != text { 221 | t.Errorf("sheet body is %q, wanted %q", txt, text) 222 | } 223 | } 224 | 225 | func TestLook_newSheetRelative(t *testing.T) { 226 | dir := tmpdir() 227 | defer os.RemoveAll(dir) 228 | const text = "Hello, World!" 229 | path := filepath.Join(dir, "a") 230 | write(path, text) 231 | 232 | var ( 233 | w = newTestWin() 234 | c = w.cols[0] 235 | s0 = NewSheet(w, ensureTrailingSlash(dir)) 236 | ) 237 | c.Add(s0) 238 | 239 | if err := lookText(c, s0, "a"); err != nil { 240 | t.Fatalf("lookText failed: %v", err) 241 | } 242 | 243 | if len(c.rows) != 3 { 244 | t.Fatalf("%d rows, wanted 3", len(c.rows)) 245 | } 246 | s, ok := c.rows[2].(*Sheet) 247 | if !ok { 248 | t.Fatalf("row[2] is type %T, wanted *Sheet", c.rows[2]) 249 | } 250 | if s.Title() != path { 251 | t.Errorf("sheet title is %q, wanted %q", s.Title(), path) 252 | } 253 | if txt := s.body.text.String(); txt != text { 254 | t.Errorf("sheet body is %q, wanted %q", txt, text) 255 | } 256 | } 257 | 258 | func newTestCol(w *Win) *Col { 259 | c := NewCol(w) 260 | w.cols = append(w.cols, c) 261 | return c 262 | } 263 | 264 | func TestLook_focusExistingSheet(t *testing.T) { 265 | dir := tmpdir() 266 | defer os.RemoveAll(dir) 267 | const text = "Hello, World!" 268 | path := filepath.Join(dir, "a") 269 | 270 | var ( 271 | w = newTestWin() 272 | s = NewSheet(w, path) 273 | ) 274 | // Put the to-be-focused sheet in a new, out-of-focus column. 275 | c := NewCol(w) 276 | w.cols = append(w.cols, c) 277 | c.Add(s) 278 | // Focus the 0th row, which is the column background. 279 | c.Row.Focus(false) 280 | c.Row = c.rows[0] 281 | c.Row.Focus(true) 282 | 283 | if w.Col == c { 284 | t.Fatalf("bad setup, c sholud be out-of-focus") 285 | } 286 | if c.Row == s { 287 | t.Fatalf("bad setup, s sholud be out-of-focus") 288 | } 289 | 290 | if err := lookText(c, nil, path); err != nil { 291 | t.Fatalf("lookText failed: %v", err) 292 | } 293 | 294 | if len(c.rows) != 2 { 295 | t.Fatalf("%d rows, wanted 2", len(c.rows)) 296 | } 297 | s, ok := c.rows[1].(*Sheet) 298 | if !ok { 299 | t.Fatalf("row[1] is type %T, wanted *Sheet", c.rows[2]) 300 | } 301 | if s.Title() != path { 302 | t.Errorf("sheet title is %q, wanted %q", s.Title(), path) 303 | } 304 | // Don't test the win body. It was never "gotten", so it's just empty. 305 | if w.Col != c { 306 | t.Errorf("col not focused, wanted it to be focused") 307 | } 308 | if c.Row != s { 309 | t.Errorf("sheet not focused, wanted it to be focused") 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // T is a text editor. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "flag" 7 | "image" 8 | "image/draw" 9 | "log" 10 | "math" 11 | "os" 12 | "runtime/pprof" 13 | "time" 14 | 15 | "github.com/eaburns/T/ui" 16 | "golang.org/x/exp/shiny/driver/gldriver" 17 | "golang.org/x/exp/shiny/screen" 18 | "golang.org/x/image/math/f64" 19 | "golang.org/x/mobile/event/key" 20 | "golang.org/x/mobile/event/lifecycle" 21 | "golang.org/x/mobile/event/mouse" 22 | "golang.org/x/mobile/event/paint" 23 | "golang.org/x/mobile/event/size" 24 | ) 25 | 26 | const tickRate = 20 * time.Millisecond 27 | 28 | var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") 29 | 30 | func main() { 31 | gldriver.Main(func(scr screen.Screen) { 32 | flag.Parse() 33 | if *cpuprofile != "" { 34 | f, err := os.Create(*cpuprofile) 35 | if err != nil { 36 | log.Fatal("could not create CPU profile: ", err) 37 | } 38 | if err := pprof.StartCPUProfile(f); err != nil { 39 | log.Fatal("could not start CPU profile: ", err) 40 | } 41 | defer pprof.StopCPUProfile() 42 | } 43 | <-newWindow(context.Background(), scr).done 44 | }) 45 | } 46 | 47 | type win struct { 48 | ctx context.Context 49 | cancel func() 50 | done chan struct{} 51 | 52 | dpi float32 53 | size image.Point 54 | screen.Window 55 | 56 | win *ui.Win 57 | } 58 | 59 | func newWindow(ctx context.Context, scr screen.Screen) *win { 60 | window, err := scr.NewWindow(nil) 61 | if err != nil { 62 | panic(err) 63 | } 64 | var e size.Event 65 | for { 66 | var ok bool 67 | if e, ok = window.NextEvent().(size.Event); ok { 68 | break 69 | } 70 | } 71 | 72 | ctx, cancel := context.WithCancel(ctx) 73 | w := &win{ 74 | ctx: ctx, 75 | cancel: cancel, 76 | done: make(chan struct{}), 77 | dpi: float32(e.PixelsPerPt) * 72.0, 78 | size: e.Size(), 79 | Window: window, 80 | } 81 | w.win = ui.NewWin(w.dpi) 82 | w.win.Resize(w.size) 83 | 84 | go tick(w) 85 | go poll(scr, w) 86 | return w 87 | } 88 | 89 | func (w *win) Release() { w.cancel() } 90 | 91 | type done struct{} 92 | 93 | func tick(w *win) { 94 | ticker := time.NewTicker(tickRate) 95 | for { 96 | select { 97 | case <-ticker.C: 98 | w.Send(time.Now()) 99 | case <-w.ctx.Done(): 100 | ticker.Stop() 101 | w.Send(done{}) 102 | return 103 | } 104 | } 105 | } 106 | 107 | func poll(scr screen.Screen, w *win) { 108 | var mods [4]bool 109 | dirty := true 110 | buf, tex := bufTex(scr, w.size) 111 | 112 | for { 113 | switch e := w.NextEvent().(type) { 114 | case done: 115 | buf.Release() 116 | tex.Release() 117 | w.Window.Release() 118 | close(w.done) 119 | return 120 | 121 | case time.Time: 122 | if w.win.Tick() { 123 | w.Send(paint.Event{}) 124 | } 125 | 126 | case lifecycle.Event: 127 | if e.To == lifecycle.StageDead { 128 | w.cancel() 129 | continue 130 | } 131 | w.win.Focus(e.To == lifecycle.StageFocused) 132 | 133 | case size.Event: 134 | if e.Size() == image.ZP { 135 | w.cancel() 136 | continue 137 | } 138 | w.size = e.Size() 139 | w.win.Resize(w.size) 140 | dirty = true 141 | if b := tex.Bounds(); b.Dx() < w.size.X || b.Dy() < w.size.Y { 142 | tex.Release() 143 | buf.Release() 144 | buf, tex = bufTex(scr, w.size.Mul(2)) 145 | } 146 | 147 | case paint.Event: 148 | rect := image.Rectangle{Max: w.size} 149 | img := buf.RGBA().SubImage(rect).(*image.RGBA) 150 | w.win.Draw(dirty, img) 151 | dirty = false 152 | tex.Upload(image.ZP, buf, buf.Bounds()) 153 | w.Draw(f64.Aff3{ 154 | 1, 0, 0, 155 | 0, 1, 0, 156 | }, tex, tex.Bounds(), draw.Src, nil) 157 | w.Publish() 158 | 159 | case mouse.Event: 160 | mouseEvent(w, e) 161 | 162 | case key.Event: 163 | mods = keyEvent(w, mods, e) 164 | } 165 | } 166 | } 167 | 168 | func mouseEvent(w *win, e mouse.Event) { 169 | switch pt := image.Pt(int(e.X), int(e.Y)); { 170 | case e.Button == mouse.ButtonWheelUp: 171 | w.win.Wheel(pt, 0, 1) 172 | 173 | case e.Button == mouse.ButtonWheelDown: 174 | w.win.Wheel(pt, 0, -1) 175 | 176 | case e.Button == mouse.ButtonWheelLeft: 177 | w.win.Wheel(pt, -1, 0) 178 | 179 | case e.Button == mouse.ButtonWheelRight: 180 | w.win.Wheel(pt, 1, 0) 181 | 182 | case e.Direction == mouse.DirNone: 183 | w.win.Move(pt) 184 | 185 | case e.Direction == mouse.DirPress: 186 | w.win.Click(pt, int(e.Button)) 187 | 188 | case e.Direction == mouse.DirRelease: 189 | w.win.Click(pt, -int(e.Button)) 190 | 191 | case e.Direction == mouse.DirStep: 192 | w.win.Click(pt, int(e.Button)) 193 | w.win.Click(pt, -int(e.Button)) 194 | } 195 | } 196 | 197 | func keyEvent(w *win, mods [4]bool, e key.Event) [4]bool { 198 | if e.Direction == key.DirNone { 199 | e.Direction = key.DirPress 200 | } 201 | if e.Direction == key.DirPress && dirKeyCode[e.Code] { 202 | dirKey(w, e) 203 | return mods 204 | } 205 | 206 | switch { 207 | case e.Code == key.CodeDeleteBackspace: 208 | e.Rune = '\b' 209 | case e.Code == key.CodeDeleteForward: 210 | e.Rune = 0x7f 211 | case e.Rune == '\r': 212 | e.Rune = '\n' 213 | } 214 | if e.Rune > 0 { 215 | if e.Direction == key.DirPress { 216 | w.win.Rune(e.Rune) 217 | } 218 | return mods 219 | } 220 | 221 | return modKey(w, mods, e) 222 | } 223 | 224 | var dirKeyCode = map[key.Code]bool{ 225 | key.CodeUpArrow: true, 226 | key.CodeDownArrow: true, 227 | key.CodeLeftArrow: true, 228 | key.CodeRightArrow: true, 229 | key.CodePageUp: true, 230 | key.CodePageDown: true, 231 | key.CodeHome: true, 232 | key.CodeEnd: true, 233 | } 234 | 235 | func dirKey(w *win, e key.Event) { 236 | switch e.Code { 237 | case key.CodeUpArrow: 238 | w.win.Dir(0, -1) 239 | 240 | case key.CodeDownArrow: 241 | w.win.Dir(0, 1) 242 | 243 | case key.CodeLeftArrow: 244 | w.win.Dir(-1, 0) 245 | 246 | case key.CodeRightArrow: 247 | w.win.Dir(1, 0) 248 | 249 | case key.CodePageUp: 250 | w.win.Dir(0, -2) 251 | 252 | case key.CodePageDown: 253 | w.win.Dir(0, 2) 254 | 255 | case key.CodeHome: 256 | w.win.Dir(0, math.MinInt16) 257 | 258 | case key.CodeEnd: 259 | w.win.Dir(0, math.MaxInt16) 260 | 261 | default: 262 | panic("impossible") 263 | } 264 | } 265 | 266 | func modKey(w *win, mods [4]bool, e key.Event) [4]bool { 267 | var newMods [4]bool 268 | if e.Modifiers&key.ModShift != 0 { 269 | newMods[1] = true 270 | } 271 | if e.Modifiers&key.ModAlt != 0 { 272 | newMods[2] = true 273 | } 274 | if e.Modifiers&key.ModMeta != 0 || 275 | e.Modifiers&key.ModControl != 0 { 276 | newMods[3] = true 277 | } 278 | for i := 0; i < len(newMods); i++ { 279 | if newMods[i] != mods[i] { 280 | m := i 281 | if !newMods[i] { 282 | m = -m 283 | } 284 | w.win.Mod(m) 285 | mods = newMods 286 | break 287 | } 288 | } 289 | return mods 290 | } 291 | 292 | func bufTex(scr screen.Screen, sz image.Point) (screen.Buffer, screen.Texture) { 293 | buf, err := scr.NewBuffer(sz) 294 | if err != nil { 295 | panic(err) 296 | } 297 | tex, err := scr.NewTexture(sz) 298 | if err != nil { 299 | panic(err) 300 | } 301 | return buf, tex 302 | } 303 | -------------------------------------------------------------------------------- /re1/rope_test.go: -------------------------------------------------------------------------------- 1 | package re1 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "unicode/utf8" 7 | 8 | "github.com/eaburns/T/rope" 9 | ) 10 | 11 | func TestFindInRope(t *testing.T) { 12 | tests := []ropeTest{ 13 | { 14 | re: "", 15 | cases: []ropeTestCase{ 16 | {str: "", s: 0, e: 0, want: []string{""}}, 17 | {str: "z", s: 0, e: 0, want: []string{""}}, 18 | {str: "z", s: 1, e: 1, want: []string{""}}, 19 | {str: "z", s: 0, e: 1, want: []string{""}}, 20 | }, 21 | }, 22 | { 23 | re: "a", 24 | cases: []ropeTestCase{ 25 | {str: "", want: nil}, 26 | {str: "z", s: 0, e: 1, want: nil}, 27 | {str: "a", s: 0, e: 0, want: nil}, 28 | {str: "a", s: 1, e: 1, want: nil}, 29 | {str: "a", s: 0, e: 1, want: []string{"a"}}, 30 | }, 31 | }, 32 | { 33 | re: "abc", 34 | cases: []ropeTestCase{ 35 | {str: "", want: nil}, 36 | {str: "z", s: 0, e: 1, want: nil}, 37 | {str: "abc", s: 0, e: 0, want: nil}, 38 | {str: "abc", s: 0, e: 1, want: nil}, 39 | {str: "abc", s: 0, e: 2, want: nil}, 40 | {str: "abc", s: 0, e: 3, want: []string{"abc"}}, 41 | {str: "abc", s: 1, e: 1, want: nil}, 42 | {str: "abc", s: 1, e: 2, want: nil}, 43 | {str: "abc", s: 1, e: 3, want: nil}, 44 | {str: "abc", s: 2, e: 2, want: nil}, 45 | {str: "abc", s: 2, e: 3, want: nil}, 46 | {str: "abc", s: 3, e: 3, want: nil}, 47 | }, 48 | }, 49 | { 50 | re: "^abc", 51 | cases: []ropeTestCase{ 52 | {str: "abc", s: 0, e: 3, want: []string{"abc"}}, 53 | {str: "\nabc", s: 1, e: 4, want: []string{"abc"}}, 54 | {str: "xabc", s: 1, e: 4, want: nil}, 55 | {str: "xabc\nabc", s: 0, e: 8, want: []string{"abc"}}, 56 | }, 57 | }, 58 | { 59 | re: "abc$", 60 | cases: []ropeTestCase{ 61 | {str: "abc", s: 0, e: 3, want: []string{"abc"}}, 62 | {str: "abc\n", s: 0, e: 3, want: []string{"abc"}}, 63 | {str: "abcx", s: 0, e: 3, want: nil}, 64 | {str: "abcxabc\n", s: 0, e: 8, want: []string{"abc"}}, 65 | {str: "abcxabc", s: 0, e: 7, want: []string{"abc"}}, 66 | }, 67 | }, 68 | { 69 | re: "^abc$", 70 | cases: []ropeTestCase{ 71 | {str: "abc", s: 0, e: 3, want: []string{"abc"}}, 72 | {str: "abc\n", s: 0, e: 3, want: []string{"abc"}}, 73 | {str: "\nabc", s: 1, e: 4, want: []string{"abc"}}, 74 | {str: "xabc", s: 1, e: 4, want: nil}, 75 | {str: "abcx", s: 0, e: 3, want: nil}, 76 | {str: "xabc\n", s: 1, e: 4, want: nil}, 77 | {str: "\nabcx", s: 1, e: 4, want: nil}, 78 | }, 79 | }, 80 | { 81 | re: "a*", 82 | cases: []ropeTestCase{ 83 | {str: "aaa", s: 0, e: 0, want: []string{""}}, 84 | {str: "aaa", s: 0, e: 1, want: []string{"a"}}, 85 | {str: "aaa", s: 0, e: 2, want: []string{"aa"}}, 86 | {str: "aaa", s: 0, e: 3, want: []string{"aaa"}}, 87 | {str: "aaa", s: 1, e: 3, want: []string{"aa"}}, 88 | {str: "aaa", s: 2, e: 3, want: []string{"a"}}, 89 | {str: "aaa", s: 3, e: 3, want: []string{""}}, 90 | {str: "aaa", s: 1, e: 1, want: []string{""}}, 91 | {str: "aaa", s: 2, e: 2, want: []string{""}}, 92 | }, 93 | }, 94 | } 95 | for _, test := range append(ropeFromFindTests(findTests), tests...) { 96 | runRopeTest(t, test, Opts{}) 97 | } 98 | } 99 | 100 | func TestFindReverseInRope(t *testing.T) { 101 | tests := []ropeTest{ 102 | { 103 | // abcX would match, but X isn't in [s,e). 104 | re: "abcX", 105 | cases: []ropeTestCase{ 106 | {str: "abcX", s: 0, e: 3, want: nil}, 107 | }, 108 | }, 109 | { 110 | re: "abc$", 111 | cases: []ropeTestCase{ 112 | {str: "abc", s: 0, e: 3, want: []string{"abc"}}, 113 | {str: "abc\n", s: 0, e: 3, want: []string{"abc"}}, 114 | {str: "abcx", s: 0, e: 3, want: nil}, 115 | }, 116 | }, 117 | { 118 | re: "^abc", 119 | cases: []ropeTestCase{ 120 | {str: "abc", s: 0, e: 3, want: []string{"abc"}}, 121 | {str: "\nabc", s: 1, e: 4, want: []string{"abc"}}, 122 | {str: "xabc", s: 1, e: 4, want: nil}, 123 | }, 124 | }, 125 | { 126 | re: "^abc$", 127 | cases: []ropeTestCase{ 128 | {str: "abc", s: 0, e: 3, want: []string{"abc"}}, 129 | {str: "\nabc", s: 1, e: 4, want: []string{"abc"}}, 130 | {str: "\nabc", s: 0, e: 4, want: []string{"abc"}}, 131 | {str: "abc\n", s: 0, e: 3, want: []string{"abc"}}, 132 | {str: "abc\n", s: 0, e: 4, want: []string{"abc"}}, 133 | {str: "\nabc\n", s: 1, e: 4, want: []string{"abc"}}, 134 | {str: "\nabc\n", s: 0, e: 4, want: []string{"abc"}}, 135 | {str: "\nabc\n", s: 1, e: 5, want: []string{"abc"}}, 136 | {str: "\nabc\n", s: 0, e: 5, want: []string{"abc"}}, 137 | {str: "xabc\n", s: 1, e: 4, want: nil}, 138 | {str: "xabc\n", s: 0, e: 4, want: nil}, 139 | {str: "xabc\n", s: 0, e: 5, want: nil}, 140 | {str: "xabc\n", s: 1, e: 5, want: nil}, 141 | {str: "\nabcx", s: 1, e: 4, want: nil}, 142 | {str: "\nabcx", s: 0, e: 4, want: nil}, 143 | {str: "\nabcx", s: 0, e: 5, want: nil}, 144 | {str: "\nabcx", s: 1, e: 5, want: nil}, 145 | }, 146 | }, 147 | { 148 | re: "^$", 149 | cases: []ropeTestCase{ 150 | {str: "", s: 0, e: 0, want: []string{""}}, 151 | {str: "\n", s: 0, e: 1, want: []string{""}}, 152 | {str: "abc\n\nxyz", s: 4, e: 8, want: []string{""}}, 153 | {str: "abc\n\nxyz", s: 0, e: 5, want: []string{""}}, 154 | }, 155 | }, 156 | { 157 | re: "(foo)(bar)", 158 | cases: []ropeTestCase{ 159 | {str: "foobarbaz", s: 0, e: 7, want: []string{"foobar", "foo", "bar"}}, 160 | }, 161 | }, 162 | { 163 | re: "a+", 164 | cases: []ropeTestCase{ 165 | {str: "aaa", s: 0, e: 0, want: nil}, 166 | {str: "aaa", s: 0, e: 1, want: []string{"a"}}, 167 | {str: "aaa", s: 0, e: 2, want: []string{"aa"}}, 168 | {str: "aaa", s: 0, e: 3, want: []string{"aaa"}}, 169 | {str: "aaa", s: 1, e: 3, want: []string{"aa"}}, 170 | {str: "aaa", s: 2, e: 3, want: []string{"a"}}, 171 | {str: "aaa", s: 3, e: 3, want: nil}, 172 | {str: "aaaxyz", s: 0, e: 6, want: []string{"aaa"}}, 173 | {str: "aaaxyz", s: 1, e: 6, want: []string{"aa"}}, 174 | {str: "aaaxyz", s: 2, e: 6, want: []string{"a"}}, 175 | {str: "aaaxyz", s: 3, e: 6, want: nil}, 176 | }, 177 | }, 178 | } 179 | for _, test := range append(ropeFromFindTests(findReverseTests), tests...) { 180 | runRopeTest(t, test, Opts{Reverse: true}) 181 | } 182 | 183 | } 184 | 185 | type ropeTest struct { 186 | re string 187 | cases []ropeTestCase 188 | } 189 | 190 | type ropeTestCase struct { 191 | str string 192 | s, e int64 193 | want []string 194 | } 195 | 196 | func ropeFromFindTests(ftests []findTest) []ropeTest { 197 | rtests := make([]ropeTest, len(ftests)) 198 | for i, ftest := range ftests { 199 | rtests[i] = ropeTest{re: ftest.re} 200 | for _, fc := range ftest.cases { 201 | e := int64(utf8.RuneCountInString(fc.str)) 202 | rc := ropeTestCase{str: fc.str, s: 0, e: e, want: fc.want} 203 | rtests[i].cases = append(rtests[i].cases, rc) 204 | } 205 | } 206 | return rtests 207 | } 208 | 209 | func runRopeTest(t *testing.T, test ropeTest, opts Opts) { 210 | t.Helper() 211 | re, residue, err := New(test.re, opts) 212 | if err != nil || residue != "" { 213 | t.Errorf("New(%q)=_,%q,%v", test.re, residue, err) 214 | return 215 | } 216 | for _, c := range test.cases { 217 | var got []string 218 | var ms []int64 219 | if opts.Reverse { 220 | ms = re.FindReverseInRope(rope.New(c.str), c.s, c.e) 221 | } else { 222 | ms = re.FindInRope(rope.New(c.str), c.s, c.e) 223 | } 224 | if len(ms) > 0 { 225 | // trim the regexp id 226 | ms = ms[:len(ms)-1] 227 | } 228 | for i := 0; i < len(ms); i += 2 { 229 | if ms[i] < 0 || ms[i+1] < 0 { 230 | got = append(got, "") 231 | continue 232 | } 233 | s, e := int(ms[i]), int(ms[i+1]) 234 | got = append(got, c.str[s:e]) 235 | } 236 | if !reflect.DeepEqual(got, c.want) { 237 | t.Errorf("New(%q).Find(%q, %d, %d)=%#v, want %#v", 238 | re.source, c.str, c.s, c.e, got, c.want) 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /ui/win.go: -------------------------------------------------------------------------------- 1 | // Package ui is the user interface of the editor. 2 | package ui 3 | 4 | import ( 5 | "image" 6 | "image/draw" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/eaburns/T/clipboard" 11 | "github.com/eaburns/T/edit" 12 | "github.com/eaburns/T/rope" 13 | "github.com/golang/freetype/truetype" 14 | "golang.org/x/image/font" 15 | ) 16 | 17 | // A Win is a window of columns of sheets. 18 | type Win struct { 19 | size image.Point 20 | *Col // focus 21 | cols []*Col 22 | widths []float64 // frac of width 23 | resizing int // col index being resized or -1 24 | 25 | dpi float32 26 | lineHeight int 27 | mods [4]bool // currently held modifier keys 28 | clipboard clipboard.Clipboard 29 | face font.Face // default font face 30 | output *Sheet 31 | 32 | mu sync.Mutex 33 | outputBuffer strings.Builder 34 | } 35 | 36 | // NewWin returns a new window. 37 | func NewWin(dpi float32) *Win { 38 | face := truetype.NewFace(defaultFont, &truetype.Options{ 39 | Size: float64(defaultFontSize), 40 | DPI: float64(dpi * (72.0 / 96.0)), 41 | }) 42 | h := (face.Metrics().Height + face.Metrics().Descent).Ceil() 43 | w := &Win{ 44 | resizing: -1, 45 | dpi: dpi, 46 | face: face, 47 | lineHeight: h, 48 | clipboard: clipboard.New(), 49 | } 50 | w.cols = []*Col{NewCol(w)} 51 | w.widths = []float64{1.0} 52 | w.Col = w.cols[0] 53 | w.output = NewSheet(w, "Output") 54 | return w 55 | } 56 | 57 | // Add adds a new column to the window and returns it. 58 | func (w *Win) Add() *Col { 59 | col := NewCol(w) 60 | f := 0.5 61 | if n := len(w.widths); n > 1 { 62 | f = (w.widths[n-2] + w.widths[n-1]) / 2.0 63 | } 64 | w.cols = append(w.cols, col) 65 | w.widths[len(w.widths)-1] = f 66 | w.widths = append(w.widths, 1.0) 67 | w.Resize(w.size) 68 | setWinFocus(w, col) 69 | return col 70 | } 71 | 72 | // Del deletes a column unless it is the last column. 73 | func (w *Win) Del(c *Col) { 74 | if len(w.cols) == 1 { 75 | return 76 | } 77 | i := colIndex(c) 78 | if i < 0 { 79 | return 80 | } 81 | w.cols = append(w.cols[:i], w.cols[i+1:]...) 82 | w.widths = append(w.widths[:i], w.widths[i+1:]...) 83 | w.widths[len(w.cols)-1] = 1.0 84 | if w.Col == c { 85 | if i == 0 { 86 | setWinFocus(w, w.cols[0]) 87 | } else { 88 | setWinFocus(w, w.cols[i-1]) 89 | } 90 | } 91 | w.Resize(w.size) 92 | } 93 | 94 | // Tick handles tick events. 95 | func (w *Win) Tick() bool { 96 | var redraw bool 97 | if showOutput(w) { 98 | redraw = true 99 | } 100 | for _, c := range w.cols { 101 | if c.Tick() { 102 | redraw = true 103 | } 104 | } 105 | return redraw 106 | } 107 | 108 | func showOutput(w *Win) bool { 109 | w.mu.Lock() 110 | output := w.outputBuffer.String() 111 | w.outputBuffer.Reset() 112 | w.mu.Unlock() 113 | 114 | if len(output) == 0 { 115 | return false 116 | } 117 | 118 | b := w.output.body 119 | b.Change(edit.Diffs{{ 120 | At: [2]int64{b.text.Len(), b.text.Len()}, 121 | Text: rope.New(output), 122 | }}) 123 | setDot(b, 1, b.text.Len(), b.text.Len()) 124 | // TODO: only showAddr on Output if the cursor was visible to begin with. 125 | // If the user scrolls up, for example, we shouldn't scroll them back down. 126 | // This should probably just be the behavior of b.Change by default. 127 | showAddr(b, b.dots[1].At[1]) 128 | 129 | w.outputBuffer.Reset() 130 | for _, c := range w.cols { 131 | for _, r := range c.rows { 132 | if r == w.output { 133 | return true 134 | } 135 | } 136 | } 137 | w.cols[len(w.cols)-1].Add(w.output) 138 | return true 139 | } 140 | 141 | // Draw draws the window. 142 | func (w *Win) Draw(dirty bool, drawImg draw.Image) { 143 | img := drawImg.(*image.RGBA) 144 | if w.size != img.Bounds().Size() { 145 | w.Resize(img.Bounds().Size()) 146 | } 147 | for i, c := range w.cols { 148 | r := img.Bounds() 149 | r.Min.X = img.Bounds().Min.X + x0(w, i) 150 | r.Max.X = img.Bounds().Min.X + x1(w, i) 151 | c.Draw(dirty, img.SubImage(r).(*image.RGBA)) 152 | if i < len(w.cols)-1 { 153 | r.Min.X = r.Max.X 154 | r.Max.X += framePx 155 | fillRect(img, frameBG, r) 156 | } 157 | } 158 | } 159 | 160 | // Resize handles resize events. 161 | func (w *Win) Resize(size image.Point) { 162 | w.size = size 163 | 164 | if nc := len(w.cols); int(dx(w)) < nc*w.lineHeight+nc*framePx { 165 | // Too small to fit everything. 166 | // Space out as much as we can, 167 | // so if the window grows, 168 | // everything is in a good place. 169 | w.widths[0] = 0.0 170 | for i := 1; i < nc; i++ { 171 | w.widths[i] = w.widths[i-1] + 1.0/float64(nc) 172 | } 173 | } 174 | w.widths[len(w.widths)-1] = 1.0 175 | 176 | for i, c := range w.cols { 177 | c.Resize(image.Pt(x1(w, i)-x0(w, i), w.size.Y)) 178 | } 179 | } 180 | 181 | // Move handles mouse move events. 182 | func (w *Win) Move(pt image.Point) { 183 | if w.resizing >= 0 { 184 | // Center the pointer horizontally on the handle. 185 | x := pt.X + w.cols[w.resizing].HandleBounds().Dx()/2 186 | resizeCol(w, x) 187 | return 188 | } 189 | 190 | pt.X -= x0(w, focusedCol(w)) 191 | w.Col.Move(pt) 192 | } 193 | 194 | func resizeCol(w *Win, x int) { 195 | dx := dx(w) 196 | newFrac := float64(x) / dx 197 | 198 | // Don't resize if either resized col would get too small. 199 | newX := int(newFrac * dx) 200 | var prev int 201 | if w.resizing > 0 { 202 | prev = x0(w, w.resizing) 203 | } 204 | if newX-prev-framePx < w.lineHeight { 205 | newFrac = float64(prev+w.lineHeight) / dx 206 | } 207 | next := x1(w, w.resizing+1) 208 | if next-newX-framePx < w.lineHeight { 209 | newFrac = float64(next-w.lineHeight-framePx) / dx 210 | } 211 | 212 | if w.widths[w.resizing] != newFrac { 213 | w.widths[w.resizing] = newFrac 214 | w.Resize(w.size) 215 | } 216 | } 217 | 218 | // Wheel handles mouse wheel events. 219 | func (w *Win) Wheel(pt image.Point, x, y int) { 220 | for i, c := range w.cols { 221 | if pt.X < x1(w, i) { 222 | pt.X -= x0(w, i) 223 | c.Wheel(pt, x, y) 224 | return 225 | } 226 | } 227 | } 228 | 229 | // Click handles click events. 230 | func (w *Win) Click(pt image.Point, button int) { 231 | if w.resizing >= 0 && button == -1 { 232 | w.resizing = -1 233 | return 234 | } 235 | if button == 1 { 236 | for i, c := range w.cols[:len(w.cols)-1] { 237 | handle := c.HandleBounds().Add(image.Pt(x0(w, i), 0)) 238 | if pt.In(handle) { 239 | if w.Col != c { 240 | w.Col.Focus(false) 241 | c.Focus(true) 242 | w.Col = c 243 | } 244 | w.resizing = i 245 | return 246 | } 247 | } 248 | } 249 | 250 | if button > 0 { 251 | setWinFocusPt(w, pt) 252 | } 253 | pt.X -= x0(w, focusedCol(w)) 254 | w.Col.Click(pt, button) 255 | } 256 | 257 | func setWinFocusPt(w *Win, pt image.Point) { 258 | for i, c := range w.cols { 259 | if pt.X < x1(w, i) { 260 | setWinFocus(w, c) 261 | break 262 | } 263 | } 264 | } 265 | 266 | // Focus handles focus change events. 267 | func (w *Win) Focus(focus bool) { 268 | if !focus { 269 | w.mods = [4]bool{} 270 | } 271 | w.Col.Focus(focus) 272 | } 273 | 274 | // Mod handles modifier key state change events. 275 | func (w *Win) Mod(m int) { 276 | switch { 277 | case m > 0 && m < len(w.mods): 278 | w.mods[m] = true 279 | case m < 0 && -m < len(w.mods): 280 | w.mods[-m] = false 281 | } 282 | w.Col.Mod(m) 283 | } 284 | 285 | // OutputString appends a string to the Output sheet 286 | // and ensures that the Output sheet is visible. 287 | // It is safe for concurrent calls. 288 | func (w *Win) OutputString(str string) { 289 | w.mu.Lock() 290 | w.outputBuffer.WriteString(str) 291 | w.mu.Unlock() 292 | } 293 | 294 | // OutputBytes appends bytes to the Output sheet 295 | // and ensures that the Output sheet is visible. 296 | // It is safe for concurrent calls. 297 | func (w *Win) OutputBytes(data []byte) { 298 | w.mu.Lock() 299 | w.outputBuffer.Write(data) 300 | w.mu.Unlock() 301 | } 302 | 303 | func x0(w *Win, i int) int { 304 | if i == 0 { 305 | return 0 306 | } 307 | return x1(w, i-1) + framePx 308 | } 309 | 310 | func x1(w *Win, i int) int { return int(w.widths[i] * dx(w)) } 311 | 312 | func dx(w *Win) float64 { return float64(w.size.X) } 313 | 314 | func setWinFocus(w *Win, c *Col) { 315 | if w.Col == c { 316 | return 317 | } 318 | w.Col.Focus(false) 319 | c.Focus(true) 320 | w.Col = c 321 | } 322 | 323 | func focusedCol(w *Win) int { 324 | i := colIndex(w.Col) 325 | if i < 0 { 326 | return 0 327 | } 328 | return i 329 | } 330 | -------------------------------------------------------------------------------- /ui/sheet.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/draw" 7 | "math" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "sort" 12 | "strings" 13 | "unicode" 14 | "unicode/utf8" 15 | 16 | "github.com/eaburns/T/edit" 17 | "github.com/eaburns/T/rope" 18 | "github.com/eaburns/T/syntax" 19 | "github.com/eaburns/T/text" 20 | ) 21 | 22 | // A Sheet is a tag and a body. 23 | // TODO: better document the Sheet type. 24 | type Sheet struct { 25 | tag *TextBox 26 | body *TextBox 27 | tagH, minTagH int 28 | size image.Point 29 | *TextBox // the focus element: the tag or the body. 30 | } 31 | 32 | // NewSheet returns a new sheet. 33 | func NewSheet(w *Win, title string) *Sheet { 34 | var ( 35 | tagTextStyles = [...]text.Style{ 36 | {FG: fg, BG: tagBG, Face: w.face}, 37 | {BG: hiBG1}, 38 | {BG: hiBG2}, 39 | {BG: hiBG3}, 40 | } 41 | bodyTextStyles = [...]text.Style{ 42 | {FG: fg, BG: bodyBG, Face: w.face}, 43 | {BG: hiBG1}, 44 | {BG: hiBG2}, 45 | {BG: hiBG3}, 46 | } 47 | ) 48 | tag := NewTextBox(w, tagTextStyles, image.ZP) 49 | body := NewTextBox(w, bodyTextStyles, image.ZP) 50 | s := &Sheet{ 51 | tag: tag, 52 | body: body, 53 | minTagH: w.lineHeight, 54 | TextBox: body, 55 | } 56 | tag.setHighlighter(s) 57 | tag.SetText(rope.New(tagText)) 58 | s.SetTitle(title) 59 | return s 60 | } 61 | 62 | // Body returns the sheet's body text box. 63 | func (s *Sheet) Body() *TextBox { return s.body } 64 | 65 | // Tick handles tic events. 66 | func (s *Sheet) Tick() bool { 67 | redraw1 := s.body.Tick() 68 | redraw2 := s.tag.Tick() 69 | return redraw1 || redraw2 70 | } 71 | 72 | // Draw draws the sheet. 73 | func (s *Sheet) Draw(dirty bool, drawImg draw.Image) { 74 | img := drawImg.(*image.RGBA) 75 | 76 | tagRect := img.Bounds() 77 | tagRect.Max.X = drawSheetHandle(s, img) 78 | tagRect.Max.Y = tagRect.Min.Y + s.tagH 79 | s.tag.Draw(dirty, img.SubImage(tagRect).(*image.RGBA)) 80 | 81 | bodyRect := img.Bounds() 82 | bodyRect.Min.Y = tagRect.Max.Y 83 | s.body.Draw(dirty, img.SubImage(bodyRect).(*image.RGBA)) 84 | } 85 | 86 | func drawSheetHandle(s *Sheet, img *image.RGBA) int { 87 | const pad = 6 88 | handle := s.HandleBounds().Add(img.Bounds().Min) 89 | r := handle 90 | r.Max.Y = r.Min.Y + s.tagH 91 | fillRect(img, tagBG, r) 92 | fillRect(img, colBG, handle.Inset(pad)) 93 | return r.Min.X 94 | } 95 | 96 | // HandleBounds returns the bounding box of the handle. 97 | func (s *Sheet) HandleBounds() image.Rectangle { 98 | return image.Rect(s.size.X-s.minTagH, 0, s.size.X, s.minTagH) 99 | } 100 | 101 | // Resize handles resize events. 102 | func (s *Sheet) Resize(size image.Point) { 103 | s.size = size 104 | resetTagHeight(s, size) 105 | s.body.Resize(image.Pt(size.X, size.Y-s.tagH)) 106 | } 107 | 108 | // Update watches for updates to the tag and resizes it to fit the text height. 109 | func (s *Sheet) Update([]syntax.Highlight, edit.Diffs, rope.Rope) []syntax.Highlight { 110 | oldTagH := s.tagH 111 | resetTagHeight(s, s.size) 112 | if s.tagH != oldTagH { 113 | s.body.Resize(image.Pt(s.size.X, s.size.Y-s.tagH)) 114 | } 115 | return nil 116 | } 117 | 118 | func resetTagHeight(s *Sheet, size image.Point) { 119 | size.X -= s.minTagH // handle 120 | s.tag.Resize(size) 121 | s.tag.Dir(0, math.MinInt16) 122 | if s.tagH = s.tag.textHeight(); s.tagH < s.minTagH { 123 | s.tagH = s.minTagH 124 | } 125 | s.tag.Resize(image.Pt(size.X, s.tagH)) 126 | } 127 | 128 | // Move handles movement events. 129 | func (s *Sheet) Move(pt image.Point) { 130 | if s.TextBox == s.body { 131 | pt.Y -= s.tagH 132 | } 133 | s.TextBox.Move(pt) 134 | } 135 | 136 | // Wheel handles mouse wheel events. 137 | func (s *Sheet) Wheel(pt image.Point, x, y int) { 138 | if pt.Y < s.tagH { 139 | s.tag.Wheel(pt, x, y) 140 | } else { 141 | pt.Y -= s.tagH 142 | s.body.Wheel(pt, x, y) 143 | } 144 | } 145 | 146 | // Click handles click events. 147 | func (s *Sheet) Click(pt image.Point, button int) (int, [2]int64) { 148 | if button > 0 { 149 | setSheetFocus(s, pt, button) 150 | } 151 | 152 | if s.TextBox == s.body { 153 | pt.Y -= s.tagH 154 | } 155 | return s.TextBox.Click(pt, button) 156 | } 157 | 158 | func setSheetFocus(s *Sheet, pt image.Point, button int) bool { 159 | if button != 1 { 160 | return false 161 | } 162 | if pt.Y < s.tagH { 163 | if s.TextBox != s.tag { 164 | s.TextBox = s.tag 165 | s.body.Focus(false) 166 | s.tag.Focus(true) 167 | return true 168 | } 169 | } else { 170 | if s.TextBox != s.body { 171 | s.TextBox = s.body 172 | s.tag.Focus(false) 173 | s.body.Focus(true) 174 | return true 175 | } 176 | } 177 | return false 178 | } 179 | 180 | // Title returns the title of the sheet. 181 | // The title is the first space-terminated string in the tag, 182 | // or if the first rune of the tag is ' , it is the first ' terminated string 183 | // with \' as an escaped ' and \\ as an escaped \. 184 | func (s *Sheet) Title() string { 185 | _, title := s.title() 186 | return title 187 | } 188 | 189 | func (s *Sheet) title() (int64, string) { 190 | txt := s.tag.text 191 | if rope.IndexRune(txt, '\'') < 0 { 192 | i := rope.IndexFunc(txt, unicode.IsSpace) 193 | if i < 0 { 194 | i = txt.Len() 195 | } 196 | return i, rope.Slice(txt, 0, i).String() 197 | } 198 | 199 | var i int64 200 | var esc bool 201 | var title strings.Builder 202 | rr := rope.NewReader(txt) 203 | rr.ReadRune() // discard ' 204 | for { 205 | r, w, err := rr.ReadRune() 206 | i += int64(w) 207 | switch { 208 | case err != nil: // must be io.EOF from rope.Reader 209 | fallthrough 210 | case !esc && r == '\'': 211 | return i + 1, title.String() // +1 for leading ' 212 | case !esc && r == '\\': 213 | esc = true 214 | default: 215 | esc = false 216 | title.WriteRune(r) 217 | } 218 | } 219 | } 220 | 221 | // SetTitle sets the title of the sheet. 222 | func (s *Sheet) SetTitle(title string) { 223 | r, _ := utf8.DecodeRuneInString(title) 224 | if r == '\'' || strings.IndexFunc(title, unicode.IsSpace) >= 0 { 225 | title = strings.Replace(title, `\`, `\\`, -1) 226 | title = strings.Replace(title, `'`, `\'`, -1) 227 | title = `'` + title + `'` 228 | } 229 | end, _ := s.title() 230 | s.tag.Change([]edit.Diff{{At: [2]int64{0, end}, Text: rope.Empty()}}) 231 | r, _, err := rope.NewReader(s.tag.text).ReadRune() 232 | if err == nil && !unicode.IsSpace(r) { 233 | title += " " 234 | } 235 | s.tag.Change([]edit.Diff{{At: [2]int64{0, 0}, Text: rope.New(title)}}) 236 | } 237 | 238 | // Get loads the body of the sheet 239 | // with the contents of the file 240 | // at the path of the sheet's title. 241 | func (s *Sheet) Get() error { 242 | title := s.Title() 243 | f, err := os.Open(title) 244 | if err != nil { 245 | return err 246 | } 247 | defer f.Close() 248 | return get(s, f) 249 | } 250 | 251 | func get(s *Sheet, f *os.File) error { 252 | st, err := f.Stat() 253 | if err != nil { 254 | return err 255 | } 256 | switch { 257 | case st.IsDir(): 258 | err = getDir(s, f) 259 | default: 260 | err = getText(s, f) 261 | } 262 | if err != nil { 263 | return err 264 | } 265 | if s.TextBox != s.body { 266 | s.TextBox.Focus(false) 267 | s.TextBox = s.body 268 | s.TextBox.Focus(true) 269 | } 270 | return nil 271 | } 272 | 273 | func getText(s *Sheet, f *os.File) error { 274 | txt, err := rope.ReadFrom(f) 275 | if err != nil { 276 | return err 277 | } 278 | s.body.setHighlighter(nil) 279 | s.body.SetText(txt) 280 | s.body.setHighlighter(syntaxHighlighter(s.win.dpi, s.Title())) 281 | return nil 282 | } 283 | 284 | func getDir(s *Sheet, f *os.File) error { 285 | s.SetTitle(ensureTrailingSlash(s.Title())) 286 | txt, err := readFromDir("", f) 287 | if err != nil { 288 | return err 289 | } 290 | s.body.SetText(txt) 291 | s.body.setHighlighter(syntaxHighlighter(s.win.dpi, s.Title())) 292 | return nil 293 | } 294 | 295 | func ensureTrailingSlash(p string) string { 296 | if r, _ := utf8.DecodeLastRuneInString(p); r != os.PathSeparator { 297 | return p + string([]rune{os.PathSeparator}) 298 | } 299 | return p 300 | } 301 | 302 | func readFromDir(prefix string, f *os.File) (rope.Rope, error) { 303 | txt := rope.Empty() 304 | fis, err := f.Readdir(-1) 305 | if err != nil { 306 | return txt, err 307 | } 308 | sortFileInfos(fis) 309 | for _, fi := range fis { 310 | name := fi.Name() 311 | if prefix != "" { 312 | name = filepath.Join(prefix, name) 313 | } 314 | if fi.IsDir() { 315 | name = ensureTrailingSlash(name) 316 | } 317 | txt = rope.Append(txt, rope.New(name+"\n")) 318 | } 319 | return txt, nil 320 | } 321 | 322 | func sortFileInfos(fis []os.FileInfo) { 323 | sort.Slice(fis, func(i, j int) bool { 324 | switch { 325 | case fis[i].IsDir() == fis[j].IsDir(): 326 | return fis[i].Name() < fis[j].Name() 327 | case fis[i].IsDir(): 328 | return true 329 | default: 330 | return false 331 | } 332 | }) 333 | } 334 | 335 | func syntaxHighlighter(dpi float32, path string) updater { 336 | for _, s := range syntaxHighlighting { 337 | switch ok, err := regexp.MatchString(s.regexp, path); { 338 | case err != nil: 339 | fmt.Println(err.Error()) 340 | case ok: 341 | return &highlighter{s.tok(dpi)} 342 | } 343 | } 344 | return nil 345 | } 346 | 347 | // Put writes the contents of the body of the sheet 348 | // to the file at the path of the sheet's title. 349 | func (s *Sheet) Put() error { 350 | f, err := os.Create(s.Title()) 351 | if err != nil { 352 | return err 353 | } 354 | if _, err := s.body.text.WriteTo(f); err != nil { 355 | f.Close() 356 | return err 357 | } 358 | return f.Close() 359 | } 360 | -------------------------------------------------------------------------------- /rope/rope.go: -------------------------------------------------------------------------------- 1 | // Package rope implements Ropes, a copy-on-write, string-like data structure 2 | // that is optimized for efficient modification of large sequences of bytes 3 | // at the cost of more expensive random-access. 4 | package rope 5 | 6 | import ( 7 | "io" 8 | "strings" 9 | "unicode/utf8" 10 | ) 11 | 12 | // Rope is a copy-on-write string, optimized for concatenation and splitting. 13 | type Rope interface { 14 | Len() int64 15 | String() string 16 | WriteTo(io.Writer) (int64, error) 17 | } 18 | 19 | type node struct { 20 | left, right Rope 21 | len int64 22 | } 23 | 24 | func (n *node) Len() int64 { return n.len } 25 | 26 | func (n *node) String() string { 27 | var s strings.Builder 28 | n.WriteTo(&s) 29 | return s.String() 30 | } 31 | 32 | func (n *node) WriteTo(w io.Writer) (int64, error) { 33 | nleft, err := n.left.WriteTo(w) 34 | if err != nil { 35 | return nleft, err 36 | } 37 | nright, err := n.right.WriteTo(w) 38 | return nleft + nright, err 39 | } 40 | 41 | type leaf struct { 42 | text string 43 | } 44 | 45 | func (l *leaf) Len() int64 { return int64(len(l.text)) } 46 | func (l *leaf) String() string { return l.text } 47 | 48 | func (l *leaf) WriteTo(w io.Writer) (int64, error) { 49 | num, err := io.WriteString(w, l.text) 50 | num64 := int64(num) 51 | return num64, err 52 | } 53 | 54 | // Empty returns an empty Rope. 55 | func Empty() Rope { return New("") } 56 | 57 | // New returns a new Rope of the given string. 58 | func New(text string) Rope { return &leaf{text: text} } 59 | 60 | // ReadFrom returns a new Rope containing 61 | // all of the bytes read from a reader until io.EOF. 62 | // On error, the returned Rope contains any bytes 63 | // read from the Reader before the error. 64 | // If no bytes were read, the Rope is empty. 65 | func ReadFrom(r io.Reader) (Rope, error) { 66 | buf := make([]byte, 32*1024) 67 | rope := Empty() 68 | for { 69 | n, err := r.Read(buf) 70 | rope = Append(rope, New(string(buf[:n]))) 71 | switch { 72 | case err == io.EOF: 73 | return rope, nil 74 | case err != nil: 75 | return rope, err 76 | } 77 | } 78 | } 79 | 80 | const smallSize = 32 81 | 82 | // Append returns the concatenation of l and then r. 83 | func Append(l, r Rope) Rope { 84 | switch { 85 | case l.Len() == 0: 86 | return r 87 | case r.Len() == 0: 88 | return l 89 | case l.Len()+r.Len() <= smallSize: 90 | return &leaf{text: l.String() + r.String()} 91 | } 92 | if l, ok := l.(*node); ok && l.right.Len()+r.Len() <= smallSize { 93 | return &node{ 94 | left: l.left, 95 | right: &leaf{text: l.right.String() + r.String()}, 96 | len: l.Len() + r.Len(), 97 | } 98 | } 99 | return &node{left: l, right: r, len: l.Len() + r.Len()} 100 | } 101 | 102 | // Split returns two new Ropes, the first contains the first i bytes, 103 | // and the second contains the remaining. 104 | // Split panics if i < 0 || i >= r.Len(). 105 | func Split(r Rope, i int64) (left, right Rope) { 106 | return split(r, i) 107 | } 108 | 109 | // Delete deletes n bytes from r beginning at index start. 110 | // Delete panics if start < 0, n < 0, or start+n > r.Len(). 111 | func Delete(r Rope, start, n int64) Rope { 112 | r, l := split(r, start) 113 | _, l = split(l, start+n-r.Len()) 114 | return Append(r, l) 115 | } 116 | 117 | // Insert inserts ins into r at index i. 118 | // Insert panics if i < 0 or i > r.Len(). 119 | func Insert(r Rope, i int64, ins Rope) Rope { 120 | r, l := split(r, i) 121 | return Append(Append(r, ins), l) 122 | } 123 | 124 | // Slice returns a new Rope containing the bytes 125 | // between start (inclusive) and end (exclusive). 126 | // Slice panics if start < 0, end < start, or end > r.Len(). 127 | func Slice(r Rope, start, end int64) Rope { 128 | _, r = split(r, start) 129 | r, _ = split(r, end-start) 130 | return r 131 | } 132 | 133 | func split(rope Rope, i int64) (left, right Rope) { 134 | if i < 0 || i > rope.Len() { 135 | panic("index out of bounds") 136 | } 137 | switch rope := rope.(type) { 138 | case *leaf: 139 | return New(rope.text[:i]), New(rope.text[i:]) 140 | case *node: 141 | switch { 142 | case i <= rope.left.Len(): 143 | l, r := split(rope.left, i) 144 | return l, Append(r, rope.right) 145 | default: 146 | l, r := split(rope.right, i-rope.left.Len()) 147 | return Append(rope.left, l), r 148 | } 149 | default: 150 | panic("impossible") 151 | } 152 | } 153 | 154 | // Reader implements io.Reader, io.ByteReader, and io.RuneReader, 155 | // reading from the contents of a Rope. 156 | type Reader struct { 157 | iter 158 | buf [utf8.UTFMax]byte 159 | n int 160 | } 161 | 162 | // NewReader returns a new *Reader 163 | // that reads the contents of the Rope. 164 | func NewReader(rope Rope) *Reader { 165 | return &Reader{iter: iter{todo: []Rope{rope}}} 166 | } 167 | 168 | // Read reads into p and returns the number of bytes read. 169 | // If there is nothing left to read, Read returns 0 and io.EOF. 170 | // Read does not return errors other than io.EOF. 171 | func (r *Reader) Read(p []byte) (int, error) { 172 | if r.n > 0 { 173 | n := copy(p, r.buf[:r.n]) 174 | copy(r.buf[:], r.buf[n:]) 175 | r.n -= n 176 | return n, nil 177 | } 178 | return r.read(p) 179 | } 180 | 181 | // ReadByte returns the next byte. 182 | // If there are no more bytes to read, ReadByte returns 0 and io.EOF. 183 | // ReadByte does not return errors other than io.EOF. 184 | func (r *Reader) ReadByte() (byte, error) { 185 | var b [1]byte 186 | _, err := r.Read(b[:]) 187 | return b[0], err 188 | } 189 | 190 | // ReadRune returns the next rune and it's byte-width. 191 | // If the next bytes are not valid UTF8, 192 | // ReadRune returns utf8.RuneError, 1. 193 | // If there are no more bytes to read, ReadRune returns 0 and io.EOF. 194 | // ReadRune does not return errors other than io.EOF. 195 | func (r *Reader) ReadRune() (rune, int, error) { 196 | for invalidUTF8(r.buf[:r.n]) && r.n < len(r.buf) { 197 | n, err := r.read(r.buf[r.n:]) 198 | r.n += n 199 | if err == io.EOF { 200 | break 201 | } 202 | } 203 | if r.n == 0 { 204 | return 0, 0, io.EOF 205 | } 206 | ru, w := utf8.DecodeRune(r.buf[:]) 207 | r.n = copy(r.buf[:], r.buf[w:r.n]) 208 | return ru, w, nil 209 | } 210 | 211 | func (r *Reader) read(p []byte) (int, error) { 212 | for len(r.text) == 0 { 213 | if !next(&r.iter, false) { 214 | return 0, io.EOF 215 | } 216 | } 217 | n := copy(p, r.text) 218 | r.text = r.text[n:] 219 | return n, nil 220 | } 221 | 222 | // ReverseReader implements 223 | // io.Reader, io.ByteReader, and io.RuneReader, 224 | // reading from the contents of a Rope in reverse. 225 | type ReverseReader struct { 226 | iter 227 | buf [utf8.UTFMax]byte 228 | n int 229 | } 230 | 231 | // NewReverseReader returns a new *ReverseReader 232 | // that reads the contents of the Rope in reverse. 233 | func NewReverseReader(rope Rope) *ReverseReader { 234 | return &ReverseReader{iter: iter{todo: []Rope{rope}}} 235 | } 236 | 237 | // Read reads into p and returns the number of bytes read. 238 | // If there is nothing left to read, Read returns 0 and io.EOF. 239 | // Read does not return errors other than io.EOF. 240 | func (r *ReverseReader) Read(p []byte) (int, error) { 241 | if r.n > 0 { 242 | n := ypocByte(p, r.buf[:r.n]) 243 | r.n -= n 244 | return n, nil 245 | } 246 | return r.read(p) 247 | } 248 | 249 | // ReadByte returns the next byte. 250 | // If there are no more bytes to read, ReadByte returns 0 and io.EOF. 251 | // ReadByte does not return errors other than io.EOF. 252 | func (r *ReverseReader) ReadByte() (byte, error) { 253 | var b [1]byte 254 | _, err := r.Read(b[:]) 255 | return b[0], err 256 | } 257 | 258 | // ReadRune returns the next rune and it's byte-width. 259 | // If the next bytes are not valid UTF8, 260 | // ReadRune returns utf8.RuneError, 1. 261 | // If there are no more bytes to read, ReadRune returns 0 and io.EOF. 262 | // ReadRune does not return errors other than io.EOF. 263 | func (r *ReverseReader) ReadRune() (rune, int, error) { 264 | for invalidUTF8(r.buf[:r.n]) && r.n < len(r.buf) { 265 | var b [1]byte 266 | if _, err := r.read(b[:]); err == io.EOF { 267 | break 268 | } 269 | r.buf[0], r.buf[1], r.buf[2], r.buf[3] = b[0], r.buf[0], r.buf[1], r.buf[2] 270 | r.n++ 271 | } 272 | if r.n == 0 { 273 | return 0, 0, io.EOF 274 | } 275 | ru, w := utf8.DecodeLastRune(r.buf[:r.n]) 276 | r.n -= w 277 | return ru, w, nil 278 | } 279 | 280 | func invalidUTF8(p []byte) bool { 281 | r, _ := utf8.DecodeRune(p) 282 | return r == utf8.RuneError 283 | } 284 | 285 | func (r *ReverseReader) read(p []byte) (int, error) { 286 | for len(r.text) == 0 { 287 | if !next(&r.iter, true) { 288 | return 0, io.EOF 289 | } 290 | } 291 | n := ypocStr(p, r.text) 292 | r.text = r.text[:len(r.text)-n] 293 | return n, nil 294 | } 295 | 296 | func ypocStr(dst []byte, src string) int { 297 | n := len(src) 298 | if len(dst) < n { 299 | n = len(dst) 300 | } 301 | for i := 0; i < n; i++ { 302 | dst[i] = src[len(src)-1-i] 303 | } 304 | return n 305 | } 306 | 307 | func ypocByte(dst []byte, src []byte) int { 308 | n := len(src) 309 | if len(dst) < n { 310 | n = len(dst) 311 | } 312 | for i := 0; i < n; i++ { 313 | dst[i] = src[len(src)-1-i] 314 | } 315 | return n 316 | } 317 | 318 | type iter struct { 319 | todo []Rope 320 | text string 321 | } 322 | 323 | func next(it *iter, rev bool) bool { 324 | n := len(it.todo) 325 | if n == 0 { 326 | return false 327 | } 328 | rope := it.todo[n-1] 329 | it.todo = it.todo[:n-1] 330 | for { 331 | switch n := rope.(type) { 332 | case *leaf: 333 | it.text = n.text 334 | return true 335 | case *node: 336 | if rev { 337 | it.todo = append(it.todo, n.left) 338 | rope = n.right 339 | } else { 340 | it.todo = append(it.todo, n.right) 341 | rope = n.left 342 | } 343 | default: 344 | panic("impossible") 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /re1/re1_test.go: -------------------------------------------------------------------------------- 1 | package re1 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | "unicode/utf8" 9 | ) 10 | 11 | func TestParse(t *testing.T) { 12 | tests := []struct { 13 | re string 14 | residue string 15 | err string 16 | delim rune 17 | }{ 18 | { 19 | re: "", 20 | }, 21 | { 22 | re: "ab/c", 23 | }, 24 | { 25 | re: "ab/c", 26 | delim: '/', 27 | residue: "c", 28 | }, 29 | { 30 | re: "ab\\/c", 31 | delim: '/', 32 | }, 33 | { 34 | re: "ab\nc", // literal newline 35 | residue: "c", 36 | }, 37 | { 38 | re: `ab\nc`, // \n newline 39 | }, 40 | { 41 | re: "ab\\\nc", // escaped newline 42 | }, 43 | { 44 | re: "?", 45 | err: "unexpected ?", 46 | }, 47 | { 48 | re: "*", 49 | err: "unexpected *", 50 | }, 51 | { 52 | re: "+", 53 | err: "unexpected +", 54 | }, 55 | { 56 | re: "|xyz", 57 | err: "unexpected |", 58 | }, 59 | { 60 | re: "abc||", 61 | err: "unexpected |", 62 | }, 63 | { 64 | re: "(", 65 | err: "unclosed (", 66 | }, 67 | { 68 | re: "(()", 69 | err: "unclosed (", 70 | }, 71 | { 72 | re: ")", 73 | err: "unopened )", 74 | }, 75 | { 76 | re: "())", 77 | err: "unopened )", 78 | }, 79 | { 80 | re: "[", 81 | err: "unclosed [", 82 | }, 83 | { 84 | re: "[]", 85 | err: "empty charclass", 86 | }, 87 | { 88 | re: "[[]", // OK 89 | }, 90 | { 91 | re: "[^]", 92 | err: "empty charclass", 93 | }, 94 | { 95 | re: "[-z]", 96 | err: "bad range", 97 | }, 98 | { 99 | re: "[a-]", 100 | err: "bad range", 101 | }, 102 | { 103 | re: "[^-z]", 104 | err: "bad range", 105 | }, 106 | { 107 | re: "[a-z-Z]", 108 | err: "bad range", 109 | }, 110 | { 111 | re: "[z-a]", 112 | err: "bad range", 113 | }, 114 | { 115 | re: `[\-a]`, // OK 116 | }, 117 | { 118 | re: `[[-\]]`, // OK 119 | }, 120 | { 121 | re: `[\]]`, // OK 122 | }, 123 | } 124 | for _, test := range tests { 125 | var opts Opts 126 | if test.delim != 0 { 127 | opts.Delimiter = test.delim 128 | } 129 | _, residue, err := New(test.re, opts) 130 | var errStr string 131 | if err != nil { 132 | errStr = err.Error() 133 | } 134 | if residue != test.residue || errStr != test.err { 135 | t.Errorf("New(%q, %#v)=_,%q,%v, want _,%q,%v", 136 | test.re, opts, residue, errStr, test.residue, test.err) 137 | } 138 | } 139 | } 140 | 141 | // These tests are pretty incomplete, because we rely on the RE2 suite instead. 142 | var findTests = []findTest{ 143 | { 144 | re: "", 145 | cases: []findTestCase{ 146 | {str: "", want: []string{""}}, 147 | {str: "z", want: []string{""}}, 148 | }, 149 | }, 150 | { 151 | re: `\`, 152 | cases: []findTestCase{ 153 | {str: ``, want: nil}, 154 | {str: ``, want: nil}, 155 | {str: `\`, want: []string{`\`}}, 156 | }, 157 | }, 158 | { 159 | re: `\\`, 160 | cases: []findTestCase{ 161 | {str: `\`, want: []string{`\`}}, 162 | }, 163 | }, 164 | { 165 | re: `\n`, 166 | cases: []findTestCase{ 167 | {str: "\n", want: []string{"\n"}}, 168 | }, 169 | }, 170 | { 171 | re: `\t`, 172 | cases: []findTestCase{ 173 | {str: ` `, want: []string{" "}}, 174 | }, 175 | }, 176 | { 177 | re: `\*`, 178 | cases: []findTestCase{ 179 | {str: "***", want: []string{"*"}}, 180 | }, 181 | }, 182 | { 183 | re: `[*]`, 184 | cases: []findTestCase{ 185 | {str: "***", want: []string{"*"}}, 186 | }, 187 | }, 188 | } 189 | 190 | func TestFind(t *testing.T) { 191 | for _, test := range findTests { 192 | runTest(t, test, Opts{}) 193 | } 194 | } 195 | 196 | var findReverseTests = []findTest{ 197 | { 198 | re: "", 199 | cases: []findTestCase{ 200 | {str: "", want: []string{""}}, 201 | {str: "z", want: []string{""}}, 202 | }, 203 | }, 204 | { 205 | re: "a", 206 | cases: []findTestCase{ 207 | {str: "", want: nil}, 208 | {str: "z", want: nil}, 209 | {str: "a", want: []string{"a"}}, 210 | {str: "aa", want: []string{"a"}}, 211 | {str: "xaa", want: []string{"a"}}, 212 | {str: "aax", want: []string{"a"}}, 213 | {str: "xaax", want: []string{"a"}}, 214 | {str: "axaax", want: []string{"a"}}, 215 | {str: "xaaxa", want: []string{"a"}}, 216 | }, 217 | }, 218 | { 219 | re: "aa", 220 | cases: []findTestCase{ 221 | {str: "", want: nil}, 222 | {str: "z", want: nil}, 223 | {str: "a", want: nil}, 224 | {str: "aa", want: []string{"aa"}}, 225 | {str: "xaa", want: []string{"aa"}}, 226 | {str: "aax", want: []string{"aa"}}, 227 | {str: "xaax", want: []string{"aa"}}, 228 | {str: "axaax", want: []string{"aa"}}, 229 | {str: "xaaxa", want: []string{"aa"}}, 230 | }, 231 | }, 232 | { 233 | re: "a*", 234 | cases: []findTestCase{ 235 | {str: "", want: []string{""}}, 236 | {str: "z", want: []string{""}}, 237 | {str: "a", want: []string{"a"}}, 238 | {str: "aa", want: []string{"aa"}}, 239 | {str: "xaa", want: []string{"aa"}}, 240 | {str: "aax", want: []string{""}}, 241 | {str: "xaax", want: []string{""}}, 242 | {str: "axaax", want: []string{""}}, 243 | {str: "xaaxa", want: []string{"a"}}, 244 | }, 245 | }, 246 | { 247 | re: "abc", 248 | cases: []findTestCase{ 249 | {str: "", want: nil}, 250 | {str: "z", want: nil}, 251 | {str: "abc", want: []string{"abc"}}, 252 | {str: "cba", want: nil}, 253 | {str: "abcxyz", want: []string{"abc"}}, 254 | }, 255 | }, 256 | { 257 | re: "(abc)*", 258 | cases: []findTestCase{ 259 | {str: "", want: []string{"", ""}}, 260 | {str: "z", want: []string{"", ""}}, 261 | {str: "abc", want: []string{"abc", "abc"}}, 262 | {str: "abcabc", want: []string{"abcabc", "abc"}}, 263 | {str: "cba", want: []string{"", ""}}, 264 | {str: "abcxyz", want: []string{"", ""}}, 265 | {str: "abcabc123abcxyz", want: []string{"", ""}}, 266 | {str: "abcabcabcxyz", want: []string{"", ""}}, 267 | }, 268 | }, 269 | { 270 | re: "(abc)+", 271 | cases: []findTestCase{ 272 | {str: "", want: nil}, 273 | {str: "z", want: nil}, 274 | {str: "abc", want: []string{"abc", "abc"}}, 275 | {str: "cba", want: nil}, 276 | {str: "abcxyz", want: []string{"abc", "abc"}}, 277 | {str: "abcabc123abcxyz", want: []string{"abc", "abc"}}, 278 | {str: "abcabcabcxyz", want: []string{"abcabcabc", "abc"}}, 279 | }, 280 | }, 281 | { 282 | re: "^abc", 283 | cases: []findTestCase{ 284 | {str: "", want: nil}, 285 | {str: "abc", want: []string{"abc"}}, 286 | {str: "xabc", want: nil}, 287 | {str: "x\nabc", want: []string{"abc"}}, 288 | }, 289 | }, 290 | { 291 | re: "abc$", 292 | cases: []findTestCase{ 293 | {str: "", want: nil}, 294 | {str: "abc", want: []string{"abc"}}, 295 | {str: "abcx", want: nil}, 296 | {str: "abc\nx", want: []string{"abc"}}, 297 | }, 298 | }, 299 | { 300 | re: "^abc$", 301 | cases: []findTestCase{ 302 | {str: "", want: nil}, 303 | {str: "xyz", want: nil}, 304 | {str: "abc", want: []string{"abc"}}, 305 | {str: "123\nabc", want: []string{"abc"}}, 306 | {str: "abc\n123", want: []string{"abc"}}, 307 | {str: "123\nabc\n123", want: []string{"abc"}}, 308 | {str: "123abc", want: nil}, 309 | {str: "abc123", want: nil}, 310 | {str: "123abc", want: nil}, 311 | {str: "abc123", want: nil}, 312 | {str: "123abc123", want: nil}, 313 | }, 314 | }, 315 | { 316 | re: "^$", 317 | cases: []findTestCase{ 318 | {str: "", want: []string{""}}, 319 | {str: "\n", want: []string{""}}, 320 | {str: "\nxyz", want: []string{""}}, 321 | {str: "xyz\n", want: []string{""}}, 322 | {str: "xyz\n123", want: nil}, 323 | }, 324 | }, 325 | { 326 | re: "(foo)(bar)", 327 | cases: []findTestCase{ 328 | {str: "foobar", want: []string{"foobar", "foo", "bar"}}, 329 | }, 330 | }, 331 | } 332 | 333 | func TestReverseFind(t *testing.T) { 334 | for _, test := range findReverseTests { 335 | runTest(t, test, Opts{Reverse: true}) 336 | } 337 | } 338 | 339 | type findTest struct { 340 | re string 341 | cases []findTestCase 342 | } 343 | 344 | type findTestCase struct { 345 | str string 346 | want []string 347 | } 348 | 349 | func runTest(t *testing.T, test findTest, opts Opts) { 350 | t.Helper() 351 | re, residue, err := New(test.re, opts) 352 | if err != nil || residue != "" { 353 | t.Errorf("New(%q)=_,%q,%v", test.re, residue, err) 354 | return 355 | } 356 | for _, c := range test.cases { 357 | if opts.Reverse { 358 | runTestCaseReverse(t, re, c) 359 | } else { 360 | runTestCaseForward(t, re, c) 361 | } 362 | } 363 | } 364 | 365 | func runTestCaseForward(t *testing.T, re *Regexp, c findTestCase) { 366 | var got []string 367 | ms := re.Find(strings.NewReader(c.str)) 368 | if len(ms) > 0 { 369 | // trim the regexp id 370 | ms = ms[:len(ms)-1] 371 | } 372 | for i := 0; i < len(ms); i += 2 { 373 | if ms[i] < 0 || ms[i+1] < 0 { 374 | got = append(got, "") 375 | continue 376 | } 377 | s, e := int(ms[i]), int(ms[i+1]) 378 | got = append(got, c.str[s:e]) 379 | } 380 | if !reflect.DeepEqual(got, c.want) { 381 | t.Errorf("New(%q).Find(%q)=%#v, want %#v", re.source, c.str, got, c.want) 382 | } 383 | } 384 | 385 | func runTestCaseReverse(t *testing.T, re *Regexp, c findTestCase) { 386 | rr, nrunes := reverse(c.str) 387 | var got []string 388 | ms := re.Find(rr) 389 | if len(ms) > 0 { 390 | // trim the regexp id 391 | ms = ms[:len(ms)-1] 392 | } 393 | for i := 0; i < len(ms); i += 2 { 394 | if ms[i] < 0 || ms[i+1] < 0 { 395 | got = append(got, "") 396 | continue 397 | } 398 | s := nrunes - int(ms[i+1]) 399 | e := nrunes - int(ms[i]) 400 | got = append(got, c.str[s:e]) 401 | } 402 | if !reflect.DeepEqual(got, c.want) { 403 | t.Errorf("New(%q).Find(%q)=%#v, want %#v", re.source, c.str, got, c.want) 404 | } 405 | } 406 | 407 | func reverse(str string) (io.RuneReader, int) { 408 | n := utf8.RuneCountInString(str) 409 | return &revReader{str: str, i: n}, n 410 | } 411 | 412 | type revReader struct { 413 | str string 414 | i int 415 | } 416 | 417 | func (rr *revReader) ReadRune() (rune, int, error) { 418 | if rr.i == 0 { 419 | return 0, 0, io.EOF 420 | } 421 | r, w := utf8.DecodeLastRuneInString(rr.str[:rr.i]) 422 | rr.i -= w 423 | return r, w, nil 424 | } 425 | -------------------------------------------------------------------------------- /ui/col.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "image" 5 | "image/draw" 6 | "unicode" 7 | 8 | "github.com/eaburns/T/rope" 9 | "github.com/eaburns/T/text" 10 | ) 11 | 12 | // A Col is a column of sheets. 13 | type Col struct { 14 | win *Win 15 | size image.Point 16 | rows []Row 17 | heights []float64 // frac of height 18 | resizing int // row index being resized or -1 19 | Row // focus 20 | } 21 | 22 | // NewCol returns a new column. 23 | func NewCol(w *Win) *Col { 24 | var ( 25 | bodyTextStyles = [...]text.Style{ 26 | {FG: fg, BG: colBG, Face: w.face}, 27 | {BG: hiBG1}, 28 | {BG: hiBG2}, 29 | {BG: hiBG3}, 30 | } 31 | ) 32 | bg := NewTextBox(w, bodyTextStyles, image.ZP) 33 | bg.SetText(rope.New(colText)) 34 | return &Col{ 35 | win: w, 36 | rows: []Row{bg}, 37 | heights: []float64{1.0}, 38 | resizing: -1, 39 | Row: bg, 40 | } 41 | } 42 | 43 | // Add adds an element to the column. 44 | func (c *Col) Add(row Row) { 45 | switch n := len(c.rows); n { 46 | case 0: 47 | panic("impossible") 48 | case 1: 49 | c.heights = []float64{0.05, 1.0} 50 | default: 51 | h0 := c.heights[n-2] 52 | c.heights[n-1] = h0 + (1.0-h0)*0.5 53 | c.heights = append(c.heights, 1.0) 54 | } 55 | c.rows = append(c.rows, row) 56 | c.Resize(c.size) 57 | setColFocus(c, row) 58 | } 59 | 60 | // Del deletes a row from the column. 61 | func (c *Col) Del(r Row) { 62 | i := rowIndex(c, r) 63 | if i == 0 { 64 | return 65 | } 66 | c.rows = append(c.rows[:i], c.rows[i+1:]...) 67 | c.heights = append(c.heights[:i-1], c.heights[i:]...) 68 | setColFocus(c, c.rows[i-1]) 69 | c.Resize(c.size) 70 | } 71 | 72 | func rowIndex(c *Col, r Row) int { 73 | for i := range c.rows { 74 | if c.rows[i] == r { 75 | return i 76 | } 77 | } 78 | return -1 79 | } 80 | 81 | // Tick handles tick events. 82 | func (c *Col) Tick() bool { 83 | var redraw bool 84 | for _, r := range c.rows { 85 | if r.Tick() { 86 | redraw = true 87 | } 88 | } 89 | return redraw 90 | } 91 | 92 | // HandleBounds returns the bounding box of the handle. 93 | func (c *Col) HandleBounds() image.Rectangle { 94 | y1 := y1(c, 0) 95 | if y1 > c.win.lineHeight { 96 | y1 = c.win.lineHeight 97 | } 98 | return image.Rect(c.size.X-c.win.lineHeight, 0, c.size.X, y1) 99 | } 100 | 101 | // Draw draws the column. 102 | func (c *Col) Draw(dirty bool, drawImage draw.Image) { 103 | img := drawImage.(*image.RGBA) 104 | if c.size != img.Bounds().Size() { 105 | c.Resize(img.Bounds().Size()) 106 | } 107 | 108 | for i, o := range c.rows { 109 | r := img.Bounds() 110 | r.Min.Y = img.Bounds().Min.Y + y0(c, i) 111 | r.Max.Y = img.Bounds().Min.Y + y1(c, i) 112 | if i == 0 { 113 | r0 := r 114 | r0.Max.X = drawColHandle(c, img) 115 | o.Draw(dirty, img.SubImage(r0).(*image.RGBA)) 116 | } else { 117 | o.Draw(dirty, img.SubImage(r).(*image.RGBA)) 118 | } 119 | if i < len(c.rows)-1 { 120 | r.Min.Y = r.Max.Y 121 | r.Max.Y += framePx 122 | fillRect(img, frameBG, r) 123 | } 124 | } 125 | } 126 | 127 | func drawColHandle(c *Col, img *image.RGBA) int { 128 | const pad = 6 129 | handle := c.HandleBounds().Add(img.Bounds().Min) 130 | r := handle 131 | r.Max.Y = r.Min.Y + int(c.heights[0]*float64(c.size.Y)) 132 | fillRect(img, colBG, r) 133 | fillRect(img, tagBG, handle.Inset(pad)) 134 | return r.Min.X 135 | } 136 | 137 | // Resize handles resize events. 138 | func (c *Col) Resize(size image.Point) { 139 | dy := dy(c) 140 | // Preserve the height of the column background in pixels, not percent. 141 | h0 := c.heights[0] * dy 142 | c.size = size 143 | c.heights[0] = clampFrac(h0 / dy) 144 | 145 | if nr := len(c.rows); int(dy) < nr*c.win.lineHeight+nr*framePx { 146 | // Too small to fit everything. 147 | // Space out as much as we can, 148 | // so if the window grows, 149 | // everything is in a good place. 150 | c.heights[0] = 0.0 151 | for i := 1; i < nr; i++ { 152 | c.heights[i] = c.heights[i-1] + 1.0/float64(nr) 153 | } 154 | } 155 | c.heights[len(c.heights)-1] = 1.0 156 | 157 | for i, o := range c.rows { 158 | a, b := y0(c, i), y1(c, i) 159 | if i == 0 { 160 | o.Resize(image.Pt(size.X-c.HandleBounds().Dx(), b-a)) 161 | continue 162 | } 163 | if b-a < c.win.lineHeight { 164 | // The row got too small. 165 | // Slide the next up to fit. 166 | y := i*framePx + a + c.win.lineHeight 167 | c.heights[i] = clampFrac(float64(y) / dy) 168 | b = y1(c, i) 169 | } 170 | o.Resize(image.Pt(size.X, b-a)) 171 | } 172 | } 173 | 174 | // Move handles mouse move events. 175 | func (c *Col) Move(pt image.Point) { 176 | if c.resizing >= 0 { 177 | switch i := colIndex(c); { 178 | case i > 0 && pt.X < c.win.lineHeight: 179 | moveRow(c.win, c.resizing+1, c, c.win.cols[i-1], pt.Y) 180 | case i < len(c.win.cols)-1 && pt.X > c.size.X+c.win.lineHeight: 181 | moveRow(c.win, c.resizing+1, c, c.win.cols[i+1], pt.Y) 182 | default: 183 | resizeRow(c, pt) 184 | } 185 | return 186 | } 187 | pt.Y -= y0(c, focusedRow(c)) 188 | c.Row.Move(pt) 189 | } 190 | 191 | // colIndex returns the index of the Col its Win's cols array. 192 | func colIndex(c *Col) int { 193 | for i := range c.win.cols { 194 | if c.win.cols[i] == c { 195 | return i 196 | } 197 | } 198 | return -1 199 | } 200 | 201 | func moveRow(w *Win, ri int, src, dst *Col, y int) { 202 | e := src.rows[ri] 203 | if src.Row != e { 204 | panic("impossible") 205 | } 206 | 207 | // Remove the row. 208 | src.rows = append(src.rows[:ri], src.rows[ri+1:]...) 209 | src.heights = append(src.heights[:ri-1], src.heights[ri:]...) 210 | 211 | // Focus the dest column and add the row. 212 | frac := clampFrac(float64(y) / float64(dst.size.Y)) 213 | for ri = range dst.heights { 214 | if frac <= dst.heights[ri] { 215 | break 216 | } 217 | } 218 | // TODO: only move a sheet if the dst can fit it. 219 | dst.rows = append(dst.rows[:ri+1], append([]Row{e}, dst.rows[ri+1:]...)...) 220 | dst.heights = append(dst.heights[:ri], append([]float64{frac}, dst.heights[ri:]...)...) 221 | 222 | if dst != src { 223 | src.resizing = -1 224 | src.Row = src.rows[0] 225 | src.Resize(src.size) 226 | dst.Row = e 227 | } 228 | dst.resizing = ri 229 | dst.Resize(dst.size) 230 | w.Col = dst 231 | } 232 | 233 | func resizeRow(c *Col, pt image.Point) { 234 | // Try to center the mouse on line 1. 235 | newY := c.resizing*framePx + pt.Y - c.win.lineHeight/2 236 | 237 | // Clamp to a multiple of line height. 238 | snap := c.win.lineHeight 239 | clamp0 := snap * (newY / snap) 240 | clamp1 := clamp0 + snap 241 | if newY-clamp0 < clamp1-newY { 242 | newY = clamp0 243 | } else { 244 | newY = clamp1 245 | } 246 | 247 | // Clamp to the window. 248 | dy := dy(c) 249 | frac := clampFrac(float64(newY) / dy) 250 | 251 | var prev int 252 | if c.resizing > 0 { 253 | prev = y0(c, c.resizing) 254 | } 255 | next := y1(c, c.resizing+1) 256 | 257 | // Swap with above or below row in the same column. 258 | if pt.Y < prev-c.win.lineHeight-framePx || 259 | pt.Y > next+c.win.lineHeight+framePx { 260 | moveRow(c.win, c.resizing+1, c, c, pt.Y) 261 | return 262 | } 263 | 264 | // Disallow the previous row from getting too small. 265 | if c.resizing > 0 && newY-prev < c.win.lineHeight { 266 | frac = float64(prev+c.win.lineHeight) / dy 267 | } 268 | // Disallow the current row from getting too small. 269 | if next-newY-framePx < c.win.lineHeight { 270 | frac = float64(next-c.win.lineHeight-framePx) / dy 271 | } 272 | 273 | if c.heights[c.resizing] != frac { 274 | c.heights[c.resizing] = frac 275 | c.Resize(c.size) 276 | } 277 | } 278 | 279 | func clampFrac(f float64) float64 { 280 | switch { 281 | case f < 0.0: 282 | return 0.0 283 | case f > 1.0: 284 | return 1.0 285 | default: 286 | return f 287 | } 288 | } 289 | 290 | // Wheel handles mouse wheel events. 291 | func (c *Col) Wheel(pt image.Point, x, y int) { 292 | for i, r := range c.rows { 293 | if pt.Y < y1(c, i) { 294 | pt.Y -= y0(c, i) 295 | r.Wheel(pt, x, y) 296 | return 297 | } 298 | } 299 | } 300 | 301 | // Click handles click events. 302 | func (c *Col) Click(pt image.Point, button int) { 303 | if c.resizing >= 0 && button == -1 { 304 | c.resizing = -1 305 | return 306 | } 307 | 308 | if button == 1 { 309 | for i := range c.rows[:len(c.rows)] { 310 | r := c.rows[i] 311 | handler, ok := r.(interface{ HandleBounds() image.Rectangle }) 312 | if !ok { 313 | continue 314 | } 315 | handle := handler.HandleBounds().Add(image.Pt(0, y0(c, i))) 316 | if pt.In(handle) { 317 | setColFocus(c, r) 318 | c.resizing = i - 1 319 | return 320 | } 321 | } 322 | } 323 | 324 | if button > 0 { 325 | setColFocusPt(c, pt) 326 | } 327 | 328 | pt.Y -= y0(c, focusedRow(c)) 329 | button, addr := c.Row.Click(pt, button) 330 | if button == -2 || button == -3 { 331 | tb := getTextBox(c.Row) 332 | if tb == nil { 333 | return 334 | } 335 | var err error 336 | s := getSheet(c.Row) 337 | txt := getClickText(tb, addr) 338 | switch button { 339 | case -2: 340 | err = execCmd(c, s, txt) 341 | case -3: 342 | err = lookText(c, s, txt) 343 | } 344 | if err != nil { 345 | c.win.OutputString(err.Error() + "\n") 346 | } 347 | } 348 | } 349 | 350 | func setColFocusPt(c *Col, pt image.Point) { 351 | for i, o := range c.rows { 352 | if pt.Y < y1(c, i) { 353 | setColFocus(c, o) 354 | return 355 | } 356 | } 357 | } 358 | 359 | func getClickText(tb *TextBox, addr [2]int64) string { 360 | if addr[0] < addr[1] { 361 | return rope.Slice(tb.text, addr[0], addr[1]).String() 362 | } 363 | if dot := tb.dots[1].At; dot[0] <= addr[0] && addr[0] < dot[1] { 364 | return rope.Slice(tb.text, dot[0], dot[1]).String() 365 | } 366 | 367 | front, back := rope.Split(tb.text, addr[0]) 368 | start := rope.LastIndexFunc(front, unicode.IsSpace) 369 | if start < 0 { 370 | start = 0 371 | } else { 372 | start++ 373 | } 374 | end := rope.IndexFunc(back, unicode.IsSpace) 375 | if end < 0 { 376 | end = tb.text.Len() 377 | } else { 378 | end += addr[0] 379 | } 380 | return rope.Slice(tb.text, start, end).String() 381 | } 382 | 383 | func getTextBox(r Row) *TextBox { 384 | switch r := r.(type) { 385 | case *Sheet: 386 | return r.TextBox 387 | case *TextBox: 388 | return r 389 | default: 390 | return nil 391 | } 392 | } 393 | 394 | func getSheet(r Row) *Sheet { 395 | if r, ok := r.(*Sheet); ok { 396 | return r 397 | } 398 | return nil 399 | } 400 | 401 | func y0(c *Col, i int) int { 402 | if i == 0 { 403 | return 0 404 | } 405 | return y1(c, i-1) + framePx 406 | } 407 | 408 | func y1(c *Col, i int) int { return int(c.heights[i] * dy(c)) } 409 | 410 | func dy(c *Col) float64 { return float64(c.size.Y) } 411 | 412 | func setColFocus(c *Col, r Row) { 413 | if c.Row == r { 414 | return 415 | } 416 | c.Row.Focus(false) 417 | r.Focus(true) 418 | c.Row = r 419 | } 420 | 421 | func focusedRow(c *Col) int { 422 | for i, o := range c.rows { 423 | if o == c.Row { 424 | return i 425 | } 426 | } 427 | return 0 428 | } 429 | -------------------------------------------------------------------------------- /re1/re1.go: -------------------------------------------------------------------------------- 1 | // Package re1 implements a very simple regular expression language. 2 | // The language is inspired by plan9 regular expressions 3 | // (https://9fans.github.io/plan9port/man/man7/regexp.html), 4 | // rsc's regexp blog posts (https://swtch.com/~rsc/regexp), 5 | // and nominally by the much more sohpistocated RE2 library 6 | // (https://github.com/google/re2). 7 | // 8 | // The grammar is: 9 | // regexp = choice. 10 | // choice = concat [ "|" choice ]. 11 | // concat = repeat [ concat ]. 12 | // repeat = term { "*" | "+" | "?" }. 13 | // term = "." | "^" | "$" | "(" regexp ")" | charclass | literal. 14 | // charclass = "[" [ "^" ] ( classlit [ "-" classlit ] ) { classlit [ "-" classlit ] } "]". 15 | // A literal is any non-meta rune or a rune preceded by \. 16 | // A classlit is any non-"]", non-"-" rune or a rune preceded by \. 17 | // 18 | // The meta characters are: 19 | // | choice 20 | // * zero or more, greedy 21 | // + one or more, greedy 22 | // ? zero or one 23 | // . any non-newline rune 24 | // ^ beginning of file or line 25 | // $ end of file or line 26 | // () capturing group 27 | // [] character class (^ negates, - is a range) 28 | // \n newline 29 | // \t tab 30 | // \ otherwise is the literal of the following rune 31 | // or is \ itself if there is no following rune. 32 | package re1 33 | 34 | import ( 35 | "errors" 36 | "io" 37 | "strings" 38 | "unicode/utf8" 39 | ) 40 | 41 | // Regexp is a compiled regular expression. 42 | type Regexp struct { 43 | prog []instr 44 | ncap int 45 | class [][][2]rune 46 | source string 47 | } 48 | 49 | // Opts are compile-time options. The zero value is default. 50 | type Opts struct { 51 | // Reverse compiles the expression for reverse matching. 52 | // This swaps the order of concatenations, and it swaps ^ and $. 53 | Reverse bool 54 | // Delimiter specifies a rune that delimits parsing if unescaped. 55 | Delimiter rune 56 | // ID is a user-specfied ID to identify the the regexp. 57 | // This is used to distinguish which regexp matched 58 | // when concatenating multiple regexps into one. 59 | ID int 60 | } 61 | 62 | // New compiles a regular expression. 63 | // The expression is terminated by the end of the string, 64 | // an un-escaped newline, 65 | // or an un-escaped delimiter (if set in opts). 66 | func New(t string, opts Opts) (*Regexp, string, error) { 67 | src := t 68 | switch re, t, err := choice(t, 0, opts); { 69 | case err != nil: 70 | return nil, "", err 71 | case re == nil: 72 | re = &Regexp{} 73 | fallthrough 74 | default: 75 | re = groupProg(re) 76 | re.prog = append(re.prog, instr{op: match, arg: opts.ID}) 77 | re.source = src 78 | return re, t, nil 79 | } 80 | } 81 | 82 | const ( 83 | any = -iota 84 | bol 85 | eol 86 | class // arg is class index 87 | nclass // arg is class index 88 | jmp // arg is jump offset 89 | fork // arg is low-priority fork offset (high-priority is 1) 90 | rfork // arg is high-priority fork offset (low-priority is 1) 91 | save // arg is save-to index 92 | match // arg is the id of the matching regexp 93 | ) 94 | 95 | type instr struct { 96 | op int 97 | arg int 98 | } 99 | 100 | func choice(t0 string, depth int, opts Opts) (*Regexp, string, error) { 101 | switch left, t, err := concat(t0, depth, opts); { 102 | case err != nil: 103 | return nil, "", err 104 | case peek(t) != '|': 105 | return left, t, nil 106 | case left == nil: 107 | return nil, "", errors.New("unexpected |") 108 | default: 109 | _, t = next(t) // eat | 110 | var right *Regexp 111 | if right, t, err = choice(t, depth, opts); err != nil { 112 | return nil, "", err 113 | } 114 | return choiceProg(left, right), t, nil 115 | } 116 | } 117 | 118 | func concat(t string, depth int, opts Opts) (*Regexp, string, error) { 119 | left, t, err := repeat(t, depth, opts) 120 | if left == nil || err != nil { 121 | return left, t, err 122 | } 123 | var right *Regexp 124 | switch right, t, err = concat(t, depth, opts); { 125 | case err != nil: 126 | return nil, "", err 127 | case right != nil: 128 | left = catProg(left, right, opts.Reverse) 129 | fallthrough 130 | default: 131 | return left, t, err 132 | } 133 | } 134 | 135 | func repeat(t string, depth int, opts Opts) (*Regexp, string, error) { 136 | left, t, err := term(t, depth, opts) 137 | if left == nil || err != nil { 138 | return left, t, err 139 | } 140 | for strings.ContainsRune("*+?", peek(t)) { 141 | var r rune 142 | r, t = next(t) 143 | left = repProg(left, r) 144 | } 145 | return left, t, nil 146 | } 147 | 148 | func term(t0 string, depth int, opts Opts) (*Regexp, string, error) { 149 | switch r, t := next(t0); r { 150 | case eof, '\n', opts.Delimiter: 151 | return nil, t, nil 152 | case '\\': 153 | r, t = esc(t) 154 | fallthrough 155 | default: 156 | return opProg(int(r)), t, nil 157 | case '.': 158 | return opProg(any), t, nil 159 | case '^': 160 | if opts.Reverse { 161 | return opProg(eol), t, nil 162 | } 163 | return opProg(bol), t, nil 164 | case '$': 165 | if opts.Reverse { 166 | return opProg(bol), t, nil 167 | } 168 | return opProg(eol), t, nil 169 | case '(': 170 | return group(t, depth, opts) 171 | case '[': 172 | return charclass(t) 173 | case '|': 174 | return nil, t0, nil 175 | case ')': 176 | if depth == 0 { 177 | return nil, t, errors.New("unopened )") 178 | } 179 | return nil, t0, nil 180 | case '*', '+', '?': 181 | return nil, "", errors.New("unexpected " + string([]rune{r})) 182 | } 183 | } 184 | 185 | func group(t string, depth int, opts Opts) (*Regexp, string, error) { 186 | left, t, err := choice(t, depth+1, opts) 187 | switch r, t := next(t); { 188 | case err != nil: 189 | return nil, "", err 190 | case r != ')': 191 | return nil, "", errors.New("unclosed (") 192 | case left == nil: 193 | left = &Regexp{} 194 | fallthrough 195 | default: 196 | return groupProg(left), t, nil 197 | } 198 | } 199 | 200 | func charclass(t string) (*Regexp, string, error) { 201 | op := class 202 | if peek(t) == '^' { 203 | _, t = next(t) // eat ^ 204 | op = nclass 205 | } 206 | var r, p rune 207 | var cl [][2]rune 208 | for len(t) > 0 { 209 | switch r, t = next(t); r { 210 | case ']': 211 | if p != 0 { 212 | cl = append(cl, [2]rune{p, p}) 213 | } 214 | if len(cl) == 0 { 215 | return nil, "", errors.New("empty charclass") 216 | } 217 | return charClassProg(op, cl), t, nil 218 | case '-': 219 | if p == 0 || peek(t) == ']' || peek(t) == '-' { 220 | return nil, "", errors.New("bad range") 221 | } 222 | r, t = next(t) 223 | if r == '\\' { 224 | r, t = esc(t) 225 | } 226 | if p >= r { 227 | return nil, "", errors.New("bad range") 228 | } 229 | cl = append(cl, [2]rune{p, r}) 230 | p = 0 231 | case '\\': 232 | r, t = esc(t) 233 | fallthrough 234 | default: 235 | if p != 0 { 236 | cl = append(cl, [2]rune{p, p}) 237 | } 238 | p = r 239 | } 240 | } 241 | return nil, "", errors.New("unclosed [") 242 | } 243 | 244 | func choiceProg(left, right *Regexp) *Regexp { 245 | prog := make([]instr, 0, 2+len(left.prog)+len(right.prog)) 246 | prog = append(prog, instr{op: fork, arg: len(left.prog) + 2}) 247 | prog = append(prog, left.prog...) 248 | left.prog = append(prog, instr{op: jmp, arg: len(right.prog) + 1}) 249 | return catProg(left, right, false) 250 | } 251 | 252 | func catProg(left, right *Regexp, rev bool) *Regexp { 253 | for i := range right.prog { 254 | instr := &right.prog[i] 255 | if instr.op == save { 256 | instr.arg += left.ncap * 2 257 | } 258 | } 259 | if rev { 260 | left, right = right, left 261 | } 262 | for _, instr := range right.prog { 263 | if instr.op == class || instr.op == nclass { 264 | instr.arg += len(left.class) 265 | } 266 | left.prog = append(left.prog, instr) 267 | } 268 | left.ncap += right.ncap 269 | left.class = append(left.class, right.class...) 270 | return left 271 | } 272 | 273 | func repProg(left *Regexp, op rune) *Regexp { 274 | prog := make([]instr, 0, len(left.prog)+2) 275 | switch op { 276 | case '+': 277 | prog = append(left.prog, instr{op: rfork, arg: -len(left.prog)}) 278 | case '*': 279 | prog = []instr{{op: fork, arg: len(left.prog) + 2}} 280 | prog = append(prog, left.prog...) 281 | prog = append(prog, instr{op: rfork, arg: -len(prog) + 1}) 282 | case '?': 283 | prog = []instr{{op: fork, arg: len(left.prog) + 1}} 284 | prog = append(prog, left.prog...) 285 | } 286 | left.prog = prog 287 | return left 288 | } 289 | 290 | func groupProg(left *Regexp) *Regexp { 291 | prog := make([]instr, 0, len(left.prog)+2) 292 | prog = append(prog, instr{op: save, arg: 0}) 293 | for _, instr := range left.prog { 294 | if instr.op == save { 295 | instr.arg += 2 296 | } 297 | prog = append(prog, instr) 298 | } 299 | left.prog = append(prog, instr{op: save, arg: 1}) 300 | left.ncap++ 301 | return left 302 | } 303 | 304 | func charClassProg(op int, cl [][2]rune) *Regexp { 305 | return &Regexp{prog: []instr{{op: op}}, class: [][][2]rune{cl}} 306 | } 307 | 308 | func opProg(op int) *Regexp { return &Regexp{prog: []instr{{op: op}}} } 309 | 310 | const eof = -1 311 | 312 | func next(t string) (rune, string) { 313 | if len(t) == 0 { 314 | return eof, "" 315 | } 316 | r, w := utf8.DecodeRuneInString(t) 317 | return r, t[w:] 318 | } 319 | 320 | func peek(t string) rune { 321 | r, _ := next(t) 322 | return r 323 | } 324 | 325 | func esc(t string) (rune, string) { 326 | var r rune 327 | switch r, t = next(t); r { 328 | case eof: 329 | r = '\\' 330 | case 'n': 331 | r = '\n' 332 | case 't': 333 | r = '\t' 334 | } 335 | return r, t 336 | } 337 | 338 | // Find returns nil on no match or a slice with pairs of int64s for each sub-expression match (0 is the full match) and the last element is the matching regexp ID. 339 | func (re *Regexp) Find(rr io.RuneReader) []int64 { return run(newVM(re, rr)) } 340 | 341 | type vm struct { 342 | re *Regexp 343 | rr io.RuneReader 344 | at, lim int64 345 | c, n rune // cur and next rune. 346 | seen []int64 // at for which each pc was last add()ed. 347 | cur, next []thread 348 | free [][]int64 349 | match []int64 350 | } 351 | 352 | type thread struct { 353 | pc int 354 | mem []int64 355 | } 356 | 357 | func newVM(re *Regexp, rr io.RuneReader) *vm { 358 | v := &vm{re: re, rr: rr, lim: -1, c: eof, n: eof} 359 | v.seen = make([]int64, len(re.prog)) 360 | for i := range v.seen { 361 | v.seen[i] = -1 362 | } 363 | read(v) 364 | return v 365 | } 366 | 367 | func newMem(v *vm, init []int64) (m []int64) { 368 | if n := len(v.free); n > 0 { 369 | m, v.free = v.free[n-1], v.free[:n-1] 370 | } else { 371 | m = make([]int64, 2*v.re.ncap+1 /* match ID */) 372 | } 373 | if init != nil { 374 | copy(m, init) 375 | return m 376 | } 377 | for i := range m { 378 | m[i] = -1 379 | } 380 | return m 381 | } 382 | 383 | func run(v *vm) []int64 { 384 | for { 385 | if v.match == nil { 386 | add(v, 0, newMem(v, nil)) 387 | } 388 | if v.lim >= 0 && v.at >= v.lim { 389 | // Check this after add() to allow empty regexps to match empty. 390 | return v.match 391 | } 392 | read(v) 393 | v.cur, v.next = v.next, v.cur[:0] 394 | for _, t := range v.cur { 395 | step(v, t.pc, t.mem) 396 | } 397 | if v.c == eof || (v.match != nil && len(v.next) == 0) { 398 | return v.match 399 | } 400 | } 401 | } 402 | 403 | func read(v *vm) { 404 | if v.n != eof { 405 | v.at += int64(utf8.RuneLen(v.n)) 406 | } 407 | v.c = v.n 408 | var err error 409 | if v.n, _, err = v.rr.ReadRune(); err != nil { 410 | v.n = eof 411 | } 412 | } 413 | 414 | func step(v *vm, pc int, mem []int64) { 415 | if !accepts(v, v.re.prog[pc]) { 416 | v.free = append(v.free, mem) 417 | return 418 | } 419 | add(v, pc+1, mem) 420 | } 421 | 422 | func accepts(v *vm, instr instr) bool { 423 | switch instr.op { 424 | case any: 425 | return v.c != '\n' && v.c != eof 426 | case class, nclass: 427 | cl := v.re.class[instr.arg] 428 | return classAccepts(v.c, cl, instr.op == nclass) 429 | default: 430 | return int(v.c) == instr.op 431 | } 432 | } 433 | 434 | func classAccepts(r rune, class [][2]rune, neg bool) bool { 435 | if r == eof { 436 | return false 437 | } 438 | for _, c := range class { 439 | if c[0] <= r && r <= c[1] { 440 | return !neg 441 | } 442 | } 443 | return neg 444 | } 445 | 446 | func add(v *vm, pc int, mem []int64) { 447 | if v.seen[pc] == v.at { 448 | v.free = append(v.free, mem) 449 | return 450 | } 451 | v.seen[pc] = v.at 452 | _add(v, pc, mem) 453 | } 454 | 455 | func _add(v *vm, pc int, mem []int64) { 456 | switch instr := v.re.prog[pc]; instr.op { 457 | default: 458 | v.next = append(v.next, thread{pc: pc, mem: mem}) 459 | case jmp: 460 | add(v, pc+instr.arg, mem) 461 | case fork: 462 | clone := newMem(v, mem) 463 | add(v, pc+1, mem) 464 | add(v, pc+instr.arg, clone) 465 | case rfork: 466 | clone := newMem(v, mem) 467 | add(v, pc+instr.arg, mem) 468 | add(v, pc+1, clone) 469 | case save: 470 | mem[instr.arg] = v.at 471 | add(v, pc+1, mem) 472 | case bol: 473 | if v.c != eof && v.c != '\n' { 474 | v.free = append(v.free, mem) 475 | return 476 | } 477 | add(v, pc+1, mem) 478 | case eol: 479 | if v.n != eof && v.n != '\n' { 480 | v.free = append(v.free, mem) 481 | return 482 | } 483 | add(v, pc+1, mem) 484 | case match: 485 | mem[len(mem)-1] = int64(instr.arg) 486 | setMatch(v, mem) 487 | } 488 | } 489 | 490 | func setMatch(v *vm, mem []int64) { 491 | switch { 492 | case v.match == nil: 493 | v.match = mem 494 | case mem[0] <= v.match[0] && mem[1] > v.match[1]: 495 | v.free = append(v.free, v.match) 496 | v.match = mem 497 | default: 498 | v.free = append(v.free, mem) 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /rope/rope_test.go: -------------------------------------------------------------------------------- 1 | package rope 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | "unicode/utf8" 10 | ) 11 | 12 | func TestEmpty(t *testing.T) { 13 | e := Empty() 14 | if s := e.String(); s != "" { 15 | t.Errorf("Empty().String()=%q, want \"\"", s) 16 | } 17 | if l := e.Len(); l != 0 { 18 | t.Errorf("Empty().Len()=%d, want 0", l) 19 | } 20 | } 21 | 22 | func TestNew(t *testing.T) { 23 | tests := []string{ 24 | "", 25 | "Hello, World", 26 | "Hello, 世界", 27 | strings.Repeat("Hello, 世界", smallSize*2/len("Hello, 世界")), 28 | } 29 | for _, test := range tests { 30 | r := New(test) 31 | if got := r.String(); got != test { 32 | t.Errorf("New(%q)=%q", test, got) 33 | } 34 | if r.Len() != int64(len(test)) { 35 | t.Errorf("New(%q).Len()=%d, want %d", test, r.Len(), len(test)) 36 | } 37 | } 38 | } 39 | 40 | func TestReadFrom(t *testing.T) { 41 | tests := []string{ 42 | "", 43 | "Hello, World", 44 | "Hello, 世界", 45 | strings.Repeat("Hello, 世界", smallSize*2/len("Hello, 世界")), 46 | } 47 | for _, test := range tests { 48 | r, err := ReadFrom(strings.NewReader(test)) 49 | if err != nil { 50 | t.Errorf("ReadFrom(%q)=_,%v", test, err) 51 | continue 52 | } 53 | if got := r.String(); got != test { 54 | t.Errorf("ReadFrom(%q)=%q", test, got) 55 | } 56 | if r.Len() != int64(len(test)) { 57 | t.Errorf("New(%q).Len()=%d, want %d", test, r.Len(), len(test)) 58 | } 59 | } 60 | } 61 | 62 | func TestAppendEmpty(t *testing.T) { 63 | r := Append(Empty(), New("x")) 64 | l, ok := r.(*leaf) 65 | if !ok || l.text != "x" { 66 | t.Errorf(`Append(Empty(), New("x"))=%v, wanted &leaf{"x"}`, r) 67 | } 68 | 69 | r = Append(New("x"), Empty()) 70 | l, ok = r.(*leaf) 71 | if !ok || l.text != "x" { 72 | t.Errorf(`Append(New("x"), Empty())=%v, wanted &leaf{"x"}`, r) 73 | } 74 | } 75 | 76 | func TestAppendSmall(t *testing.T) { 77 | xs := strings.Repeat("x", smallSize-1) 78 | 79 | r := Append(New(xs), New("y")) 80 | l, ok := r.(*leaf) 81 | if !ok || l.text != xs+"y" { 82 | t.Errorf(`Append(New(%q), New("y"))=%#v, wanted &leaf{%q}`, xs, r, xs+"y") 83 | } 84 | 85 | r = Append(New("y"), New(xs)) 86 | l, ok = r.(*leaf) 87 | if !ok || l.text != "y"+xs { 88 | t.Errorf(`Append(New("y"), New(%q))=%#v, wanted &leaf{%q}`, xs, r, "y"+xs) 89 | } 90 | } 91 | 92 | func TestAppendSmallNiece(t *testing.T) { 93 | // If the right child of the left node is a leaf, 94 | // and appending to it would keep the size 95 | // beneath smallSize, then just append to it. 96 | 97 | xs := strings.Repeat("x", smallSize-1) 98 | niece := &node{ 99 | left: &leaf{text: "w"}, 100 | right: &leaf{text: xs}, 101 | len: smallSize, 102 | } 103 | 104 | expect := &node{ 105 | left: &leaf{text: "w"}, 106 | right: &leaf{text: xs + "y"}, 107 | len: smallSize + 1, 108 | } 109 | if r := Append(niece, New("y")); !reflect.DeepEqual(r, expect) { 110 | t.Errorf("got %#v (%s), want %#v", r, r, expect) 111 | } 112 | } 113 | 114 | func TestSplit(t *testing.T) { 115 | for i := 0; i < len(deepText); i++ { 116 | want := [2]string{deepText[:i], deepText[i:]} 117 | r0, r1 := Split(deepRope, int64(i)) 118 | got := [2]string{r0.String(), r1.String()} 119 | if got != want { 120 | t.Errorf("Split(%q, %d)=%q, want %q", deepRope, i, got, want) 121 | } 122 | } 123 | } 124 | 125 | func TestDelete(t *testing.T) { 126 | for i := 0; i < len(deepText); i++ { 127 | for n := 1; n < len(deepText)-i; n++ { 128 | want := deepText[:i] + deepText[i+n:] 129 | r := Delete(deepRope, int64(i), int64(n)) 130 | if got := r.String(); got != want { 131 | t.Errorf("Delete(%q, %d, %d)=%q, want %q", 132 | deepRope, i, n, got, want) 133 | } 134 | } 135 | } 136 | } 137 | 138 | func TestInsert(t *testing.T) { 139 | big := strings.Repeat("x", smallSize*2) 140 | small := "x" 141 | for i := 0; i < len(deepText); i++ { 142 | for _, ins := range [...]string{big, small} { 143 | want := deepText[:i] + ins + deepText[i:] 144 | r := Insert(deepRope, int64(i), New(ins)) 145 | if got := r.String(); got != want { 146 | t.Errorf("Insert(%q, %d, %q)=%q, want %q", 147 | deepRope, i, ins, got, want) 148 | } 149 | } 150 | } 151 | } 152 | 153 | func TestSlice(t *testing.T) { 154 | for i := 0; i < len(deepText); i++ { 155 | for j := i; j < len(deepText); j++ { 156 | want := deepText[i:j] 157 | r := Slice(deepRope, int64(i), int64(j)) 158 | if got := r.String(); got != want { 159 | t.Errorf("Slice(%q, %d, %d)=%q, want %q", 160 | deepRope, i, j, got, want) 161 | } 162 | } 163 | } 164 | } 165 | 166 | func TestRead(t *testing.T) { 167 | tests := []struct { 168 | rope Rope 169 | text string 170 | }{ 171 | {rope: New(""), text: ""}, 172 | {rope: New("Hello, World"), text: "Hello, World"}, 173 | {rope: New("Hello, 世界"), text: "Hello, 世界"}, 174 | {rope: New(longText), text: longText}, 175 | {rope: smallRope, text: smallText}, 176 | {rope: deepRope, text: deepText}, 177 | } 178 | for _, test := range tests { 179 | data, err := ioutil.ReadAll(NewReader(test.rope)) 180 | if err != nil { 181 | t.Errorf("NewReader(%q).Read() error: %v", 182 | test.text, err) 183 | continue 184 | } 185 | if got := string(data); got != test.text { 186 | t.Errorf("NewReader(%q).Read()=%q", test.text, got) 187 | } 188 | } 189 | } 190 | 191 | func TestReadByte(t *testing.T) { 192 | tests := []struct { 193 | rope Rope 194 | text string 195 | }{ 196 | {rope: New(""), text: ""}, 197 | {rope: New("Hello, World"), text: "Hello, World"}, 198 | {rope: New("Hello, 世界"), text: "Hello, 世界"}, 199 | {rope: New(longText), text: longText}, 200 | {rope: smallRope, text: smallText}, 201 | {rope: deepRope, text: deepText}, 202 | } 203 | for _, test := range tests { 204 | data, err := readAllByte(NewReader(test.rope)) 205 | if err != nil { 206 | t.Errorf("NewReader(%q).ReadByte() error: %v", 207 | test.text, err) 208 | continue 209 | } 210 | if got := string(data); got != test.text { 211 | t.Errorf("NewReader(%q).ReadByte()=%q", test.text, got) 212 | } 213 | } 214 | } 215 | 216 | func TestReadRune(t *testing.T) { 217 | tests := []struct { 218 | rope Rope 219 | text string 220 | }{ 221 | {rope: New(""), text: ""}, 222 | {rope: New("Hello, World"), text: "Hello, World"}, 223 | {rope: New("Hello, 世界"), text: "Hello, 世界"}, 224 | {rope: New(longText), text: longText}, 225 | {rope: smallRope, text: smallText}, 226 | {rope: deepRope, text: deepText}, 227 | { 228 | rope: New(repl), 229 | text: repl, 230 | }, 231 | { 232 | // Three bad UTF8 bytes give three replacement characters. 233 | rope: New("\x80\x80\x80abc"), 234 | text: repl + repl + repl + "abc", 235 | }, 236 | { 237 | // The first two bytes of a 3-byte UTF8 character 238 | // give two replacement runes. 239 | rope: New("\xE2\x98abc"), // \xE2\x98\xBA == ☺ 240 | text: repl + repl + "abc", 241 | }, 242 | { 243 | // Interleaved good and bad bytes. 244 | rope: New("\x80α\x80β\x80ξ"), 245 | text: repl + "α" + repl + "β" + repl + "ξ", 246 | }, 247 | } 248 | for _, test := range tests { 249 | got, err := readAllRune(NewReader(test.rope)) 250 | if err != nil { 251 | t.Errorf("NewReader(%q).ReadRune() error: %v", 252 | test.text, err) 253 | continue 254 | } 255 | if got != test.text { 256 | t.Errorf("NewReader(%q).ReadRune()=%q", test.text, got) 257 | } 258 | } 259 | } 260 | 261 | func TestReadRuneThenRead(t *testing.T) { 262 | // Contains the first two bytes of a 3-byte UTF8 rune. 263 | rope := New("\xE2\x98abc") 264 | r := NewReader(rope) 265 | 266 | // Read the first byte of the bad rune. 267 | ru, w, err := r.ReadRune() 268 | if ru != utf8.RuneError || w != 1 || err != nil { 269 | t.Fatalf("r.ReadRune()=%x,%d,%v, want %x,1,nil", ru, w, err, repl) 270 | } 271 | 272 | // Read the next byte with Read 273 | var b [1]byte 274 | n, err := r.Read(b[:]) 275 | if n != 1 || b[0] != 0x98 || err != nil { 276 | t.Fatalf("r.Read(1)=%x,%d,%v, want 0x98,1,nil", b[0], n, err) 277 | } 278 | 279 | // Now read the next rune. 280 | ru, w, err = r.ReadRune() 281 | if ru != 'a' || w != 1 || err != nil { 282 | t.Fatalf("r.ReadRune()=%c,%d,%v, want 'a',1,nil", ru, w, err) 283 | } 284 | } 285 | 286 | func TestReverseRead(t *testing.T) { 287 | tests := []struct { 288 | rope Rope 289 | text string 290 | }{ 291 | {rope: New(""), text: ""}, 292 | {rope: New("Hello, World"), text: reverseBytes("Hello, World")}, 293 | {rope: New("Hello, 世界"), text: reverseBytes("Hello, 世界")}, 294 | {rope: New(longText), text: reverseBytes(longText)}, 295 | {rope: smallRope, text: reverseBytes(smallText)}, 296 | {rope: deepRope, text: reverseBytes(deepText)}, 297 | } 298 | for _, test := range tests { 299 | data, err := ioutil.ReadAll(NewReverseReader(test.rope)) 300 | if err != nil { 301 | t.Errorf("NewReverseReader(%q).Read() error: %v", test.text, err) 302 | continue 303 | } 304 | if got := string(data); got != test.text { 305 | t.Errorf("NewReverseReader(%q).Read()=%q", test.text, got) 306 | } 307 | } 308 | } 309 | 310 | func TestReverseReadByte(t *testing.T) { 311 | tests := []struct { 312 | rope Rope 313 | text string 314 | }{ 315 | {rope: New(""), text: ""}, 316 | {rope: New("Hello, World"), text: reverseBytes("Hello, World")}, 317 | {rope: New("Hello, 世界"), text: reverseBytes("Hello, 世界")}, 318 | {rope: New(longText), text: reverseBytes(longText)}, 319 | {rope: smallRope, text: reverseBytes(smallText)}, 320 | {rope: deepRope, text: reverseBytes(deepText)}, 321 | } 322 | for _, test := range tests { 323 | data, err := readAllByte(NewReverseReader(test.rope)) 324 | if err != nil { 325 | t.Errorf("NewReverseReader(%q).ReadByte() error: %v", 326 | test.text, err) 327 | continue 328 | } 329 | if got := string(data); got != test.text { 330 | t.Errorf("NewReverseReader(%q).ReadByte()=%q", test.text, got) 331 | } 332 | } 333 | } 334 | 335 | func TestReverseReaderReadRune(t *testing.T) { 336 | tests := []struct { 337 | rope Rope 338 | text string 339 | }{ 340 | {rope: New(""), text: ""}, 341 | {rope: New("Hello, World"), text: reverseRunes("Hello, World")}, 342 | {rope: New("Hello, 世界"), text: reverseRunes("Hello, 世界")}, 343 | {rope: New(longText), text: reverseRunes(longText)}, 344 | {rope: smallRope, text: reverseRunes(smallText)}, 345 | {rope: deepRope, text: reverseRunes(deepText)}, 346 | { 347 | rope: New(repl), 348 | text: repl, 349 | }, 350 | { 351 | // Three bad UTF8 bytes give three replacement characters. 352 | rope: New("\x80\x80\x80abc"), 353 | text: "cba" + repl + repl + repl, 354 | }, 355 | { 356 | // The first two bytes of a 3-byte UTF8 character 357 | // give two replacement runes. 358 | rope: New("\xE2\x98abc"), // \xE2\x98\xBA == ☺ 359 | text: "cba" + repl + repl, 360 | }, 361 | { 362 | // Interleaved good and bad bytes. 363 | rope: New("\x80a\x80b\x80c"), 364 | text: "c" + repl + "b" + repl + "a" + repl, 365 | }, 366 | { 367 | // Interleaved good and bad bytes. 368 | rope: New("\x80α\x80β\x80ξ"), 369 | text: "ξ" + repl + "β" + repl + "α" + repl, 370 | }, 371 | } 372 | for _, test := range tests { 373 | got, err := readAllRune(NewReverseReader(test.rope)) 374 | if err != nil { 375 | t.Errorf("NewReverseReader(%q).ReadRune() error: %v", 376 | test.text, err) 377 | continue 378 | } 379 | if got != test.text { 380 | t.Errorf("NewReverseReader(%q).ReadRune()=%q", test.text, got) 381 | } 382 | } 383 | } 384 | 385 | func TestReverseReadRuneThenRead(t *testing.T) { 386 | // The suffix is the first two bytes of a 3-byte UTF8 rune. 387 | rope := New("abc\xE2\x98") 388 | r := NewReverseReader(rope) 389 | 390 | // Read the last byte of the bad rune. 391 | ru, w, err := r.ReadRune() 392 | if ru != utf8.RuneError || w != 1 || err != nil { 393 | t.Fatalf("r.ReadRune()=%x,%d,%v, want %x,1,nil", ru, w, err, repl) 394 | } 395 | 396 | // Read the next byte with Read 397 | var b [1]byte 398 | n, err := r.Read(b[:]) 399 | if n != 1 || b[0] != 0xE2 || err != nil { 400 | t.Fatalf("r.Read(1)=%x,%d,%v, want 0xE2,1,nil", b[0], n, err) 401 | } 402 | 403 | // Now read the next rune. 404 | ru, w, err = r.ReadRune() 405 | if ru != 'c' || w != 1 || err != nil { 406 | t.Fatalf("r.ReadRune()=%c,%d,%v, want 'c',1,nil", ru, w, err) 407 | } 408 | } 409 | 410 | func readAllByte(r io.ByteReader) ([]byte, error) { 411 | var data []byte 412 | for { 413 | b, err := r.ReadByte() 414 | if err == io.EOF { 415 | return data, nil 416 | } 417 | if err != nil { 418 | return nil, err 419 | } 420 | data = append(data, b) 421 | } 422 | } 423 | 424 | func readAllRune(r io.RuneReader) (string, error) { 425 | var data []rune 426 | for { 427 | switch ru, _, err := r.ReadRune(); { 428 | case err == io.EOF: 429 | return string(data), nil 430 | case err != nil: 431 | return "", err 432 | default: 433 | data = append(data, ru) 434 | } 435 | } 436 | } 437 | 438 | func reverseBytes(str string) string { 439 | bs := []byte(str) 440 | for i := 0; i < len(str)/2; i++ { 441 | bs[i], bs[len(bs)-1-i] = bs[len(bs)-1-i], bs[i] 442 | } 443 | return string(bs) 444 | } 445 | 446 | func reverseRunes(str string) string { 447 | rs := []rune(str) 448 | for i := 0; i < len(rs)/2; i++ { 449 | rs[i], rs[len(rs)-1-i] = rs[len(rs)-1-i], rs[i] 450 | } 451 | return string(rs) 452 | } 453 | 454 | const repl = "\xef\xbf\xbd" 455 | 456 | var ( 457 | longText = strings.Repeat("Hello, 世界", smallSize*2/len("Hello, 世界")) 458 | 459 | smallRope, smallText = func() (Rope, string) { 460 | const text = "Hello, 世界" 461 | return &leaf{text: text}, text 462 | }() 463 | 464 | deepRope, deepText = func() (Rope, string) { 465 | r := &node{ 466 | left: &leaf{}, 467 | right: &node{ 468 | left: &node{ 469 | left: &node{ 470 | left: &leaf{text: "H"}, 471 | right: &node{ 472 | left: &leaf{text: "e"}, 473 | right: &leaf{}, 474 | len: 1, 475 | }, 476 | len: 2, 477 | }, 478 | right: &node{ 479 | left: &leaf{text: "l"}, 480 | right: &leaf{text: "l"}, 481 | len: 2, 482 | }, 483 | len: 4, 484 | }, 485 | right: &node{ 486 | left: &node{ 487 | left: &leaf{text: "o"}, 488 | right: &leaf{text: ", "}, 489 | len: 3, 490 | }, 491 | right: &node{ 492 | left: &node{ 493 | left: &leaf{text: "World"}, 494 | right: &leaf{text: "!"}, 495 | len: 6, 496 | }, 497 | right: &leaf{}, 498 | len: 6, 499 | }, 500 | len: 8, 501 | }, 502 | len: 12, 503 | }, 504 | len: 12, 505 | } 506 | return r, "Hello, World!" 507 | }() 508 | ) 509 | 510 | // Some old test that tests who-knows-what. 511 | func TestAppendSplit(t *testing.T) { 512 | tests := []string{ 513 | "", 514 | "Hello, World", 515 | "Hello, 世界", 516 | strings.Repeat("Hello, 世界", smallSize*2/len("Hello, 世界")), 517 | } 518 | for _, test := range tests { 519 | for i := range test { 520 | r := Append(New(test[:i]), New(test[i:])) 521 | ok := true 522 | if got := r.String(); got != test { 523 | t.Errorf("Append(%q, %q)=%q", test[:i], test[i:], got) 524 | ok = false 525 | } 526 | if r.Len() != int64(len(test)) { 527 | t.Errorf("Append(%q, %q).Len()=%d, want %d", 528 | test[:i], test[i:], r.Len(), len(test)) 529 | ok = false 530 | } 531 | if !ok { 532 | continue 533 | } 534 | 535 | for j := range test { 536 | left, right := Split(r, int64(j)) 537 | gotLeft := left.String() 538 | gotRight := right.String() 539 | if gotLeft != test[:j] || gotRight != test[j:] { 540 | t.Errorf("Split(Append(%q, %q))=%q,%q", 541 | test[:i], test[i:], gotLeft, gotRight) 542 | } 543 | if left.Len() != int64(j) { 544 | t.Errorf("Split(Append(%q, %q)).left.Len()=%d, want %d", 545 | test[:i], test[i:], left.Len(), j) 546 | } 547 | if right.Len() != int64(len(test)-j) { 548 | t.Errorf("Split(Append(%q, %q)).right.Len()=%d, want %d", 549 | test[:i], test[i:], right.Len(), len(test)-j) 550 | } 551 | } 552 | } 553 | } 554 | } 555 | -------------------------------------------------------------------------------- /edit/edit.go: -------------------------------------------------------------------------------- 1 | // Package edit implements a subset of the Sam editing language. 2 | // It does not implement undo/redo, or multi-buffer commands. 3 | // However, these could be easily added on top of this implementation. 4 | // 5 | // The langage is described below using an informal, EBNF-like style. 6 | // Items enclosed in brackets, [ ] ,are optional, and 7 | // items in braces, { }, may be repeated 0 or more times. 8 | // 9 | // Some details may differ from the original. 10 | // See https://9fans.github.io/plan9port/man/man1/sam.html 11 | // for the original language description. 12 | // 13 | // Addresses 14 | // 15 | // The edit language is comprised of addresses and commands. 16 | // Addresses identify a sequence of runes in the text. 17 | // They are described by the grammar: 18 | // addr = range. 19 | // 20 | // range = [ relative ] "," [ range ] | [ relative ] ";" [ range ] | relative. 21 | // [a2],[a3] is the string from the start of the address a2 to the end of a3. 22 | // If the a2 is absent, 0 is used. If the a3 is absent, $ is used. 23 | // 24 | // [a2];[a3] is like the previous, 25 | // but with . set to the address a2 before evaluating a3. 26 | // If the a2 is absent, 0 is used. If the a3 is absent, $ is used. 27 | // 28 | // relative = [ simple ] "+" [ relative ] | [ simple ] "-" [ relative ] | simple relative | simple. 29 | // [a1]+[a2] is the address a2 evaluated from the end of a1. 30 | // If the a1 is absent, . is used. If the a2 is absent, 1 is used. 31 | // 32 | // [a1]-[a2] is the address a2 evaluated in reverse from the start of a1. 33 | // If the a1 is absent, . is used. If the a2 is absent, 1 is used. 34 | // 35 | // a1 a2 is the same as a1+a2; the + is inserted. 36 | // 37 | // simple = "$" | "." | "#" digits | digits | "/" regexp [ "/" ]. 38 | // $ is the empty string at the end of the text. 39 | // . is the current address of the editor, called dot. 40 | // #n is the empty string after rune number n. If n is absent then 1 is used. 41 | // n is the nth line in the text. 0 is the string before the first full line. 42 | // / regexp / is the first match of the regular expression going forward. 43 | // 44 | // A regexp is an re1 regular expression delimited by / or a newline. 45 | // (See https://godoc.org/github.com/eaburns/T/re1) 46 | // Regexp matches wrap at the end (or beginning) of the text. 47 | // The resulting match may straddle the starting point. 48 | // 49 | // All operators are left-associative. 50 | // 51 | // Commands 52 | // 53 | // Commands print text or compute diffs on the text which later can be applied. 54 | // 55 | // In the following, the literal "/" can be any non-space rune 56 | // that replaces "/" consistently throughout the production. 57 | // Such a rune acts as a user-chosen delimiter. 58 | // 59 | // For any command that begin with an optional, leading address, 60 | // if the address is elided, then dot is used instead. 61 | // 62 | // A command is one of the following: 63 | // [ addr ] ( "a" | "c" | "i" ) "/" [ text ] [ "/" ]. 64 | // [ addr ] ( "a" | "c" | "i" ) "\n" lines of text "\n.\n". 65 | // Appends a string after the address (a), 66 | // changes the string at the address (c), or 67 | // inserts a string before the address (i). 68 | // 69 | // The text can be supplied in one of two forms: 70 | // 71 | // The first form begins with a non-space rune, the delimiter. 72 | // The text consists of all runes until the end of input, 73 | // a non-escaped newline, or a non-escaped delimiter. 74 | // Pairs of runes beginning with \ are called escapes. 75 | // They are interpreted specially: 76 | // \n is a newline 77 | // \t is a tab 78 | // \ followed by any other rune is that rune. 79 | // If the rune is the delimiter, it is non-delimiting. 80 | // \ followed by no rune (end of input) is \. 81 | // 82 | // For example: 83 | // #3 a/Hello, World!/ 84 | // appends the string "Hello, World!" after the 3rd rune. 85 | // 86 | // The second form begins with a newline and 87 | // ends with a line containing only a period, ".". 88 | // In this form, escapes are not interpreted; they are literal. 89 | // This form is convenient for multi-line text. 90 | // 91 | // For example: 92 | // #3 a 93 | // Hello, 94 | // World 95 | // ! 96 | // . 97 | // appends the string "Hello,\nWorld\n!" after the 3rd rune. 98 | // 99 | // [ addr ] "d". 100 | // Deletes the string at the address. 101 | // 102 | // [ addr ] "t" addr. 103 | // Copies the string from the first address to after the second. 104 | // 105 | // [ addr ] "m" addr. 106 | // Moves the string from the first address to after the second. 107 | // 108 | // [ addr ] "p". 109 | // Prints the string at the address. 110 | // 111 | // [ addr ] "s" [ digits ] "/" regexp "/" [ substitution ] "/" [ "g" ]. 112 | // Substitutes matches of a regexp within the address. 113 | // As above, the regexp uses the re1 syntax. 114 | // 115 | // A number N after s indicates to substitute the Nth match 116 | // of the regular expression in the address range. 117 | // If n == 0, the first match is substituted. 118 | // If the substitution is followed by the letter g, 119 | // all matches in the address range are substituted. 120 | // If both a number N and the letter g are present, 121 | // the Nth match and all following in the address range 122 | // are substituted. 123 | // 124 | // Substitution text replaces matches of the regular expression. 125 | // The substitution text is interpreted the same way as 126 | // the delimited form of the a, c, and i commands, except that 127 | // an escaped digit (0-9) is not interpreted as the digit itself 128 | // but is substituted with text matched by the regexp. 129 | // The digits 1-9 correspond to text matched by capturing groups 130 | // numbered from left-to-right by order of their open parenthesis. 131 | // The 0 is the entire match. 132 | // 133 | // [ addr ] ( "x" | "y" ) "/" regexp "/" command. 134 | // Executes a command for each match of the regular expression in the address. 135 | // 136 | // Dot is set either to each match of the regular expression (x) 137 | // or to the strings before, between, and after the matches (y), 138 | // and the command is executed. 139 | // It is an error if the resulting edits are not in ascending order. 140 | // 141 | // Note, the command is only interpreted if there is a match, 142 | // so a malformed command is not reported if there is no match. 143 | // 144 | // [ addr ] ( "g" | "v" ) "/" regexp "/" command. 145 | // Conditionally executes a command if a regular expression 146 | // matches in the address. 147 | // 148 | // If the regular expression matches (g) or doesn't match (v) 149 | // in the addressed string, dot is set to the address 150 | // and the command is executed. 151 | // It is an error if the resulting edits are not in ascending order. 152 | // 153 | // Note, the command is only interpreted if the condition is satisfied, 154 | // so a malformed command is not reported if not. 155 | // 156 | // [ addr ] ( "|" | "<" | ">" ) shell command. 157 | // Pipes the addressed string to and/or from shell commands. 158 | // 159 | // The | form pipes the addressed string to standard input 160 | // of a shell command and overwrites it with the 161 | // the standard output of the command. 162 | // The < and > forms are like the | form, 163 | // but < only overwrites with the command's standard output, 164 | // and > only pipes to the command's standard input. 165 | // In all cases, the standard error of the command is printed. 166 | // In the case of >, the standard output is also printed. 167 | // 168 | // The shell command is any text terminated by either 169 | // the end of input or an un-escaped newline. 170 | // The text is passed as the -c argument of 171 | // the shell program from the SHELL environment variable. 172 | // If SHELL is unset, /bin/sh is used. 173 | // 174 | // [ addr ] "{" { "\n" command } [ "\n" ] [ "}" ]. 175 | // Performs a sequence of commands. 176 | // 177 | // Before performing each command, dot is set to the address. 178 | // Commands do not see modifications made by each other. 179 | // Each sees the original text, before any changes are made. 180 | // It is an error if the resulting edits are not in ascending order. 181 | package edit 182 | 183 | import ( 184 | "errors" 185 | "io" 186 | "os" 187 | "os/exec" 188 | "strconv" 189 | "strings" 190 | "sync" 191 | "unicode" 192 | "unicode/utf8" 193 | 194 | "github.com/eaburns/T/re1" 195 | "github.com/eaburns/T/rope" 196 | ) 197 | 198 | // A Diffs is a sequence of Diffs. 199 | type Diffs []Diff 200 | 201 | // A Diff describes a single change to a contiguous span of bytes. 202 | type Diff struct { 203 | // At is the byte address of the span changed. 204 | At [2]int64 205 | // Text is the text to which the span changed. 206 | // Text may be nil if the addressed string was deleted. 207 | Text rope.Rope 208 | } 209 | 210 | // NoCommandError is returned when there was no command to execute. 211 | type NoCommandError struct { 212 | // At contains the evaluation of the address preceding the missing command. 213 | // An empty address is dot, so an empty edit results in a NoCommandError. 214 | // At is set to the value of dot. 215 | At [2]int64 216 | } 217 | 218 | // Error returns the error string "no command". 219 | func (e NoCommandError) Error() string { 220 | return "no command" 221 | } 222 | 223 | // TextLen is the length of Text; 0 if Text is nil. 224 | func (d Diff) TextLen() int64 { 225 | if d.Text == nil { 226 | return 0 227 | } 228 | return d.Text.Len() 229 | } 230 | 231 | // Update returns an updated dot accounting for the changes of the diffs. 232 | func (ds Diffs) Update(dot [2]int64) [2]int64 { 233 | for _, d := range ds { 234 | dot = d.Update(dot) 235 | } 236 | return dot 237 | } 238 | 239 | // Update returns an updated dot accounting for the change of the diff. 240 | // For example, if the diff added or deleted text before the dot, 241 | // then dot is increased or decreased accordingly. 242 | func (d Diff) Update(dot [2]int64) [2]int64 { 243 | switch delta := d.TextLen() - (d.At[1] - d.At[0]); { 244 | case d.At[0] >= dot[1]: 245 | // after dot 246 | break 247 | 248 | case d.At[1] <= dot[0]: 249 | // before dot 250 | dot[0] += delta 251 | dot[1] += delta 252 | 253 | case dot[0] <= d.At[0] && d.At[1] < dot[1]: 254 | // inside dot 255 | dot[1] += delta 256 | 257 | case d.At[0] <= dot[0] && dot[1] < d.At[1]: 258 | // over dot 259 | dot[0] = d.At[0] 260 | dot[1] = d.At[0] 261 | 262 | case d.At[1] < dot[1]: 263 | // a prefix of dot 264 | dot[0] = d.At[0] + d.TextLen() 265 | dot[1] = dot[0] + (dot[1] - d.At[1]) 266 | 267 | default: 268 | // a suffix of dot 269 | dot[1] = d.At[0] 270 | } 271 | return dot 272 | } 273 | 274 | // Apply returns the result of applying the diffs 275 | // and a new sequence of diffs that will undo the changes 276 | // if applied to the returned rope. 277 | func (ds Diffs) Apply(ro rope.Rope) (rope.Rope, Diffs) { 278 | undo := make(Diffs, len(ds)) 279 | for i, d := range ds { 280 | ro, undo[len(ds)-i-1] = d.Apply(ro) 281 | } 282 | return ro, undo 283 | } 284 | 285 | // Apply returns the result of applying the diff 286 | // and a new Diff that will undo the change 287 | // if applied to the returned rope. 288 | func (d Diff) Apply(ro rope.Rope) (rope.Rope, Diff) { 289 | var deleted rope.Rope 290 | if d.At[0] < d.At[1] { 291 | deleted = rope.Slice(ro, d.At[0], d.At[1]) 292 | ro = rope.Delete(ro, d.At[0], d.At[1]-d.At[0]) 293 | } 294 | if d.TextLen() > 0 { 295 | ro = rope.Insert(ro, d.At[0], d.Text) 296 | } 297 | return ro, Diff{At: [2]int64{d.At[0], d.At[0] + d.TextLen()}, Text: deleted} 298 | } 299 | 300 | // Addr computes an address using the given value for dot. 301 | func Addr(dot [2]int64, t string, ro rope.Rope) ([2]int64, error) { 302 | var err error 303 | switch dot, t, err = addr(&dot, ro, t); { 304 | case err != nil: 305 | return [2]int64{}, err 306 | case strings.TrimSpace(t) != "": 307 | return [2]int64{}, errors.New("expected end-of-input") 308 | case dot[0] < 0: 309 | return [2]int64{}, errors.New("no address") 310 | default: 311 | return dot, nil 312 | } 313 | } 314 | 315 | // Edit computes an edit on the rope using the given value for dot. 316 | func Edit(dot [2]int64, t string, print io.Writer, ro rope.Rope) (Diffs, error) { 317 | switch ds, t, err := edit(dot, t, print, ro); { 318 | case err != nil: 319 | return nil, err 320 | case strings.TrimSpace(t) != "": 321 | return nil, errors.New("expected end-of-input") 322 | default: 323 | return ds, nil 324 | } 325 | } 326 | 327 | func edit(dot [2]int64, t string, print io.Writer, ro rope.Rope) (Diffs, string, error) { 328 | a, t, err := addr(&dot, ro, t) 329 | switch { 330 | case err != nil: 331 | return nil, "", err 332 | case a[0] < 0: 333 | a = dot 334 | } 335 | switch r, t := next(trimSpaceLeft(t)); r { 336 | default: 337 | return nil, "", errors.New("bad command " + string([]rune{r})) 338 | case eof, '\n': 339 | return nil, "", NoCommandError{At: a} 340 | case 'a', 'c', 'd', 'i': 341 | return change(a, t, r, ro) 342 | case 'm': 343 | return move(dot, a, t, ro) 344 | case 'p': 345 | _, err := rope.Slice(ro, a[0], a[1]).WriteTo(print) 346 | return nil, t, err 347 | case 't': 348 | return copy(dot, a, t, ro) 349 | case 's': 350 | return sub(a, t, ro) 351 | case 'g', 'v': 352 | return cond(a, t, r, print, ro) 353 | case 'x', 'y': 354 | return loop(a, t, r, print, ro) 355 | case '{': 356 | return seq(a, t, print, ro) 357 | case '<', '>', '|': 358 | return pipe(a, t, r, print, ro) 359 | } 360 | } 361 | 362 | func change(a [2]int64, t string, op rune, ro rope.Rope) (Diffs, string, error) { 363 | switch op { 364 | case 'a': 365 | a[0] = a[1] 366 | case 'i': 367 | a[1] = a[0] 368 | } 369 | var text string 370 | if op != 'd' { 371 | text, t = parseText(t) 372 | } 373 | return Diffs{{At: a, Text: rope.New(text)}}, t, nil 374 | } 375 | 376 | func parseText(t0 string) (text, t string) { 377 | t = strings.TrimLeftFunc(t0, func(r rune) bool { 378 | return unicode.IsSpace(r) && r != '\n' 379 | }) 380 | switch { 381 | case len(t) == 0: 382 | return "", "" 383 | case t[0] == '\n': 384 | return parseLines(t[1:]) 385 | default: 386 | delim, w := utf8.DecodeRuneInString(t) 387 | return parseDelimited(t[w:], nil, delim) 388 | } 389 | } 390 | 391 | func parseLines(t string) (string, string) { 392 | var i int 393 | nl := true 394 | var s strings.Builder 395 | for { 396 | var r rune 397 | switch r, t = next(t); { 398 | case r == eof: 399 | if nl && i > 0 { 400 | s.WriteRune('\n') 401 | } 402 | return s.String(), "" 403 | case nl && r == '.' && len(t) == 0: 404 | return s.String(), t 405 | case nl && r == '.' && t[0] == '\n': 406 | return s.String(), t[1:] 407 | default: 408 | if nl && i > 0 { 409 | s.WriteRune('\n') 410 | } 411 | i++ 412 | if r == '\n' { 413 | nl = true 414 | } else { 415 | s.WriteRune(r) 416 | nl = false 417 | } 418 | } 419 | } 420 | } 421 | 422 | func parseDelimited(t string, sub func(int) string, delim rune) (string, string) { 423 | var s strings.Builder 424 | for { 425 | var r rune 426 | switch r, t = next(t); { 427 | case r == eof || r == '\n' || r == delim: 428 | return s.String(), t 429 | case r == '\\' && sub != nil: 430 | if r, t1 := next(t); '0' <= r && r <= '9' { 431 | t = t1 432 | s.WriteString(sub(int(r - '0'))) 433 | continue 434 | } 435 | fallthrough 436 | case r == '\\': 437 | r, t = next(t) 438 | s.WriteRune(esc(r)) 439 | default: 440 | s.WriteRune(r) 441 | } 442 | } 443 | } 444 | 445 | func esc(r rune) rune { 446 | switch r { 447 | case eof: 448 | return '\\' 449 | case 'n', '\n': 450 | return '\n' 451 | case 't': 452 | return '\t' 453 | default: 454 | return r 455 | } 456 | } 457 | 458 | func move(dot, a [2]int64, t string, ro rope.Rope) (Diffs, string, error) { 459 | b, t, err := addr(&dot, ro, t) 460 | switch { 461 | case err != nil: 462 | return nil, "", err 463 | case b[0] < 0: 464 | return nil, "", errors.New("expected address") 465 | case a[0] == a[1]: 466 | // Moving nothing is a no-op, 467 | return nil, t, nil 468 | case a[0] <= b[1] && b[1] < a[1]: 469 | // Moving to a destination inside the moved text is a no-op, 470 | return nil, t, nil 471 | case a[1] < b[1]: 472 | // Moving text from before the dest, slide left by the delta 473 | b[1] -= a[1] - a[0] 474 | } 475 | ds := Diffs{ 476 | {At: a, Text: nil}, 477 | {At: [2]int64{b[1], b[1]}, Text: rope.Slice(ro, a[0], a[1])}, 478 | } 479 | return ds, t, nil 480 | } 481 | 482 | func copy(dot, a [2]int64, t string, ro rope.Rope) (Diffs, string, error) { 483 | b, t, err := addr(&dot, ro, t) 484 | switch { 485 | case err != nil: 486 | return nil, "", err 487 | case a[0] == a[1]: 488 | // Copying nothing is a no-op, 489 | return nil, t, nil 490 | case b[0] < 0: 491 | return nil, "", errors.New("expected address") 492 | } 493 | return Diffs{{At: [2]int64{b[1], b[1]}, Text: rope.Slice(ro, a[0], a[1])}}, t, nil 494 | } 495 | 496 | func sub(a [2]int64, t string, ro rope.Rope) (Diffs, string, error) { 497 | n, t, _ := number(trimSpaceLeft(t)) 498 | delim, _ := next(trimSpaceLeft(t)) 499 | re, t, err := parseRegexp(t) 500 | if err != nil { 501 | return nil, "", err 502 | } 503 | tmpl := t 504 | _, t = parseDelimited(t, nil, delim) 505 | var global bool 506 | if r, t1 := next(trimSpaceLeft(t)); r == 'g' { 507 | t = t1 508 | global = true 509 | } 510 | 511 | var ds Diffs 512 | var adj int64 513 | var ms []int64 514 | sub := func(n int) string { 515 | i := n * 2 516 | if i >= len(ms) || ms[i] < 0 { 517 | return "" 518 | } 519 | return rope.Slice(ro, ms[i], ms[i+1]).String() 520 | } 521 | for a[0] <= a[1] { 522 | if ms = re.FindInRope(ro, a[0], a[1]); ms == nil { 523 | break 524 | } 525 | if len(ms) > 0 { 526 | ms = ms[:len(ms)-1] // trim regexp ID 527 | } 528 | if ms[1] == ms[0] { 529 | a[0]++ 530 | } else { 531 | a[0] = ms[1] 532 | } 533 | n-- 534 | if n > 0 { 535 | continue 536 | } 537 | s, _ := parseDelimited(tmpl, sub, delim) 538 | ds = append(ds, Diff{ 539 | At: [2]int64{ms[0] - adj, ms[1] - adj}, 540 | Text: rope.New(s), 541 | }) 542 | if !global { 543 | break 544 | } 545 | adj += ms[1] - ms[0] - int64(len(s)) 546 | } 547 | if len(ds) == 0 { 548 | return nil, "", errors.New("no match") 549 | } 550 | return ds, t, nil 551 | } 552 | 553 | func cond(a [2]int64, t string, op rune, print io.Writer, ro rope.Rope) (Diffs, string, error) { 554 | re, t, err := parseRegexp(t) 555 | if err != nil { 556 | return nil, "", err 557 | } 558 | cmd, t := splitNewline(t) 559 | ms := re.FindInRope(ro, a[0], a[1]) 560 | if op == 'g' && ms == nil || op == 'v' && ms != nil { 561 | return nil, t, nil 562 | } 563 | ds, err := Edit(a, cmd, print, ro) 564 | return ds, t, err 565 | } 566 | 567 | func loop(a [2]int64, t string, op rune, print io.Writer, ro rope.Rope) (Diffs, string, error) { 568 | re, t, err := parseRegexp(t) 569 | if err != nil { 570 | return nil, "", err 571 | } 572 | cmd, t := splitNewline(t) 573 | var diffs Diffs 574 | prev := a[0] 575 | at := int64(-1) 576 | var adj int64 577 | for a[0] <= a[1] { 578 | ms := re.FindInRope(ro, a[0], a[1]) 579 | if ms == nil { 580 | break 581 | } 582 | if ms[0] == ms[1] { 583 | // If we matched empty, advance by one over it. 584 | a[0] = ms[1] + 1 585 | } else { 586 | a[0] = ms[1] 587 | } 588 | dot := [2]int64{ms[0], ms[1]} 589 | if op == 'y' { 590 | dot = [2]int64{prev, ms[0]} 591 | prev = ms[1] 592 | } 593 | var ds Diffs 594 | if ds, err = Edit(dot, cmd, print, ro); err != nil { 595 | return nil, "", err 596 | } 597 | if at, adj, diffs, err = appendAdjusted(at, adj, diffs, ds); err != nil { 598 | return nil, "", err 599 | } 600 | } 601 | if op == 'y' { 602 | ds, err := Edit([2]int64{prev, a[1]}, cmd, print, ro) 603 | if err != nil { 604 | return nil, "", err 605 | } 606 | if _, _, diffs, err = appendAdjusted(at, adj, diffs, ds); err != nil { 607 | return nil, "", err 608 | } 609 | } 610 | return diffs, t, nil 611 | } 612 | 613 | func seq(a [2]int64, t string, print io.Writer, ro rope.Rope) (Diffs, string, error) { 614 | var diffs Diffs 615 | at := int64(-1) 616 | var adj int64 617 | for { 618 | t = trimSpaceLeft(t) 619 | if r, t1 := next(t); r == '}' { 620 | return diffs, t1, nil 621 | } else if r == eof { 622 | return nil, "", errors.New("unclosed {") 623 | } 624 | 625 | var cmd string 626 | cmd, t = splitNewline(t) 627 | ds, err := Edit(a, cmd, print, ro) 628 | if err != nil { 629 | return nil, "", err 630 | } 631 | if at, adj, diffs, err = appendAdjusted(at, adj, diffs, ds); err != nil { 632 | return nil, "", err 633 | } 634 | } 635 | } 636 | 637 | func appendAdjusted(at, adj int64, diffs, ds Diffs) (int64, int64, Diffs, error) { 638 | var atNext int64 639 | for i := range ds { 640 | d := &ds[i] 641 | if d.At[0] < at { 642 | return 0, 0, nil, errors.New("out of order") 643 | } 644 | // TODO: if a single diff was a delete+add instead of delete|add, 645 | // then we could just update at here. 646 | atNext = d.At[1] 647 | 648 | d.At[0] += adj 649 | d.At[1] += adj 650 | } 651 | at = atNext 652 | 653 | for _, d := range ds { 654 | adj += d.Text.Len() - (d.At[1] - d.At[0]) 655 | } 656 | diffs = append(diffs, ds...) 657 | return at, adj, diffs, nil 658 | } 659 | 660 | func pipe(a [2]int64, t string, op rune, print io.Writer, ro rope.Rope) (Diffs, string, error) { 661 | shell := os.Getenv("SHELL") 662 | if shell == "" { 663 | shell = "/bin/sh" 664 | } 665 | arg, t := splitNewline(t) 666 | if arg = strings.TrimSpace(arg); arg == "" { 667 | return nil, "", errors.New("expected command") 668 | } 669 | cmd := exec.Command(shell, "-c", arg) 670 | stdin, stdout, err := openPipes(cmd, print, op) 671 | if err != nil { 672 | return nil, "", err 673 | } 674 | if err := cmd.Start(); err != nil { 675 | closeIfNonNil(stdin) 676 | closeIfNonNil(stdout) 677 | return nil, "", err 678 | } 679 | 680 | var ds Diffs 681 | var wg sync.WaitGroup 682 | if op == '<' || op == '|' { 683 | wg.Add(1) 684 | go func() { 685 | txt, _ := rope.ReadFrom(stdout) 686 | ds = Diffs{{At: a, Text: txt}} 687 | wg.Done() 688 | }() 689 | } 690 | if op == '>' || op == '|' { 691 | wg.Add(1) 692 | go func() { 693 | rope.Slice(ro, a[0], a[1]).WriteTo(stdin) 694 | stdin.Close() 695 | wg.Done() 696 | }() 697 | } 698 | 699 | wg.Wait() 700 | err = cmd.Wait() 701 | return ds, t, err 702 | } 703 | 704 | func openPipes(cmd *exec.Cmd, print io.Writer, op rune) (io.WriteCloser, io.ReadCloser, error) { 705 | cmd.Stderr = print 706 | if op == '>' { 707 | cmd.Stdout = print 708 | } 709 | var err error 710 | var stdin io.WriteCloser 711 | if op == '>' || op == '|' { 712 | if stdin, err = cmd.StdinPipe(); err != nil { 713 | return nil, nil, err 714 | } 715 | } 716 | var stdout io.ReadCloser 717 | if op == '<' || op == '|' { 718 | if stdout, err = cmd.StdoutPipe(); err != nil { 719 | closeIfNonNil(stdin) 720 | return nil, nil, err 721 | } 722 | } 723 | return stdin, stdout, nil 724 | } 725 | 726 | func closeIfNonNil(c io.Closer) { 727 | if c != nil { 728 | c.Close() 729 | } 730 | } 731 | 732 | func splitNewline(str string) (string, string) { 733 | i := strings.IndexRune(str, '\n') 734 | if i < 0 { 735 | return str, "" 736 | } 737 | return str[:i+1], str[i+1:] 738 | } 739 | 740 | func parseRegexp(t string) (*re1.Regexp, string, error) { 741 | delim, t := next(trimSpaceLeft(t)) 742 | if delim == eof { 743 | return nil, "", errors.New("expected regular expression") 744 | } 745 | re, t, err := re1.New(t, re1.Opts{Delimiter: delim}) 746 | return re, t, err 747 | } 748 | 749 | func addr(dot *[2]int64, ro rope.Rope, t string) ([2]int64, string, error) { 750 | left, t, err := addr1(*dot, 0, false, ro, t) 751 | if err != nil { 752 | return [2]int64{}, "", err 753 | } 754 | if left, t, err = addr2(*dot, left, ro, t); err != nil { 755 | return [2]int64{}, "", err 756 | } 757 | return addr3(dot, left, ro, t) 758 | } 759 | 760 | func addr3(dot *[2]int64, left [2]int64, ro rope.Rope, t0 string) ([2]int64, string, error) { 761 | r, t := next(trimSpaceLeft(t0)) 762 | switch { 763 | case r == eof: 764 | return left, "", nil 765 | case r != ',' && r != ';': 766 | return left, t0, nil 767 | case left[0] < 0: 768 | left = [2]int64{} 769 | } 770 | if r == ';' { 771 | *dot = left 772 | } 773 | switch right, t, err := addr(dot, ro, t); { 774 | case err != nil: 775 | return [2]int64{}, "", err 776 | case right[0] < 0: 777 | right = [2]int64{ro.Len(), ro.Len()} 778 | fallthrough 779 | default: 780 | if left[0] > right[1] { 781 | return [2]int64{}, t, errors.New("address out of order") 782 | } 783 | return addr3(dot, [2]int64{left[0], right[1]}, ro, t) 784 | } 785 | } 786 | 787 | func addr2(dot, left [2]int64, ro rope.Rope, t0 string) ([2]int64, string, error) { 788 | r, t := next(trimSpaceLeft(t0)) 789 | switch { 790 | case r == eof: 791 | return left, "", nil 792 | case strings.ContainsRune(addr1First, r): 793 | t, r = t0, '+' // Insert + 794 | case r != '+' && r != '-': 795 | return left, t0, nil 796 | } 797 | if left[0] < 0 { 798 | left = dot 799 | } 800 | at := left[1] 801 | if r == '-' { 802 | at = left[0] 803 | } 804 | switch right, t, err := addr1(dot, at, r == '-', ro, t); { 805 | case err != nil: 806 | return [2]int64{}, "", err 807 | case right[0] < 0: 808 | if right, _, err = addr1(dot, at, r == '-', ro, "1"); err != nil { 809 | return [2]int64{}, t, err 810 | } 811 | fallthrough 812 | default: 813 | return addr2(dot, right, ro, t) 814 | } 815 | } 816 | 817 | const addr1First = ".'#0123456789/$" 818 | 819 | func addr1(dot [2]int64, at int64, rev bool, ro rope.Rope, t0 string) ([2]int64, string, error) { 820 | t0 = trimSpaceLeft(t0) 821 | switch r, t := next(t0); r { 822 | case eof: 823 | return [2]int64{-1, -1}, "", nil 824 | default: 825 | return [2]int64{-1, -1}, t0, nil 826 | case '.': 827 | return dot, t, nil 828 | case '#': 829 | return runeAddr(ro, at, rev, t) 830 | case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 831 | return lineAddr(ro, at, rev, t0) 832 | case '/': 833 | return regexpAddr(ro, at, rev, t) 834 | case '$': 835 | return [2]int64{ro.Len(), ro.Len()}, t, nil 836 | } 837 | } 838 | 839 | func runeAddr(ro rope.Rope, at int64, rev bool, t string) ([2]int64, string, error) { 840 | nrunes, t, err := number(t) 841 | if err != nil { 842 | return [2]int64{}, "", err 843 | } 844 | var r io.RuneReader 845 | if rev { 846 | sl := rope.Slice(ro, 0, at) 847 | r = rope.NewReverseReader(sl) 848 | } else { 849 | sl := rope.Slice(ro, at, ro.Len()) 850 | r = rope.NewReader(sl) 851 | } 852 | var nbytes int64 853 | for nrunes > 0 { 854 | _, w, err := r.ReadRune() 855 | if err != nil { 856 | return [2]int64{}, "", errors.New("address out of range") 857 | } 858 | nbytes += int64(w) 859 | nrunes-- 860 | } 861 | if rev { 862 | return [2]int64{at - nbytes, at - nbytes}, t, nil 863 | } 864 | return [2]int64{at + nbytes, at + nbytes}, t, nil 865 | } 866 | 867 | func lineAddr(ro rope.Rope, at int64, rev bool, t string) ([2]int64, string, error) { 868 | n, t, err := number(t) 869 | if err != nil { 870 | return [2]int64{}, "", err 871 | } 872 | var dot [2]int64 873 | if rev { 874 | r := rope.NewReverseReader(rope.Slice(ro, 0, at)) 875 | dot, err = lineReverse(r, at, n) 876 | } else { 877 | // Check the previous rune. 878 | rr := rope.NewReverseReader(rope.Slice(ro, 0, at)) 879 | if r, _, err := rr.ReadRune(); err != nil || r == '\n' { 880 | n-- 881 | } 882 | r := rope.NewReader(rope.Slice(ro, at, ro.Len())) 883 | dot, err = lineForward(r, at, n) 884 | } 885 | return dot, t, err 886 | } 887 | 888 | func lineForward(in *rope.Reader, at int64, nlines int) ([2]int64, error) { 889 | dot := [2]int64{at, at} 890 | for nlines >= 0 { 891 | b, err := in.ReadByte() 892 | switch { 893 | case err != nil && nlines == 0: 894 | b = '\n' 895 | case err != nil: 896 | return [2]int64{}, errors.New("address out of range") 897 | default: 898 | at++ 899 | } 900 | if b == '\n' { 901 | nlines-- 902 | dot[0], dot[1] = dot[1], at 903 | } 904 | } 905 | return dot, nil 906 | } 907 | 908 | func lineReverse(in *rope.ReverseReader, at int64, nlines int) ([2]int64, error) { 909 | dot := [2]int64{at, at} 910 | for { 911 | b, err := in.ReadByte() 912 | switch { 913 | case err != nil && nlines == 0: 914 | b = '\n' 915 | case err != nil: 916 | if nlines == 1 { 917 | return [2]int64{}, nil 918 | } 919 | return [2]int64{}, errors.New("address out of range") 920 | } 921 | if b == '\n' { 922 | dot[0], dot[1] = at, dot[0] 923 | if nlines--; nlines < 0 { 924 | return dot, nil 925 | } 926 | } 927 | at-- 928 | } 929 | } 930 | 931 | func regexpAddr(ro rope.Rope, at int64, rev bool, t string) ([2]int64, string, error) { 932 | re, t, err := re1.New(t, re1.Opts{Delimiter: '/', Reverse: rev}) 933 | if err != nil { 934 | return [2]int64{}, "", err 935 | } 936 | var ms []int64 937 | if rev { 938 | ms = re.FindReverseInRope(ro, 0, at) 939 | if ms == nil { 940 | ms = re.FindReverseInRope(ro, 0, ro.Len()) 941 | } 942 | } else { 943 | ms = re.FindInRope(ro, at, ro.Len()) 944 | if ms == nil { 945 | ms = re.FindInRope(ro, 0, ro.Len()) 946 | } 947 | } 948 | if len(ms) == 0 { 949 | return [2]int64{}, t, errors.New("no match") 950 | } 951 | return [2]int64{ms[0], ms[1]}, t, err 952 | } 953 | 954 | const eof = -1 955 | 956 | func next(t string) (rune, string) { 957 | if len(t) == 0 { 958 | return eof, "" 959 | } 960 | r, w := utf8.DecodeRuneInString(t) 961 | return r, t[w:] 962 | } 963 | 964 | func trimSpaceLeft(t string) string { 965 | return strings.TrimLeftFunc(t, unicode.IsSpace) 966 | } 967 | 968 | func number(t string) (int, string, error) { 969 | var i int 970 | for { 971 | r, w := utf8.DecodeRuneInString(t[i:]) 972 | if r < '0' || '9' < r { 973 | break 974 | } 975 | i += w 976 | } 977 | if i == 0 { 978 | return 1, t, nil // defaults to 1 979 | } 980 | n, err := strconv.Atoi(t[:i]) 981 | if err != nil { 982 | return 0, t, err 983 | } 984 | return n, t[i:], nil 985 | } 986 | -------------------------------------------------------------------------------- /ui/text_box.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "bufio" 5 | "image" 6 | "image/color" 7 | "image/draw" 8 | "io/ioutil" 9 | "math" 10 | "strconv" 11 | "strings" 12 | "time" 13 | "unicode" 14 | "unicode/utf8" 15 | 16 | "github.com/eaburns/T/edit" 17 | "github.com/eaburns/T/rope" 18 | "github.com/eaburns/T/syntax" 19 | "github.com/eaburns/T/text" 20 | "golang.org/x/image/math/fixed" 21 | ) 22 | 23 | const ( 24 | blinkDuration = 500 * time.Millisecond 25 | dragScrollDuration = 20 * time.Millisecond 26 | wheelScrollDuration = 20 * time.Millisecond 27 | doubleClickDuration = 500 * time.Millisecond 28 | ) 29 | 30 | // TextBox is an editable text box UI widget. 31 | type TextBox struct { 32 | win *Win 33 | size image.Point 34 | text rope.Rope 35 | at int64 // address of the first rune in the window 36 | 37 | focus bool 38 | showCursor bool 39 | blinkTime time.Time 40 | cursorCol int // rune offset of the cursor in its line; -1 is recompute 41 | 42 | button int // currently held mouse button 43 | pt image.Point // where's the mouse? 0 is just after textPadPx 44 | clickAt int64 // address of the glyph clicked by the mouse 45 | clickTime time.Time 46 | dragAt int64 // address of the glyph under the dragging mouse 47 | dragTextBox image.Rectangle // bounding-box of the dragAt glyph 48 | dragScrollTime time.Time // time when dragging off screen scrolls 49 | wheelTime time.Time // time when we will consider the next wheel 50 | 51 | style text.Style 52 | dots [4]syntax.Highlight // cursor for unused, click 1, click 2, and click 3. 53 | highlight []syntax.Highlight // highlighted words 54 | syntax []syntax.Highlight // syntax highlighting 55 | highlighter updater // syntax highlighter 56 | 57 | dirty bool 58 | _lines []line 59 | now func() time.Time 60 | } 61 | 62 | type line struct { 63 | dirty bool 64 | n int64 65 | a, h fixed.Int26_6 66 | spans []span 67 | } 68 | 69 | type span struct { 70 | w fixed.Int26_6 71 | style text.Style 72 | text string 73 | } 74 | 75 | // NewTextBox returns a new, empty text box. 76 | // The styles are: 77 | // 0: default style 78 | // 1: 1-click selection style 79 | // 2: 2-click selection style 80 | // 3: 3-click selection style 81 | func NewTextBox(w *Win, styles [4]text.Style, size image.Point) *TextBox { 82 | b := &TextBox{ 83 | win: w, 84 | size: size, 85 | text: rope.Empty(), 86 | style: styles[0], 87 | dots: [...]syntax.Highlight{ 88 | {Style: styles[0]}, 89 | {Style: styles[1]}, 90 | {Style: styles[2]}, 91 | {Style: styles[3]}, 92 | }, 93 | cursorCol: -1, 94 | now: func() time.Time { return time.Now() }, 95 | } 96 | return b 97 | } 98 | 99 | // Text returns the current text of the text box. 100 | func (b *TextBox) Text() rope.Rope { return b.text } 101 | 102 | // SetText sets the text of the text box. 103 | // The text box always must be redrawn after setting the text. 104 | func (b *TextBox) SetText(text rope.Rope) { 105 | b.text = text 106 | 107 | b.at = 0 108 | b.cursorCol = -1 109 | b.clickAt = 0 110 | b.dragAt = 0 111 | b.dragTextBox = image.ZR 112 | for i := range b.dots { 113 | b.dots[i].At = [2]int64{} 114 | } 115 | b.highlight = nil 116 | if b.highlighter != nil { 117 | b.syntax = b.highlighter.Update(nil, nil, b.text) 118 | } 119 | dirtyLines(b) 120 | } 121 | 122 | // textHeight returns the height of the displayed text. 123 | func (b *TextBox) textHeight() int { 124 | var y fixed.Int26_6 125 | lines := b.lines() 126 | for _, l := range lines { 127 | y += l.h 128 | } 129 | if len(lines) > 0 { 130 | l := &lines[len(lines)-1] 131 | if len(l.spans) > 0 { 132 | s := &l.spans[len(l.spans)-1] 133 | r, _ := utf8.DecodeLastRuneInString(s.text) 134 | if r == '\n' { 135 | m := b.style.Face.Metrics() 136 | h := m.Height + m.Descent 137 | y += h 138 | } 139 | } 140 | } 141 | return y.Ceil() 142 | } 143 | 144 | type updater interface { 145 | Update([]syntax.Highlight, edit.Diffs, rope.Rope) []syntax.Highlight 146 | } 147 | 148 | func (b *TextBox) setHighlighter(highlighter updater) { 149 | b.highlighter = highlighter 150 | if b.highlighter != nil { 151 | b.syntax = b.highlighter.Update(nil, nil, b.text) 152 | } 153 | dirtyLines(b) 154 | } 155 | 156 | // Edit performs an edit on the text of the text box 157 | // and returns the diffs applied to the text. 158 | // If more than 0 diffs are returned, the text box needs to be redrawn. 159 | func (b *TextBox) Edit(t string) (edit.Diffs, error) { return ed(b, t) } 160 | 161 | func ed(b *TextBox, t string) (edit.Diffs, error) { 162 | dot := b.dots[1].At 163 | diffs, err := edit.Edit(dot, t, ioutil.Discard, b.text) 164 | if err != nil { 165 | return nil, err 166 | } 167 | if len(diffs) > 0 { 168 | dot := b.dots[1].At 169 | b.Change(diffs) 170 | dot[0] = diffs[len(diffs)-1].At[0] 171 | dot[1] = dot[0] + diffs[len(diffs)-1].TextLen() 172 | b.dots[1].At = dot 173 | } 174 | return diffs, nil 175 | } 176 | 177 | // Change applies a set of diffs to the text box. 178 | func (b *TextBox) Change(diffs edit.Diffs) { 179 | if len(diffs) == 0 { 180 | return 181 | } 182 | dirtyLines(b) 183 | b.text, _ = diffs.Apply(b.text) 184 | 185 | // TODO: if something else deletes \n before TextBox.at, scroll up 186 | // to the beginning of the previous line. 187 | b.at = diffs.Update([2]int64{b.at, b.at})[0] 188 | 189 | for i := 1; i < len(b.dots); i++ { 190 | b.dots[i].At = diffs.Update(b.dots[i].At) 191 | } 192 | if b.highlighter != nil { 193 | b.syntax = b.highlighter.Update(b.syntax, diffs, b.text) 194 | } 195 | for i := range b.highlight { 196 | b.highlight[i].At = diffs.Update(b.highlight[i].At) 197 | } 198 | } 199 | 200 | // Copy copies the selected text into the system clipboard. 201 | func (b *TextBox) Copy() error { 202 | r := rope.Slice(b.text, b.dots[1].At[0], b.dots[1].At[1]) 203 | return b.win.clipboard.Store(r) 204 | } 205 | 206 | // Paste pastes the text from the system clipboard to the selection. 207 | func (b *TextBox) Paste() error { 208 | r, err := b.win.clipboard.Fetch() 209 | if err != nil { 210 | return err 211 | } 212 | b.Change(edit.Diffs{{At: b.dots[1].At, Text: r}}) 213 | return nil 214 | } 215 | 216 | // Cut copies the selected text into the system clipboard 217 | // and deletes it from the text box. 218 | func (b *TextBox) Cut() error { 219 | if err := b.Copy(); err != nil { 220 | return err 221 | } 222 | b.Change(edit.Diffs{{At: b.dots[1].At, Text: rope.Empty()}}) 223 | return nil 224 | } 225 | 226 | // Resize handles a resize event. 227 | // The text box must always be redrawn after being resized. 228 | func (b *TextBox) Resize(size image.Point) { 229 | b.size = size 230 | dirtyLines(b) 231 | } 232 | 233 | // Focus handles a focus state change. 234 | func (b *TextBox) Focus(focus bool) { 235 | b.focus = focus 236 | b.showCursor = focus 237 | dirtyDot(b, b.dots[1].At) 238 | if focus { 239 | b.blinkTime = b.now().Add(blinkDuration) 240 | } else { 241 | b.button = 0 242 | } 243 | } 244 | 245 | // Tick handles periodic ticks that drive 246 | // asynchronous events for the text box. 247 | // It returns whether the text box image needs to be redrawn. 248 | // 249 | // Tick is intended to be called at regular intervals, 250 | // fast enough to drive cursor blinking and mouse-drag scolling. 251 | func (b *TextBox) Tick() bool { 252 | now := b.now() 253 | redraw := b.dirty 254 | if b.focus && b.dots[1].At[0] == b.dots[1].At[1] && !b.blinkTime.After(now) { 255 | b.blinkTime = now.Add(blinkDuration) 256 | b.showCursor = !b.showCursor 257 | dirtyDot(b, b.dots[1].At) 258 | } 259 | if b.button == 1 && 260 | !b.dragScrollTime.After(now) { 261 | var ymax fixed.Int26_6 262 | atMax := b.at 263 | for _, l := range b.lines() { 264 | ymax += l.h 265 | atMax += l.n 266 | } 267 | switch { 268 | case b.pt.Y < 0: 269 | scrollUp(b, 1) 270 | b.Move(b.pt) 271 | b.dragScrollTime = now.Add(dragScrollDuration) 272 | case b.pt.Y >= ymax.Floor() && atMax < b.text.Len(): 273 | scrollDown(b, 1) 274 | b.Move(b.pt) 275 | b.dragScrollTime = now.Add(dragScrollDuration) 276 | } 277 | } 278 | return redraw 279 | } 280 | 281 | // Move handles the event of the mouse cursor moving to a point 282 | // and returns whether the text box image needs to be redrawn. 283 | func (b *TextBox) Move(pt image.Point) { 284 | pt.X -= textPadPx 285 | b.pt = pt 286 | if b.button <= 0 || b.button >= len(b.dots) || pt.In(b.dragTextBox) { 287 | return 288 | } 289 | b.dragAt, b.dragTextBox = atPoint(b, pt) 290 | if b.clickAt <= b.dragAt { 291 | setDot(b, b.button, b.clickAt, b.dragAt) 292 | } else { 293 | setDot(b, b.button, b.dragAt, b.clickAt) 294 | } 295 | } 296 | 297 | // Wheel handles the event of the mouse wheel rolling 298 | // and returns whether the text box image needs to be redrawn. 299 | // -y is roll up. 300 | // +y is roll down. 301 | // -x is roll left. 302 | // +x is roll right. 303 | func (b *TextBox) Wheel(_ image.Point, x, y int) { 304 | now := b.now() 305 | if b.wheelTime.After(now) { 306 | return 307 | } 308 | b.wheelTime = now.Add(wheelScrollDuration) 309 | switch { 310 | case y < 0: 311 | scrollDown(b, 1) 312 | case y > 0: 313 | scrollUp(b, 1) 314 | } 315 | } 316 | 317 | // Click handles a mouse button press or release event. 318 | // The first return value is the button ultimately pressed 319 | // (this can differ from the argument button, for example, 320 | // if modifier keys are being held). 321 | // If the button is < 0, the second return value is the clicked address. 322 | // The third return value is whether the text box image needs to be redrawn. 323 | // 324 | // The absolute value of the argument indicates the mouse button. 325 | // A positive value indicates the button was pressed. 326 | // A negative value indicates the button was released. 327 | func (b *TextBox) Click(pt image.Point, button int) (int, [2]int64) { 328 | pt.X -= textPadPx 329 | b.pt = pt 330 | switch { 331 | case b.button > 0 && button > 0: 332 | // b.button/button mouse chord; ignore it for now. 333 | return button, [2]int64{} 334 | 335 | case b.button > 0 && button == -b.button: 336 | return unclick(b) 337 | 338 | case b.button == 0 && button == 1 && b.win.mods[2]: 339 | button = 2 340 | 341 | case b.button == 0 && button == 1 && b.win.mods[3]: 342 | button = 3 343 | 344 | case b.button != 1 && button == -1: // mod-button unclick 345 | return unclick(b) 346 | } 347 | if button > 0 { 348 | click(b, button) 349 | } 350 | return button, [2]int64{} 351 | } 352 | 353 | func unclick(b *TextBox) (int, [2]int64) { 354 | button := b.button 355 | b.button = 0 356 | dot := b.dots[button].At 357 | if button != 1 { 358 | setDot(b, button, 0, 0) 359 | } 360 | return -button, dot 361 | } 362 | 363 | func click(b *TextBox, button int) { 364 | b.button = button 365 | if button == 1 { 366 | if b.now().Sub(b.clickTime) < doubleClickDuration { 367 | doubleClick(b) 368 | return 369 | } 370 | b.clickTime = b.now() 371 | } 372 | b.clickAt, b.dragTextBox = atPoint(b, b.pt) 373 | setDot(b, button, b.clickAt, b.clickAt) 374 | if button == 1 { 375 | b.cursorCol = -1 376 | } 377 | } 378 | 379 | var delim = [][2]rune{ 380 | {'(', ')'}, 381 | {'{', '}'}, 382 | {'[', ']'}, 383 | {'<', '>'}, 384 | {'«', '»'}, 385 | {'\'', '\''}, 386 | {'"', '"'}, 387 | {'`', '`'}, 388 | {'“', '”'}, 389 | } 390 | 391 | func doubleClick(b *TextBox) { 392 | prev := prevRune(b) 393 | for _, ds := range delim { 394 | if ds[0] == prev { 395 | selectForwardDelim(b, ds[0], ds[1]) 396 | return 397 | } 398 | } 399 | cur := curRune(b) 400 | for _, ds := range delim { 401 | if ds[1] == cur { 402 | selectReverseDelim(b, ds[1], ds[0]) 403 | return 404 | } 405 | } 406 | if prev == -1 || prev == '\n' || cur == -1 || cur == '\n' { 407 | selectLine(b) 408 | return 409 | } 410 | if wordRune(cur) { 411 | selectWord(b) 412 | return 413 | } 414 | } 415 | 416 | func prevRune(b *TextBox) rune { 417 | front, _ := rope.Split(b.text, b.dots[1].At[0]) 418 | rr := rope.NewReverseReader(front) 419 | r, _, err := rr.ReadRune() 420 | if err != nil { 421 | return -1 422 | } 423 | return r 424 | } 425 | 426 | func curRune(b *TextBox) rune { 427 | _, back := rope.Split(b.text, b.dots[1].At[0]) 428 | rr := rope.NewReader(back) 429 | r, _, err := rr.ReadRune() 430 | if err != nil { 431 | return -1 432 | } 433 | return r 434 | } 435 | 436 | func selectForwardDelim(b *TextBox, open, close rune) { 437 | nest := 1 438 | _, back := rope.Split(b.text, b.dots[1].At[0]) 439 | end := rope.IndexFunc(back, func(r rune) bool { 440 | switch r { 441 | case close: 442 | nest-- 443 | case open: 444 | nest++ 445 | } 446 | return nest == 0 447 | }) 448 | if end < 0 { 449 | return 450 | } 451 | setDot(b, 1, b.dots[1].At[0], end+b.dots[1].At[0]) 452 | } 453 | 454 | func selectReverseDelim(b *TextBox, open, close rune) { 455 | nest := 1 456 | front, _ := rope.Split(b.text, b.dots[1].At[0]) 457 | start := rope.LastIndexFunc(front, func(r rune) bool { 458 | switch r { 459 | case close: 460 | nest-- 461 | case open: 462 | nest++ 463 | } 464 | return nest == 0 465 | }) 466 | if start < 0 { 467 | return 468 | } 469 | setDot(b, 1, start+int64(utf8.RuneLen(open)), b.dots[1].At[0]) 470 | } 471 | 472 | func selectLine(b *TextBox) { 473 | front, back := rope.Split(b.text, b.dots[1].At[0]) 474 | start := rope.LastIndexFunc(front, func(r rune) bool { return r == '\n' }) 475 | if start < 0 { 476 | start = 0 477 | } else { 478 | start++ // Don't include the \n. 479 | } 480 | end := rope.IndexFunc(back, func(r rune) bool { return r == '\n' }) 481 | if end < 0 { 482 | end = b.text.Len() 483 | } else { 484 | end += b.dots[1].At[0] + 1 // Do include the \n. 485 | } 486 | setDot(b, 1, start, end) 487 | } 488 | 489 | func selectWord(b *TextBox) { 490 | front, back := rope.Split(b.text, b.dots[1].At[0]) 491 | var delim rune 492 | start := rope.LastIndexFunc(front, func(r rune) bool { 493 | delim = r 494 | return !wordRune(r) 495 | }) 496 | if start < 0 { 497 | start = 0 498 | } else { 499 | start += int64(utf8.RuneLen(delim)) 500 | } 501 | end := rope.IndexFunc(back, func(r rune) bool { return !wordRune(r) }) 502 | if end < 0 { 503 | end = b.text.Len() 504 | } else { 505 | end += b.dots[1].At[0] 506 | } 507 | setDot(b, 1, start, end) 508 | } 509 | 510 | func wordRune(r rune) bool { 511 | return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' 512 | } 513 | 514 | // Dir handles a keyboard directional event 515 | // and returns whether the text box image needs to be redrawn. 516 | // 517 | // These events are generated by the arrow keys, 518 | // page up and down keys, and the home and end keys. 519 | // Exactly one of x or y must be non-zero. 520 | // 521 | // If the absolute value is 1, then it is treated as an arrow key 522 | // in the corresponding direction (x-horizontal, y-vertical, 523 | // negative-left/up, positive-right/down). 524 | // If the absolute value is math.MinInt16, it is treated as a home event. 525 | // If the absolute value is math.MathInt16, it is end. 526 | // Otherwise, if the value for y is non-zero it is page up/down. 527 | // Other non-zero values for x are currently ignored. 528 | // 529 | // Dir only handles key press events, not key releases. 530 | func (b *TextBox) Dir(x, y int) { 531 | switch { 532 | case x == -1: 533 | at := leftRight(b, "-") 534 | b.cursorCol = -1 535 | setDot(b, 1, at, at) 536 | case x == 1: 537 | at := leftRight(b, "+") 538 | b.cursorCol = -1 539 | setDot(b, 1, at, at) 540 | case y == -1: 541 | at := upDown(b, "-") 542 | setDot(b, 1, at, at) 543 | case y == 1: 544 | at := upDown(b, "+") 545 | setDot(b, 1, at, at) 546 | case y == math.MinInt16: 547 | showAddr(b, 0) 548 | case y == math.MaxInt16: 549 | showAddr(b, b.text.Len()) 550 | case y < 0: 551 | scrollUp(b, pageSize(b)) 552 | case y > 0: 553 | scrollDown(b, pageSize(b)) 554 | } 555 | } 556 | 557 | func pageSize(b *TextBox) int { 558 | m := b.style.Face.Metrics() 559 | h := (m.Height + m.Descent).Floor() 560 | if h == 0 { 561 | return 1 562 | } 563 | return b.size.Y / (4 * h) 564 | } 565 | 566 | func leftRight(b *TextBox, dir string) int64 { 567 | var at [2]int64 568 | var err error 569 | if b.dots[1].At[0] < b.dots[1].At[1] { 570 | at, err = edit.Addr(b.dots[1].At, dir+"#0", b.text) 571 | } else { 572 | at, err = edit.Addr(b.dots[1].At, dir+"#1", b.text) 573 | } 574 | if err != nil { 575 | return b.dots[1].At[0] 576 | } 577 | return at[0] 578 | } 579 | 580 | func upDown(b *TextBox, dir string) int64 { 581 | if b.cursorCol < 0 { 582 | b.cursorCol = cursorCol(b) 583 | } 584 | 585 | // prev/next line 586 | // -+ selects the entire line containing dot. 587 | // This handles the case where the cursor is at 0, 588 | // and 0+1 is the first line instead of the second. 589 | at, err := edit.Addr(b.dots[1].At, "-+"+dir, b.text) 590 | if err != nil { 591 | if dir == "+" { 592 | return b.text.Len() 593 | } 594 | return 0 595 | } 596 | 597 | // rune offset into the line 598 | max := at[1] 599 | at, err = edit.Addr([2]int64{at[0], at[0]}, "+#"+strconv.Itoa(b.cursorCol), b.text) 600 | if err != nil || max == 0 { 601 | return max 602 | } 603 | if at[0] >= max { 604 | return max - 1 605 | } 606 | return at[0] 607 | } 608 | 609 | func cursorCol(b *TextBox) int { 610 | var n int 611 | rr := rope.NewReverseReader(rope.Slice(b.text, 0, b.dots[1].At[0])) 612 | for { 613 | r, _, err := rr.ReadRune() 614 | if err != nil || r == '\n' { 615 | break 616 | } 617 | n++ 618 | } 619 | return n 620 | } 621 | 622 | func scrollUp(b *TextBox, delta int) { 623 | if b.at == 0 { 624 | return 625 | } 626 | bol, err := edit.Addr([2]int64{b.at, b.at}, "-0", b.text) 627 | if err != nil { 628 | panic(err.Error()) 629 | } 630 | if b.at != bol[0] { 631 | b.at = bol[0] 632 | delta-- 633 | } 634 | for i := 0; i < delta; i++ { 635 | at, err := edit.Addr([2]int64{b.at, b.at}, "-1", b.text) 636 | if err != nil { 637 | panic(err.Error()) 638 | } 639 | if b.at = at[0]; b.at == 0 { 640 | break 641 | } 642 | } 643 | dirtyLines(b) 644 | } 645 | 646 | func scrollDown(b *TextBox, delta int) { 647 | lines := b.lines() 648 | for i := 0; i < delta; i++ { 649 | if len(lines) > 0 { 650 | b.at += lines[0].n 651 | lines = lines[1:] 652 | continue 653 | } 654 | at, err := edit.Addr([2]int64{b.at, b.at}, "+1", b.text) 655 | if err != nil { 656 | // Must be EOF. 657 | b.at = b.text.Len() 658 | break 659 | } 660 | if b.at = at[0]; b.at == 0 { 661 | break 662 | } 663 | } 664 | dirtyLines(b) 665 | } 666 | 667 | // Mod handles a modifier key state change event. 668 | func (b *TextBox) Mod(m int) { 669 | if b.button > 0 { 670 | b.Click(b.pt, m) 671 | } 672 | } 673 | 674 | const ( 675 | esc = 0x1b 676 | del = 0x7f 677 | ) 678 | 679 | // Rune handles the event of a rune being typed 680 | // and returns whether the text box image needs to be redrawn. 681 | // 682 | // The argument is a rune indicating the glyph typed 683 | // after interpretation by any system-dependent 684 | // keyboard/layout mapping. 685 | // For example, if the 'a' key is pressed 686 | // while the shift key is held, 687 | // the argument would be the letter 'A'. 688 | // 689 | // If the rune is positive, the event is a key press, 690 | // if negative, a key release. 691 | func (b *TextBox) Rune(r rune) { 692 | switch r { 693 | case '\b': 694 | if b.dots[1].At[0] == b.dots[1].At[1] { 695 | ed(b, ".-#1,.d") 696 | } else { 697 | ed(b, ".d") 698 | } 699 | case del, esc: 700 | if b.dots[1].At[0] == b.dots[1].At[1] { 701 | ed(b, ".,.+#1d") 702 | } else { 703 | ed(b, ".d") 704 | } 705 | case '/': 706 | ed(b, ".c/\\/") 707 | case '\n': 708 | ed(b, ".c/\\n") 709 | default: 710 | ed(b, ".c/"+string([]rune{r})) 711 | } 712 | setDot(b, 1, b.dots[1].At[1], b.dots[1].At[1]) 713 | } 714 | 715 | // Draw draws the text box to the image with the upper-left of the box at 0,0. 716 | func (b *TextBox) Draw(dirty bool, img draw.Image) { 717 | size := img.Bounds().Size() 718 | if dirty || size != b.size { 719 | b.size = size 720 | dirtyLines(b) 721 | } 722 | if !b.dirty { 723 | return 724 | } 725 | b.dirty = false 726 | at := b.at 727 | lines := b.lines() 728 | var y fixed.Int26_6 729 | for i := range lines { 730 | l := &lines[i] 731 | if !l.dirty { 732 | y += l.h 733 | at += l.n 734 | continue 735 | } 736 | at1 := at + l.n 737 | drawLine(b, img, at, y, *l) 738 | l.dirty = false 739 | y += l.h 740 | at = at1 741 | } 742 | if y.Floor() < size.Y { 743 | r := image.Rect(0, y.Floor(), size.X, size.Y) 744 | fillRect(img, b.style.BG, r.Add(img.Bounds().Min)) 745 | } 746 | 747 | // Draw a cursor for empty text. 748 | if b.text.Len() == 0 { 749 | m := b.style.Face.Metrics() 750 | h := m.Height + m.Descent 751 | drawCursor(b, img, fixed.I(textPadPx), 0, h) 752 | return 753 | } 754 | // Draw a cursor just after the last line of text. 755 | if len(lines) == 0 { 756 | return 757 | } 758 | lastLine := &lines[len(lines)-1] 759 | if b.dots[1].At[0] == b.dots[1].At[1] && 760 | at == b.dots[1].At[0] && 761 | at == b.text.Len() && 762 | lastRune(lastLine) == '\n' { 763 | m := b.style.Face.Metrics() 764 | h := m.Height + m.Descent 765 | drawCursor(b, img, fixed.I(textPadPx), y, y+h) 766 | } 767 | } 768 | 769 | func drawLine(b *TextBox, img draw.Image, at int64, y0 fixed.Int26_6, l line) { 770 | var prevRune rune 771 | x0 := fixed.I(textPadPx) 772 | yb, y1 := y0+l.a, y0+l.h 773 | 774 | // leading padding 775 | pad := image.Rect(0, y0.Floor(), textPadPx, y1.Floor()) 776 | fillRect(img, b.style.BG, pad.Add(img.Bounds().Min)) 777 | 778 | for i, s := range l.spans { 779 | x1 := x0 + s.w 780 | 781 | bbox := image.Rect(x0.Floor(), y0.Floor(), x1.Floor(), y1.Floor()) 782 | fillRect(img, s.style.BG, bbox.Add(img.Bounds().Min)) 783 | 784 | for _, r := range s.text { 785 | if prevRune != 0 { 786 | x0 += s.style.Face.Kern(prevRune, r) 787 | } 788 | prevRune = r 789 | var adv fixed.Int26_6 790 | if r == '\t' || r == '\n' { 791 | adv = advance(b, s.style, x0-fixed.I(textPadPx), r) 792 | } else { 793 | adv = drawGlyph(img, s.style, x0, yb, r) 794 | } 795 | if b.dots[1].At[0] == b.dots[1].At[1] && b.dots[1].At[0] == at { 796 | drawCursor(b, img, x0, y0, y1) 797 | } 798 | x0 += adv 799 | at += int64(utf8.RuneLen(r)) 800 | } 801 | x0 = x1 802 | if i < len(l.spans)-1 && l.spans[i+1].style.Face != s.style.Face { 803 | prevRune = 0 804 | } 805 | } 806 | 807 | // trailing padding 808 | r := image.Rect(x0.Floor(), y0.Floor(), img.Bounds().Size().X, y1.Floor()) 809 | fillRect(img, b.style.BG, r.Add(img.Bounds().Min)) 810 | 811 | if b.dots[1].At[0] == b.dots[1].At[1] && 812 | at == b.dots[1].At[0] && 813 | at == b.text.Len() && 814 | prevRune != '\n' { 815 | drawCursor(b, img, x0, y0, y1) 816 | } 817 | } 818 | 819 | func drawGlyph(img draw.Image, style text.Style, x0, yb fixed.Int26_6, r rune) fixed.Int26_6 { 820 | pt := fixed.Point26_6{X: x0, Y: yb} 821 | dr, m, mp, adv, ok := style.Face.Glyph(pt, r) 822 | if !ok { 823 | dr, m, mp, adv, _ = style.Face.Glyph(pt, unicode.ReplacementChar) 824 | } 825 | dr = dr.Add(img.Bounds().Min) 826 | fg := image.NewUniform(style.FG) 827 | draw.DrawMask(img, dr, fg, image.ZP, m, mp, draw.Over) 828 | return adv 829 | } 830 | 831 | func drawCursor(b *TextBox, img draw.Image, x, y0, y1 fixed.Int26_6) { 832 | if !b.showCursor { 833 | return 834 | } 835 | x0 := x.Floor() 836 | r := image.Rect(x0, y0.Floor(), x0+cursorWidthPx, y1.Floor()) 837 | fillRect(img, b.style.FG, r.Add(img.Bounds().Min)) 838 | } 839 | 840 | func fillRect(img draw.Image, c color.Color, r image.Rectangle) { 841 | draw.Draw(img, r, image.NewUniform(c), image.ZP, draw.Src) 842 | } 843 | 844 | func atPoint(b *TextBox, pt image.Point) (int64, image.Rectangle) { 845 | lines := b.lines() 846 | if len(lines) == 0 { 847 | m := b.style.Face.Metrics() 848 | h := m.Height + m.Descent 849 | return b.at, image.Rect(0, 0, 0, h.Floor()) 850 | } 851 | 852 | at := b.at 853 | var l *line 854 | var y0, y1 fixed.Int26_6 855 | for i := range lines { 856 | l = &lines[i] 857 | y1 = y0 + l.h 858 | if i == len(lines)-1 || y1.Floor() > pt.Y { 859 | break 860 | } 861 | at += l.n 862 | y0 = y1 863 | } 864 | 865 | if y1.Floor() <= pt.Y && lastRune(l) == '\n' { 866 | // The cursor is at the beginning of the line following the last. 867 | m := b.style.Face.Metrics() 868 | h := m.Height + m.Descent 869 | return at + l.n, image.Rect(0, y1.Floor(), 0, (y1 + h).Floor()) 870 | } 871 | 872 | at0 := at 873 | var s *span 874 | var prevTextStyle text.Style 875 | var prevRune rune 876 | var x0, x1 fixed.Int26_6 877 | for i := range l.spans { 878 | s = &l.spans[i] 879 | r, _ := utf8.DecodeRuneInString(s.text) 880 | if prevTextStyle == s.style { 881 | x0 += kern(s.style, prevRune, r) 882 | } 883 | x1 = x0 + s.w 884 | if i == len(l.spans)-1 || x1.Floor() > pt.X { 885 | break 886 | } 887 | at += int64(len(s.text)) 888 | prevRune, _ = utf8.DecodeLastRuneInString(s.text) 889 | prevTextStyle = s.style 890 | x0 = x1 891 | } 892 | 893 | if y1.Floor() <= pt.Y { 894 | x := x0.Floor() 895 | return at0 + l.n, image.Rect(x, y1.Floor(), x, (y1 + l.h).Floor()) 896 | } 897 | 898 | x1 = x0 899 | for _, r := range s.text { 900 | x0 += kern(s.style, prevRune, r) 901 | x1 = x0 + advance(b, s.style, x0, r) 902 | if x1.Floor() > pt.X { 903 | break 904 | } 905 | rl := utf8.RuneLen(r) 906 | at += int64(rl) 907 | x0 = x1 908 | prevRune = r 909 | } 910 | rect := image.Rect(x0.Floor(), y0.Floor(), x1.Floor(), y1.Floor()) 911 | return at, rect 912 | } 913 | 914 | func lastRune(l *line) rune { 915 | if len(l.spans) == 0 { 916 | return utf8.RuneError 917 | } 918 | s := &l.spans[len(l.spans)-1] 919 | r, _ := utf8.DecodeLastRuneInString(s.text) 920 | return r 921 | } 922 | 923 | func setDot(b *TextBox, i int, start, end int64) { 924 | if start < 0 || start > b.text.Len() { 925 | panic("bad start") 926 | } 927 | if end < 0 || end > b.text.Len() { 928 | panic("bad end") 929 | } 930 | dirtyDot(b, b.dots[i].At) 931 | b.dots[i].At[0] = start 932 | b.dots[i].At[1] = end 933 | if i == 1 && start == end { 934 | b.showCursor = true 935 | b.blinkTime = b.now().Add(blinkDuration) 936 | } 937 | if dirtyDot(b, b.dots[i].At) { 938 | showAddr(b, b.dots[i].At[0]) 939 | } 940 | } 941 | 942 | func showAddr(b *TextBox, at int64) { 943 | bol, err := edit.Addr([2]int64{at, at}, "-0", b.text) 944 | if err != nil { 945 | panic(err.Error()) 946 | } 947 | b.at = bol[0] 948 | // TODO: This shows the start of the line containing the addr. 949 | // If it's a multi-line text line, then we may need to scroll forward 950 | // in order to see the address. 951 | scrollUp(b, pageSize(b)) 952 | dirtyLines(b) 953 | } 954 | 955 | // dirtyDot returns true if the dot is a point that is off screen. 956 | func dirtyDot(b *TextBox, dot [2]int64) bool { 957 | if dot[0] < dot[1] { 958 | dirtyLines(b) 959 | return false 960 | } 961 | b.dirty = true 962 | at0 := b.at 963 | lines := b.lines() 964 | var y0 fixed.Int26_6 965 | for i := range lines { 966 | at1 := at0 + lines[i].n 967 | if at0 <= dot[0] && dot[0] < at1 { 968 | lines[i].dirty = true 969 | return false 970 | } 971 | y0 += lines[i].h 972 | at0 = at1 973 | } 974 | if n := len(lines); n > 0 && 975 | dot[0] == b.text.Len() && 976 | lastRune(&lines[n-1]) != '\n' { 977 | lines[n-1].dirty = true 978 | return false 979 | } 980 | m := b.style.Face.Metrics() 981 | h := m.Height + m.Descent 982 | return dot[0] < b.at || (y0+h).Floor() >= b.size.Y 983 | } 984 | 985 | func dirtyLines(b *TextBox) { 986 | b.dirty = true 987 | b._lines = b._lines[:0] 988 | } 989 | 990 | func (b *TextBox) lines() []line { 991 | if len(b._lines) == 0 { 992 | reset(b) 993 | } 994 | return b._lines 995 | } 996 | 997 | func reset(b *TextBox) { 998 | at := b.at 999 | rs := bufio.NewReader( 1000 | rope.NewReader(rope.Slice(b.text, b.at, b.text.Len())), 1001 | ) 1002 | maxx := b.size.X - 2*textPadPx 1003 | var y fixed.Int26_6 1004 | var txt strings.Builder 1005 | stack := [][]syntax.Highlight{b.syntax, b.highlight, {b.dots[1]}, {b.dots[2]}, {b.dots[3]}} 1006 | for at < b.text.Len() && y < fixed.I(b.size.Y) { 1007 | var prevRune rune 1008 | var x0, x fixed.Int26_6 1009 | m := b.style.Face.Metrics() 1010 | line := line{dirty: true, a: m.Ascent, h: m.Height + m.Descent} 1011 | style, stack, next := nextTextStyle(b.style, stack, at) 1012 | for { 1013 | r, w, err := rs.ReadRune() 1014 | if err != nil { 1015 | break 1016 | } 1017 | x += kern(style, prevRune, r) 1018 | if r == '\n' { 1019 | txt.WriteRune(r) 1020 | at++ 1021 | line.n++ 1022 | x = fixed.I(maxx) 1023 | break 1024 | } 1025 | adv := advance(b, style, x, r) 1026 | if (x + adv).Ceil() >= maxx { 1027 | x = fixed.I(maxx) 1028 | rs.UnreadRune() 1029 | break 1030 | } 1031 | txt.WriteRune(r) 1032 | x += adv 1033 | at += int64(w) 1034 | line.n += int64(w) 1035 | if at == next { 1036 | appendSpan(&line, x0, x, style, &txt) 1037 | x0 = x 1038 | prevFace := style.Face 1039 | style, stack, next = nextTextStyle(b.style, stack, at) 1040 | if prevFace != style.Face { 1041 | prevRune = 0 1042 | } 1043 | } 1044 | } 1045 | appendSpan(&line, x0, x, style, &txt) 1046 | if y += line.h; y > fixed.I(b.size.Y) { 1047 | break 1048 | } 1049 | b._lines = append(b._lines, line) 1050 | } 1051 | } 1052 | 1053 | func appendSpan(line *line, x0, x fixed.Int26_6, style text.Style, text *strings.Builder) { 1054 | m := style.Face.Metrics() 1055 | line.a = max(line.a, m.Ascent) 1056 | line.h = max(line.h, m.Height+m.Descent) 1057 | line.spans = append(line.spans, span{ 1058 | w: x - x0, 1059 | text: text.String(), 1060 | style: style, 1061 | }) 1062 | text.Reset() 1063 | } 1064 | 1065 | func kern(style text.Style, prev, cur rune) fixed.Int26_6 { 1066 | if prev == 0 { 1067 | return 0 1068 | } 1069 | return style.Face.Kern(prev, cur) 1070 | } 1071 | 1072 | func max(a, b fixed.Int26_6) fixed.Int26_6 { 1073 | if a > b { 1074 | return a 1075 | } 1076 | return b 1077 | } 1078 | 1079 | func nextTextStyle(def text.Style, stack [][]syntax.Highlight, at int64) (text.Style, [][]syntax.Highlight, int64) { 1080 | style, next := def, int64(-1) 1081 | for i := range stack { 1082 | for len(stack[i]) > 0 && stack[i][0].At[1] <= at { 1083 | stack[i] = stack[i][1:] 1084 | } 1085 | if len(stack[i]) == 0 { 1086 | continue 1087 | } 1088 | hi := stack[i][0] 1089 | if hi.At[0] > at { 1090 | if hi.At[0] < next || next < 0 { 1091 | next = hi.At[0] 1092 | } 1093 | continue 1094 | } 1095 | if hi.At[1] < next || next < 0 { 1096 | next = hi.At[1] 1097 | } 1098 | style = style.Merge(hi.Style) 1099 | } 1100 | return style, stack, next 1101 | } 1102 | 1103 | func advance(b *TextBox, style text.Style, x fixed.Int26_6, r rune) fixed.Int26_6 { 1104 | switch r { 1105 | case '\n': 1106 | return fixed.I(b.size.X-2*textPadPx) - x 1107 | case '\t': 1108 | spaceWidth, ok := b.style.Face.GlyphAdvance(' ') 1109 | if !ok { 1110 | return 0 1111 | } 1112 | tabWidth := spaceWidth.Mul(fixed.I(8)) 1113 | adv := tabWidth - (x % tabWidth) 1114 | if adv < spaceWidth { 1115 | adv += tabWidth 1116 | } 1117 | return adv 1118 | default: 1119 | adv, ok := style.Face.GlyphAdvance(r) 1120 | if !ok { 1121 | adv, _ = style.Face.GlyphAdvance(unicode.ReplacementChar) 1122 | } 1123 | return adv 1124 | } 1125 | } 1126 | --------------------------------------------------------------------------------