diff --git a/acedoc.go b/acedoc.go index b0f0802..1a48df2 100644 --- a/acedoc.go +++ b/acedoc.go @@ -1,46 +1,10 @@ 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 @@ -69,78 +33,6 @@ func (d Delta) NLines() uint { 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 "" @@ -156,6 +48,7 @@ func (d Delta) Line(i uint) string { func (d Delta) NRows() uint { r := d.End.Row - d.Start.Row + r += 1 if r == 0 { return 1 } diff --git a/acedoc_test.go b/acedoc_test.go index 15f8f8f..7de21fd 100644 --- a/acedoc_test.go +++ b/acedoc_test.go @@ -11,18 +11,47 @@ func TestDelta(t *testing.T) { } 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")}, + + // empty tests + + {"empty", + "", "", Delta{}}, + + {"empty with content", + "a", "a", Delta{}}, + + // insert tests + + {"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 tests + {"remove", + "a", "", Remove(0, 0, "a")}, + + {"remove part", + "aa", "a", Remove(0, 0, "a")}, + + {"remove across line", + "a\nb", "a", Remove(0, 1, "\nb")}, } for _, c := range cases { diff --git a/delta.go b/delta.go index b74f9a0..2a983bc 100644 --- a/delta.go +++ b/delta.go @@ -1,6 +1,14 @@ package acedoc +import ( + "fmt" + "strings" +) + /* ***** BEGIN LICENSE BLOCK ***** +* +* Original license for JS algorithm, which has been rewritten in Go: +* * Distributed under the BSD license: * * Copyright (c) 2010, Ajax.org B.V. @@ -29,3 +37,124 @@ package acedoc * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ***** END LICENSE BLOCK ***** */ + +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") + nlines := len(lines) - 1 + return Delta{ + Action: DeltaRemove, + Lines: lines, + Start: Position{row, col}, + End: Position{row + uint(nlines), uint(len(lines[nlines]))}, + } +} + +func Insert(row, col uint, str string) Delta { + lines := strings.Split(str, "\n") + nlines := uint(len(lines) - 1) + return Delta{ + Action: DeltaInsert, + Lines: lines, + Start: Position{row, col}, + End: Position{row + nlines, uint(len(lines[nlines]))}, + } +} + +func (d *Document) applyInsert(dl Delta) error { + + row := dl.Start.Row + col := dl.Start.Column + line := d.Lines[row] + + 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 + } + + return nil +} + +func (d *Document) applyRemove(dl Delta) error { + row := dl.Start.Row + col := dl.Start.Column + line := d.Lines[row] + endCol := dl.End.Column + endRow := dl.End.Row + + if row == endRow { + d.Lines[row] = line[:col] + line[endCol:] + } else { + d.Lines[endRow] = d.Lines[row][:col] + d.Lines[endRow][endCol:] + newlines := []string{} + newlines = append(newlines, d.Lines[:row]...) + newlines = append(newlines, d.Lines[endRow:]...) + d.Lines = newlines + } + + return nil +} + +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) + } + + switch dl.Action { + case DeltaInsert: + return d.applyInsert(dl) + case DeltaRemove: + return d.applyRemove(dl) + } + + 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 +}