commit 1c029d98566ce670f79b77334b2977f986bcdd9d Author: Stephen Searles Date: Mon Jun 12 01:30:51 2017 -0700 initial diff --git a/acedoc.go b/acedoc.go new file mode 100644 index 0000000..b0f0802 --- /dev/null +++ b/acedoc.go @@ -0,0 +1,196 @@ +package acedoc + +import ( + "fmt" + "strings" + "sync" +) + +type DeltaAction bool + +const ( + DeltaInsert DeltaAction = true + DeltaRemove DeltaAction = false +) + +type Delta struct { + Action DeltaAction + Lines []string + Start, End Position +} + +func Remove(row, col uint, str string) Delta { + lines := strings.Split(str, "\n") + return Delta{ + Action: DeltaRemove, + Lines: lines, + Start: Position{row, col}, + End: Position{row + uint(len(lines)-1), uint(len(lines[len(lines)-1]))}, + } + +} + +func Insert(row, col uint, str string) Delta { + lines := strings.Split(str, "\n") + return Delta{ + Action: DeltaInsert, + Lines: lines, + Start: Position{row, col}, + End: Position{row + uint(len(lines)-1), uint(len(lines[len(lines)-1]))}, + } + +} + +type Document struct { + mtx sync.RWMutex + Lines []string +} + +func NewString(str string) *Document { + d := &Document{ + Lines: strings.Split(str, "\n"), + } + return d +} + +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)) + if n == 0 { + n = 1 + } + return n +} + +func (d *Document) Apply(dl Delta) error { + d.mtx.Lock() + defer d.mtx.Unlock() + + err := d.validate(dl) + if err != nil { + return fmt.Errorf("delta does not apply to document: %v", err) + } + + row := dl.Start.Row + col := dl.Start.Column + line := d.Lines[row] + + switch dl.Action { + case DeltaInsert: + + if dl.NLines() == 1 { + d.Lines[row] = line[:col] + dl.Line(0) + line[col:] + } else { + newlines := []string{} + if row != 0 { + newlines = append(newlines, d.Lines[:row-1]...) // old content + } + newlines = append(newlines, d.Lines[row][:col]+dl.Lines[0]) + newlines = append(newlines, dl.Lines[1:]...) // new content + newlines[len(newlines)-1] += d.Lines[row][col:] + if uint(len(d.Lines)) > row { + newlines = append(newlines, d.Lines[row+1:]...) // old content + } + d.Lines = newlines + } + + case DeltaRemove: + endCol := dl.End.Column + endRow := dl.End.Row + + if row == endRow { + d.Lines[row] = line[:col] + line[endCol:] + } else { + newlines := []string{} + newlines = append(newlines, d.Lines[:row]...) + newlines = append(newlines, d.Lines[endRow:]...) + newlines[endRow] = newlines[endRow][endCol:] + d.Lines = newlines + } + } + + return nil +} + +func (d *Document) validate(dl Delta) error { + if !d.InDocument(dl.Start) { + return fmt.Errorf("start is not in document") + } + + if dl.Action == DeltaRemove && !d.InDocument(dl.End) { + return fmt.Errorf("end is not in document for remove") + } + + if 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) + + 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 nil +} + +func (d Document) Line(i uint) string { + if i >= uint(len(d.Lines)) { + return "" + } + return d.Lines[i] +} +func (d Delta) Line(i uint) string { + if i >= uint(len(d.Lines)) { + return "" + } + return d.Lines[i] +} + +func (d Delta) NRows() uint { + r := d.End.Row - d.Start.Row + if r == 0 { + return 1 + } + return r +} + +func (d Delta) Rows() uint { + r := d.End.Row - d.Start.Row + return r +} + +func (d Delta) Cols() uint { + r := d.End.Column + return r +} + +func (d *Document) Contents() string { + d.mtx.RLock() + defer d.mtx.RUnlock() + + return strings.Join(d.Lines, "\n") +} + +type Position struct { + Row, Column uint +} + +func (d *Document) InDocument(pos Position) bool { + if pos.Row > d.NLines() { + return false + } + + line := d.Line(pos.Row) + if pos.Column > uint(len(line)) { + return false + } + return true +} diff --git a/acedoc_test.go b/acedoc_test.go new file mode 100644 index 0000000..15f8f8f --- /dev/null +++ b/acedoc_test.go @@ -0,0 +1,42 @@ +package acedoc + +import "testing" + +func TestDelta(t *testing.T) { + type c struct { + name string + start string + expect string + delta Delta + } + + cases := []c{ + {"empty", "", "", Delta{}}, + {"empty with content", "a", "a", Delta{}}, + {"insert with no content", "a", "a", Insert(0, 1, "")}, + {"insert", "a", "ab", Insert(0, 1, "b")}, + {"insert across line", "a", "a\nb", Insert(0, 1, "\nb")}, + {"insert new line", "ab", "a\nb", Insert(0, 1, "\n")}, + {"insert lines at beginning", "ab", "\n\nab", Insert(0, 0, "\n\n")}, + {"insert lines at end", "ab", "ab\n\n", Insert(0, 2, "\n\n")}, + {"insert more lines", "a\nb", "a\n\n\nb", Insert(0, 1, "\n\n")}, + + {"remove", "a", "", Remove(0, 0, "a")}, + {"remove across line", "a\nb", "a", Remove(0, 1, "\nb")}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + doc := NewString(c.start) + err := doc.Apply(c.delta) + if err != nil { + t.Error(err) + return + } + + if doc.Contents() != c.expect { + t.Errorf("unexpected output: %q, expected: %q", doc.Contents(), c.expect) + } + }) + } +} diff --git a/delta.go b/delta.go new file mode 100644 index 0000000..b74f9a0 --- /dev/null +++ b/delta.go @@ -0,0 +1,31 @@ +package acedoc + +/* ***** BEGIN LICENSE BLOCK ***** +* Distributed under the BSD license: +* +* Copyright (c) 2010, Ajax.org B.V. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* * Neither the name of Ajax.org B.V. nor the +* names of its contributors may be used to endorse or promote products +* derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +* +* ***** END LICENSE BLOCK ***** */