github.com/google/yamlfmt@v0.12.2-0.20240514121411-7f77800e2681/internal/multilinediff/multilinediff.go (about)

     1  // Copyright 2024 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package multilinediff
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  
    21  	"github.com/google/go-cmp/cmp"
    22  	"github.com/google/go-cmp/cmp/cmpopts"
    23  )
    24  
    25  // Get the diff between two strings.
    26  func Diff(a, b, lineSep string) (string, int) {
    27  	reporter := Reporter{LineSep: lineSep}
    28  	cmp.Diff(
    29  		a, b,
    30  		cmpopts.AcyclicTransformer("multiline", func(s string) []string {
    31  			return strings.Split(s, lineSep)
    32  		}),
    33  		cmp.Reporter(&reporter),
    34  	)
    35  	return reporter.String(), reporter.DiffCount
    36  }
    37  
    38  type diffType int
    39  
    40  const (
    41  	diffTypeEqual diffType = iota
    42  	diffTypeChange
    43  	diffTypeAdd
    44  )
    45  
    46  type diffLine struct {
    47  	diff diffType
    48  	old  string
    49  	new  string
    50  }
    51  
    52  func (l diffLine) toLine(length int) string {
    53  	line := ""
    54  
    55  	switch l.diff {
    56  	case diffTypeChange:
    57  		line += "- "
    58  	case diffTypeAdd:
    59  		line += "+ "
    60  	default:
    61  		line += "  "
    62  	}
    63  
    64  	line += l.old
    65  
    66  	for i := 0; i < length-len(l.old); i++ {
    67  		line += " "
    68  	}
    69  
    70  	line += "  "
    71  
    72  	line += l.new
    73  
    74  	return line
    75  }
    76  
    77  // A pretty reporter to pass into cmp.Diff using the cmd.Reporter function.
    78  type Reporter struct {
    79  	LineSep   string
    80  	DiffCount int
    81  
    82  	path  cmp.Path
    83  	lines []diffLine
    84  }
    85  
    86  func (r *Reporter) PushStep(ps cmp.PathStep) {
    87  	r.path = append(r.path, ps)
    88  }
    89  
    90  func (r *Reporter) Report(rs cmp.Result) {
    91  	line := diffLine{}
    92  	vOld, vNew := r.path.Last().Values()
    93  	if !rs.Equal() {
    94  		r.DiffCount++
    95  		if vOld.IsValid() {
    96  			line.diff = diffTypeChange
    97  			line.old = fmt.Sprintf("%+v", vOld)
    98  		}
    99  		if vNew.IsValid() {
   100  			if line.diff == diffTypeEqual {
   101  				line.diff = diffTypeAdd
   102  			}
   103  			line.new = fmt.Sprintf("%+v", vNew)
   104  		}
   105  	} else {
   106  		line.old = fmt.Sprintf("%+v", vOld)
   107  		line.new = fmt.Sprintf("%+v", vOld)
   108  	}
   109  	r.lines = append(r.lines, line)
   110  }
   111  
   112  func (r *Reporter) PopStep() {
   113  	r.path = r.path[:len(r.path)-1]
   114  }
   115  
   116  func (r *Reporter) String() string {
   117  	maxLen := 0
   118  	for _, l := range r.lines {
   119  		if len(l.old) > maxLen {
   120  			maxLen = len(l.old)
   121  		}
   122  	}
   123  
   124  	diffLines := []string{}
   125  	for _, l := range r.lines {
   126  		diffLines = append(diffLines, l.toLine(maxLen))
   127  	}
   128  
   129  	return strings.Join(diffLines, r.LineSep)
   130  }