// 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 import ( "fmt" "strings" ) // Unified represents a set of edits as a unified diff. type Unified struct { // From is the name of the original file. From string // To is the name of the modified file. To string // Hunks is the set of edit hunks needed to transform the file content. Hunks []*Hunk } // Hunk represents a contiguous set of line edits to apply. type Hunk struct { // The line in the original source where the hunk starts. FromLine int // The line in the original source where the hunk finishes. ToLine int // The set of line based edits to apply. Lines []Line } // Line represents a single line operation to apply as part of a Hunk. type Line struct { // Kind is the type of line this represents, deletion, insertion or copy. Kind OpKind // Content is the content of this line. // For deletion it is the line being removed, for all others it is the line // to put in the output. Content string } // OpKind is used to denote the type of operation a line represents. type OpKind int const ( // Delete is the operation kind for a line that is present in the input // but not in the output. Delete OpKind = iota // Insert is the operation kind for a line that is new in the output. Insert // Equal is the operation kind for a line that is the same in the input and // output, often used to provide context around edited lines. Equal ) // String returns a human readable representation of an OpKind. It is not // intended for machine processing. func (k OpKind) String() string { switch k { case Delete: return "delete" case Insert: return "insert" case Equal: return "equal" default: panic("unknown operation kind") } } const ( edge = 3 gap = edge * 2 ) // ToUnified takes a file contents and a sequence of edits, and calculates // a unified diff that represents those edits. func ToUnified(from, to string, content string, edits []TextEdit) Unified { u := Unified{ From: from, To: to, } if len(edits) == 0 { return u } c, edits, partial := prepareEdits(content, edits) if partial { edits = lineEdits(content, c, edits) } lines := splitLines(content) var h *Hunk last := 0 toLine := 0 for _, edit := range edits { start := edit.Span.Start().Line() - 1 end := edit.Span.End().Line() - 1 switch { case h != nil && start == last: //direct extension case h != nil && start <= last+gap: //within range of previous lines, add the joiners addEqualLines(h, lines, last, start) default: //need to start a new hunk if h != nil { // add the edge to the previous hunk addEqualLines(h, lines, last, last+edge) u.Hunks = append(u.Hunks, h) } toLine += start - last h = &Hunk{ FromLine: start + 1, ToLine: toLine + 1, } // add the edge to the new hunk delta := addEqualLines(h, lines, start-edge, start) h.FromLine -= delta h.ToLine -= delta } last = start for i := start; i < end; i++ { h.Lines = append(h.Lines, Line{Kind: Delete, Content: lines[i]}) last++ } if edit.NewText != "" { for _, line := range splitLines(edit.NewText) { h.Lines = append(h.Lines, Line{Kind: Insert, Content: line}) toLine++ } } } if h != nil { // add the edge to the final hunk addEqualLines(h, lines, last, last+edge) u.Hunks = append(u.Hunks, h) } return u } func splitLines(text string) []string { lines := strings.SplitAfter(text, "\n") if lines[len(lines)-1] == "" { lines = lines[:len(lines)-1] } return lines } func addEqualLines(h *Hunk, lines []string, start, end int) int { delta := 0 for i := start; i < end; i++ { if i < 0 { continue } if i >= len(lines) { return delta } h.Lines = append(h.Lines, Line{Kind: Equal, Content: lines[i]}) delta++ } return delta } // Format converts a unified diff to the standard textual form for that diff. // The output of this function can be passed to tools like patch. func (u Unified) Format(f fmt.State, r rune) { if len(u.Hunks) == 0 { return } fmt.Fprintf(f, "--- %s\n", u.From) fmt.Fprintf(f, "+++ %s\n", u.To) for _, hunk := range u.Hunks { fromCount, toCount := 0, 0 for _, l := range hunk.Lines { switch l.Kind { case Delete: fromCount++ case Insert: toCount++ default: fromCount++ toCount++ } } fmt.Fprint(f, "@@") if fromCount > 1 { fmt.Fprintf(f, " -%d,%d", hunk.FromLine, fromCount) } else { fmt.Fprintf(f, " -%d", hunk.FromLine) } if toCount > 1 { fmt.Fprintf(f, " +%d,%d", hunk.ToLine, toCount) } else { fmt.Fprintf(f, " +%d", hunk.ToLine) } fmt.Fprint(f, " @@\n") for _, l := range hunk.Lines { switch l.Kind { case Delete: fmt.Fprintf(f, "-%s", l.Content) case Insert: fmt.Fprintf(f, "+%s", l.Content) default: fmt.Fprintf(f, " %s", l.Content) } if !strings.HasSuffix(l.Content, "\n") { fmt.Fprintf(f, "\n\\ No newline at end of file\n") } } } }