From 5bee6a6eb846889ed5ea5a203f049c2c1673469d Mon Sep 17 00:00:00 2001 From: Ian Cottrell Date: Thu, 3 Oct 2019 14:18:24 -0400 Subject: [PATCH] internal/lsp: cleanup the diff package Make sure everything is documented and move things to sensible files now all the cross package shuffling is done Change-Id: I884053a207d6741cda066afa5da91b00f1dfd31c Reviewed-on: https://go-review.googlesource.com/c/tools/+/198877 Run-TryBot: Ian Cottrell TryBot-Result: Gobot Gobot Reviewed-by: Rebecca Stambler --- internal/lsp/diff/apply_edits.go | 54 -------------- internal/lsp/diff/diff.go | 74 +++++++++++++------ .../{apply_edits_test.go => diff_test.go} | 0 internal/lsp/diff/unified.go | 57 ++++++++++++-- 4 files changed, 105 insertions(+), 80 deletions(-) delete mode 100644 internal/lsp/diff/apply_edits.go rename internal/lsp/diff/{apply_edits_test.go => diff_test.go} (100%) diff --git a/internal/lsp/diff/apply_edits.go b/internal/lsp/diff/apply_edits.go deleted file mode 100644 index a7ecdf83e1..0000000000 --- a/internal/lsp/diff/apply_edits.go +++ /dev/null @@ -1,54 +0,0 @@ -package diff - -import ( - "bytes" - "sort" - - "golang.org/x/tools/internal/span" -) - -func ApplyEdits(before string, edits []TextEdit) string { - // Preconditions: - // - all of the edits apply to before - // - and all the spans for each TextEdit have the same URI - - // copy edits so we don't make a mess of the caller's slice - s := make([]TextEdit, len(edits)) - copy(s, edits) - edits = s - - // TODO(matloob): Initialize the Converter Once? - var conv span.Converter = span.NewContentConverter("", []byte(before)) - offset := func(point span.Point) int { - if point.HasOffset() { - return point.Offset() - } - offset, err := conv.ToOffset(point.Line(), point.Column()) - if err != nil { - panic(err) - } - return offset - } - - // sort the copy - sort.Slice(edits, func(i, j int) bool { return offset(edits[i].Span.Start()) < offset(edits[j].Span.Start()) }) - - var after bytes.Buffer - beforeOffset := 0 - for _, edit := range edits { - if offset(edit.Span.Start()) < beforeOffset { - panic("overlapping edits") // TODO(matloob): ApplyEdits doesn't return an error. What do we do? - } else if offset(edit.Span.Start()) > beforeOffset { - after.WriteString(before[beforeOffset:offset(edit.Span.Start())]) - beforeOffset = offset(edit.Span.Start()) - } - // offset(edit.Span.Start) is now equal to beforeOffset - after.WriteString(edit.NewText) - beforeOffset += offset(edit.Span.End()) - offset(edit.Span.Start()) - } - if beforeOffset < len(before) { - after.WriteString(before[beforeOffset:]) - beforeOffset = len(before[beforeOffset:]) // just to preserve invariants - } - return after.String() -} diff --git a/internal/lsp/diff/diff.go b/internal/lsp/diff/diff.go index 3710c7a34a..eb4fa6d8c5 100644 --- a/internal/lsp/diff/diff.go +++ b/internal/lsp/diff/diff.go @@ -6,6 +6,7 @@ package diff import ( + "bytes" "sort" "golang.org/x/tools/internal/span" @@ -18,29 +19,10 @@ type TextEdit struct { NewText string } +// ComputeEdits is the type for a function that produces a set of edits that +// convert from the before content to the after content. type ComputeEdits func(uri span.URI, before, after string) []TextEdit -type OpKind int - -const ( - Delete OpKind = iota - Insert - Equal -) - -func (k OpKind) String() string { - switch k { - case Delete: - return "delete" - case Insert: - return "insert" - case Equal: - return "equal" - default: - panic("unknown operation kind") - } -} - // SortTextEdits attempts to order all edits by their starting points. // The sort is stable so that edits with the same starting point will not // be reordered. @@ -50,3 +32,53 @@ func SortTextEdits(d []TextEdit) { return span.Compare(d[i].Span, d[j].Span) < 0 }) } + +// ApplyEdits applies the set of edits to the before and returns the resulting +// content. +// It may panic or produce garbage if the edits are not valid for the provided +// before content. +func ApplyEdits(before string, edits []TextEdit) string { + // Preconditions: + // - all of the edits apply to before + // - and all the spans for each TextEdit have the same URI + + // copy edits so we don't make a mess of the caller's slice + s := make([]TextEdit, len(edits)) + copy(s, edits) + edits = s + + // TODO(matloob): Initialize the Converter Once? + var conv span.Converter = span.NewContentConverter("", []byte(before)) + offset := func(point span.Point) int { + if point.HasOffset() { + return point.Offset() + } + offset, err := conv.ToOffset(point.Line(), point.Column()) + if err != nil { + panic(err) + } + return offset + } + + // sort the copy + sort.Slice(edits, func(i, j int) bool { return offset(edits[i].Span.Start()) < offset(edits[j].Span.Start()) }) + + var after bytes.Buffer + beforeOffset := 0 + for _, edit := range edits { + if offset(edit.Span.Start()) < beforeOffset { + panic("overlapping edits") // TODO(matloob): ApplyEdits doesn't return an error. What do we do? + } else if offset(edit.Span.Start()) > beforeOffset { + after.WriteString(before[beforeOffset:offset(edit.Span.Start())]) + beforeOffset = offset(edit.Span.Start()) + } + // offset(edit.Span.Start) is now equal to beforeOffset + after.WriteString(edit.NewText) + beforeOffset += offset(edit.Span.End()) - offset(edit.Span.Start()) + } + if beforeOffset < len(before) { + after.WriteString(before[beforeOffset:]) + beforeOffset = len(before[beforeOffset:]) // just to preserve invariants + } + return after.String() +} diff --git a/internal/lsp/diff/apply_edits_test.go b/internal/lsp/diff/diff_test.go similarity index 100% rename from internal/lsp/diff/apply_edits_test.go rename to internal/lsp/diff/diff_test.go diff --git a/internal/lsp/diff/unified.go b/internal/lsp/diff/unified.go index 4c10c43b51..5cc93865c4 100644 --- a/internal/lsp/diff/unified.go +++ b/internal/lsp/diff/unified.go @@ -11,27 +11,72 @@ import ( "golang.org/x/tools/internal/span" ) +// Unified represents a set of edits as a unified diff. type Unified struct { - From, To string - Hunks []*Hunk + // 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 - ToLine int - Lines []Line + // 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 OpKind + // 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, @@ -121,6 +166,8 @@ func addEqualLines(h *Hunk, lines []string, start, end int) int { 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