commit
1c029d9856
@ -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 |
||||||
|
} |
@ -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) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -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 ***** */ |
Loading…
Reference in new issue