Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: put continuous insertions with the same line in order of applying #282

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
)

require (
github.com/google/go-cmp v0.6.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/haya14busa/go-checkstyle v0.0.0-20170303121022-5e9d09f51fa1/go.mod h1:RsN5RGgVYeXpcXNtWyztD5VIe7VNSEqpJvF2iEH7QvI=
Expand Down
71 changes: 48 additions & 23 deletions langserver/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,57 +7,82 @@
package langserver

import (
"slices"
"strings"
)

// 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
)

// Sources:
// https://blog.jcoglan.com/2017/02/17/the-myers-diff-algorithm-part-3/
// https://www.codeproject.com/Articles/42279/%2FArticles%2F42279%2FInvestigating-Myers-diff-algorithm-Part-1-of-2

// ComputeEdits computes diff edits from 2 string inputs
// ComputeEdits returns the diffs of two strings using a simple
// line-based implementation, like [diff.Strings].
func ComputeEdits(_ DocumentURI, before, after string) []TextEdit {
ops := operations(splitLines(before), splitLines(after))
edits := make([]TextEdit, 0, len(ops))

// If there're some insertions on the same line, they must be reversed to be applied in order.
// So memorize the last insertion line and its index.
var insHankLine int
var insHankIndex int
for _, op := range ops {
switch op.Kind {
case Delete:
case opDelete:
// Delete: unformatted[i1:i2] is deleted.
edits = append(edits, TextEdit{Range: Range{
Start: Position{Line: op.I1, Character: 0},
End: Position{Line: op.I2, Character: 0},
}})
case Insert:
insHankLine = -1 // Reset insertion hanking
case opInsert:
// Insert: formatted[j1:j2] is inserted at unformatted[i1:i1].
if content := strings.Join(op.Content, ""); content != "" {
edits = append(edits, TextEdit{
newEdit := TextEdit{
Range: Range{
Start: Position{Line: op.I1, Character: 0},
End: Position{Line: op.I2, Character: 0},
},
NewText: content,
})
}

if insHankLine == op.I1 {
// If there're some insertion on the same line, insert it before the hank of the last insertions.
edits = slices.Insert(edits, insHankIndex, newEdit)
} else {
insHankLine = op.I1
insHankIndex = len(edits)
edits = append(edits, newEdit)
}
}
}
}
return edits
}

// opKind is used to denote the type of operation a line represents.
type opKind int

const (
opDelete opKind = iota // line deleted from input (-)
opInsert // line inserted into output (+)
opEqual // line present in input and output
)

func (kind opKind) String() string {
switch kind {
case opDelete:
return "delete"
case opInsert:
return "insert"
case opEqual:
return "equal"
default:
panic("unknown opKind")
}
}

type operation struct {
Kind OpKind
Kind opKind
Content []string // content from b
I1, I2 int // indices of the line in a
J1 int // indices of the line in b, J2 implied by len(Content)
Expand All @@ -83,7 +108,7 @@ func operations(a, b []string) []*operation {
return
}
op.I2 = i2
if op.Kind == Insert {
if op.Kind == opInsert {
op.Content = b[op.J1:j2]
}
solution[i] = op
Expand All @@ -99,7 +124,7 @@ func operations(a, b []string) []*operation {
for snake[0]-snake[1] > x-y {
if op == nil {
op = &operation{
Kind: Delete,
Kind: opDelete,
I1: x,
J1: y,
}
Expand All @@ -115,7 +140,7 @@ func operations(a, b []string) []*operation {
for snake[0]-snake[1] < x-y {
if op == nil {
op = &operation{
Kind: Insert,
Kind: opInsert,
I1: x,
J1: y,
}
Expand Down
62 changes: 62 additions & 0 deletions langserver/diff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package langserver_test

import (
"testing"

"github.com/google/go-cmp/cmp"

"github.com/mattn/efm-langserver/langserver"
)

func TestComputeEdits(t *testing.T) {
cases := []struct {
name string
before, after string
want []langserver.TextEdit
}{
{
name: "Empty",
before: "", after: "", want: []langserver.TextEdit{},
},
{
name: "Empty to one line",
before: "", after: "foo", want: []langserver.TextEdit{
{NewText: "foo"},
},
},
{
name: "One line to another",
before: "foo", after: "bar", want: []langserver.TextEdit{
{Range: langserver.Range{End: langserver.Position{Line: 1}}},
{Range: langserver.Range{Start: langserver.Position{Line: 1}, End: langserver.Position{Line: 1}}, NewText: "bar"},
},
},
{
name: "Replace multi lines (issue 281)",
before: "foo\nbar\nbaz\n", after: "one\ntwo\nthree\n",
want: []langserver.TextEdit{
{Range: langserver.Range{End: langserver.Position{Line: 3}}},
{
Range: langserver.Range{Start: langserver.Position{Line: 3}, End: langserver.Position{Line: 3}},
NewText: "three\n",
},
{
Range: langserver.Range{Start: langserver.Position{Line: 3}, End: langserver.Position{Line: 3}},
NewText: "two\n",
},
{
Range: langserver.Range{Start: langserver.Position{Line: 3}, End: langserver.Position{Line: 3}},
NewText: "one\n",
},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
edits := langserver.ComputeEdits("", c.before, c.after)
if diff := cmp.Diff(c.want, edits); diff != "" {
t.Errorf("unexpected edits (-want +got):\n%s", diff)
}
})
}
}
Loading