Skip to content

Commit a0c8f66

Browse files
Add formatting methods that modify file content in-place
Implement requested formatting API that matches TypeScript fourslash: - FormatDocument() - formats entire document and updates content - FormatSelection(startMarker, endMarker) - formats selection between markers - FormatOnType(marker, character) - applies on-type formatting - CurrentLineContentIs(text) - verifies current line content - CurrentFileContentIs(text) - verifies entire file content - FormatDocumentChangesNothing() - verifies formatting produces no changes Added test cases demonstrating each method. Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com>
1 parent 8455858 commit a0c8f66

File tree

5 files changed

+278
-0
lines changed

5 files changed

+278
-0
lines changed

internal/fourslash/fourslash.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2396,3 +2396,199 @@ func (f *FourslashTest) applyEditsToString(content string, edits []*lsproto.Text
23962396

23972397
return result
23982398
}
2399+
2400+
// FormatDocument applies formatting to the entire document and updates the file content.
2401+
// This method modifies the file in place, similar to how TypeScript's fourslash works.
2402+
func (f *FourslashTest) FormatDocument(t *testing.T) {
2403+
options := &lsproto.FormattingOptions{
2404+
TabSize: 4,
2405+
InsertSpaces: true,
2406+
}
2407+
2408+
params := &lsproto.DocumentFormattingParams{
2409+
TextDocument: lsproto.TextDocumentIdentifier{
2410+
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
2411+
},
2412+
Options: options,
2413+
}
2414+
2415+
resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentFormattingInfo, params)
2416+
if resMsg == nil {
2417+
t.Fatal("Nil response received for document formatting request")
2418+
}
2419+
if !resultOk {
2420+
resp := resMsg.AsResponse()
2421+
if resp.Result == nil {
2422+
// No formatting needed
2423+
return
2424+
}
2425+
t.Fatalf("Unexpected response type for document formatting request: %T", resp.Result)
2426+
}
2427+
2428+
if result.TextEdits != nil && len(*result.TextEdits) > 0 {
2429+
f.applyTextEdits(t, *result.TextEdits)
2430+
}
2431+
}
2432+
2433+
// FormatSelection applies formatting to the selection between two markers and updates the file content.
2434+
func (f *FourslashTest) FormatSelection(t *testing.T, startMarkerName string, endMarkerName string) {
2435+
startMarker, ok := f.testData.MarkerPositions[startMarkerName]
2436+
if !ok {
2437+
t.Fatalf("Start marker '%s' not found", startMarkerName)
2438+
}
2439+
endMarker, ok := f.testData.MarkerPositions[endMarkerName]
2440+
if !ok {
2441+
t.Fatalf("End marker '%s' not found", endMarkerName)
2442+
}
2443+
if startMarker.FileName() != endMarker.FileName() {
2444+
t.Fatalf("Markers '%s' and '%s' are in different files", startMarkerName, endMarkerName)
2445+
}
2446+
2447+
f.ensureActiveFile(t, startMarker.FileName())
2448+
2449+
formatRange := lsproto.Range{
2450+
Start: startMarker.LSPosition,
2451+
End: endMarker.LSPosition,
2452+
}
2453+
2454+
options := &lsproto.FormattingOptions{
2455+
TabSize: 4,
2456+
InsertSpaces: true,
2457+
}
2458+
2459+
params := &lsproto.DocumentRangeFormattingParams{
2460+
TextDocument: lsproto.TextDocumentIdentifier{
2461+
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
2462+
},
2463+
Range: formatRange,
2464+
Options: options,
2465+
}
2466+
2467+
resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentRangeFormattingInfo, params)
2468+
if resMsg == nil {
2469+
t.Fatal("Nil response received for range formatting request")
2470+
}
2471+
if !resultOk {
2472+
resp := resMsg.AsResponse()
2473+
if resp.Result == nil {
2474+
// No formatting needed
2475+
return
2476+
}
2477+
t.Fatalf("Unexpected response type for range formatting request: %T", resp.Result)
2478+
}
2479+
2480+
if result.TextEdits != nil && len(*result.TextEdits) > 0 {
2481+
f.applyTextEdits(t, *result.TextEdits)
2482+
}
2483+
}
2484+
2485+
// FormatOnType applies on-type formatting at the specified marker position after typing the given character.
2486+
func (f *FourslashTest) FormatOnType(t *testing.T, markerName string, character string) {
2487+
f.GoToMarker(t, markerName)
2488+
2489+
options := &lsproto.FormattingOptions{
2490+
TabSize: 4,
2491+
InsertSpaces: true,
2492+
}
2493+
2494+
params := &lsproto.DocumentOnTypeFormattingParams{
2495+
TextDocument: lsproto.TextDocumentIdentifier{
2496+
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
2497+
},
2498+
Position: f.currentCaretPosition,
2499+
Ch: character,
2500+
Options: options,
2501+
}
2502+
2503+
resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentOnTypeFormattingInfo, params)
2504+
if resMsg == nil {
2505+
t.Fatal("Nil response received for on-type formatting request")
2506+
}
2507+
if !resultOk {
2508+
resp := resMsg.AsResponse()
2509+
if resp.Result == nil {
2510+
// No formatting needed
2511+
return
2512+
}
2513+
t.Fatalf("Unexpected response type for on-type formatting request: %T", resp.Result)
2514+
}
2515+
2516+
if result.TextEdits != nil && len(*result.TextEdits) > 0 {
2517+
f.applyTextEdits(t, *result.TextEdits)
2518+
}
2519+
}
2520+
2521+
// CurrentLineContentIs verifies that the current line (where the caret is positioned) has the expected content.
2522+
func (f *FourslashTest) CurrentLineContentIs(t *testing.T, expectedText string) {
2523+
script := f.getScriptInfo(f.activeFilename)
2524+
lineIndex := int(f.currentCaretPosition.Line)
2525+
2526+
if lineIndex >= len(script.lineMap.LineStarts) {
2527+
t.Fatalf("Current line index %d is out of bounds (file has %d lines)", lineIndex, len(script.lineMap.LineStarts))
2528+
}
2529+
2530+
lineStart := int(script.lineMap.LineStarts[lineIndex])
2531+
var lineEnd int
2532+
if lineIndex+1 < len(script.lineMap.LineStarts) {
2533+
lineEnd = int(script.lineMap.LineStarts[lineIndex+1])
2534+
// Remove trailing newline characters
2535+
for lineEnd > lineStart && (script.content[lineEnd-1] == '\n' || script.content[lineEnd-1] == '\r') {
2536+
lineEnd--
2537+
}
2538+
} else {
2539+
lineEnd = len(script.content)
2540+
}
2541+
2542+
actualText := script.content[lineStart:lineEnd]
2543+
if actualText != expectedText {
2544+
t.Fatalf("Expected current line to be:\n %q\nbut got:\n %q", expectedText, actualText)
2545+
}
2546+
}
2547+
2548+
// CurrentFileContentIs verifies that the active file has the expected content.
2549+
func (f *FourslashTest) CurrentFileContentIs(t *testing.T, expectedContent string) {
2550+
script := f.getScriptInfo(f.activeFilename)
2551+
actualContent := script.content
2552+
2553+
if actualContent != expectedContent {
2554+
t.Fatalf("Expected file content to be:\n%s\n\nbut got:\n%s", expectedContent, actualContent)
2555+
}
2556+
}
2557+
2558+
// FormatDocumentChangesNothing verifies that formatting the document produces no changes.
2559+
func (f *FourslashTest) FormatDocumentChangesNothing(t *testing.T) {
2560+
script := f.getScriptInfo(f.activeFilename)
2561+
originalContent := script.content
2562+
2563+
options := &lsproto.FormattingOptions{
2564+
TabSize: 4,
2565+
InsertSpaces: true,
2566+
}
2567+
2568+
params := &lsproto.DocumentFormattingParams{
2569+
TextDocument: lsproto.TextDocumentIdentifier{
2570+
Uri: lsconv.FileNameToDocumentURI(f.activeFilename),
2571+
},
2572+
Options: options,
2573+
}
2574+
2575+
resMsg, result, resultOk := sendRequest(t, f, lsproto.TextDocumentFormattingInfo, params)
2576+
if resMsg == nil {
2577+
t.Fatal("Nil response received for document formatting request")
2578+
}
2579+
if !resultOk {
2580+
resp := resMsg.AsResponse()
2581+
if resp.Result == nil {
2582+
// No edits means no changes, which is what we expect
2583+
return
2584+
}
2585+
t.Fatalf("Unexpected response type for document formatting request: %T", resp.Result)
2586+
}
2587+
2588+
if result.TextEdits != nil && len(*result.TextEdits) > 0 {
2589+
formattedContent := f.applyEditsToString(originalContent, *result.TextEdits)
2590+
if formattedContent != originalContent {
2591+
t.Fatalf("Expected formatting to produce no changes, but content changed:\nOriginal:\n%s\n\nFormatted:\n%s", originalContent, formattedContent)
2592+
}
2593+
}
2594+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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/testutil"
8+
)
9+
10+
func TestFormatDocumentChangesNothing(t *testing.T) {
11+
t.Parallel()
12+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
13+
const content = `const x = 1;
14+
const y = 2;`
15+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
16+
f.FormatDocumentChangesNothing(t)
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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/testutil"
8+
)
9+
10+
func TestFormatDocumentInPlace(t *testing.T) {
11+
t.Parallel()
12+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
13+
const content = `/*1*/for (;;) { }
14+
/*2*/for (var x;x<0;x++) { }
15+
/*3*/for (var x ;x<0 ;x++) { }`
16+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
17+
f.FormatDocument(t)
18+
// After formatting, verify the formatted content
19+
f.CurrentFileContentIs(t, `for (; ;) { }
20+
for (var x; x < 0; x++) { }
21+
for (var x; x < 0; x++) { } `)
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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/testutil"
8+
)
9+
10+
func TestFormatOnTypeInPlace(t *testing.T) {
11+
t.Parallel()
12+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
13+
const content = `if (foo) {
14+
if (bar) {/**/}
15+
}`
16+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
17+
f.FormatOnType(t, "", "{")
18+
// On-type formatting may format the opening brace line
19+
f.CurrentFileContentIs(t, `if (foo) {
20+
if (bar) {}
21+
}`)
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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/testutil"
8+
)
9+
10+
func TestFormatSelectionInPlace(t *testing.T) {
11+
t.Parallel()
12+
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
13+
const content = `const x = 1;
14+
/*1*/function foo(a,b){return a+b;}/*2*/
15+
const y = 2;`
16+
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
17+
f.FormatSelection(t, "1", "2")
18+
f.CurrentFileContentIs(t, `const x = 1;
19+
function foo(a, b) { return a + b; }
20+
const y = 2;`)
21+
}

0 commit comments

Comments
 (0)