Skip to content

Commit

Permalink
internal/lsp: abstract the diff library so it can be substituted
Browse files Browse the repository at this point in the history
 this moves the actual diff algorithm into a different package and then provides hooks so it can be easily replaced with an alternate algorithm.

Change-Id: Ia0359f58878493599ea0e0fda8920f21100e16f1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/190898
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
  • Loading branch information
Ian Cottrell authored and ianthehat committed Aug 20, 2019
1 parent d9ab56a commit 85edb9e
Show file tree
Hide file tree
Showing 18 changed files with 192 additions and 143 deletions.
8 changes: 2 additions & 6 deletions internal/lsp/cmd/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ import (
"flag"
"fmt"
"io/ioutil"
"strings"

"golang.org/x/tools/internal/lsp"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
errors "golang.org/x/xerrors"
)
Expand Down Expand Up @@ -82,9 +80,7 @@ func (f *format) Run(ctx context.Context, args ...string) error {
if err != nil {
return errors.Errorf("%v: %v", spn, err)
}
ops := source.EditsToDiff(sedits)
lines := diff.SplitLines(string(file.mapper.Content))
formatted := strings.Join(diff.ApplyEdits(lines, ops), "")
formatted := diff.ApplyEdits(string(file.mapper.Content), sedits)
printIt := true
if f.List {
printIt = false
Expand All @@ -100,7 +96,7 @@ func (f *format) Run(ctx context.Context, args ...string) error {
}
if f.Diff {
printIt = false
u := diff.ToUnified(filename+".orig", filename, lines, ops)
u := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits)
fmt.Print(u)
}
if printIt {
Expand Down
32 changes: 32 additions & 0 deletions internal/lsp/diff/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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 supports a pluggable diff algorithm.
package diff

import (
"sort"

"golang.org/x/tools/internal/span"
)

// TextEdit represents a change to a section of a document.
// The text within the specified span should be replaced by the supplied new text.
type TextEdit struct {
Span span.Span
NewText string
}

var (
ComputeEdits func(uri span.URI, before, after string) []TextEdit
ApplyEdits func(before string, edits []TextEdit) string
ToUnified func(from, to string, before string, edits []TextEdit) string
)

func SortTextEdits(d []TextEdit) {
// Use a stable sort to maintain the order of edits inserted at the same position.
sort.SliceStable(d, func(i int, j int) bool {
return span.Compare(d[i].Span, d[j].Span) < 0
})
}
80 changes: 80 additions & 0 deletions internal/lsp/diff/myers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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"

"golang.org/x/tools/internal/lsp/diff/myers"
"golang.org/x/tools/internal/span"
)

func init() {
ComputeEdits = myersComputeEdits
ApplyEdits = myersApplyEdits
ToUnified = myersToUnified
}

func myersComputeEdits(uri span.URI, before, after string) []TextEdit {
u := myers.SplitLines(before)
f := myers.SplitLines(after)
return myersDiffToEdits(uri, myers.Operations(u, f))
}

func myersApplyEdits(before string, edits []TextEdit) string {
ops := myersEditsToDiff(edits)
return strings.Join(myers.ApplyEdits(myers.SplitLines(before), ops), "")
}

func myersToUnified(from, to string, before string, edits []TextEdit) string {
u := myers.SplitLines(before)
ops := myersEditsToDiff(edits)
return fmt.Sprint(myers.ToUnified(from, to, u, ops))
}

func myersDiffToEdits(uri span.URI, ops []*myers.Op) []TextEdit {
edits := make([]TextEdit, 0, len(ops))
for _, op := range ops {
s := span.New(uri, span.NewPoint(op.I1+1, 1, 0), span.NewPoint(op.I2+1, 1, 0))
switch op.Kind {
case myers.Delete:
// Delete: unformatted[i1:i2] is deleted.
edits = append(edits, TextEdit{Span: s})
case myers.Insert:
// Insert: formatted[j1:j2] is inserted at unformatted[i1:i1].
if content := strings.Join(op.Content, ""); content != "" {
edits = append(edits, TextEdit{Span: s, NewText: content})
}
}
}
return edits
}

func myersEditsToDiff(edits []TextEdit) []*myers.Op {
iToJ := 0
ops := make([]*myers.Op, len(edits))
for i, edit := range edits {
i1 := edit.Span.Start().Line() - 1
i2 := edit.Span.End().Line() - 1
kind := myers.Insert
if edit.NewText == "" {
kind = myers.Delete
}
ops[i] = &myers.Op{
Kind: kind,
Content: myers.SplitLines(edit.NewText),
I1: i1,
I2: i2,
J1: i1 + iToJ,
}
if kind == myers.Insert {
iToJ += len(ops[i].Content)
} else {
iToJ -= i2 - i1
}
}
return ops
}
4 changes: 2 additions & 2 deletions internal/lsp/diff/diff.go → internal/lsp/diff/myers/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package diff implements the Myers diff algorithm.
package diff
// Package myers implements the Myers diff algorithm.
package myers

import "strings"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package diff_test
package myers_test

import (
"flag"
Expand All @@ -14,7 +14,7 @@ import (
"strings"
"testing"

"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/diff/myers"
)

const (
Expand All @@ -28,22 +28,22 @@ var verifyDiff = flag.Bool("verify-diff", false, "Check that the unified diff ou
func TestDiff(t *testing.T) {
for _, test := range []struct {
a, b string
lines []*diff.Op
operations []*diff.Op
lines []*myers.Op
operations []*myers.Op
unified string
nodiff bool
}{
{
a: "A\nB\nC\n",
b: "A\nB\nC\n",
operations: []*diff.Op{},
operations: []*myers.Op{},
unified: `
`[1:]}, {
a: "A\n",
b: "B\n",
operations: []*diff.Op{
&diff.Op{Kind: diff.Delete, I1: 0, I2: 1, J1: 0},
&diff.Op{Kind: diff.Insert, Content: []string{"B\n"}, I1: 1, I2: 1, J1: 0},
operations: []*myers.Op{
&myers.Op{Kind: myers.Delete, I1: 0, I2: 1, J1: 0},
&myers.Op{Kind: myers.Insert, Content: []string{"B\n"}, I1: 1, I2: 1, J1: 0},
},
unified: `
@@ -1 +1 @@
Expand All @@ -52,9 +52,9 @@ func TestDiff(t *testing.T) {
`[1:]}, {
a: "A",
b: "B",
operations: []*diff.Op{
&diff.Op{Kind: diff.Delete, I1: 0, I2: 1, J1: 0},
&diff.Op{Kind: diff.Insert, Content: []string{"B"}, I1: 1, I2: 1, J1: 0},
operations: []*myers.Op{
&myers.Op{Kind: myers.Delete, I1: 0, I2: 1, J1: 0},
&myers.Op{Kind: myers.Insert, Content: []string{"B"}, I1: 1, I2: 1, J1: 0},
},
unified: `
@@ -1 +1 @@
Expand All @@ -65,12 +65,12 @@ func TestDiff(t *testing.T) {
`[1:]}, {
a: "A\nB\nC\nA\nB\nB\nA\n",
b: "C\nB\nA\nB\nA\nC\n",
operations: []*diff.Op{
&diff.Op{Kind: diff.Delete, I1: 0, I2: 1, J1: 0},
&diff.Op{Kind: diff.Delete, I1: 1, I2: 2, J1: 0},
&diff.Op{Kind: diff.Insert, Content: []string{"B\n"}, I1: 3, I2: 3, J1: 1},
&diff.Op{Kind: diff.Delete, I1: 5, I2: 6, J1: 4},
&diff.Op{Kind: diff.Insert, Content: []string{"C\n"}, I1: 7, I2: 7, J1: 5},
operations: []*myers.Op{
&myers.Op{Kind: myers.Delete, I1: 0, I2: 1, J1: 0},
&myers.Op{Kind: myers.Delete, I1: 1, I2: 2, J1: 0},
&myers.Op{Kind: myers.Insert, Content: []string{"B\n"}, I1: 3, I2: 3, J1: 1},
&myers.Op{Kind: myers.Delete, I1: 5, I2: 6, J1: 4},
&myers.Op{Kind: myers.Insert, Content: []string{"C\n"}, I1: 7, I2: 7, J1: 5},
},
unified: `
@@ -1,7 +1,6 @@
Expand All @@ -89,10 +89,10 @@ func TestDiff(t *testing.T) {
{
a: "A\nB\n",
b: "A\nC\n\n",
operations: []*diff.Op{
&diff.Op{Kind: diff.Delete, I1: 1, I2: 2, J1: 1},
&diff.Op{Kind: diff.Insert, Content: []string{"C\n"}, I1: 2, I2: 2, J1: 1},
&diff.Op{Kind: diff.Insert, Content: []string{"\n"}, I1: 2, I2: 2, J1: 2},
operations: []*myers.Op{
&myers.Op{Kind: myers.Delete, I1: 1, I2: 2, J1: 1},
&myers.Op{Kind: myers.Insert, Content: []string{"C\n"}, I1: 2, I2: 2, J1: 1},
&myers.Op{Kind: myers.Insert, Content: []string{"\n"}, I1: 2, I2: 2, J1: 2},
},
unified: `
@@ -1,2 +1,3 @@
Expand Down Expand Up @@ -120,9 +120,9 @@ func TestDiff(t *testing.T) {
+K
`[1:]},
} {
a := diff.SplitLines(test.a)
b := diff.SplitLines(test.b)
ops := diff.Operations(a, b)
a := myers.SplitLines(test.a)
b := myers.SplitLines(test.b)
ops := myers.Operations(a, b)
if test.operations != nil {
if len(ops) != len(test.operations) {
t.Fatalf("expected %v operations, got %v", len(test.operations), len(ops))
Expand All @@ -134,15 +134,15 @@ func TestDiff(t *testing.T) {
}
}
}
applied := diff.ApplyEdits(a, ops)
applied := myers.ApplyEdits(a, ops)
for i, want := range applied {
got := b[i]
if got != want {
t.Errorf("expected %v got %v", want, got)
}
}
if test.unified != "" {
diff := diff.ToUnified(fileA, fileB, a, ops)
diff := myers.ToUnified(fileA, fileB, a, ops)
got := fmt.Sprint(diff)
if !strings.HasPrefix(got, unifiedPrefix) {
t.Errorf("expected prefix:\n%s\ngot:\n%s", unifiedPrefix, got)
Expand All @@ -166,7 +166,7 @@ func TestDiff(t *testing.T) {
}

func getDiffOutput(a, b string) (string, error) {
fileA, err := ioutil.TempFile("", "diff.in")
fileA, err := ioutil.TempFile("", "myers.in")
if err != nil {
return "", err
}
Expand All @@ -177,7 +177,7 @@ func getDiffOutput(a, b string) (string, error) {
if err := fileA.Close(); err != nil {
return "", err
}
fileB, err := ioutil.TempFile("", "diff.in")
fileB, err := ioutil.TempFile("", "myers.in")
if err != nil {
return "", err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package diff
package myers

import (
"fmt"
Expand Down
9 changes: 5 additions & 4 deletions internal/lsp/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package lsp
import (
"context"

"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
Expand Down Expand Up @@ -51,7 +52,7 @@ func spanToRange(ctx context.Context, view source.View, spn span.Span) (source.G
return f, m, rng, nil
}

func ToProtocolEdits(m *protocol.ColumnMapper, edits []source.TextEdit) ([]protocol.TextEdit, error) {
func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.TextEdit) ([]protocol.TextEdit, error) {
if edits == nil {
return nil, nil
}
Expand All @@ -69,17 +70,17 @@ func ToProtocolEdits(m *protocol.ColumnMapper, edits []source.TextEdit) ([]proto
return result, nil
}

func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]source.TextEdit, error) {
func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.TextEdit, error) {
if edits == nil {
return nil, nil
}
result := make([]source.TextEdit, len(edits))
result := make([]diff.TextEdit, len(edits))
for i, edit := range edits {
spn, err := m.RangeSpan(edit.Range)
if err != nil {
return nil, err
}
result[i] = source.TextEdit{
result[i] = diff.TextEdit{
Span: spn,
NewText: edit.NewText,
}
Expand Down
8 changes: 3 additions & 5 deletions internal/lsp/lsp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,7 @@ func (r *runner) Format(t *testing.T, data tests.Formats) {
if err != nil {
t.Error(err)
}
ops := source.EditsToDiff(sedits)
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(m.Content)), ops), "")
got := diff.ApplyEdits(string(m.Content), sedits)
if gofmted != got {
t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, gofmted, got)
}
Expand Down Expand Up @@ -334,8 +333,7 @@ func (r *runner) Import(t *testing.T, data tests.Imports) {
if err != nil {
t.Error(err)
}
ops := source.EditsToDiff(sedits)
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(m.Content)), ops), "")
got := diff.ApplyEdits(string(m.Content), sedits)
if goimported != got {
t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, goimported, got)
}
Expand Down Expand Up @@ -549,7 +547,7 @@ func (r *runner) Rename(t *testing.T, data tests.Renames) {
}
}

func applyEdits(contents string, edits []source.TextEdit) string {
func applyEdits(contents string, edits []diff.TextEdit) string {
res := contents

// Apply the edits from the end of the file forward
Expand Down
3 changes: 2 additions & 1 deletion internal/lsp/source/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/fuzzy"
"golang.org/x/tools/internal/lsp/snippet"
"golang.org/x/tools/internal/span"
Expand Down Expand Up @@ -41,7 +42,7 @@ type CompletionItem struct {
// Additional text edits should be used to change text unrelated to the current cursor position
// (for example adding an import statement at the top of the file if the completion item will
// insert an unqualified type).
AdditionalTextEdits []TextEdit
AdditionalTextEdits []diff.TextEdit

// Depth is how many levels were searched to find this completion.
// For example when completing "foo<>", "fooBar" is depth 0, and
Expand Down
Loading

0 comments on commit 85edb9e

Please sign in to comment.