mirror of
https://github.com/golang/go
synced 2024-11-05 17:26:11 -07:00
17d4c77023
Change-Id: I3a7db9261f0bc16609af6d58b363ec52474cb07e Reviewed-on: https://go-review.googlesource.com/c/tools/+/200697 Run-TryBot: Ian Cottrell <iancottrell@google.com> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
160 lines
5.1 KiB
Go
160 lines
5.1 KiB
Go
// 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
|
|
|
|
// 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
|
|
}
|