Skip to content

Port over some baselined hover/quick info tests #1476

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 30, 2025
Merged
27 changes: 26 additions & 1 deletion internal/fourslash/_scripts/convertFourslash.mts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
case "baselineFindAllReferences":
// `verify.baselineFindAllReferences(...)`
return [parseBaselineFindAllReferencesArgs(callExpression.arguments)];
case "baselineQuickInfo":
return [parseBaselineQuickInfo(callExpression.arguments)];
case "baselineGoToDefinition":
case "baselineGetDefinitionAtPosition":
// Both of these take the same arguments, but differ in that...
Expand Down Expand Up @@ -702,6 +704,16 @@ function parseBaselineGoToDefinitionArgs(args: readonly ts.Expression[]): Verify
};
}

function parseBaselineQuickInfo(args: ts.NodeArray<ts.Expression>): Cmd {
if (args.length !== 0) {
// All calls are currently empty!
throw new Error("Expected no arguments in verify.baselineQuickInfo");
}
return {
kind: "verifyBaselineQuickInfo",
};
}

function parseKind(expr: ts.Expression): string | undefined {
if (!ts.isStringLiteral(expr)) {
console.error(`Expected string literal for kind, got ${expr.getText()}`);
Expand Down Expand Up @@ -849,6 +861,10 @@ interface VerifyBaselineGoToDefinitionCmd {
ranges?: boolean;
}

interface VerifyBaselineQuickInfoCmd {
kind: "verifyBaselineQuickInfo";
}

interface GoToCmd {
kind: "goTo";
// !!! `selectRange` and `rangeStart` require parsing variables and `test.ranges()[n]`
Expand All @@ -861,7 +877,13 @@ interface EditCmd {
goStatement: string;
}

type Cmd = VerifyCompletionsCmd | VerifyBaselineFindAllReferencesCmd | VerifyBaselineGoToDefinitionCmd | GoToCmd | EditCmd;
type Cmd =
| VerifyCompletionsCmd
| VerifyBaselineFindAllReferencesCmd
| VerifyBaselineGoToDefinitionCmd
| VerifyBaselineQuickInfoCmd
| GoToCmd
| EditCmd;

function generateVerifyCompletions({ marker, args, isNewIdentifierLocation }: VerifyCompletionsCmd): string {
let expectedList: string;
Expand Down Expand Up @@ -917,6 +939,9 @@ function generateCmd(cmd: Cmd): string {
return generateBaselineFindAllReferences(cmd as VerifyBaselineFindAllReferencesCmd);
case "verifyBaselineGoToDefinition":
return generateBaselineGoToDefinition(cmd as VerifyBaselineGoToDefinitionCmd);
case "verifyBaselineQuickInfo":
// Quick Info -> Hover
return `f.VerifyBaselineHover(t)`;
case "goTo":
return generateGoToCommand(cmd as GoToCmd);
case "edit":
Expand Down
4 changes: 4 additions & 0 deletions internal/fourslash/_scripts/failingTests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ TestJsDocFunctionTypeCompletionsNoCrash
TestJsdocExtendsTagCompletion
TestJsdocImplementsTagCompletion
TestJsdocImportTagCompletion1
TestJsdocLink2
TestJsdocLink3
TestJsdocLink6
TestJsdocLink_findAllReferences1
TestJsdocOverloadTagCompletion
TestJsdocParameterNameCompletion
Expand Down Expand Up @@ -302,6 +305,7 @@ TestPathCompletionsTypesVersionsWildcard4
TestPathCompletionsTypesVersionsWildcard5
TestPathCompletionsTypesVersionsWildcard6
TestProtoVarVisibleWithOuterScopeUnderscoreProto
TestQuickInfoAlias
TestReferencesForExportedValues
TestReferencesForStatementKeywords
TestReferencesInComment
Expand Down
100 changes: 100 additions & 0 deletions internal/fourslash/baselineutil.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package fourslash

import (
"cmp"
"errors"
"fmt"
"io/fs"
"regexp"
"slices"
"strings"
"testing"

"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
Expand Down Expand Up @@ -443,6 +445,104 @@ func (t *textWithContext) readableJsoncBaseline(text string) {
}
}

type markerAndItem[T any] struct {
Marker *Marker `json:"marker"`
Item T `json:"item"`
}

func annotateContentWithTooltips[T comparable](
t *testing.T,
f *FourslashTest,
markersAndItems []markerAndItem[T],
opName string,
getRange func(item T) *lsproto.Range,
getTooltipLines func(item T, prev T) []string,
) string {
barWithGutter := "| " + strings.Repeat("-", 70)

// sort by file, then *backwards* by position in the file
// so we can insert multiple times on a line without counting
sorted := slices.Clone(markersAndItems)
slices.SortFunc(sorted, func(a, b markerAndItem[T]) int {
if c := cmp.Compare(a.Marker.FileName(), b.Marker.FileName()); c != 0 {
return c
}
return -cmp.Compare(a.Marker.Position, b.Marker.Position)
})

filesToLines := collections.NewOrderedMapWithSizeHint[string, []string](1)
var previous T
for _, itemAndMarker := range sorted {
marker := itemAndMarker.Marker
item := itemAndMarker.Item

textRange := getRange(item)
if textRange == nil {
start := marker.LSPosition
end := start
end.Character = end.Character + 1
textRange = &lsproto.Range{Start: start, End: end}
}

if textRange.Start.Line != textRange.End.Line {
t.Fatalf("Expected text range to be on a single line, got %v", textRange)
}
underline := strings.Repeat(" ", int(textRange.Start.Character)) +
strings.Repeat("^", int(textRange.End.Character-textRange.Start.Character))

fileName := marker.FileName()
lines, ok := filesToLines.Get(fileName)
if !ok {
lines = lineSplitter.Split(f.getScriptInfo(fileName).content, -1)
}

var tooltipLines []string
if item != *new(T) {
tooltipLines = getTooltipLines(item, previous)
}
if len(tooltipLines) == 0 {
tooltipLines = []string{fmt.Sprintf("No %s at /*%s*/.", opName, *marker.Name)}
}
tooltipLines = core.Map(tooltipLines, func(line string) string {
return "| " + line
})

linesToInsert := make([]string, len(tooltipLines)+3)
linesToInsert[0] = underline
linesToInsert[1] = barWithGutter
copy(linesToInsert[2:], tooltipLines)
linesToInsert[len(linesToInsert)-1] = barWithGutter

lines = slices.Insert(
lines,
int(textRange.Start.Line+1),
linesToInsert...,
)
filesToLines.Set(fileName, lines)

previous = item
}

builder := strings.Builder{}
seenFirst := false
for fileName, lines := range filesToLines.Entries() {
builder.WriteString(fmt.Sprintf("=== %s ===\n", fileName))
for _, line := range lines {
builder.WriteString("// ")
builder.WriteString(line)
builder.WriteByte('\n')
}

if seenFirst {
builder.WriteString("\n\n")
} else {
seenFirst = true
}
}

return builder.String()
}

func (t *textWithContext) sliceOfContent(start *int, end *int) string {
if start == nil || *start < 0 {
start = ptrTo(0)
Expand Down
93 changes: 93 additions & 0 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,99 @@ func (f *FourslashTest) VerifyBaselineGoToDefinition(
baseline.Run(t, f.baseline.getBaselineFileName(), f.baseline.content.String(), baseline.Options{})
}

func (f *FourslashTest) VerifyBaselineHover(t *testing.T) {
if f.baseline != nil {
t.Fatalf("Error during test '%s': Another baseline is already in progress", t.Name())
} else {
f.baseline = &baselineFromTest{
content: &strings.Builder{},
baselineName: "hover/" + strings.TrimPrefix(t.Name(), "Test"),
ext: ".baseline",
}
}

// empty baseline after test completes
defer func() {
f.baseline = nil
}()

markersAndItems := core.MapFiltered(f.Markers(), func(marker *Marker) (markerAndItem[*lsproto.Hover], bool) {
if marker.Name == nil {
return markerAndItem[*lsproto.Hover]{}, false
}

params := &lsproto.HoverParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: ls.FileNameToDocumentURI(f.activeFilename),
},
Position: marker.LSPosition,
}

resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentHoverInfo, params)
var prefix string
if f.lastKnownMarkerName != nil {
prefix = fmt.Sprintf("At marker '%s': ", *f.lastKnownMarkerName)
} else {
prefix = fmt.Sprintf("At position (Ln %d, Col %d): ", f.currentCaretPosition.Line, f.currentCaretPosition.Character)
}
if resMsg == nil {
t.Fatalf(prefix+"Nil response received for quick info request", f.lastKnownMarkerName)
}
if !resultOk {
t.Fatalf(prefix+"Unexpected response type for quick info request: %T", resMsg.AsResponse().Result)
}

return markerAndItem[*lsproto.Hover]{Marker: marker, Item: result.Hover}, true
})

getRange := func(item *lsproto.Hover) *lsproto.Range {
if item == nil || item.Range == nil {
return nil
}
return item.Range
}

getTooltipLines := func(item, _prev *lsproto.Hover) []string {
var result []string

if item.Contents.MarkupContent != nil {
result = strings.Split(item.Contents.MarkupContent.Value, "\n")
}
if item.Contents.String != nil {
result = strings.Split(*item.Contents.String, "\n")
}
if item.Contents.MarkedStringWithLanguage != nil {
result = appendLinesForMarkedStringWithLanguage(result, item.Contents.MarkedStringWithLanguage)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably don't need this, I think this has been deprecated, but I guess it's fine to have it.

}
if item.Contents.MarkedStrings != nil {
for _, ms := range *item.Contents.MarkedStrings {
if ms.MarkedStringWithLanguage != nil {
result = appendLinesForMarkedStringWithLanguage(result, ms.MarkedStringWithLanguage)
} else {
result = append(result, *ms.String)
}
}
}

return result
}

f.baseline.addResult("QuickInfo", annotateContentWithTooltips(t, f, markersAndItems, "quickinfo", getRange, getTooltipLines))
if jsonStr, err := core.StringifyJson(markersAndItems, "", " "); err == nil {
f.baseline.content.WriteString(jsonStr)
} else {
t.Fatalf("Failed to stringify markers and items for baseline: %v", err)
}
baseline.Run(t, f.baseline.getBaselineFileName(), f.baseline.content.String(), baseline.Options{})
}

func appendLinesForMarkedStringWithLanguage(result []string, ms *lsproto.MarkedStringWithLanguage) []string {
result = append(result, "```"+ms.Language)
result = append(result, ms.Value)
result = append(result, "```")
return result
}

// Collects all named markers if provided, or defaults to anonymous ranges
func (f *FourslashTest) lookupMarkersOrGetRanges(t *testing.T, markers []string) []MarkerOrRange {
var referenceLocations []MarkerOrRange
Expand Down
Loading
Loading