gopls/internal/lsp/fake: fix EOF bug in applyEdits

The previous logic would map EOF to a byte offset one beyond
the actual file length.

Fixed golang/go#57627

Change-Id: I6b2e33e1195bbf567752c0e13164e234fd1457a6
Reviewed-on: https://go-review.googlesource.com/c/tools/+/460856
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
diff --git a/gopls/internal/lsp/fake/edit.go b/gopls/internal/lsp/fake/edit.go
index 3eb13ea..7f15a41 100644
--- a/gopls/internal/lsp/fake/edit.go
+++ b/gopls/internal/lsp/fake/edit.go
@@ -114,26 +114,33 @@
 	src := strings.Join(lines, "\n")
 
 	// Build a table of byte offset of start of each line.
-	lineOffset := make([]int, len(lines)+1)
+	lineOffset := make([]int, len(lines))
 	offset := 0
 	for i, line := range lines {
 		lineOffset[i] = offset
 		offset += len(line) + len("\n")
 	}
-	lineOffset[len(lines)] = offset // EOF
 
-	var badCol error
+	var posErr error
 	posToOffset := func(pos Pos) int {
-		offset := lineOffset[pos.Line]
 		// Convert pos.Column (runes) to a UTF-8 byte offset.
-		if pos.Line < len(lines) {
-			for i := 0; i < pos.Column; i++ {
-				r, sz := utf8.DecodeRuneInString(src[offset:])
-				if r == '\n' && badCol == nil {
-					badCol = fmt.Errorf("bad column")
-				}
-				offset += sz
+		if pos.Line > len(lines) {
+			posErr = fmt.Errorf("bad line")
+			return 0
+		}
+		if pos.Line == len(lines) {
+			if pos.Column > 0 {
+				posErr = fmt.Errorf("bad column")
 			}
+			return len(src) // EOF
+		}
+		offset := lineOffset[pos.Line]
+		for i := 0; i < pos.Column; i++ {
+			r, sz := utf8.DecodeRuneInString(src[offset:])
+			if r == '\n' && posErr == nil {
+				posErr = fmt.Errorf("bad column")
+			}
+			offset += sz
 		}
 		return offset
 	}
@@ -153,5 +160,5 @@
 		return nil, err
 	}
 
-	return strings.Split(patched, "\n"), badCol
+	return strings.Split(patched, "\n"), posErr
 }
diff --git a/gopls/internal/lsp/fake/edit_test.go b/gopls/internal/lsp/fake/edit_test.go
index f87d921..fc42117 100644
--- a/gopls/internal/lsp/fake/edit_test.go
+++ b/gopls/internal/lsp/fake/edit_test.go
@@ -47,6 +47,23 @@
 			want: "ABC\nD12\n345\nJKL",
 		},
 		{
+			label:   "regression test for issue #57627",
+			content: "go 1.18\nuse moda/a",
+			edits: []Edit{
+				{
+					Start: Pos{Line: 1, Column: 0},
+					End:   Pos{Line: 1, Column: 0},
+					Text:  "\n",
+				},
+				{
+					Start: Pos{Line: 2, Column: 0},
+					End:   Pos{Line: 2, Column: 0},
+					Text:  "\n",
+				},
+			},
+			want: "go 1.18\n\nuse moda/a\n",
+		},
+		{
 			label:   "end before start",
 			content: "ABC\nDEF\nGHI\nJKL",
 			edits: []Edit{{
diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go
index 08ab21e..1d2ade2 100644
--- a/gopls/internal/regtest/modfile/modfile_test.go
+++ b/gopls/internal/regtest/modfile/modfile_test.go
@@ -1185,3 +1185,17 @@
 		env.Await(EmptyDiagnostics("go.mod"))
 	})
 }
+
+// This is a regression test for a bug in the line-oriented implementation
+// of the "apply diffs" operation used by the fake editor.
+func TestIssue57627(t *testing.T) {
+	const files = `
+-- go.work --
+package main
+`
+	Run(t, files, func(t *testing.T, env *Env) {
+		env.OpenFile("go.work")
+		env.SetBufferContent("go.work", "go 1.18\nuse moda/a")
+		env.SaveBuffer("go.work") // doesn't fail
+	})
+}