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

Commit 91b1fc8

Browse files
committed
add document highlight for template control flow keywords
Implement textDocument/documentHighlight LSP capability to highlight matching template control flow keywords (if/else/end, range/end, with/end, etc.). When the cursor is on a template keyword like {{if}}, {{else}}, or {{end}}, all related keywords in the same control flow block are highlighted together. This leverages the existing NextLinkedSibling circular list that the parser already creates for control flow nodes.
1 parent 74c2a46 commit 91b1fc8

7 files changed

Lines changed: 222 additions & 14 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
# gozer-w0ax
3+
title: Add document highlight for template brace matching
4+
status: completed
5+
type: feature
6+
priority: normal
7+
created_at: 2026-01-19T02:56:39Z
8+
updated_at: 2026-01-19T03:00:40Z
9+
---
10+
11+
Implement textDocument/documentHighlight LSP capability to highlight matching template control flow keywords (e.g., clicking {{end}} highlights its corresponding {{if}}/{{range}}/etc.).
12+
13+
## Background
14+
The parser already links matching template blocks via `NextLinkedSibling` pointers in `GroupStatementNode`. This creates a linked list connecting related control flow statements:
15+
```
16+
{{if .foo}} → {{else if .bar}} → {{else}} → {{end}}
17+
```
18+
19+
## Checklist
20+
- [ ] Add DocumentHighlightProvider capability to server initialization
21+
- [ ] Add textDocument/documentHighlight method registration in protocol.go
22+
- [ ] Implement ProcessDocumentHighlightRequest in methods.go
23+
- [ ] Create helper to find GroupStatementNode at cursor position
24+
- [ ] Traverse NextLinkedSibling chain to collect all related keywords
25+
- [ ] Return highlight ranges for all matched keywords
26+
- [ ] Add tests for document highlight functionality

cmd/go-template-lsp/lsp/methods.go

Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,11 @@ type InitializeParams struct {
8787

8888
// ServerCapabilities describes the capabilities this server supports.
8989
type ServerCapabilities struct {
90-
TextDocumentSync int `json:"textDocumentSync"`
91-
HoverProvider bool `json:"hoverProvider"`
92-
DefinitionProvider bool `json:"definitionProvider"`
93-
FoldingRangeProvider bool `json:"foldingRangeProvider"`
90+
TextDocumentSync int `json:"textDocumentSync"`
91+
HoverProvider bool `json:"hoverProvider"`
92+
DefinitionProvider bool `json:"definitionProvider"`
93+
FoldingRangeProvider bool `json:"foldingRangeProvider"`
94+
DocumentHighlightProvider bool `json:"documentHighlightProvider"`
9495
}
9596

9697
// InitializeResult is the response to the initialize request.
@@ -217,10 +218,11 @@ func ProcessInitializeRequest(
217218
Id: req.Id,
218219
Result: InitializeResult{
219220
Capabilities: ServerCapabilities{
220-
TextDocumentSync: TextDocumentSyncFull,
221-
HoverProvider: true,
222-
DefinitionProvider: true,
223-
FoldingRangeProvider: true,
221+
TextDocumentSync: TextDocumentSyncFull,
222+
HoverProvider: true,
223+
DefinitionProvider: true,
224+
FoldingRangeProvider: true,
225+
DocumentHighlightProvider: true,
224226
},
225227
},
226228
}
@@ -682,3 +684,109 @@ func ProcessFoldingRangeRequest(
682684

683685
return responseData, fileName
684686
}
687+
688+
// DocumentHighlightKind represents the kind of document highlight.
689+
type DocumentHighlightKind int
690+
691+
const (
692+
// DocumentHighlightText is a textual highlight (default).
693+
DocumentHighlightText DocumentHighlightKind = 1
694+
// DocumentHighlightRead is a read-access highlight.
695+
DocumentHighlightRead DocumentHighlightKind = 2
696+
// DocumentHighlightWrite is a write-access highlight.
697+
DocumentHighlightWrite DocumentHighlightKind = 3
698+
)
699+
700+
// DocumentHighlightParams holds parameters for textDocument/documentHighlight.
701+
type DocumentHighlightParams struct {
702+
TextDocument TextDocumentIdentifier `json:"textDocument"`
703+
Position Position `json:"position"`
704+
}
705+
706+
// DocumentHighlightResult represents a document highlight.
707+
type DocumentHighlightResult struct {
708+
Range Range `json:"range"`
709+
Kind DocumentHighlightKind `json:"kind"`
710+
}
711+
712+
// ProcessDocumentHighlightRequest handles textDocument/documentHighlight.
713+
func ProcessDocumentHighlightRequest(
714+
data []byte,
715+
parsedFiles map[string]*parser.GroupStatementNode,
716+
textFromClient map[string][]byte,
717+
muTextFromClient *sync.Mutex,
718+
) (response []byte, fileName string) {
719+
req := RequestMessage[DocumentHighlightParams]{}
720+
721+
err := json.Unmarshal(data, &req)
722+
if err != nil {
723+
slog.Warn("Error unmarshalling document highlight request: " + err.Error())
724+
return nil, ""
725+
}
726+
727+
var rootNode *parser.GroupStatementNode
728+
fileUri := req.Params.TextDocument.Uri
729+
730+
muTextFromClient.Lock()
731+
fileContent := textFromClient[fileUri]
732+
733+
if fileContent != nil {
734+
rootNode, _ = tmpl.ParseSingleFile(fileContent)
735+
}
736+
737+
if rootNode == nil {
738+
rootNode = parsedFiles[fileUri]
739+
}
740+
741+
muTextFromClient.Unlock()
742+
743+
defer func() {
744+
if r := recover(); r != nil {
745+
msg := r.(string)
746+
slog.Error(msg,
747+
slog.Group("details",
748+
slog.String("file_uri", fileUri),
749+
slog.String("file_content", string(fileContent)),
750+
),
751+
)
752+
panic(msg)
753+
}
754+
}()
755+
756+
var res ResponseMessage[[]DocumentHighlightResult]
757+
res.Id = req.Id
758+
res.JsonRpc = req.JsonRpc
759+
760+
if rootNode == nil {
761+
// Return empty result if file not found
762+
responseData, err := json.Marshal(res)
763+
if err != nil {
764+
slog.Warn("Error marshalling document highlight response: " + err.Error())
765+
return nil, fileName
766+
}
767+
return responseData, fileName
768+
}
769+
770+
position := lexer.Position{
771+
Line: uintToInt(req.Params.Position.Line),
772+
Character: uintToInt(req.Params.Position.Character),
773+
}
774+
775+
ranges := tmpl.DocumentHighlight(rootNode, position)
776+
777+
for _, rng := range ranges {
778+
highlight := DocumentHighlightResult{
779+
Range: ConvertParserRangeToLspRange(rng),
780+
Kind: DocumentHighlightText,
781+
}
782+
res.Result = append(res.Result, highlight)
783+
}
784+
785+
responseData, err := json.Marshal(res)
786+
if err != nil {
787+
slog.Warn("Error marshalling document highlight response: " + err.Error())
788+
return nil, fileName
789+
}
790+
791+
return responseData, fileName
792+
}

cmd/go-template-lsp/lsp/protocol.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const (
3030
MethodHover = "textDocument/hover"
3131
MethodDefinition = "textDocument/definition"
3232
MethodFoldingRange = "textDocument/foldingRange"
33+
MethodDocumentHighlight = "textDocument/documentHighlight"
3334
MethodPublishDiagnostics = "textDocument/publishDiagnostics"
3435
)
3536

cmd/go-template-lsp/main.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,11 @@ type requestCounter struct {
4646
DidOpen int
4747
DidChange int
4848
}
49-
FoldingRange int
50-
Definition int
51-
Hover int
52-
Other int
49+
FoldingRange int
50+
DocumentHighlight int
51+
Definition int
52+
Hover int
53+
Other int
5354
}
5455

5556
// TargetFileExtensions lists the file extensions this LSP supports.
@@ -201,6 +202,16 @@ func main() {
201202
muTextFromClient,
202203
)
203204

205+
case lsp.MethodDocumentHighlight:
206+
serverCounter.DocumentHighlight++
207+
isRequestResponse = true
208+
response, _ = lsp.ProcessDocumentHighlightRequest(
209+
data,
210+
storage.ParsedFiles,
211+
textFromClient,
212+
muTextFromClient,
213+
)
214+
204215
default:
205216
serverCounter.Other++
206217
}

internal/template/template.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,68 @@ func FoldingRange(
471471
return foldingGroups, foldingComments
472472
}
473473

474+
// DocumentHighlight returns the keyword ranges of all linked control flow keywords
475+
// when the cursor is on one of them. For example, if the cursor is on {{if}}, {{else}},
476+
// or {{end}}, all related keywords in the same control flow block are highlighted.
477+
func DocumentHighlight(
478+
rootNode *parser.GroupStatementNode,
479+
position lexer.Position,
480+
) []lexer.Range {
481+
// Find the GroupStatementNode whose KeywordRange contains the position
482+
var foundNode *parser.GroupStatementNode
483+
queue := make([]*parser.GroupStatementNode, 0, 10)
484+
queue = append(queue, rootNode)
485+
counter := 0
486+
487+
for len(queue) > 0 {
488+
if counter++; counter > 10_000 {
489+
panic("infinite loop while computing 'DocumentHighlight()'")
490+
}
491+
492+
node := queue[0]
493+
queue = queue[1:]
494+
495+
// Check if position is within this node's keyword range
496+
if !node.KeywordRange.IsEmpty() && node.KeywordRange.Contains(position) {
497+
foundNode = node
498+
break
499+
}
500+
501+
// Add children to queue
502+
for _, statement := range node.Statements {
503+
if groupNode, ok := statement.(*parser.GroupStatementNode); ok {
504+
queue = append(queue, groupNode)
505+
}
506+
}
507+
}
508+
509+
if foundNode == nil {
510+
return nil
511+
}
512+
513+
// Collect all keyword ranges by walking the NextLinkedSibling circular list
514+
ranges := make([]lexer.Range, 0, 4)
515+
startNode := foundNode
516+
current := foundNode
517+
518+
for {
519+
if !current.KeywordRange.IsEmpty() {
520+
ranges = append(ranges, current.KeywordRange)
521+
}
522+
523+
current = current.NextLinkedSibling
524+
if current == nil || current == startNode {
525+
break
526+
}
527+
528+
if len(ranges) > 100 {
529+
panic("too many linked siblings while computing 'DocumentHighlight()'")
530+
}
531+
}
532+
533+
return ranges
534+
}
535+
474536
// HasFileExtension reports whether fileName's extension is found within extensions.
475537
func HasFileExtension(fileName string, extensions []string) bool {
476538
for _, ext := range extensions {

zed-ext/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "go-template-lsp"
3-
version = "0.5.1"
3+
version = "0.6.0"
44
edition = "2021"
55
license = "MIT"
66

zed-ext/extension.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
id = "go-template-lsp"
22
name = "Go Template LSP"
3-
version = "0.5.1"
3+
version = "0.6.0"
44
schema_version = 1
55
authors = ["Jason Abbott", "yayolande", "Nikita Galaiko", "Mahmud Ridwan"]
66
description = "Go template support with LSP (hover, diagnostics, go-to-definition)"

0 commit comments

Comments
 (0)