diff --git a/internal/ast/ast.go b/internal/ast/ast.go index ab2f247e68..11c0fd53d3 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -309,6 +309,12 @@ func (n *Node) Text() string { return n.AsRegularExpressionLiteral().Text case KindJSDocText: return strings.Join(n.AsJSDocText().text, "") + case KindJSDocLink: + return strings.Join(n.AsJSDocLink().text, "") + case KindJSDocLinkCode: + return strings.Join(n.AsJSDocLinkCode().text, "") + case KindJSDocLinkPlain: + return strings.Join(n.AsJSDocLinkPlain().text, "") } panic(fmt.Sprintf("Unhandled case in Node.Text: %T", n.data)) } diff --git a/internal/ls/hover.go b/internal/ls/hover.go index f0fb670c3b..5720b920eb 100644 --- a/internal/ls/hover.go +++ b/internal/ls/hover.go @@ -407,6 +407,30 @@ func writeComments(b *strings.Builder, comments []*ast.Node) { } } b.WriteString(text) + case ast.KindJSDocLinkCode: + // !!! TODO: This is a temporary placeholder implementation that needs to be updated later + name := comment.Name() + text := comment.AsJSDocLinkCode().Text() + if name != nil { + if text == "" { + writeEntityName(b, name) + } else { + writeEntityNameParts(b, name) + } + } + b.WriteString(text) + case ast.KindJSDocLinkPlain: + // !!! TODO: This is a temporary placeholder implementation that needs to be updated later + name := comment.Name() + text := comment.AsJSDocLinkPlain().Text() + if name != nil { + if text == "" { + writeEntityName(b, name) + } else { + writeEntityNameParts(b, name) + } + } + b.WriteString(text) } } } diff --git a/internal/ls/hover_test.go b/internal/ls/hover_test.go new file mode 100644 index 0000000000..d4057f9756 --- /dev/null +++ b/internal/ls/hover_test.go @@ -0,0 +1,97 @@ +package ls_test + +import ( + "context" + "testing" + + "github.com/microsoft/typescript-go/internal/bundled" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" + "gotest.tools/v3/assert" +) + +func TestHover(t *testing.T) { + t.Parallel() + if !bundled.Embedded { + // Without embedding, we'd need to read all of the lib files out from disk into the MapFS. + // Just skip this for now. + t.Skip("bundled files are not embedded") + } + + testCases := []struct { + title string + input string + expected map[string]*lsproto.Hover + }{ + { + title: "JSDocLinksPanic", + input: ` +// @filename: index.ts +/** + * A function with JSDoc links that previously caused panic + * {@link console.log} and {@linkcode Array.from} and {@linkplain Object.keys} + */ +function myFunction() { + return "test"; +} + +/*marker*/myFunction();`, + expected: map[string]*lsproto.Hover{ + "marker": { + Contents: lsproto.MarkupContentOrMarkedStringOrMarkedStrings{ + MarkupContent: &lsproto.MarkupContent{ + Kind: lsproto.MarkupKindMarkdown, + Value: "```tsx\nfunction myFunction(): string\n```\nA function with JSDoc links that previously caused panic\n`console.log` and `Array.from` and `Object.keys`\n", + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.title, func(t *testing.T) { + t.Parallel() + runHoverTest(t, testCase.input, testCase.expected) + }) + } +} + +func runHoverTest(t *testing.T, input string, expected map[string]*lsproto.Hover) { + testData := fourslash.ParseTestData(t, input, "/mainFile.ts") + file := testData.Files[0].FileName() + markerPositions := testData.MarkerPositions + ctx := projecttestutil.WithRequestID(t.Context()) + languageService, done := createLanguageServiceForHover(ctx, file, map[string]any{ + file: testData.Files[0].Content, + }) + defer done() + + for markerName, expectedResult := range expected { + marker, ok := markerPositions[markerName] + if !ok { + t.Fatalf("No marker found for '%s'", markerName) + } + result, err := languageService.ProvideHover( + ctx, + ls.FileNameToDocumentURI(file), + marker.LSPosition) + assert.NilError(t, err) + if expectedResult == nil { + assert.Assert(t, result == nil) + } else { + assert.Assert(t, result != nil) + assert.DeepEqual(t, *result, *expectedResult) + } + } +} + +func createLanguageServiceForHover(ctx context.Context, fileName string, files map[string]any) (*ls.LanguageService, func()) { + projectService, _ := projecttestutil.Setup(files, nil) + projectService.OpenFile(fileName, files[fileName].(string), core.GetScriptKindFromFileName(fileName), "") + project := projectService.Projects()[0] + return project.GetLanguageServiceForRequest(ctx) +}