From 01bc829c167edd7a27d4e46dc4509484af574567 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:15:47 +0000 Subject: [PATCH 1/6] Initial plan From 6befa0b16f92f1ea3ad7eae08d7c2a89efdb32aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:36:09 +0000 Subject: [PATCH 2/6] 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 <972891+DanielRosenwasser@users.noreply.github.com> --- internal/fourslash/baselineutil.go | 2 + internal/fourslash/fourslash.go | 161 ++++++++++++++++++ .../tests/basicFormatDocument_test.go | 24 +++ .../fourslash/tests/basicFormatOnType_test.go | 23 +++ .../tests/basicFormatSelection_test.go | 24 +++ .../basicFormatDocument.baseline | 14 ++ .../formatOnType/basicFormatOnType.baseline | 10 ++ .../basicFormatSelection.baseline | 10 ++ 8 files changed, 268 insertions(+) create mode 100644 internal/fourslash/tests/basicFormatDocument_test.go create mode 100644 internal/fourslash/tests/basicFormatOnType_test.go create mode 100644 internal/fourslash/tests/basicFormatSelection_test.go create mode 100644 testdata/baselines/reference/fourslash/formatDocument/basicFormatDocument.baseline create mode 100644 testdata/baselines/reference/fourslash/formatOnType/basicFormatOnType.baseline create mode 100644 testdata/baselines/reference/fourslash/formatSelection/basicFormatSelection.baseline diff --git a/internal/fourslash/baselineutil.go b/internal/fourslash/baselineutil.go index b640ef071c..9aee1b1a7b 100644 --- a/internal/fourslash/baselineutil.go +++ b/internal/fourslash/baselineutil.go @@ -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: diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 13cfa1c2bc..218e188cc2 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -2221,3 +2221,164 @@ 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 { + t.Fatalf("Unexpected response type for document formatting request: %T", resMsg.AsResponse().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. +func (f *FourslashTest) VerifyFormatSelection(t *testing.T, markerOrRange MarkerOrRange, options *lsproto.FormattingOptions) { + if options == nil { + options = &lsproto.FormattingOptions{ + TabSize: 4, + InsertSpaces: true, + } + } + + f.goToMarker(t, markerOrRange) + var formatRange lsproto.Range + if rangeMarker, ok := markerOrRange.(*RangeMarker); ok { + formatRange = rangeMarker.LSRange + } else { + // If it's just a marker position, format from that position to the end of the line + formatRange = lsproto.Range{ + Start: f.currentCaretPosition, + End: f.currentCaretPosition, + } + } + + 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 { + t.Fatalf("Unexpected response type for range formatting request: %T", resMsg.AsResponse().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. +func (f *FourslashTest) VerifyFormatOnType(t *testing.T, marker string, character string, options *lsproto.FormattingOptions) { + if options == nil { + options = &lsproto.FormattingOptions{ + TabSize: 4, + InsertSpaces: true, + } + } + + f.GoToMarker(t, marker) + + 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 + sortedEdits := slices.Clone(edits) + slices.SortFunc(sortedEdits, func(a, b *lsproto.TextEdit) int { + aStart := converters.LineAndCharacterToPosition(script, a.Range.Start) + bStart := converters.LineAndCharacterToPosition(script, b.Range.Start) + return int(bStart) - int(aStart) + }) + + result := content + for _, edit := range sortedEdits { + start := int(converters.LineAndCharacterToPosition(script, edit.Range.Start)) + end := int(converters.LineAndCharacterToPosition(script, edit.Range.End)) + result = result[:start] + edit.NewText + result[end:] + } + + return result +} diff --git a/internal/fourslash/tests/basicFormatDocument_test.go b/internal/fourslash/tests/basicFormatDocument_test.go new file mode 100644 index 0000000000..dbfeb9ffc4 --- /dev/null +++ b/internal/fourslash/tests/basicFormatDocument_test.go @@ -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, + }) +} diff --git a/internal/fourslash/tests/basicFormatOnType_test.go b/internal/fourslash/tests/basicFormatOnType_test.go new file mode 100644 index 0000000000..5b86520be6 --- /dev/null +++ b/internal/fourslash/tests/basicFormatOnType_test.go @@ -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, + }) +} diff --git a/internal/fourslash/tests/basicFormatSelection_test.go b/internal/fourslash/tests/basicFormatSelection_test.go new file mode 100644 index 0000000000..f90189e85a --- /dev/null +++ b/internal/fourslash/tests/basicFormatSelection_test.go @@ -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, + }) +} diff --git a/testdata/baselines/reference/fourslash/formatDocument/basicFormatDocument.baseline b/testdata/baselines/reference/fourslash/formatDocument/basicFormatDocument.baseline new file mode 100644 index 0000000000..6435d8d7c9 --- /dev/null +++ b/testdata/baselines/reference/fourslash/formatDocument/basicFormatDocument.baseline @@ -0,0 +1,14 @@ +// === formatDocument === +// Original (/basicFormatDocument.ts): +// const x = 1 ; +// 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); diff --git a/testdata/baselines/reference/fourslash/formatOnType/basicFormatOnType.baseline b/testdata/baselines/reference/fourslash/formatOnType/basicFormatOnType.baseline new file mode 100644 index 0000000000..31eb477c3a --- /dev/null +++ b/testdata/baselines/reference/fourslash/formatOnType/basicFormatOnType.baseline @@ -0,0 +1,10 @@ +// === formatOnType === +// Original (/basicFormatOnType.ts): +// function foo() { +// const x=1; +// } + +// Formatted: +// function foo() { +// const x=1; +// } diff --git a/testdata/baselines/reference/fourslash/formatSelection/basicFormatSelection.baseline b/testdata/baselines/reference/fourslash/formatSelection/basicFormatSelection.baseline new file mode 100644 index 0000000000..5fbd46e63f --- /dev/null +++ b/testdata/baselines/reference/fourslash/formatSelection/basicFormatSelection.baseline @@ -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; From a77e23bb726b2bc184356cd38a77aa1f7e8dd253 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:40:52 +0000 Subject: [PATCH 3/6] Fix API consistency in formatting verification methods Make parameter types consistent across all three formatting methods: - VerifyFormatDocument() takes no marker parameter - VerifyFormatSelection() takes *RangeMarker (from f.Ranges()) - VerifyFormatOnType() takes string marker name Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- internal/fourslash/fourslash.go | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 218e188cc2..d8c03b32be 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -2252,7 +2252,8 @@ func (f *FourslashTest) VerifyFormatDocument(t *testing.T, options *lsproto.Form // VerifyFormatSelection verifies formatting of a selected range. // It sends a textDocument/rangeFormatting request and compares the result with a baseline. -func (f *FourslashTest) VerifyFormatSelection(t *testing.T, markerOrRange MarkerOrRange, options *lsproto.FormattingOptions) { +// 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, @@ -2260,17 +2261,8 @@ func (f *FourslashTest) VerifyFormatSelection(t *testing.T, markerOrRange Marker } } - f.goToMarker(t, markerOrRange) - var formatRange lsproto.Range - if rangeMarker, ok := markerOrRange.(*RangeMarker); ok { - formatRange = rangeMarker.LSRange - } else { - // If it's just a marker position, format from that position to the end of the line - formatRange = lsproto.Range{ - Start: f.currentCaretPosition, - End: f.currentCaretPosition, - } - } + f.goToMarker(t, rangeMarker) + formatRange := rangeMarker.LSRange params := &lsproto.DocumentRangeFormattingParams{ TextDocument: lsproto.TextDocumentIdentifier{ @@ -2293,7 +2285,8 @@ func (f *FourslashTest) VerifyFormatSelection(t *testing.T, markerOrRange Marker // VerifyFormatOnType verifies on-type formatting (e.g., after typing `;`, `}`, or newline). // It sends a textDocument/onTypeFormatting request and compares the result with a baseline. -func (f *FourslashTest) VerifyFormatOnType(t *testing.T, marker string, character string, options *lsproto.FormattingOptions) { +// 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, @@ -2301,7 +2294,7 @@ func (f *FourslashTest) VerifyFormatOnType(t *testing.T, marker string, characte } } - f.GoToMarker(t, marker) + f.GoToMarker(t, markerName) params := &lsproto.DocumentOnTypeFormattingParams{ TextDocument: lsproto.TextDocumentIdentifier{ From 84558588223d8548af43b13afa11e43bfd793e47 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:44:55 +0000 Subject: [PATCH 4/6] Address code review feedback - Remove unnecessary goToMarker call in VerifyFormatSelection - Add consistent null result handling to all three formatting methods - Optimize sort function by caching position conversions Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- internal/fourslash/fourslash.go | 45 ++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index d8c03b32be..afd7225fac 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -2244,7 +2244,13 @@ func (f *FourslashTest) VerifyFormatDocument(t *testing.T, options *lsproto.Form t.Fatal("Nil response received for document formatting request") } if !resultOk { - t.Fatalf("Unexpected response type for document formatting request: %T", resMsg.AsResponse().Result) + // 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) @@ -2261,7 +2267,7 @@ func (f *FourslashTest) VerifyFormatSelection(t *testing.T, rangeMarker *RangeMa } } - f.goToMarker(t, rangeMarker) + f.ensureActiveFile(t, rangeMarker.FileName()) formatRange := rangeMarker.LSRange params := &lsproto.DocumentRangeFormattingParams{ @@ -2277,7 +2283,13 @@ func (f *FourslashTest) VerifyFormatSelection(t *testing.T, rangeMarker *RangeMa t.Fatal("Nil response received for range formatting request") } if !resultOk { - t.Fatalf("Unexpected response type for range formatting request: %T", resMsg.AsResponse().Result) + // 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) @@ -2359,18 +2371,27 @@ func (f *FourslashTest) applyEditsToString(content string, edits []*lsproto.Text }) // Sort edits in reverse order to avoid affecting positions - sortedEdits := slices.Clone(edits) - slices.SortFunc(sortedEdits, func(a, b *lsproto.TextEdit) int { - aStart := converters.LineAndCharacterToPosition(script, a.Range.Start) - bStart := converters.LineAndCharacterToPosition(script, b.Range.Start) - return int(bStart) - int(aStart) + // 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 _, edit := range sortedEdits { - start := int(converters.LineAndCharacterToPosition(script, edit.Range.Start)) - end := int(converters.LineAndCharacterToPosition(script, edit.Range.End)) - result = result[:start] + edit.NewText + result[end:] + 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 From a0c8f66c0ee2220b02a7a1127d23f54672076d73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:41:21 +0000 Subject: [PATCH 5/6] 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> --- internal/fourslash/fourslash.go | 196 ++++++++++++++++++ .../formatDocumentChangesNothing_test.go | 17 ++ .../tests/formatDocumentInPlace_test.go | 22 ++ .../tests/formatOnTypeInPlace_test.go | 22 ++ .../tests/formatSelectionInPlace_test.go | 21 ++ 5 files changed, 278 insertions(+) create mode 100644 internal/fourslash/tests/formatDocumentChangesNothing_test.go create mode 100644 internal/fourslash/tests/formatDocumentInPlace_test.go create mode 100644 internal/fourslash/tests/formatOnTypeInPlace_test.go create mode 100644 internal/fourslash/tests/formatSelectionInPlace_test.go diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index afd7225fac..8db74e00f2 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -2396,3 +2396,199 @@ func (f *FourslashTest) applyEditsToString(content string, edits []*lsproto.Text return result } + +// FormatDocument applies formatting to the entire document and updates the file content. +// This method modifies the file in place, similar to how TypeScript's fourslash works. +func (f *FourslashTest) FormatDocument(t *testing.T) { + 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 { + resp := resMsg.AsResponse() + if resp.Result == nil { + // No formatting needed + return + } + t.Fatalf("Unexpected response type for document formatting request: %T", resp.Result) + } + + if result.TextEdits != nil && len(*result.TextEdits) > 0 { + f.applyTextEdits(t, *result.TextEdits) + } +} + +// FormatSelection applies formatting to the selection between two markers and updates the file content. +func (f *FourslashTest) FormatSelection(t *testing.T, startMarkerName string, endMarkerName string) { + startMarker, ok := f.testData.MarkerPositions[startMarkerName] + if !ok { + t.Fatalf("Start marker '%s' not found", startMarkerName) + } + endMarker, ok := f.testData.MarkerPositions[endMarkerName] + if !ok { + t.Fatalf("End marker '%s' not found", endMarkerName) + } + if startMarker.FileName() != endMarker.FileName() { + t.Fatalf("Markers '%s' and '%s' are in different files", startMarkerName, endMarkerName) + } + + f.ensureActiveFile(t, startMarker.FileName()) + + formatRange := lsproto.Range{ + Start: startMarker.LSPosition, + End: endMarker.LSPosition, + } + + options := &lsproto.FormattingOptions{ + TabSize: 4, + InsertSpaces: true, + } + + 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 { + resp := resMsg.AsResponse() + if resp.Result == nil { + // No formatting needed + return + } + t.Fatalf("Unexpected response type for range formatting request: %T", resp.Result) + } + + if result.TextEdits != nil && len(*result.TextEdits) > 0 { + f.applyTextEdits(t, *result.TextEdits) + } +} + +// FormatOnType applies on-type formatting at the specified marker position after typing the given character. +func (f *FourslashTest) FormatOnType(t *testing.T, markerName string, character string) { + f.GoToMarker(t, markerName) + + options := &lsproto.FormattingOptions{ + TabSize: 4, + InsertSpaces: true, + } + + 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 { + resp := resMsg.AsResponse() + if resp.Result == nil { + // No formatting needed + return + } + t.Fatalf("Unexpected response type for on-type formatting request: %T", resp.Result) + } + + if result.TextEdits != nil && len(*result.TextEdits) > 0 { + f.applyTextEdits(t, *result.TextEdits) + } +} + +// CurrentLineContentIs verifies that the current line (where the caret is positioned) has the expected content. +func (f *FourslashTest) CurrentLineContentIs(t *testing.T, expectedText string) { + script := f.getScriptInfo(f.activeFilename) + lineIndex := int(f.currentCaretPosition.Line) + + if lineIndex >= len(script.lineMap.LineStarts) { + t.Fatalf("Current line index %d is out of bounds (file has %d lines)", lineIndex, len(script.lineMap.LineStarts)) + } + + lineStart := int(script.lineMap.LineStarts[lineIndex]) + var lineEnd int + if lineIndex+1 < len(script.lineMap.LineStarts) { + lineEnd = int(script.lineMap.LineStarts[lineIndex+1]) + // Remove trailing newline characters + for lineEnd > lineStart && (script.content[lineEnd-1] == '\n' || script.content[lineEnd-1] == '\r') { + lineEnd-- + } + } else { + lineEnd = len(script.content) + } + + actualText := script.content[lineStart:lineEnd] + if actualText != expectedText { + t.Fatalf("Expected current line to be:\n %q\nbut got:\n %q", expectedText, actualText) + } +} + +// CurrentFileContentIs verifies that the active file has the expected content. +func (f *FourslashTest) CurrentFileContentIs(t *testing.T, expectedContent string) { + script := f.getScriptInfo(f.activeFilename) + actualContent := script.content + + if actualContent != expectedContent { + t.Fatalf("Expected file content to be:\n%s\n\nbut got:\n%s", expectedContent, actualContent) + } +} + +// FormatDocumentChangesNothing verifies that formatting the document produces no changes. +func (f *FourslashTest) FormatDocumentChangesNothing(t *testing.T) { + script := f.getScriptInfo(f.activeFilename) + originalContent := script.content + + 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 { + resp := resMsg.AsResponse() + if resp.Result == nil { + // No edits means no changes, which is what we expect + return + } + t.Fatalf("Unexpected response type for document formatting request: %T", resp.Result) + } + + if result.TextEdits != nil && len(*result.TextEdits) > 0 { + formattedContent := f.applyEditsToString(originalContent, *result.TextEdits) + if formattedContent != originalContent { + t.Fatalf("Expected formatting to produce no changes, but content changed:\nOriginal:\n%s\n\nFormatted:\n%s", originalContent, formattedContent) + } + } +} diff --git a/internal/fourslash/tests/formatDocumentChangesNothing_test.go b/internal/fourslash/tests/formatDocumentChangesNothing_test.go new file mode 100644 index 0000000000..fa6e82d416 --- /dev/null +++ b/internal/fourslash/tests/formatDocumentChangesNothing_test.go @@ -0,0 +1,17 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestFormatDocumentChangesNothing(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `const x = 1; +const y = 2;` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.FormatDocumentChangesNothing(t) +} diff --git a/internal/fourslash/tests/formatDocumentInPlace_test.go b/internal/fourslash/tests/formatDocumentInPlace_test.go new file mode 100644 index 0000000000..ba0af41719 --- /dev/null +++ b/internal/fourslash/tests/formatDocumentInPlace_test.go @@ -0,0 +1,22 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestFormatDocumentInPlace(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `/*1*/for (;;) { } +/*2*/for (var x;x<0;x++) { } +/*3*/for (var x ;x<0 ;x++) { }` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.FormatDocument(t) + // After formatting, verify the formatted content + f.CurrentFileContentIs(t, `for (; ;) { } +for (var x; x < 0; x++) { } +for (var x; x < 0; x++) { } `) +} diff --git a/internal/fourslash/tests/formatOnTypeInPlace_test.go b/internal/fourslash/tests/formatOnTypeInPlace_test.go new file mode 100644 index 0000000000..f671b22c55 --- /dev/null +++ b/internal/fourslash/tests/formatOnTypeInPlace_test.go @@ -0,0 +1,22 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestFormatOnTypeInPlace(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `if (foo) { + if (bar) {/**/} +}` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.FormatOnType(t, "", "{") + // On-type formatting may format the opening brace line + f.CurrentFileContentIs(t, `if (foo) { + if (bar) {} +}`) +} diff --git a/internal/fourslash/tests/formatSelectionInPlace_test.go b/internal/fourslash/tests/formatSelectionInPlace_test.go new file mode 100644 index 0000000000..f0e17138e4 --- /dev/null +++ b/internal/fourslash/tests/formatSelectionInPlace_test.go @@ -0,0 +1,21 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestFormatSelectionInPlace(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `const x = 1; +/*1*/function foo(a,b){return a+b;}/*2*/ +const y = 2;` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.FormatSelection(t, "1", "2") + f.CurrentFileContentIs(t, `const x = 1; +function foo(a, b) { return a + b; } +const y = 2;`) +} From b05216eb42b42199017d3cf6dfa78bdbba301a4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 00:14:52 +0000 Subject: [PATCH 6/6] Address PR feedback: remove unused markers and fix nil pointer crash - Remove unused markers (/*1*/, /*2*/, /*3*/) from formatDocumentInPlace_test.go - Apply fix from PR #1993 to prevent nil pointer crash in indent.go when firstListChild is nil - Add comment about known trailing space issue (#1997) The indent.go fix prevents crashes when formatting ranges where the list has no first child. Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- internal/format/indent.go | 7 +++++-- internal/fourslash/tests/formatDocumentInPlace_test.go | 7 ++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/internal/format/indent.go b/internal/format/indent.go index 2565666d2f..170873c2a4 100644 --- a/internal/format/indent.go +++ b/internal/format/indent.go @@ -66,8 +66,11 @@ func getIndentationForNodeWorker( // }, { itself contributes nothing. // prop: 1 L3 - The indentation of the second object literal is best understood by // }) looking at the relationship between the list and *first* list item. - listLine, _ := getStartLineAndCharacterForNode(firstListChild, sourceFile) - listIndentsChild := firstListChild != nil && listLine > containingListOrParentStartLine + var listIndentsChild bool + if firstListChild != nil { + listLine, _ := getStartLineAndCharacterForNode(firstListChild, sourceFile) + listIndentsChild = listLine > containingListOrParentStartLine + } actualIndentation := getActualIndentationForListItem(current, sourceFile, options, listIndentsChild) if actualIndentation != -1 { return actualIndentation + indentationDelta diff --git a/internal/fourslash/tests/formatDocumentInPlace_test.go b/internal/fourslash/tests/formatDocumentInPlace_test.go index ba0af41719..d8068696ba 100644 --- a/internal/fourslash/tests/formatDocumentInPlace_test.go +++ b/internal/fourslash/tests/formatDocumentInPlace_test.go @@ -10,12 +10,13 @@ import ( func TestFormatDocumentInPlace(t *testing.T) { t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") - const content = `/*1*/for (;;) { } -/*2*/for (var x;x<0;x++) { } -/*3*/for (var x ;x<0 ;x++) { }` + const content = `for (;;) { } +for (var x;x<0;x++) { } +for (var x ;x<0 ;x++) { }` f := fourslash.NewFourslash(t, nil /*capabilities*/, content) f.FormatDocument(t) // After formatting, verify the formatted content + // Note: trailing space on last line is a known issue (see https://github.com/microsoft/typescript-go/issues/1997) f.CurrentFileContentIs(t, `for (; ;) { } for (var x; x < 0; x++) { } for (var x; x < 0; x++) { } `)