1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
|
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package diff supports a pluggable diff algorithm.
package diff
import (
"sort"
"strings"
"golang.org/x/tools/internal/span"
)
// TextEdit represents a change to a section of a document.
// The text within the specified span should be replaced by the supplied new text.
type TextEdit struct {
Span span.Span
NewText string
}
// ComputeEdits is the type for a function that produces a set of edits that
// convert from the before content to the after content.
type ComputeEdits func(uri span.URI, before, after string) ([]TextEdit, error)
// SortTextEdits attempts to order all edits by their starting points.
// The sort is stable so that edits with the same starting point will not
// be reordered.
func SortTextEdits(d []TextEdit) {
// Use a stable sort to maintain the order of edits inserted at the same position.
sort.SliceStable(d, func(i int, j int) bool {
return span.Compare(d[i].Span, d[j].Span) < 0
})
}
// ApplyEdits applies the set of edits to the before and returns the resulting
// content.
// It may panic or produce garbage if the edits are not valid for the provided
// before content.
func ApplyEdits(before string, edits []TextEdit) string {
// Preconditions:
// - all of the edits apply to before
// - and all the spans for each TextEdit have the same URI
if len(edits) == 0 {
return before
}
_, edits, _ = prepareEdits(before, edits)
after := strings.Builder{}
last := 0
for _, edit := range edits {
start := edit.Span.Start().Offset()
if start > last {
after.WriteString(before[last:start])
last = start
}
after.WriteString(edit.NewText)
last = edit.Span.End().Offset()
}
if last < len(before) {
after.WriteString(before[last:])
}
return after.String()
}
// LineEdits takes a set of edits and expands and merges them as necessary
// to ensure that there are only full line edits left when it is done.
func LineEdits(before string, edits []TextEdit) []TextEdit {
if len(edits) == 0 {
return nil
}
c, edits, partial := prepareEdits(before, edits)
if partial {
edits = lineEdits(before, c, edits)
}
return edits
}
// prepareEdits returns a sorted copy of the edits
func prepareEdits(before string, edits []TextEdit) (*span.TokenConverter, []TextEdit, bool) {
partial := false
c := span.NewContentConverter("", []byte(before))
copied := make([]TextEdit, len(edits))
for i, edit := range edits {
edit.Span, _ = edit.Span.WithAll(c)
copied[i] = edit
partial = partial ||
edit.Span.Start().Offset() >= len(before) ||
edit.Span.Start().Column() > 1 || edit.Span.End().Column() > 1
}
SortTextEdits(copied)
return c, copied, partial
}
// lineEdits rewrites the edits to always be full line edits
func lineEdits(before string, c *span.TokenConverter, edits []TextEdit) []TextEdit {
adjusted := make([]TextEdit, 0, len(edits))
current := TextEdit{Span: span.Invalid}
for _, edit := range edits {
if current.Span.IsValid() && edit.Span.Start().Line() <= current.Span.End().Line() {
// overlaps with the current edit, need to combine
// first get the gap from the previous edit
gap := before[current.Span.End().Offset():edit.Span.Start().Offset()]
// now add the text of this edit
current.NewText += gap + edit.NewText
// and then adjust the end position
current.Span = span.New(current.Span.URI(), current.Span.Start(), edit.Span.End())
} else {
// does not overlap, add previous run (if there is one)
adjusted = addEdit(before, adjusted, current)
// and then remember this edit as the start of the next run
current = edit
}
}
// add the current pending run if there is one
return addEdit(before, adjusted, current)
}
func addEdit(before string, edits []TextEdit, edit TextEdit) []TextEdit {
if !edit.Span.IsValid() {
return edits
}
// if edit is partial, expand it to full line now
start := edit.Span.Start()
end := edit.Span.End()
if start.Column() > 1 {
// prepend the text and adjust to start of line
delta := start.Column() - 1
start = span.NewPoint(start.Line(), 1, start.Offset()-delta)
edit.Span = span.New(edit.Span.URI(), start, end)
edit.NewText = before[start.Offset():start.Offset()+delta] + edit.NewText
}
if start.Offset() >= len(before) && start.Line() > 1 && before[len(before)-1] != '\n' {
// after end of file that does not end in eol, so join to last line of file
// to do this we need to know where the start of the last line was
eol := strings.LastIndex(before, "\n")
if eol < 0 {
// file is one non terminated line
eol = 0
}
delta := len(before) - eol
start = span.NewPoint(start.Line()-1, 1, start.Offset()-delta)
edit.Span = span.New(edit.Span.URI(), start, end)
edit.NewText = before[start.Offset():start.Offset()+delta] + edit.NewText
}
if end.Column() > 1 {
remains := before[end.Offset():]
eol := strings.IndexRune(remains, '\n')
if eol < 0 {
eol = len(remains)
} else {
eol++
}
end = span.NewPoint(end.Line()+1, 1, end.Offset()+eol)
edit.Span = span.New(edit.Span.URI(), start, end)
edit.NewText = edit.NewText + remains[:eol]
}
edits = append(edits, edit)
return edits
}
|