Skip to content

Commit e6a261d

Browse files
Add documentation links to LSP CEL hover (#4385)
Similar to how we add links for protobuf hover docs to <protobuf.com> or <buf.build>, this adds relevant links for the various CEL functions/operators/etc. to <celbyexample.com> or <protovalidate.com>.
1 parent 0c54c1f commit e6a261d

3 files changed

Lines changed: 199 additions & 74 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## [Unreleased]
44

55
- Add support for `--rbs_out` as a `protoc_builtin` plugin (requires protoc v34.0+).
6+
- Add relevant links from CEL LSP hover documentation to either <celbyexample.com> or <protovalidate.com>
67

78
## [v1.66.1] - 2026-03-09
89

private/buf/buflsp/hover_cel.go

Lines changed: 136 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/google/cel-go/cel"
2525
"github.com/google/cel-go/common"
2626
"github.com/google/cel-go/common/ast"
27+
"github.com/google/cel-go/common/operators"
2728
"github.com/google/cel-go/common/overloads"
2829
"github.com/google/cel-go/common/types"
2930
"go.lsp.dev/protocol"
@@ -497,25 +498,25 @@ func formatCELHoverContent(info *celHoverInfo, celEnv *cel.Env) string {
497498
return ""
498499
}
499500

501+
var result string
500502
switch info.kind {
501503
case celHoverKeyword:
502-
return getCELKeywordDocs(info.text)
504+
result = getCELKeywordDocs(info.text)
503505
case celHoverFunction:
504-
result := getCELFunctionDocs(info.text, celEnv)
506+
result = getCELFunctionDocs(info.text, celEnv)
505507
// Add inferred return type if available
506508
if info.celType != nil {
507509
result += fmt.Sprintf("\n\n**Return type**: `%s`", getCELTypeString(info.celType))
508510
}
509-
return result
510511
case celHoverOperator:
511-
return getCELOperatorDocs(info.text, celEnv)
512+
result = getCELOperatorDocs(info.text, celEnv)
512513
case celHoverMacro:
513-
return getCELMacroDocs(info.text, celEnv)
514+
result = getCELMacroDocs(info.text, celEnv)
514515
case celHoverType:
515-
return getCELTypeDocs(info.text, celEnv)
516+
result = getCELTypeDocs(info.text, celEnv)
516517
case celHoverField:
517518
// For fields, show comprehensive proto field info
518-
result := fmt.Sprintf("**Field**: `%s`", info.text)
519+
result = fmt.Sprintf("**Field**: `%s`", info.text)
519520

520521
// Add CEL type information
521522
if info.celType != nil {
@@ -526,25 +527,148 @@ func formatCELHoverContent(info *celHoverInfo, celEnv *cel.Env) string {
526527
if !info.protoMember.IsZero() {
527528
result += "\n\n" + getProtoFieldDocumentation(info.protoMember)
528529
}
529-
530-
return result
531530
case celHoverVariable:
532531
// Comprehension variable
533-
result := fmt.Sprintf("**Variable**: `%s`\n\nLoop variable from comprehension.", info.text)
532+
result = fmt.Sprintf("**Variable**: `%s`\n\nLoop variable from comprehension.", info.text)
534533
if info.celType != nil {
535534
result += fmt.Sprintf("\n\n**Type**: `%s`", getCELTypeString(info.celType))
536535
}
537-
return result
538536
case celHoverLiteral:
539537
// Literal value
540538
typeName := "value"
541539
if info.celType != nil {
542540
typeName = getCELTypeString(info.celType)
543541
}
544-
return fmt.Sprintf("**Literal**: `%s`\n\n**Type**: %s", info.text, typeName)
542+
result = fmt.Sprintf("**Literal**: `%s`\n\n**Type**: %s", info.text, typeName)
545543
default:
546544
return ""
547545
}
546+
547+
// Append documentation link if available
548+
if link := celDocumentationLinks(info.kind, info.text); link != "" {
549+
result += "\n\n" + link
550+
}
551+
552+
return result
553+
}
554+
555+
// celDocumentationLinks returns a markdown link to external documentation for a CEL hover item.
556+
// Returns an empty string if no documentation link is available.
557+
func celDocumentationLinks(kind celHoverKind, name string) string {
558+
switch kind {
559+
case celHoverKeyword:
560+
if name == "this" {
561+
return celProtovalidateLink("this")
562+
}
563+
case celHoverOperator:
564+
switch name {
565+
case operators.LogicalAnd:
566+
return celByExampleLink("logical-operators/#and")
567+
case operators.LogicalOr:
568+
return celByExampleLink("logical-operators/#or")
569+
case operators.LogicalNot:
570+
return celByExampleLink("logical-operators/#not")
571+
case operators.Equals, operators.NotEquals:
572+
return celByExampleLink("comparison/#equality")
573+
case operators.Less, operators.LessEquals, operators.Greater, operators.GreaterEquals:
574+
return celByExampleLink("comparison/#ordering")
575+
case operators.Add, operators.Subtract, operators.Multiply, operators.Divide, operators.Modulo:
576+
return celByExampleLink("arithmetic/")
577+
case operators.In:
578+
return celByExampleLink("collections/#membership-and-access")
579+
case operators.Conditional:
580+
return celByExampleLink("ternary/")
581+
case operators.Index:
582+
return celByExampleLink("lists/")
583+
}
584+
case celHoverMacro:
585+
switch name {
586+
case operators.Has:
587+
return celByExampleLink("has/")
588+
case operators.All:
589+
return celByExampleLink("all/")
590+
case operators.Exists:
591+
return celByExampleLink("exists/")
592+
case operators.ExistsOne:
593+
return celByExampleLink("exists-one/")
594+
case operators.Filter:
595+
return celByExampleLink("filter/")
596+
case operators.Map:
597+
return celByExampleLink("map-macro/")
598+
}
599+
case celHoverFunction:
600+
switch name {
601+
// String functions
602+
case overloads.Size:
603+
return celByExampleLink("strings/#size")
604+
case overloads.Contains, overloads.StartsWith, overloads.EndsWith:
605+
return celByExampleLink("strings/#substring-search")
606+
case overloads.Matches:
607+
return celByExampleLink("strings/#regular-expressions")
608+
case "split", "join":
609+
return celByExampleLink("strings/#split-and-join")
610+
case "lowerAscii", "upperAscii":
611+
return celByExampleLink("strings/#case-conversion")
612+
case "indexOf", "lastIndexOf":
613+
return celByExampleLink("strings/#position-search")
614+
case "charAt":
615+
return celByExampleLink("strings/#character-access")
616+
case "substring":
617+
return celByExampleLink("strings/#substring")
618+
case "replace":
619+
return celByExampleLink("strings/#replace")
620+
case "trim":
621+
return celByExampleLink("strings/#trim")
622+
case "reverse":
623+
return celByExampleLink("strings/#reverse")
624+
// Timestamp and duration functions
625+
case overloads.TimeGetFullYear, overloads.TimeGetMonth, overloads.TimeGetDate,
626+
overloads.TimeGetDayOfMonth, overloads.TimeGetDayOfWeek, overloads.TimeGetDayOfYear,
627+
overloads.TimeGetHours, overloads.TimeGetMinutes, overloads.TimeGetSeconds,
628+
overloads.TimeGetMilliseconds:
629+
return celByExampleLink("time/#timestamp-components")
630+
// Protovalidate string extension functions
631+
case "isEmail":
632+
return celProtovalidateLink("isemail")
633+
case "isHostname":
634+
return celProtovalidateLink("ishostname")
635+
case "isIp":
636+
return celProtovalidateLink("isip")
637+
case "isIpPrefix":
638+
return celProtovalidateLink("isipprefix")
639+
case "isUri":
640+
return celProtovalidateLink("isuri")
641+
case "isUriRef":
642+
return celProtovalidateLink("isuriref")
643+
case "isHostAndPort":
644+
return celProtovalidateLink("ishostandport")
645+
}
646+
case celHoverType:
647+
switch name {
648+
case overloads.TypeConvertInt, overloads.TypeConvertUint, overloads.TypeConvertDouble:
649+
return celByExampleLink("type-conversions/#numeric-conversions")
650+
case overloads.TypeConvertString:
651+
return celByExampleLink("type-conversions/#string-conversions")
652+
case overloads.TypeConvertBytes:
653+
return celByExampleLink("type-conversions/#bytes-conversions")
654+
case overloads.TypeConvertTimestamp, overloads.TypeConvertDuration:
655+
return celByExampleLink("type-conversions/#time-conversions")
656+
case overloads.TypeConvertDyn:
657+
return celByExampleLink("type-conversions/#dynamic-type")
658+
}
659+
}
660+
return ""
661+
}
662+
663+
// celByExampleLink returns a markdown link to a page on celbyexample.com.
664+
func celByExampleLink(path string) string {
665+
return "[CEL by Example](https://celbyexample.com/" + path + ")"
666+
}
667+
668+
// celProtovalidateLink returns a markdown link to an anchor on the Protovalidate CEL extensions
669+
// reference page.
670+
func celProtovalidateLink(anchor string) string {
671+
return "[Protovalidate CEL Extensions](https://protovalidate.com/reference/cel_extensions/#" + anchor + ")"
548672
}
549673

550674
// resolveCELFieldAccess resolves a CEL SelectExpr to a proto field/member.

0 commit comments

Comments
 (0)