initial
This commit is contained in:
@@ -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 ***** */
|
||||
Reference in New Issue
Block a user