| // 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 |
| } |