diff --git a/misc/stdlib_diff/filediff.go b/misc/stdlib_diff/filediff.go index 746c2a689b2..278ba67e02a 100644 --- a/misc/stdlib_diff/filediff.go +++ b/misc/stdlib_diff/filediff.go @@ -4,57 +4,143 @@ import ( "fmt" "os" "strings" + + "github.com/hexops/gotextdiff" + "github.com/hexops/gotextdiff/myers" + "github.com/hexops/gotextdiff/span" ) // FileDiff is a struct for comparing differences between two files. type FileDiff struct { - Src []string // Lines of the source file. - Dst []string // Lines of the destination file. - Algorithm // Algorithm used for comparison. + Src string // Name of the source file. + Dst string // Name of the destination file. + srcContent string // Content of the source file. + dstContent string // Content of the destination file. + srcLines []string // Lines of the source file. + dstLines []string // Lines of the destination file. + } // LineDifferrence represents a difference in a line during file comparison. type LineDifferrence struct { Line string // The line content. Operation operation // The operation performed on the line (e.g., "add", "delete", "equal"). + Number int } // NewFileDiff creates a new FileDiff instance for comparing differences between // the specified source and destination files. It initializes the source and // destination file lines . func NewFileDiff(srcPath, dstPath string) (*FileDiff, error) { - src, err := getFileLines(srcPath) + src, err := getFileContent(srcPath) if err != nil { return nil, fmt.Errorf("can't read src file: %w", err) } - dst, err := getFileLines(dstPath) + dst, err := getFileContent(dstPath) if err != nil { return nil, fmt.Errorf("can't read dst file: %w", err) } return &FileDiff{ - Src: src, - Dst: dst, - Algorithm: NewMyers(src, dst), + srcContent: src, + dstContent: dst, + srcLines: strings.Split(src, "\n"), + dstLines: strings.Split(dst, "\n"), + Src: srcPath, + Dst: dstPath, }, nil } // Differences returns the differences in lines between the source and // destination files using the configured diff algorithm. func (f *FileDiff) Differences() (src, dst []LineDifferrence) { - return f.Diff() + var ( + srcIndex, dstIndex int + insertCount, deleteCount int + dstDiff, srcDiff []LineDifferrence + ) + + if len(f.dstContent) == 0 { + return f.destEmpty() + } + + if len(f.srcContent) == 0 { + return f.srcEmpty() + } + + /* printUntil prints all the lines thar are equal + because they do not appear on the computed edits from gotextdiff + so we need to add them manually looping always from the current value of + srcIndex until the line before the start of the hunk computed diff, hunk.FromLine-1 + + We need to print all the lines before each hunk and then ensure the end of the file is printed too + */ + printUntil := func(until int) { + for i := srcIndex; i < until; i++ { + dstDiff = append(dstDiff, LineDifferrence{Line: f.srcLines[srcIndex], Operation: equal, Number: dstIndex + 1}) + srcDiff = append(srcDiff, LineDifferrence{Line: f.srcLines[srcIndex], Operation: equal, Number: srcIndex + 1}) + srcIndex++ + dstIndex++ + } + } + + edits := myers.ComputeEdits(span.URIFromPath(f.Src), f.srcContent, f.dstContent) + unified := gotextdiff.ToUnified(f.Src, f.Dst, f.srcContent, edits) + for _, hunk := range unified.Hunks { + printUntil(hunk.FromLine - 1) + + for _, line := range hunk.Lines { + switch line.Kind { + case gotextdiff.Insert: + insertCount++ + dstIndex++ + dstDiff = append(dstDiff, LineDifferrence{Line: line.Content, Operation: insert, Number: dstIndex}) + + case gotextdiff.Equal: + srcIndex++ + dstIndex++ + dstDiff = append(dstDiff, LineDifferrence{Line: line.Content, Operation: equal, Number: dstIndex}) + srcDiff = append(srcDiff, LineDifferrence{Line: line.Content, Operation: equal, Number: srcIndex}) + + case gotextdiff.Delete: + srcIndex++ + deleteCount++ + srcDiff = append(srcDiff, LineDifferrence{Line: line.Content, Operation: delete, Number: srcIndex}) + } + } + } + + printUntil(len(f.srcLines)) + return srcDiff, dstDiff +} + +func (f *FileDiff) destEmpty() ([]LineDifferrence, []LineDifferrence) { + srcDiff := []LineDifferrence{} + for index, line := range f.srcLines { + srcDiff = append(srcDiff, LineDifferrence{Line: line, Operation: delete, Number: index + 1}) + } + + return srcDiff, make([]LineDifferrence, 0) +} + +func (f *FileDiff) srcEmpty() ([]LineDifferrence, []LineDifferrence) { + destDiff := []LineDifferrence{} + for index, line := range f.dstLines { + destDiff = append(destDiff, LineDifferrence{Line: line, Operation: insert, Number: index + 1}) + } + + return make([]LineDifferrence, 0), destDiff } -// getFileLines reads and returns the lines of a file given its path. -func getFileLines(p string) ([]string, error) { +// getFileContent reads and returns the lines of a file given its path. +func getFileContent(p string) (string, error) { data, err := os.ReadFile(p) if err != nil { if os.IsNotExist(err) { - return nil, nil + return "", nil } - return nil, err + return "", err } - lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") - return lines, nil + return strings.ReplaceAll(string(data), "\t", " "), nil } diff --git a/misc/stdlib_diff/go.mod b/misc/stdlib_diff/go.mod index b533ae398f0..4e200f56ebb 100644 --- a/misc/stdlib_diff/go.mod +++ b/misc/stdlib_diff/go.mod @@ -1,3 +1,5 @@ module github.com/gnolang/gno/misc/stdlib_diff go 1.21.0 + +require github.com/hexops/gotextdiff v1.0.3 diff --git a/misc/stdlib_diff/go.sum b/misc/stdlib_diff/go.sum new file mode 100644 index 00000000000..e71200ae5ce --- /dev/null +++ b/misc/stdlib_diff/go.sum @@ -0,0 +1,2 @@ +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= diff --git a/misc/stdlib_diff/myers.go b/misc/stdlib_diff/myers.go deleted file mode 100644 index c967dd639da..00000000000 --- a/misc/stdlib_diff/myers.go +++ /dev/null @@ -1,166 +0,0 @@ -package main - -import ( - "slices" -) - -var _ Algorithm = (*Myers)(nil) - -// Myers is a struct representing the Myers algorithm for line-based difference. -type Myers struct { - src []string // Lines of the source file. - dst []string // Lines of the destination file. -} - -// NewMyers creates a new Myers instance with the specified source and destination lines. -func NewMyers(src, dst []string) *Myers { - return &Myers{ - src: src, - dst: dst, - } -} - -// Do performs the Myers algorithm to find the differences between source and destination files. -// It returns the differences as two slices of LineDifferrence representing source and destination changes. -func (m *Myers) Diff() ([]LineDifferrence, []LineDifferrence) { - var ( - srcIndex, dstIndex int - insertCount, deleteCount int - dstDiff, srcDiff []LineDifferrence - ) - - operations := m.doMyers() - - for _, op := range operations { - switch op { - case insert: - dstDiff = append(dstDiff, LineDifferrence{Line: m.dst[dstIndex], Operation: op}) - srcDiff = append(srcDiff, LineDifferrence{Line: "", Operation: equal}) - dstIndex++ - insertCount++ - continue - - case equal: - dstDiff = append(dstDiff, LineDifferrence{Line: m.src[srcIndex], Operation: op}) - srcDiff = append(srcDiff, LineDifferrence{Line: m.src[srcIndex], Operation: op}) - srcIndex++ - dstIndex++ - continue - - case delete: - dstDiff = append(dstDiff, LineDifferrence{Line: "", Operation: equal}) - srcDiff = append(srcDiff, LineDifferrence{Line: m.src[srcIndex], Operation: op}) - srcIndex++ - deleteCount++ - continue - } - } - - // Means that src file is empty. - if insertCount == len(srcDiff) { - srcDiff = make([]LineDifferrence, 0) - } - // Means that dst file is empty. - if deleteCount == len(dstDiff) { - dstDiff = make([]LineDifferrence, 0) - } - return srcDiff, dstDiff -} - -// doMyers performs the Myers algorithm and returns the list of operations. -func (m *Myers) doMyers() []operation { - var tree []map[int]int - var x, y int - - srcLen := len(m.src) - dstLen := len(m.dst) - max := srcLen + dstLen - - for pathLen := 0; pathLen <= max; pathLen++ { - optimalCoordinates := make(map[int]int, pathLen+2) - tree = append(tree, optimalCoordinates) - - if pathLen == 0 { - commonPrefixLen := 0 - for srcLen > commonPrefixLen && dstLen > commonPrefixLen && m.src[commonPrefixLen] == m.dst[commonPrefixLen] { - commonPrefixLen++ - } - optimalCoordinates[0] = commonPrefixLen - - if commonPrefixLen == srcLen && commonPrefixLen == dstLen { - return m.getAllOperations(tree) - } - continue - } - - lastV := tree[pathLen-1] - - for k := -pathLen; k <= pathLen; k += 2 { - if k == -pathLen || (k != pathLen && lastV[k-1] < lastV[k+1]) { - x = lastV[k+1] - } else { - x = lastV[k-1] + 1 - } - - y = x - k - - for x < srcLen && y < dstLen && m.src[x] == m.dst[y] { - x, y = x+1, y+1 - } - - optimalCoordinates[k] = x - - if x == srcLen && y == dstLen { - return m.getAllOperations(tree) - } - } - } - - return m.getAllOperations(tree) -} - -// getAllOperations retrieves the list of operations from the calculated tree. -func (m *Myers) getAllOperations(tree []map[int]int) []operation { - var operations []operation - var k, prevK, prevX, prevY int - - x := len(m.src) - y := len(m.dst) - - for pathLen := len(tree) - 1; pathLen > 0; pathLen-- { - k = x - y - lastV := tree[pathLen-1] - - if k == -pathLen || (k != pathLen && lastV[k-1] < lastV[k+1]) { - prevK = k + 1 - } else { - prevK = k - 1 - } - - prevX = lastV[prevK] - prevY = prevX - prevK - - for x > prevX && y > prevY { - operations = append(operations, equal) - x -= 1 - y -= 1 - } - - if x == prevX { - operations = append(operations, insert) - } else { - operations = append(operations, delete) - } - - x, y = prevX, prevY - } - - if tree[0][0] != 0 { - for i := 0; i < tree[0][0]; i++ { - operations = append(operations, equal) - } - } - - slices.Reverse(operations) - return operations -} diff --git a/misc/stdlib_diff/templates/package_diff_template.html b/misc/stdlib_diff/templates/package_diff_template.html index 9b2158fc73b..6a41cf38db3 100644 --- a/misc/stdlib_diff/templates/package_diff_template.html +++ b/misc/stdlib_diff/templates/package_diff_template.html @@ -3,16 +3,20 @@ {{- range .}} {{- if eq .Operation 1}}
- -

{{.Line}}

+ {{.Number}}

{{.Line}}

{{- else if eq .Operation 2}}
- +

{{.Line}}

+ {{.Number}}

{{.Line}}

+
+ + {{- else if eq .Operation 3}} +
+ {{.Number}} +

{{.Line}}

- {{- else if eq .Line ""}} -
{{- else}} -

{{.Line}}

+

{{.Line}}

{{- end}} {{- end}}