Skip to content
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