Skip to content

Commit 6befa0b

Browse files
Add fourslash support for formatting requests
- Add VerifyFormatDocument() for textDocument/formatting - Add VerifyFormatSelection() for textDocument/rangeFormatting - Add VerifyFormatOnType() for textDocument/onTypeFormatting - Add baseline support for formatting results - Create example tests demonstrating the functionality Co-authored-by: DanielRosenwasser <[email protected]>
1 parent 01bc829 commit 6befa0b

File tree

8 files changed

+268
-0
lines changed

8 files changed

+268
-0
lines changed

internal/fourslash/baselineutil.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ func getBaselineExtension(command string) string {
5151
return "baseline"
5252
case "Auto Imports":
5353
return "baseline.md"
54+
case "formatDocument", "formatSelection", "formatOnType":
55+
return "baseline"
5456
case "findAllReferences", "goToDefinition", "findRenameLocations":
5557
return "baseline.jsonc"
5658
default:

internal/fourslash/fourslash.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,3 +2221,164 @@ func (f *FourslashTest) verifyBaselines(t *testing.T) {
22212221
}
22222222

22232223
var AnyTextEdits *[]*lsproto.TextEdit
2224+
2225+
// VerifyFormatDocument verifies formatting of the entire document.
2226+
// It sends a textDocument/formatting request and compares the result with a baseline.
2227+
func (f *FourslashTest) VerifyFormatDocument(t *testing.T, options *lsproto.FormattingOptions) {
2228+
if options == nil {
2229+
options = &lsproto.FormattingOptions{
2230+
TabSize: 4,
2231+
InsertSpaces: true,
2232+
}
2233+
}
2234+
2235+
params := &lsproto.DocumentFormattingParams{
2236+
TextDocument: lsproto.TextDocumentIdentifier{
2237+
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
2238+
},
2239+
Options: options,
2240+
}
2241+
2242+
resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentFormattingInfo, params)
2243+
if resMsg == nil {
2244+
t.Fatal("Nil response received for document formatting request")
2245+
}
2246+
if !resultOk {
2247+
t.Fatalf("Unexpected response type for document formatting request: %T", resMsg.AsResponse().Result)
2248+
}
2249+
2250+
f.addFormattingResultToBaseline(t, "formatDocument", result.TextEdits)
2251+
}
2252+
2253+
// VerifyFormatSelection verifies formatting of a selected range.
2254+
// It sends a textDocument/rangeFormatting request and compares the result with a baseline.
2255+
func (f *FourslashTest) VerifyFormatSelection(t *testing.T, markerOrRange MarkerOrRange, options *lsproto.FormattingOptions) {
2256+
if options == nil {
2257+
options = &lsproto.FormattingOptions{
2258+
TabSize: 4,
2259+
InsertSpaces: true,
2260+
}
2261+
}
2262+
2263+
f.goToMarker(t, markerOrRange)
2264+
var formatRange lsproto.Range
2265+
if rangeMarker, ok := markerOrRange.(*RangeMarker); ok {
2266+
formatRange = rangeMarker.LSRange
2267+
} else {
2268+
// If it's just a marker position, format from that position to the end of the line
2269+
formatRange = lsproto.Range{
2270+
Start: f.currentCaretPosition,
2271+
End: f.currentCaretPosition,
2272+
}
2273+
}
2274+
2275+
params := &lsproto.DocumentRangeFormattingParams{
2276+
TextDocument: lsproto.TextDocumentIdentifier{
2277+
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
2278+
},
2279+
Range: formatRange,
2280+
Options: options,
2281+
}
2282+
2283+
resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentRangeFormattingInfo, params)
2284+
if resMsg == nil {
2285+
t.Fatal("Nil response received for range formatting request")
2286+
}
2287+
if !resultOk {
2288+
t.Fatalf("Unexpected response type for range formatting request: %T", resMsg.AsResponse().Result)
2289+
}
2290+
2291+
f.addFormattingResultToBaseline(t, "formatSelection", result.TextEdits)
2292+
}
2293+
2294+
// VerifyFormatOnType verifies on-type formatting (e.g., after typing `;`, `}`, or newline).
2295+
// It sends a textDocument/onTypeFormatting request and compares the result with a baseline.
2296+
func (f *FourslashTest) VerifyFormatOnType(t *testing.T, marker string, character string, options *lsproto.FormattingOptions) {
2297+
if options == nil {
2298+
options = &lsproto.FormattingOptions{
2299+
TabSize: 4,
2300+
InsertSpaces: true,
2301+
}
2302+
}
2303+
2304+
f.GoToMarker(t, marker)
2305+
2306+
params := &lsproto.DocumentOnTypeFormattingParams{
2307+
TextDocument: lsproto.TextDocumentIdentifier{
2308+
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
2309+
},
2310+
Position: f.currentCaretPosition,
2311+
Ch: character,
2312+
Options: options,
2313+
}
2314+
2315+
resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentOnTypeFormattingInfo, params)
2316+
if resMsg == nil {
2317+
t.Fatal("Nil response received for on-type formatting request")
2318+
}
2319+
if !resultOk {
2320+
// Check if result is nil - this is valid, just means no formatting needed
2321+
resp := resMsg.AsResponse()
2322+
if resp.Result == nil {
2323+
// No formatting edits needed
2324+
f.addFormattingResultToBaseline(t, "formatOnType", nil)
2325+
return
2326+
}
2327+
t.Fatalf("Unexpected response type for on-type formatting request: %T", resp.Result)
2328+
}
2329+
2330+
f.addFormattingResultToBaseline(t, "formatOnType", result.TextEdits)
2331+
}
2332+
2333+
// addFormattingResultToBaseline adds formatting results to the baseline.
2334+
// It shows the original file content and the formatted content side by side.
2335+
func (f *FourslashTest) addFormattingResultToBaseline(t *testing.T, command string, edits *[]*lsproto.TextEdit) {
2336+
script := f.getScriptInfo(f.activeFilename)
2337+
originalContent := script.content
2338+
2339+
var formattedContent string
2340+
if edits == nil || len(*edits) == 0 {
2341+
formattedContent = originalContent
2342+
} else {
2343+
// Apply edits to get formatted content
2344+
formattedContent = f.applyEditsToString(originalContent, *edits)
2345+
}
2346+
2347+
var result strings.Builder
2348+
result.WriteString(fmt.Sprintf("// Original (%s):\n", f.activeFilename))
2349+
for _, line := range strings.Split(originalContent, "\n") {
2350+
result.WriteString("// " + line + "\n")
2351+
}
2352+
result.WriteString("\n")
2353+
result.WriteString("// Formatted:\n")
2354+
for _, line := range strings.Split(formattedContent, "\n") {
2355+
result.WriteString("// " + line + "\n")
2356+
}
2357+
2358+
f.addResultToBaseline(t, command, result.String())
2359+
}
2360+
2361+
// applyEditsToString applies text edits to a string and returns the result.
2362+
func (f *FourslashTest) applyEditsToString(content string, edits []*lsproto.TextEdit) string {
2363+
script := newScriptInfo(f.activeFilename, content)
2364+
converters := lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *lsconv.LSPLineMap {
2365+
return script.lineMap
2366+
})
2367+
2368+
// Sort edits in reverse order to avoid affecting positions
2369+
sortedEdits := slices.Clone(edits)
2370+
slices.SortFunc(sortedEdits, func(a, b *lsproto.TextEdit) int {
2371+
aStart := converters.LineAndCharacterToPosition(script, a.Range.Start)
2372+
bStart := converters.LineAndCharacterToPosition(script, b.Range.Start)
2373+
return int(bStart) - int(aStart)
2374+
})
2375+
2376+
result := content
2377+
for _, edit := range sortedEdits {
2378+
start := int(converters.LineAndCharacterToPosition(script, edit.Range.Start))
2379+
end := int(converters.LineAndCharacterToPosition(script, edit.Range.End))
2380+
result = result[:start] + edit.NewText + result[end:]
2381+
}
2382+
2383+
return result
2384+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package fourslash_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/fourslash"
7+
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
8+
"github.com/microsoft/typescript-go/internal/testutil"
9+
)
10+
11+
func TestBasicFormatDocument(t *testing.T) {
12+
t.Parallel()
13+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
14+
const content = `const x = 1 ;
15+
function foo ( a , b ) {
16+
return a + b ;
17+
}
18+
const y = foo( 2 , 3 ) ;`
19+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
20+
f.VerifyFormatDocument(t, &lsproto.FormattingOptions{
21+
TabSize: 4,
22+
InsertSpaces: true,
23+
})
24+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package fourslash_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/fourslash"
7+
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
8+
"github.com/microsoft/typescript-go/internal/testutil"
9+
)
10+
11+
func TestBasicFormatOnType(t *testing.T) {
12+
t.Parallel()
13+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
14+
const content = `function foo() {/*a*/
15+
const x=1;
16+
}`
17+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
18+
// Verify formatting after typing opening curly brace
19+
f.VerifyFormatOnType(t, "a", "{", &lsproto.FormattingOptions{
20+
TabSize: 4,
21+
InsertSpaces: true,
22+
})
23+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package fourslash_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/fourslash"
7+
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
8+
"github.com/microsoft/typescript-go/internal/testutil"
9+
)
10+
11+
func TestBasicFormatSelection(t *testing.T) {
12+
t.Parallel()
13+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
14+
const content = `const x = 1;
15+
[|function foo(a,b){return a+b;}|]
16+
const y = 2;`
17+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
18+
// Format only the function declaration
19+
rangeToFormat := f.Ranges()[0]
20+
f.VerifyFormatSelection(t, rangeToFormat, &lsproto.FormattingOptions{
21+
TabSize: 4,
22+
InsertSpaces: true,
23+
})
24+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// === formatDocument ===
2+
// Original (/basicFormatDocument.ts):
3+
// const x = 1 ;
4+
// function foo ( a , b ) {
5+
// return a + b ;
6+
// }
7+
// const y = foo( 2 , 3 ) ;
8+
9+
// Formatted:
10+
// const x = 1;
11+
// function foo(a, b) {
12+
// return a + b;
13+
// }
14+
// const y = foo(2, 3);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// === formatOnType ===
2+
// Original (/basicFormatOnType.ts):
3+
// function foo() {
4+
// const x=1;
5+
// }
6+
7+
// Formatted:
8+
// function foo() {
9+
// const x=1;
10+
// }
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// === formatSelection ===
2+
// Original (/basicFormatSelection.ts):
3+
// const x = 1;
4+
// function foo(a,b){return a+b;}
5+
// const y = 2;
6+
7+
// Formatted:
8+
// const x = 1;
9+
// function foo(a, b) { return a + b; }
10+
// const y = 2;

0 commit comments

Comments
 (0)