client and a little refactoring

master
Stephen Searles 7 years ago
parent eafc496037
commit b66e36f0e3
  1. 45
      acedoc.go
  2. 134
      client.go
  3. 46
      client_test.go
  4. 100
      delta.go

@ -6,26 +6,38 @@ import (
) )
type Document struct { type Document struct {
mtx sync.RWMutex mtx sync.RWMutex
Lines []string lines []string
deltas []Delta
clients []*Client
}
type DeltaHandler interface {
Handle([]Delta) error
}
type DeltaHandlerFunc func([]Delta) error
func (fn DeltaHandlerFunc) Handle(d []Delta) error {
return fn(d)
} }
func NewString(str string) *Document { func NewString(str string) *Document {
d := &Document{ d := &Document{
Lines: strings.Split(str, "\n"), lines: strings.Split(str, "\n"),
} }
return d return d
} }
func (d *Document) NLines() uint { func (d *Document) nLines() uint {
n := uint(len(d.Lines)) n := uint(len(d.lines))
if n == 0 { if n == 0 {
n = 1 n = 1
} }
return n return n
} }
func (d Delta) NLines() uint { func (d Delta) nLines() uint {
n := uint(len(d.Lines)) n := uint(len(d.Lines))
if n == 0 { if n == 0 {
n = 1 n = 1
@ -33,20 +45,21 @@ func (d Delta) NLines() uint {
return n return n
} }
func (d Document) Line(i uint) string { func (d Document) line(i uint) string {
if i >= uint(len(d.Lines)) { if i >= uint(len(d.lines)) {
return "" return ""
} }
return d.Lines[i] return d.lines[i]
} }
func (d Delta) Line(i uint) string {
func (d Delta) line(i uint) string {
if i >= uint(len(d.Lines)) { if i >= uint(len(d.Lines)) {
return "" return ""
} }
return d.Lines[i] return d.Lines[i]
} }
func (d Delta) NRows() uint { func (d Delta) nRows() uint {
r := d.End.Row - d.Start.Row r := d.End.Row - d.Start.Row
r += 1 r += 1
if r == 0 { if r == 0 {
@ -55,12 +68,12 @@ func (d Delta) NRows() uint {
return r return r
} }
func (d Delta) Rows() uint { func (d Delta) rows() uint {
r := d.End.Row - d.Start.Row r := d.End.Row - d.Start.Row
return r return r
} }
func (d Delta) Cols() uint { func (d Delta) cols() uint {
r := d.End.Column r := d.End.Column
return r return r
} }
@ -69,7 +82,7 @@ func (d *Document) Contents() string {
d.mtx.RLock() d.mtx.RLock()
defer d.mtx.RUnlock() defer d.mtx.RUnlock()
return strings.Join(d.Lines, "\n") return strings.Join(d.lines, "\n")
} }
type Position struct { type Position struct {
@ -77,11 +90,11 @@ type Position struct {
} }
func (d *Document) InDocument(pos Position) bool { func (d *Document) InDocument(pos Position) bool {
if pos.Row > d.NLines() { if pos.Row > d.nLines() {
return false return false
} }
line := d.Line(pos.Row) line := d.line(pos.Row)
if pos.Column > uint(len(line)) { if pos.Column > uint(len(line)) {
return false return false
} }

@ -0,0 +1,134 @@
package acedoc
import (
"sync"
"time"
)
func (d *Document) Client(h DeltaHandler) *Client {
c := &Client{
ch: make(chan Delta, 10),
errs: make(chan error, 10),
doc: d,
handler: h,
}
go func() {
for {
dls := c.pullDeltas()
if dls != nil && c.handler != nil {
go func() {
err := c.handler.Handle(dls)
if err != nil {
select {
case c.errs <- err:
default:
c.Close()
return
}
}
}()
}
}
}()
d.mtx.Lock()
defer d.mtx.Unlock()
d.clients = append(d.clients, c)
return c
}
func (d *Document) removeClient(c *Client) {
d.mtx.Lock()
defer d.mtx.Unlock()
for i, ci := range d.clients {
if ci == c {
newclients := append([]*Client{}, d.clients[:i]...)
if len(d.clients) > i {
newclients = append(newclients, d.clients[i+1:]...)
}
d.clients = newclients
return
}
}
}
type Client struct {
ch chan Delta
errs chan error
closing sync.Mutex
closed bool
doc *Document
maxDeltaBucket int
pending []Delta
handler DeltaHandler
}
// Close closes the client
func (c *Client) Close() {
c.closing.Lock()
defer c.closing.Unlock()
if !c.closed {
c.doc.removeClient(c)
close(c.ch)
close(c.errs)
c.closed = true
}
}
// Error returns any pending errors from pushing deltas to the client.
func (c *Client) Error() error {
var err error
select {
case err = <-c.errs:
return err
default:
}
return nil
}
// PushDeltas pushes deltas into the document.
func (c *Client) PushDeltas(d ...Delta) error {
return c.doc.Apply(d...)
}
func (c *Client) pullDeltas() []Delta {
maxSize := c.maxDeltaBucket
if maxSize == 0 {
maxSize = 50
}
timeout := 10 * time.Microsecond
var ds []Delta
for {
var d Delta
var ok bool
select {
case d, ok = <-c.ch:
case <-time.After(timeout):
return ds
}
if !ok {
return ds
}
if d.Equal(Delta{}) {
continue
}
ds = append(ds, d)
if len(ds) == maxSize {
return ds
}
}
}

@ -0,0 +1,46 @@
package acedoc
import (
"testing"
"time"
)
func TestClient(t *testing.T) {
doc := NewString("")
aliceCh := make(chan []Delta, 5)
alice := doc.Client(DeltaHandlerFunc(func(ds []Delta) error {
aliceCh <- ds
return nil
}))
defer alice.Close()
bobCh := make(chan []Delta, 5)
bob := doc.Client(DeltaHandlerFunc(func(ds []Delta) error {
bobCh <- ds
return nil
}))
defer bob.Close()
delta := Insert(0, 0, "alphabet")
err := alice.PushDeltas(delta)
if err != nil {
t.Error("got error pushing delta:", err)
}
select {
case got := <-bobCh:
if len(got) != 1 {
t.Errorf("got %d deltas, expected %d", len(got), 1)
} else if !got[0].Equal(delta) {
t.Error("didn't get the correct delta, saw:", got[0], "expected:", delta)
}
case <-time.After(10 * time.Millisecond):
t.Error("didn't get any delta")
}
if got := doc.Contents(); got != "alphabet" {
t.Errorf("saw doc %q, expected %q", got, "alphabet")
}
}

@ -51,6 +51,32 @@ type Delta struct {
Start, End Position Start, End Position
} }
func (d Delta) Equal(other Delta) bool {
if d.Start != other.Start {
return false
}
if d.End != other.End {
return false
}
if d.Action != other.Action {
return false
}
if len(d.Lines) != len(other.Lines) {
return false
}
for i := range d.Lines {
if d.Lines[i] != other.Lines[i] {
return false
}
}
return true
}
func Remove(row, col uint, str string) Delta { func Remove(row, col uint, str string) Delta {
lines := strings.Split(str, "\n") lines := strings.Split(str, "\n")
nlines := len(lines) - 1 nlines := len(lines) - 1
@ -73,65 +99,71 @@ func Insert(row, col uint, str string) Delta {
} }
} }
func (d *Document) applyInsert(dl Delta) error { func (d *Document) applyInsert(dl Delta) {
row := dl.Start.Row row := dl.Start.Row
col := dl.Start.Column col := dl.Start.Column
line := d.Lines[row] line := d.lines[row]
if dl.NLines() == 1 { if dl.nLines() == 1 {
d.Lines[row] = line[:col] + dl.Line(0) + line[col:] d.lines[row] = line[:col] + dl.line(0) + line[col:]
} else { } else {
newlines := []string{} newlines := []string{}
if row != 0 { if row != 0 {
newlines = append(newlines, d.Lines[:row-1]...) // old content newlines = append(newlines, d.lines[:row-1]...) // old content
} }
newlines = append(newlines, d.Lines[row][:col]+dl.Lines[0]) newlines = append(newlines, d.lines[row][:col]+dl.Lines[0])
newlines = append(newlines, dl.Lines[1:]...) // new content newlines = append(newlines, dl.Lines[1:]...) // new content
newlines[len(newlines)-1] += d.Lines[row][col:] newlines[len(newlines)-1] += d.lines[row][col:]
if uint(len(d.Lines)) > row { if uint(len(d.lines)) > row {
newlines = append(newlines, d.Lines[row+1:]...) // old content newlines = append(newlines, d.lines[row+1:]...) // old content
} }
d.Lines = newlines d.lines = newlines
} }
return nil
} }
func (d *Document) applyRemove(dl Delta) error { func (d *Document) applyRemove(dl Delta) {
row := dl.Start.Row row := dl.Start.Row
col := dl.Start.Column col := dl.Start.Column
line := d.Lines[row] line := d.lines[row]
endCol := dl.End.Column endCol := dl.End.Column
endRow := dl.End.Row endRow := dl.End.Row
if row == endRow { if row == endRow {
d.Lines[row] = line[:col] + line[endCol:] d.lines[row] = line[:col] + line[endCol:]
} else { } else {
d.Lines[endRow] = d.Lines[row][:col] + d.Lines[endRow][endCol:] d.lines[endRow] = d.lines[row][:col] + d.lines[endRow][endCol:]
newlines := []string{} newlines := []string{}
newlines = append(newlines, d.Lines[:row]...) newlines = append(newlines, d.lines[:row]...)
newlines = append(newlines, d.Lines[endRow:]...) newlines = append(newlines, d.lines[endRow:]...)
d.Lines = newlines d.lines = newlines
} }
return nil
} }
func (d *Document) Apply(dl Delta) error { func (d *Document) Apply(dls ...Delta) error {
d.mtx.Lock() d.mtx.Lock()
defer d.mtx.Unlock() defer d.mtx.Unlock()
err := d.validate(dl) for _, dl := range dls {
if err != nil { err := d.validate(dl)
return fmt.Errorf("delta does not apply to document: %v", err) if err != nil {
return fmt.Errorf("delta does not apply to document: %v", err)
}
} }
switch dl.Action { for _, dl := range dls {
case DeltaInsert: switch dl.Action {
return d.applyInsert(dl) case DeltaInsert:
case DeltaRemove: d.applyInsert(dl)
return d.applyRemove(dl) case DeltaRemove:
d.applyRemove(dl)
}
d.deltas = append(d.deltas, dl)
for _, c := range d.clients {
c.ch <- dl
}
} }
return nil return nil
@ -146,14 +178,14 @@ func (d *Document) validate(dl Delta) error {
return fmt.Errorf("end is not in document for remove") return fmt.Errorf("end is not in document for remove")
} }
if dl.NLines() != dl.NRows() { if dl.nLines() != dl.nRows() {
return fmt.Errorf("delta has %d lines, but positions show range of %d lines", dl.NLines(), dl.NRows()) return fmt.Errorf("delta has %d lines, but positions show range of %d lines", dl.nLines(), dl.nRows())
} }
lastDlLine := dl.Line(dl.NRows() - 1) lastDlLine := dl.line(dl.nRows() - 1)
if uint(len(lastDlLine)) != dl.Cols() { if uint(len(lastDlLine)) != dl.cols() {
return fmt.Errorf("delta has %d chars on the final line, but positions show range of %d chars", len(lastDlLine), dl.Cols()) return fmt.Errorf("delta has %d chars on the final line, but positions show range of %d chars", len(lastDlLine), dl.cols())
} }
return nil return nil

Loading…
Cancel
Save