github.com/v2fly/tools@v0.100.0/internal/lsp/diff/diff.go (about)

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