You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
239 lines
5.7 KiB
239 lines
5.7 KiB
package acedoc
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"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.
|
|
* 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 ***** */
|
|
|
|
type DeltaAction bool
|
|
|
|
const (
|
|
DeltaInsert DeltaAction = true
|
|
DeltaRemove DeltaAction = false
|
|
)
|
|
|
|
func (d DeltaAction) String() string {
|
|
if d == DeltaInsert {
|
|
return "insert"
|
|
}
|
|
return "remove"
|
|
}
|
|
|
|
func (d DeltaAction) MarshalJSON() ([]byte, error) {
|
|
if d == DeltaInsert {
|
|
return []byte(`"insert"`), nil
|
|
}
|
|
return []byte(`"remove"`), nil
|
|
}
|
|
|
|
func (d *DeltaAction) UnmarshalJSON(b []byte) error {
|
|
if string(b) == "null" {
|
|
return nil
|
|
}
|
|
|
|
if string(b) == `"insert"` {
|
|
*d = DeltaInsert
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type Delta struct {
|
|
Action DeltaAction `json:"action"`
|
|
Lines []string `json:"lines"`
|
|
Start Position `json:"start"`
|
|
End Position `json:"end"`
|
|
|
|
source *Client
|
|
}
|
|
|
|
func (d Delta) String() string {
|
|
return fmt.Sprintf("%s:%q(%v->%v)", d.Action, strings.Join(d.Lines, "\n"), d.Start, d.End)
|
|
}
|
|
|
|
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 {
|
|
dl := Insert(row, col, str)
|
|
dl.Action = DeltaRemove
|
|
return dl
|
|
}
|
|
|
|
func Insert(row, col uint, str string) Delta {
|
|
lines := strings.Split(str, "\n")
|
|
|
|
var endcol uint
|
|
addedLines := uint(len(lines) - 1)
|
|
|
|
if len(lines) == 0 {
|
|
addedLines = 1
|
|
} else {
|
|
lastLine := lines[len(lines)-1]
|
|
endcol += uint(len(lastLine))
|
|
}
|
|
|
|
if addedLines == 0 {
|
|
endcol += col
|
|
}
|
|
|
|
return Delta{
|
|
Action: DeltaInsert,
|
|
Lines: lines,
|
|
Start: Position{row, col},
|
|
End: Position{row + addedLines, endcol},
|
|
}
|
|
}
|
|
|
|
func (d *Document) applyInsert(dl Delta) {
|
|
|
|
row := dl.Start.Row
|
|
col := dl.Start.Column
|
|
|
|
if uint(len(d.lines)) == row {
|
|
d.lines = append(d.lines, "")
|
|
}
|
|
|
|
line := d.line(row)
|
|
|
|
if dl.nLines() == 1 {
|
|
d.lines[row] = line[:col] + dl.line(0) + line[col:]
|
|
} else {
|
|
newlines := []string{}
|
|
newlines = append(newlines, d.lines[:row]...) // 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
|
|
}
|
|
|
|
}
|
|
|
|
func (d *Document) applyRemove(dl Delta) {
|
|
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
|
|
}
|
|
}
|
|
|
|
func (d *Document) Apply(dls ...Delta) error {
|
|
d.mtx.Lock()
|
|
defer d.mtx.Unlock()
|
|
|
|
for _, dl := range dls {
|
|
err := d.validate(dl)
|
|
if err != nil {
|
|
return fmt.Errorf("delta does not apply to document: %v", err)
|
|
}
|
|
|
|
switch dl.Action {
|
|
case DeltaInsert:
|
|
d.applyInsert(dl)
|
|
case DeltaRemove:
|
|
d.applyRemove(dl)
|
|
}
|
|
|
|
d.deltas = append(d.deltas, dl)
|
|
for _, c := range d.clients {
|
|
if dl.source == c {
|
|
continue
|
|
}
|
|
c.ch <- dl
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Document) validate(dl Delta) error {
|
|
if !d.InDocument(dl.Start) {
|
|
return fmt.Errorf("start %v is not in document %q", dl.Start, d.contents())
|
|
}
|
|
|
|
if dl.Action == DeltaRemove && !d.InDocument(dl.End) {
|
|
return fmt.Errorf("end %v is not in document %q for remove", dl.End, d.contents())
|
|
}
|
|
|
|
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 !(len(lastDlLine) == 0 && dl.cols() < 0) && len(lastDlLine) != int(math.Abs(float64(dl.cols()))) {
|
|
return fmt.Errorf("delta %v has %d chars on the final line, but positions (%v -> %v) show range of %d chars", dl, len(lastDlLine), dl.Start, dl.End, dl.cols())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|