Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/fourslash/baselineutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ func getBaselineExtension(command string) string {
return "baseline"
case "Auto Imports":
return "baseline.md"
case "formatDocument", "formatSelection", "formatOnType":
return "baseline"
case "findAllReferences", "goToDefinition", "findRenameLocations":
return "baseline.jsonc"
default:
Expand Down
175 changes: 175 additions & 0 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -2221,3 +2221,178 @@ func (f *FourslashTest) verifyBaselines(t *testing.T) {
}

var AnyTextEdits *[]*lsproto.TextEdit

// VerifyFormatDocument verifies formatting of the entire document.
// It sends a textDocument/formatting request and compares the result with a baseline.
func (f *FourslashTest) VerifyFormatDocument(t *testing.T, options *lsproto.FormattingOptions) {
if options == nil {
options = &lsproto.FormattingOptions{
TabSize: 4,
InsertSpaces: true,
}
}

params := &lsproto.DocumentFormattingParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
},
Options: options,
}

resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentFormattingInfo, params)
if resMsg == nil {
t.Fatal("Nil response received for document formatting request")
}
if !resultOk {
// Check if result is nil - this is valid, just means no formatting needed
resp := resMsg.AsResponse()
if resp.Result == nil {
f.addFormattingResultToBaseline(t, "formatDocument", nil)
return
}
t.Fatalf("Unexpected response type for document formatting request: %T", resp.Result)
}

f.addFormattingResultToBaseline(t, "formatDocument", result.TextEdits)
}

// VerifyFormatSelection verifies formatting of a selected range.
// It sends a textDocument/rangeFormatting request and compares the result with a baseline.
// rangeMarker should be obtained from f.Ranges()[index].
func (f *FourslashTest) VerifyFormatSelection(t *testing.T, rangeMarker *RangeMarker, options *lsproto.FormattingOptions) {
if options == nil {
options = &lsproto.FormattingOptions{
TabSize: 4,
InsertSpaces: true,
}
}

f.ensureActiveFile(t, rangeMarker.FileName())
formatRange := rangeMarker.LSRange

params := &lsproto.DocumentRangeFormattingParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
},
Range: formatRange,
Options: options,
}

resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentRangeFormattingInfo, params)
if resMsg == nil {
t.Fatal("Nil response received for range formatting request")
}
if !resultOk {
// Check if result is nil - this is valid, just means no formatting needed
resp := resMsg.AsResponse()
if resp.Result == nil {
f.addFormattingResultToBaseline(t, "formatSelection", nil)
return
}
t.Fatalf("Unexpected response type for range formatting request: %T", resp.Result)
}

f.addFormattingResultToBaseline(t, "formatSelection", result.TextEdits)
}

// VerifyFormatOnType verifies on-type formatting (e.g., after typing `;`, `}`, or newline).
// It sends a textDocument/onTypeFormatting request and compares the result with a baseline.
// markerName should be the name of a marker in the test file (e.g., "a" for /*a*/).
func (f *FourslashTest) VerifyFormatOnType(t *testing.T, markerName string, character string, options *lsproto.FormattingOptions) {
if options == nil {
options = &lsproto.FormattingOptions{
TabSize: 4,
InsertSpaces: true,
}
}

f.GoToMarker(t, markerName)

params := &lsproto.DocumentOnTypeFormattingParams{
TextDocument: lsproto.TextDocumentIdentifier{
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
},
Position: f.currentCaretPosition,
Ch: character,
Options: options,
}

resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentOnTypeFormattingInfo, params)
if resMsg == nil {
t.Fatal("Nil response received for on-type formatting request")
}
if !resultOk {
// Check if result is nil - this is valid, just means no formatting needed
resp := resMsg.AsResponse()
if resp.Result == nil {
// No formatting edits needed
f.addFormattingResultToBaseline(t, "formatOnType", nil)
return
}
t.Fatalf("Unexpected response type for on-type formatting request: %T", resp.Result)
}

f.addFormattingResultToBaseline(t, "formatOnType", result.TextEdits)
}

// addFormattingResultToBaseline adds formatting results to the baseline.
// It shows the original file content and the formatted content side by side.
func (f *FourslashTest) addFormattingResultToBaseline(t *testing.T, command string, edits *[]*lsproto.TextEdit) {
script := f.getScriptInfo(f.activeFilename)
originalContent := script.content

var formattedContent string
if edits == nil || len(*edits) == 0 {
formattedContent = originalContent
} else {
// Apply edits to get formatted content
formattedContent = f.applyEditsToString(originalContent, *edits)
}

var result strings.Builder
result.WriteString(fmt.Sprintf("// Original (%s):\n", f.activeFilename))
for _, line := range strings.Split(originalContent, "\n") {
result.WriteString("// " + line + "\n")
}
result.WriteString("\n")
result.WriteString("// Formatted:\n")
for _, line := range strings.Split(formattedContent, "\n") {
result.WriteString("// " + line + "\n")
}

f.addResultToBaseline(t, command, result.String())
}

// applyEditsToString applies text edits to a string and returns the result.
func (f *FourslashTest) applyEditsToString(content string, edits []*lsproto.TextEdit) string {
script := newScriptInfo(f.activeFilename, content)
converters := lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *lsconv.LSPLineMap {
return script.lineMap
})

// Sort edits in reverse order to avoid affecting positions
// Create a slice with cached position conversions for efficient sorting
type editWithPosition struct {
edit *lsproto.TextEdit
pos core.TextPos
}
editsWithPositions := make([]editWithPosition, len(edits))
for i, edit := range edits {
editsWithPositions[i] = editWithPosition{
edit: edit,
pos: converters.LineAndCharacterToPosition(script, edit.Range.Start),
}
}
slices.SortFunc(editsWithPositions, func(a, b editWithPosition) int {
return int(b.pos) - int(a.pos)
})

result := content
for _, editWithPos := range editsWithPositions {
start := int(editWithPos.pos)
end := int(converters.LineAndCharacterToPosition(script, editWithPos.edit.Range.End))
result = result[:start] + editWithPos.edit.NewText + result[end:]
}

return result
}
24 changes: 24 additions & 0 deletions internal/fourslash/tests/basicFormatDocument_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestBasicFormatDocument(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `const x = 1 ;
function foo ( a , b ) {
return a + b ;
}
const y = foo( 2 , 3 ) ;`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyFormatDocument(t, &lsproto.FormattingOptions{
TabSize: 4,
InsertSpaces: true,
})
}
23 changes: 23 additions & 0 deletions internal/fourslash/tests/basicFormatOnType_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestBasicFormatOnType(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `function foo() {/*a*/
const x=1;
}`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
// Verify formatting after typing opening curly brace
f.VerifyFormatOnType(t, "a", "{", &lsproto.FormattingOptions{
TabSize: 4,
InsertSpaces: true,
})
}
24 changes: 24 additions & 0 deletions internal/fourslash/tests/basicFormatSelection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/testutil"
)

func TestBasicFormatSelection(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `const x = 1;
[|function foo(a,b){return a+b;}|]
const y = 2;`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
// Format only the function declaration
rangeToFormat := f.Ranges()[0]
f.VerifyFormatSelection(t, rangeToFormat, &lsproto.FormattingOptions{
TabSize: 4,
InsertSpaces: true,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// === formatDocument ===
// Original (/basicFormatDocument.ts):
// const x = 1 ;
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason why each line of the original and formatted context is prefixed with // ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The // prefix makes baseline files valid code comments, making them easier to read when viewed directly. This follows the pattern used by other baseline-generating methods in fourslash (e.g., getBaselineForLocationsWithFileContents). The approach keeps baselines human-readable while ensuring they're syntactically valid.

// function foo ( a , b ) {
// return a + b ;
// }
// const y = foo( 2 , 3 ) ;

// Formatted:
// const x = 1;
// function foo(a, b) {
// return a + b;
// }
// const y = foo(2, 3);
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// === formatOnType ===
// Original (/basicFormatOnType.ts):
// function foo() {
// const x=1;
// }

// Formatted:
// function foo() {
// const x=1;
// }
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// === formatSelection ===
// Original (/basicFormatSelection.ts):
// const x = 1;
// function foo(a,b){return a+b;}
// const y = 2;

// Formatted:
// const x = 1;
// function foo(a, b) { return a + b; }
// const y = 2;