gopls/internal/golang/completion: fix package clause completion suffix

CL 585275 introduced a call to completion.Surrounding.Suffix that
exposed a latent bug in package clause completions: the completion
content was not being correctly computed as containing the cursor.

Fix the heuristics for completing at "package<whitespace>|", so that the
surrounding content is correct.

As a result of this change the 'editRegexp' from some completion tests
now no longer includes the newline, which also seems more correct.

Fixes golang/go#68169

Change-Id: I32ea20a7f9f0096aef85ed77c64d3b4dfeedef45
Reviewed-on: https://go-review.googlesource.com/c/tools/+/594796
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
Auto-Submit: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/golang/completion/package.go b/gopls/internal/golang/completion/package.go
index ba9ddc2..e71f5c9 100644
--- a/gopls/internal/golang/completion/package.go
+++ b/gopls/internal/golang/completion/package.go
@@ -106,39 +106,20 @@
 
 	// First, consider the possibility that we have a valid "package" keyword
 	// with an empty package name ("package "). "package" is parsed as an
-	// *ast.BadDecl since it is a keyword. This logic would allow "package" to
-	// appear on any line of the file as long as it's the first code expression
-	// in the file.
-	lines := strings.Split(string(pgf.Src), "\n")
-	cursorLine := safetoken.Line(tok, cursor)
-	if cursorLine <= 0 || cursorLine > len(lines) {
-		return nil, fmt.Errorf("invalid line number")
+	// *ast.BadDecl since it is a keyword.
+	start, err := safetoken.Offset(tok, expr.Pos())
+	if err != nil {
+		return nil, err
 	}
-	if safetoken.StartPosition(fset, expr.Pos()).Line == cursorLine {
-		words := strings.Fields(lines[cursorLine-1])
-		if len(words) > 0 && words[0] == PACKAGE {
-			content := PACKAGE
-			// Account for spaces if there are any.
-			if len(words) > 1 {
-				content += " "
-			}
-
-			start := expr.Pos()
-			end := token.Pos(int(expr.Pos()) + len(content) + 1)
-			// We have verified that we have a valid 'package' keyword as our
-			// first expression. Ensure that cursor is in this keyword or
-			// otherwise fallback to the general case.
-			if cursor >= start && cursor <= end {
-				return &Selection{
-					content: content,
-					cursor:  cursor,
-					tokFile: tok,
-					start:   start,
-					end:     end,
-					mapper:  m,
-				}, nil
-			}
-		}
+	if offset > start && string(bytes.TrimRight(pgf.Src[start:offset], " ")) == PACKAGE {
+		return &Selection{
+			content: string(pgf.Src[start:offset]),
+			cursor:  cursor,
+			tokFile: tok,
+			start:   expr.Pos(),
+			end:     cursor,
+			mapper:  m,
+		}, nil
 	}
 
 	// If the cursor is after the start of the expression, no package
diff --git a/gopls/internal/test/integration/completion/completion_test.go b/gopls/internal/test/integration/completion/completion_test.go
index 0ca46da..c96e569 100644
--- a/gopls/internal/test/integration/completion/completion_test.go
+++ b/gopls/internal/test/integration/completion/completion_test.go
@@ -121,11 +121,11 @@
 			want:          nil,
 		},
 		{
-			name:          "package completion after keyword 'package'",
+			name:          "package completion after package keyword",
 			filename:      "fruits/testfile2.go",
 			triggerRegexp: "package()",
 			want:          []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"},
-			editRegexp:    "package\n",
+			editRegexp:    "package",
 		},
 		{
 			name:          "package completion with 'pac' prefix",
@@ -164,14 +164,14 @@
 			filename:      "123f_r.u~its-123/testfile.go",
 			triggerRegexp: "package()",
 			want:          []string{"package fruits123", "package fruits123_test", "package main"},
-			editRegexp:    "package\n",
+			editRegexp:    "package",
 		},
 		{
 			name:          "package completion for invalid dir name",
 			filename:      ".invalid-dir@-name/testfile.go",
 			triggerRegexp: "package()",
 			want:          []string{"package main"},
-			editRegexp:    "package\n",
+			editRegexp:    "package",
 		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
diff --git a/gopls/internal/test/integration/completion/fixedbugs_test.go b/gopls/internal/test/integration/completion/fixedbugs_test.go
new file mode 100644
index 0000000..5269584
--- /dev/null
+++ b/gopls/internal/test/integration/completion/fixedbugs_test.go
@@ -0,0 +1,40 @@
+// Copyright 2024 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 completion
+
+import (
+	"testing"
+
+	. "golang.org/x/tools/gopls/internal/test/integration"
+)
+
+func TestPackageCompletionCrash_Issue68169(t *testing.T) {
+	// This test reproduces the scenario of golang/go#68169, a crash in
+	// completion.Selection.Suffix.
+	//
+	// The file content here is extracted from the issue.
+	const files = `
+-- go.mod --
+module example.com
+
+go 1.18
+-- playdos/play.go --
+package  
+`
+
+	Run(t, files, func(t *testing.T, env *Env) {
+		env.OpenFile("playdos/play.go")
+		// Previously, this call would crash gopls as it was incorrectly computing
+		// the surrounding completion suffix.
+		completions := env.Completion(env.RegexpSearch("playdos/play.go", "package  ()"))
+		if len(completions.Items) == 0 {
+			t.Fatal("Completion() returned empty results")
+		}
+		// Sanity check: we should get package clause compeltion.
+		if got, want := completions.Items[0].Label, "package playdos"; got != want {
+			t.Errorf("Completion()[0].Label == %s, want %s", got, want)
+		}
+	})
+}