@@ -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 \n Loop variable from comprehension." , info .text )
532+ result = fmt .Sprintf ("**Variable**: `%s`\n \n Loop 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