gopls: enhance read/write access distinction in document highlighting for symbols

The definition of 'write access' is same as it in GoLand. Some examples are
access to variables in declaration, assignment(left value), self increasing,
channel sending and composite literal.

The algorithm to find write access is same as it in jdt (Java LSP), by
visiting every write statement in ast traversal and collecting the positions
of access to variables.

Fixes golang/go#64579

Change-Id: I497ec7f15906cf4157ad1965e01264eb35ce973b
GitHub-Last-Rev: cee436c69b6d25ebf42ce4f8daeb8fc88ccac793
GitHub-Pull-Request: golang/tools#503
Reviewed-on: https://go-review.googlesource.com/c/tools/+/597675
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
diff --git a/gopls/doc/features/passive.md b/gopls/doc/features/passive.md
index 4d814ac..dc9c138 100644
--- a/gopls/doc/features/passive.md
+++ b/gopls/doc/features/passive.md
@@ -135,6 +135,9 @@
 More than one of these rules may be activated by a single selection,
 for example, by an identifier that is also a return operand.
 
+Different occurrences of the same identifier may be color-coded to distinguish
+"read" from "write" references to a given variable symbol.
+
 <img src='../assets/document-highlight.png'>
 
 Client support:
diff --git a/gopls/internal/golang/highlight.go b/gopls/internal/golang/highlight.go
index ea8a4930..863c09f 100644
--- a/gopls/internal/golang/highlight.go
+++ b/gopls/internal/golang/highlight.go
@@ -19,7 +19,7 @@
 	"golang.org/x/tools/internal/event"
 )
 
-func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Range, error) {
+func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.DocumentHighlight, error) {
 	ctx, done := event.Start(ctx, "golang.Highlight")
 	defer done()
 
@@ -54,28 +54,31 @@
 	if err != nil {
 		return nil, err
 	}
-	var ranges []protocol.Range
-	for rng := range result {
+	var ranges []protocol.DocumentHighlight
+	for rng, kind := range result {
 		rng, err := pgf.PosRange(rng.start, rng.end)
 		if err != nil {
 			return nil, err
 		}
-		ranges = append(ranges, rng)
+		ranges = append(ranges, protocol.DocumentHighlight{
+			Range: rng,
+			Kind:  kind,
+		})
 	}
 	return ranges, nil
 }
 
 // highlightPath returns ranges to highlight for the given enclosing path,
 // which should be the result of astutil.PathEnclosingInterval.
-func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRange]struct{}, error) {
-	result := make(map[posRange]struct{})
+func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRange]protocol.DocumentHighlightKind, error) {
+	result := make(map[posRange]protocol.DocumentHighlightKind)
 	switch node := path[0].(type) {
 	case *ast.BasicLit:
 		// Import path string literal?
 		if len(path) > 1 {
 			if imp, ok := path[1].(*ast.ImportSpec); ok {
 				highlight := func(n ast.Node) {
-					result[posRange{start: n.Pos(), end: n.End()}] = struct{}{}
+					highlightNode(result, n, protocol.Text)
 				}
 
 				// Highlight the import itself...
@@ -124,10 +127,8 @@
 				highlightLoopControlFlow(path, info, result)
 			}
 		}
-	default:
-		// If the cursor is in an unidentified area, return empty results.
-		return nil, nil
 	}
+
 	return result, nil
 }
 
@@ -145,7 +146,7 @@
 //
 // As a special case, if the cursor is within a complicated expression, control
 // flow highlighting is disabled, as it would highlight too much.
-func highlightFuncControlFlow(path []ast.Node, result map[posRange]unit) {
+func highlightFuncControlFlow(path []ast.Node, result map[posRange]protocol.DocumentHighlightKind) {
 
 	var (
 		funcType   *ast.FuncType   // type of enclosing func, or nil
@@ -211,10 +212,7 @@
 
 	if highlightAll {
 		// Add the "func" part of the func declaration.
-		result[posRange{
-			start: funcType.Func,
-			end:   funcEnd,
-		}] = unit{}
+		highlightRange(result, funcType.Func, funcEnd, protocol.Text)
 	} else if returnStmt == nil && !inResults {
 		return // nothing to highlight
 	} else {
@@ -242,7 +240,7 @@
 				for _, field := range funcType.Results.List {
 					for j, name := range field.Names {
 						if inNode(name) || highlightIndexes[i+j] {
-							result[posRange{name.Pos(), name.End()}] = unit{}
+							highlightNode(result, name, protocol.Text)
 							highlightIndexes[i+j] = true
 							break findField // found/highlighted the specific name
 						}
@@ -257,7 +255,7 @@
 					// ...where it would make more sense to highlight only y. But we don't
 					// reach this function if not in a func, return, ident, or basiclit.
 					if inNode(field) || highlightIndexes[i] {
-						result[posRange{field.Pos(), field.End()}] = unit{}
+						highlightNode(result, field, protocol.Text)
 						highlightIndexes[i] = true
 						if inNode(field) {
 							for j := range field.Names {
@@ -286,12 +284,12 @@
 			case *ast.ReturnStmt:
 				if highlightAll {
 					// Add the entire return statement.
-					result[posRange{n.Pos(), n.End()}] = unit{}
+					highlightNode(result, n, protocol.Text)
 				} else {
 					// Add the highlighted indexes.
 					for i, expr := range n.Results {
 						if highlightIndexes[i] {
-							result[posRange{expr.Pos(), expr.End()}] = unit{}
+							highlightNode(result, expr, protocol.Text)
 						}
 					}
 				}
@@ -304,7 +302,7 @@
 }
 
 // highlightUnlabeledBreakFlow highlights the innermost enclosing for/range/switch or swlect
-func highlightUnlabeledBreakFlow(path []ast.Node, info *types.Info, result map[posRange]struct{}) {
+func highlightUnlabeledBreakFlow(path []ast.Node, info *types.Info, result map[posRange]protocol.DocumentHighlightKind) {
 	// Reverse walk the path until we find closest loop, select, or switch.
 	for _, n := range path {
 		switch n.(type) {
@@ -323,7 +321,7 @@
 
 // highlightLabeledFlow highlights the enclosing labeled for, range,
 // or switch statement denoted by a labeled break or continue stmt.
-func highlightLabeledFlow(path []ast.Node, info *types.Info, stmt *ast.BranchStmt, result map[posRange]struct{}) {
+func highlightLabeledFlow(path []ast.Node, info *types.Info, stmt *ast.BranchStmt, result map[posRange]protocol.DocumentHighlightKind) {
 	use := info.Uses[stmt.Label]
 	if use == nil {
 		return
@@ -350,7 +348,7 @@
 	return nil
 }
 
-func highlightLoopControlFlow(path []ast.Node, info *types.Info, result map[posRange]struct{}) {
+func highlightLoopControlFlow(path []ast.Node, info *types.Info, result map[posRange]protocol.DocumentHighlightKind) {
 	var loop ast.Node
 	var loopLabel *ast.Ident
 	stmtLabel := labelFor(path)
@@ -372,11 +370,9 @@
 	}
 
 	// Add the for statement.
-	rng := posRange{
-		start: loop.Pos(),
-		end:   loop.Pos() + token.Pos(len("for")),
-	}
-	result[rng] = struct{}{}
+	rngStart := loop.Pos()
+	rngEnd := loop.Pos() + token.Pos(len("for"))
+	highlightRange(result, rngStart, rngEnd, protocol.Text)
 
 	// Traverse AST to find branch statements within the same for-loop.
 	ast.Inspect(loop, func(n ast.Node) bool {
@@ -391,7 +387,7 @@
 			return true
 		}
 		if b.Label == nil || info.Uses[b.Label] == info.Defs[loopLabel] {
-			result[posRange{start: b.Pos(), end: b.End()}] = struct{}{}
+			highlightNode(result, b, protocol.Text)
 		}
 		return true
 	})
@@ -404,7 +400,7 @@
 		}
 
 		if n, ok := n.(*ast.BranchStmt); ok && n.Tok == token.CONTINUE {
-			result[posRange{start: n.Pos(), end: n.End()}] = struct{}{}
+			highlightNode(result, n, protocol.Text)
 		}
 		return true
 	})
@@ -422,13 +418,13 @@
 		}
 		// statement with labels that matches the loop
 		if b.Label != nil && info.Uses[b.Label] == info.Defs[loopLabel] {
-			result[posRange{start: b.Pos(), end: b.End()}] = struct{}{}
+			highlightNode(result, b, protocol.Text)
 		}
 		return true
 	})
 }
 
-func highlightSwitchFlow(path []ast.Node, info *types.Info, result map[posRange]struct{}) {
+func highlightSwitchFlow(path []ast.Node, info *types.Info, result map[posRange]protocol.DocumentHighlightKind) {
 	var switchNode ast.Node
 	var switchNodeLabel *ast.Ident
 	stmtLabel := labelFor(path)
@@ -450,11 +446,9 @@
 	}
 
 	// Add the switch statement.
-	rng := posRange{
-		start: switchNode.Pos(),
-		end:   switchNode.Pos() + token.Pos(len("switch")),
-	}
-	result[rng] = struct{}{}
+	rngStart := switchNode.Pos()
+	rngEnd := switchNode.Pos() + token.Pos(len("switch"))
+	highlightRange(result, rngStart, rngEnd, protocol.Text)
 
 	// Traverse AST to find break statements within the same switch.
 	ast.Inspect(switchNode, func(n ast.Node) bool {
@@ -471,7 +465,7 @@
 		}
 
 		if b.Label == nil || info.Uses[b.Label] == info.Defs[switchNodeLabel] {
-			result[posRange{start: b.Pos(), end: b.End()}] = struct{}{}
+			highlightNode(result, b, protocol.Text)
 		}
 		return true
 	})
@@ -489,37 +483,115 @@
 		}
 
 		if b.Label != nil && info.Uses[b.Label] == info.Defs[switchNodeLabel] {
-			result[posRange{start: b.Pos(), end: b.End()}] = struct{}{}
+			highlightNode(result, b, protocol.Text)
 		}
 
 		return true
 	})
 }
 
-func highlightIdentifier(id *ast.Ident, file *ast.File, info *types.Info, result map[posRange]struct{}) {
-	highlight := func(n ast.Node) {
-		result[posRange{start: n.Pos(), end: n.End()}] = struct{}{}
+func highlightNode(result map[posRange]protocol.DocumentHighlightKind, n ast.Node, kind protocol.DocumentHighlightKind) {
+	highlightRange(result, n.Pos(), n.End(), kind)
+}
+
+func highlightRange(result map[posRange]protocol.DocumentHighlightKind, pos, end token.Pos, kind protocol.DocumentHighlightKind) {
+	rng := posRange{pos, end}
+	// Order of traversal is important: some nodes (e.g. identifiers) are
+	// visited more than once, but the kind set during the first visitation "wins".
+	if _, exists := result[rng]; !exists {
+		result[rng] = kind
 	}
+}
+
+func highlightIdentifier(id *ast.Ident, file *ast.File, info *types.Info, result map[posRange]protocol.DocumentHighlightKind) {
 
 	// obj may be nil if the Ident is undefined.
 	// In this case, the behavior expected by tests is
 	// to match other undefined Idents of the same name.
 	obj := info.ObjectOf(id)
 
+	highlightIdent := func(n *ast.Ident, kind protocol.DocumentHighlightKind) {
+		if n.Name == id.Name && info.ObjectOf(n) == obj {
+			highlightNode(result, n, kind)
+		}
+	}
+	// highlightWriteInExpr is called for expressions that are
+	// logically on the left side of an assignment.
+	// We follow the behavior of VSCode+Rust and GoLand, which differs
+	// slightly from types.TypeAndValue.Assignable:
+	//     *ptr = 1       // ptr write
+	//     *ptr.field = 1 // ptr read, field write
+	//     s.field = 1    // s read, field write
+	//     array[i] = 1   // array read
+	var highlightWriteInExpr func(expr ast.Expr)
+	highlightWriteInExpr = func(expr ast.Expr) {
+		switch expr := expr.(type) {
+		case *ast.Ident:
+			highlightIdent(expr, protocol.Write)
+		case *ast.SelectorExpr:
+			highlightIdent(expr.Sel, protocol.Write)
+		case *ast.StarExpr:
+			highlightWriteInExpr(expr.X)
+		case *ast.ParenExpr:
+			highlightWriteInExpr(expr.X)
+		}
+	}
+
 	ast.Inspect(file, func(n ast.Node) bool {
 		switch n := n.(type) {
-		case *ast.Ident:
-			if n.Name == id.Name && info.ObjectOf(n) == obj {
-				highlight(n)
+		case *ast.AssignStmt:
+			for _, s := range n.Lhs {
+				highlightWriteInExpr(s)
 			}
-
+		case *ast.GenDecl:
+			if n.Tok == token.CONST || n.Tok == token.VAR {
+				for _, spec := range n.Specs {
+					if spec, ok := spec.(*ast.ValueSpec); ok {
+						for _, ele := range spec.Names {
+							highlightWriteInExpr(ele)
+						}
+					}
+				}
+			}
+		case *ast.IncDecStmt:
+			highlightWriteInExpr(n.X)
+		case *ast.SendStmt:
+			highlightWriteInExpr(n.Chan)
+		case *ast.CompositeLit:
+			t := info.TypeOf(n)
+			if ptr, ok := t.Underlying().(*types.Pointer); ok {
+				t = ptr.Elem()
+			}
+			if _, ok := t.Underlying().(*types.Struct); ok {
+				for _, expr := range n.Elts {
+					if expr, ok := (expr).(*ast.KeyValueExpr); ok {
+						highlightWriteInExpr(expr.Key)
+					}
+				}
+			}
+		case *ast.RangeStmt:
+			highlightWriteInExpr(n.Key)
+			highlightWriteInExpr(n.Value)
+		case *ast.Field:
+			for _, name := range n.Names {
+				highlightIdent(name, protocol.Text)
+			}
+		case *ast.Ident:
+			// This case is reached for all Idents,
+			// including those also visited by highlightWriteInExpr.
+			if is[*types.Var](info.ObjectOf(n)) {
+				highlightIdent(n, protocol.Read)
+			} else {
+				// kind of idents in PkgName, etc. is Text
+				highlightIdent(n, protocol.Text)
+			}
 		case *ast.ImportSpec:
 			pkgname, ok := typesutil.ImportedPkgName(info, n)
 			if ok && pkgname == obj {
 				if n.Name != nil {
-					highlight(n.Name)
+					highlightNode(result, n.Name, protocol.Text)
 				} else {
-					highlight(n)
+					highlightNode(result, n, protocol.Text)
 				}
 			}
 		}
diff --git a/gopls/internal/server/highlight.go b/gopls/internal/server/highlight.go
index f60f01e..35ffc2d 100644
--- a/gopls/internal/server/highlight.go
+++ b/gopls/internal/server/highlight.go
@@ -33,19 +33,7 @@
 		if err != nil {
 			event.Error(ctx, "no highlight", err)
 		}
-		return toProtocolHighlight(rngs), nil
+		return rngs, nil
 	}
 	return nil, nil // empty result
 }
-
-func toProtocolHighlight(rngs []protocol.Range) []protocol.DocumentHighlight {
-	result := make([]protocol.DocumentHighlight, 0, len(rngs))
-	kind := protocol.Text
-	for _, rng := range rngs {
-		result = append(result, protocol.DocumentHighlight{
-			Kind:  kind,
-			Range: rng,
-		})
-	}
-	return result
-}
diff --git a/gopls/internal/test/marker/doc.go b/gopls/internal/test/marker/doc.go
index 5acc4ab..758d08f 100644
--- a/gopls/internal/test/marker/doc.go
+++ b/gopls/internal/test/marker/doc.go
@@ -157,9 +157,21 @@
     source. If the formatting request fails, the golden file must contain
     the error message.
 
-  - highlight(src location, dsts ...location): makes a
+  - highlightall(all ...documentHighlight): makes a textDocument/highlight
+    request at each location in "all" and checks that the result is "all".
+    In other words, given highlightall(X1, X2, ..., Xn), it checks that
+    highlight(X1) = highlight(X2) = ... = highlight(Xn) = {X1, X2, ..., Xn}.
+    In general, highlight sets are not equivalence classes; for asymmetric
+    cases, use @highlight instead.
+    Each element of "all" is the label of a @hiloc marker.
+
+  - highlight(src location, dsts ...documentHighlight): makes a
     textDocument/highlight request at the given src location, which should
-    highlight the provided dst locations.
+    highlight the provided dst locations and kinds.
+
+  - hiloc(label, location, kind): defines a documentHighlight value of the
+    given location and kind. Use its label in a @highlightall marker to
+    indicate the expected result of a highlight query.
 
   - hover(src, dst location, sm stringMatcher): performs a textDocument/hover
     at the src location, and checks that the result is the dst location, with
diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go
index c745686..de43ab6 100644
--- a/gopls/internal/test/marker/marker_test.go
+++ b/gopls/internal/test/marker/marker_test.go
@@ -29,6 +29,7 @@
 	"time"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 
 	"golang.org/x/tools/go/expect"
 	"golang.org/x/tools/gopls/internal/cache"
@@ -506,8 +507,9 @@
 
 // Supported value marker functions. See [valueMarkerFunc] for more details.
 var valueMarkerFuncs = map[string]func(marker){
-	"loc":  valueMarkerFunc(locMarker),
-	"item": valueMarkerFunc(completionItemMarker),
+	"loc":   valueMarkerFunc(locMarker),
+	"item":  valueMarkerFunc(completionItemMarker),
+	"hiloc": valueMarkerFunc(highlightLocationMarker),
 }
 
 // Supported action marker functions. See [actionMarkerFunc] for more details.
@@ -524,6 +526,7 @@
 	"foldingrange":     actionMarkerFunc(foldingRangeMarker),
 	"format":           actionMarkerFunc(formatMarker),
 	"highlight":        actionMarkerFunc(highlightMarker),
+	"highlightall":     actionMarkerFunc(highlightAllMarker),
 	"hover":            actionMarkerFunc(hoverMarker),
 	"hovererr":         actionMarkerFunc(hoverErrMarker),
 	"implementation":   actionMarkerFunc(implementationMarker),
@@ -1593,28 +1596,60 @@
 	compareGolden(mark, got, golden)
 }
 
-func highlightMarker(mark marker, src protocol.Location, dsts ...protocol.Location) {
-	highlights := mark.run.env.DocumentHighlight(src)
-	var got []protocol.Range
-	for _, h := range highlights {
-		got = append(got, h.Range)
+func highlightLocationMarker(mark marker, loc protocol.Location, kindName expect.Identifier) protocol.DocumentHighlight {
+	var kind protocol.DocumentHighlightKind
+	switch kindName {
+	case "read":
+		kind = protocol.Read
+	case "write":
+		kind = protocol.Write
+	case "text":
+		kind = protocol.Text
+	default:
+		mark.errorf("invalid highlight kind: %q", kindName)
 	}
 
-	var want []protocol.Range
-	for _, d := range dsts {
-		want = append(want, d.Range)
+	return protocol.DocumentHighlight{
+		Range: loc.Range,
+		Kind:  kind,
 	}
+}
+func sortDocumentHighlights(s []protocol.DocumentHighlight) {
+	sort.Slice(s, func(i, j int) bool {
+		return protocol.CompareRange(s[i].Range, s[j].Range) < 0
+	})
+}
 
-	sortRanges := func(s []protocol.Range) {
-		sort.Slice(s, func(i, j int) bool {
-			return protocol.CompareRange(s[i], s[j]) < 0
-		})
+// highlightAllMarker makes textDocument/highlight
+// requests at locations of equivalence classes. Given input
+// highlightall(X1, X2, ..., Xn), the marker checks
+// highlight(X1) = highlight(X2) = ... = highlight(Xn) = {X1, X2, ..., Xn}.
+// It is not the general rule for all highlighting, and use @highlight
+// for asymmetric cases.
+//
+// TODO(b/288111111): this is a bit of a hack. We should probably
+// have a more general way of testing that a function is idempotent.
+func highlightAllMarker(mark marker, all ...protocol.DocumentHighlight) {
+	sortDocumentHighlights(all)
+	for _, src := range all {
+		loc := protocol.Location{URI: mark.uri(), Range: src.Range}
+		got := mark.run.env.DocumentHighlight(loc)
+		sortDocumentHighlights(got)
+
+		if d := cmp.Diff(all, got); d != "" {
+			mark.errorf("DocumentHighlight(%v) mismatch (-want +got):\n%s", loc, d)
+		}
 	}
+}
 
-	sortRanges(got)
-	sortRanges(want)
+func highlightMarker(mark marker, src protocol.DocumentHighlight, dsts ...protocol.DocumentHighlight) {
+	loc := protocol.Location{URI: mark.uri(), Range: src.Range}
+	got := mark.run.env.DocumentHighlight(loc)
 
-	if diff := cmp.Diff(want, got); diff != "" {
+	sortDocumentHighlights(got)
+	sortDocumentHighlights(dsts)
+
+	if diff := cmp.Diff(dsts, got, cmpopts.EquateEmpty()); diff != "" {
 		mark.errorf("DocumentHighlight(%v) mismatch (-want +got):\n%s", src, diff)
 	}
 }
diff --git a/gopls/internal/test/marker/testdata/highlight/controlflow.txt b/gopls/internal/test/marker/testdata/highlight/controlflow.txt
index 25cc939..c09f748 100644
--- a/gopls/internal/test/marker/testdata/highlight/controlflow.txt
+++ b/gopls/internal/test/marker/testdata/highlight/controlflow.txt
@@ -11,12 +11,12 @@
 -- issue60589.go --
 package p
 
-// This test verifies that control flow lighlighting correctly
+// This test verifies that control flow highlighting correctly
 // accounts for multi-name result parameters.
 // In golang/go#60589, it did not.
 
-func _() (foo int, bar, baz string) { //@ loc(func, "func"), loc(foo, "foo"), loc(fooint, "foo int"), loc(int, "int"), loc(bar, "bar"), loc(beforebaz, " baz"), loc(baz, "baz"), loc(barbazstring, "bar, baz string"), loc(beforestring, re`() string`), loc(string, "string")
-	return 0, "1", "2" //@ loc(return, `return 0, "1", "2"`), loc(l0, "0"), loc(l1, `"1"`), loc(l2, `"2"`)
+func _() (foo int, bar, baz string) { //@ hiloc(func, "func", text), hiloc(foo, "foo", text), hiloc(fooint, "foo int", text), hiloc(int, "int", text), hiloc(bar, "bar", text), hiloc(beforebaz, " baz", text), hiloc(baz, "baz", text), hiloc(barbazstring, "bar, baz string", text), hiloc(beforestring, re`() string`, text), hiloc(string, "string", text)
+	return 0, "1", "2" //@ hiloc(return, `return 0, "1", "2"`, text), hiloc(l0, "0", text), hiloc(l1, `"1"`, text), hiloc(l2, `"2"`, text)
 }
 
 // Assertions, expressed here to avoid clutter above.
@@ -38,8 +38,8 @@
 // Check that duplicate result names do not cause
 // inaccurate highlighting.
 
-func _() (x, x int32) { //@ loc(x1, re`\((x)`), loc(x2, re`(x) int`), diag(x1, re"redeclared"), diag(x2, re"redeclared")
-	return 1, 2 //@ loc(one, "1"), loc(two, "2")
+func _() (x, x int32) { //@ loc(locx1, re`\((x)`), loc(locx2, re`(x) int`), hiloc(x1, re`\((x)`, text), hiloc(x2, re`(x) int`, text), diag(locx1, re"redeclared"), diag(locx2, re"redeclared")
+	return 1, 2 //@ hiloc(one, "1", text), hiloc(two, "2", text)
 }
 
 //@ highlight(one, one, x1)
@@ -53,7 +53,8 @@
 // This test checks that gopls doesn't crash while highlighting
 // functions with no body (golang/go#65516).
 
-func Foo() (int, string) //@highlight("int", "int"), highlight("func", "func")
+func Foo() (int, string) //@hiloc(noBodyInt, "int", text), hiloc(noBodyFunc, "func", text)
+//@highlight(noBodyInt, noBodyInt), highlight(noBodyFunc, noBodyFunc)
 
 -- issue65952.go --
 package p
@@ -62,10 +63,12 @@
 // return values in functions with no results.
 
 func _() {
-	return 0 //@highlight("0", "0"), diag("0", re"too many return")
+	return 0 //@hiloc(ret1, "0", text), diag("0", re"too many return")
+	//@highlight(ret1, ret1)
 }
 
 func _() () {
 	// TODO(golang/go#65966): fix the triplicate diagnostics here.
-	return 0 //@highlight("0", "0"), diag("0", re"too many return"), diag("0", re"too many return"), diag("0", re"too many return")
+	return 0 //@hiloc(ret2, "0", text), diag("0", re"too many return"), diag("0", re"too many return"), diag("0", re"too many return")
+	//@highlight(ret2, ret2)
 }
diff --git a/gopls/internal/test/marker/testdata/highlight/highlight.txt b/gopls/internal/test/marker/testdata/highlight/highlight.txt
index 10b3025..68d13d1 100644
--- a/gopls/internal/test/marker/testdata/highlight/highlight.txt
+++ b/gopls/internal/test/marker/testdata/highlight/highlight.txt
@@ -4,96 +4,96 @@
 package highlights
 
 import (
-	"fmt"         //@loc(fmtImp, "\"fmt\""),highlight(fmtImp, fmtImp, fmt1, fmt2, fmt3, fmt4)
-	h2 "net/http" //@loc(hImp, "h2"),highlight(hImp, hImp, hUse)
+	"fmt"         //@hiloc(fmtImp, "\"fmt\"", text),highlightall(fmtImp, fmt1, fmt2, fmt3, fmt4)
+	h2 "net/http" //@hiloc(hImp, "h2", text),highlightall(hImp, hUse)
 	"sort"
 )
 
-type F struct{ bar int } //@loc(barDeclaration, "bar"),highlight(barDeclaration, barDeclaration, bar1, bar2, bar3)
+type F struct{ bar int } //@hiloc(barDeclaration, "bar", text),highlightall(barDeclaration, bar1, bar2, bar3)
 
 func _() F {
 	return F{
-		bar: 123, //@loc(bar1, "bar"),highlight(bar1, barDeclaration, bar1, bar2, bar3)
+		bar: 123, //@hiloc(bar1, "bar", write)
 	}
 }
 
-var foo = F{bar: 52} //@loc(fooDeclaration, "foo"),loc(bar2, "bar"),highlight(fooDeclaration, fooDeclaration, fooUse),highlight(bar2, barDeclaration, bar1, bar2, bar3)
+var foo = F{bar: 52} //@hiloc(fooDeclaration, "foo", write),hiloc(bar2, "bar", write),highlightall(fooDeclaration, fooUse)
 
-func Print() { //@loc(printFunc, "Print"),highlight(printFunc, printFunc, printTest)
-	_ = h2.Client{} //@loc(hUse, "h2"),highlight(hUse, hImp, hUse)
+func Print() { //@hiloc(printFunc, "Print", text),highlightall(printFunc, printTest)
+	_ = h2.Client{} //@hiloc(hUse, "h2", text)
 
-	fmt.Println(foo) //@loc(fooUse, "foo"),highlight(fooUse, fooDeclaration, fooUse),loc(fmt1, "fmt"),highlight(fmt1, fmtImp, fmt1, fmt2, fmt3, fmt4)
-	fmt.Print("yo")  //@loc(printSep, "Print"),highlight(printSep, printSep, print1, print2),loc(fmt2, "fmt"),highlight(fmt2, fmtImp, fmt1, fmt2, fmt3, fmt4)
+	fmt.Println(foo) //@hiloc(fooUse, "foo", read),hiloc(fmt1, "fmt", text)
+	fmt.Print("yo")  //@hiloc(printSep, "Print", text),highlightall(printSep, print1, print2),hiloc(fmt2, "fmt", text)
 }
 
-func (x *F) Inc() { //@loc(xRightDecl, "x"),loc(xLeftDecl, " *"),highlight(xRightDecl, xRightDecl, xUse),highlight(xLeftDecl, xRightDecl, xUse)
-	x.bar++ //@loc(xUse, "x"),loc(bar3, "bar"),highlight(xUse, xRightDecl, xUse),highlight(bar3, barDeclaration, bar1, bar2, bar3)
+func (x *F) Inc() { //@hiloc(xRightDecl, "x", text),hiloc(xLeftDecl, " *", text),highlightall(xRightDecl, xUse),highlight(xLeftDecl, xRightDecl, xUse)
+	x.bar++ //@hiloc(xUse, "x", read),hiloc(bar3, "bar", write)
 }
 
 func testFunctions() {
-	fmt.Print("main start") //@loc(print1, "Print"),highlight(print1, printSep, print1, print2),loc(fmt3, "fmt"),highlight(fmt3, fmtImp, fmt1, fmt2, fmt3, fmt4)
-	fmt.Print("ok")         //@loc(print2, "Print"),highlight(print2, printSep, print1, print2),loc(fmt4, "fmt"),highlight(fmt4, fmtImp, fmt1, fmt2, fmt3, fmt4)
-	Print()                 //@loc(printTest, "Print"),highlight(printTest, printFunc, printTest)
+	fmt.Print("main start") //@hiloc(print1, "Print", text),hiloc(fmt3, "fmt", text)
+	fmt.Print("ok")         //@hiloc(print2, "Print", text),hiloc(fmt4, "fmt", text)
+	Print()                 //@hiloc(printTest, "Print", text)
 }
 
 // DocumentHighlight is undefined, so its uses below are type errors.
 // Nevertheless, document highlighting should still work.
-//@diag(doc1, re"undefined|undeclared"), diag(doc2, re"undefined|undeclared"), diag(doc3, re"undefined|undeclared")
+//@diag(locdoc1, re"undefined|undeclared"), diag(locdoc2, re"undefined|undeclared"), diag(locdoc3, re"undefined|undeclared")
 
-func toProtocolHighlight(rngs []int) []DocumentHighlight { //@loc(doc1, "DocumentHighlight"),loc(docRet1, "[]DocumentHighlight"),highlight(doc1, docRet1, doc1, doc2, doc3, result)
-	result := make([]DocumentHighlight, 0, len(rngs)) //@loc(doc2, "DocumentHighlight"),highlight(doc2, doc1, doc2, doc3)
+func toProtocolHighlight(rngs []int) []DocumentHighlight { //@loc(locdoc1, "DocumentHighlight"), hiloc(doc1, "DocumentHighlight", text),hiloc(docRet1, "[]DocumentHighlight", text),highlight(doc1, docRet1, doc1, doc2, doc3, result)
+	result := make([]DocumentHighlight, 0, len(rngs)) //@loc(locdoc2, "DocumentHighlight"), hiloc(doc2, "DocumentHighlight", text),highlight(doc2, doc1, doc2, doc3)
 	for _, rng := range rngs {
-		result = append(result, DocumentHighlight{ //@loc(doc3, "DocumentHighlight"),highlight(doc3, doc1, doc2, doc3)
+		result = append(result, DocumentHighlight{ //@loc(locdoc3, "DocumentHighlight"), hiloc(doc3, "DocumentHighlight", text),highlight(doc3, doc1, doc2, doc3)
 			Range: rng,
 		})
 	}
-	return result //@loc(result, "result")
+	return result //@hiloc(result, "result", text)
 }
 
 func testForLoops() {
-	for i := 0; i < 10; i++ { //@loc(forDecl1, "for"),highlight(forDecl1, forDecl1, brk1, cont1)
+	for i := 0; i < 10; i++ { //@hiloc(forDecl1, "for", text),highlightall(forDecl1, brk1, cont1)
 		if i > 8 {
-			break //@loc(brk1, "break"),highlight(brk1, forDecl1, brk1, cont1)
+			break //@hiloc(brk1, "break", text)
 		}
 		if i < 2 {
-			for j := 1; j < 10; j++ { //@loc(forDecl2, "for"),highlight(forDecl2, forDecl2, cont2)
+			for j := 1; j < 10; j++ { //@hiloc(forDecl2, "for", text),highlightall(forDecl2, cont2)
 				if j < 3 {
-					for k := 1; k < 10; k++ { //@loc(forDecl3, "for"),highlight(forDecl3, forDecl3, cont3)
+					for k := 1; k < 10; k++ { //@hiloc(forDecl3, "for", text),highlightall(forDecl3, cont3)
 						if k < 3 {
-							continue //@loc(cont3, "continue"),highlight(cont3, forDecl3, cont3)
+							continue //@hiloc(cont3, "continue", text)
 						}
 					}
-					continue //@loc(cont2, "continue"),highlight(cont2, forDecl2, cont2)
+					continue //@hiloc(cont2, "continue", text)
 				}
 			}
-			continue //@loc(cont1, "continue"),highlight(cont1, forDecl1, brk1, cont1)
+			continue //@hiloc(cont1, "continue", text)
 		}
 	}
 
 	arr := []int{}
-	for i := range arr { //@loc(forDecl4, "for"),highlight(forDecl4, forDecl4, brk4, cont4)
+	for i := range arr { //@hiloc(forDecl4, "for", text),highlightall(forDecl4, brk4, cont4)
 		if i > 8 {
-			break //@loc(brk4, "break"),highlight(brk4, forDecl4, brk4, cont4)
+			break //@hiloc(brk4, "break", text)
 		}
 		if i < 4 {
-			continue //@loc(cont4, "continue"),highlight(cont4, forDecl4, brk4, cont4)
+			continue //@hiloc(cont4, "continue", text)
 		}
 	}
 
 Outer:
-	for i := 0; i < 10; i++ { //@loc(forDecl5, "for"),highlight(forDecl5, forDecl5, brk5, brk6, brk8)
-		break //@loc(brk5, "break"),highlight(brk5, forDecl5, brk5, brk6, brk8)
-		for { //@loc(forDecl6, "for"),highlight(forDecl6, forDecl6, cont5), diag("for", re"unreachable")
+	for i := 0; i < 10; i++ { //@hiloc(forDecl5, "for", text),highlightall(forDecl5, brk5, brk6, brk8)
+		break //@hiloc(brk5, "break", text)
+		for { //@hiloc(forDecl6, "for", text),highlightall(forDecl6, cont5), diag("for", re"unreachable")
 			if i == 1 {
-				break Outer //@loc(brk6, "break Outer"),highlight(brk6, forDecl5, brk5, brk6, brk8)
+				break Outer //@hiloc(brk6, "break Outer", text)
 			}
-			switch i { //@loc(switch1, "switch"),highlight(switch1, switch1, brk7)
+			switch i { //@hiloc(switch1, "switch", text),highlightall(switch1, brk7)
 			case 5:
-				break //@loc(brk7, "break"),highlight(brk7, switch1, brk7)
+				break //@hiloc(brk7, "break", text)
 			case 6:
-				continue //@loc(cont5, "continue"),highlight(cont5, forDecl6, cont5)
+				continue //@hiloc(cont5, "continue", text)
 			case 7:
-				break Outer //@loc(brk8, "break Outer"),highlight(brk8, forDecl5, brk5, brk6, brk8)
+				break Outer //@hiloc(brk8, "break Outer", text)
 			}
 		}
 	}
@@ -103,56 +103,56 @@
 	var i, j int
 
 L1:
-	for { //@loc(forDecl7, "for"),highlight(forDecl7, forDecl7, brk10, cont6)
+	for { //@hiloc(forDecl7, "for", text),highlightall(forDecl7, brk10, cont6)
 	L2:
-		switch i { //@loc(switch2, "switch"),highlight(switch2, switch2, brk11, brk12, brk13)
+		switch i { //@hiloc(switch2, "switch", text),highlightall(switch2, brk11, brk12, brk13)
 		case 1:
-			switch j { //@loc(switch3, "switch"),highlight(switch3, switch3, brk9)
+			switch j { //@hiloc(switch3, "switch", text),highlightall(switch3, brk9)
 			case 1:
-				break //@loc(brk9, "break"),highlight(brk9, switch3, brk9)
+				break //@hiloc(brk9, "break", text)
 			case 2:
-				break L1 //@loc(brk10, "break L1"),highlight(brk10, forDecl7, brk10, cont6)
+				break L1 //@hiloc(brk10, "break L1", text)
 			case 3:
-				break L2 //@loc(brk11, "break L2"),highlight(brk11, switch2, brk11, brk12, brk13)
+				break L2 //@hiloc(brk11, "break L2", text)
 			default:
-				continue //@loc(cont6, "continue"),highlight(cont6, forDecl7, brk10, cont6)
+				continue //@hiloc(cont6, "continue", text)
 			}
 		case 2:
-			break //@loc(brk12, "break"),highlight(brk12, switch2, brk11, brk12, brk13)
+			break //@hiloc(brk12, "break", text)
 		default:
-			break L2 //@loc(brk13, "break L2"),highlight(brk13, switch2, brk11, brk12, brk13)
+			break L2 //@hiloc(brk13, "break L2", text)
 		}
 	}
 }
 
-func testReturn() bool { //@loc(func1, "func"),loc(bool1, "bool"),highlight(func1, func1, fullRet11, fullRet12),highlight(bool1, bool1, false1, bool2, true1)
+func testReturn() bool { //@hiloc(func1, "func", text),hiloc(bool1, "bool", text),highlight(func1, func1, fullRet11, fullRet12),highlight(bool1, bool1, false1, bool2, true1)
 	if 1 < 2 {
-		return false //@loc(ret11, "return"),loc(fullRet11, "return false"),loc(false1, "false"),highlight(ret11, func1, fullRet11, fullRet12)
+		return false //@hiloc(ret11, "return", text),hiloc(fullRet11, "return false", text),hiloc(false1, "false", text),highlight(ret11, func1, fullRet11, fullRet12)
 	}
 	candidates := []int{}
-	sort.SliceStable(candidates, func(i, j int) bool { //@loc(func2, "func"),loc(bool2, "bool"),highlight(func2, func2, fullRet2)
-		return candidates[i] > candidates[j] //@loc(ret2, "return"),loc(fullRet2, "return candidates[i] > candidates[j]"),highlight(ret2, func2, fullRet2)
+	sort.SliceStable(candidates, func(i, j int) bool { //@hiloc(func2, "func", text),hiloc(bool2, "bool", text),highlight(func2, func2, fullRet2)
+		return candidates[i] > candidates[j] //@hiloc(ret2, "return", text),hiloc(fullRet2, "return candidates[i] > candidates[j]", text),highlight(ret2, func2, fullRet2)
 	})
-	return true //@loc(ret12, "return"),loc(fullRet12, "return true"),loc(true1, "true"),highlight(ret12, func1, fullRet11, fullRet12)
+	return true //@hiloc(ret12, "return", text),hiloc(fullRet12, "return true", text),hiloc(true1, "true", text),highlight(ret12, func1, fullRet11, fullRet12)
 }
 
-func testReturnFields() float64 { //@loc(retVal1, "float64"),highlight(retVal1, retVal1, retVal11, retVal21)
+func testReturnFields() float64 { //@hiloc(retVal1, "float64", text),highlight(retVal1, retVal1, retVal11, retVal21)
 	if 1 < 2 {
-		return 20.1 //@loc(retVal11, "20.1"),highlight(retVal11, retVal1, retVal11, retVal21)
+		return 20.1 //@hiloc(retVal11, "20.1", text),highlight(retVal11, retVal1, retVal11, retVal21)
 	}
-	z := 4.3 //@loc(zDecl, "z")
-	return z //@loc(retVal21, "z"),highlight(retVal21, retVal1, retVal11, zDecl, retVal21)
+	z := 4.3 //@hiloc(zDecl, "z", write)
+	return z //@hiloc(retVal21, "z", text),highlight(retVal21, retVal1, retVal11, zDecl, retVal21)
 }
 
-func testReturnMultipleFields() (float32, string) { //@loc(retVal31, "float32"),loc(retVal32, "string"),highlight(retVal31, retVal31, retVal41, retVal51),highlight(retVal32, retVal32, retVal42, retVal52)
-	y := "im a var" //@loc(yDecl, "y"),
+func testReturnMultipleFields() (float32, string) { //@hiloc(retVal31, "float32", text),hiloc(retVal32, "string", text),highlight(retVal31, retVal31, retVal41, retVal51),highlight(retVal32, retVal32, retVal42, retVal52)
+	y := "im a var" //@hiloc(yDecl, "y", write),
 	if 1 < 2 {
-		return 20.1, y //@loc(retVal41, "20.1"),loc(retVal42, "y"),highlight(retVal41, retVal31, retVal41, retVal51),highlight(retVal42, retVal32, yDecl, retVal42, retVal52)
+		return 20.1, y //@hiloc(retVal41, "20.1", text),hiloc(retVal42, "y", text),highlight(retVal41, retVal31, retVal41, retVal51),highlight(retVal42, retVal32, yDecl, retVal42, retVal52)
 	}
-	return 4.9, "test" //@loc(retVal51, "4.9"),loc(retVal52, "\"test\""),highlight(retVal51, retVal31, retVal41, retVal51),highlight(retVal52, retVal32, retVal42, retVal52)
+	return 4.9, "test" //@hiloc(retVal51, "4.9", text),hiloc(retVal52, "\"test\"", text),highlight(retVal51, retVal31, retVal41, retVal51),highlight(retVal52, retVal32, retVal42, retVal52)
 }
 
-func testReturnFunc() int32 { //@loc(retCall, "int32")
-	mulch := 1          //@loc(mulchDec, "mulch"),highlight(mulchDec, mulchDec, mulchRet)
-	return int32(mulch) //@loc(mulchRet, "mulch"),loc(retFunc, "int32"),loc(retTotal, "int32(mulch)"),highlight(mulchRet, mulchDec, mulchRet),highlight(retFunc, retCall, retFunc, retTotal)
+func testReturnFunc() int32 { //@hiloc(retCall, "int32", text)
+	mulch := 1          //@hiloc(mulchDec, "mulch", write),highlight(mulchDec, mulchDec, mulchRet)
+	return int32(mulch) //@hiloc(mulchRet, "mulch", read),hiloc(retFunc, "int32", text),hiloc(retTotal, "int32(mulch)", text),highlight(mulchRet, mulchDec, mulchRet),highlight(retFunc, retCall, retFunc, retTotal)
 }
diff --git a/gopls/internal/test/marker/testdata/highlight/highlight_kind.txt b/gopls/internal/test/marker/testdata/highlight/highlight_kind.txt
new file mode 100644
index 0000000..bd059f7
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/highlight/highlight_kind.txt
@@ -0,0 +1,88 @@
+This test checks textDocument/highlight with highlight kinds.
+For example, a use of a variable is reported as a "read",
+and an assignment to a variable is reported as a "write".
+(Note that the details don't align exactly with the Go
+type-checker notions of values versus addressable variables).
+
+
+-- highlight_kind.go --
+package a
+
+type Nest struct {
+	nest *Nest //@hiloc(fNest, "nest", text)
+}
+type MyMap map[string]string
+
+type NestMap map[Nest]Nest
+
+func highlightTest() {
+	const constIdent = 1 //@hiloc(constIdent, "constIdent", write)
+	//@highlightall(constIdent)
+	var varNoInit int    //@hiloc(varNoInit, "varNoInit", write)
+	(varNoInit) = 1      //@hiloc(varNoInitAssign, "varNoInit", write)
+	_ = varNoInit        //@hiloc(varNoInitRead, "varNoInit", read)
+	//@highlightall(varNoInit, varNoInitAssign, varNoInitRead)
+
+	str, num := "hello", 2 //@hiloc(str, "str", write), hiloc(num, "num", write)
+	_, _ = str, num        //@hiloc(strRead, "str", read), hiloc(numRead, "num", read)
+	//@highlightall(str, strRead, strMapKey, strMapVal, strMyMapKey, strMyMapVal, strMyMapSliceKey, strMyMapSliceVal, strMyMapPtrSliceKey, strMyMapPtrSliceVal)
+	//@highlightall(num, numRead, numAddr, numIncr, numMul)
+	nest := &Nest{nest: nil} //@hiloc(nest, "nest", write),hiloc(fNestComp, re`(nest):`, write)
+	nest.nest = &Nest{}      //@hiloc(nestSelX, "nest", read), hiloc(fNestSel, re`(nest) =`, write)
+	*nest.nest = Nest{}      //@hiloc(nestSelXStar, "nest", read), hiloc(fNestSelStar, re`(nest) =`, write)
+	//@highlightall(nest, nestSelX, nestSelXStar, nestMapVal)
+	//@highlightall(fNest, fNestComp, fNestSel, fNestSelStar, fNestSliceComp, fNestPtrSliceComp, fNestMapKey)
+
+	pInt := &num //@hiloc(pInt, "pInt", write),hiloc(numAddr, "num", read)
+	// StarExpr is reported as "write" in GoLand and Rust Analyzer
+	*pInt = 3               //@hiloc(pIntStar, "pInt", write)
+	var ppInt **int = &pInt //@hiloc(ppInt, "ppInt", write),hiloc(pIntAddr, re`&(pInt)`, read)
+	**ppInt = 4             //@hiloc(ppIntStar, "ppInt", write)
+	*(*ppInt) = 4           //@hiloc(ppIntParen, "ppInt", write)
+	//@highlightall(pInt, pIntStar, pIntAddr)
+	//@highlightall(ppInt, ppIntStar, ppIntParen)
+
+	num++    //@hiloc(numIncr, "num", write)
+	num *= 1 //@hiloc(numMul, "num", write)
+
+	var ch chan int = make(chan int, 10) //@hiloc(ch, "ch", write)
+	ch <- 3                              //@hiloc(chSend, "ch", write)
+	<-ch                                 //@hiloc(chRecv, "ch", read)
+	//@highlightall(ch, chSend, chRecv)
+
+	var nums []int = []int{1, 2} //@hiloc(nums, "nums", write)
+	// IndexExpr is reported as "read" in GoLand, Rust Analyzer and Java JDT
+	nums[0] = 1 //@hiloc(numsIndex, "nums", read)
+	//@highlightall(nums, numsIndex)
+
+	mapLiteral := map[string]string{ //@hiloc(mapLiteral, "mapLiteral", write)
+		str: str, //@hiloc(strMapKey, "str", read),hiloc(strMapVal, re`(str),`, read)
+	}
+	for key, value := range mapLiteral { //@hiloc(mapKey, "key", write), hiloc(mapVal, "value", write), hiloc(mapLiteralRange, "mapLiteral", read)
+		_, _ = key, value //@hiloc(mapKeyRead, "key", read), hiloc(mapValRead, "value", read)
+	}
+	//@highlightall(mapLiteral, mapLiteralRange)
+	//@highlightall(mapKey, mapKeyRead)
+	//@highlightall(mapVal, mapValRead)
+
+	nestSlice := []Nest{
+		{nest: nil}, //@hiloc(fNestSliceComp, "nest", write)
+	}
+	nestPtrSlice := []*Nest{
+		{nest: nil}, //@hiloc(fNestPtrSliceComp, "nest", write)
+	}
+	myMap := MyMap{
+		str: str, //@hiloc(strMyMapKey, "str", read),hiloc(strMyMapVal, re`(str),`, read)
+	}
+	myMapSlice := []MyMap{
+		{str: str}, //@hiloc(strMyMapSliceKey, "str", read),hiloc(strMyMapSliceVal, re`: (str)`, read)
+	}
+	myMapPtrSlice := []*MyMap{
+		{str: str}, //@hiloc(strMyMapPtrSliceKey, "str", read),hiloc(strMyMapPtrSliceVal, re`: (str)`, read)
+	}
+	nestMap := NestMap{
+		Nest{nest: nil}: *nest, //@hiloc(fNestMapKey, "nest", write), hiloc(nestMapVal, re`(nest),`, read)
+	}
+
+	_, _, _, _, _, _ = myMap, nestSlice, nestPtrSlice, myMapSlice, myMapPtrSlice, nestMap
+}
diff --git a/gopls/internal/test/marker/testdata/highlight/issue60435.txt b/gopls/internal/test/marker/testdata/highlight/issue60435.txt
index 324e4b8..0eef080 100644
--- a/gopls/internal/test/marker/testdata/highlight/issue60435.txt
+++ b/gopls/internal/test/marker/testdata/highlight/issue60435.txt
@@ -7,9 +7,9 @@
 package highlights
 
 import (
-	"net/http"          //@loc(httpImp, `"net/http"`)
-	"net/http/httptest" //@loc(httptestImp, `"net/http/httptest"`)
+	"net/http"          //@hiloc(httpImp, `"net/http"`, text)
+	"net/http/httptest" //@hiloc(httptestImp, `"net/http/httptest"`, text)
 )
 
 var _ = httptest.NewRequest
-var _ = http.NewRequest //@loc(here, "http"), highlight(here, here, httpImp)
+var _ = http.NewRequest //@hiloc(here, "http", text), highlight(here, here, httpImp)
diff --git a/gopls/internal/test/marker/testdata/highlight/switchbreak.txt b/gopls/internal/test/marker/testdata/highlight/switchbreak.txt
index b486ad1..3893b4c 100644
--- a/gopls/internal/test/marker/testdata/highlight/switchbreak.txt
+++ b/gopls/internal/test/marker/testdata/highlight/switchbreak.txt
@@ -7,15 +7,15 @@
 func _(x any) {
 	for {
 		// type switch
-		switch x.(type) { //@loc(tswitch, "switch")
+		switch x.(type) { //@hiloc(tswitch, "switch", text)
 		default:
-			break //@highlight("break", tswitch, "break")
+			break //@hiloc(tbreak, "break", text),highlight(tbreak, tswitch, tbreak)
 		}
 
 		// value switch
-		switch { //@loc(vswitch, "switch")
+		switch { //@hiloc(vswitch, "switch", text)
 		default:
-			break //@highlight("break", vswitch, "break")
+			break //@hiloc(vbreak, "break", text), highlight(vbreak, vswitch, vbreak)
 		}
 	}
 }