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 }