├── AUTHORS ├── copyright.sh ├── .travis.yml ├── updatecov.sh ├── LICENSE ├── ui ├── font.go ├── ui.go ├── main.go ├── screen_test.go ├── client.go ├── text │ ├── main.go │ └── text_test.go ├── sheet.go ├── server.go ├── textbox.go ├── column.go └── window.go ├── editor ├── benchmark_test.go ├── editortest │ └── editortest.go ├── editor.go ├── client.go └── view │ ├── view.go │ └── view_test.go ├── gok.sh ├── README.md ├── edit ├── runes │ ├── bench_test.go │ ├── runes.go │ ├── runes_test.go │ └── buffer.go ├── edittest │ └── edittest.go ├── addr_bench_test.go ├── edit_example_test.go ├── text.go ├── buffer_test.go └── buffer.go ├── ted └── ted.go └── websocket ├── websocket_test.go └── websocket.go /AUTHORS: -------------------------------------------------------------------------------- 1 | Ethan Burns 2 | Scott Kiesel 3 | Steve McCoy 4 | Geoffrey Mitchell 5 | -------------------------------------------------------------------------------- /copyright.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Adds a Copyright comment to all .go files without one. 3 | 4 | for f in $(find . -name \*.go); do 5 | if grep -q "Copyright" $f; then 6 | echo skipping $f 7 | continue 8 | fi 9 | mv $f ${f}~ 10 | echo -e "// Copyright © 2016, The T Authors.\n" > $f 11 | cat ${f}~ >> $f 12 | rm ${f}~ 13 | done -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 1.6 4 | 5 | notifications: 6 | email: false 7 | 8 | env: 9 | - PATH=$HOME/gopath/bin:$PATH 10 | 11 | install: 12 | - go get golang.org/x/tools/cmd/cover 13 | - go get github.com/mattn/goveralls 14 | - go get golang.org/x/lint/golint 15 | - go get -d -v ./... && go build -v ./... 16 | 17 | script: 18 | - ./gok.sh 19 | - go test -race ./... 20 | - if [[ $TRAVIS_SECURE_ENV_VARS = "true" ]]; then bash ./updatecov.sh; fi 21 | -------------------------------------------------------------------------------- /updatecov.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | test -n "$COVERALLS_TOKEN" || { 4 | echo "COVERALLS_TOKEN not set, skipping coveralls" 5 | exit 0 6 | } 7 | 8 | echo "mode: set" > profile 9 | for dir in $(find . -maxdepth 10 -name '*.go' -exec dirname '{}' \; | uniq ); do 10 | go test -v -coverprofile=p $dir > out 2>&1 || { 11 | cat out 12 | rm -f p out 13 | exit 1 14 | } 15 | test -f p && cat p | grep -v "mode: set" >> profile 16 | rm -f p out 17 | done 18 | 19 | $HOME/gopath/bin/goveralls -coverprofile=profile -service=travis-ci -repotoken $COVERALLS_TOKEN 20 | rm -f profile 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2015, The T Authors. 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /ui/font.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package ui 4 | 5 | import ( 6 | "log" 7 | 8 | "github.com/golang/freetype/truetype" 9 | "golang.org/x/image/font" 10 | "golang.org/x/image/font/basicfont" 11 | "golang.org/x/image/font/gofont/goregular" 12 | ) 13 | 14 | var defaultFont = loadDefaultFont() 15 | 16 | func newFace(dpi float64) font.Face { 17 | if defaultFont == nil { 18 | return basicfont.Face7x13 19 | } 20 | return truetype.NewFace(defaultFont, &truetype.Options{ 21 | Size: 11, // pt 22 | DPI: dpi, 23 | }) 24 | } 25 | 26 | func loadDefaultFont() *truetype.Font { 27 | ttf, err := truetype.Parse(goregular.TTF) 28 | if err != nil { 29 | log.Printf("Failed to load default font %s", err) 30 | return nil 31 | } 32 | return ttf 33 | } 34 | -------------------------------------------------------------------------------- /editor/benchmark_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package editor 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/eaburns/T/edit" 9 | "github.com/eaburns/T/editor/editortest" 10 | ) 11 | 12 | func BenchmarkDo(b *testing.B) { 13 | s := editortest.NewServer(NewServer()) 14 | defer s.Close() 15 | 16 | buffersURL := s.PathURL("/", "buffers") 17 | buf, err := NewBuffer(buffersURL) 18 | if err != nil { 19 | panic(err) 20 | } 21 | bufferURL := s.PathURL(buf.Path) 22 | ed, err := NewEditor(bufferURL) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | textURL := s.PathURL(ed.Path, "text") 28 | if _, err := Do(textURL, edit.Change(edit.All, "Hello, World")); err != nil { 29 | panic(err) 30 | } 31 | 32 | b.ResetTimer() 33 | for i := 0; i < b.N; i++ { 34 | if _, err := Do(textURL, edit.Print(edit.All)); err != nil { 35 | panic(err) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /editor/editortest/editortest.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | // Package editortest provides an editor server for use in tests. 4 | package editortest 5 | 6 | import ( 7 | "net/http/httptest" 8 | "net/url" 9 | "path" 10 | 11 | "github.com/gorilla/mux" 12 | ) 13 | 14 | // EditorServer is an interface implemented by editor.Server. 15 | type EditorServer interface { 16 | RegisterHandlers(*mux.Router) 17 | Close() error 18 | } 19 | 20 | // Server is an HTTP editor server. 21 | type Server struct { 22 | // URL is the URL of the server. 23 | URL *url.URL 24 | 25 | editorServer EditorServer 26 | httpServer *httptest.Server 27 | } 28 | 29 | // NewServer returns a new, running Server. 30 | func NewServer(editorServer EditorServer) *Server { 31 | router := mux.NewRouter() 32 | editorServer.RegisterHandlers(router) 33 | httpServer := httptest.NewServer(router) 34 | url, err := url.Parse(httpServer.URL) 35 | if err != nil { 36 | panic(err) 37 | } 38 | return &Server{ 39 | URL: url, 40 | editorServer: editorServer, 41 | httpServer: httpServer, 42 | } 43 | } 44 | 45 | // PathURL returns the URL for the given path on this server. 46 | func (s *Server) PathURL(elems ...string) *url.URL { 47 | v := *s.URL 48 | v.Path = path.Join(elems...) 49 | return &v 50 | } 51 | 52 | // Close closes the Server. 53 | func (s *Server) Close() { 54 | s.httpServer.Close() 55 | s.editorServer.Close() 56 | } 57 | -------------------------------------------------------------------------------- /gok.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Verifies that go code passes go fmt, go vet, golint, and go test. 4 | # 5 | 6 | o=$(mktemp tmp.XXXXXXXXXX) 7 | 8 | fail() { 9 | echo Failed 10 | cat $o 11 | rm $o 12 | exit 1 13 | } 14 | 15 | trap fail INT TERM 16 | 17 | echo Formatting 18 | gofmt -l $(find . -name '*.go') 2>&1 > $o 19 | test $(wc -l $o | awk '{ print $1 }') = "0" || fail 20 | 21 | echo Vetting 22 | go vet ./... 2>&1 > $o || fail 23 | 24 | echo Testing 25 | go test -test.timeout=10s ./... 2>&1 > $o || fail 26 | 27 | echo Linting 28 | golint .\ 29 | | grep -v 'should omit type SimpleAddress'\ 30 | | grep -v 'Buffer.Mark should have comment'\ 31 | | grep -v 'Buffer.SetMark should have comment'\ 32 | | grep -v 'Buffer.Change should have comment'\ 33 | | grep -v 'Buffer.Apply should have comment'\ 34 | | grep -v 'Buffer.Cancel should have comment'\ 35 | | grep -v 'Buffer.Undo should have comment'\ 36 | | grep -v 'Buffer.Reader should have comment'\ 37 | | grep -v 'Buffer.Redo should have comment'\ 38 | | grep -v 'Substitute.Do should have comment'\ 39 | | grep -v 'MarshalText should have comment'\ 40 | | grep -v 'UnmarshalText should have comment'\ 41 | | egrep -v "don't use underscores.*Test.*"\ 42 | > $o 2>&1 43 | # Silly: diff the grepped golint output with empty. 44 | # If it's non-empty, error, otherwise succeed. 45 | e=$(tempfile) 46 | touch $e 47 | diff $o $e > /dev/null || { rm $e; fail; } 48 | 49 | echo Leaks? 50 | ls /tmp | egrep "edit" 2>&1 > $o && fail 51 | 52 | rm $o $e 53 | -------------------------------------------------------------------------------- /ui/ui.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | // Package ui implements the T text editor UI. 4 | package ui 5 | 6 | // A NewWindowRequest requests a new window be created. 7 | type NewWindowRequest struct { 8 | // Width is the requested width. 9 | Width int `json:"width"` 10 | 11 | // Height is the requested height. 12 | Height int `json:"height"` 13 | } 14 | 15 | // A NewColumnRequest requests a new column be created. 16 | type NewColumnRequest struct { 17 | // X is the left-side of the column 18 | // given as a fraction of the window width. 19 | // X where 20 | // 0 the left side of the window 21 | // 0.5 the center of the window 22 | // 1 the right side of the window 23 | // A new column may be restricted to a minimum width. 24 | X float64 `json:"x"` 25 | } 26 | 27 | // A NewSheetRequest requests a new sheet be created. 28 | type NewSheetRequest struct { 29 | // URL is either the root URL of an editor server, 30 | // or the URL of an existing editor server buffer. 31 | // 32 | // If URL is an existing buffer, that buffer will be used as the sheet body. 33 | // Otherwise, a new buffer is created on the editor server for the body. 34 | URL string `json:"url"` 35 | } 36 | 37 | // A Window describes an opened window. 38 | type Window struct { 39 | // ID is the ID of the window. 40 | ID string `json:"id"` 41 | 42 | // Path is the path of the window's resource. 43 | Path string `json:"path"` 44 | } 45 | 46 | // A Sheet describes an opened sheet. 47 | type Sheet struct { 48 | // ID is the ID of the sheet. 49 | ID string `json:"id"` 50 | 51 | // Path is the path to the sheet's resource. 52 | Path string `json:"path"` 53 | 54 | // WindowPath is the path to the sheet's window's resource. 55 | WindowPath string `json:"windowPath"` 56 | 57 | // TagURL is the URL of the tag's buffer. 58 | TagURL string `json:"tagUrl"` 59 | 60 | // BodyURL is the URL of the body's buffer. 61 | BodyURL string `json:"bodyUrl"` 62 | } 63 | -------------------------------------------------------------------------------- /ui/main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | // +build ignore 4 | 5 | // Main is demo program to try out the ui package. 6 | package main 7 | 8 | import ( 9 | "image" 10 | "net/http/httptest" 11 | "net/url" 12 | "os" 13 | "path" 14 | 15 | "github.com/eaburns/T/editor" 16 | "github.com/eaburns/T/editor/editortest" 17 | "github.com/eaburns/T/ui" 18 | "github.com/gorilla/mux" 19 | "github.com/pkg/profile" 20 | "golang.org/x/exp/shiny/driver" 21 | "golang.org/x/exp/shiny/screen" 22 | ) 23 | 24 | func main() { driver.Main(Main) } 25 | 26 | // Main is the logical main function, called by the shiny driver. 27 | func Main(scr screen.Screen) { 28 | profiler := profile.Start(profile.CPUProfile) 29 | es := editortest.NewServer(editor.NewServer()) 30 | 31 | os.Setenv("T_INTERFACE_URL", es.PathURL("/").String()) 32 | 33 | r := mux.NewRouter() 34 | s := ui.NewServer(scr, es.PathURL("/")) 35 | s.SetDoneHandler(func() { 36 | es.Close() 37 | profiler.Stop() 38 | os.Exit(0) 39 | }) 40 | s.RegisterHandlers(r) 41 | baseURL, err := url.Parse(httptest.NewServer(r).URL) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | wins := *baseURL 47 | wins.Path = path.Join("/", "windows") 48 | 49 | win, err := ui.NewWindow(&wins, image.Pt(800, 600)) 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | sheets := *baseURL 55 | sheets.Path = path.Join(win.Path, "sheets") 56 | 57 | if _, err := ui.NewSheet(&sheets, es.PathURL("/")); err != nil { 58 | panic(err) 59 | } 60 | 61 | cols := *baseURL 62 | cols.Path = path.Join(win.Path, "columns") 63 | 64 | if err := ui.NewColumn(&cols, 0.33); err != nil { 65 | panic(err) 66 | } 67 | if _, err := ui.NewSheet(&sheets, es.PathURL("/")); err != nil { 68 | panic(err) 69 | } 70 | if _, err := ui.NewSheet(&sheets, es.PathURL("/")); err != nil { 71 | panic(err) 72 | } 73 | 74 | if err := ui.NewColumn(&cols, 0.66); err != nil { 75 | panic(err) 76 | } 77 | if _, err := ui.NewSheet(&sheets, es.PathURL("/")); err != nil { 78 | panic(err) 79 | } 80 | if _, err := ui.NewSheet(&sheets, es.PathURL("/")); err != nil { 81 | panic(err) 82 | } 83 | 84 | select {} 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/eaburns/T.svg?branch=master)](https://travis-ci.org/eaburns/T) 2 | [![Coverage Status](https://coveralls.io/repos/eaburns/T/badge.svg?branch=master&service=github)](https://coveralls.io/github/eaburns/T?branch=master) 3 | [![GoDoc](https://godoc.org/github.com/eaburns/T?status.svg)](https://godoc.org/github.com/eaburns/T) 4 | 5 | T is a text editor inspired by the Acme and Sam editors 6 | of the [Plan9](http://plan9.bell-labs.com/plan9/) operating system 7 | and [Plan9 from User Space](https://swtch.com/plan9port/) project. 8 | It aims to be much like Acme (see [Russ Cox's tour of Acme for a taste](http://research.swtch.com/acme)). 9 | 10 | 11 | T is still in the early stages of development. 12 | Here's a screenshot of the latest demo: 13 | ![screenshot](https://raw.githubusercontent.com/wiki/eaburns/T/screenshot.png) 14 | You can try it yourself with a couple of simple commands: 15 | ``` 16 | go get -u github.com/eaburns/T/... 17 | go run $GOPATH/src/github.com/eaburns/T/ui/main.go 18 | ``` 19 | 20 | By design, the T editor is client/server based 21 | It serves an HTTP API allowing client programs to implement editor extensions. 22 | 23 | The API is split across two different servers. 24 | One is the editor backend server which actually performs edits on buffers using the T edit language. 25 | The editor server supports multiple buffers and multiple, concurrent editors per-buffer. 26 | This allows programs to easily edit files using a simple HTTP interface. 27 | 28 | One such program is the T user interface itself. 29 | The user interface connects to edit servers to provide an Acme-like interface for editing. 30 | All edits made by the user interface use the editor API. 31 | This ensures that the editor API is sufficient to support a full-featured graphical editor, 32 | and it also allows the interface to sit on a remote machine. 33 | 34 | The user interface is also the second HTTP server. 35 | It serves an API for manipulating the interface, creating new windows, columns, sheets, etc. 36 | 37 | Together, these two servers will make it possible to customize and extend T to your liking — 38 | assuming that you are happy writing a little bit of code. 39 | -------------------------------------------------------------------------------- /edit/runes/bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015, The T Authors. 2 | 3 | // Pieces copied from go/test/bench/go1/regexp_test.go, 4 | // which requires the following notice: 5 | // Copyright 2013 The Go Authors. All rights reserved. 6 | 7 | package runes 8 | 9 | import ( 10 | "math/rand" 11 | "testing" 12 | ) 13 | 14 | const benchBlockSize = 4096 15 | 16 | func randomRunes(n int) []rune { 17 | rand.Seed(0) 18 | rs := make([]rune, n) 19 | for i := range rs { 20 | rs[i] = rune(rand.Int()) 21 | } 22 | return rs 23 | } 24 | 25 | func writeBench(b *testing.B, n int) { 26 | r := NewBuffer(benchBlockSize) 27 | defer r.Close() 28 | rs := randomRunes(n) 29 | b.SetBytes(int64(n * runeBytes)) 30 | b.ResetTimer() 31 | for i := 0; i < b.N; i++ { 32 | r.Insert(rs, 0) 33 | r.Delete(int64(n), 0) 34 | } 35 | } 36 | 37 | func BenchmarkWrite1(b *testing.B) { writeBench(b, 1) } 38 | func BenchmarkWrite1k(b *testing.B) { writeBench(b, 1024) } 39 | func BenchmarkWrite4k(b *testing.B) { writeBench(b, 4096) } 40 | func BenchmarkWrite10k(b *testing.B) { writeBench(b, 1048576) } 41 | 42 | func readBench(b *testing.B, n int) { 43 | r := NewBuffer(benchBlockSize) 44 | defer r.Close() 45 | r.Insert(randomRunes(n), 0) 46 | b.SetBytes(int64(n * runeBytes)) 47 | b.ResetTimer() 48 | for i := 0; i < b.N; i++ { 49 | r.Read(n, 0) 50 | } 51 | } 52 | 53 | func BenchmarkRead1(b *testing.B) { readBench(b, 1) } 54 | func BenchmarkRead1k(b *testing.B) { readBench(b, 1024) } 55 | func BenchmarkRead4k(b *testing.B) { readBench(b, 4096) } 56 | func BenchmarkRead10k(b *testing.B) { readBench(b, 1048576) } 57 | 58 | func benchmarkRune(b *testing.B, n int, rnd bool) { 59 | r := NewBuffer(benchBlockSize) 60 | defer r.Close() 61 | r.Insert(randomRunes(n), 0) 62 | 63 | inds := make([]int64, 4096) 64 | for i := range inds { 65 | if rnd { 66 | inds[i] = rand.Int63n(int64(n)) 67 | } else { 68 | inds[i] = int64(i % n) 69 | } 70 | } 71 | 72 | b.SetBytes(runeBytes) 73 | b.ResetTimer() 74 | for i := 0; i < b.N; i++ { 75 | r.Rune(inds[i%len(inds)]) 76 | } 77 | 78 | } 79 | 80 | func BenchmarkRune10kRand(b *testing.B) { benchmarkRune(b, 1048576, true) } 81 | func BenchmarkRune10kScan(b *testing.B) { benchmarkRune(b, 1048576, false) } 82 | func BenchmarkRuneCacheRand(b *testing.B) { benchmarkRune(b, benchBlockSize, true) } 83 | func BenchmarkRuneCacheScan(b *testing.B) { benchmarkRune(b, benchBlockSize, false) } 84 | -------------------------------------------------------------------------------- /editor/editor.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | // Package editor provides a server that serves an HTTP editor API, 4 | // and functions convenient client access to the server. 5 | package editor 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "time" 11 | 12 | "github.com/eaburns/T/edit" 13 | ) 14 | 15 | const wsTimeout = 5 * time.Second 16 | 17 | // A Buffer describes a buffer. 18 | type Buffer struct { 19 | // ID is the ID of the buffer. 20 | ID string `json:"id"` 21 | 22 | // Path is the path to the buffer's resource. 23 | Path string `json:"path"` 24 | 25 | // Sequence is the sequence number of the last edit on the buffer. 26 | Sequence int `json:"sequence"` 27 | 28 | // Editors containts the buffer's editors. 29 | Editors []Editor `json:"editors"` 30 | } 31 | 32 | // An Editor describes an editor. 33 | type Editor struct { 34 | // ID is the ID of the editor. 35 | ID string `json:"id"` 36 | 37 | // Path is the path to the editor's resource. 38 | Path string `json:"path"` 39 | 40 | // BufferPath is the path to the editor's buffer's resource. 41 | BufferPath string `json:"bufferPath"` 42 | } 43 | 44 | type editRequest struct{ edit.Edit } 45 | 46 | func (e *editRequest) MarshalText() ([]byte, error) { return []byte(e.String()), nil } 47 | 48 | func (e *editRequest) UnmarshalText(text []byte) error { 49 | var err error 50 | r := bytes.NewReader(text) 51 | if e.Edit, err = edit.Ed(r); err != nil { 52 | return err 53 | } 54 | if l := r.Len(); l != 0 { 55 | return errors.New("unexpected trailing text: " + string(text[l:])) 56 | } 57 | return nil 58 | } 59 | 60 | // An EditResult is result of performing an edito on a buffer. 61 | type EditResult struct { 62 | // Sequence is the sequence number unique to the edit. 63 | Sequence int `json:"sequence"` 64 | 65 | // Print is any data that the edit printed. 66 | Print string `json:"print,omitempty"` 67 | 68 | // Error is any error that occurred. 69 | Error string `json:"error,omitempty"` 70 | } 71 | 72 | // A ChangeList is an atomic sequence of changes 73 | // made by an edit to a buffer. 74 | type ChangeList struct { 75 | // Sequence is the sequence number 76 | // unique to the edit that made the changes. 77 | Sequence int `json:"sequence"` 78 | 79 | // Changes contains the changes made by an edit. 80 | // The changes are in the sequence applied to the buffer. 81 | Changes []Change `json:"changes"` 82 | } 83 | 84 | // MaxInline is the maximum size, in bytes, for which Change.Text is set. 85 | const MaxInline = 8 86 | 87 | // A Change is a single change made to a string of a buffer. 88 | type Change struct { 89 | // Span identifies the string of the buffer that was changed. 90 | // 91 | // The units of the Span are runes; 92 | // the first is the inclusive starting rune index, 93 | // and the second is the exclusive ending rune index. 94 | // 95 | // NOTE: in the future, we plan to change Span to use byte indices. 96 | edit.Span `json:"span"` 97 | 98 | // NewSize is the size, in runes, to which the span changed. 99 | NewSize int64 `json:"newSize"` 100 | 101 | // Text is the text to which the span changed. 102 | // Text is not set if the either new text size is 0 103 | // or greater than MaxInline bytes. 104 | Text []byte `json:"text"` 105 | } 106 | -------------------------------------------------------------------------------- /edit/edittest/edittest.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | // Package edittest contains utility functions for testing edits. 4 | package edittest 5 | 6 | import ( 7 | "fmt" 8 | "sort" 9 | ) 10 | 11 | // StateEquals returns whether the state (text plus marks) 12 | // equals the described state. 13 | func StateEquals(text string, marks map[rune][2]int64, descr string) bool { 14 | wantText, wantMarks := ParseState(descr) 15 | if text != wantText || len(marks) != len(wantMarks) { 16 | return false 17 | } 18 | for m, a := range marks { 19 | if wantMarks[m] != a { 20 | return false 21 | } 22 | } 23 | return true 24 | } 25 | 26 | // ParseState parses an editor state description. 27 | // 28 | // An editor state description describes 29 | // the contents of the buffer and the editor's marks. 30 | // Runes that are not between { and } represent the buffer contents. 31 | // Each rune between { and } represent 32 | // the beginning (first occurrence) 33 | // or end (second occurrence) 34 | // of a mark region with the name of the rune. 35 | // 36 | // As a special case, the empty string, "", is equal to "{..}". 37 | // 38 | // For example: 39 | // "{mm}abc{.}xyz{.n}123{n}αβξ" 40 | // Is a buffer with the contents: 41 | // "abcxyz123αβξ" 42 | // The mark m is the empty string at the beginning of the buffer. 43 | // The mark . contains the text "xyz". 44 | // The mark n contains the text "123". 45 | func ParseState(str string) (string, map[rune][2]int64) { 46 | if str == "" { 47 | str = "{..}" 48 | } 49 | 50 | var mark bool 51 | var contents []rune 52 | marks := make(map[rune][2]int64) 53 | count := make(map[rune]int) 54 | for _, r := range str { 55 | switch { 56 | case !mark && r == '{': 57 | mark = true 58 | case mark && r == '}': 59 | mark = false 60 | case mark: 61 | count[r]++ 62 | at := int64(len(contents)) 63 | if s, ok := marks[r]; !ok { 64 | marks[r] = [2]int64{at} 65 | } else { 66 | marks[r] = [2]int64{s[0], at} 67 | } 68 | default: 69 | contents = append(contents, r) 70 | } 71 | } 72 | for m, c := range count { 73 | if c != 2 { 74 | panic(fmt.Sprintf("%q, mark %c appears %d times", str, m, c)) 75 | } 76 | } 77 | return string(contents), marks 78 | } 79 | 80 | // StateString returns the state string description of the text and marks. 81 | // The returned string is in the format of ParseState. 82 | // The returned string is normalized so that multiple marks within { and } 83 | // are lexicographically ordered. 84 | func StateString(text string, marks map[rune][2]int64) string { 85 | addrMarks := make(map[int64]RuneSlice) 86 | for m, a := range marks { 87 | addrMarks[a[0]] = append(addrMarks[a[0]], m) 88 | addrMarks[a[1]] = append(addrMarks[a[1]], m) 89 | } 90 | var c []rune 91 | str := []rune(text) 92 | for i := 0; i < len(str)+1; i++ { 93 | if ms := addrMarks[int64(i)]; len(ms) > 0 { 94 | sort.Sort(ms) 95 | c = append(c, '{') 96 | c = append(c, ms...) 97 | c = append(c, '}') 98 | } 99 | if i < len(str) { 100 | c = append(c, str[i]) 101 | } 102 | } 103 | return string(c) 104 | 105 | } 106 | 107 | type RuneSlice []rune 108 | 109 | func (rs RuneSlice) Len() int { return len(rs) } 110 | func (rs RuneSlice) Less(i, j int) bool { return rs[i] < rs[j] } 111 | func (rs RuneSlice) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] } 112 | -------------------------------------------------------------------------------- /ui/screen_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package ui 4 | 5 | import ( 6 | "image" 7 | "image/color" 8 | "image/draw" 9 | 10 | "golang.org/x/exp/shiny/screen" 11 | "golang.org/x/image/math/f64" 12 | "golang.org/x/mobile/event/size" 13 | "golang.org/x/mobile/geom" 14 | ) 15 | 16 | type stubScreen struct{} 17 | 18 | func (*stubScreen) NewBuffer(size image.Point) (screen.Buffer, error) { 19 | return newTestBuffer(size), nil 20 | } 21 | 22 | func (*stubScreen) NewTexture(size image.Point) (screen.Texture, error) { 23 | return newTestTexture(size), nil 24 | } 25 | 26 | func (*stubScreen) NewWindow(opts *screen.NewWindowOptions) (screen.Window, error) { 27 | w := newTestWindow(opts) 28 | const pxPerPt = defaultDPI / ptPerInch 29 | w.Send(size.Event{ 30 | WidthPx: opts.Width, 31 | HeightPx: opts.Height, 32 | WidthPt: geom.Pt(opts.Width) * pxPerPt, 33 | HeightPt: geom.Pt(opts.Height) * pxPerPt, 34 | PixelsPerPt: pxPerPt, 35 | }) 36 | return w, nil 37 | } 38 | 39 | type stubBuffer struct{ img *image.RGBA } 40 | 41 | func newTestBuffer(size image.Point) *stubBuffer { 42 | return &stubBuffer{img: image.NewRGBA(image.Rect(0, 0, size.X, size.Y))} 43 | } 44 | func (*stubBuffer) Release() {} 45 | func (t *stubBuffer) Size() image.Point { return t.img.Bounds().Size() } 46 | func (t *stubBuffer) Bounds() image.Rectangle { return t.img.Bounds() } 47 | func (t *stubBuffer) RGBA() *image.RGBA { return t.img } 48 | 49 | type stubTexture struct{ size image.Point } 50 | 51 | func newTestTexture(size image.Point) *stubTexture { 52 | return &stubTexture{size: size} 53 | } 54 | func (*stubTexture) Release() {} 55 | func (t *stubTexture) Size() image.Point { return t.size } 56 | func (t *stubTexture) Bounds() image.Rectangle { 57 | return image.Rect(0, 0, t.size.X, t.size.Y) 58 | } 59 | func (*stubTexture) Upload(image.Point, screen.Buffer, image.Rectangle) {} 60 | func (*stubTexture) Fill(image.Rectangle, color.Color, draw.Op) {} 61 | 62 | type stubWindow struct { 63 | w, h int 64 | publish chan bool 65 | events chan interface{} 66 | } 67 | 68 | func newTestWindow(opts *screen.NewWindowOptions) *stubWindow { 69 | publish := make(chan bool) 70 | return &stubWindow{ 71 | w: opts.Width, 72 | h: opts.Height, 73 | publish: publish, 74 | events: make(chan interface{}, 100), 75 | } 76 | } 77 | 78 | func (t *stubWindow) Send(event interface{}) { t.events <- event } 79 | func (t *stubWindow) SendFirst(event interface{}) { panic("unimplemented") } 80 | func (t *stubWindow) NextEvent() interface{} { return <-t.events } 81 | 82 | func (t *stubWindow) Release() { 83 | for range t.publish { 84 | } 85 | } 86 | 87 | func (*stubWindow) Upload(image.Point, screen.Buffer, image.Rectangle) {} 88 | func (*stubWindow) Fill(image.Rectangle, color.Color, draw.Op) {} 89 | func (*stubWindow) Draw(f64.Aff3, screen.Texture, image.Rectangle, draw.Op, *screen.DrawOptions) {} 90 | func (*stubWindow) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) { 91 | } 92 | func (*stubWindow) Copy(image.Point, screen.Texture, image.Rectangle, draw.Op, *screen.DrawOptions) {} 93 | func (*stubWindow) Scale(image.Rectangle, screen.Texture, image.Rectangle, draw.Op, *screen.DrawOptions) { 94 | } 95 | func (t *stubWindow) Publish() screen.PublishResult { 96 | go func() { t.publish <- true }() 97 | return screen.PublishResult{BackBufferPreserved: false} 98 | } 99 | -------------------------------------------------------------------------------- /ted/ted.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | // Ted is a text editor similar to sam in it's non-downloaded mode. 4 | // 5 | // The Ted editor is mostly intended as an experiment. 6 | // It edits a single buffer, with the T edit language. 7 | // The T language is documented here: 8 | // https://godoc.org/github.com/eaburns/T/edit#Ed. 9 | // Ted adds a few additional commands: 10 | // e filename replaces the buffer with the contents of the file 11 | // w filename saves the buffer to the named file 12 | // q quits 13 | package main 14 | 15 | import ( 16 | "bufio" 17 | "flag" 18 | "fmt" 19 | "io" 20 | "os" 21 | "strings" 22 | 23 | "github.com/eaburns/T/edit" 24 | ) 25 | 26 | var ( 27 | logPath = flag.String("log", "", "a file to which all T edit commands are logged") 28 | ) 29 | 30 | func main() { 31 | flag.Parse() 32 | 33 | var log io.Writer 34 | if *logPath != "" { 35 | var err error 36 | if log, err = os.Create(*logPath); err != nil { 37 | fmt.Println("failed to open log:", err) 38 | return 39 | } 40 | } 41 | 42 | in := bufio.NewReader(os.Stdin) 43 | 44 | buf := edit.NewBuffer() 45 | defer buf.Close() 46 | 47 | var nl bool 48 | var prevAddr edit.Edit 49 | var file string 50 | for { 51 | var e edit.Edit 52 | r, _, err := in.ReadRune() 53 | switch { 54 | case err != nil && err != io.EOF: 55 | fmt.Println("failed to read input:", err) 56 | return 57 | case err == io.EOF || r == 'q': 58 | return 59 | case r == '\n': 60 | if nl && prevAddr != nil { 61 | e = prevAddr 62 | } 63 | case r == 'w': 64 | line, err := readLine(in) 65 | if err != nil { 66 | fmt.Println("failed to read input:", err) 67 | return 68 | } 69 | if line != "" { 70 | file = strings.TrimSpace(line) 71 | } 72 | if file == "" { 73 | fmt.Println("w requires an argument") 74 | continue 75 | } 76 | e = edit.PipeTo(edit.All, "cat > "+file) 77 | case r == 'e': 78 | line, err := readLine(in) 79 | if err != nil { 80 | fmt.Println("failed to read input:", err) 81 | return 82 | } 83 | if line == "" { 84 | fmt.Println("e requires an argument") 85 | continue 86 | } 87 | file = strings.TrimSpace(line) 88 | e = edit.PipeFrom(edit.All, "cat < "+file) 89 | default: 90 | if err := in.UnreadRune(); err != nil { 91 | panic(err) // Can't fail with bufio.Reader. 92 | } 93 | var err error 94 | e, err = edit.Ed(in) 95 | if err != nil { 96 | fmt.Println(err) 97 | readLine(in) // Chomp until EOL. 98 | continue 99 | } 100 | } 101 | nl = r == '\n' 102 | if e == nil { 103 | continue 104 | } 105 | 106 | if err := e.Do(buf, os.Stdout); err != nil { 107 | fmt.Println(err) 108 | continue 109 | } 110 | if log != nil { 111 | if _, err := io.WriteString(log, e.String()+"\n"); err != nil { 112 | fmt.Println("failed log edit:", err) 113 | return 114 | } 115 | } 116 | 117 | if strings.HasSuffix(e.String(), "=") || strings.HasSuffix(e.String(), "=#") { 118 | // The Edit just printed an address. Put a newline after it. 119 | fmt.Println("") 120 | } 121 | 122 | prevAddr = nil 123 | if strings.HasSuffix(e.String(), "k.") { 124 | // The Edit just set the address of dot. Print dot. 125 | prevAddr = e 126 | if err := edit.Print(edit.Dot).Do(buf, os.Stdout); err != nil { 127 | fmt.Println("failed to edit:", err) 128 | return 129 | } 130 | } 131 | } 132 | } 133 | 134 | func readLine(in io.RuneScanner) (string, error) { 135 | var s []rune 136 | for { 137 | switch r, _, err := in.ReadRune(); { 138 | case err != nil && err != io.EOF: 139 | return "", err 140 | case err == io.EOF || r == '\n': 141 | return string(s), nil 142 | default: 143 | s = append(s, r) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /ui/client.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package ui 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "errors" 9 | "image" 10 | "io" 11 | "io/ioutil" 12 | "net/http" 13 | "net/url" 14 | ) 15 | 16 | // ErrNotFound indicates that a resource is not found. 17 | var ErrNotFound = errors.New("not found") 18 | 19 | // Close does a DELETE. 20 | // The URL is expected to point at either a window path or a sheet path. 21 | func Close(URL *url.URL) error { return request(URL, http.MethodDelete, nil, nil) } 22 | 23 | // WindowList goes a GET and returns a list of Windows from the response body. 24 | // The URL is expected to point to the server's windows list. 25 | func WindowList(URL *url.URL) ([]Window, error) { 26 | var list []Window 27 | if err := request(URL, http.MethodGet, nil, &list); err != nil { 28 | return nil, err 29 | } 30 | return list, nil 31 | } 32 | 33 | // NewWindow PUTs a NewWindowRequest 34 | // with Width and Height set to size.X and size.Y respectively, 35 | // and returns a Window from the response body. 36 | // The URL is expected to point at the server's windows list. 37 | func NewWindow(URL *url.URL, size image.Point) (Window, error) { 38 | req := NewWindowRequest{ 39 | Width: size.X, 40 | Height: size.Y, 41 | } 42 | var win Window 43 | if err := request(URL, http.MethodPut, req, &win); err != nil { 44 | return Window{}, err 45 | } 46 | return win, nil 47 | } 48 | 49 | // NewColumn PUTs a NewColumnRequest. 50 | // If the response status code is NotFound, ErrNotFound is returned. 51 | // The URL is expected to point to a window's columns list. 52 | func NewColumn(URL *url.URL, x float64) error { 53 | req := NewColumnRequest{X: x} 54 | return request(URL, http.MethodPut, req, nil) 55 | } 56 | 57 | // NewSheet does a PUT and areturns a Sheet from the response body. 58 | // If the response status code is NotFound, ErrNotFound is returned. 59 | // The URL is expected to point to a window's sheets list. 60 | func NewSheet(uiURL *url.URL, editorOrBufferURL *url.URL) (Sheet, error) { 61 | req := NewSheetRequest{ 62 | URL: editorOrBufferURL.String(), 63 | } 64 | var sheet Sheet 65 | if err := request(uiURL, http.MethodPut, req, &sheet); err != nil { 66 | return Sheet{}, err 67 | } 68 | return sheet, nil 69 | } 70 | 71 | // SheetList goes a GET and returns a list of Sheets from the response body. 72 | // The URL is expected to point to the server's sheets list. 73 | func SheetList(URL *url.URL) ([]Sheet, error) { 74 | var list []Sheet 75 | if err := request(URL, http.MethodGet, nil, &list); err != nil { 76 | return nil, err 77 | } 78 | return list, nil 79 | } 80 | 81 | // Request makes an HTTP request to the given URL. 82 | // req is the body of the request. 83 | // If it implements io.Reader it is used directly as the body, 84 | // otherwise it is JSON-encoded. 85 | func request(url *url.URL, method string, req interface{}, resp interface{}) error { 86 | var body io.Reader 87 | if req != nil { 88 | if r, ok := req.(io.Reader); ok { 89 | body = r 90 | } else { 91 | d, err := json.Marshal(req) 92 | if err != nil { 93 | return err 94 | } 95 | body = bytes.NewReader(d) 96 | } 97 | } 98 | httpReq, err := http.NewRequest(method, url.String(), body) 99 | if err != nil { 100 | return err 101 | } 102 | httpResp, err := http.DefaultClient.Do(httpReq) 103 | if err != nil { 104 | return err 105 | } 106 | defer httpResp.Body.Close() 107 | if httpResp.StatusCode != http.StatusOK { 108 | return responseError(httpResp) 109 | } 110 | if resp == nil { 111 | return nil 112 | } 113 | return json.NewDecoder(httpResp.Body).Decode(resp) 114 | } 115 | 116 | func responseError(resp *http.Response) error { 117 | switch resp.StatusCode { 118 | case http.StatusNotFound: 119 | return ErrNotFound 120 | default: 121 | data, _ := ioutil.ReadAll(resp.Body) 122 | return errors.New(resp.Status + ": " + string(data)) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /edit/addr_bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015, The T Authors. 2 | 3 | // Copied from go/test/bench/go1/regexp_test.go, 4 | // which has the following notice: 5 | // 6 | // Copyright 2013 The Go Authors. All rights reserved. 7 | // Use of this source code is governed by a BSD-style 8 | // license that can be found in the LICENSE file. 9 | 10 | package edit 11 | 12 | import ( 13 | "bytes" 14 | "math/rand" 15 | "testing" 16 | ) 17 | 18 | // benchmark based on regexp/exec_test.go 19 | 20 | func makeEditor(n int) (buf *Buffer, lines int, runes int64) { 21 | rand.Seed(0) // For reproducibility. 22 | data := make([]byte, n) 23 | for i := 0; i < n; i++ { 24 | if rand.Intn(30) == 0 { 25 | lines++ 26 | data[i] = '\n' 27 | } else { 28 | data[i] = byte(rand.Intn(0x7E+1-0x20) + 0x20) 29 | } 30 | } 31 | buf = NewBuffer() 32 | if _, err := buf.Change(Span{}, bytes.NewReader(data)); err != nil { 33 | panic(err) 34 | } 35 | if err := buf.Apply(); err != nil { 36 | panic(err) 37 | } 38 | return buf, lines, int64(n) 39 | } 40 | 41 | func benchmarkRune(b *testing.B, n int) { 42 | ed, _, runes := makeEditor(n) 43 | defer ed.Close() 44 | b.ResetTimer() 45 | b.SetBytes(int64(n)) 46 | for i := 0; i < b.N; i++ { 47 | if _, err := Rune(runes).Where(ed); err != nil { 48 | b.Fatal(err.Error()) 49 | } 50 | } 51 | } 52 | 53 | func BenchmarkRunex32(b *testing.B) { benchmarkRune(b, 32<<0) } 54 | func BenchmarkRunex1K(b *testing.B) { benchmarkRune(b, 1<<10) } 55 | func BenchmarkRunex1M(b *testing.B) { benchmarkRune(b, 1<<20) } 56 | func BenchmarkRunex32M(b *testing.B) { benchmarkRune(b, 32<<20) } 57 | 58 | func benchmarkLine(b *testing.B, n int) { 59 | buf, lines, _ := makeEditor(n) 60 | defer buf.Close() 61 | if lines == 0 { 62 | b.Fatalf("too few lines: %d", lines) 63 | } 64 | b.ResetTimer() 65 | b.SetBytes(int64(n)) 66 | for i := 0; i < b.N; i++ { 67 | if _, err := Line(lines).Where(buf); err != nil { 68 | b.Fatal(err.Error()) 69 | } 70 | } 71 | } 72 | 73 | func BenchmarkLinex32(b *testing.B) { benchmarkLine(b, 32<<0) } 74 | func BenchmarkLinex1K(b *testing.B) { benchmarkLine(b, 1<<10) } 75 | func BenchmarkLinex1M(b *testing.B) { benchmarkLine(b, 1<<20) } 76 | func BenchmarkLinex32M(b *testing.B) { benchmarkLine(b, 32<<20) } 77 | 78 | func benchmarkRegexp(b *testing.B, re string, n int) { 79 | buf, _, _ := makeEditor(n) 80 | defer buf.Close() 81 | b.ResetTimer() 82 | b.SetBytes(int64(n)) 83 | for i := 0; i < b.N; i++ { 84 | switch _, err := Regexp(re).Where(buf); { 85 | case err == nil: 86 | panic("unexpected match") 87 | case err != ErrNoMatch: 88 | panic(err) 89 | } 90 | } 91 | } 92 | 93 | const ( 94 | easy0 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ$" 95 | easy1 = "A[AB]B[BC]C[CD]D[DE]E[EF]F[FG]G[GH]H[HI]I[IJ]J$" 96 | medium = "[XYZ]ABCDEFGHIJKLMNOPQRSTUVWXYZ$" 97 | hard = "[ -~]*ABCDEFGHIJKLMNOPQRSTUVWXYZ$" 98 | ) 99 | 100 | func BenchmarkRegexpEasy0x32(b *testing.B) { benchmarkRegexp(b, easy0, 32<<0) } 101 | func BenchmarkRegexpEasy0x1K(b *testing.B) { benchmarkRegexp(b, easy0, 1<<10) } 102 | func BenchmarkRegexpEasy0x1M(b *testing.B) { benchmarkRegexp(b, easy0, 1<<20) } 103 | func BenchmarkRegexpEasy0x32M(b *testing.B) { benchmarkRegexp(b, easy0, 32<<20) } 104 | func BenchmarkRegexpEasy1x32(b *testing.B) { benchmarkRegexp(b, easy1, 32<<0) } 105 | func BenchmarkRegexpEasy1x1K(b *testing.B) { benchmarkRegexp(b, easy1, 1<<10) } 106 | func BenchmarkRegexpEasy1x1M(b *testing.B) { benchmarkRegexp(b, easy1, 1<<20) } 107 | func BenchmarkRegexpEasy1x32M(b *testing.B) { benchmarkRegexp(b, easy1, 32<<20) } 108 | func BenchmarkRegexpMediumx32(b *testing.B) { benchmarkRegexp(b, medium, 1<<0) } 109 | func BenchmarkRegexpMediumx1K(b *testing.B) { benchmarkRegexp(b, medium, 1<<10) } 110 | func BenchmarkRegexpMediumx1M(b *testing.B) { benchmarkRegexp(b, medium, 1<<20) } 111 | func BenchmarkRegexpMediumx32M(b *testing.B) { benchmarkRegexp(b, medium, 32<<20) } 112 | func BenchmarkRegexpHardx32(b *testing.B) { benchmarkRegexp(b, hard, 32<<0) } 113 | func BenchmarkRegexpHardx1K(b *testing.B) { benchmarkRegexp(b, hard, 1<<10) } 114 | func BenchmarkRegexpHardx1M(b *testing.B) { benchmarkRegexp(b, hard, 1<<20) } 115 | func BenchmarkRegexpHardx32M(b *testing.B) { benchmarkRegexp(b, hard, 32<<20) } 116 | -------------------------------------------------------------------------------- /websocket/websocket_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package websocket 4 | 5 | import ( 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "path" 11 | "strconv" 12 | "testing" 13 | ) 14 | 15 | func TestDialNotFound(t *testing.T) { 16 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | http.NotFound(w, r) 18 | }) 19 | s := httptest.NewServer(handler) 20 | defer s.Close() 21 | 22 | URL, err := url.Parse(s.URL) 23 | if err != nil { 24 | t.Fatalf("url.Parse(%q)=_,%v", s.URL, err) 25 | } 26 | URL.Scheme = "ws" 27 | URL.Path = path.Join("/", "notfound") 28 | 29 | conn, err := Dial(URL) 30 | if hsErr, ok := err.(HandshakeError); !ok || hsErr.StatusCode != http.StatusNotFound { 31 | t.Errorf("Dial(%s)=_,%v, want HandshakeError{StatusCode: 400}", URL, err) 32 | conn.Close() 33 | } 34 | } 35 | 36 | func TestEcho(t *testing.T) { 37 | const N = 10 38 | 39 | handler := http.HandlerFunc(echoUntilClose(t)) 40 | s := httptest.NewServer(handler) 41 | defer s.Close() 42 | 43 | URL, err := url.Parse(s.URL) 44 | if err != nil { 45 | t.Fatalf("url.Parse(%q)=_,%v", s.URL, err) 46 | } 47 | URL.Scheme = "ws" 48 | conn, err := Dial(URL) 49 | if err != nil { 50 | t.Fatalf("Dial(%s)=_,%v", URL, err) 51 | } 52 | 53 | for i := 0; i < N; i++ { 54 | sent := strconv.Itoa(i) 55 | if err := conn.Send(sent); err != nil { 56 | t.Fatalf("client conn.Send(%q)=%v", sent, err) 57 | } 58 | var recvd string 59 | if err := conn.Recv(&recvd); err != nil { 60 | t.Fatalf("client conn.Recv(&recvd)=%v", err) 61 | } 62 | if recvd != sent { 63 | t.Errorf("recvd=%q, want %q", recvd, sent) 64 | } 65 | } 66 | if err := conn.Close(); err != nil { 67 | t.Errorf("client conn.Close()=%v", err) 68 | } 69 | } 70 | 71 | func TestRecvOnClosedConn(t *testing.T) { 72 | handler := http.HandlerFunc(recvUntilClose(t)) 73 | s := httptest.NewServer(handler) 74 | defer s.Close() 75 | 76 | URL, err := url.Parse(s.URL) 77 | if err != nil { 78 | t.Fatalf("url.Parse(%q)=_,%v", s.URL, err) 79 | } 80 | URL.Scheme = "ws" 81 | conn, err := Dial(URL) 82 | if err != nil { 83 | t.Fatalf("Dial(%s)=_,%v", URL, err) 84 | } 85 | if err := conn.Close(); err != nil { 86 | t.Fatalf("client conn.Close()=%v", err) 87 | } 88 | 89 | for i := 0; i < 3; i++ { 90 | if err := conn.Recv(nil); err != io.EOF { 91 | t.Errorf("client %d conv.Recv(nil)=%v, want %v", i, err, io.EOF) 92 | } 93 | } 94 | } 95 | 96 | func TestRecvNill(t *testing.T) { 97 | handler := http.HandlerFunc(echoUntilClose(t)) 98 | s := httptest.NewServer(handler) 99 | defer s.Close() 100 | 101 | URL, err := url.Parse(s.URL) 102 | if err != nil { 103 | t.Fatalf("url.Parse(%q)=_,%v", s.URL, err) 104 | } 105 | URL.Scheme = "ws" 106 | conn, err := Dial(URL) 107 | if err != nil { 108 | t.Fatalf("Dial(%s)=_,%v", URL, err) 109 | } 110 | if err := conn.Send("abc"); err != nil { 111 | t.Fatalf("conn.Send(\"abc\")=%v", err) 112 | } 113 | if err := conn.Recv(nil); err != nil { 114 | t.Errorf("conn.Recv(nill)=%v, want nil", err) 115 | } 116 | if err := conn.Close(); err != nil { 117 | t.Fatalf("client conn.Close()=%v", err) 118 | } 119 | } 120 | 121 | func echoUntilClose(t *testing.T) http.HandlerFunc { 122 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 123 | conn, err := Upgrade(w, r) 124 | if err != nil { 125 | t.Fatalf("Upgrade(w, r)=%v", err) 126 | } 127 | for { 128 | var s string 129 | switch err := conn.Recv(&s); { 130 | case err == io.EOF: 131 | if err := conn.Close(); err != nil { 132 | t.Errorf("server conn.Close()=%v", err) 133 | } 134 | return 135 | case err != nil: 136 | t.Fatalf("server conn.Recv(&s)=%v", err) 137 | default: 138 | if err := conn.Send(s); err != nil { 139 | t.Fatalf("server conn.Send(%q)=%v", s, err) 140 | } 141 | } 142 | } 143 | }) 144 | } 145 | 146 | func recvUntilClose(t *testing.T) http.HandlerFunc { 147 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 148 | conn, err := Upgrade(w, r) 149 | if err != nil { 150 | t.Fatalf("Upgrade(w, r)=%v", err) 151 | } 152 | for { 153 | switch err := conn.Recv(nil); { 154 | case err == nil: 155 | continue 156 | case err != io.EOF: 157 | t.Errorf("server conn.Recv(nill)=%v", err) 158 | } 159 | if err := conn.Close(); err != nil { 160 | t.Errorf("server conn.Close()=%v", err) 161 | } 162 | } 163 | }) 164 | } 165 | -------------------------------------------------------------------------------- /edit/edit_example_test.go: -------------------------------------------------------------------------------- 1 | package edit 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func ExampleAddress() { 11 | buf := NewBuffer() 12 | defer buf.Close() 13 | if err := Append(All, "Hello, 世界!").Do(buf, ioutil.Discard); err != nil { 14 | panic(err) 15 | } 16 | 17 | // The Addr function parses an address from a []rune. 18 | // It is intended to be called with runes input by a UI. 19 | // This wrapper makes it a bit more friendly for our example. 20 | parseAddr := func(s string) Address { 21 | a, err := Addr(strings.NewReader(s)) 22 | if err != nil { 23 | panic(err) 24 | } 25 | return a 26 | } 27 | 28 | // Create various addresses. 29 | // Addresses can be created in two ways: 30 | // 1) With the T address language, parsing them with Addr. 31 | // 2) With functions. 32 | addrs := []Address{ 33 | // 0,$ is how T specifies the address of the entire buffer. 34 | parseAddr("0,$"), 35 | // , is short-hand for 0,$ 36 | parseAddr(","), 37 | // The address can also be constructed directly. 38 | Rune(0).To(End), 39 | // All is a convenient variable for the address of the entire buffer. 40 | All, 41 | 42 | // Regular expressions. 43 | parseAddr("/Hello/"), 44 | Regexp("Hello"), 45 | // A regular expression, searching in reverse. 46 | End.Minus(Regexp("Hello")), 47 | 48 | // Line addresses. 49 | parseAddr("1"), 50 | Line(1), 51 | 52 | // Range addresses. 53 | parseAddr("#1,#5"), 54 | Rune(1).To(Rune(5)), 55 | 56 | // Addresses relative to other addresses. 57 | parseAddr("#0+/l/,#5"), 58 | Rune(0).Plus(Regexp("l")).To(Rune(5)), 59 | parseAddr("$-/l/,#5"), 60 | End.Minus(Regexp("l")).To(Rune(5)), 61 | } 62 | 63 | // Print the contents of the editor at each address to os.Stdout. 64 | for _, a := range addrs { 65 | s, err := a.Where(buf) 66 | if err != nil { 67 | panic(err) 68 | } 69 | if _, err := io.Copy(os.Stdout, buf.Reader(s)); err != nil { 70 | panic(err) 71 | } 72 | os.Stdout.WriteString("\n") 73 | } 74 | 75 | // Output: 76 | // Hello, 世界! 77 | // Hello, 世界! 78 | // Hello, 世界! 79 | // Hello, 世界! 80 | // Hello 81 | // Hello 82 | // Hello 83 | // Hello, 世界! 84 | // Hello, 世界! 85 | // ello 86 | // ello 87 | // llo 88 | // llo 89 | // lo 90 | // lo 91 | } 92 | 93 | func ExampleEdit() { 94 | buf := NewBuffer() 95 | defer buf.Close() 96 | if err := Append(All, "Hello, World!\n").Do(buf, ioutil.Discard); err != nil { 97 | panic(err) 98 | } 99 | 100 | // The Ed function parses an Edit from a []rune. 101 | // It is intended to be called with runes input by a UI. 102 | // This wrapper makes it a bit more friendly for our example. 103 | parseEd := func(s string) Edit { 104 | e, err := Ed(strings.NewReader(s)) 105 | if err != nil { 106 | panic(err) 107 | } 108 | return e 109 | } 110 | 111 | // Create various Edits. 112 | // Edits can be created in two ways: 113 | // 1) With the T command language, parsing them with Ed. 114 | // 2) With functions. 115 | edits := []Edit{ 116 | // p prints the contents at the address preceeding it. 117 | parseEd("0,$p"), 118 | // Here is the same Edit built with a funciton. 119 | Print(All), 120 | // This prints a different address. 121 | Print(Regexp(",").Plus(Rune(1)).To(End)), 122 | 123 | // c changes the buffer at a given address preceeding it. 124 | // After this change, the buffer will contain: "Hello, 世界!\n" 125 | parseEd("/World/c/世界"), 126 | parseEd(",p"), 127 | 128 | // Or you can do it with functions. 129 | // After this change, the buffer will contain: "Hello, World!\n" again. 130 | Change(Regexp("世界"), "World"), 131 | Print(All), 132 | 133 | // There is infinite Undo… 134 | parseEd("u"), 135 | Undo(1), 136 | 137 | // … and infinite Redo. 138 | parseEd("r"), 139 | Redo(1), 140 | Print(All), 141 | 142 | // You can also edit with regexp substitution. 143 | Change(All, "...===...\n"), 144 | Sub(All, "(=+)", "---${1}---"), 145 | Print(All), 146 | parseEd(`,s/[.]/_/g`), 147 | Print(All), 148 | 149 | // And various other things… 150 | } 151 | 152 | // Perform the Edits. 153 | // Printed output is written to os.Stdout. 154 | for _, e := range edits { 155 | if err := e.Do(buf, os.Stdout); err != nil { 156 | panic(err) 157 | } 158 | } 159 | 160 | // Output: 161 | // Hello, World! 162 | // Hello, World! 163 | // World! 164 | // Hello, 世界! 165 | // Hello, World! 166 | // Hello, World! 167 | // ...---===---... 168 | // ___---===---___ 169 | } 170 | -------------------------------------------------------------------------------- /edit/text.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package edit 4 | 5 | import ( 6 | "errors" 7 | "io" 8 | ) 9 | 10 | var ( 11 | // ErrInvalidArgument indicates an invalid, out-of-range Span. 12 | ErrInvalidArgument = errors.New("invalid argument") 13 | 14 | // ErrOutOfSequence indicates that a change modifies text 15 | // overlapping or preceeding the previous, staged change. 16 | ErrOutOfSequence = errors.New("out of sequence") 17 | ) 18 | 19 | // A Text provides a read-only view of a sequence of text. 20 | // 21 | // Strings of the text are identified by Spans. 22 | // The unit of measurement for a Span is unspecified; 23 | // it is determined by the implementation 24 | // using the Size method and the width return of RuneReader.RuneRead. 25 | type Text interface { 26 | // Size returns the size of the Text. 27 | Size() int64 28 | 29 | // Mark returns the Span of a mark. 30 | // If the range was never set, Mark returns Span{}. 31 | Mark(rune) Span 32 | 33 | // RuneReader returns a RuneReader that reads runes from the given Span. 34 | // 35 | // If the Size of the Span is negative, the reader returns runes in reverse. 36 | // 37 | // If either endpoint of the Span is negative or greater than the Size of the Text, 38 | // an ErrInvalidArgument is retured by the RuneReader.ReadRune method. 39 | RuneReader(Span) io.RuneReader 40 | 41 | // Reader returns a Reader that reads the Span as bytes. 42 | // 43 | // An ErrInvalidArgument error is returned by the Reader.Read method if 44 | // either endpoint of the Span is negative or greater than the Size of the Text, 45 | // or if the Size of the Span is negative. 46 | Reader(Span) io.Reader 47 | } 48 | 49 | // An Editor provides a read-write view of a sequence of text. 50 | // 51 | // An Editor changes the Text using a two-step procedure. 52 | // 53 | // The first step is to stage a batch of changes 54 | // using repeated calls to the Change method. 55 | // The Change method does not modify the Text, 56 | // but logs the desired change to a staging buffer. 57 | // 58 | // The second step is to apply the staged changes 59 | // by calling the Apply method. 60 | // The Apply method applies the changes to the Text 61 | // in the order that they were added to the staging log. 62 | // 63 | // An Editor also has an Undo stack and a Redo stack. 64 | // The stacks hold batches of changes, 65 | // providing support for unlimited undoing and redoing 66 | // of changes made by calls to Apply, Undo, and Redo. 67 | type Editor interface { 68 | Text 69 | 70 | // SetMark sets the Span of a mark. 71 | // 72 | // ErrInvalidArgument is returned 73 | // if either endpoint of the Span is negative 74 | // or greater than the Size of the Text. 75 | SetMark(rune, Span) error 76 | 77 | // Change stages a change that modifies a Span of text 78 | // to contain the data from a Reader, 79 | // to be applied on the next call to Apply, 80 | // and returns the size of text read from the Reader. 81 | // This method does not modify the Text. 82 | // 83 | // It is an error if a change modifies text 84 | // overlapping or preceding a previously staged, unapplied change. 85 | // In such a case, ErrOutOfSequence is returned. 86 | // 87 | // If an error is returned, previously staged changes are canceled. 88 | // They will not be performed on the next call to Apply. 89 | Change(Span, io.Reader) (int64, error) 90 | 91 | // Apply applies all changes since the previous call to Apply, 92 | // updates all marks to reflect the changes, 93 | // logs the applied changes to the Undo stack, 94 | // and clears the Redo stack. 95 | Apply() error 96 | 97 | // Undo undoes the changes at the top of the Undo stack. 98 | // It updates all marks to reflect the changes, 99 | // and logs the undone changes to the Redo stack. 100 | Undo() error 101 | 102 | // Redo redoes the changes at the top of the Redo stack. 103 | // It updates all marks to reflect the changes, 104 | // and logs the redone changes to the Undo stack. 105 | Redo() error 106 | } 107 | 108 | // A Span identifies a string within a Text. 109 | type Span [2]int64 110 | 111 | // Size returns the size of the Span. 112 | func (s Span) Size() int64 { return s[1] - s[0] } 113 | 114 | // Update updates s to account for t changing to size n. 115 | func (s Span) Update(t Span, n int64) Span { 116 | // Clip, unless t is entirely within s. 117 | if s[0] >= t[0] || t[1] > s[1] { 118 | if t.Contains(s[0]) { 119 | s[0] = t[1] 120 | } 121 | if t.Contains(s[1] - 1) { 122 | s[1] = t[0] 123 | } 124 | if s[0] > s[1] { 125 | s[1] = s[0] 126 | } 127 | } 128 | // Move. 129 | d := n - t.Size() 130 | if s[1] >= t[1] { 131 | s[1] += d 132 | } 133 | if s[0] >= t[1] { 134 | s[0] += d 135 | } 136 | return s 137 | } 138 | 139 | // Contains returns whether a location is within the Span. 140 | func (s Span) Contains(l int64) bool { return s[0] <= l && l < s[1] } 141 | -------------------------------------------------------------------------------- /websocket/websocket.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | // Package websocket provides a wrapper for github.com/gorilla/websocket. 4 | // The wrapper has limited features; the point is ease of use for some common cases. 5 | // It does NOT check the request Origin header. 6 | // All of its methods are safe for concurrent use. 7 | // It automatically applies a send timeout. 8 | // It transparently handles the closing handshake. 9 | package websocket 10 | 11 | import ( 12 | "encoding/json" 13 | "io" 14 | "net/http" 15 | "net/url" 16 | "sync" 17 | "time" 18 | 19 | "github.com/gorilla/websocket" 20 | ) 21 | 22 | const ( 23 | // SendTimeout is the amount of time to wait on a Send before giving up. 24 | SendTimeout = 5 * time.Second 25 | 26 | // CloseRecvTimeout is the amount of time waiting during Close 27 | // for the remote peer to send a Close message 28 | // before shutting down the connection. 29 | CloseRecvTimeout = 5 * time.Second 30 | 31 | // HandshakeTimeout is the amount of time to wait 32 | // for the connection handshake to complete. 33 | HandshakeTimeout = 5 * time.Second 34 | ) 35 | 36 | // ErrCloseSent is returned by Send if sending to a connection that is closing. 37 | var ErrCloseSent = websocket.ErrCloseSent 38 | 39 | // A HandshakeError is returned if Dial fails the handshake. 40 | type HandshakeError struct { 41 | // Status is the string representation of the HTTP response status code. 42 | Status string 43 | // StatusCode is the numeric HTTP response status code. 44 | StatusCode int 45 | } 46 | 47 | func (err HandshakeError) Error() string { return err.Status } 48 | 49 | var upgrader = websocket.Upgrader{ 50 | HandshakeTimeout: HandshakeTimeout, 51 | CheckOrigin: func(*http.Request) bool { return true }, 52 | } 53 | 54 | // A Conn is a websocket connection. 55 | type Conn struct { 56 | conn *websocket.Conn 57 | send chan sendReq 58 | recv chan recvMsg 59 | sendCloseOnce sync.Once 60 | sendCloseError error 61 | } 62 | 63 | // Dial dials a websocket and returns a new Conn. 64 | // 65 | // If the handshake fails, a HandshakeError is returned. 66 | func Dial(URL *url.URL) (*Conn, error) { 67 | hdr := make(http.Header) 68 | conn, resp, err := websocket.DefaultDialer.Dial(URL.String(), hdr) 69 | if err == websocket.ErrBadHandshake && resp.StatusCode != http.StatusOK { 70 | return nil, HandshakeError{Status: resp.Status, StatusCode: resp.StatusCode} 71 | } 72 | if err != nil { 73 | return nil, err 74 | } 75 | return newConn(conn), nil 76 | } 77 | 78 | // Upgrade upgrades an HTTP handler and returns an new *Conn. 79 | func Upgrade(w http.ResponseWriter, req *http.Request) (*Conn, error) { 80 | conn, err := upgrader.Upgrade(w, req, nil) 81 | if err != nil { 82 | return nil, err 83 | } 84 | return newConn(conn), nil 85 | } 86 | 87 | func newConn(conn *websocket.Conn) *Conn { 88 | c := &Conn{ 89 | conn: conn, 90 | send: make(chan sendReq, 10), 91 | recv: make(chan recvMsg, 10), 92 | } 93 | go c.goSend() 94 | go c.goRecv() 95 | return c 96 | } 97 | 98 | // Close closes the websocket connection, 99 | // unblocking any blocked calls to Recv or Send, 100 | // and blocks until the closing handshake completes 101 | // or CloseRecvTimeout timeout expires. 102 | // 103 | // Close should not be called more than once. 104 | func (c *Conn) Close() error { 105 | close(c.send) 106 | 107 | err := c.sendClose() 108 | timer := time.NewTimer(CloseRecvTimeout) 109 | if err != nil { 110 | timer.Stop() 111 | c.conn.Close() 112 | } 113 | 114 | for { 115 | select { 116 | case _, ok := <-c.recv: 117 | if !ok { 118 | if timer.Stop() { 119 | err = c.conn.Close() 120 | } 121 | return err 122 | } 123 | case <-timer.C: 124 | err = c.conn.Close() 125 | } 126 | } 127 | } 128 | 129 | // Send sends a JSON-encoded message. 130 | // 131 | // Send must not be called on a closed connection. 132 | func (c *Conn) Send(msg interface{}) error { 133 | result := make(chan error) 134 | c.send <- sendReq{msg: msg, result: result} 135 | return <-result 136 | } 137 | 138 | type sendReq struct { 139 | msg interface{} 140 | result chan<- error 141 | } 142 | 143 | func (c *Conn) goSend() { 144 | for req := range c.send { 145 | dl := time.Now().Add(SendTimeout) 146 | c.conn.SetWriteDeadline(dl) 147 | err := c.conn.WriteJSON(req.msg) 148 | req.result <- err 149 | } 150 | } 151 | 152 | // Recv receives the next JSON-encoded message into msg. 153 | // If msg is nill, the received message is discarded. 154 | // 155 | // This function must be called continually until Close() is called, 156 | // otherwise the connection will not respond to ping/pong messages. 157 | // 158 | // Calling Recv on a closed connection returns io.EOF. 159 | func (c *Conn) Recv(msg interface{}) error { 160 | r, ok := <-c.recv 161 | if !ok { 162 | return io.EOF 163 | } 164 | if r.err != nil { 165 | return r.err 166 | } 167 | if msg == nil { 168 | return nil 169 | } 170 | return json.Unmarshal(r.p, msg) 171 | } 172 | 173 | type recvMsg struct { 174 | p []byte 175 | err error 176 | } 177 | 178 | func (c *Conn) goRecv() { 179 | defer close(c.recv) 180 | 181 | for { 182 | messageType, p, err := c.conn.ReadMessage() 183 | if messageType == websocket.TextMessage { 184 | c.recv <- recvMsg{p: p, err: err} 185 | } 186 | if err != nil { 187 | // If this errors, a subsequent call to Close will return the error. 188 | c.sendClose() 189 | // ReadMessage cannot receive messages after it returns an error. 190 | // So give up on waiting for a Close from the peer. 191 | return 192 | } 193 | } 194 | } 195 | 196 | func (c *Conn) sendClose() error { 197 | c.sendCloseOnce.Do(func() { 198 | dl := time.Now().Add(SendTimeout) 199 | c.sendCloseError = c.conn.WriteControl(websocket.CloseMessage, nil, dl) 200 | // If we receive a Close from the peer, 201 | // gorilla will send the Close response for us. 202 | // We don't bother tracking this, so just ignore this. error. 203 | if c.sendCloseError == websocket.ErrCloseSent { 204 | c.sendCloseError = nil 205 | } 206 | }) 207 | return c.sendCloseError 208 | } 209 | -------------------------------------------------------------------------------- /editor/client.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package editor 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "errors" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "net/url" 13 | 14 | "github.com/eaburns/T/edit" 15 | "github.com/eaburns/T/websocket" 16 | ) 17 | 18 | var ( 19 | // ErrNotFound indicates that a resource is not found. 20 | ErrNotFound = errors.New("not found") 21 | 22 | // ErrRange indicates an out-of-range Address. 23 | ErrRange = errors.New("bad range") 24 | ) 25 | 26 | func request(url *url.URL, method string, body io.Reader, resp interface{}) error { 27 | httpReq, err := http.NewRequest(method, url.String(), body) 28 | if err != nil { 29 | return err 30 | } 31 | httpResp, err := http.DefaultClient.Do(httpReq) 32 | if err != nil { 33 | return err 34 | } 35 | defer httpResp.Body.Close() 36 | if httpResp.StatusCode != http.StatusOK { 37 | return responseError(httpResp) 38 | } 39 | if resp == nil { 40 | return nil 41 | } 42 | return json.NewDecoder(httpResp.Body).Decode(resp) 43 | } 44 | 45 | // Close does a DELETE. 46 | // The URL is expected to point at either a buffer path or an editor path. 47 | func Close(URL *url.URL) error { return request(URL, http.MethodDelete, nil, nil) } 48 | 49 | // BufferList does a GET and returns a list of Buffers from the response body. 50 | // The URL is expected to point at an editor server's buffers list. 51 | func BufferList(URL *url.URL) ([]Buffer, error) { 52 | var list []Buffer 53 | if err := request(URL, http.MethodGet, nil, &list); err != nil { 54 | return nil, err 55 | } 56 | return list, nil 57 | } 58 | 59 | // NewBuffer does a PUT and returns a Buffer from the response body. 60 | // The URL is expected to point at an editor server's buffers list. 61 | func NewBuffer(URL *url.URL) (Buffer, error) { 62 | var buf Buffer 63 | if err := request(URL, http.MethodPut, nil, &buf); err != nil { 64 | return Buffer{}, err 65 | } 66 | return buf, nil 67 | } 68 | 69 | // BufferInfo does a GET and returns a Buffer from the response body. 70 | // The URL is expected to point at a buffer path. 71 | func BufferInfo(URL *url.URL) (Buffer, error) { 72 | var buf Buffer 73 | if err := request(URL, http.MethodGet, nil, &buf); err != nil { 74 | return Buffer{}, err 75 | } 76 | return buf, nil 77 | } 78 | 79 | // A ChangeStream reads changes made to a buffer. 80 | // Methods on ChangeStream are safe for use by concurrent go routines. 81 | type ChangeStream struct { 82 | conn *websocket.Conn 83 | } 84 | 85 | // Close unblocks any calls to Next and closes the stream. 86 | func (s *ChangeStream) Close() error { return s.conn.Close() } 87 | 88 | // Next returns the next ChangeList from the stream. 89 | // Calling Next on a closed ChangeStream returns io.EOF. 90 | func (s *ChangeStream) Next() (ChangeList, error) { 91 | var cl ChangeList 92 | return cl, s.conn.Recv(&cl) 93 | } 94 | 95 | // Changes returns a ChangeStream that reads changes made to a buffer. 96 | // The URL is expected to point at the changes file of a buffer. 97 | // Note that the changes file is a websocket, and must use a ws scheme: 98 | // ws://host:port/buffer//changes 99 | func Changes(URL *url.URL) (*ChangeStream, error) { 100 | conn, err := websocket.Dial(URL) 101 | if err != nil { 102 | if isNotFoundError(err) { 103 | err = ErrNotFound 104 | } 105 | return nil, err 106 | } 107 | return &ChangeStream{conn: conn}, nil 108 | } 109 | 110 | func isNotFoundError(err error) bool { 111 | hsErr, ok := err.(websocket.HandshakeError) 112 | return ok && hsErr.StatusCode == http.StatusNotFound 113 | } 114 | 115 | // NewEditor does a PUT and returns an Editor from the response body. 116 | // The URL is expected to point at a buffer path. 117 | func NewEditor(URL *url.URL) (Editor, error) { 118 | var ed Editor 119 | if err := request(URL, http.MethodPut, nil, &ed); err != nil { 120 | return Editor{}, err 121 | } 122 | return ed, nil 123 | } 124 | 125 | // EditorInfo does a GET and returns an Editor from the response body. 126 | // The URL is expected to point at an editor path. 127 | func EditorInfo(URL *url.URL) (Editor, error) { 128 | var ed Editor 129 | if err := request(URL, http.MethodGet, nil, &ed); err != nil { 130 | return Editor{}, err 131 | } 132 | return ed, nil 133 | } 134 | 135 | // Reader returns an io.ReadCloser that reads the text from a given Address. 136 | // If non-nil, the returned io.ReadCloser must be closed by the caller. 137 | // If the Address is non-nil, it is set as the value of the addr URL parameter. 138 | // The URL is expected to point at an editor's text path. 139 | func Reader(URL *url.URL, addr edit.Address) (io.ReadCloser, error) { 140 | urlCopy := *URL 141 | if addr != nil { 142 | vals := make(url.Values) 143 | vals["addr"] = []string{addr.String()} 144 | urlCopy.RawQuery += "&" + vals.Encode() 145 | } 146 | 147 | httpResp, err := http.Get(urlCopy.String()) 148 | if err != nil { 149 | return nil, err 150 | } 151 | if httpResp.StatusCode != http.StatusOK { 152 | defer httpResp.Body.Close() 153 | return nil, responseError(httpResp) 154 | } 155 | return httpResp.Body, nil 156 | } 157 | 158 | // Do POSTs a sequence of edits and returns a list of the EditResults 159 | // from the response body. 160 | // The URL is expected to point at an editor path. 161 | func Do(URL *url.URL, edits ...edit.Edit) ([]EditResult, error) { 162 | var eds []editRequest 163 | for _, ed := range edits { 164 | eds = append(eds, editRequest{ed}) 165 | } 166 | body := bytes.NewBuffer(nil) 167 | if err := json.NewEncoder(body).Encode(eds); err != nil { 168 | return nil, err 169 | } 170 | var results []EditResult 171 | if err := request(URL, http.MethodPost, body, &results); err != nil { 172 | return nil, err 173 | } 174 | return results, nil 175 | } 176 | 177 | func responseError(resp *http.Response) error { 178 | switch resp.StatusCode { 179 | case http.StatusNotFound: 180 | return ErrNotFound 181 | case http.StatusRequestedRangeNotSatisfiable: 182 | return ErrRange 183 | default: 184 | data, _ := ioutil.ReadAll(resp.Body) 185 | return errors.New(resp.Status + ": " + string(data)) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /edit/runes/runes.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015, The T Authors. 2 | 3 | // Package runes provides unbounded, file-backed rune buffers 4 | // and io-package-style interfaces for reading and writing rune slices. 5 | package runes 6 | 7 | import ( 8 | "bytes" 9 | "io" 10 | "unicode/utf8" 11 | ) 12 | 13 | // MinRead is the minimum rune buffer size 14 | // passed to a Read call. 15 | const MinRead = bytes.MinRead 16 | 17 | // Reader wraps the basic Read method. 18 | // It behaves like io.Reader 19 | // but it accepts a slice of runes 20 | // instead of a slice of bytes, 21 | // and returns the number of runes read 22 | // instead of the number of bytes read. 23 | type Reader interface { 24 | Read([]rune) (int, error) 25 | } 26 | 27 | // Writer wraps the basic Write method. 28 | // It behaves like io.Writer 29 | // but it accepts a slice of runes 30 | // instead of a slice of bytes. 31 | type Writer interface { 32 | Write([]rune) (int, error) 33 | } 34 | 35 | // ReaderFrom wraps the ReadFrom method. 36 | // It reads runes from the reader 37 | // until there are no more runes to read. 38 | type ReaderFrom interface { 39 | ReadFrom(Reader) (int64, error) 40 | } 41 | 42 | type utf8Reader struct { 43 | r Reader 44 | buf *bytes.Buffer 45 | } 46 | 47 | // UTF8Reader returns a buffering io.Reader that reads UTF8 from r. 48 | func UTF8Reader(r Reader) io.Reader { 49 | return utf8Reader{r: r, buf: bytes.NewBuffer(nil)} 50 | } 51 | 52 | func (r utf8Reader) Read(p []byte) (int, error) { 53 | if err := r.fill(len(p)); err != nil { 54 | return 0, err 55 | } 56 | return r.buf.Read(p) 57 | } 58 | 59 | func (r utf8Reader) fill(min int) error { 60 | if r.buf.Len() > 0 { 61 | return nil 62 | } 63 | if min < MinRead { 64 | min = MinRead 65 | } 66 | rs := make([]rune, min) 67 | n, err := r.r.Read(rs) 68 | for i := 0; i < n; i++ { 69 | if _, err := r.buf.WriteRune(rs[i]); err != nil { 70 | return err 71 | } 72 | } 73 | if err != nil && err != io.EOF { 74 | return err 75 | } 76 | return nil 77 | } 78 | 79 | type utf8Writer struct{ w io.Writer } 80 | 81 | // UTF8Writer returns a Writer that writes UTF8 to w. 82 | func UTF8Writer(w io.Writer) Writer { return utf8Writer{w} } 83 | 84 | func (w utf8Writer) Write(p []rune) (int, error) { 85 | for i, r := range p { 86 | var e [utf8.UTFMax]byte 87 | sz := utf8.EncodeRune(e[:], r) 88 | switch n, err := w.w.Write(e[:sz]); { 89 | case n < sz: 90 | return i, err 91 | case err != nil: 92 | return i + 1, err 93 | } 94 | } 95 | return len(p), nil 96 | } 97 | 98 | type limitedReader struct { 99 | r Reader 100 | n, limit int64 101 | } 102 | 103 | // LimitReader returns a Reader that reads no more than n runes from r. 104 | func LimitReader(r Reader, n int64) Reader { return &limitedReader{r: r, limit: n} } 105 | 106 | func (r *limitedReader) Read(p []rune) (int, error) { 107 | if r.n >= r.limit { 108 | return 0, io.EOF 109 | } 110 | n := len(p) 111 | if max := r.limit - r.n; max < int64(n) { 112 | n = int(max) 113 | } 114 | m, err := r.r.Read(p[:n]) 115 | r.n += int64(m) 116 | return m, err 117 | } 118 | 119 | type byteReader struct { 120 | s []byte 121 | } 122 | 123 | // ByteReader returns a Reader that reads runes from a []byte. 124 | func ByteReader(s []byte) Reader { return &byteReader{s} } 125 | 126 | // Len returns the number of runes in the unread portion of the string. 127 | func (r *byteReader) Len() int64 { return int64(utf8.RuneCount(r.s)) } 128 | 129 | func (r *byteReader) Read(p []rune) (int, error) { 130 | for i := range p { 131 | if len(r.s) == 0 { 132 | return i, io.EOF 133 | } 134 | var w int 135 | p[i], w = utf8.DecodeRune(r.s) 136 | r.s = r.s[w:] 137 | } 138 | return len(p), nil 139 | } 140 | 141 | type stringReader struct { 142 | s string 143 | } 144 | 145 | // StringReader returns a Reader that reads runes from a string. 146 | func StringReader(s string) Reader { return &stringReader{s} } 147 | 148 | // Len returns the number of runes in the unread portion of the string. 149 | func (r *stringReader) Len() int64 { return int64(utf8.RuneCountInString(r.s)) } 150 | 151 | func (r *stringReader) Read(p []rune) (int, error) { 152 | for i := range p { 153 | if len(r.s) == 0 { 154 | return i, io.EOF 155 | } 156 | var w int 157 | p[i], w = utf8.DecodeRuneInString(r.s) 158 | r.s = r.s[w:] 159 | } 160 | return len(p), nil 161 | } 162 | 163 | type sliceReader struct { 164 | rs []rune 165 | } 166 | 167 | // SliceReader returns a Reader that reads runes from a slice. 168 | func SliceReader(rs []rune) Reader { return &sliceReader{rs} } 169 | 170 | // EmptyReader returns a Reader that is empty. 171 | // All calls to read return 0, io.EOF. 172 | func EmptyReader() Reader { return &sliceReader{} } 173 | 174 | // Len returns the number of runes in the unread portion of the slice. 175 | func (r *sliceReader) Len() int64 { return int64(len(r.rs)) } 176 | 177 | func (r *sliceReader) Read(p []rune) (int, error) { 178 | if len(r.rs) == 0 { 179 | return 0, io.EOF 180 | } 181 | n := copy(p, r.rs) 182 | r.rs = r.rs[n:] 183 | return n, nil 184 | } 185 | 186 | type runesReader struct{ r io.RuneReader } 187 | 188 | // RunesReader returns a Reader that reads from an io.RuneReader. 189 | func RunesReader(r io.RuneReader) Reader { return runesReader{r} } 190 | 191 | func (r runesReader) Read(p []rune) (int, error) { 192 | for i := range p { 193 | ru, _, err := r.r.ReadRune() 194 | if err != nil { 195 | return i, err 196 | } 197 | p[i] = ru 198 | } 199 | return len(p), nil 200 | } 201 | 202 | // ReadAll reads runes from the reader 203 | // until an error or io.EOF is encountered. 204 | // It returns all of the runes read. 205 | // On success, the error is nil, not io.EOF. 206 | func ReadAll(r Reader) ([]rune, error) { 207 | var rs []rune 208 | p := make([]rune, MinRead) 209 | for { 210 | n, err := r.Read(p) 211 | rs = append(rs, p[:n]...) 212 | if err != nil { 213 | if err == io.EOF { 214 | err = nil 215 | } 216 | return rs, err 217 | } 218 | } 219 | } 220 | 221 | // Copy copies from src into dst until either EOF is reached or an error occurs. 222 | // It returns the number of runes written or the first error encountered, if any. 223 | // 224 | // A successful Copy returns err == nil, not err == io.EOF. 225 | // 226 | // If dst implements the ReaderFrom interface, 227 | // the copy is implemented by calling dst.ReadFrom. 228 | func Copy(dst Writer, src Reader) (int64, error) { 229 | if rf, ok := dst.(ReaderFrom); ok { 230 | return rf.ReadFrom(src) 231 | } 232 | return slowCopy(dst, src) 233 | } 234 | 235 | func slowCopy(dst Writer, src Reader) (int64, error) { 236 | var tot int64 237 | var buf [MinRead]rune 238 | for { 239 | nr, rerr := src.Read(buf[:]) 240 | nw, werr := dst.Write(buf[:nr]) 241 | tot += int64(nw) 242 | switch { 243 | case rerr != nil && rerr != io.EOF: 244 | return tot, rerr 245 | case werr != nil: 246 | return tot, werr 247 | case rerr == io.EOF: 248 | return tot, nil 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /ui/text/main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | // +build ignore 4 | 5 | // Main is demo program to try out the text package. 6 | package main 7 | 8 | import ( 9 | "image" 10 | "image/color" 11 | "image/draw" 12 | "log" 13 | "runtime" 14 | "unicode/utf8" 15 | 16 | "github.com/eaburns/T/ui/text" 17 | "github.com/golang/freetype/truetype" 18 | "github.com/pkg/profile" 19 | "golang.org/x/exp/shiny/driver" 20 | "golang.org/x/exp/shiny/screen" 21 | "golang.org/x/image/font" 22 | "golang.org/x/image/font/basicfont" 23 | "golang.org/x/image/font/gofont/goregular" 24 | "golang.org/x/mobile/event/key" 25 | "golang.org/x/mobile/event/lifecycle" 26 | "golang.org/x/mobile/event/mouse" 27 | "golang.org/x/mobile/event/paint" 28 | "golang.org/x/mobile/event/size" 29 | ) 30 | 31 | const ( 32 | fontPath = "/home/eaburns/.fonts/LucidaSansRegular.ttf" 33 | ) 34 | 35 | var ( 36 | hi = &text.Style{ 37 | Face: loadFace(), 38 | BG: color.NRGBA{R: 0xB6, G: 0xDA, B: 0xFD, A: 0xFF}, 39 | FG: color.Black, 40 | } 41 | opts = text.Options{ 42 | DefaultStyle: text.Style{ 43 | Face: loadFace(), 44 | BG: color.NRGBA{R: 0xFA, G: 0xF0, B: 0xE6, A: 0xFF}, 45 | FG: color.Black, 46 | }, 47 | TabWidth: 4, 48 | Padding: 10, 49 | } 50 | ) 51 | 52 | func loadFace() font.Face { 53 | ttf, err := truetype.Parse(goregular.TTF) 54 | if err != nil { 55 | log.Printf("Failed to load default font %s", err) 56 | return basicfont.Face7x13 57 | } 58 | return truetype.NewFace(ttf, &truetype.Options{ 59 | Size: 11, // pt 60 | DPI: 96, 61 | }) 62 | } 63 | 64 | func init() { runtime.LockOSThread() } 65 | 66 | func main() { driver.Main(Main) } 67 | 68 | // Main is the logical main function, the real main function is hijacked by shiny. 69 | func Main(scr screen.Screen) { 70 | defer profile.Start(profile.MemProfile).Stop() 71 | 72 | width, height := 300, 300 73 | win, err := scr.NewWindow(&screen.NewWindowOptions{ 74 | Width: width, 75 | Height: height, 76 | }) 77 | if err != nil { 78 | panic(err) 79 | } 80 | defer win.Release() 81 | 82 | sz := image.Pt(width, height) 83 | at := image.Pt(sz.X/20, sz.Y/20) 84 | opts.Size = image.Pt(sz.X-sz.X/10, sz.Y-sz.Y/10) 85 | setter := text.NewSetter(opts) 86 | defer setter.Release() 87 | 88 | var a0, a1 int 89 | txt := resetText(setter, nil, a0, a1) 90 | defer txt.Release() 91 | 92 | var drag bool 93 | for { 94 | switch e := win.NextEvent().(type) { 95 | case lifecycle.Event: 96 | if e.To == lifecycle.StageDead { 97 | return 98 | } 99 | 100 | case key.Event: 101 | if e.Direction == key.DirRelease { 102 | continue 103 | } 104 | switch e.Code { 105 | case key.CodeDeleteForward: 106 | elPaso = elPaso[:0] 107 | a0, a1 = 0, 0 108 | case key.CodeDeleteBackspace: 109 | typeRune(&elPaso, '\b') 110 | if a0 > len(elPaso) { 111 | a0 = len(elPaso) 112 | } 113 | if a1 > len(elPaso) { 114 | a1 = len(elPaso) 115 | } 116 | case key.CodeReturnEnter: 117 | typeRune(&elPaso, '\n') 118 | case key.CodeTab: 119 | typeRune(&elPaso, '\t') 120 | default: 121 | if e.Rune < 0 { 122 | continue 123 | } 124 | typeRune(&elPaso, e.Rune) 125 | } 126 | txt = resetText(setter, txt, a0, a1) 127 | win.Send(paint.Event{}) 128 | 129 | case mouse.Event: 130 | switch e.Direction { 131 | case mouse.DirPress: 132 | click := image.Pt(int(e.X), int(e.Y)) 133 | a0 = txt.Index(click.Sub(at)) 134 | a1 = a0 135 | drag = true 136 | txt = resetText(setter, txt, a0, a1) 137 | win.Send(paint.Event{}) 138 | 139 | case mouse.DirRelease: 140 | drag = false 141 | 142 | case mouse.DirNone: 143 | if !drag { 144 | continue 145 | } 146 | a1Prev := a1 147 | click := image.Pt(int(e.X), int(e.Y)) 148 | a1 = txt.Index(click.Sub(at)) 149 | if a1 != a1Prev { 150 | txt = resetText(setter, txt, a0, a1) 151 | win.Send(paint.Event{}) 152 | } 153 | } 154 | 155 | case size.Event: 156 | sz = e.Size() 157 | at = image.Pt(sz.X/20, sz.Y/20) 158 | opts.Size = image.Pt(sz.X-sz.X/10, sz.Y-sz.Y/10) 159 | setter.Reset(opts) 160 | txt = resetText(setter, txt, a0, a1) 161 | 162 | case paint.Event: 163 | bg := image.White 164 | r := image.Rect(at.X, at.Y, at.X+opts.Size.X, at.Y+opts.Size.Y) 165 | win.Fill(image.Rect(0, 0, sz.X, r.Min.Y), bg, draw.Src) // top 166 | win.Fill(image.Rect(0, r.Max.Y, sz.X, sz.Y), bg, draw.Src) // bottom 167 | win.Fill(image.Rect(0, r.Min.Y, r.Min.X, r.Max.Y), bg, draw.Src) // left 168 | win.Fill(image.Rect(r.Min.X, r.Min.Y, sz.X, r.Max.Y), bg, draw.Src) // right 169 | txt.Draw(at, scr, win) 170 | win.Publish() 171 | } 172 | } 173 | } 174 | 175 | func resetText(setter *text.Setter, prev *text.Text, a0, a1 int) *text.Text { 176 | if prev != nil { 177 | prev.Release() 178 | } 179 | if a1 < a0 { 180 | a0, a1 = a1, a0 181 | } 182 | setter.Add(elPaso[:a0]) 183 | setter.AddStyle(hi, elPaso[a0:a1]) 184 | setter.Add(elPaso[a1:len(elPaso)]) 185 | return setter.Set() 186 | } 187 | 188 | func typeRune(txt *[]byte, r rune) { 189 | if r == '\b' { 190 | if l := len(*txt); l > 0 { 191 | *txt = (*txt)[:l-1] 192 | } 193 | } else { 194 | var bs [utf8.UTFMax]byte 195 | *txt = append(*txt, bs[:utf8.EncodeRune(bs[:], r)]...) 196 | } 197 | } 198 | 199 | var elPaso = []byte(`Out in the West Texas town of El Paso 200 | I fell in love with a Mexican girl 201 | Nighttime would find me in Rosa's cantina 202 | Music would play and Felina would whirl 203 | Blacker than night were the eyes of Felina 204 | Wicked and evil while casting a spell 205 | My love was deep for this Mexican maiden 206 | I was in love but in vain, I could tell 207 | One night a wild young cowboy came in 208 | Wild as the West Texas wind 209 | Dashing and daring, a drink he was sharing 210 | With wicked Felina, the girl that I loved 211 | So in anger I 212 | Challenged his right for the love of this maiden 213 | Down went his hand for the gun that he wore 214 | My challenge was answered in less than a heartbeat 215 | The handsome young stranger lay dead on the floor 216 | Just for a moment I stood there in silence 217 | Shocked by the foul evil deed I had done 218 | Many thoughts raced through my mind as I stood there 219 | I had but one chance and that was to run 220 | Out through the back door of Rosa's I ran 221 | Out where the horses were tied 222 | I caught a good one, it looked like it could run 223 | Up on its back and away I did ride 224 | Just as fast as I 225 | Could from the West Texas town of El Paso 226 | Out to the badlands of New Mexico 227 | Back in El Paso my life would be worthless 228 | Everything's gone in life; nothing is left 229 | It's been so long since I've seen the young maiden 230 | My love is stronger than my fear of death 231 | I saddled up and away I did go 232 | Riding alone in the dark 233 | Maybe tomorrow, a bullet may find me 234 | Tonight nothing's worse than this pain in my heart 235 | And at last here I 236 | Am on the hill overlooking El Paso 237 | I can see Rosa's cantina below 238 | My love is strong and it pushes me onward 239 | Down off the hill to Felina I go 240 | Off to my right I see five mounted cowboys 241 | Off to my left ride a dozen or more 242 | Shouting and shooting, I can't let them catch me 243 | I have to make it to Rosa's back door 244 | Something is dreadfully wrong for I feel 245 | A deep burning pain in my side 246 | Though I am trying to stay in the saddle 247 | I'm getting weary, unable to ride 248 | But my love for 249 | Felina is strong and I rise where I've fallen 250 | Though I am weary I can't stop to rest 251 | I see the white puff of smoke from the rifle 252 | I feel the bullet go deep in my chest 253 | From out of nowhere Felina has found me 254 | Kissing my cheek as she kneels by my side 255 | Cradled by two loving arms that I'll die for 256 | One little kiss and Felina, goodbye`) 257 | -------------------------------------------------------------------------------- /edit/runes/runes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015, The T Authors. 2 | 3 | package runes 4 | 5 | import ( 6 | "bytes" 7 | "io" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | var ( 14 | helloWorldTestRunes = []rune("Hello, World! αβξ") 15 | helloWorldReadTests = readTests{ 16 | {0, ""}, 17 | {5, "Hello"}, 18 | {2, ", "}, 19 | {6, "World!"}, 20 | {0, ""}, 21 | {1, " "}, 22 | {100, "αβξ"}, 23 | } 24 | ) 25 | 26 | // A Reader that is not special-cased for any fast paths. 27 | type testReader struct{ Reader } 28 | 29 | func (r testReader) Read(p []rune) (int, error) { return r.Reader.Read(p) } 30 | 31 | // A Writer that is not special-cased for any fast paths. 32 | type testWriter struct{ Writer } 33 | 34 | func (w testWriter) Write(p []rune) (int, error) { return w.Writer.Write(p) } 35 | 36 | func TestReadAll(t *testing.T) { 37 | manyRunes := make([]rune, MinRead*1.5) 38 | for i := range manyRunes { 39 | manyRunes[i] = rune(i) 40 | } 41 | tests := [][]rune{ 42 | helloWorldTestRunes, 43 | manyRunes, 44 | } 45 | for _, test := range tests { 46 | r := SliceReader(test) 47 | rs, err := ReadAll(r) 48 | if !reflect.DeepEqual(rs, test) || err != nil { 49 | t.Errorf("ReadAll(·)=%q,%v, want %q,", string(rs), err, string(test)) 50 | } 51 | } 52 | } 53 | 54 | func TestSliceReader(t *testing.T) { 55 | r := SliceReader(helloWorldTestRunes) 56 | helloWorldReadTests.run(t, r) 57 | } 58 | 59 | func TestByteReader(t *testing.T) { 60 | r := ByteReader([]byte(string(helloWorldTestRunes))) 61 | helloWorldReadTests.run(t, r) 62 | } 63 | 64 | func TestStringReader(t *testing.T) { 65 | r := StringReader(string(helloWorldTestRunes)) 66 | helloWorldReadTests.run(t, r) 67 | } 68 | 69 | func TestRunesReader(t *testing.T) { 70 | r := RunesReader(strings.NewReader(string(helloWorldTestRunes))) 71 | helloWorldReadTests.run(t, r) 72 | } 73 | 74 | // TestLimitedReaderBigReader tests the LimitedReader 75 | // where the underlying reader is bigger than the limit. 76 | func TestLimitedReaderBigReader(t *testing.T) { 77 | left := int64(len(helloWorldTestRunes)) 78 | bigRunes := make([]rune, left*10) 79 | copy(bigRunes, helloWorldTestRunes) 80 | r := LimitReader(SliceReader(bigRunes), left) 81 | helloWorldReadTests.run(t, r) 82 | } 83 | 84 | // TestLimitedReaderSmallReader tests the LimitedReader 85 | // where the underlying reader is smaller than the limit. 86 | func TestLimitedReaderSmallReader(t *testing.T) { 87 | // Chop off the last 3 runes, 88 | // and the last readTest element. 89 | rs := helloWorldTestRunes[:len(helloWorldTestRunes)-3] 90 | tests := helloWorldReadTests[:len(helloWorldReadTests)-1] 91 | 92 | left := int64(len(helloWorldTestRunes)) 93 | r := LimitReader(SliceReader(rs), left) 94 | tests.run(t, r) 95 | } 96 | 97 | type readTests []struct { 98 | n int 99 | want string 100 | } 101 | 102 | func (tests readTests) run(t *testing.T, r Reader) { 103 | for _, test := range tests { 104 | w := []rune(test.want) 105 | p := make([]rune, test.n) 106 | m, err := r.Read(p) 107 | if m != len(w) || !reflect.DeepEqual(p[:m], w) || (err != nil && err != io.EOF) { 108 | t.Errorf("Read(len=%d)=%d,%v; %q want %d,; %q", 109 | test.n, m, err, string(p[:m]), len(w), test.want) 110 | return 111 | } 112 | } 113 | n, err := r.Read(make([]rune, 1)) 114 | if n != 0 || err != io.EOF { 115 | t.Errorf("Read(len=1)=%d,%v, want 0,io.EOF", n, err) 116 | } 117 | } 118 | 119 | func TestUTF8Reader(t *testing.T) { 120 | type want struct { 121 | n int 122 | want string 123 | } 124 | tests := []struct { 125 | runes string 126 | reads []want 127 | }{ 128 | {"", []want{}}, 129 | {"abc", []want{{1, "a"}, {1, "b"}, {1, "c"}}}, 130 | {"abc", []want{{1, "a"}, {2, "bc"}}}, 131 | {"abc", []want{{3, "abc"}}}, 132 | {"abc", []want{{2, "ab"}, {1, "c"}}}, 133 | {"abc", []want{{4, "abc"}}}, 134 | {"abc", []want{{1024, "abc"}}}, 135 | {"abc", []want{{0, ""}, {1024, "abc"}}}, 136 | {"αβξ", []want{{2, "α"}, {2, "β"}, {2, "ξ"}}}, 137 | {"αβξ", []want{ 138 | {2, "α"}, 139 | {2, "β"}, 140 | {1, "\xce"}, {1, "\xbe"}, // ξ 141 | }}, 142 | {"αβξ", []want{ 143 | {2, "α"}, 144 | {1, "\xce"}, {1, "\xb2"}, // β 145 | {1, "\xce"}, {1, "\xbe"}, // ξ 146 | }}, 147 | {"αβξ", []want{ 148 | {1, "\xce"}, {1, "\xb1"}, // α 149 | {1, "\xce"}, {1, "\xb2"}, // β 150 | {1, "\xce"}, {1, "\xbe"}, // ξ 151 | }}, 152 | {"αβξ", []want{ 153 | {4, "αβ"}, 154 | {1, "\xce"}, {1, "\xbe"}, // ξ 155 | }}, 156 | {"αβξ", []want{ 157 | {3, "α\xce"}, {1, "\xb2"}, 158 | {1, "\xce"}, {1, "\xbe"}, // ξ 159 | }}, 160 | {"αβξ", []want{ 161 | {3, "α\xce"}, {2, "\xb2\xce"}, {1, "\xbe"}, 162 | }}, 163 | {"αβξ", []want{ 164 | {3, "α\xce"}, {3, "\xb2ξ"}, 165 | }}, 166 | } 167 | for _, test := range tests { 168 | r := UTF8Reader(StringReader(test.runes)) 169 | for _, read := range test.reads { 170 | p := make([]byte, read.n) 171 | n, err := r.Read(p) 172 | if got := string(p[:n]); err != nil || got != read.want { 173 | t.Errorf("r.Read({len=%d})=%d,%v,p=%q, want %d,nil,p=%q", 174 | len(p), n, err, got, len(read.want), read.want) 175 | break 176 | } 177 | } 178 | n, err := r.Read(make([]byte, 1)) 179 | if err != io.EOF { 180 | t.Errorf("r.Read({len=1})=%d,%v, want 0,io.EOF", n, err) 181 | } 182 | } 183 | } 184 | 185 | func TestUTF8Writer(t *testing.T) { 186 | tests := []struct { 187 | writes []string 188 | want string 189 | }{ 190 | {[]string{""}, ""}, 191 | {[]string{"Hello,", "", " ", "", "World!"}, "Hello, World!"}, 192 | {[]string{"Hello", ",", " ", "World!"}, "Hello, World!"}, 193 | {[]string{"Hello", ",", " ", "世界!"}, "Hello, 世界!"}, 194 | } 195 | for _, test := range tests { 196 | b := bytes.NewBuffer(nil) 197 | w := UTF8Writer(b) 198 | for _, write := range test.writes { 199 | rs := []rune(write) 200 | n, err := w.Write(rs) 201 | if n != len(rs) || err != nil { 202 | t.Errorf("w.Write(%q)=%d,%v, want %d,nil", test.writes, n, err, len(rs)) 203 | } 204 | } 205 | if str := b.String(); str != test.want { 206 | t.Errorf("write %#v, want=%q, got %q", test.writes, str, test.want) 207 | } 208 | } 209 | } 210 | 211 | func TestCopy(t *testing.T) { 212 | for _, test := range insertTests { 213 | rs := []rune(test.add) 214 | n := int64(len(rs)) 215 | bSrc := NewBuffer(testBlockSize) 216 | defer bSrc.Close() 217 | if err := bSrc.Insert(rs, 0); err != nil { 218 | t.Fatalf("b.Insert(%q, 0)=%v, want nil", rs, err) 219 | } 220 | srcs := []func() Reader{ 221 | func() Reader { return StringReader(string(rs)) }, 222 | func() Reader { return SliceReader(rs) }, 223 | func() Reader { return bSrc.Reader(0) }, 224 | func() Reader { return LimitReader(bSrc.Reader(0), n) }, 225 | } 226 | // Fast path. 227 | for _, src := range srcs { 228 | bDst := NewBuffer(testBlockSize) 229 | test.initBuffer(t, bDst) 230 | testCopy(t, test, bDst, bDst.Writer(test.at), src()) 231 | bDst.Close() 232 | } 233 | // Slow path. 234 | for _, src := range srcs { 235 | bDst := NewBuffer(testBlockSize) 236 | test.initBuffer(t, bDst) 237 | testCopy(t, test, bDst, testWriter{bDst.Writer(test.at)}, src()) 238 | bDst.Close() 239 | } 240 | } 241 | } 242 | 243 | func TestCopySmallLimiterReader(t *testing.T) { 244 | srcRunes := []rune{'☺', '☹', '☹', '☹', '☹'} 245 | test := insertTest{ 246 | init: "abcdef", 247 | add: string(srcRunes[:1]), 248 | at: 3, 249 | want: "abc☺def", 250 | err: "", 251 | } 252 | 253 | bSrc := NewBuffer(testBlockSize) 254 | defer bSrc.Close() 255 | if err := bSrc.Insert(srcRunes, 0); err != nil { 256 | t.Fatalf("b.Insert(%q, 0)=%v, want nil", srcRunes, err) 257 | } 258 | src := LimitReader(bSrc.Reader(0), 1) 259 | 260 | bDst := NewBuffer(testBlockSize) 261 | defer bDst.Close() 262 | test.initBuffer(t, bDst) 263 | dst := bDst.Writer(test.at) 264 | 265 | testCopy(t, test, bDst, dst, src) 266 | } 267 | 268 | func testCopy(t *testing.T, test insertTest, bDst *Buffer, dst Writer, src Reader) { 269 | n, err := Copy(dst, src) 270 | add := []rune(test.add) 271 | if !errMatch(test.err, err) || (n != int64(len(add)) && test.err == "") { 272 | t.Errorf("Copy(%#v, %#v)=%v,%v, want %v,%q", 273 | dst, src, n, err, len(test.add), test.err) 274 | } 275 | if test.err != "" { 276 | return 277 | } 278 | if s := bDst.String(); s != test.want || err != nil { 279 | t.Errorf("Copy(%#v, %#v); dst.String()=%q,%v, want %q,nil", 280 | dst, src, s, err, test.want) 281 | return 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /ui/sheet.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package ui 4 | 5 | import ( 6 | "image" 7 | "image/color" 8 | "image/draw" 9 | "net/url" 10 | "sync" 11 | 12 | "github.com/eaburns/T/edit" 13 | "github.com/eaburns/T/ui/text" 14 | "golang.org/x/exp/shiny/screen" 15 | "golang.org/x/mobile/event/key" 16 | "golang.org/x/mobile/event/mouse" 17 | "golang.org/x/mobile/event/paint" 18 | ) 19 | 20 | var ( 21 | separatorColor = color.Gray16{0xAAAA} 22 | tagColors = []color.Color{ 23 | color.NRGBA{R: 0xE6, G: 0xF0, B: 0xFA, A: 0xFF}, 24 | color.NRGBA{R: 0xE6, G: 0xFA, B: 0xF0, A: 0xFF}, 25 | color.NRGBA{R: 0xF0, G: 0xE6, B: 0xFA, A: 0xFF}, 26 | color.NRGBA{R: 0xF0, G: 0xFA, B: 0xE6, A: 0xFF}, 27 | color.NRGBA{R: 0xFA, G: 0xE6, B: 0xF0, A: 0xFF}, 28 | } 29 | mu sync.Mutex 30 | nextTagColor = 0 31 | ) 32 | 33 | const sheetTagText = "Get Undo Look" 34 | 35 | // A sheet is an editable view of a buffer of text. 36 | // Each sheet contains an editable tag and body. 37 | // The tag is a, typically short, header, 38 | // beginning with the name of the sheet's file (if any) 39 | // followed by various commands to operate on the sheet. 40 | // The body contains the body text of the sheet. 41 | type sheet struct { 42 | id string 43 | col *column 44 | win *window 45 | image.Rectangle 46 | 47 | tag *textBox 48 | body *textBox 49 | sep image.Rectangle 50 | 51 | // SubFocus is either the tag, the body, or nil. 52 | subFocus handler 53 | 54 | p image.Point 55 | button mouse.Button 56 | 57 | origX int 58 | origY float64 59 | } 60 | 61 | // NewSheet creates a new sheet. 62 | // URL is either the root path to an editor server, 63 | // or the path to an open buffer of an editor server. 64 | // The body uses the given URL for its buffer (either a new one or existing). 65 | // The tag uses a new buffer created on the window server's editor. 66 | func newSheet(id string, URL *url.URL, w *window) (*sheet, error) { 67 | s := &sheet{id: id, win: w} 68 | 69 | mu.Lock() 70 | tagBG := tagColors[nextTagColor%len(tagColors)] 71 | nextTagColor++ 72 | mu.Unlock() 73 | 74 | tag, err := newTextBox(w, *w.server.editorURL, text.Style{ 75 | Face: w.face, 76 | FG: color.Black, 77 | BG: tagBG, 78 | }) 79 | if err != nil { 80 | return nil, err 81 | } 82 | tag.view.DoAsync(edit.Change(edit.All, "/sheet/"+id+" "+sheetTagText+" "), 83 | edit.Set(edit.End, '.')) 84 | s.tag = tag 85 | 86 | body, err := newTextBox(w, *URL, text.Style{ 87 | Face: w.face, 88 | FG: color.Black, 89 | BG: color.NRGBA{R: 0xFA, G: 0xF0, B: 0xE6, A: 0xFF}, 90 | }) 91 | if err != nil { 92 | tag.close() 93 | return nil, err 94 | } 95 | s.body = body 96 | 97 | return s, nil 98 | } 99 | 100 | func (s *sheet) close() { 101 | if s.win == nil { 102 | // Already closed. 103 | // This can happen if the sheet is in focus when the window is closed. 104 | // The in-focus handler is closed, and so are all columns. 105 | return 106 | } 107 | s.tag.close() 108 | s.body.close() 109 | s.win = nil 110 | } 111 | 112 | var tagFileAddr = edit.Rune(0).To(edit.Rune(0).Plus(edit.Regexp(`\S*`))) 113 | 114 | func (s *sheet) tagFileName() string { 115 | // TODO(eaburns): This is a blocking RPC, but it's called in the window handler go routine. Don't do that. Use a view to update this asynchronously. 116 | res, err := s.tag.doSync(edit.Print(tagFileAddr)) 117 | if err != nil { 118 | panic("failed to read tag: " + err.Error()) 119 | } 120 | if res[0].Error != "" { 121 | panic("failed to read tag: " + res[0].Error) 122 | } 123 | return res[0].Print 124 | } 125 | 126 | func (s *sheet) setTagFileName(str string) { 127 | s.tag.doAsync(edit.Change(tagFileAddr, str)) 128 | } 129 | 130 | func (s *sheet) updateText() { 131 | b := &s.Rectangle 132 | 133 | tagMax := b.Dy() 134 | if min := s.minHeight(); tagMax < min { 135 | tagMax = min 136 | } 137 | // TODO(eaburns): This is awful; it's too easy to forget to set topLeft, 138 | // it's too unintuitive that setSize needs to be called to update the text. 139 | s.tag.topLeft = b.Min 140 | s.tag.setSize(image.Pt(b.Dx(), tagMax)) 141 | tagHeight := s.tag.text.LinesHeight() 142 | 143 | s.body.topLeft = image.Pt(b.Min.X, b.Min.Y+tagHeight+borderWidth) 144 | s.body.setSize(image.Pt(b.Dx(), b.Dy()-tagHeight-borderWidth)) 145 | 146 | s.sep = image.Rectangle{ 147 | Min: image.Pt(b.Min.X, b.Min.Y+tagHeight), 148 | Max: image.Pt(b.Max.X, b.Min.Y+tagHeight+borderWidth), 149 | } 150 | } 151 | 152 | func (s *sheet) minHeight() int { return minHeight(s.tag.opts) } 153 | 154 | func (s *sheet) bounds() image.Rectangle { return s.Rectangle } 155 | 156 | func (s *sheet) setBounds(b image.Rectangle) { 157 | s.Rectangle = b 158 | s.updateText() 159 | } 160 | 161 | func (s *sheet) setColumn(c *column) { s.col = c } 162 | 163 | func (s *sheet) focus(p image.Point) handler { 164 | prev := s.subFocus 165 | if p.Y < s.sep.Min.Y { 166 | s.subFocus = s.tag 167 | } else if p.Y >= s.sep.Max.Y { 168 | s.subFocus = s.body 169 | } 170 | if s.subFocus != prev { 171 | if prev != nil { 172 | prev.changeFocus(s.win, false) 173 | } 174 | if s.subFocus != nil { 175 | s.subFocus.changeFocus(s.win, true) 176 | } 177 | // Always redraw on focus change. 178 | s.win.Send(paint.Event{}) 179 | } 180 | return s 181 | } 182 | 183 | func (s *sheet) draw(scr screen.Screen, win screen.Window) { 184 | s.updateText() 185 | 186 | s.tag.drawLines(scr, win) 187 | win.Fill(s.sep, separatorColor, draw.Over) 188 | s.body.draw(scr, win) 189 | } 190 | 191 | // DrawLast is called if the sheet is in focus, after the entire window has been drawn. 192 | // It draws the sheet if being dragged. 193 | func (s *sheet) drawLast(scr screen.Screen, win screen.Window) { 194 | if s.col == nil { 195 | s.draw(scr, win) 196 | drawBorder(s.bounds(), win) 197 | } 198 | } 199 | 200 | func (s *sheet) changeFocus(win *window, inFocus bool) { 201 | if s.subFocus != nil { 202 | s.subFocus.changeFocus(win, inFocus) 203 | } 204 | } 205 | 206 | func (s *sheet) tick(win *window) bool { 207 | if s.subFocus != nil { 208 | return s.subFocus.tick(win) 209 | } 210 | return false 211 | } 212 | 213 | func (s *sheet) key(w *window, event key.Event) bool { 214 | var redraw bool 215 | switch event.Code { 216 | case key.CodeLeftShift, key.CodeRightShift: 217 | if event.Direction == key.DirRelease && s.col == nil { 218 | // We were dragging, and shift was released. Put it back. 219 | if _, c := columnAt(w, s.origX); !c.addFrame(s.origY, s) { 220 | panic("can't put it back") 221 | } 222 | redraw = true 223 | } 224 | } 225 | if s.subFocus != nil && s.subFocus.key(w, event) { 226 | redraw = true 227 | } 228 | return redraw 229 | } 230 | 231 | func (s *sheet) mouse(w *window, event mouse.Event) bool { 232 | p := image.Pt(int(event.X), int(event.Y)) 233 | 234 | switch event.Direction { 235 | case mouse.DirPress: 236 | if s.button == mouse.ButtonNone { 237 | s.p = p 238 | s.button = event.Button 239 | break 240 | } 241 | // A second button was pressed while the first was held. 242 | // Sheets don't use chords; treat this as a release of the first. 243 | event.Button = s.button 244 | fallthrough 245 | 246 | case mouse.DirRelease: 247 | if event.Button != s.button { 248 | // It's not the pressed button. Ignore it. 249 | break 250 | } 251 | defer func() { s.button = mouse.ButtonNone }() 252 | 253 | if event.Modifiers != key.ModShift { 254 | break 255 | } 256 | switch s.button { 257 | case mouse.ButtonLeft: 258 | if s.col != nil { 259 | defer func() { s.col.setBounds(s.col.bounds()) }() 260 | i := frameIndex(s.col, s) 261 | if slideUp(s.col, i, s.minHeight()) { 262 | return true 263 | } 264 | return slideDown(s.col, i, s.minHeight()) 265 | } 266 | _, c := columnAt(w, p.X) 267 | yfrac := float64(s.Min.Y) / float64(c.Dy()) 268 | if c.addFrame(yfrac, s) { 269 | return true 270 | } 271 | if _, c = columnAt(w, s.origX); !c.addFrame(s.origY, s) { 272 | panic("can't put it back") 273 | } 274 | return true 275 | case mouse.ButtonMiddle: 276 | s.win.server.deleteSheet(s.id) 277 | return false 278 | } 279 | 280 | case mouse.DirNone: 281 | if s.button == mouse.ButtonNone || event.Modifiers != key.ModShift { 282 | break 283 | } 284 | switch s.button { 285 | case mouse.ButtonLeft: 286 | if s.col == nil { 287 | s.setBounds(s.Add(p.Sub(s.Min))) 288 | return true 289 | } 290 | dx := s.p.X - p.X 291 | dy := s.p.Y - p.Y 292 | if dx*dx+dy*dy > 100 { 293 | s.p = p 294 | i := frameIndex(s.col, s) 295 | if i < 0 { 296 | return false 297 | } 298 | s.origX = s.Min.X + s.Dx()/2 299 | s.origY = s.col.ys[i] 300 | s.col.removeFrame(s) 301 | return true 302 | } 303 | } 304 | } 305 | 306 | if s.subFocus != nil { 307 | return s.subFocus.mouse(w, event) 308 | } 309 | return false 310 | } 311 | -------------------------------------------------------------------------------- /edit/buffer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015, The T Authors. 2 | 3 | package edit 4 | 5 | import ( 6 | "errors" 7 | "io/ioutil" 8 | "strings" 9 | "testing" 10 | "unicode/utf8" 11 | 12 | "github.com/eaburns/T/edit/runes" 13 | ) 14 | 15 | // String returns a string containing the entire editor contents. 16 | func (buf *Buffer) String() string { 17 | data, err := ioutil.ReadAll(buf.Reader(Span{0, buf.Size()})) 18 | if err != nil { 19 | panic(err) 20 | } 21 | return string(data) 22 | } 23 | 24 | func TestBufferClose(t *testing.T) { 25 | if err := NewBuffer().Close(); err != nil { 26 | t.Fatalf("failed to close the buffer: %v", err) 27 | } 28 | } 29 | 30 | var badSpans = []Span{ 31 | Span{-1, 0}, 32 | Span{0, -1}, 33 | Span{1, 0}, 34 | Span{0, 1}, 35 | } 36 | 37 | func TestBufferBadSetMark(t *testing.T) { 38 | for _, s := range badSpans { 39 | buf := NewBuffer() 40 | defer buf.Close() 41 | if err := buf.SetMark('.', s); err != ErrInvalidArgument { 42 | t.Errorf("buf.SetMark('.', %v)=%v, want %v", s, err, ErrInvalidArgument) 43 | } 44 | } 45 | } 46 | 47 | func TestBufferBadRuneReader(t *testing.T) { 48 | for _, s := range badSpans { 49 | buf := NewBuffer() 50 | defer buf.Close() 51 | if _, _, err := buf.RuneReader(s).ReadRune(); err != ErrInvalidArgument { 52 | t.Errorf("buf.RuneReader(%v).ReadRune()=_,_,%v, want %v", s, err, ErrInvalidArgument) 53 | } 54 | } 55 | } 56 | 57 | func TestBufferBadReader(t *testing.T) { 58 | for _, s := range badSpans { 59 | buf := NewBuffer() 60 | defer buf.Close() 61 | var d [1]byte 62 | if _, err := buf.Reader(s).Read(d[:]); err != ErrInvalidArgument { 63 | t.Errorf("buf.Reader(%v).Read(·)=_,%v, want %v", s, err, ErrInvalidArgument) 64 | } 65 | } 66 | } 67 | 68 | func TestBufferChangeOutOfSequence(t *testing.T) { 69 | buf := NewBuffer() 70 | defer buf.Close() 71 | const init = "Hello, 世界" 72 | if _, err := buf.Change(Span{}, strings.NewReader(init)); err != nil { 73 | panic(err) 74 | } 75 | if err := buf.Apply(); err != nil { 76 | panic(err) 77 | } 78 | 79 | if _, err := buf.Change(Span{10, 20}, strings.NewReader("Hello")); err != nil { 80 | panic(err) 81 | } 82 | if _, err := buf.Change(Span{0, 10}, strings.NewReader("World")); err != ErrOutOfSequence { 83 | t.Errorf("buf.Change(Span{0, 10}, _)=%v, want %v", err, ErrOutOfSequence) 84 | } 85 | 86 | // Make sure previously staged changes were cleared. 87 | const str = "Goodbye" 88 | buf.Change(Span{0, buf.Size()}, strings.NewReader(str)) 89 | if err := buf.Apply(); err != nil { 90 | t.Fatalf("buf.Apply()=%v, want nil", err) 91 | } 92 | // Make sure that "Hello, World!" was never written to the bufer. 93 | all, err := ioutil.ReadAll(buf.Reader(Span{0, buf.Size()})) 94 | if string(all) != str || err != nil { 95 | t.Fatalf("ioutil.ReadAll(buf)=%q,%v, want %q,nil", string(all), err, str) 96 | } 97 | } 98 | 99 | func TestBufferChangeSize(t *testing.T) { 100 | buf := NewBuffer() 101 | defer buf.Close() 102 | 103 | const str = "Hello, 世界" 104 | want := int64(utf8.RuneCountInString(str)) 105 | got, err := buf.Change(Span{}, strings.NewReader(str)) 106 | if got != want || err != nil { 107 | t.Errorf("buf.Change(Span{}, %q)=%v,%v, want %v,nil", str, got, err, want) 108 | } 109 | } 110 | 111 | func TestLogEntryEmpty(t *testing.T) { 112 | l := newLog() 113 | defer l.close() 114 | if !logFirst(l).end() { 115 | t.Errorf("empty logFirst(l).end()=false, want true") 116 | } 117 | if !logFirst(l).prev().end() { 118 | t.Errorf("empty logFirst(l).prev().end()=false, want true") 119 | } 120 | if !logLast(l).end() { 121 | t.Errorf("empty logLast(l).end()=false, want true") 122 | } 123 | if !logLast(l).next().end() { 124 | t.Errorf("empty logLast(l).next().end()=false, want true") 125 | } 126 | } 127 | 128 | func TestLogEntryWrap(t *testing.T) { 129 | entries := []testEntry{ 130 | {seq: 0, span: Span{0, 0}, str: "Hello, World!"}, 131 | } 132 | l := initTestLog(t, entries) 133 | defer l.close() 134 | 135 | it := logFirst(l) 136 | if it != logLast(l) { 137 | t.Errorf("it != logLast") 138 | } 139 | if !it.next().end() { 140 | t.Errorf("it.next().end()=false, want true") 141 | } 142 | if it.next().next() != logFirst(l) { 143 | t.Errorf("it.next().next() != logFirst(l)") 144 | } 145 | if !it.prev().end() { 146 | t.Errorf("it.prev().end()=false, want true") 147 | } 148 | if it.prev().prev() != logLast(l) { 149 | t.Errorf("it.prev().prev() != logLast(l)") 150 | } 151 | } 152 | 153 | func TestLogEntryBackAndForth(t *testing.T) { 154 | entries := []testEntry{ 155 | {seq: 0, span: Span{0, 0}, str: "Hello, World!"}, 156 | {seq: 1, span: Span{0, 5}, str: "Foo, Bar, Baz"}, 157 | {seq: 1, span: Span{8, 10}, str: "Ms. Pepper"}, 158 | {seq: 2, span: Span{20, 50}, str: "Hello, 世界"}, 159 | } 160 | l := initTestLog(t, entries) 161 | defer l.close() 162 | 163 | // Go forward. 164 | it := logFirst(l) 165 | for i := range entries { 166 | checkEntry(t, i, entries, it) 167 | it = it.next() 168 | } 169 | if !it.end() { 170 | t.Fatalf("end: it.end()=false, want true") 171 | } 172 | 173 | // Then go back. 174 | it = it.prev() 175 | for i := len(entries) - 1; i >= 0; i-- { 176 | checkEntry(t, i, entries, it) 177 | it = it.prev() 178 | } 179 | if !it.end() { 180 | t.Fatalf("start: it.start()=false, want true") 181 | } 182 | } 183 | 184 | func TestLogAt(t *testing.T) { 185 | entries := []testEntry{ 186 | {seq: 0, span: Span{0, 0}, str: "Hello, World!"}, 187 | {seq: 1, span: Span{0, 5}, str: "Foo, Bar, Baz"}, 188 | {seq: 1, span: Span{8, 10}, str: "Ms. Pepper"}, 189 | {seq: 2, span: Span{20, 50}, str: "Hello, 世界"}, 190 | } 191 | l := initTestLog(t, entries) 192 | defer l.close() 193 | 194 | // Get the location of each entry. 195 | var locs []int64 196 | for it := logFirst(l); !it.end(); it = it.next() { 197 | locs = append(locs, it.offs) 198 | } 199 | if len(locs) != len(entries) { 200 | t.Fatalf("len(locs)=%v, want %v\n", len(locs), len(entries)) 201 | } 202 | 203 | // Test logAt. 204 | for i := range entries { 205 | checkEntry(t, i, entries, logAt(l, locs[i])) 206 | } 207 | } 208 | 209 | func TestLogEntryError(t *testing.T) { 210 | entries := []testEntry{ 211 | {seq: 0, span: Span{0, 0}, str: "Hello, World!"}, 212 | {seq: 1, span: Span{0, 5}, str: "Foo, Bar, Baz"}, 213 | {seq: 1, span: Span{8, 10}, str: "Ms. Pepper"}, 214 | {seq: 2, span: Span{20, 50}, str: "Hello, 世界"}, 215 | } 216 | l := initTestLog(t, entries) 217 | defer l.close() 218 | it := logFirst(l) 219 | it.err = errors.New("test error") 220 | if !it.next().end() { 221 | t.Errorf("!it.next().end()") 222 | } 223 | if !it.prev().end() { 224 | t.Errorf("!it.prev().end()") 225 | } 226 | } 227 | 228 | func TestLogEntryStore(t *testing.T) { 229 | entries := []testEntry{ 230 | {seq: 0, span: Span{0, 0}, str: "Hello, World!"}, 231 | {seq: 1, span: Span{0, 5}, str: "Foo, Bar, Baz"}, 232 | {seq: 1, span: Span{8, 10}, str: "Ms. Pepper"}, 233 | {seq: 2, span: Span{20, 50}, str: "Hello, 世界"}, 234 | } 235 | l := initTestLog(t, entries) 236 | defer l.close() 237 | 238 | // Modify an entry. 239 | e1 := logFirst(l).next() 240 | if err := e1.store(); err != nil { 241 | t.Fatalf("e1.store()=%v, want nil", err) 242 | } 243 | 244 | // Check that the entry is modified and others are not. 245 | e := logFirst(l) 246 | for i := range entries { 247 | checkEntry(t, i, entries, e) 248 | e = e.next() 249 | } 250 | } 251 | 252 | func TestLogEntryPop(t *testing.T) { 253 | entries := []testEntry{ 254 | {seq: 0, str: "Hello, World"}, 255 | {seq: 1, str: "☹☺"}, 256 | {seq: 1, str: "---"}, 257 | {seq: 1, str: "aeu"}, 258 | {seq: 2, str: "Testing 123"}, 259 | } 260 | l := initTestLog(t, entries) 261 | defer l.close() 262 | 263 | seq2 := logLast(l) 264 | if err := seq2.pop(); err != nil { 265 | t.Fatalf("seq2.pop()=%v, want nil", err) 266 | } 267 | // {seq: 1, str: "aeu"} 268 | checkEntry(t, len(entries)-2, entries, logLast(l)) 269 | 270 | seq1 := logLast(l).prev().prev() 271 | // {seq: 1, str: "☹☺"} 272 | checkEntry(t, 1, entries, seq1) 273 | if err := seq1.pop(); err != nil { 274 | t.Fatalf("seq1.pop()=%v, want nil", err) 275 | } 276 | // {seq: 0, str: "Hello, World"} 277 | checkEntry(t, 0, entries, logLast(l)) 278 | 279 | seq0 := logLast(l) 280 | if err := seq0.pop(); err != nil { 281 | t.Fatalf("seq0.pop()=%v, want nil", err) 282 | } 283 | if !logLast(l).end() { 284 | t.Fatal("popped last entry, log is not empty") 285 | } 286 | 287 | end := logLast(l) 288 | if err := end.pop(); err != nil { 289 | t.Fatalf("end.pop()=%v, want nil", err) 290 | } 291 | if !logLast(l).end() { 292 | t.Fatal("popped end entry, log is not empty") 293 | } 294 | } 295 | 296 | type testEntry struct { 297 | seq int32 298 | span Span 299 | str string 300 | } 301 | 302 | func checkEntry(t *testing.T, i int, entries []testEntry, e entry) { 303 | te := entries[i] 304 | if got := readSource(e.data()); got != te.str { 305 | t.Fatalf("entry %d: e.data()=%q, want %q\n", i, got, te.str) 306 | } 307 | if e.seq != te.seq { 308 | t.Fatalf("entry %d: e.h.seq=%v, want %v\n", i, e.seq, te.seq) 309 | } 310 | if e.span != te.span { 311 | t.Fatalf("entry %d: e.h.span=%v, want %v\n", i, e.span, te.span) 312 | } 313 | t.Logf("entry %d ok", i) 314 | } 315 | 316 | func initTestLog(t *testing.T, entries []testEntry) *log { 317 | l := newLog() 318 | for _, e := range entries { 319 | r := runes.StringReader(e.str) 320 | if _, err := l.append(e.seq, e.span, r); err != nil { 321 | t.Fatalf("l.append(%v, %v, %q)=%v", e.seq, e.span, e.str, err) 322 | } 323 | } 324 | return l 325 | } 326 | 327 | func readSource(src runes.Reader) string { 328 | rs, err := runes.ReadAll(src) 329 | if err != nil { 330 | panic(err) 331 | } 332 | return string(rs) 333 | } 334 | -------------------------------------------------------------------------------- /ui/server.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | // Package ui contains the T user interface. 4 | package ui 5 | 6 | import ( 7 | "encoding/json" 8 | "image" 9 | "net/http" 10 | "net/url" 11 | "path" 12 | "strconv" 13 | "sync" 14 | 15 | "github.com/gorilla/mux" 16 | "golang.org/x/exp/shiny/screen" 17 | ) 18 | 19 | // Server is a T user interface server 20 | type Server struct { 21 | screen screen.Screen 22 | editorURL *url.URL 23 | windows map[string]*window 24 | sheets map[string]*sheet 25 | nextID int 26 | done func() 27 | sync.RWMutex 28 | } 29 | 30 | // NewServer returns a new Server for the given Screen. 31 | // The editorURL must be the root URL of an editor server. 32 | // Column and sheet tags use buffers created on this editor server. 33 | func NewServer(scr screen.Screen, editorURL *url.URL) *Server { 34 | editorURL.Path = "/" 35 | return &Server{ 36 | screen: scr, 37 | editorURL: editorURL, 38 | windows: make(map[string]*window), 39 | sheets: make(map[string]*sheet), 40 | done: func() {}, 41 | } 42 | } 43 | 44 | // SetDoneHandler sets the function which is called if the last window is closed. 45 | // By default, the done handler is a no-op. 46 | func (s *Server) SetDoneHandler(f func()) { 47 | s.Lock() 48 | s.done = f 49 | s.Unlock() 50 | } 51 | 52 | // Close closes all windows. 53 | // The server should not be used after calling Close. 54 | func (s *Server) Close() error { 55 | s.Lock() 56 | defer s.Unlock() 57 | for _, w := range s.windows { 58 | w.close() 59 | } 60 | s.windows = nil 61 | s.sheets = nil 62 | return nil 63 | } 64 | 65 | // RegisterHandlers registers handlers for the following paths and methods: 66 | // 67 | // /windows is the list of opened windows. 68 | // 69 | // GET returns a Window list of the opened windows. 70 | // Returns: 71 | // • OK on success. 72 | // • Internal Server Error on internal error. 73 | // 74 | // PUT creates a new window with a single column and returns its Window. 75 | // The body must be a NewWindowRequest. 76 | // Returns: 77 | // • OK on success. 78 | // • Internal Server Error on internal error. 79 | // • Bad Request if the WindowRequest is malformed. 80 | // 81 | // /window/ is the window with the given ID. 82 | // 83 | // DELETE deletes the window and all of its sheets. 84 | // The server process exits when the last window is deleted. 85 | // Returns: 86 | // • OK on success. 87 | // • Internal Server Error on internal error. 88 | // • Not Found if the buffer is not found. 89 | // 90 | // /window//columns is the list of the window's columns. 91 | // 92 | // PUT adds a column to the window. 93 | // The body must be a NewColumnRequest. 94 | // Returns: 95 | // • OK on success. 96 | // • Internal Server Error on internal error 97 | // or if a new column cannot fit on the window. 98 | // • Not Found if the window is not found. 99 | // • Bad Request if the WindowRequest is malformed. 100 | // 101 | // /window//sheets is the list of the window's sheets. 102 | // 103 | // PUT adds a sheet to the left-most column of the window 104 | // and returns its Sheet. 105 | // Returns: 106 | // • OK on success. 107 | // • Internal Server Error on internal error 108 | // or if a new sheet cannot fit in the column. 109 | // • Not Found if the window is not found. 110 | // 111 | // /sheets is the list of opened sheets. 112 | // 113 | // GET returns a Sheet list of the opened sheets. 114 | // Returns: 115 | // • OK on success. 116 | // • Internal Server Error on internal error. 117 | // 118 | // /sheet/ is the sheet with the given ID. 119 | // 120 | // DELETE deletes the sheet. 121 | // Returns: 122 | // • OK on success. 123 | // • Internal Server Error on internal error. 124 | // • Not Found if the sheet is not found. 125 | // 126 | // Unless otherwise stated, the body of all error responses is the error message. 127 | func (s *Server) RegisterHandlers(r *mux.Router) { 128 | r.HandleFunc("/windows", s.listWindowsHandler).Methods(http.MethodGet) 129 | r.HandleFunc("/windows", s.newWindowHandler).Methods(http.MethodPut) 130 | r.HandleFunc("/window/{id}", s.deleteWindowHandler).Methods(http.MethodDelete) 131 | r.HandleFunc("/window/{id}/columns", s.newColumnHandler).Methods(http.MethodPut) 132 | r.HandleFunc("/window/{id}/sheets", s.newSheetHandler).Methods(http.MethodPut) 133 | r.HandleFunc("/sheets", s.listSheetsHandler).Methods(http.MethodGet) 134 | r.HandleFunc("/sheet/{id}", s.deleteSheetHandler).Methods(http.MethodDelete) 135 | } 136 | 137 | // respond JSON encodes resp to w, and sends an Internal Server Error on failure. 138 | func respond(w http.ResponseWriter, resp interface{}) { 139 | if err := json.NewEncoder(w).Encode(resp); err != nil { 140 | http.Error(w, err.Error(), http.StatusInternalServerError) 141 | } 142 | } 143 | 144 | // MakeWindow returns a Window for the corresponding window. 145 | // It must be called with the server lock held. 146 | func makeWindow(w *window) Window { 147 | return Window{ 148 | ID: w.id, 149 | Path: windowPath(w), 150 | } 151 | } 152 | 153 | func windowPath(w *window) string { 154 | return path.Join("/", "window", w.id) 155 | } 156 | 157 | func (s *Server) listWindowsHandler(w http.ResponseWriter, req *http.Request) { 158 | s.RLock() 159 | var wins []Window 160 | for _, w := range s.windows { 161 | wins = append(wins, makeWindow(w)) 162 | } 163 | s.RUnlock() 164 | respond(w, wins) 165 | } 166 | 167 | func (s *Server) newWindowHandler(w http.ResponseWriter, req *http.Request) { 168 | var wreq NewWindowRequest 169 | if err := json.NewDecoder(req.Body).Decode(&wreq); err != nil { 170 | http.Error(w, err.Error(), http.StatusBadRequest) 171 | return 172 | } 173 | s.Lock() 174 | id := strconv.Itoa(s.nextID) 175 | s.nextID++ 176 | s.Unlock() 177 | win, err := newWindow(id, s, image.Pt(wreq.Width, wreq.Height)) 178 | if err != nil { 179 | http.Error(w, err.Error(), http.StatusInternalServerError) 180 | return 181 | } 182 | s.Lock() 183 | s.windows[id] = win 184 | resp := makeWindow(win) 185 | s.Unlock() 186 | respond(w, resp) 187 | } 188 | 189 | func (s *Server) deleteWindowHandler(w http.ResponseWriter, req *http.Request) { 190 | if !s.delWin(mux.Vars(req)["id"]) { 191 | http.NotFound(w, req) 192 | } 193 | } 194 | 195 | func (s *Server) delWin(winID string) bool { 196 | s.Lock() 197 | defer s.Unlock() 198 | w, ok := s.windows[winID] 199 | if !ok { 200 | return false 201 | } 202 | delete(s.windows, w.id) 203 | w.close() 204 | if len(s.windows) == 0 { 205 | s.done() 206 | } 207 | return true 208 | } 209 | 210 | func (s *Server) newColumnHandler(w http.ResponseWriter, req *http.Request) { 211 | var creq NewColumnRequest 212 | if err := json.NewDecoder(req.Body).Decode(&creq); err != nil { 213 | http.Error(w, err.Error(), http.StatusBadRequest) 214 | return 215 | } 216 | 217 | s.Lock() 218 | win, ok := s.windows[mux.Vars(req)["id"]] 219 | if !ok { 220 | s.Unlock() 221 | http.NotFound(w, req) 222 | return 223 | } 224 | errChan := make(chan error) 225 | win.Send(func() { 226 | c, err := newColumn(win) 227 | if err == nil { 228 | win.addColumn(creq.X, c) 229 | } 230 | errChan <- err 231 | }) 232 | s.Unlock() 233 | if err := <-errChan; err != nil { 234 | // TODO(eaburns): this may be an http error, propogate it. 235 | http.Error(w, err.Error(), http.StatusInternalServerError) 236 | } 237 | } 238 | 239 | func (s *Server) newSheetHandler(w http.ResponseWriter, req *http.Request) { 240 | var sreq NewSheetRequest 241 | if err := json.NewDecoder(req.Body).Decode(&sreq); err != nil { 242 | http.Error(w, err.Error(), http.StatusBadRequest) 243 | return 244 | } 245 | 246 | URL, err := url.Parse(sreq.URL) 247 | if err != nil { 248 | http.Error(w, "bad URL: "+sreq.URL, http.StatusBadRequest) 249 | return 250 | } 251 | 252 | s.Lock() 253 | win, ok := s.windows[mux.Vars(req)["id"]] 254 | if !ok { 255 | s.Unlock() 256 | http.NotFound(w, req) 257 | return 258 | } 259 | f, err := s.newSheet(win, URL) 260 | if err != nil { 261 | s.Unlock() 262 | // TODO(eaburns): This may be an http response error. 263 | // Return that status and message, not StatusInternalServerError. 264 | http.Error(w, err.Error(), http.StatusInternalServerError) 265 | return 266 | } 267 | resp := makeSheet(f) 268 | s.Unlock() 269 | respond(w, resp) 270 | } 271 | 272 | // NewSheet creates a new sheet, adds it to the server's sheet list 273 | // and asynchronously adds it to the window. 274 | // 275 | // This method must be called with the server lock held. 276 | func (s *Server) newSheet(win *window, URL *url.URL) (*sheet, error) { 277 | f, err := newSheet(strconv.Itoa(s.nextID), URL, win) 278 | if err != nil { 279 | return nil, err 280 | } 281 | s.nextID++ 282 | s.sheets[f.id] = f 283 | win.Send(func() { win.addFrame(f) }) 284 | return f, nil 285 | } 286 | 287 | // MakeSheet returns a Sheet for the corresponding sheet. 288 | // It must be called with the server lock held. 289 | func makeSheet(h *sheet) Sheet { 290 | return Sheet{ 291 | ID: h.id, 292 | Path: path.Join("/", "sheet", h.id), 293 | WindowPath: path.Join("/", "window", h.win.id), 294 | TagURL: h.tag.bufferURL.String(), 295 | BodyURL: h.body.bufferURL.String(), 296 | } 297 | } 298 | 299 | func (s *Server) listSheetsHandler(w http.ResponseWriter, req *http.Request) { 300 | s.RLock() 301 | var sheets []Sheet 302 | for _, h := range s.sheets { 303 | sheets = append(sheets, makeSheet(h)) 304 | } 305 | s.RUnlock() 306 | respond(w, sheets) 307 | } 308 | 309 | func (s *Server) deleteSheetHandler(w http.ResponseWriter, req *http.Request) { 310 | if !s.deleteSheet(mux.Vars(req)["id"]) { 311 | http.NotFound(w, req) 312 | } 313 | } 314 | 315 | func (s *Server) deleteSheet(sheetID string) bool { 316 | s.Lock() 317 | defer s.Unlock() 318 | f, ok := s.sheets[sheetID] 319 | if !ok { 320 | return false 321 | } 322 | delete(s.sheets, sheetID) 323 | f.win.Send(func() { f.win.deleteFrame(f) }) 324 | return true 325 | } 326 | -------------------------------------------------------------------------------- /editor/view/view.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | // Package view provides a View type, 4 | // which is an editor client 5 | // that maintains a local, consistent copy 6 | // of a segment of its buffer, 7 | // and a set of marks. 8 | // 9 | // A View tracks text within line boundaries 10 | // defined by a starting line 11 | // and a number of following lines. 12 | // The View text and marks can be read atomically, 13 | // giving a consistent view of the tracked segment of the buffer. 14 | // It can also be scrolled or warped to new starting line, 15 | // and it can be resized to track a different number of lines. 16 | // 17 | // A typical user will: 18 | // In one go routine: 19 | // 1. Receive from the Notify channel to wait for a change. 20 | // 2. Call View to read the updated text and marks. 21 | // 3. Goto 1. 22 | // 23 | // In another go routine: 24 | // • Call Scroll, Resize, and Do as desired. 25 | package view 26 | 27 | // TODO(eaburns): more efficient support for change-style edits. 28 | // Currently, the Do method refreshes the entire text and all marks. 29 | // This is needed in case the user changes one of the marks. 30 | // Add View.Change(a Address, to string), which cannot set marks, 31 | // and doesn't need a complete refresh. 32 | 33 | import ( 34 | "fmt" 35 | "net/url" 36 | "path" 37 | "strings" 38 | "sync" 39 | 40 | "github.com/eaburns/T/edit" 41 | "github.com/eaburns/T/editor" 42 | ) 43 | 44 | const ( 45 | // ViewMark is the mark rune indicating the start 46 | // of the text tracked by a View. 47 | ViewMark = '0' 48 | 49 | // TmpMark is a mark used temporarily to save and restore dot. 50 | TmpMark = '1' 51 | ) 52 | 53 | // A View is an editor client 54 | // that maintains a local, consistent copy 55 | // of a segment of its buffer, 56 | // and a set of marks. 57 | // 58 | // All methods of a View are safe to call concurrently. 59 | type View struct { 60 | // Notify is used to notify of changes to the View. 61 | // An empty struct is sent on Notify when the view changes. 62 | // Notify is closed when the View is closed. 63 | // Notify is single-buffered; if a send cannot proceed, it is dropped. 64 | Notify <-chan struct{} 65 | 66 | editorURL *url.URL 67 | textURL *url.URL 68 | changes *editor.ChangeStream 69 | do chan<- doRequest 70 | 71 | seq int 72 | 73 | mu sync.RWMutex 74 | n int 75 | text []byte 76 | marks []Mark 77 | } 78 | 79 | // A Mark is a mark tracked by a View. 80 | type Mark struct { 81 | // Name is the mark rune. 82 | Name rune 83 | // Where is the address of the mark as rune offsets. 84 | Where [2]int64 85 | } 86 | 87 | type doRequest struct { 88 | edits []edit.Edit 89 | result chan<- doResponse 90 | } 91 | 92 | type doResponse struct { 93 | error error 94 | results []editor.EditResult 95 | } 96 | 97 | // New returns a new View for a buffer. 98 | // The new view tracks the empty string at line 0 and the given marks. 99 | func New(bufferURL *url.URL, markRunes ...rune) (*View, error) { 100 | ed, err := editor.NewEditor(bufferURL) 101 | if err != nil { 102 | return nil, err 103 | } 104 | editorURL := *bufferURL 105 | editorURL.Path = ed.Path 106 | textURL := *bufferURL 107 | textURL.Path = path.Join(ed.Path, "text") 108 | 109 | changesURL := editorURL 110 | changesURL.Path = path.Join(bufferURL.Path, "changes") 111 | changesURL.Scheme = "ws" 112 | changes, err := editor.Changes(&changesURL) 113 | if err != nil { 114 | editor.Close(&editorURL) 115 | return nil, err 116 | } 117 | 118 | dedupedMarks := make(map[rune]bool) 119 | dedupedMarks[ViewMark] = true 120 | for _, r := range markRunes { 121 | dedupedMarks[r] = true 122 | } 123 | marks := make([]Mark, 0, len(markRunes)) 124 | for r := range dedupedMarks { 125 | marks = append(marks, Mark{ 126 | Name: r, 127 | Where: [2]int64{-1, -1}, 128 | }) 129 | } 130 | 131 | // Notify has a single-element buffer. 132 | // The sender always does a non-blocking send. 133 | // If there is no receiver and the channel is empty, 134 | // the notification is sent. 135 | // If there is no receiver and the channel is full, 136 | // the notification is dropped; 137 | // the next receiver will get the one sitting in the channel. 138 | Notify := make(chan struct{}, 1) 139 | do := make(chan doRequest) 140 | 141 | v := &View{ 142 | Notify: Notify, 143 | editorURL: &editorURL, 144 | textURL: &textURL, 145 | changes: changes, 146 | do: do, 147 | marks: marks, 148 | } 149 | 150 | go v.run(do, Notify) 151 | 152 | v.do <- doRequest{} 153 | <-Notify 154 | 155 | return v, nil 156 | } 157 | 158 | // Close closes the view, and deletes its editor. 159 | func (v *View) Close() error { 160 | close(v.do) 161 | err := v.changes.Close() 162 | editorErr := editor.Close(v.editorURL) 163 | if err == nil { 164 | err = editorErr 165 | } 166 | return err 167 | } 168 | 169 | // View calls the function with the current text and marks. 170 | // The text and marks will not change until f returns. 171 | func (v *View) View(f func(text []byte, marks []Mark)) { 172 | v.mu.RLock() 173 | f(v.text, v.marks) 174 | v.mu.RUnlock() 175 | } 176 | 177 | // Resize resizes the View to track the given number of lines, 178 | // and returns whether the size actually changed. 179 | func (v *View) Resize(nLines int) bool { 180 | if nLines < 0 { 181 | nLines = 0 182 | } 183 | v.mu.Lock() 184 | if v.n == nLines { 185 | v.mu.Unlock() 186 | return false 187 | } 188 | v.n = nLines 189 | v.mu.Unlock() 190 | v.do <- doRequest{} 191 | return true 192 | } 193 | 194 | // Scroll scrolls the View by the given delta. 195 | func (v *View) Scroll(deltaLines int) { 196 | if deltaLines == 0 { 197 | return 198 | } 199 | var a edit.Address 200 | mark := edit.Mark(ViewMark) 201 | zero := edit.Clamp(edit.Rune(0)) 202 | if deltaLines < 0 { 203 | lines := edit.Clamp(edit.Line(-deltaLines)) 204 | a = mark.Minus(lines).Minus(zero) 205 | } else { 206 | lines := edit.Clamp(edit.Line(deltaLines)) 207 | a = mark.Plus(lines).Plus(zero) 208 | } 209 | v.Warp(a) 210 | } 211 | 212 | // Warp moves the first line of the view 213 | // to the line containin the beginning of an Address. 214 | func (v *View) Warp(addr edit.Address) { v.DoAsync(edit.Set(addr, ViewMark)) } 215 | 216 | // Do performs edits using the View's Editor and returns the results. 217 | // If there is an error requesting the edits, it is returned through the error return. 218 | // If there is an error performing any of the individual edits, it is reported in the EditResult. 219 | func (v *View) Do(edits ...edit.Edit) ([]editor.EditResult, error) { 220 | result := make(chan doResponse) 221 | v.do <- doRequest{edits: edits, result: result} 222 | r := <-result 223 | return r.results, r.error 224 | } 225 | 226 | // DoAsync performs edits asynchronously using the View's Editor. 227 | // If there is an error requesting the edits, it is logged, and discarded. 228 | // The EditResults are silently discarded. 229 | func (v *View) DoAsync(edits ...edit.Edit) { v.do <- doRequest{edits: edits} } 230 | 231 | func (v *View) run(do <-chan doRequest, Notify chan<- struct{}) { 232 | changes := make(chan editor.ChangeList) 233 | go func(changes chan<- editor.ChangeList) { 234 | defer close(changes) 235 | for { 236 | cl, err := v.changes.Next() 237 | if err != nil { 238 | // TODO(eaburns): return error on Close. 239 | return 240 | } 241 | changes <- cl 242 | } 243 | }(changes) 244 | 245 | defer func() { 246 | close(Notify) 247 | // Flush any remaining channels on return. 248 | for range do { 249 | } 250 | for range changes { 251 | } 252 | }() 253 | 254 | for { 255 | select { 256 | case vd, ok := <-do: 257 | if !ok { 258 | return 259 | } 260 | if err := v.edit(vd, Notify); err != nil { 261 | // TODO(eaburns): return error on Close. 262 | return 263 | } 264 | case cl, ok := <-changes: 265 | if !ok { 266 | return 267 | } 268 | if v.seq >= cl.Sequence { 269 | break 270 | } 271 | // TODO(eaburns): this does a complete, blocking refresh. 272 | // Don't require a complete refresh with every change. 273 | if err := v.edit(doRequest{}, Notify); err != nil { 274 | return 275 | } 276 | } 277 | } 278 | } 279 | 280 | var ( 281 | saveDot = edit.Set(edit.Dot, TmpMark) 282 | restoreDot = edit.Set(edit.Mark('1'), '.') 283 | ) 284 | 285 | func (v *View) edit(vd doRequest, Notify chan<- struct{}) error { 286 | v.mu.RLock() 287 | var prints []edit.Edit 288 | for _, m := range v.marks { 289 | n := m.Name 290 | if m.Name == '.' { 291 | n = TmpMark 292 | } 293 | prints = append(prints, edit.Where(edit.Mark(n))) 294 | } 295 | // Use the start of the mark's line, regardless of where it ends up in the line. 296 | start := edit.Mark(ViewMark).Minus(edit.Line(0)).Minus(edit.Rune(0)) 297 | end := start.Plus(edit.Clamp(edit.Line(v.n))) 298 | win := start.To(end) 299 | prints = append(prints, edit.Print(win)) 300 | v.mu.RUnlock() 301 | 302 | edits := append(vd.edits, saveDot, edit.Block(edit.All, prints...), restoreDot) 303 | res, err := editor.Do(v.textURL, edits...) 304 | if err != nil { 305 | if vd.result != nil { 306 | go func() { vd.result <- doResponse{error: err} }() 307 | } 308 | return err 309 | } 310 | if vd.result != nil { 311 | go func() { vd.result <- doResponse{results: res[:len(res)-3]} }() 312 | } 313 | 314 | v.mu.Lock() 315 | defer v.mu.Unlock() 316 | 317 | update := res[len(res)-2] 318 | printed := strings.SplitN(update.Print, "\n", len(prints)) 319 | if len(printed) != len(prints) || update.Error != "" { 320 | panic(fmt.Sprintf("bad update: len(%v)=%d want %d, Error=%v", 321 | printed, len(printed), len(v.marks)+1, update.Error)) 322 | } 323 | for i := range v.marks { 324 | m := &v.marks[i] 325 | n, err := fmt.Sscanf(printed[i], "#%d,#%d", &m.Where[0], &m.Where[1]) 326 | if n == 1 { 327 | m.Where[1] = m.Where[0] 328 | } else if n != 2 || err != nil { 329 | panic("failed to scan address: " + printed[i]) 330 | } 331 | } 332 | v.text = []byte(printed[len(printed)-1]) 333 | v.seq = update.Sequence 334 | 335 | select { 336 | case Notify <- struct{}{}: 337 | default: 338 | } 339 | 340 | return nil 341 | } 342 | -------------------------------------------------------------------------------- /ui/textbox.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package ui 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "image" 9 | "image/color" 10 | "image/draw" 11 | "log" 12 | "math" 13 | "net/url" 14 | "path" 15 | "sync" 16 | "time" 17 | 18 | "github.com/eaburns/T/edit" 19 | "github.com/eaburns/T/editor" 20 | "github.com/eaburns/T/editor/view" 21 | "github.com/eaburns/T/ui/text" 22 | "golang.org/x/exp/shiny/screen" 23 | "golang.org/x/mobile/event/key" 24 | "golang.org/x/mobile/event/mouse" 25 | "golang.org/x/mobile/event/paint" 26 | ) 27 | 28 | const ( 29 | cursorWidth = 1 // px 30 | blinkDuration = 500 * time.Millisecond 31 | ) 32 | 33 | // A textBox is an editable text box. 34 | type textBox struct { 35 | bufferURL *url.URL 36 | view *view.View 37 | opts text.Options 38 | setter *text.Setter 39 | text *text.Text 40 | topLeft image.Point 41 | 42 | textLen int 43 | l0, dot0 int64 44 | 45 | // Col is the column number of the cursor, or -1 if unknown. 46 | col int 47 | 48 | lastBlink time.Time 49 | inFocus, blinkOn bool 50 | 51 | mu sync.RWMutex 52 | reset bool 53 | win *window 54 | } 55 | 56 | // NewTextBod creates a new text box. 57 | // URL is either the root path to an editor server, 58 | // or the path to an open buffer of an editor server. 59 | func newTextBox(w *window, URL url.URL, style text.Style) (t *textBox, err error) { 60 | if URL.Path == "/" { 61 | URL.Path = path.Join("/", "buffers") 62 | buf, err := editor.NewBuffer(&URL) 63 | if err != nil { 64 | return nil, err 65 | } 66 | URL.Path = buf.Path 67 | defer func(newBufferURL url.URL) { 68 | if err != nil { 69 | editor.Close(&newBufferURL) 70 | } 71 | }(URL) 72 | } 73 | if ok, err := path.Match("/buffer/*", URL.Path); err != nil { 74 | // The only error is path.ErrBadPattern. This pattern is not bad. 75 | panic(err) 76 | } else if !ok { 77 | return nil, errors.New("bad buffer path: " + URL.Path) 78 | } 79 | 80 | v, err := view.New(&URL, '.') 81 | if err != nil { 82 | return nil, err 83 | } 84 | opts := text.Options{ 85 | DefaultStyle: style, 86 | TabWidth: 4, 87 | Padding: 2, 88 | } 89 | setter := text.NewSetter(opts) 90 | t = &textBox{ 91 | bufferURL: &URL, 92 | view: v, 93 | opts: opts, 94 | setter: setter, 95 | text: setter.Set(), 96 | col: -1, 97 | win: w, 98 | } 99 | go func() { 100 | for range v.Notify { 101 | t.mu.Lock() 102 | t.reset = true 103 | if t.win != nil { 104 | t.win.Send(paint.Event{}) 105 | } 106 | t.mu.Unlock() 107 | } 108 | }() 109 | return t, nil 110 | } 111 | 112 | func (t *textBox) close() { 113 | t.mu.Lock() 114 | t.win = nil 115 | t.mu.Unlock() 116 | 117 | t.text.Release() 118 | t.setter.Release() 119 | t.view.Close() 120 | editor.Close(t.bufferURL) 121 | } 122 | 123 | // SetSize resets the text if either the size changed or the text changed. 124 | func (t *textBox) setSize(size image.Point) { 125 | t.mu.Lock() 126 | if !t.reset && t.opts.Size == size { 127 | t.mu.Unlock() 128 | return 129 | } 130 | t.reset = false 131 | t.mu.Unlock() 132 | 133 | h := t.opts.DefaultStyle.Face.Metrics().Height 134 | t.view.Resize(size.Y / h.Round()) 135 | t.text.Release() 136 | t.opts.Size = size 137 | t.setter.Reset(t.opts) 138 | 139 | t.view.View(func(text []byte, marks []view.Mark) { 140 | t.textLen = len(text) 141 | t.setter.Add(text) 142 | for _, m := range marks { 143 | switch m.Name { 144 | case view.ViewMark: 145 | t.l0 = m.Where[0] 146 | case '.': 147 | t.dot0 = m.Where[0] 148 | } 149 | } 150 | }) 151 | 152 | t.text = t.setter.Set() 153 | 154 | if t.inFocus { 155 | t.blinkOn = true 156 | t.lastBlink = time.Now() 157 | } 158 | } 159 | 160 | func (t *textBox) draw(scr screen.Screen, win screen.Window) { 161 | t.text.Draw(t.topLeft, scr, win) 162 | t.drawDot(t.topLeft, win) 163 | } 164 | 165 | func (t *textBox) drawLines(scr screen.Screen, win screen.Window) { 166 | t.text.DrawLines(t.topLeft, scr, win) 167 | t.drawDot(t.topLeft, win) 168 | } 169 | 170 | func (t *textBox) drawDot(pt image.Point, win screen.Window) { 171 | l, d := t.l0, t.dot0 172 | if !t.blinkOn || d < t.l0 || d > l+int64(t.textLen) || t.opts.Size.X < cursorWidth { 173 | return 174 | } 175 | i := int(d - l) 176 | r := t.text.GlyphBox(i).Add(pt) 177 | r.Max.X = r.Min.X + cursorWidth 178 | win.Fill(r, color.Black, draw.Src) 179 | } 180 | 181 | func (t *textBox) changeFocus(_ *window, inFocus bool) { 182 | t.inFocus = inFocus 183 | t.blinkOn = inFocus 184 | t.lastBlink = time.Now() 185 | } 186 | 187 | func (t *textBox) tick(win *window) bool { 188 | if s := time.Since(t.lastBlink); s < blinkDuration { 189 | return false 190 | } 191 | t.lastBlink = time.Now() 192 | t.blinkOn = !t.blinkOn 193 | return true 194 | } 195 | 196 | func (t *textBox) key(_ *window, event key.Event) bool { 197 | handleKey(t, event) 198 | return false 199 | } 200 | 201 | func (t *textBox) mouse(w *window, event mouse.Event) bool { 202 | handleMouse(t, event) 203 | return false 204 | } 205 | 206 | func (t *textBox) drawLast(scr screen.Screen, win screen.Window) {} 207 | 208 | func (t *textBox) doSync(eds ...edit.Edit) ([]editor.EditResult, error) { 209 | t.col = -1 210 | return t.view.Do(eds...) 211 | } 212 | 213 | func (t *textBox) doAsync(eds ...edit.Edit) { 214 | t.col = -1 215 | t.view.DoAsync(eds...) 216 | } 217 | 218 | func (t *textBox) where(p image.Point) int64 { 219 | return int64(t.text.Index(p.Sub(t.topLeft))) + t.l0 220 | } 221 | 222 | func (t *textBox) exec(c string) { 223 | t.mu.RLock() 224 | w := t.win 225 | t.mu.RUnlock() 226 | go w.exec(c) 227 | } 228 | 229 | func (t *textBox) setColumn(c int) { t.col = c } 230 | func (t *textBox) column() int { return t.col } 231 | 232 | var ( 233 | dot = edit.Dot 234 | zero = edit.Clamp(edit.Rune(0)) 235 | one = edit.Clamp(edit.Rune(1)) 236 | oneLine = edit.Clamp(edit.Line(1)) 237 | twoLines = edit.Clamp(edit.Line(2)) 238 | moveDotRight = edit.Set(dot.Plus(one), '.') 239 | moveDotLeft = edit.Set(dot.Minus(one), '.') 240 | backspace = edit.Delete(dot.Minus(one).To(dot)) 241 | backline = edit.Delete(dot.Minus(edit.Line(0)).To(dot.Plus(zero))) 242 | backword = edit.Delete(dot.Plus(zero).Minus(edit.Regexp(`\w*\W*`))) 243 | newline = []edit.Edit{edit.Change(dot, "\n"), edit.Set(dot.Plus(zero), '.')} 244 | tab = []edit.Edit{edit.Change(dot, "\t"), edit.Set(dot.Plus(zero), '.')} 245 | ) 246 | 247 | type doer interface { 248 | // DoSync clears the column marker 249 | // and performs the edit, 250 | // and returns the result. 251 | doSync(...edit.Edit) ([]editor.EditResult, error) 252 | 253 | // DoAsync clears the column marker 254 | // and performs the edit asynchronously, 255 | // discarding the result. 256 | doAsync(...edit.Edit) 257 | } 258 | 259 | type mouseHandler interface { 260 | doer 261 | // Where returns the rune address 262 | // corresponding to the glyph at the given point. 263 | where(image.Point) int64 264 | // Exec executes a command. 265 | exec(string) 266 | } 267 | 268 | func handleMouse(h mouseHandler, event mouse.Event) { 269 | if event.Modifiers != 0 { 270 | return 271 | } 272 | 273 | p := image.Pt(int(event.X), int(event.Y)) 274 | 275 | switch event.Direction { 276 | case mouse.DirPress: 277 | switch event.Button { 278 | case mouse.ButtonLeft: 279 | h.doAsync(edit.Set(edit.Rune(h.where(p)), '.')) 280 | case mouse.ButtonMiddle: 281 | // TODO(eaburns): This makes a blocking RPC, 282 | // but it's called from the mouse handler. 283 | // We should find a way to avoid blocking in the mouse handler. 284 | rune := edit.Rune(h.where(p)) 285 | re := edit.Regexp(`[a-zA-Z0-9_.\-+/]*`) // file name characters 286 | res, err := h.doSync(edit.Print(rune.Minus(re).To(rune.Plus(re))), 287 | edit.Set(rune, '.')) 288 | if err != nil { 289 | log.Println("failed to read command: ", err) 290 | return 291 | } 292 | if res[0].Error != "" { 293 | log.Println("failed to read command: ", res[0].Error) 294 | return 295 | } 296 | h.exec(res[0].Print) 297 | } 298 | } 299 | } 300 | 301 | type keyHandler interface { 302 | doer 303 | column() int 304 | setColumn(int) 305 | } 306 | 307 | // HandleKey encapsulates the keyboard editing logic for a textBox. 308 | func handleKey(h keyHandler, event key.Event) { 309 | if event.Direction == key.DirRelease { 310 | return 311 | } 312 | switch event.Code { 313 | case key.CodeUpArrow: 314 | col := getColumn(h) 315 | re := fmt.Sprintf("(?:.?){%d}", col) 316 | up := dot.Minus(oneLine).Minus(zero).Plus(edit.Regexp(re)).Plus(zero) 317 | h.doAsync(edit.Set(up, '.')) 318 | h.setColumn(col) 319 | case key.CodeDownArrow: 320 | col := getColumn(h) 321 | re := fmt.Sprintf("(?:.?){%d}", col) 322 | // We use .-1+2, because .+1 does not move dot 323 | // if it is at the beginning of an empty line. 324 | up := dot.Minus(oneLine).Plus(twoLines).Minus(zero).Plus(edit.Regexp(re)).Plus(zero) 325 | h.doAsync(edit.Set(up, '.')) 326 | h.setColumn(col) 327 | case key.CodeRightArrow: 328 | h.doAsync(moveDotRight) 329 | case key.CodeLeftArrow: 330 | h.doAsync(moveDotLeft) 331 | case key.CodeDeleteBackspace: 332 | h.doAsync(backspace) 333 | case key.CodeReturnEnter: 334 | h.doAsync(newline...) 335 | case key.CodeTab: 336 | h.doAsync(tab...) 337 | default: 338 | switch event.Modifiers { 339 | case 0, key.ModShift: 340 | if event.Rune >= 0 { 341 | r := string(event.Rune) 342 | h.doAsync(edit.Change(dot, r), edit.Set(dot.Plus(zero), '.')) 343 | } 344 | case key.ModControl: 345 | switch event.Rune { 346 | case 'a': 347 | h.doAsync(edit.Set(dot.Minus(edit.Line(0)).Minus(zero), '.')) 348 | case 'e': 349 | h.doAsync(edit.Set(dot.Minus(zero).Plus(edit.Regexp("$")), '.')) 350 | case 'h': 351 | h.doAsync(backspace) 352 | case 'u': 353 | h.doAsync(backline) 354 | case 'w': 355 | h.doAsync(backword) 356 | } 357 | } 358 | } 359 | } 360 | 361 | // Column returns the desired column number of the keyHandler. 362 | func getColumn(h keyHandler) int { 363 | if c := h.column(); c >= 0 { 364 | return c 365 | } 366 | 367 | // TODO(eaburns): This makes a blocking RPC, but it's called from the key handler. 368 | // We should find a way to avoid blocking in the key handler. 369 | res, err := h.doSync(edit.Where(dot.Minus(edit.Line(0)))) 370 | if err != nil { 371 | panic("failed to get column number: " + err.Error()) 372 | } 373 | if res[0].Error != "" { 374 | panic("failed to get column number: " + res[0].Error) 375 | } 376 | var w [2]int64 377 | if n, err := fmt.Sscanf(res[0].Print, "#%d,#%d", &w[0], &w[1]); n == 1 { 378 | w[1] = w[0] 379 | } else if n != 2 || err != nil { 380 | panic("failed to scan address: " + res[0].Print) 381 | } 382 | 383 | var c int 384 | if d := w[1] - w[0]; d > math.MaxInt32 { 385 | c = 0 386 | } else { 387 | c = int(d) 388 | } 389 | h.setColumn(c) 390 | return c 391 | } 392 | -------------------------------------------------------------------------------- /ui/column.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package ui 4 | 5 | import ( 6 | "image" 7 | "image/color" 8 | "image/draw" 9 | 10 | "github.com/eaburns/T/edit" 11 | "github.com/eaburns/T/ui/text" 12 | "golang.org/x/exp/shiny/screen" 13 | "golang.org/x/mobile/event/key" 14 | "golang.org/x/mobile/event/mouse" 15 | ) 16 | 17 | // A frame is a rectangular section of a win that can be attached to a column. 18 | type frame interface { 19 | // Bounds returns the current bounds of the frame. 20 | bounds() image.Rectangle 21 | 22 | // SetBounds sets the bounds of the frame to the given rectangle 23 | setBounds(image.Rectangle) 24 | 25 | // SetColumn sets the frame's column. 26 | setColumn(*column) 27 | 28 | // Focus returns the handler that is in focus at the given coordinate. 29 | // The upper-left of the frame is the Min point of its bounds. 30 | focus(image.Point) handler 31 | 32 | // Draw draws the frame to the window. 33 | draw(scr screen.Screen, win screen.Window) 34 | 35 | // Close closes the frame. 36 | // It is called by the containing object when that object has been removed. 37 | // Close should release the resources of the frame. 38 | // It should not remove the frame from its containing object, 39 | // because close is only intended to be called 40 | // after the frame has been removed from its container. 41 | close() 42 | 43 | // MinHeight returns the frame's minimum recommended height in pixels. 44 | // If the window is smaller than minHeight, the frame may also be smaller. 45 | minHeight() int 46 | } 47 | 48 | type column struct { 49 | win *window 50 | image.Rectangle 51 | frames []frame 52 | ys []float64 53 | } 54 | 55 | // NewColumn returns a new column, with a body, but no window or bounds. 56 | func newColumn(w *window) (*column, error) { 57 | c := new(column) 58 | tag, err := newColumnTag(w) 59 | if err != nil { 60 | return nil, err 61 | } 62 | c.addFrame(0, tag) 63 | return c, nil 64 | } 65 | 66 | func (c *column) close() { 67 | // Closing the column is handled by closing the columnTag. 68 | // 69 | // TODO(eaburns): this is ugly, merge the columnTag into the column 70 | // instead of making it a frame. 71 | c.frames[0].close() 72 | } 73 | 74 | func (c *column) bounds() image.Rectangle { return c.Rectangle } 75 | 76 | func (c *column) setBounds(bounds image.Rectangle) { 77 | c.Rectangle = bounds 78 | height := float64(bounds.Dy()) 79 | for i := len(c.frames) - 1; i >= 0; i-- { 80 | f := c.frames[i] 81 | b := bounds 82 | if i > 0 { 83 | b.Min.Y = bounds.Min.Y + int(height*c.ys[i]) 84 | } 85 | if i < len(c.frames)-1 { 86 | b.Max.Y = c.frames[i+1].bounds().Min.Y - borderWidth 87 | } 88 | f.setBounds(b) 89 | } 90 | } 91 | 92 | func (c *column) setAfterResizeBounds(bounds image.Rectangle) { 93 | c.Rectangle = bounds 94 | height := float64(bounds.Dy()) 95 | for i := len(c.frames) - 1; i >= 0; i-- { 96 | f := c.frames[i] 97 | b := bounds 98 | if i > 0 { 99 | if i == 1 { 100 | b.Min.Y = c.frames[0].bounds().Max.Y + borderWidth 101 | } else { 102 | b.Min.Y = bounds.Min.Y + int(height*c.ys[i]) 103 | } 104 | } 105 | if i < len(c.frames)-1 { 106 | b.Max.Y = c.frames[i+1].bounds().Min.Y - borderWidth 107 | } 108 | f.setBounds(b) 109 | } 110 | } 111 | 112 | func (c *column) focus(p image.Point) handler { 113 | for _, f := range c.frames { 114 | if p.In(f.bounds()) { 115 | return f.focus(p) 116 | } 117 | } 118 | return nil 119 | } 120 | 121 | func (c *column) draw(scr screen.Screen, win screen.Window) { 122 | for i, f := range c.frames { 123 | f.draw(scr, win) 124 | if i == len(c.frames)-1 { 125 | continue 126 | } 127 | g := c.frames[i+1] 128 | b := c.bounds() 129 | b.Min.Y = f.bounds().Max.Y 130 | b.Max.Y = g.bounds().Min.Y 131 | win.Fill(b, borderColor, draw.Over) 132 | } 133 | } 134 | 135 | func (c *column) removeFrame(f frame) bool { 136 | i := frameIndex(c, f) 137 | if i <= 0 { 138 | return false 139 | } 140 | c.frames = append(c.frames[:i], c.frames[i+1:]...) 141 | c.ys = append(c.ys[:i], c.ys[i+1:]...) 142 | c.setBounds(c.bounds()) 143 | f.setColumn(nil) 144 | return true 145 | } 146 | 147 | func (c *column) addFrame(yfrac float64, f frame) bool { 148 | if len(c.frames) == 0 { 149 | c.frames = []frame{f} 150 | c.ys = []float64{0.0} 151 | f.setColumn(c) 152 | f.setBounds(c.bounds()) 153 | return true 154 | } 155 | y := int(yfrac * float64(c.Dy())) 156 | i, splitFrame := frameAt(c, y) 157 | 158 | // Push away from the column edges. 159 | if y < 0 { 160 | y = 0 161 | yfrac = 0.0 162 | } 163 | if max := c.Dy() - f.minHeight() - borderWidth; y > max { 164 | y = max 165 | yfrac = float64(y) / float64(c.Dy()) 166 | } 167 | 168 | // The frame we are splitting goes on top. 169 | // The added frame goes on the bottom. 170 | splitBounds := splitFrame.bounds() 171 | if topSize := y - splitBounds.Min.Y; i > 0 && topSize < splitFrame.minHeight() { 172 | if !slideUp(c, i, splitFrame.minHeight()-topSize) { 173 | y += splitFrame.minHeight() - topSize 174 | yfrac = float64(y) / float64(c.Dy()) 175 | } 176 | } 177 | if bottomSize := splitBounds.Max.Y - y - borderWidth; bottomSize < f.minHeight() { 178 | if !slideDown(c, i, f.minHeight()-bottomSize) { 179 | return false 180 | } 181 | } 182 | 183 | c.frames = append(c.frames, nil) 184 | if i+2 < len(c.frames) { 185 | copy(c.frames[i+2:], c.frames[i+1:]) 186 | } 187 | c.frames[i+1] = f 188 | 189 | c.ys = append(c.ys, 0) 190 | if i+2 < len(c.ys) { 191 | copy(c.ys[i+2:], c.ys[i+1:]) 192 | } 193 | c.ys[i+1] = yfrac 194 | 195 | f.setColumn(c) 196 | c.setBounds(c.bounds()) 197 | 198 | return true 199 | } 200 | 201 | // FrameIndex returns the index of the frame within the column, 202 | // or -1 if the frame is not in the column. 203 | func frameIndex(c *column, f frame) int { 204 | for i := range c.frames { 205 | if c.frames[i] == f { 206 | return i 207 | } 208 | } 209 | return -1 210 | } 211 | 212 | // FrameAt returns the frame containing pixel row y. 213 | // If y < 0, the top-most frame is returned. 214 | // If y > width, the the bottom-most frame is returned. 215 | func frameAt(c *column, y int) (i int, f frame) { 216 | if y < 0 { 217 | return 0, c.frames[0] 218 | } 219 | for i, f = range c.frames { 220 | if f.bounds().Max.Y > y { 221 | return i, f 222 | } 223 | } 224 | return len(c.frames) - 1, c.frames[len(c.frames)-1] 225 | } 226 | 227 | func slideUp(c *column, i int, delta int) bool { 228 | if i <= 0 { 229 | return false 230 | } 231 | min := c.frames[i-1].minHeight() 232 | y := c.frames[i].bounds().Min.Y - delta 233 | if sz := y - c.frames[i-1].bounds().Min.Y - borderWidth; sz < min { 234 | if !slideUp(c, i-1, min-sz) { 235 | return false 236 | } 237 | } 238 | c.ys[i] = float64(y) / float64(c.Dy()) 239 | return true 240 | } 241 | 242 | func slideDown(c *column, i int, delta int) bool { 243 | if i > len(c.frames)-2 { 244 | return false 245 | } 246 | min := c.frames[i].minHeight() 247 | y := c.frames[i].bounds().Max.Y + delta 248 | if sz := c.frames[i+1].bounds().Max.Y - borderWidth - y; sz < min { 249 | if !slideDown(c, i+1, min-sz) { 250 | return false 251 | } 252 | } 253 | c.ys[i+1] = float64(y) / float64(c.Dy()) 254 | return true 255 | } 256 | 257 | const columnTagText = "Newcol New Cut Paste Snarf Putall" 258 | 259 | type columnTag struct { 260 | col *column 261 | text *textBox 262 | image.Rectangle 263 | 264 | p image.Point 265 | button mouse.Button 266 | origX float64 267 | } 268 | 269 | func newColumnTag(w *window) (*columnTag, error) { 270 | text, err := newTextBox(w, *w.server.editorURL, text.Style{ 271 | Face: w.face, 272 | FG: color.Black, 273 | BG: color.Gray16{0xF5F5}, 274 | }) 275 | if err != nil { 276 | return nil, err 277 | } 278 | text.view.DoAsync(edit.Change(edit.All, columnTagText+" "), edit.Set(edit.End, '.')) 279 | return &columnTag{text: text}, nil 280 | } 281 | 282 | func (t *columnTag) close() { 283 | if t.col == nil { 284 | // Already closed. 285 | // This can happen if the columnTag is in focus when the window is closed. 286 | // The in-focus handler is closed, and so are all columns. 287 | return 288 | } 289 | // The columnTag is t.col.frames[0]; it's already closing, close the rest. 290 | for _, f := range t.col.frames[1:] { 291 | f.close() 292 | } 293 | t.col = nil 294 | } 295 | 296 | func (t *columnTag) bounds() image.Rectangle { return t.Rectangle } 297 | 298 | func (t *columnTag) setBounds(b image.Rectangle) { 299 | t.text.topLeft = b.Min 300 | t.text.setSize(b.Size()) 301 | t.Rectangle = b 302 | } 303 | 304 | func (*columnTag) minHeight() int { return 0 } 305 | 306 | func (t *columnTag) setColumn(c *column) { t.col = c } 307 | 308 | func (t *columnTag) focus(image.Point) handler { return t } 309 | 310 | func (t *columnTag) draw(scr screen.Screen, win screen.Window) { 311 | t.text.setSize(t.Size()) // Reset the text in case it changed. 312 | t.text.draw(scr, win) 313 | } 314 | 315 | func (t *columnTag) drawLast(scr screen.Screen, win screen.Window) { 316 | if t.col.win != nil { 317 | return 318 | } 319 | t.col.draw(scr, win) 320 | drawBorder(t.col.bounds(), win) 321 | } 322 | 323 | func drawBorder(b image.Rectangle, win screen.Window) { 324 | x0, x1 := b.Min.X, b.Max.X 325 | y0, y1 := b.Min.Y, b.Max.Y 326 | win.Fill(image.Rect(x0, y0-borderWidth, x1, y0), borderColor, draw.Over) 327 | win.Fill(image.Rect(x0-borderWidth, y0, x0, y1), borderColor, draw.Over) 328 | win.Fill(image.Rect(x0, y1, x1, y1+borderWidth), borderColor, draw.Over) 329 | win.Fill(image.Rect(x1, y0, x1+borderWidth, y1), borderColor, draw.Over) 330 | } 331 | 332 | func (t *columnTag) changeFocus(win *window, inFocus bool) { 333 | t.text.changeFocus(win, inFocus) 334 | } 335 | 336 | func (t *columnTag) tick(win *window) bool { 337 | return t.text.tick(win) 338 | } 339 | 340 | func (t *columnTag) key(w *window, event key.Event) bool { 341 | var redraw bool 342 | switch event.Code { 343 | case key.CodeLeftShift, key.CodeRightShift: 344 | if event.Direction == key.DirRelease && t.col.win == nil { 345 | // We were dragging, and shift was released. Put it back. 346 | // BUG(eaburns): column 0 still ends up as column 1. 347 | if !w.addColumn(t.origX, t.col) { 348 | panic("can't put it back") 349 | } 350 | redraw = true 351 | } 352 | } 353 | if t.text.key(w, event) { 354 | redraw = true 355 | } 356 | return redraw 357 | } 358 | 359 | func (t *columnTag) mouse(w *window, event mouse.Event) bool { 360 | p := image.Pt(int(event.X), int(event.Y)) 361 | 362 | switch event.Direction { 363 | case mouse.DirPress: 364 | if t.button == mouse.ButtonNone { 365 | t.p = p 366 | t.button = event.Button 367 | break 368 | } 369 | // A second button was pressed while the first was held. 370 | // ColumnTag doesn't use chords; treat this as a release of the first. 371 | event.Button = t.button 372 | fallthrough 373 | 374 | case mouse.DirRelease: 375 | if event.Button != t.button { 376 | // It's not the pressed button. Ignore it. 377 | break 378 | } 379 | defer func() { t.button = mouse.ButtonNone }() 380 | 381 | if event.Modifiers != key.ModShift { 382 | break 383 | } 384 | switch t.button { 385 | case mouse.ButtonLeft: 386 | if t.col.win != nil { 387 | defer func() { t.col.setBounds(t.col.bounds()) }() 388 | return slideDown(t.col, 0, minHeight(t.text.opts)) 389 | } 390 | if w.addColumn(float64(t.Min.X)/float64(w.Dx()), t.col) { 391 | return true 392 | } 393 | // It didn't fit; just put it back where it came from. 394 | if !w.addColumn(t.origX, t.col) { 395 | panic("can't put it back") 396 | } 397 | return true 398 | case mouse.ButtonMiddle: 399 | w.deleteColumn(t.col) 400 | return true 401 | } 402 | 403 | case mouse.DirNone: 404 | if t.button == mouse.ButtonNone || event.Modifiers != key.ModShift { 405 | break 406 | } 407 | switch t.button { 408 | case mouse.ButtonLeft: 409 | if t.col.win == nil { 410 | t.col.setBounds(t.col.Add(p.Sub(t.col.Min))) 411 | return true 412 | } 413 | dx := t.p.X - p.X 414 | dy := t.p.Y - p.Y 415 | if dx*dx+dy*dy > 100 { 416 | t.p = p 417 | i := columnIndex(w, t.col) 418 | if i < 0 { 419 | return false 420 | } 421 | t.origX = w.xs[i] 422 | return w.removeColumn(t.col) 423 | } 424 | } 425 | } 426 | return t.text.mouse(w, event) 427 | } 428 | 429 | // MinHeight returns the minimum height 430 | // for a single line of text in the default style 431 | // plus padding and a border. 432 | func minHeight(opts text.Options) int { 433 | return opts.DefaultStyle.Face.Metrics().Height.Round() + opts.Padding*2 + borderWidth 434 | } 435 | -------------------------------------------------------------------------------- /editor/view/view_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package view 4 | 5 | import ( 6 | "net/url" 7 | "path" 8 | "reflect" 9 | "testing" 10 | "time" 11 | 12 | "github.com/eaburns/T/edit" 13 | "github.com/eaburns/T/editor" 14 | "github.com/eaburns/T/editor/editortest" 15 | ) 16 | 17 | func TestNew(t *testing.T) { 18 | bufferURL, close := testBuffer() 19 | defer close() 20 | setText(bufferURL, "1\n2\n3\n") 21 | 22 | v, err := New(bufferURL) 23 | if err != nil { 24 | t.Fatalf("New(%q)=_,%v, want _,nil", bufferURL, err) 25 | } 26 | defer v.Close() 27 | 28 | wantMarks := []Mark{{Name: ViewMark}} 29 | v.View(func(text []byte, marks []Mark) { 30 | if len(text) != 0 || !reflect.DeepEqual(wantMarks, marks) { 31 | t.Errorf("v.View(·)=%v,%v, want {},%v", text, marks, wantMarks) 32 | } 33 | }) 34 | } 35 | 36 | func TestResizeScroll(t *testing.T) { 37 | const lines = "1\n2\n3\n" 38 | tests := []struct { 39 | size int 40 | scroll []int 41 | want string 42 | }{ 43 | {size: -1, want: ""}, 44 | {size: 0, want: ""}, 45 | {size: 1, want: "1\n"}, 46 | {size: 2, want: "1\n2\n"}, 47 | {size: 3, want: "1\n2\n3\n"}, 48 | {size: 4, want: "1\n2\n3\n"}, 49 | {size: 100, want: "1\n2\n3\n"}, 50 | 51 | {size: 1, scroll: []int{-1}, want: "1\n"}, 52 | {size: 1, scroll: []int{0}, want: "1\n"}, 53 | {size: 1, scroll: []int{1}, want: "2\n"}, 54 | {size: 1, scroll: []int{2}, want: "3\n"}, 55 | {size: 1, scroll: []int{3}, want: ""}, 56 | {size: 1, scroll: []int{100}, want: ""}, 57 | 58 | {size: 2, scroll: []int{-1}, want: "1\n2\n"}, 59 | {size: 2, scroll: []int{0}, want: "1\n2\n"}, 60 | {size: 2, scroll: []int{1}, want: "2\n3\n"}, 61 | {size: 2, scroll: []int{2}, want: "3\n"}, 62 | {size: 2, scroll: []int{3}, want: ""}, 63 | {size: 2, scroll: []int{100}, want: ""}, 64 | 65 | {size: 1, scroll: []int{-1, -1}, want: "1\n"}, 66 | {size: 1, scroll: []int{0, -1}, want: "1\n"}, 67 | {size: 1, scroll: []int{1, -1}, want: "1\n"}, 68 | {size: 1, scroll: []int{2, -1}, want: "2\n"}, 69 | {size: 1, scroll: []int{3, -1}, want: "3\n"}, 70 | {size: 1, scroll: []int{4, -1}, want: "3\n"}, 71 | 72 | {size: 100, scroll: []int{100}, want: ""}, 73 | {size: 100, scroll: []int{-100}, want: "1\n2\n3\n"}, 74 | {size: 100, scroll: []int{100, -50}, want: "1\n2\n3\n"}, 75 | {size: 100, scroll: []int{-100, 50}, want: ""}, 76 | } 77 | 78 | bufferURL, close := testBuffer() 79 | defer close() 80 | setText(bufferURL, lines) 81 | 82 | for _, test := range tests { 83 | v, err := New(bufferURL) 84 | if err != nil { 85 | t.Fatalf("New(%q)=_,%v, want _,nil", bufferURL, err) 86 | } 87 | if v.Resize(test.size) { 88 | wait(v) 89 | } 90 | for _, s := range test.scroll { 91 | if s != 0 { 92 | v.Scroll(s) 93 | wait(v) 94 | } 95 | } 96 | v.View(func(text []byte, marks []Mark) { 97 | if str := string(text); str != test.want { 98 | t.Errorf("size %d, scroll %v: v.View(·)=%q,%v, want %q,_", 99 | test.size, test.scroll, str, marks, test.want) 100 | } 101 | }) 102 | if err := v.Close(); err != nil { 103 | t.Fatalf("v.Close()=%v\n", err) 104 | } 105 | } 106 | } 107 | 108 | func TestWarp(t *testing.T) { 109 | const lines = "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n" 110 | tests := []struct { 111 | size int 112 | warp edit.Address 113 | want string 114 | }{ 115 | {size: 1, warp: edit.Rune(0), want: "1\n"}, 116 | {size: 1, warp: edit.All, want: "1\n"}, 117 | {size: 1, warp: edit.End, want: ""}, 118 | {size: 1, warp: edit.Line(0), want: "1\n"}, 119 | {size: 1, warp: edit.Line(1), want: "1\n"}, 120 | {size: 1, warp: edit.Line(2), want: "2\n"}, 121 | {size: 1, warp: edit.Line(8), want: "8\n"}, 122 | {size: 1, warp: edit.Line(9), want: "9\n"}, 123 | {size: 1, warp: edit.Line(10), want: "0\n"}, 124 | {size: 1, warp: edit.Clamp(edit.Line(11)), want: ""}, 125 | {size: 1, warp: edit.Regexp("5"), want: "5\n"}, 126 | {size: 1, warp: edit.Regexp("5\n6\n7"), want: "5\n"}, 127 | } 128 | 129 | bufferURL, close := testBuffer() 130 | defer close() 131 | setText(bufferURL, lines) 132 | 133 | for _, test := range tests { 134 | v, err := New(bufferURL) 135 | if err != nil { 136 | t.Fatalf("New(%q)=_,%v, want _,nil", bufferURL, err) 137 | } 138 | if v.Resize(test.size) { 139 | wait(v) 140 | } 141 | v.Warp(test.warp) 142 | wait(v) 143 | v.View(func(text []byte, marks []Mark) { 144 | if str := string(text); str != test.want { 145 | t.Errorf("size %d, warp %s: v.View(·)=%q,%v, want %q,_", 146 | test.size, test.warp, str, marks, test.want) 147 | } 148 | }) 149 | if err := v.Close(); err != nil { 150 | t.Fatalf("v.Close()=%v\n", err) 151 | } 152 | } 153 | } 154 | 155 | type doTest struct { 156 | name string 157 | init string 158 | size, scroll int 159 | do edit.Edit 160 | want, error, print string 161 | } 162 | 163 | var doTests = []doTest{ 164 | { 165 | name: "change all", 166 | init: "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", 167 | size: 1, 168 | do: edit.Change(edit.All, "Hello, World\n"), 169 | want: "Hello, World\n", 170 | }, 171 | { 172 | name: "delete all", 173 | init: "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", 174 | size: 1, 175 | do: edit.Delete(edit.All), 176 | want: "", 177 | }, 178 | { 179 | name: "change in view", 180 | init: "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", 181 | size: 100, 182 | do: edit.Change(edit.Regexp("4\n5\n6"), "6\n5\n4"), 183 | want: "1\n2\n3\n6\n5\n4\n7\n8\n9\n0\n", 184 | }, 185 | { 186 | name: "delete in view", 187 | init: "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", 188 | size: 100, 189 | do: edit.Delete(edit.Line(2).To(edit.Line(9))), 190 | want: "1\n0\n", 191 | }, 192 | { 193 | name: "delete before", 194 | init: "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", 195 | size: 1, 196 | scroll: 1, 197 | do: edit.Delete(edit.Line(1)), 198 | want: "2\n", 199 | }, 200 | { 201 | name: "delete preceding newline", 202 | init: "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", 203 | size: 3, 204 | scroll: 1, 205 | do: edit.Change(edit.Regexp(`1\n`), "1"), 206 | want: "12\n3\n4\n", 207 | }, 208 | } 209 | 210 | func TestDo(t *testing.T) { 211 | tests := append(doTests, 212 | doTest{ 213 | name: "error", 214 | init: "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", 215 | size: 1, 216 | do: edit.Change(edit.Regexp("no match"), "Hello, World"), 217 | want: "1\n", 218 | error: "no match", 219 | }, 220 | doTest{ 221 | name: "print", 222 | init: "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", 223 | size: 1, 224 | do: edit.Print(edit.Line(5).To(edit.Line(7))), 225 | want: "1\n", 226 | print: "5\n6\n7\n", 227 | }) 228 | 229 | bufferURL, close := testBuffer() 230 | defer close() 231 | 232 | for _, test := range tests { 233 | setText(bufferURL, test.init) 234 | 235 | v, err := New(bufferURL) 236 | if err != nil { 237 | t.Fatalf("New(%q)=_,%v, want _,nil", bufferURL, err) 238 | } 239 | 240 | if v.Resize(test.size) { 241 | wait(v) 242 | } 243 | 244 | v.Scroll(test.scroll) 245 | if test.scroll != 0 { 246 | wait(v) 247 | } 248 | 249 | result, err := v.Do(test.do) 250 | if err != nil { 251 | t.Fatalf("%s: v.Do(%q)=%v,%v, want _,nil", test.name, test.do, result, err) 252 | } 253 | 254 | wait(v) 255 | v.View(func(text []byte, marks []Mark) { 256 | if str := string(text); str != test.want { 257 | t.Errorf("%s: v.View(·)=%q,%v, want %q,_", test.name, str, marks, test.want) 258 | } 259 | }) 260 | if len(result) != 1 { 261 | t.Fatalf("%s: len(result)=%d, want 1", test.name, len(result)) 262 | } 263 | r := result[0] 264 | if r.Print != test.print { 265 | t.Errorf("%s: r.Print=%q, want %q", test.name, r.Print, test.print) 266 | } 267 | if r.Error != test.error { 268 | t.Errorf("%s: r.Error=%q, want %q", test.name, r.Error, test.error) 269 | } 270 | if err := v.Close(); err != nil { 271 | t.Fatalf("v.Close()=%v\n", err) 272 | } 273 | } 274 | } 275 | 276 | func TestConcurrentChange(t *testing.T) { 277 | bufferURL, close := testBuffer() 278 | defer close() 279 | 280 | for _, test := range doTests { 281 | if test.error != "" || test.print != "" { 282 | panic(test.name + " error and print must not be set") 283 | } 284 | 285 | setText(bufferURL, test.init) 286 | 287 | v, err := New(bufferURL) 288 | if err != nil { 289 | t.Fatalf("New(%q)=_,%v, want _,nil", bufferURL, err) 290 | } 291 | 292 | if v.Resize(test.size) { 293 | wait(v) 294 | } 295 | 296 | v.Scroll(test.scroll) 297 | if test.scroll != 0 { 298 | wait(v) 299 | } 300 | 301 | // Make a change using a different editor. 302 | do(bufferURL, test.do) 303 | 304 | wait(v) 305 | v.View(func(text []byte, marks []Mark) { 306 | if str := string(text); str != test.want { 307 | t.Errorf("%s: v.View(·)=%q,%v, want %q,_", test.name, str, marks, test.want) 308 | } 309 | }) 310 | 311 | if err := v.Close(); err != nil { 312 | t.Fatalf("v.Close()=%v\n", err) 313 | } 314 | } 315 | } 316 | 317 | func TestTrackMarks(t *testing.T) { 318 | bufferURL, close := testBuffer() 319 | defer close() 320 | 321 | const lines = "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n" 322 | setText(bufferURL, lines) 323 | 324 | v, err := New(bufferURL, 'm', '.') 325 | if err != nil { 326 | t.Fatalf("New(%q, 'm', '.')=_,%v, want _,nil", bufferURL, err) 327 | } 328 | defer v.Close() 329 | 330 | if v.Resize(4) { 331 | wait(v) 332 | } 333 | 334 | v.DoAsync(edit.Set(edit.Rune(5), 'm'), edit.Set(edit.Rune(10), '.')) 335 | wait(v) 336 | 337 | want := [2]int64{5, 5} 338 | if got, ok := markAddr(v, 'm'); !ok || got != want { 339 | t.Errorf("mark['m']=%v,%v, want %v,true", got, ok, want) 340 | } 341 | 342 | want = [2]int64{10, 10} 343 | if got, ok := markAddr(v, '.'); !ok || got != want { 344 | t.Errorf("mark['.']=%v,%v, want %v,true", got, ok, want) 345 | } 346 | 347 | v.DoAsync(edit.Delete(edit.Rune(1).To(edit.Rune(2)))) 348 | wait(v) 349 | 350 | want = [2]int64{4, 4} 351 | if got, ok := markAddr(v, 'm'); !ok || got != want { 352 | t.Errorf("mark['m']=%v,%v, want %v,true", got, ok, want) 353 | } 354 | 355 | want = [2]int64{1, 1} 356 | if got, ok := markAddr(v, '.'); !ok || got != want { 357 | t.Errorf("mark['.']=%v,%v, want %v,true", got, ok, want) 358 | } 359 | } 360 | 361 | // TestMaintainDot checks that, whatever we do under the hood, we don't affect dot. 362 | func TestMaintainDot(t *testing.T) { 363 | bufferURL, close := testBuffer() 364 | defer close() 365 | 366 | v, err := New(bufferURL, 'm') 367 | if err != nil { 368 | t.Fatalf("New(%q, 'm')=_,%v, want _,nil", bufferURL, err) 369 | } 370 | defer v.Close() 371 | 372 | if v.Resize(1) { 373 | wait(v) 374 | } 375 | 376 | str := "Hello, 世界\n" 377 | v.DoAsync(edit.Change(edit.All, str)) 378 | wait(v) 379 | 380 | e := edit.Print(edit.Dot) 381 | result, err := v.Do(e) 382 | if err != nil || len(result) != 1 || result[0].Print != str { 383 | t.Errorf("v.Do(%q)=%v,%v, want []EditResult{{Print: %q}}, nil", e, result, err, str) 384 | } 385 | } 386 | 387 | func TestMalformedEditError(t *testing.T) { 388 | bufferURL, close := testBuffer() 389 | defer close() 390 | 391 | v, err := New(bufferURL, 'm') 392 | if err != nil { 393 | t.Fatalf("New(%q, 'm')=_,%v, want _,nil", bufferURL, err) 394 | } 395 | defer v.Close() 396 | 397 | // The regexp is malformed. 398 | badEdit := edit.Print(edit.Regexp(`][`)) 399 | res, err := v.Do(badEdit) 400 | if err == nil { 401 | t.Errorf("v.Do(%q)=%v,%v, want _,non-nil", badEdit, res, err) 402 | } 403 | } 404 | 405 | func markAddr(v *View, name rune) ([2]int64, bool) { 406 | var ok bool 407 | var where [2]int64 408 | v.View(func(_ []byte, marks []Mark) { 409 | for _, m := range marks { 410 | if m.Name == name { 411 | ok = true 412 | where = m.Where 413 | } 414 | } 415 | }) 416 | return where, ok 417 | } 418 | 419 | func setText(bufferURL *url.URL, str string) { 420 | do(bufferURL, edit.Change(edit.All, str)) 421 | } 422 | 423 | func do(bufferURL *url.URL, e edit.Edit) { 424 | ed, err := editor.NewEditor(bufferURL) 425 | if err != nil { 426 | panic(err) 427 | } 428 | editorURL := *bufferURL 429 | editorURL.Path = ed.Path 430 | defer editor.Close(&editorURL) 431 | 432 | textURL := editorURL 433 | textURL.Path = path.Join(ed.Path, "text") 434 | res, err := editor.Do(&textURL, e) 435 | if err != nil { 436 | panic(err) 437 | } 438 | if len(res) != 1 { 439 | panic("expected 1 result") 440 | } 441 | if res[0].Error != "" { 442 | panic(res[0].Error) 443 | } 444 | } 445 | 446 | func wait(v *View) { 447 | timer := time.NewTimer(10 * time.Second) 448 | defer timer.Stop() 449 | select { 450 | case <-v.Notify: 451 | case <-timer.C: 452 | panic("timed out") 453 | } 454 | } 455 | 456 | func testBuffer() (bufferURL *url.URL, close func()) { 457 | s := editortest.NewServer(editor.NewServer()) 458 | b, err := editor.NewBuffer(s.PathURL("/", "buffers")) 459 | if err != nil { 460 | panic(err) 461 | } 462 | return s.PathURL(b.Path), s.Close 463 | } 464 | -------------------------------------------------------------------------------- /edit/buffer.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015, The T Authors. 2 | 3 | package edit 4 | 5 | import ( 6 | "bufio" 7 | "io" 8 | 9 | "github.com/eaburns/T/edit/runes" 10 | ) 11 | 12 | // A Buffer implements the Editor interface, 13 | // editing an unbounded sequence of runes. 14 | type Buffer struct { 15 | runes *runes.Buffer 16 | pending, undo, redo *log 17 | seq int32 18 | marks map[rune]Span 19 | } 20 | 21 | // NewBuffer returns a new, empty Buffer. 22 | func NewBuffer() *Buffer { return newBuffer(runes.NewBuffer(1 << 12)) } 23 | 24 | func newBuffer(rs *runes.Buffer) *Buffer { 25 | return &Buffer{ 26 | runes: rs, 27 | undo: newLog(), 28 | redo: newLog(), 29 | pending: newLog(), 30 | marks: make(map[rune]Span), 31 | } 32 | } 33 | 34 | // Close closes the Buffer and releases its resources. 35 | func (buf *Buffer) Close() error { 36 | errs := []error{ 37 | buf.runes.Close(), 38 | buf.pending.close(), 39 | buf.undo.close(), 40 | buf.redo.close(), 41 | } 42 | for _, e := range errs { 43 | if e != nil { 44 | return e 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | // Change changes the string identified by at 51 | // to contain the runes from the Reader. 52 | // 53 | // This method must be called with the Lock held. 54 | func (buf *Buffer) change(s Span, src runes.Reader) error { 55 | if err := buf.runes.Delete(s.Size(), s[0]); err != nil { 56 | return err 57 | } 58 | n, err := runes.Copy(buf.runes.Writer(s[0]), src) 59 | if err != nil { 60 | return err 61 | } 62 | for m := range buf.marks { 63 | buf.marks[m] = buf.marks[m].Update(s, n) 64 | } 65 | return nil 66 | } 67 | 68 | // Size implements the Size method of the Text interface. 69 | // 70 | // It returns the number of Runes in the Buffer. 71 | func (buf *Buffer) Size() int64 { return buf.runes.Size() } 72 | 73 | func (buf *Buffer) Mark(m rune) Span { return buf.marks[m] } 74 | 75 | func (buf *Buffer) SetMark(m rune, s Span) error { 76 | if size := buf.Size(); s[0] < 0 || s[1] < 0 || s[0] > size || s[1] > size { 77 | return ErrInvalidArgument 78 | } 79 | buf.marks[m] = s 80 | return nil 81 | } 82 | 83 | type runeReader struct { 84 | span Span 85 | buffer *Buffer 86 | } 87 | 88 | func (rr *runeReader) ReadRune() (r rune, w int, err error) { 89 | switch size := rr.span.Size(); { 90 | case size == 0: 91 | return 0, 0, io.EOF 92 | case size < 0: 93 | rr.span[0]-- 94 | r, err = rr.buffer.runes.Rune(rr.span[0]) 95 | default: 96 | r, err = rr.buffer.runes.Rune(rr.span[0]) 97 | rr.span[0]++ 98 | } 99 | return r, 1, err 100 | } 101 | 102 | type badRange struct{} 103 | 104 | func (badRange) Read([]byte) (int, error) { return 0, ErrInvalidArgument } 105 | func (badRange) ReadRune() (rune, int, error) { return 0, 0, ErrInvalidArgument } 106 | 107 | // RuneReader implements the Runes method of the Text interface. 108 | // 109 | // Each non-error ReadRune operation returns a width of 1. 110 | func (buf *Buffer) RuneReader(s Span) io.RuneReader { 111 | if size := buf.Size(); s[0] < 0 || s[1] < 0 || s[0] > size || s[1] > size { 112 | return badRange{} 113 | } 114 | return &runeReader{span: s, buffer: buf} 115 | } 116 | 117 | func (buf *Buffer) Reader(s Span) io.Reader { 118 | if size := buf.Size(); s[0] < 0 || s[1] < 0 || s[0] > size || s[1] > size { 119 | return badRange{} 120 | } 121 | rr := runes.LimitReader(buf.runes.Reader(s[0]), s.Size()) 122 | return runes.UTF8Reader(rr) 123 | } 124 | 125 | func (buf *Buffer) Change(s Span, r io.Reader) (n int64, err error) { 126 | if prev := logLast(buf.pending); !prev.end() && s[0] < prev.span[1] { 127 | err = ErrOutOfSequence 128 | } else { 129 | rr := runes.RunesReader(bufio.NewReader(r)) 130 | n, err = buf.pending.append(buf.seq, s, rr) 131 | } 132 | if err != nil { 133 | buf.pending.reset() 134 | } 135 | return n, err 136 | } 137 | 138 | func (buf *Buffer) Apply() error { 139 | for e := logFirst(buf.pending); !e.end(); e = e.next() { 140 | undoSpan := Span{e.span[0], e.span[0] + e.size} 141 | undoSrc := buf.runes.Reader(e.span[0]) 142 | undoSrc = runes.LimitReader(undoSrc, e.span.Size()) 143 | if _, err := buf.undo.append(buf.seq, undoSpan, undoSrc); err != nil { 144 | return err 145 | } 146 | } 147 | dot := buf.marks['.'] 148 | for e := logFirst(buf.pending); !e.end(); e = e.next() { 149 | if e.span[0] == dot[0] { 150 | // If they have the same start, grow dot. 151 | // Otherwise, update would simply leave it 152 | // as a point address and move it. 153 | dot[1] = dot.Update(e.span, e.size)[1] 154 | } else { 155 | dot = dot.Update(e.span, e.size) 156 | } 157 | for f := e.next(); !f.end(); f = f.next() { 158 | f.span = f.span.Update(e.span, e.size) 159 | if err := f.store(); err != nil { 160 | return err 161 | } 162 | } 163 | 164 | if err := buf.change(e.span, e.data()); err != nil { 165 | return err 166 | } 167 | } 168 | buf.pending.reset() 169 | buf.redo.reset() 170 | buf.marks['.'] = dot 171 | buf.seq++ 172 | return nil 173 | } 174 | 175 | func (buf *Buffer) Undo() error { 176 | marks0 := make(map[rune]Span, len(buf.marks)) 177 | for r, s := range buf.marks { 178 | marks0[r] = s 179 | } 180 | defer func() { buf.marks = marks0 }() 181 | 182 | all := Span{-1, 0} 183 | start := logLastFrame(buf.undo) 184 | if start.end() { 185 | return nil 186 | } 187 | for e := start; !e.end(); e = e.next() { 188 | redoSpan := Span{e.span[0], e.span[0] + e.size} 189 | redoSrc := buf.runes.Reader(e.span[0]) 190 | redoSrc = runes.LimitReader(redoSrc, e.span.Size()) 191 | if _, err := buf.redo.append(buf.seq, redoSpan, redoSrc); err != nil { 192 | return err 193 | } 194 | 195 | if all[0] < 0 { 196 | all[0] = e.span[0] 197 | } 198 | all[1] = e.span[0] + e.size 199 | if err := buf.change(e.span, e.data()); err != nil { 200 | return err 201 | } 202 | } 203 | buf.marks['.'] = all 204 | marks0 = buf.marks 205 | buf.seq++ 206 | return start.pop() 207 | } 208 | 209 | func (buf *Buffer) Redo() error { 210 | marks0 := make(map[rune]Span, len(buf.marks)) 211 | for r, s := range buf.marks { 212 | marks0[r] = s 213 | } 214 | defer func() { buf.marks = marks0 }() 215 | 216 | start := logLastFrame(buf.redo) 217 | if start.end() { 218 | return nil 219 | } 220 | for e := start; !e.end(); e = e.next() { 221 | undoSpan := Span{e.span[0], e.span[0] + e.size} 222 | undoSrc := buf.runes.Reader(e.span[0]) 223 | undoSrc = runes.LimitReader(undoSrc, e.span.Size()) 224 | if _, err := buf.undo.append(buf.seq, undoSpan, undoSrc); err != nil { 225 | return err 226 | } 227 | } 228 | 229 | all := Span{0, -1} 230 | for e := logLast(buf.redo); e != start.prev(); e = e.prev() { 231 | all[0] = e.span[0] 232 | if all[1] < 0 { 233 | all[1] = e.span[0] + e.size 234 | } else { 235 | all[1] += e.size - e.span.Size() 236 | } 237 | if err := buf.change(e.span, e.data()); err != nil { 238 | return err 239 | } 240 | } 241 | 242 | buf.marks['.'] = all 243 | marks0 = buf.marks 244 | buf.seq++ 245 | return start.pop() 246 | } 247 | 248 | // A log holds a record of changes made to a buffer. 249 | // It consists of an unbounded number of entries. 250 | // Each entry has a header and zero or more runes of data. 251 | // The header contains the Span of the change 252 | // in the original unchanged buffer. 253 | // The header also contains the size of the data, 254 | // and a meta fields used for navigating the log. 255 | // The data following the header is a sequence of runes 256 | // which the change uses to replace the runes 257 | // in the string addressed in the header. 258 | type log struct { 259 | buf *runes.Buffer 260 | // Last is the offset of the last header in the log. 261 | last int64 262 | } 263 | 264 | func newLog() *log { return &log{buf: runes.NewBuffer(1 << 12)} } 265 | 266 | func (l *log) close() error { return l.buf.Close() } 267 | 268 | func (l *log) reset() { 269 | l.last = 0 270 | l.buf.Reset() 271 | } 272 | 273 | type header struct { 274 | // Prev is the offset into the log 275 | // of the beginning of the previous entry's header. 276 | // If there is no previous entry, prev is -1. 277 | prev int64 278 | // Span is the original Span that changed. 279 | span Span 280 | // Size is the new size of the address after the change. 281 | // The header is followed by size runes containing 282 | // the new contents of the changed address. 283 | size int64 284 | // Seq is a sequence number that uniqely identifies 285 | // the edit that made this change. 286 | seq int32 287 | } 288 | 289 | const headerRunes = 9 290 | 291 | func (h *header) marshal() []rune { 292 | var rs [headerRunes]int32 293 | rs[0] = int32(h.prev >> 32) 294 | rs[1] = int32(h.prev & 0xFFFFFFFF) 295 | rs[2] = int32(h.span[0] >> 32) 296 | rs[3] = int32(h.span[0] & 0xFFFFFFFF) 297 | rs[4] = int32(h.span[1] >> 32) 298 | rs[5] = int32(h.span[1] & 0xFFFFFFFF) 299 | rs[6] = int32(h.size >> 32) 300 | rs[7] = int32(h.size & 0xFFFFFFFF) 301 | rs[8] = h.seq 302 | return rs[:] 303 | } 304 | 305 | func (h *header) unmarshal(data []rune) { 306 | if len(data) < headerRunes { 307 | panic("bad log") 308 | } 309 | h.prev = int64(data[0])<<32 | int64(data[1]) 310 | h.span[0] = int64(data[2])<<32 | int64(data[3]) 311 | h.span[1] = int64(data[4])<<32 | int64(data[5]) 312 | h.size = int64(data[6])<<32 | int64(data[7]) 313 | h.seq = data[8] 314 | } 315 | 316 | func (l *log) append(seq int32, s Span, src runes.Reader) (int64, error) { 317 | prev := l.last 318 | l.last = l.buf.Size() 319 | n, err := runes.Copy(l.buf.Writer(l.last), src) 320 | if err != nil { 321 | return n, err 322 | } 323 | // Insert the header before the data. 324 | h := header{ 325 | prev: prev, 326 | span: s, 327 | size: n, 328 | seq: seq, 329 | } 330 | return n, l.buf.Insert(h.marshal(), l.last) 331 | } 332 | 333 | type entry struct { 334 | l *log 335 | header 336 | offs int64 337 | err error 338 | } 339 | 340 | // LogFirst returns the first entry in the log. 341 | // If the log is empty, logFirst returns the dummy end entry. 342 | func logFirst(l *log) entry { return logAt(l, 0) } 343 | 344 | // LogLast returns the last entry in the log. 345 | // If the log is empty, logLast returns the dummy end entry. 346 | func logLast(l *log) entry { return logAt(l, l.last) } 347 | 348 | // LogAt returns the entry at the given log offset. 349 | // If the log is empty, logAt returns the dummy end entry. 350 | func logAt(l *log, offs int64) entry { 351 | if l.buf.Size() == 0 { 352 | return entry{l: l, offs: -1} 353 | } 354 | it := entry{l: l, offs: offs} 355 | it.load() 356 | return it 357 | } 358 | 359 | // LogLastFrame returns the start of the last frame on the log. 360 | // A frame is a sequence of log entries with the same seq. 361 | // If the log is empty, logLastFrame returns the dummy end entry. 362 | func logLastFrame(l *log) entry { 363 | e := logLast(l) 364 | for !e.end() { 365 | prev := e.prev() 366 | if prev.end() || prev.seq != e.seq { 367 | break 368 | } 369 | e = prev 370 | } 371 | return e 372 | } 373 | 374 | // End returns whether the entry is the dummy end entry. 375 | func (e entry) end() bool { return e.offs < 0 } 376 | 377 | // Next returns the next entry in the log. 378 | // Calling next on the dummy end entry returns 379 | // the first entry in the log. 380 | // On error, the end entry is returned with the error set. 381 | // Calling next on an entry with the error set returns 382 | // the same entry. 383 | func (e entry) next() entry { 384 | switch { 385 | case e.err != nil: 386 | return entry{l: e.l, offs: -1, err: e.err} 387 | case e.end(): 388 | return logFirst(e.l) 389 | case e.offs == e.l.last: 390 | e.offs = -1 391 | default: 392 | e.offs += headerRunes + e.size 393 | } 394 | e.load() 395 | return e 396 | } 397 | 398 | // Prev returns the previous entry in the log. 399 | // Calling prev on the dummy end entry returns 400 | // the last entry in the log. 401 | // On error, the end entry is returned with the error set. 402 | // Calling prev on an entry with the error set returns 403 | // the same entry. 404 | func (e entry) prev() entry { 405 | switch { 406 | case e.err != nil: 407 | return entry{l: e.l, offs: -1, err: e.err} 408 | case e.end(): 409 | return logLast(e.l) 410 | case e.offs == 0: 411 | e.offs = -1 412 | default: 413 | e.offs = e.header.prev 414 | } 415 | e.load() 416 | return e 417 | } 418 | 419 | // Load loads the entry's header. 420 | // If the log is empty, loading an entry 421 | // makes the entry the dummy end entry. 422 | func (e *entry) load() { 423 | if e.err != nil || e.offs < 0 { 424 | return 425 | } 426 | var data []rune 427 | data, e.err = e.l.buf.Read(headerRunes, e.offs) 428 | if e.err != nil { 429 | e.offs = -1 430 | } else { 431 | e.header.unmarshal(data) 432 | } 433 | } 434 | 435 | // Store stores the entry's header back to the log. 436 | // Store does nothing on the end entry 437 | // or an entry with its error set 438 | func (e *entry) store() error { 439 | if e.err != nil || e.offs < 0 { 440 | return nil 441 | } 442 | if err := e.l.buf.Delete(headerRunes, e.offs); err != nil { 443 | return err 444 | } 445 | return e.l.buf.Insert(e.header.marshal(), e.offs) 446 | } 447 | 448 | // Data returns the entry's data. 449 | func (e entry) data() runes.Reader { 450 | if e.err != nil { 451 | panic("data called on the end iterator") 452 | } 453 | from := e.offs + headerRunes 454 | return runes.LimitReader(e.l.buf.Reader(from), e.size) 455 | } 456 | 457 | // Pop removes this entry and all following it from the log. 458 | // Popping the end entry is a no-op. 459 | func (e entry) pop() error { 460 | if e.end() { 461 | return nil 462 | } 463 | l := e.l 464 | if p := e.prev(); p.end() { 465 | l.last = 0 466 | } else { 467 | l.last = p.offs 468 | } 469 | return e.l.buf.Delete(l.buf.Size()-e.offs, e.offs) 470 | } 471 | -------------------------------------------------------------------------------- /edit/runes/buffer.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2015, The T Authors. 2 | 3 | package runes 4 | 5 | import ( 6 | "encoding/binary" 7 | "errors" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "strconv" 12 | ) 13 | 14 | // RuneBytes is the number of bytes in Go's rune type. 15 | const runeBytes = 4 16 | 17 | // A Buffer is an unbounded rune buffer backed by a file. 18 | type Buffer struct { 19 | // F is the file that backs the buffer. It is created lazily. 20 | f ReaderWriterAt 21 | // BlockSize is the maximum number of runes in a block. 22 | blockSize int 23 | // Blocks contains all blocks of the buffer in order. 24 | // Free contains blocks that are free to be re-allocated. 25 | blocks, free []block 26 | // End is the byte offset of the end of the backing file. 27 | end int64 28 | 29 | // Cache is the index of the block whose data is currently cached. 30 | cached int 31 | // Cached0 is the address of the first rune in the cached block. 32 | cached0 int64 33 | // Cache is the cached data. 34 | cache []rune 35 | // Dirty tracks whether the cached data has changed since it was read. 36 | dirty bool 37 | 38 | // Size is the number of runes in the buffer. 39 | size int64 40 | } 41 | 42 | // A ReaderWriterAt implements the io.ReaderAt and io.WriterAt interfaces. 43 | type ReaderWriterAt interface { 44 | io.ReaderAt 45 | io.WriterAt 46 | } 47 | 48 | // A block describes a portion of the buffer and its location in the backing file. 49 | type block struct { 50 | // Start is the byte offset of the block in the file. 51 | start int64 52 | // N is the number of runes in the block. 53 | n int 54 | } 55 | 56 | // NewBuffer returns a new, empty buffer. 57 | // No more than blockSize runes are cached in memory. 58 | func NewBuffer(blockSize int) *Buffer { 59 | return &Buffer{ 60 | blockSize: blockSize, 61 | cached: -1, 62 | cache: make([]rune, blockSize), 63 | } 64 | } 65 | 66 | // NewBufferReaderWriterAt is like NewBuffer but uses 67 | // the given ReaderWriterAt as its backing store. 68 | // If the ReaderWriterAt implements io.Closer, it is closed when the buffer is closed. 69 | // If the ReaderWriterAt is an *os.File, the file is removed when the buffer is closed. 70 | func NewBufferReaderWriterAt(blockSize int, f ReaderWriterAt) *Buffer { 71 | b := NewBuffer(blockSize) 72 | b.f = f 73 | return b 74 | } 75 | 76 | // Close closes the buffer and removes it's backing file. 77 | func (b *Buffer) Close() error { 78 | b.cache = nil 79 | switch f := b.f.(type) { 80 | case *os.File: 81 | path := f.Name() 82 | if err := f.Close(); err != nil { 83 | return err 84 | } 85 | return os.Remove(path) 86 | case io.Closer: 87 | return f.Close() 88 | default: 89 | return nil 90 | } 91 | } 92 | 93 | // Size returns the number of runes in the buffer. 94 | func (b *Buffer) Size() int64 { return b.size } 95 | 96 | // Rune returns the rune at the given offset. 97 | // If the rune is out of range it panics. 98 | func (b *Buffer) Rune(offs int64) (rune, error) { 99 | if offs < 0 || offs >= b.Size() { 100 | panic("rune index out of bounds") 101 | } 102 | if q0 := b.cached0; b.cached >= 0 && q0 <= offs && offs < q0+int64(b.blocks[b.cached].n) { 103 | return b.cache[offs-q0], nil 104 | } 105 | i, q0 := b.blockAt(offs) 106 | if _, err := b.get(i); err != nil { 107 | return -1, err 108 | } 109 | return b.cache[offs-q0], nil 110 | } 111 | 112 | // Read reads runes from the buffer beginning at a given offset. 113 | // It is an error to read out of the range of the buffer. 114 | func (b *Buffer) Read(n int, offs int64) ([]rune, error) { 115 | r := b.Reader(offs) 116 | r = LimitReader(r, int64(n)) 117 | return ReadAll(r) 118 | } 119 | 120 | type reader struct { 121 | *Buffer 122 | pos int64 123 | } 124 | 125 | // Len returns the number of runes in the unread portion of the reader. 126 | func (r *reader) Len() int64 { return r.Size() - r.pos } 127 | 128 | // Reader returns a Reader that reads from the Buffer 129 | // beginning at the given offset. 130 | // The returned Reader need not be closed. 131 | // The Buffer must not be modified 132 | // between Read calls on the returned Reader. 133 | func (b *Buffer) Reader(offs int64) Reader { return &reader{Buffer: b, pos: offs} } 134 | 135 | func (r *reader) Read(p []rune) (int, error) { 136 | if r.pos < 0 || r.pos > r.Size() { 137 | return 0, os.ErrInvalid 138 | } 139 | if r.pos == r.Size() { 140 | return 0, io.EOF 141 | } 142 | i, blkStart := r.blockAt(r.pos) 143 | blk, err := r.get(i) 144 | if err != nil { 145 | return 0, err 146 | } 147 | cacheOffs := int(r.pos - blkStart) 148 | n := copy(p, r.cache[cacheOffs:blk.n]) 149 | r.pos += int64(n) 150 | return n, nil 151 | } 152 | 153 | // Insert inserts runes into the buffer at the given offset. 154 | // It is an error to insert at a point than is out of the range of the buffer. 155 | func (b *Buffer) Insert(p []rune, offs int64) error { 156 | _, err := b.Writer(offs).Write(p) 157 | return err 158 | } 159 | 160 | // Writer returns a Writer that inserts into the Buffer 161 | // beginning at the given offset. 162 | // The returned Writer need not be closed. 163 | func (b *Buffer) Writer(offs int64) Writer { return &writer{Buffer: b, pos: offs} } 164 | 165 | type writer struct { 166 | *Buffer 167 | pos int64 168 | } 169 | 170 | func (w *writer) Write(p []rune) (int, error) { 171 | n, err := w.ReadFrom(SliceReader(p)) 172 | w.pos += n 173 | return int(n), err 174 | } 175 | 176 | func (w *writer) ReadFrom(r Reader) (int64, error) { 177 | return w.Buffer.ReaderFrom(w.pos).ReadFrom(r) 178 | } 179 | 180 | // ReaderFrom returns a ReaderFrom that inserts into the Buffer 181 | // beginning at the given offset. 182 | func (b *Buffer) ReaderFrom(offs int64) ReaderFrom { 183 | return &readerFrom{Buffer: b, pos: offs} 184 | } 185 | 186 | type readerFrom struct { 187 | *Buffer 188 | pos int64 189 | } 190 | 191 | func (dst *readerFrom) ReadFrom(r Reader) (int64, error) { 192 | if dst.pos < 0 || dst.pos > dst.Size() { 193 | return 0, os.ErrInvalid 194 | } 195 | if sz, ok := readLen(r); ok { 196 | return fastReadFrom(dst, r, sz) 197 | } 198 | return slowCopy(dst.Buffer.Writer(dst.pos), r) 199 | } 200 | 201 | func fastReadFrom(dst *readerFrom, r Reader, sz int64) (int64, error) { 202 | var tot int64 203 | for tot < sz { 204 | p, err := dst.makeSpace(sz-tot, dst.pos+tot) 205 | if err != nil { 206 | return tot, err 207 | } 208 | n, err := readFull(r, p) 209 | tot += int64(n) 210 | if err != nil { 211 | return tot, err 212 | } 213 | } 214 | var more [1]rune 215 | switch n, err := r.Read(more[:]); { 216 | case n > 0: 217 | panic("more to read") 218 | case err == io.EOF: 219 | return tot, nil 220 | default: 221 | return tot, err 222 | } 223 | } 224 | 225 | func readFull(r Reader, p []rune) (int, error) { 226 | var tot int 227 | for tot < len(p) { 228 | n, err := r.Read(p[tot:]) 229 | tot += n 230 | if err != nil { 231 | if err == io.EOF { 232 | panic("not enough to read") 233 | } 234 | return tot, err 235 | } 236 | } 237 | return tot, nil 238 | } 239 | 240 | // ReadLen returns the number of runes in the unread portion of the reader, 241 | // or false if the number cannot be determined. 242 | func readLen(r Reader) (int64, bool) { 243 | switch r := r.(type) { 244 | case *limitedReader: 245 | if sz, ok := readLen(r.r); ok { 246 | if r.limit < sz { 247 | return r.limit, true 248 | } 249 | return sz, true 250 | } 251 | 252 | case interface { 253 | Len() int64 254 | }: 255 | return r.Len(), true 256 | } 257 | return 0, false 258 | } 259 | 260 | // MakeSpace makes space in the buffer 261 | // for up to n runes at the given address 262 | // and returns a slice of the cache 263 | // corresponding to the space. 264 | func (b *Buffer) makeSpace(n, at int64) ([]rune, error) { 265 | var i int 266 | var blkStart int64 267 | switch { 268 | case len(b.blocks) == 0: 269 | // Insert the initial block. 270 | var err error 271 | i, err = b.insertAt(at) 272 | if err != nil { 273 | return nil, err 274 | } 275 | case at == b.Size(): 276 | // Try to extend the last block if we are inserting on the end. 277 | for _, blk := range b.blocks[:len(b.blocks)-1] { 278 | blkStart += int64(blk.n) 279 | } 280 | i = len(b.blocks) - 1 281 | default: 282 | i, blkStart = b.blockAt(at) 283 | } 284 | blk, err := b.get(i) 285 | if err != nil { 286 | return nil, err 287 | } 288 | blkSpace := b.blockSize - blk.n 289 | if blkSpace == 0 { 290 | if i, err = b.insertAt(at); err != nil { 291 | return nil, err 292 | } 293 | if blk, err = b.get(i); err != nil { 294 | return nil, err 295 | } 296 | blkStart = at 297 | blkSpace = b.blockSize 298 | } 299 | if n < int64(blkSpace) { 300 | blkSpace = int(n) 301 | } 302 | cacheOffs := int(at - blkStart) 303 | copy(b.cache[cacheOffs+blkSpace:], b.cache[cacheOffs:blk.n]) 304 | b.dirty = true 305 | blk.n += blkSpace 306 | b.size += int64(blkSpace) 307 | return b.cache[cacheOffs : cacheOffs+blkSpace], nil 308 | } 309 | 310 | // Delete deletes runes from the buffer starting at the given offset. 311 | // It is an error to delete out of the range of the buffer. 312 | func (b *Buffer) Delete(n, offs int64) error { 313 | if n < 0 { 314 | panic("bad count: " + strconv.FormatInt(n, 10)) 315 | } 316 | if offs < 0 || offs+n > b.Size() { 317 | return errors.New("invalid offset: " + strconv.FormatInt(offs, 10)) 318 | } 319 | for n > 0 { 320 | i, q0 := b.blockAt(offs) 321 | blk, err := b.get(i) 322 | if err != nil { 323 | return err 324 | } 325 | o := int(offs - q0) 326 | m := blk.n - o 327 | if int64(m) > n { 328 | m = int(n) 329 | } 330 | if o == 0 && n >= int64(blk.n) { 331 | // Remove the entire block. 332 | b.freeBlock(*blk) 333 | b.blocks = append(b.blocks[:i], b.blocks[i+1:]...) 334 | b.cached0 = -1 335 | b.cached = -1 336 | } else { 337 | // Remove a portion of the block. 338 | copy(b.cache[o:], b.cache[o+m:]) 339 | b.dirty = true 340 | blk.n -= m 341 | } 342 | n -= int64(m) 343 | b.size -= int64(m) 344 | } 345 | return nil 346 | } 347 | 348 | // Reset resets the buffer to empty. 349 | func (b *Buffer) Reset() { 350 | for _, blk := range b.blocks { 351 | b.freeBlock(blk) 352 | } 353 | b.blocks = b.blocks[:0] 354 | b.cached = -1 355 | b.size = 0 356 | } 357 | 358 | func (b *Buffer) allocBlock() block { 359 | if l := len(b.free); l > 0 { 360 | blk := b.free[l-1] 361 | b.free = b.free[:l-1] 362 | return blk 363 | } 364 | blk := block{start: b.end} 365 | b.end += int64(b.blockSize * runeBytes) 366 | return blk 367 | } 368 | 369 | func (b *Buffer) freeBlock(blk block) { 370 | b.free = append(b.free, block{start: blk.start}) 371 | } 372 | 373 | // BlockAt returns the index and start address of the block containing the address. 374 | // BlockAt panics if the address is not within the range of the buffer. 375 | func (b *Buffer) blockAt(at int64) (int, int64) { 376 | if at < 0 || at >= b.Size() { 377 | panic("invalid offset: " + strconv.FormatInt(at, 10)) 378 | } 379 | var q0 int64 380 | for i, blk := range b.blocks { 381 | if q0 <= at && at < q0+int64(blk.n) { 382 | return i, q0 383 | } 384 | q0 += int64(blk.n) 385 | } 386 | panic("impossible") 387 | } 388 | 389 | // insertAt inserts a block at the address and returns the new block's index. 390 | // If a block contains the address then it is split. 391 | func (b *Buffer) insertAt(at int64) (int, error) { 392 | if at == b.Size() { 393 | b.blocks = append(b.blocks, b.allocBlock()) 394 | return len(b.blocks) - 1, nil 395 | } 396 | 397 | i, q0 := b.blockAt(at) 398 | o := int(at - q0) 399 | blk := b.blocks[i] 400 | if at == q0 { 401 | // Adding immediately before blk, no need to split. 402 | nblk := b.allocBlock() 403 | b.blocks = append(b.blocks[:i], append([]block{nblk}, b.blocks[i:]...)...) 404 | if b.cached == i { 405 | b.cached = i + 1 406 | } 407 | return i, nil 408 | } 409 | 410 | // Splitting blk. 411 | // Make sure it's both on disk and in the cache. 412 | if b.cached == i && b.dirty { 413 | if err := b.put(); err != nil { 414 | return -1, err 415 | } 416 | } else if _, err := b.get(i); err != nil { 417 | return -1, err 418 | } 419 | 420 | // Resize blk. 421 | b.blocks[i].n = int(o) 422 | 423 | // Insert the new, empty block. 424 | nblk := b.allocBlock() 425 | b.blocks = append(b.blocks[:i+1], append([]block{nblk}, b.blocks[i+1:]...)...) 426 | 427 | // Allocate a block for the second half of blk and set it as the cache. 428 | // The next put will write it out. 429 | nblk = b.allocBlock() 430 | b.blocks = append(b.blocks[:i+2], append([]block{nblk}, b.blocks[i+2:]...)...) 431 | b.blocks[i+2].n = blk.n - o 432 | copy(b.cache, b.cache[o:]) 433 | b.cached = i + 2 434 | b.dirty = true 435 | 436 | return i + 1, nil 437 | } 438 | 439 | // File returns an *os.File, creating a new file if one is not created yet. 440 | func (b *Buffer) file() (ReaderWriterAt, error) { 441 | if b.f == nil { 442 | f, err := ioutil.TempFile(os.TempDir(), "edit") 443 | if err != nil { 444 | return nil, err 445 | } 446 | b.f = f 447 | } 448 | return b.f, nil 449 | } 450 | 451 | // Put writes the cached block back to the file. 452 | func (b *Buffer) put() error { 453 | if b.cached < 0 || !b.dirty || len(b.cache) == 0 { 454 | return nil 455 | } 456 | blk := b.blocks[b.cached] 457 | f, err := b.file() 458 | if err != nil { 459 | return err 460 | } 461 | bs := make([]byte, blk.n*runeBytes) 462 | for i, r := range b.cache[:blk.n] { 463 | binary.LittleEndian.PutUint32(bs[i*runeBytes:], uint32(r)) 464 | } 465 | if _, err := f.WriteAt(bs, blk.start); err != nil { 466 | return err 467 | } 468 | b.dirty = false 469 | return nil 470 | } 471 | 472 | // Get loads the cache with the data from the block at the given index, 473 | // returning a pointer to it. 474 | func (b *Buffer) get(i int) (*block, error) { 475 | if b.cached == i { 476 | return &b.blocks[i], nil 477 | } 478 | if err := b.put(); err != nil { 479 | return nil, err 480 | } 481 | 482 | blk := b.blocks[i] 483 | f, err := b.file() 484 | if err != nil { 485 | return nil, err 486 | } 487 | bs := make([]byte, blk.n*runeBytes) 488 | if _, err := f.ReadAt(bs, blk.start); err != nil { 489 | if err == io.EOF { 490 | panic("unexpected EOF") 491 | } 492 | return nil, err 493 | } 494 | j := 0 495 | for len(bs) > 0 { 496 | b.cache[j] = rune(binary.LittleEndian.Uint32(bs)) 497 | bs = bs[runeBytes:] 498 | j++ 499 | } 500 | b.cached = i 501 | b.dirty = false 502 | b.cached0 = 0 503 | for j := 0; j < i; j++ { 504 | b.cached0 += int64(b.blocks[j].n) 505 | } 506 | return &b.blocks[i], nil 507 | } 508 | -------------------------------------------------------------------------------- /ui/window.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package ui 4 | 5 | import ( 6 | "bufio" 7 | "image" 8 | "image/color" 9 | "image/draw" 10 | "io" 11 | "log" 12 | "os" 13 | "os/exec" 14 | "strings" 15 | "time" 16 | 17 | "github.com/eaburns/T/edit" 18 | "golang.org/x/exp/shiny/screen" 19 | "golang.org/x/image/font" 20 | "golang.org/x/mobile/event/key" 21 | "golang.org/x/mobile/event/lifecycle" 22 | "golang.org/x/mobile/event/mouse" 23 | "golang.org/x/mobile/event/paint" 24 | "golang.org/x/mobile/event/size" 25 | ) 26 | 27 | // A handler is an interactive portion of a window 28 | // that can receive keyboard and mouse events. 29 | // 30 | // Handlers gain focus when the mouse hovers over them 31 | // and they maintain focus until the mouse moves off of them. 32 | // However, during a mouse drag event, 33 | // when the pointer moves while a button is held, 34 | // the handler maintains focus 35 | // even if the pointer moves off of the handler. 36 | type handler interface { 37 | // ChangeFocus is called when the focus of the handler changes. 38 | // If the handler is coming into focus, 39 | // then the bool argument is true, 40 | // otherwise it is false. 41 | // The window always redraws on a focus change event. 42 | changeFocus(*window, bool) 43 | 44 | // Tick is called whenever the window considers redrawing. 45 | // This occurs at almost-regular intervals/ 46 | // deponding on how long the window required 47 | // to draw its previous frame. 48 | // The return value is whether to redraw the window. 49 | tick(*window) bool 50 | 51 | // Key is called if the handler is in forcus 52 | // and the window receives a keyboard event. 53 | // The return value is whether to redraw the window. 54 | key(*window, key.Event) bool 55 | 56 | // Mouse is called if the handler is in focus 57 | // and the window receives a mouse event. 58 | // The return value is whether to redraw the window. 59 | mouse(*window, mouse.Event) bool 60 | 61 | // DrawLast is called if the handler is in focus 62 | // while the window is redrawn. 63 | // It is always called after everything else on the window has been drawn. 64 | // 65 | // TODO(eaburns): textbox is a handler, but doesn't implement this. 66 | // Instead, drawLast should be a separate interface, 67 | // and only used for handlers that implement it. 68 | drawLast(scr screen.Screen, win screen.Window) 69 | } 70 | 71 | const ( 72 | minFrameWidth = 20 // px 73 | borderWidth = 1 // px 74 | ) 75 | 76 | var borderColor = color.Black 77 | 78 | const ( 79 | ptPerInch = 72 80 | defaultDPI = 96 81 | ) 82 | 83 | type window struct { 84 | id string 85 | server *Server 86 | screen.Window 87 | face font.Face 88 | dpi float64 89 | image.Rectangle 90 | 91 | columns []*column 92 | xs []float64 93 | 94 | inFocus handler 95 | p image.Point 96 | } 97 | 98 | func newWindow(id string, s *Server, size image.Point) (*window, error) { 99 | win, err := s.screen.NewWindow(&screen.NewWindowOptions{ 100 | Width: size.X, 101 | Height: size.Y, 102 | }) 103 | if err != nil { 104 | return nil, err 105 | } 106 | w := &window{ 107 | id: id, 108 | server: s, 109 | Window: win, 110 | Rectangle: image.Rect(0, 0, size.X, size.Y), 111 | 112 | // dpi is set to the true value by a size.Event. 113 | dpi: defaultDPI, 114 | } 115 | w.getDPI() 116 | c, err := newColumn(w) 117 | if err != nil { 118 | win.Release() 119 | w.face.Close() 120 | return nil, err 121 | } 122 | w.addColumn(0.0, c) 123 | go w.events() 124 | return w, nil 125 | } 126 | 127 | // GetDPI reads and discards events until a size.Event, from which the DPI is read. 128 | func (w *window) getDPI() { 129 | for { 130 | switch e := w.NextEvent().(type) { 131 | case size.Event: 132 | w.dpi = float64(e.PixelsPerPt * ptPerInch) 133 | w.face = newFace(w.dpi) 134 | return 135 | } 136 | } 137 | } 138 | 139 | type closeEvent struct{} 140 | 141 | func (w *window) events() { 142 | events := make(chan interface{}) 143 | go func() { 144 | for { 145 | e := w.NextEvent() 146 | if _, ok := e.(closeEvent); ok { 147 | close(events) 148 | return 149 | } 150 | events <- e 151 | } 152 | }() 153 | 154 | const drawTime = 33 * time.Millisecond 155 | timer := time.NewTimer(drawTime) 156 | defer timer.Stop() 157 | 158 | var click int 159 | var redraw bool 160 | for { 161 | select { 162 | case <-timer.C: 163 | if w.inFocus != nil && w.inFocus.tick(w) { 164 | redraw = true 165 | } 166 | if !redraw { 167 | timer.Reset(drawTime) 168 | break 169 | } 170 | w.draw(w.server.screen, w.Window) 171 | if w.inFocus != nil { 172 | w.inFocus.drawLast(w.server.screen, w.Window) 173 | } 174 | w.Publish() 175 | timer.Reset(drawTime) 176 | redraw = false 177 | 178 | case e, ok := <-events: 179 | if !ok { 180 | for _, c := range w.columns { 181 | c.close() 182 | } 183 | // TODO(eaburns): Don't call this if the frame is not detached. 184 | if f, ok := w.inFocus.(frame); ok { 185 | f.close() 186 | } 187 | w.face.Close() 188 | w.Release() 189 | return 190 | } 191 | switch e := e.(type) { 192 | case func(): 193 | e() 194 | redraw = true 195 | 196 | case lifecycle.Event: 197 | if e.To == lifecycle.StageDead { 198 | w.server.delWin(w.id) 199 | } 200 | 201 | case paint.Event: 202 | redraw = true 203 | 204 | case size.Event: 205 | w.setBoundsAfterResize(image.Rectangle{Max: e.Size()}) 206 | 207 | case key.Event: 208 | if w.inFocus != nil && w.inFocus.key(w, e) { 209 | redraw = true 210 | } 211 | 212 | case mouse.Event: 213 | var dir mouse.Direction 214 | w.p, dir = image.Pt(int(e.X), int(e.Y)), e.Direction 215 | switch dir { 216 | case mouse.DirPress: 217 | click++ 218 | case mouse.DirRelease: 219 | click-- 220 | } 221 | if dir == mouse.DirNone && click == 0 && w.refocus() { 222 | redraw = true 223 | } 224 | if w.inFocus != nil { 225 | if w.inFocus.mouse(w, e) { 226 | redraw = true 227 | } 228 | } 229 | // After sending a press or release to the focus, 230 | // check whether it's still in focus. 231 | if dir != mouse.DirNone && w.refocus() { 232 | redraw = true 233 | } 234 | } 235 | } 236 | } 237 | } 238 | 239 | func (w *window) close() { 240 | w.Send(closeEvent{}) 241 | } 242 | 243 | func (w *window) refocus() bool { 244 | prev := w.inFocus 245 | for _, c := range w.columns { 246 | if w.p.In(c.bounds()) { 247 | w.inFocus = c.focus(w.p) 248 | break 249 | } 250 | } 251 | if prev == w.inFocus { 252 | return false 253 | } 254 | if prev != nil { 255 | prev.changeFocus(w, false) 256 | } 257 | if w.inFocus != nil { 258 | w.inFocus.changeFocus(w, true) 259 | } 260 | return true 261 | } 262 | 263 | func (w *window) bounds() image.Rectangle { return w.Rectangle } 264 | 265 | func (w *window) setBounds(bounds image.Rectangle) { 266 | w.Rectangle = bounds 267 | width := float64(bounds.Dx()) 268 | for i := len(w.columns) - 1; i >= 0; i-- { 269 | c := w.columns[i] 270 | b := bounds 271 | if i > 0 { 272 | b.Min.X = bounds.Min.X + int(width*w.xs[i]) 273 | } 274 | if i < len(w.columns)-1 { 275 | b.Max.X = w.columns[i+1].bounds().Min.X - borderWidth 276 | } 277 | c.setBounds(b) 278 | } 279 | } 280 | 281 | func (w *window) setBoundsAfterResize(bounds image.Rectangle) { 282 | w.Rectangle = bounds 283 | width := float64(bounds.Dx()) 284 | for i := len(w.columns) - 1; i >= 0; i-- { 285 | c := w.columns[i] 286 | b := bounds 287 | if i > 0 { 288 | b.Min.X = bounds.Min.X + int(width*w.xs[i]) 289 | } 290 | if i < len(w.columns)-1 { 291 | b.Max.X = w.columns[i+1].bounds().Min.X - borderWidth 292 | } 293 | c.setAfterResizeBounds(b) 294 | } 295 | } 296 | 297 | func (w *window) draw(scr screen.Screen, win screen.Window) { 298 | for i, c := range w.columns { 299 | c.draw(scr, win) 300 | if i == len(w.columns)-1 { 301 | continue 302 | } 303 | d := w.columns[i+1] 304 | b := w.bounds() 305 | b.Min.X = c.bounds().Max.X 306 | b.Max.X = d.bounds().Min.X 307 | win.Fill(b, borderColor, draw.Over) 308 | } 309 | } 310 | 311 | // AddFrame adds the frame to the last column of the window. 312 | func (w *window) addFrame(f frame) { 313 | c := w.columns[len(w.columns)-1] 314 | var y int 315 | if len(w.columns) == 1 && len(c.frames) == 1 { 316 | y = minHeight(w.columns[0].frames[0].(*columnTag).text.opts) 317 | } 318 | if len(c.frames) > 1 { 319 | f := c.frames[len(c.frames)-1] 320 | b := f.bounds() 321 | y = b.Min.Y + b.Dy()/2 322 | } 323 | c.addFrame(float64(y)/float64(c.Dy()), f) 324 | } 325 | 326 | func (w *window) deleteFrame(f frame) { 327 | for _, c := range w.columns { 328 | for _, g := range c.frames { 329 | if g == f { 330 | c.removeFrame(f) 331 | } 332 | } 333 | } 334 | if h := f.(handler); h == w.inFocus { 335 | w.refocus() 336 | } 337 | f.close() 338 | } 339 | 340 | func (w *window) deleteColumn(c *column) { 341 | if w.removeColumn(c) { 342 | c.close() 343 | } 344 | } 345 | 346 | func (w *window) removeColumn(c *column) bool { 347 | if len(w.columns) < 2 { 348 | return false 349 | } 350 | i := columnIndex(w, c) 351 | if i < 0 { 352 | return false 353 | } 354 | w.columns = append(w.columns[:i], w.columns[i+1:]...) 355 | w.xs = append(w.xs[:i], w.xs[i+1:]...) 356 | w.setBounds(w.bounds()) 357 | c.win = nil 358 | return true 359 | } 360 | 361 | func columnIndex(w *window, c *column) int { 362 | for i := range w.columns { 363 | if w.columns[i] == c { 364 | return i 365 | } 366 | } 367 | return -1 368 | } 369 | 370 | // AddCol adds a column to the window such that its left side at pixel xfrac*w.Dx(). 371 | // However, if the window has no columns, its left side is always at 0.0. 372 | func (w *window) addColumn(xfrac float64, c *column) bool { 373 | if len(w.columns) == 0 { 374 | w.columns = []*column{c} 375 | w.xs = []float64{0.0} 376 | c.win = w 377 | c.setBounds(w.bounds()) 378 | return true 379 | } 380 | x := int(float64(w.Dx()) * xfrac) 381 | i, splitCol := columnAt(w, x) 382 | 383 | // Push away from the window edges. 384 | if x < minFrameWidth { 385 | x = minFrameWidth 386 | xfrac = float64(x) / float64(w.Dx()) 387 | } 388 | if max := w.Dx() - minFrameWidth - borderWidth; x > max { 389 | x = max 390 | xfrac = float64(x) / float64(w.Dx()) 391 | } 392 | 393 | if leftSize := x - splitCol.Min.X; leftSize < minFrameWidth { 394 | if !slideLeft(w, i, minFrameWidth-leftSize) { 395 | x += minFrameWidth - leftSize 396 | xfrac = float64(x) / float64(w.Dx()) 397 | } 398 | } 399 | if rightSize := splitCol.Max.X - x - borderWidth; rightSize < minFrameWidth { 400 | if !slideRight(w, i, minFrameWidth-rightSize) { 401 | return false 402 | } 403 | } 404 | 405 | w.columns = append(w.columns, nil) 406 | if i+2 < len(w.columns) { 407 | copy(w.columns[i+2:], w.columns[i+1:]) 408 | } 409 | w.columns[i+1] = c 410 | 411 | w.xs = append(w.xs, 0) 412 | if i+2 < len(w.xs) { 413 | copy(w.xs[i+2:], w.xs[i+1:]) 414 | } 415 | w.xs[i+1] = xfrac 416 | 417 | c.win = w 418 | w.setBounds(w.bounds()) 419 | return true 420 | } 421 | 422 | // ColumnAt returns the column containing pixel column x. 423 | // If x < 0, the left-most column is returned. 424 | // If x > width, the the right-most column is returned. 425 | func columnAt(w *window, x int) (i int, c *column) { 426 | if x < 0 { 427 | return 0, w.columns[0] 428 | } 429 | for i, c = range w.columns { 430 | if c.Max.X > x { 431 | return i, c 432 | } 433 | } 434 | return len(w.columns) - 1, w.columns[len(w.columns)-1] 435 | } 436 | 437 | // TODO(eaburns): slideRight and slideLeft should slide as far as possible. 438 | // Currently, if they can't slide the entire delta, they slide nothing at all. 439 | 440 | func slideLeft(w *window, i int, delta int) bool { 441 | if i <= 0 { 442 | return false 443 | } 444 | x := w.columns[i].Min.X - delta 445 | if sz := x - w.columns[i-1].Min.X; sz < minFrameWidth { 446 | if !slideLeft(w, i-1, minFrameWidth-sz) { 447 | return false 448 | } 449 | } 450 | w.xs[i] = float64(x) / float64(w.Dx()) 451 | return true 452 | } 453 | 454 | func slideRight(w *window, i int, delta int) bool { 455 | if i > len(w.columns)-2 { 456 | return false 457 | } 458 | x := w.columns[i].Max.X + delta 459 | if sz := w.columns[i+1].Max.X - borderWidth - x; sz < minFrameWidth { 460 | if !slideRight(w, i+1, minFrameWidth-sz) { 461 | return false 462 | } 463 | } 464 | w.xs[i+1] = float64(x) / float64(w.Dx()) 465 | return true 466 | } 467 | 468 | // TODO(eaburns): take a *sheet as an optional argument for setting T_SHEET. 469 | func (w *window) exec(commandLine string) { 470 | scanner := bufio.NewScanner(strings.NewReader(commandLine)) 471 | scanner.Split(bufio.ScanWords) 472 | var words []string 473 | for scanner.Scan() { 474 | words = append(words, scanner.Text()) 475 | } 476 | if len(words) == 0 { 477 | return 478 | } 479 | 480 | out, in, err := os.Pipe() 481 | if err != nil { 482 | log.Println("failed to open pipe:", err) 483 | return 484 | } 485 | go pipeOutput(w, out) 486 | 487 | cmd := exec.Command(words[0], words[1:]...) 488 | cmd.Env = os.Environ() 489 | cmd.Env = append(cmd.Env, "T_WINDOW_PATH="+windowPath(w)) 490 | cmd.Stdout = in 491 | cmd.Stderr = in 492 | cmd.Run() 493 | in.Close() 494 | } 495 | 496 | func pipeOutput(w *window, out io.ReadCloser) { 497 | defer out.Close() 498 | var buf [4096]byte 499 | for { 500 | switch n, err := out.Read(buf[:]); { 501 | case err == io.EOF: 502 | return 503 | case err != nil: 504 | log.Println("read error:", err) 505 | return 506 | default: 507 | str := string(buf[:n]) 508 | w.Send(func() { w.output(str) }) 509 | } 510 | } 511 | } 512 | 513 | // Output writes the string to the window's output sheet. 514 | // 515 | // Output must be called in the window's UI goroutine. 516 | func (w *window) output(str string) *sheet { 517 | const outSheetName = "+output" 518 | var out *sheet 519 | w.server.Lock() 520 | for _, s := range w.server.sheets { 521 | if s.win == w && s.tagFileName() == outSheetName { 522 | out = s 523 | break 524 | } 525 | } 526 | if out != nil { 527 | w.server.Unlock() 528 | } else { 529 | var err error 530 | out, err = w.server.newSheet(w, w.server.editorURL) 531 | w.server.Unlock() 532 | if err != nil { 533 | log.Printf("failed to create %s sheet: %v", outSheetName, err) 534 | return nil 535 | } 536 | out.setTagFileName(outSheetName) 537 | w.refocus() 538 | } 539 | out.body.doAsync(edit.Append(edit.End, str)) 540 | return out 541 | } 542 | -------------------------------------------------------------------------------- /ui/text/text_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016, The T Authors. 2 | 3 | package text 4 | 5 | import ( 6 | "bytes" 7 | "image" 8 | "testing" 9 | "unicode" 10 | "unicode/utf8" 11 | 12 | "golang.org/x/image/font" 13 | "golang.org/x/image/math/fixed" 14 | ) 15 | 16 | func TestAdd(t *testing.T) { 17 | opts := Options{ 18 | DefaultStyle: Style{Face: &unitFace{}}, 19 | Size: image.Pt(5, 5), 20 | TabWidth: 2, 21 | } 22 | 23 | tests := []struct { 24 | name string 25 | opts Options 26 | adds []string 27 | want string 28 | }{ 29 | { 30 | name: "nothing added", 31 | opts: opts, 32 | adds: []string{}, 33 | want: "", 34 | }, 35 | { 36 | name: "add empty", 37 | opts: opts, 38 | adds: []string{"", "", ""}, 39 | want: "", 40 | }, 41 | { 42 | name: "single add fits line", 43 | opts: opts, 44 | adds: []string{"12345"}, 45 | want: "[12345]", 46 | }, 47 | { 48 | name: "multi-add fits line", 49 | opts: opts, 50 | adds: []string{"1", "2", "3", "4", "5"}, 51 | want: "[12345]", 52 | }, 53 | { 54 | name: "single add width breaks line", 55 | opts: opts, 56 | adds: []string{"1234567890abcde"}, 57 | want: "[12345][67890][abcde]", 58 | }, 59 | { 60 | name: "multi-add width breaks line", 61 | opts: opts, 62 | adds: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e"}, 63 | want: "[12345][67890][abcde]", 64 | }, 65 | { 66 | name: "newline breaks line", 67 | opts: opts, 68 | adds: []string{"1\n2\n", "3\n4\n5\n"}, 69 | want: "[1\n][2\n][3\n][4\n][5\n]", 70 | }, 71 | { 72 | name: "non-ASCII", 73 | opts: opts, 74 | adds: []string{"αβξδεφγθικ"}, 75 | want: "[αβξδε][φγθικ]", 76 | }, 77 | { 78 | name: "tab", 79 | opts: opts, 80 | adds: []string{"\t\t5", "6\t\t0", "a\t\t\tbreak"}, 81 | want: "[\t\t5][6\t\t0][a\t\t\t][break]", 82 | }, 83 | { 84 | name: "tab no less than space", 85 | opts: Options{ 86 | DefaultStyle: advStyle(map[rune]fixed.Int26_6{ 87 | ' ': fixed.I(2), 88 | 'a': fixed.I(1), 89 | }), 90 | Size: image.Pt(5, 5), 91 | TabWidth: 1, 92 | }, 93 | // Would tab to 1→2, but that is less than 2, so go ahead to 3. 94 | adds: []string{"a\taaaaaa"}, 95 | want: "[a\ta][aaaaa]", 96 | }, 97 | { 98 | name: "replacement rune", 99 | opts: Options{ 100 | DefaultStyle: advStyle(map[rune]fixed.Int26_6{ 101 | unicode.ReplacementChar: fixed.I(1), 102 | }), 103 | Size: image.Pt(5, 5), 104 | }, 105 | // Would tab to 1→2, but that is less than 2, so go ahead to 3. 106 | adds: []string{"1234567890"}, 107 | want: "[12345][67890]", 108 | }, 109 | { 110 | name: "stop adding when max height is exceeded", 111 | opts: Options{ 112 | DefaultStyle: Style{Face: &unitFace{}}, 113 | Size: image.Pt(1, 2), 114 | TabWidth: 2, 115 | }, 116 | adds: []string{"12345", "67890"}, 117 | want: "[1][2][3]", 118 | }, 119 | { 120 | name: "add to empty line doesn't fit", 121 | opts: Options{ 122 | DefaultStyle: Style{Face: &unitFace{}}, 123 | Size: image.Pt(0, 10), 124 | TabWidth: 2, 125 | }, 126 | adds: []string{"12345"}, 127 | want: "[1][2][3][4][5]", 128 | }, 129 | } 130 | 131 | for _, test := range tests { 132 | s := NewSetter(test.opts) 133 | for _, str := range test.adds { 134 | s.Add([]byte(str)) 135 | } 136 | if got := lineString(s.Set()); got != test.want { 137 | t.Errorf("%s s.Set()=%q, want, %q", test.name, got, test.want) 138 | } 139 | } 140 | } 141 | 142 | func TestAddVerticalMetrics(t *testing.T) { 143 | tallHeight, tallAscent := fixed.I(1000), fixed.I(800) 144 | tall := Style{ 145 | Face: &testFace{ 146 | adv: map[rune]fixed.Int26_6{'a': fixed.I(1)}, 147 | height: tallHeight, 148 | ascent: tallAscent, 149 | }, 150 | } 151 | mediumHeight, mediumAscent := fixed.I(100), fixed.I(80) 152 | medium := Style{ 153 | Face: &testFace{ 154 | adv: map[rune]fixed.Int26_6{'a': fixed.I(1)}, 155 | height: mediumHeight, 156 | ascent: mediumAscent, 157 | }, 158 | } 159 | short := Style{ 160 | Face: &testFace{ 161 | adv: map[rune]fixed.Int26_6{'a': fixed.I(1)}, 162 | height: fixed.I(10), 163 | ascent: fixed.I(8), 164 | }, 165 | } 166 | opts := Options{ 167 | DefaultStyle: medium, 168 | Size: image.Pt(5, 100000), 169 | } 170 | s := NewSetter(opts) 171 | 172 | // First line has the tall height, which is taller than the default. 173 | s.Add([]byte{'a'}) 174 | s.AddStyle(&tall, []byte{'a', '\n'}) 175 | 176 | // Second line has the medium height, since short is shorter than default. 177 | s.Add([]byte{'a'}) 178 | s.AddStyle(&short, []byte{'a', '\n'}) 179 | 180 | txt := s.Set() 181 | 182 | if len(txt.lines) != 2 { 183 | t.Fatalf("txt.len(%v)=%d, want 2", txt.lines, len(txt.lines)) 184 | } 185 | if x := txt.lines[0].h; x != tallHeight { 186 | t.Errorf("txt.lines[0].h=%v, want %v", x, tallHeight) 187 | } 188 | if x := txt.lines[0].a; x != tallAscent { 189 | t.Errorf("txt.lines[0].a=%v, want %v", x, tallAscent) 190 | } 191 | if x := txt.lines[1].h; x != mediumHeight { 192 | t.Errorf("txt.lines[1].h=%v, want %v", x, mediumHeight) 193 | } 194 | if x := txt.lines[1].a; x != mediumAscent { 195 | t.Errorf("txt.lines[1].a=%v, want %v", x, mediumAscent) 196 | } 197 | } 198 | 199 | func TestLinesHeight(t *testing.T) { 200 | const ( 201 | pad = 3 202 | // Height fits 10 unit-height lines plus 2*pad. 203 | height = 16 204 | ) 205 | s := NewSetter(Options{ 206 | DefaultStyle: Style{Face: &unitFace{}}, 207 | Size: image.Pt(1000, height), 208 | Padding: pad, 209 | }) 210 | 211 | // Lines less than size. 212 | s.Add([]byte("1\n2\n3")) 213 | txt := s.Set() 214 | if txt.Size().Y != height { 215 | t.Errorf("txt.Size().Y=%d, want %d", txt.Size().Y, height) 216 | } 217 | if h := txt.LinesHeight(); h != len(txt.lines)+2*pad { 218 | t.Errorf("without trailing newline txt.LinesHeight()=%d, want %d", 219 | h, len(txt.lines)+2*pad) 220 | } 221 | 222 | // Lines less than size, trailing newline 223 | s.Add([]byte("1\n2\n3\n")) 224 | txt = s.Set() 225 | if txt.Size().Y != height { 226 | t.Errorf("txt.Size().Y=%d, want %d", txt.Size().Y, height) 227 | } 228 | if h := txt.LinesHeight(); h != len(txt.lines)+1+2*pad { 229 | t.Errorf("with trailing newline txt.LinesHeight()=%d, want %d", 230 | h, len(txt.lines)+1+2*pad) 231 | } 232 | 233 | // Size less than line height. 234 | s.Add([]byte("1\n2\n3\n4\n5\n6\n7\n8\n9\n0\nX\nY\nZ")) 235 | txt = s.Set() 236 | if txt.Size().Y != height { 237 | t.Errorf("txt.Size().Y=%d, want %d", txt.Size().Y, height) 238 | } 239 | const maxLines = 10 // Only 10 lines fit the height. 240 | if h := txt.LinesHeight(); h != maxLines+2*pad { 241 | t.Errorf("size less than height txt.LinesHeight()=%d, want %d", 242 | h, maxLines+2*pad) 243 | } 244 | 245 | // Height less than padding 246 | s0 := NewSetter(Options{ 247 | DefaultStyle: Style{Face: &unitFace{}}, 248 | Size: image.Pt(5, pad/2), 249 | Padding: pad, 250 | }) 251 | s0.Add([]byte("1\n2\n3\n")) 252 | txt = s0.Set() 253 | if h := txt.LinesHeight(); h != s0.opts.Size.Y { 254 | t.Errorf("height less than padding txt.LinesHeight()=%d, want %d", 255 | h, s0.opts.Size.Y) 256 | } 257 | 258 | // Negative height. 259 | s1 := NewSetter(Options{ 260 | DefaultStyle: Style{Face: &unitFace{}}, 261 | Size: image.Pt(5, -1), 262 | Padding: pad, 263 | }) 264 | s1.Add([]byte("1\n2\n3\n")) 265 | txt = s1.Set() 266 | if h := txt.LinesHeight(); h != 0 { 267 | t.Errorf("negative height txt.LinesHeight()=%d, want %d", h, 0) 268 | } 269 | } 270 | 271 | func TestReset(t *testing.T) { 272 | s := NewSetter(Options{ 273 | DefaultStyle: Style{Face: &unitFace{}}, 274 | Size: image.Pt(5, 5), 275 | }) 276 | 277 | s.Add([]byte("1234567890abcde")) 278 | 279 | s.Reset(Options{ 280 | DefaultStyle: Style{Face: &unitFace{}}, 281 | Size: image.Pt(10, 5), 282 | }) 283 | 284 | s.Add([]byte("1234567890abcde")) 285 | 286 | // Previously added text is removed. 287 | // Lines break at 10, not 5. 288 | want := "[1234567890][abcde]" 289 | got := lineString(s.Set()) 290 | if want != got { 291 | t.Errorf("got=%q, want=%q", got, want) 292 | } 293 | } 294 | 295 | func TestTextIndex(t *testing.T) { 296 | s := NewSetter(Options{ 297 | DefaultStyle: Style{ 298 | Face: &testFace{ 299 | adv: map[rune]fixed.Int26_6{ 300 | 'α': fixed.I(10), 301 | 'β': fixed.I(10), 302 | 'ξ': fixed.I(10), 303 | 'd': fixed.I(10), 304 | ' ': fixed.I(10), 305 | 'f': fixed.I(10), 306 | '←': fixed.I(10), 307 | '→': fixed.I(10), 308 | }, 309 | height: fixed.I(10), 310 | }}, 311 | Size: image.Pt(50, 50), 312 | Padding: 10, 313 | TabWidth: 1, 314 | }) 315 | s.Add([]byte("αβξ")) 316 | s.Add([]byte("d\tf")) 317 | s.Add([]byte("←→")) 318 | txt := s.Set() 319 | 320 | // 10x10 px squares. 321 | // We check the index at the middle point of each. 322 | // 01234 323 | // 0 _____ 324 | // 1 _αβξ_ 325 | // 2 _d\tf_ 326 | // 3 _←→__ 327 | // 4 _____ 328 | wants := [25]rune{ 329 | 'α', 'α', 'α', 'α', 'α', 330 | 'α', 'α', 'β', 'ξ', 'd', 331 | 'd', 'd', '\t', 'f', '←', 332 | '←', '←', '→', '·', '·', 333 | '·', '·', '·', '·', '·', 334 | } 335 | text := []byte("αβξd\tf←→·") 336 | for y := 0; y < 5; y++ { 337 | for x := 0; x < 5; x++ { 338 | pt := image.Pt(10*x+5, 10*y+5) 339 | want := wants[y*5+x] 340 | wanti := bytes.IndexRune([]byte(text), want) 341 | goti := txt.Index(pt) 342 | got, _ := utf8.DecodeRune(text[goti:]) 343 | if got != want { 344 | t.Errorf("txt.Index(%v)=%d (%q), want %d (%q)", 345 | pt, goti, got, wanti, want) 346 | } 347 | } 348 | } 349 | } 350 | 351 | func TestTextGlyphBox(t *testing.T) { 352 | const ( 353 | pad = 3 354 | lineHeight = 1 355 | ) 356 | 357 | opts := Options{ 358 | DefaultStyle: Style{Face: &unitFace{}}, 359 | Size: image.Pt(100, 100), 360 | Padding: pad, 361 | TabWidth: 2, 362 | } 363 | 364 | tests := []struct { 365 | name string 366 | opts Options 367 | text string 368 | index int 369 | want image.Rectangle 370 | }{ 371 | { 372 | name: "empty text", 373 | opts: opts, 374 | text: "", 375 | index: 0, 376 | want: image.Rect(pad, pad, pad, lineHeight+pad), 377 | }, 378 | { 379 | name: "text too small for padding", 380 | opts: Options{ 381 | DefaultStyle: Style{Face: &unitFace{}}, 382 | Size: image.Pt(1, 1), 383 | Padding: pad, 384 | }, 385 | text: "abc\ndef", 386 | index: 1, 387 | want: image.ZR, 388 | }, 389 | { 390 | name: "index beyond end", 391 | opts: opts, 392 | text: "abc\ndef", 393 | index: 8, 394 | // We want the empty rectangle after 'f', the 3rd glyph of line 2. 395 | want: image.Rect(pad+3, pad+1, pad+3, pad+2), 396 | }, 397 | { 398 | name: "index way beyond end", 399 | opts: opts, 400 | text: "abc\ndef", 401 | index: 8000, 402 | // We want the empty rectangle after 'f', the 3rd glyph of line 2. 403 | want: image.Rect(pad+3, pad+1, pad+3, pad+2), 404 | }, 405 | { 406 | name: "negative index", 407 | opts: opts, 408 | text: "abc\ndef", 409 | index: -1, 410 | want: image.Rect(pad, pad, pad+1, pad+1), 411 | }, 412 | { 413 | name: "first line first rune", 414 | opts: opts, 415 | text: "abc\ndef", 416 | index: 0, 417 | want: image.Rect(pad, pad, pad+1, pad+1), 418 | }, 419 | { 420 | name: "first line second rune", 421 | opts: opts, 422 | text: "abc\ndef", 423 | index: 1, 424 | want: image.Rect(pad+1, pad, pad+2, pad+1), 425 | }, 426 | { 427 | name: "first line last rune", 428 | opts: opts, 429 | text: "abc\ndef", 430 | index: 3, 431 | want: image.Rect(pad+3, pad, pad+4, pad+1), 432 | }, 433 | { 434 | name: "second line first rune", 435 | opts: opts, 436 | text: "abc\ndef", 437 | index: 4, 438 | want: image.Rect(pad, pad+1, pad+1, pad+2), 439 | }, 440 | { 441 | name: "tab", 442 | opts: opts, 443 | text: "a\tb\tc", 444 | index: 1, 445 | want: image.Rect(pad+1, pad, pad+2, pad+1), 446 | }, 447 | { 448 | name: "trailing newline", 449 | opts: opts, 450 | text: "a\n", 451 | index: 3, 452 | // There is a newline at the end, 453 | // so the box beyond the text 454 | // is the start of the next line. 455 | want: image.Rect(pad, pad+1, pad, pad+2), 456 | }, 457 | { 458 | name: "last line extends beyond ymax", 459 | opts: Options{ 460 | DefaultStyle: Style{Face: &unitFace{}}, 461 | Size: image.Pt(100, 2*pad+1), 462 | Padding: pad, 463 | }, 464 | text: "a\nb", 465 | index: 3, 466 | // Only 1 line fits with padding. 467 | // B will be added, but it will extend just beyond ymax, 468 | // so its box is reported as the zero Rectangle. 469 | want: image.ZR, 470 | }, 471 | } 472 | 473 | for _, test := range tests { 474 | s := NewSetter(test.opts) 475 | if test.text != "" { 476 | s.Add([]byte(test.text)) 477 | } 478 | txt := s.Set() 479 | if got := txt.GlyphBox(test.index); got != test.want { 480 | t.Errorf("%s txt.GlyphBox(%d)=%v, want %v", 481 | test.name, test.index, got, test.want) 482 | } 483 | } 484 | } 485 | 486 | func lineString(t *Text) string { 487 | buf := bytes.NewBuffer(nil) 488 | for _, l := range t.lines { 489 | buf.WriteRune('[') 490 | for _, s := range l.spans { 491 | buf.WriteString(s.text) 492 | } 493 | buf.WriteRune(']') 494 | } 495 | return buf.String() 496 | } 497 | 498 | type unitFace struct{} 499 | 500 | func (unitFace) Close() error { return nil } 501 | 502 | func (unitFace) Glyph(fixed.Point26_6, rune) (image.Rectangle, image.Image, image.Point, fixed.Int26_6, bool) { 503 | panic("unimplemented") 504 | } 505 | 506 | func (unitFace) GlyphAdvance(rune) (fixed.Int26_6, bool) { return fixed.I(1), true } 507 | 508 | func (unitFace) Kern(rune, rune) fixed.Int26_6 { return 0 } 509 | 510 | func (unitFace) GlyphBounds(rune) (fixed.Rectangle26_6, fixed.Int26_6, bool) { 511 | return fixed.R(0, 0, 1, 1), fixed.I(1), true 512 | } 513 | 514 | func (unitFace) Metrics() font.Metrics { 515 | return font.Metrics{Height: fixed.I(1), Ascent: fixed.I(1)} 516 | } 517 | 518 | func advStyle(adv map[rune]fixed.Int26_6) Style { 519 | return Style{Face: &testFace{adv: adv, height: fixed.I(1)}} 520 | } 521 | 522 | type testFace struct { 523 | adv map[rune]fixed.Int26_6 524 | kern map[[2]rune]fixed.Int26_6 525 | height, ascent fixed.Int26_6 526 | } 527 | 528 | func (testFace) Close() error { return nil } 529 | 530 | func (testFace) Glyph(fixed.Point26_6, rune) (image.Rectangle, image.Image, image.Point, fixed.Int26_6, bool) { 531 | panic("unimplemented") 532 | } 533 | 534 | func (f testFace) GlyphAdvance(r rune) (fixed.Int26_6, bool) { 535 | a, ok := f.adv[r] 536 | return a, ok 537 | } 538 | 539 | func (f testFace) Kern(r0, r1 rune) fixed.Int26_6 { return f.kern[[2]rune{r0, r1}] } 540 | 541 | func (f testFace) GlyphBounds(r rune) (fixed.Rectangle26_6, fixed.Int26_6, bool) { 542 | a, ok := f.adv[r] 543 | if !ok { 544 | return fixed.Rectangle26_6{}, 0, false 545 | } 546 | b := fixed.Rectangle26_6{ 547 | Min: fixed.Point26_6{Y: -f.ascent}, 548 | Max: fixed.Point26_6{X: a, Y: f.height - f.ascent}, 549 | } 550 | return b, a, true 551 | } 552 | 553 | func (f testFace) Metrics() font.Metrics { 554 | return font.Metrics{Height: f.height, Ascent: f.ascent} 555 | } 556 | --------------------------------------------------------------------------------