client and a little refactoring
This commit is contained in:
@@ -7,17 +7,37 @@ 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))
|
||||||
|
if n == 0 {
|
||||||
|
n = 1
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -25,28 +45,21 @@ func (d *Document) NLines() uint {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d Delta) NLines() uint {
|
func (d Document) line(i uint) string {
|
||||||
n := uint(len(d.Lines))
|
if i >= uint(len(d.lines)) {
|
||||||
if n == 0 {
|
|
||||||
n = 1
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Document) Line(i uint) string {
|
|
||||||
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()
|
||||||
|
|
||||||
|
for _, dl := range dls {
|
||||||
err := d.validate(dl)
|
err := d.validate(dl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("delta does not apply to document: %v", err)
|
return fmt.Errorf("delta does not apply to document: %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dl := range dls {
|
||||||
switch dl.Action {
|
switch dl.Action {
|
||||||
case DeltaInsert:
|
case DeltaInsert:
|
||||||
return d.applyInsert(dl)
|
d.applyInsert(dl)
|
||||||
case DeltaRemove:
|
case DeltaRemove:
|
||||||
return d.applyRemove(dl)
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user