Skip to content

Commit 18eabdd

Browse files
authored
Create clickable links in quick info from @link JSDoc tags (#1935)
1 parent 71b47c4 commit 18eabdd

15 files changed

+120
-91
lines changed

internal/fourslash/tests/basicQuickInfo_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ class Foo/*3*/ {
2828
`
2929
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
3030
f.VerifyQuickInfoAt(t, "1", "var someVar: number", "Some var")
31-
f.VerifyQuickInfoAt(t, "2", "var otherVar: number", "Other var\nSee `someVar`")
31+
f.VerifyQuickInfoAt(t, "2", "var otherVar: number", "Other var\nSee [someVar](file:///basicQuickInfo.ts#4,5-4,12)")
3232
}

internal/ls/completions.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5091,7 +5091,7 @@ func (l *LanguageService) getCompletionItemDetails(
50915091
case symbolCompletion.symbol != nil:
50925092
symbolDetails := symbolCompletion.symbol
50935093
actions := l.getCompletionItemActions(ctx, checker, file, position, itemData, symbolDetails)
5094-
return createCompletionDetailsForSymbol(
5094+
return l.createCompletionDetailsForSymbol(
50955095
item,
50965096
symbolDetails.symbol,
50975097
checker,
@@ -5287,7 +5287,7 @@ type codeAction struct {
52875287
changes []*lsproto.TextEdit
52885288
}
52895289

5290-
func createCompletionDetailsForSymbol(
5290+
func (l *LanguageService) createCompletionDetailsForSymbol(
52915291
item *lsproto.CompletionItem,
52925292
symbol *ast.Symbol,
52935293
checker *checker.Checker,
@@ -5300,7 +5300,7 @@ func createCompletionDetailsForSymbol(
53005300
details = append(details, action.description)
53015301
edits = append(edits, action.changes...)
53025302
}
5303-
quickInfo, documentation := getQuickInfoAndDocumentationForSymbol(checker, symbol, location)
5303+
quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(checker, symbol, location)
53045304
details = append(details, quickInfo)
53055305
if len(edits) != 0 {
53065306
item.AdditionalTextEdits = &edits

internal/ls/hover.go

Lines changed: 74 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto.
2727
}
2828
c, done := program.GetTypeCheckerForFile(ctx, file)
2929
defer done()
30-
quickInfo, documentation := getQuickInfoAndDocumentation(c, node)
30+
quickInfo, documentation := l.getQuickInfoAndDocumentation(c, node)
3131
if quickInfo == "" {
3232
return lsproto.HoverOrNull{}, nil
3333
}
@@ -43,19 +43,19 @@ func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto.
4343
}, nil
4444
}
4545

46-
func getQuickInfoAndDocumentation(c *checker.Checker, node *ast.Node) (string, string) {
47-
return getQuickInfoAndDocumentationForSymbol(c, c.GetSymbolAtLocation(node), getNodeForQuickInfo(node))
46+
func (l *LanguageService) getQuickInfoAndDocumentation(c *checker.Checker, node *ast.Node) (string, string) {
47+
return l.getQuickInfoAndDocumentationForSymbol(c, c.GetSymbolAtLocation(node), getNodeForQuickInfo(node))
4848
}
4949

50-
func getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) (string, string) {
50+
func (l *LanguageService) getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbol, node *ast.Node) (string, string) {
5151
quickInfo, declaration := getQuickInfoAndDeclarationAtLocation(c, symbol, node)
5252
if quickInfo == "" {
5353
return "", ""
5454
}
5555
var b strings.Builder
5656
if declaration != nil {
5757
if jsdoc := getJSDocOrTag(declaration); jsdoc != nil && !containsTypedefTag(jsdoc) {
58-
writeComments(&b, jsdoc.Comments())
58+
l.writeComments(&b, c, jsdoc.Comments())
5959
if jsdoc.Kind == ast.KindJSDoc {
6060
if tags := jsdoc.AsJSDoc().Tags; tags != nil {
6161
for _, tag := range tags.Nodes {
@@ -90,7 +90,7 @@ func getQuickInfoAndDocumentationForSymbol(c *checker.Checker, symbol *ast.Symbo
9090
b.WriteString("— ")
9191
}
9292
}
93-
writeComments(&b, comments)
93+
l.writeComments(&b, c, comments)
9494
}
9595
}
9696
}
@@ -411,61 +411,90 @@ func writeCode(b *strings.Builder, lang string, code string) {
411411
b.WriteByte('\n')
412412
}
413413

414-
func writeComments(b *strings.Builder, comments []*ast.Node) {
414+
func (l *LanguageService) writeComments(b *strings.Builder, c *checker.Checker, comments []*ast.Node) {
415415
for _, comment := range comments {
416416
switch comment.Kind {
417417
case ast.KindJSDocText:
418418
b.WriteString(comment.Text())
419-
case ast.KindJSDocLink:
420-
name := comment.Name()
421-
text := comment.AsJSDocLink().Text()
422-
if name != nil {
423-
if text == "" {
424-
writeEntityName(b, name)
425-
} else {
426-
writeEntityNameParts(b, name)
427-
}
428-
}
429-
b.WriteString(text)
419+
case ast.KindJSDocLink, ast.KindJSDocLinkPlain:
420+
l.writeJSDocLink(b, c, comment, false /*quote*/)
430421
case ast.KindJSDocLinkCode:
431-
// !!! TODO: This is a temporary placeholder implementation that needs to be updated later
432-
name := comment.Name()
433-
text := comment.AsJSDocLinkCode().Text()
434-
if name != nil {
435-
if text == "" {
436-
writeEntityName(b, name)
437-
} else {
438-
writeEntityNameParts(b, name)
439-
}
440-
}
441-
b.WriteString(text)
442-
case ast.KindJSDocLinkPlain:
443-
// !!! TODO: This is a temporary placeholder implementation that needs to be updated later
444-
name := comment.Name()
445-
text := comment.AsJSDocLinkPlain().Text()
446-
if name != nil {
447-
if text == "" {
448-
writeEntityName(b, name)
449-
} else {
450-
writeEntityNameParts(b, name)
451-
}
422+
l.writeJSDocLink(b, c, comment, true /*quote*/)
423+
}
424+
}
425+
}
426+
427+
func (l *LanguageService) writeJSDocLink(b *strings.Builder, c *checker.Checker, link *ast.Node, quote bool) {
428+
name := link.Name()
429+
text := strings.Trim(link.Text(), " ")
430+
if name == nil {
431+
writeQuotedString(b, text, quote)
432+
return
433+
}
434+
if ast.IsIdentifier(name) && (name.Text() == "http" || name.Text() == "https") && strings.HasPrefix(text, "://") {
435+
linkText := name.Text() + text
436+
linkUri := linkText
437+
if commentPos := strings.IndexFunc(linkText, func(ch rune) bool { return ch == ' ' || ch == '|' }); commentPos >= 0 {
438+
linkUri = linkText[:commentPos]
439+
linkText = trimCommentPrefix(linkText[commentPos:])
440+
if linkText == "" {
441+
linkText = linkUri
452442
}
453-
b.WriteString(text)
454443
}
444+
writeMarkdownLink(b, linkText, linkUri, quote)
445+
return
446+
}
447+
declarations := getDeclarationsFromLocation(c, name)
448+
if len(declarations) != 0 {
449+
declaration := declarations[0]
450+
file := ast.GetSourceFileOfNode(declaration)
451+
node := core.OrElse(ast.GetNameOfDeclaration(declaration), declaration)
452+
loc := l.getMappedLocation(file.FileName(), createRangeFromNode(node, file))
453+
prefixLen := core.IfElse(strings.HasPrefix(text, "()"), 2, 0)
454+
linkText := trimCommentPrefix(text[prefixLen:])
455+
if linkText == "" {
456+
linkText = getEntityNameString(name) + text[:prefixLen]
457+
}
458+
linkUri := fmt.Sprintf("%s#%d,%d-%d,%d", loc.Uri, loc.Range.Start.Line+1, loc.Range.Start.Character+1, loc.Range.End.Line+1, loc.Range.End.Character+1)
459+
writeMarkdownLink(b, linkText, linkUri, quote)
460+
return
455461
}
462+
writeQuotedString(b, getEntityNameString(name)+" "+text, quote)
463+
}
464+
465+
func trimCommentPrefix(text string) string {
466+
return strings.TrimLeft(strings.TrimPrefix(strings.TrimLeft(text, " "), "|"), " ")
467+
}
468+
469+
func writeMarkdownLink(b *strings.Builder, text string, uri string, quote bool) {
470+
b.WriteString("[")
471+
writeQuotedString(b, text, quote)
472+
b.WriteString("](")
473+
b.WriteString(uri)
474+
b.WriteString(")")
456475
}
457476

458477
func writeOptionalEntityName(b *strings.Builder, name *ast.Node) {
459478
if name != nil {
460479
b.WriteString(" ")
461-
writeEntityName(b, name)
480+
writeQuotedString(b, getEntityNameString(name), true /*quote*/)
481+
}
482+
}
483+
484+
func writeQuotedString(b *strings.Builder, str string, quote bool) {
485+
if quote && !strings.Contains(str, "`") {
486+
b.WriteString("`")
487+
b.WriteString(str)
488+
b.WriteString("`")
489+
} else {
490+
b.WriteString(str)
462491
}
463492
}
464493

465-
func writeEntityName(b *strings.Builder, name *ast.Node) {
466-
b.WriteString("`")
467-
writeEntityNameParts(b, name)
468-
b.WriteString("`")
494+
func getEntityNameString(name *ast.Node) string {
495+
var b strings.Builder
496+
writeEntityNameParts(&b, name)
497+
return b.String()
469498
}
470499

471500
func writeEntityNameParts(b *strings.Builder, node *ast.Node) {

internal/ls/string_completions.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -683,10 +683,10 @@ func (l *LanguageService) getStringLiteralCompletionDetails(
683683
if completions == nil {
684684
return item
685685
}
686-
return stringLiteralCompletionDetails(item, name, contextToken, completions, file, checker)
686+
return l.stringLiteralCompletionDetails(item, name, contextToken, completions, file, checker)
687687
}
688688

689-
func stringLiteralCompletionDetails(
689+
func (l *LanguageService) stringLiteralCompletionDetails(
690690
item *lsproto.CompletionItem,
691691
name string,
692692
location *ast.Node,
@@ -706,7 +706,7 @@ func stringLiteralCompletionDetails(
706706
properties := completion.fromProperties
707707
for _, symbol := range properties.symbols {
708708
if symbol.Name == name {
709-
return createCompletionDetailsForSymbol(item, symbol, checker, location, nil /*actions*/)
709+
return l.createCompletionDetailsForSymbol(item, symbol, checker, location, nil /*actions*/)
710710
}
711711
}
712712
case completion.fromTypes != nil:

testdata/baselines/reference/fourslash/quickInfo/jsdocLink1.baseline

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
// | ```tsx
1717
// | function CC(): void
1818
// | ```
19-
// | `C`
19+
// | [C](file:///jsdocLink1.ts#1,7-1,8)
2020
// |
21-
// | *@wat* — Makes a `C`. A default one.
22-
// | C()
23-
// | C|postfix text
24-
// | unformattedpostfix text
21+
// | *@wat* — Makes a [C](file:///jsdocLink1.ts#1,7-1,8). A default one.
22+
// | [C()](file:///jsdocLink1.ts#1,7-1,8)
23+
// | [postfix text](file:///jsdocLink1.ts#1,7-1,8)
24+
// | unformatted postfix text
2525
// |
26-
// | *@see* — `C` its great
26+
// | *@see* — [C](file:///jsdocLink1.ts#1,7-1,8) its great
2727
// |
2828
// | ----------------------------------------------------------------------
2929
// }
@@ -41,7 +41,7 @@
4141
"item": {
4242
"contents": {
4343
"kind": "markdown",
44-
"value": "```tsx\nfunction CC(): void\n```\n`C`\n\n*@wat* — Makes a `C`. A default one.\nC()\nC|postfix text\nunformattedpostfix text\n\n*@see* — `C` its great\n"
44+
"value": "```tsx\nfunction CC(): void\n```\n[C](file:///jsdocLink1.ts#1,7-1,8)\n\n*@wat* — Makes a [C](file:///jsdocLink1.ts#1,7-1,8). A default one.\n[C()](file:///jsdocLink1.ts#1,7-1,8)\n[postfix text](file:///jsdocLink1.ts#1,7-1,8)\nunformatted postfix text\n\n*@see* — [C](file:///jsdocLink1.ts#1,7-1,8) its great\n"
4545
}
4646
}
4747
}

testdata/baselines/reference/fourslash/quickInfo/jsdocLink4.baseline

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// | ```tsx
99
// | (method) I.bar(): void
1010
// | ```
11-
// | `I`
11+
// | [I](file:///jsdocLink4.ts#1,15-1,16)
1212
// | ----------------------------------------------------------------------
1313
// }
1414
// /** {@link I} */
@@ -18,7 +18,7 @@
1818
// | ```tsx
1919
// | var n: number
2020
// | ```
21-
// | `I`
21+
// | [I](file:///jsdocLink4.ts#1,15-1,16)
2222
// | ----------------------------------------------------------------------
2323
// /**
2424
// * A real, very serious {@link I to an interface}. Right there.
@@ -32,9 +32,9 @@
3232
// | ```tsx
3333
// | function f(x: any): void
3434
// | ```
35-
// | A real, very serious Ito an interface. Right there.
35+
// | A real, very serious [to an interface](file:///jsdocLink4.ts#1,15-1,16). Right there.
3636
// |
37-
// | *@param* `x` — one Poshere too
37+
// | *@param* `x` — one [here too](file:///jsdocLink4.ts#14,6-14,9)
3838
// | ----------------------------------------------------------------------
3939
// type Pos = [number, number]
4040
[
@@ -51,7 +51,7 @@
5151
"item": {
5252
"contents": {
5353
"kind": "markdown",
54-
"value": "```tsx\n(method) I.bar(): void\n```\n`I`"
54+
"value": "```tsx\n(method) I.bar(): void\n```\n[I](file:///jsdocLink4.ts#1,15-1,16)"
5555
}
5656
}
5757
},
@@ -68,7 +68,7 @@
6868
"item": {
6969
"contents": {
7070
"kind": "markdown",
71-
"value": "```tsx\nvar n: number\n```\n`I`"
71+
"value": "```tsx\nvar n: number\n```\n[I](file:///jsdocLink4.ts#1,15-1,16)"
7272
}
7373
}
7474
},
@@ -85,7 +85,7 @@
8585
"item": {
8686
"contents": {
8787
"kind": "markdown",
88-
"value": "```tsx\nfunction f(x: any): void\n```\nA real, very serious Ito an interface. Right there.\n\n*@param* `x` — one Poshere too"
88+
"value": "```tsx\nfunction f(x: any): void\n```\nA real, very serious [to an interface](file:///jsdocLink4.ts#1,15-1,16). Right there.\n\n*@param* `x` — one [here too](file:///jsdocLink4.ts#14,6-14,9)"
8989
}
9090
}
9191
}

testdata/baselines/reference/fourslash/quickInfo/jsdocLink5.baseline

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
// | ```tsx
1414
// | function f(x: any): void
1515
// | ```
16-
// | g() g() g() g() 0 g()1 g() 2
17-
// | u() u() u() u() 0 u()1 u() 2
16+
// | [g()](file:///jsdocLink5.ts#1,10-1,11) [g()](file:///jsdocLink5.ts#1,10-1,11) [g()](file:///jsdocLink5.ts#1,10-1,11) [0](file:///jsdocLink5.ts#1,10-1,11) [1](file:///jsdocLink5.ts#1,10-1,11) [2](file:///jsdocLink5.ts#1,10-1,11)
17+
// | u () u () u () u () 0 u ()1 u () 2
1818
// | ----------------------------------------------------------------------
1919
[
2020
{
@@ -30,7 +30,7 @@
3030
"item": {
3131
"contents": {
3232
"kind": "markdown",
33-
"value": "```tsx\nfunction f(x: any): void\n```\ng() g() g() g() 0 g()1 g() 2\nu() u() u() u() 0 u()1 u() 2"
33+
"value": "```tsx\nfunction f(x: any): void\n```\n[g()](file:///jsdocLink5.ts#1,10-1,11) [g()](file:///jsdocLink5.ts#1,10-1,11) [g()](file:///jsdocLink5.ts#1,10-1,11) [0](file:///jsdocLink5.ts#1,10-1,11) [1](file:///jsdocLink5.ts#1,10-1,11) [2](file:///jsdocLink5.ts#1,10-1,11)\nu () u () u () u () 0 u ()1 u () 2"
3434
}
3535
}
3636
}

testdata/baselines/reference/fourslash/quickInfo/quickInfoForJSDocWithHttpLinks.baseline

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
// | ```
5353
// |
5454
// |
55-
// | *@see* — https://hva
55+
// | *@see* — [https://hva](https://hva)
5656
// | ----------------------------------------------------------------------
5757
//
5858
// /** {@link https://hvaD} */
@@ -62,7 +62,7 @@
6262
// | ```tsx
6363
// | var see3: boolean
6464
// | ```
65-
// | https://hvaD
65+
// | [https://hvaD](https://hvaD)
6666
// | ----------------------------------------------------------------------
6767
[
6868
{
@@ -146,7 +146,7 @@
146146
"item": {
147147
"contents": {
148148
"kind": "markdown",
149-
"value": "```tsx\nvar see2: boolean\n```\n\n\n*@see* — https://hva "
149+
"value": "```tsx\nvar see2: boolean\n```\n\n\n*@see* — [https://hva](https://hva) "
150150
}
151151
}
152152
},
@@ -163,7 +163,7 @@
163163
"item": {
164164
"contents": {
165165
"kind": "markdown",
166-
"value": "```tsx\nvar see3: boolean\n```\nhttps://hvaD"
166+
"value": "```tsx\nvar see3: boolean\n```\n[https://hvaD](https://hvaD)"
167167
}
168168
}
169169
}

0 commit comments

Comments
 (0)