Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.

Commit 45756fc

Browse files
committed
fix hover on keywords returning wrong content
When hovering over control flow keywords (if, else, range, end), the LSP incorrectly returned the next sibling keyword instead of empty content. For example, hovering on 'if' would show 'else'. Fixed by returning nil early from FindSourceDefinitionFromPosition when IsKeyword is true. Added test to verify keywords return empty hover. Also updated CLAUDE.md to reflect the actual project (Go template LSP).
1 parent d61e215 commit 45756fc

5 files changed

Lines changed: 97 additions & 38 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
# gozer-45gq
3+
title: Hover on keywords returns wrong/unexpected content
4+
status: completed
5+
type: bug
6+
created_at: 2026-01-19T17:18:47Z
7+
updated_at: 2026-01-19T17:18:47Z
8+
---
9+
10+
When hovering over control flow keywords like `if`, `else`, `range`, `end`, the hover handler returns incorrect content (e.g., showing 'else' when hovering on 'if').
11+
12+
## Root Cause
13+
In `analyzer_lsp.go`, `FindSourceDefinitionFromPosition` sets `IsKeyword = true` when the cursor is on a keyword (lines 303-308), but then doesn't check this flag - it proceeds to try matching the keyword as a variable/template definition, producing wrong results.
14+
15+
## Expected Behavior
16+
Keywords should either:
17+
1. Return no hover content (preferred - they're not identifiers with type info)
18+
2. Return documentation about the keyword
19+
20+
## Checklist
21+
- [x] Create failing test demonstrating the bug
22+
- [x] Fix `FindSourceDefinitionFromPosition` to return nil when `IsKeyword` is true
23+
- [x] Run tests and linter

CLAUDE.md

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,11 @@
1-
# CLAUDE.md
1+
# Gozer
22

3-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4-
5-
## Overview
6-
7-
HTML linter written in Go. Validates HTML for accessibility, best practices, and common mistakes. Implements rules from html-validate.org.
3+
Go HTML template LSP and Zed extension
84

95
## Guidelines
106

117
- Be concise
8+
- When fixing or investigating code issues, ALWAYS create a failing test FIRST to demonstrate understanding of the problem THEN change code and confirm the test passes
129
- Run `golangci-lint run --fix` after modifying Go code
1310
- Run `go test ./...` after changes
1411
- **NEVER commit without explicit user request**
15-
- Use external test packages (`package foo_test`)
16-
17-
## Commands
18-
19-
```bash
20-
go build ./... # Build
21-
go test ./... # Test all
22-
go test ./linter -run TestLintContent # Run single test
23-
golangci-lint run # Lint
24-
go install . # Install to $GOBIN
25-
go-html-validate --help # Usage
26-
```
27-
28-
## Architecture
29-
30-
**Data flow:** `main.go``linter.Linter``parser.ParseFragment``rules.Rule.Check()``reporter.Reporter`
31-
32-
**Key types:**
33-
- `parser.Document` - parsed HTML tree with `Walk(func(*Node) bool)` for traversal
34-
- `parser.Node` - wraps `html.Node` with `HasAttr()`, `GetAttr()`, `TextContent()`, `IsElement()` helpers
35-
- `rules.Rule` interface - `Name()`, `Description()`, `Check(*parser.Document) []Result`
36-
- `rules.Result` - lint finding with `Rule`, `Message`, `Filename`, `Line`, `Col`, `Severity`
37-
38-
**Template handling:** The parser preprocesses Go template syntax (`{{...}}`) before parsing. Files starting with `{{define` are marked as template fragments.
39-
40-
## Adding Rules
41-
42-
1. Create `rules/rule_name.go` implementing `rules.Rule` interface
43-
2. Add rule name constant to `rules/rule.go`
44-
3. Register in `NewRegistry()` in `rules/rule.go`
45-
4. Add tests in `linter/linter_*_test.go` (grouped by category: accessibility, validation, deprecated, etc.)

internal/template/analyzer/analyzer_lsp.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ func FindSourceDefinitionFromPosition(
3232
return nil
3333
}
3434

35+
// Keywords (if, else, range, end, etc.) don't have hover content
36+
if seeker.IsKeyword {
37+
return nil
38+
}
39+
3540
//
3641
// 2. From the node and token found, find the appropriate 'Source Definition'
3742
//

internal/template/template_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,3 +710,68 @@ func TestHover_MultipleInstancesSameLine(t *testing.T) {
710710
})
711711
}
712712
}
713+
714+
// TestHover_KeywordsReturnEmpty verifies that hovering over control flow keywords
715+
// (if, else, range, end, etc.) returns empty hover content, not incorrect values.
716+
func TestHover_KeywordsReturnEmpty(t *testing.T) {
717+
// Template with various control flow keywords
718+
// Line 0: {{if .Cond}}yes{{else}}no{{end}}
719+
// ^^ if at char 2
720+
// ^^^^ else at char 16
721+
// ^^^ end at char 25
722+
source := `{{if .Cond}}yes{{else}}no{{end}}`
723+
724+
root, parseErrs := template.ParseSingleFile([]byte(source))
725+
if len(parseErrs) != 0 {
726+
t.Fatalf("Parse errors: %v", parseErrs)
727+
}
728+
729+
workspace := map[string]*parser.GroupStatementNode{
730+
"test.html": root,
731+
}
732+
733+
file, _ := template.DefinitionAnalysisSingleFile("test.html", workspace)
734+
if file == nil {
735+
t.Fatal("Expected non-nil file definition")
736+
}
737+
738+
testCases := []struct {
739+
name string
740+
pos lexer.Position
741+
}{
742+
{
743+
name: "if keyword",
744+
pos: lexer.Position{Line: 0, Character: 3}, // on "if"
745+
},
746+
{
747+
name: "else keyword",
748+
pos: lexer.Position{Line: 0, Character: 17}, // on "else"
749+
},
750+
{
751+
name: "end keyword",
752+
pos: lexer.Position{Line: 0, Character: 26}, // on "end"
753+
},
754+
}
755+
756+
for _, tc := range testCases {
757+
t.Run(tc.name, func(t *testing.T) {
758+
hoverText, hoverRange := template.Hover(file, tc.pos)
759+
760+
if hoverText != "" {
761+
t.Errorf(
762+
"Expected empty hover text for %s, but got %q",
763+
tc.name,
764+
hoverText,
765+
)
766+
}
767+
768+
if !hoverRange.IsEmpty() {
769+
t.Errorf(
770+
"Expected empty hover range for %s, but got %v",
771+
tc.name,
772+
hoverRange,
773+
)
774+
}
775+
})
776+
}
777+
}

zed-ext/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)