github.com/v2fly/tools@v0.100.0/internal/lsp/diff/unified.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
     6  
     7  import (
     8  	"fmt"
     9  	"strings"
    10  )
    11  
    12  // Unified represents a set of edits as a unified diff.
    13  type Unified struct {
    14  	// From is the name of the original file.
    15  	From string
    16  	// To is the name of the modified file.
    17  	To string
    18  	// Hunks is the set of edit hunks needed to transform the file content.
    19  	Hunks []*Hunk
    20  }
    21  
    22  // Hunk represents a contiguous set of line edits to apply.
    23  type Hunk struct {
    24  	// The line in the original source where the hunk starts.
    25  	FromLine int
    26  	// The line in the original source where the hunk finishes.
    27  	ToLine int
    28  	// The set of line based edits to apply.
    29  	Lines []Line
    30  }
    31  
    32  // Line represents a single line operation to apply as part of a Hunk.
    33  type Line struct {
    34  	// Kind is the type of line this represents, deletion, insertion or copy.
    35  	Kind OpKind
    36  	// Content is the content of this line.
    37  	// For deletion it is the line being removed, for all others it is the line
    38  	// to put in the output.
    39  	Content string
    40  }
    41  
    42  // OpKind is used to denote the type of operation a line represents.
    43  type OpKind int
    44  
    45  const (
    46  	// Delete is the operation kind for a line that is present in the input
    47  	// but not in the output.
    48  	Delete OpKind = iota
    49  	// Insert is the operation kind for a line that is new in the output.
    50  	Insert
    51  	// Equal is the operation kind for a line that is the same in the input and
    52  	// output, often used to provide context around edited lines.
    53  	Equal
    54  )
    55  
    56  // String returns a human readable representation of an OpKind. It is not
    57  // intended for machine processing.
    58  func (k OpKind) String() string {
    59  	switch k {
    60  	case Delete:
    61  		return "delete"
    62  	case Insert:
    63  		return "insert"
    64  	case Equal:
    65  		return "equal"
    66  	default:
    67  		panic("unknown operation kind")
    68  	}
    69  }
    70  
    71  const (
    72  	edge = 3
    73  	gap  = edge * 2
    74  )
    75  
    76  // ToUnified takes a file contents and a sequence of edits, and calculates
    77  // a unified diff that represents those edits.
    78  func ToUnified(from, to string, content string, edits []TextEdit) Unified {
    79  	u := Unified{
    80  		From: from,
    81  		To:   to,
    82  	}
    83  	if len(edits) == 0 {
    84  		return u
    85  	}
    86  	c, edits, partial := prepareEdits(content, edits)
    87  	if partial {
    88  		edits = lineEdits(content, c, edits)
    89  	}
    90  	lines := splitLines(content)
    91  	var h *Hunk
    92  	last := 0
    93  	toLine := 0
    94  	for _, edit := range edits {
    95  		start := edit.Span.Start().Line() - 1
    96  		end := edit.Span.End().Line() - 1
    97  		switch {
    98  		case h != nil && start == last:
    99  			//direct extension
   100  		case h != nil && start <= last+gap:
   101  			//within range of previous lines, add the joiners
   102  			addEqualLines(h, lines, last, start)
   103  		default:
   104  			//need to start a new hunk
   105  			if h != nil {
   106  				// add the edge to the previous hunk
   107  				addEqualLines(h, lines, last, last+edge)
   108  				u.Hunks = append(u.Hunks, h)
   109  			}
   110  			toLine += start - last
   111  			h = &Hunk{
   112  				FromLine: start + 1,
   113  				ToLine:   toLine + 1,
   114  			}
   115  			// add the edge to the new hunk
   116  			delta := addEqualLines(h, lines, start-edge, start)
   117  			h.FromLine -= delta
   118  			h.ToLine -= delta
   119  		}
   120  		last = start
   121  		for i := start; i < end; i++ {
   122  			h.Lines = append(h.Lines, Line{Kind: Delete, Content: lines[i]})
   123  			last++
   124  		}
   125  		if edit.NewText != "" {
   126  			for _, line := range splitLines(edit.NewText) {
   127  				h.Lines = append(h.Lines, Line{Kind: Insert, Content: line})
   128  				toLine++
   129  			}
   130  		}
   131  	}
   132  	if h != nil {
   133  		// add the edge to the final hunk
   134  		addEqualLines(h, lines, last, last+edge)
   135  		u.Hunks = append(u.Hunks, h)
   136  	}
   137  	return u
   138  }
   139  
   140  func splitLines(text string) []string {
   141  	lines := strings.SplitAfter(text, "\n")
   142  	if lines[len(lines)-1] == "" {
   143  		lines = lines[:len(lines)-1]
   144  	}
   145  	return lines
   146  }
   147  
   148  func addEqualLines(h *Hunk, lines []string, start, end int) int {
   149  	delta := 0
   150  	for i := start; i < end; i++ {
   151  		if i < 0 {
   152  			continue
   153  		}
   154  		if i >= len(lines) {
   155  			return delta
   156  		}
   157  		h.Lines = append(h.Lines, Line{Kind: Equal, Content: lines[i]})
   158  		delta++
   159  	}
   160  	return delta
   161  }
   162  
   163  // Format converts a unified diff to the standard textual form for that diff.
   164  // The output of this function can be passed to tools like patch.
   165  func (u Unified) Format(f fmt.State, r rune) {
   166  	if len(u.Hunks) == 0 {
   167  		return
   168  	}
   169  	fmt.Fprintf(f, "--- %s\n", u.From)
   170  	fmt.Fprintf(f, "+++ %s\n", u.To)
   171  	for _, hunk := range u.Hunks {
   172  		fromCount, toCount := 0, 0
   173  		for _, l := range hunk.Lines {
   174  			switch l.Kind {
   175  			case Delete:
   176  				fromCount++
   177  			case Insert:
   178  				toCount++
   179  			default:
   180  				fromCount++
   181  				toCount++
   182  			}
   183  		}
   184  		fmt.Fprint(f, "@@")
   185  		if fromCount > 1 {
   186  			fmt.Fprintf(f, " -%d,%d", hunk.FromLine, fromCount)
   187  		} else {
   188  			fmt.Fprintf(f, " -%d", hunk.FromLine)
   189  		}
   190  		if toCount > 1 {
   191  			fmt.Fprintf(f, " +%d,%d", hunk.ToLine, toCount)
   192  		} else {
   193  			fmt.Fprintf(f, " +%d", hunk.ToLine)
   194  		}
   195  		fmt.Fprint(f, " @@\n")
   196  		for _, l := range hunk.Lines {
   197  			switch l.Kind {
   198  			case Delete:
   199  				fmt.Fprintf(f, "-%s", l.Content)
   200  			case Insert:
   201  				fmt.Fprintf(f, "+%s", l.Content)
   202  			default:
   203  				fmt.Fprintf(f, " %s", l.Content)
   204  			}
   205  			if !strings.HasSuffix(l.Content, "\n") {
   206  				fmt.Fprintf(f, "\n\\ No newline at end of file\n")
   207  			}
   208  		}
   209  	}
   210  }