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 }