From 8565f67b05736655fb8a6404a79af1ebb3498946 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:50:11 +0000 Subject: [PATCH 1/6] Initial plan From ad66098df560ad0c8934a28e2dd2474e82d96074 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:06:00 +0000 Subject: [PATCH 2/6] Add test for whitespace between comments formatting bug Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/80928e9e-fba6-45da-90d9-0475a8456707 Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- internal/format/bug_test.go | 60 +++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 internal/format/bug_test.go diff --git a/internal/format/bug_test.go b/internal/format/bug_test.go new file mode 100644 index 00000000000..4047462d130 --- /dev/null +++ b/internal/format/bug_test.go @@ -0,0 +1,60 @@ +package format_test + +import ( +"testing" + +"github.com/microsoft/typescript-go/internal/ast" +"github.com/microsoft/typescript-go/internal/core" +"github.com/microsoft/typescript-go/internal/format" +"github.com/microsoft/typescript-go/internal/ls/lsutil" +"github.com/microsoft/typescript-go/internal/parser" +) + +func TestFormatCompare(t *testing.T) { +// Test with and without the whitespace between comments to see the diff +textA := " const x = \"hello\"\n//\n\n//\n" // no whitespace between comments (empty line) +textB := " const x = \"hello\"\n//\n \n//\n" // whitespace between comments (space on line 3) + +ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{ +EditorSettings: lsutil.EditorSettings{ +TabSize: 4, +IndentSize: 4, +NewLineCharacter: "\n", +ConvertTabsToSpaces: core.TSTrue, +IndentStyle: lsutil.IndentStyleSmart, +TrimTrailingWhitespace: core.TSTrue, +}, +}, "\n") + +t.Run("without whitespace (works)", func(t *testing.T) { +sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ +FileName: "/test.ts", +Path: "/test.ts", +}, textA, core.ScriptKindTS) +edits := format.FormatDocument(ctx, sourceFile) +t.Logf("Text: %q", textA) +for i, e := range edits { +t.Logf("Edit %d: pos=%d end=%d newText=%q [%q]", i, e.Pos(), e.End(), e.NewText, textA[e.Pos():e.End()]) +} +result := applyBulkEdits(textA, edits) +t.Logf("Result: %q", result) +}) + +t.Run("with whitespace (breaks)", func(t *testing.T) { +sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ +FileName: "/test.ts", +Path: "/test.ts", +}, textB, core.ScriptKindTS) +edits := format.FormatDocument(ctx, sourceFile) +t.Logf("Text: %q", textB) +for i, e := range edits { +t.Logf("Edit %d: pos=%d end=%d newText=%q [%q]", i, e.Pos(), e.End(), e.NewText, textB[e.Pos():e.End()]) +} +// Check for overlapping/duplicate edits +for i := 1; i < len(edits); i++ { +if edits[i].Pos() < edits[i-1].End() { +t.Errorf("Overlapping edits: edit %d [%d,%d) overlaps with edit %d [%d,%d)", i-1, edits[i-1].Pos(), edits[i-1].End(), i, edits[i].Pos(), edits[i].End()) +} +} +}) +} From e1829d8a5be5ed76ffbd1adfcd94280274bf7523 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:15:09 +0000 Subject: [PATCH 3/6] Fix duplicate edits in trimTrailingWhitespacesForRemainingRange When formatting a file with whitespace-only lines between single-line comments, trimTrailingWhitespacesForRemainingRange could move startPos backwards past already-processed ranges, causing duplicate delete edits for the same whitespace. This resulted in overlapping edits that caused the entire format operation to fail silently in VS Code. Fix: only advance startPos when the comment trivia end is beyond the current startPos, preventing backwards movement. Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/80928e9e-fba6-45da-90d9-0475a8456707 Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- internal/format/bug_test.go | 87 ++++++++----------- internal/format/span.go | 4 +- .../formatWhitespaceBetweenComments.js | 14 +++ .../formatWhitespaceBetweenComments.symbols | 10 +++ .../formatWhitespaceBetweenComments.types | 11 +++ .../formatWhitespaceBetweenComments.ts | 6 ++ 6 files changed, 80 insertions(+), 52 deletions(-) create mode 100644 testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.js create mode 100644 testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.symbols create mode 100644 testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.types create mode 100644 testdata/tests/cases/compiler/formatWhitespaceBetweenComments.ts diff --git a/internal/format/bug_test.go b/internal/format/bug_test.go index 4047462d130..6e03b26f16c 100644 --- a/internal/format/bug_test.go +++ b/internal/format/bug_test.go @@ -1,60 +1,45 @@ package format_test import ( -"testing" + "testing" -"github.com/microsoft/typescript-go/internal/ast" -"github.com/microsoft/typescript-go/internal/core" -"github.com/microsoft/typescript-go/internal/format" -"github.com/microsoft/typescript-go/internal/ls/lsutil" -"github.com/microsoft/typescript-go/internal/parser" + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/format" + "github.com/microsoft/typescript-go/internal/ls/lsutil" + "github.com/microsoft/typescript-go/internal/parser" + "gotest.tools/v3/assert" ) -func TestFormatCompare(t *testing.T) { -// Test with and without the whitespace between comments to see the diff -textA := " const x = \"hello\"\n//\n\n//\n" // no whitespace between comments (empty line) -textB := " const x = \"hello\"\n//\n \n//\n" // whitespace between comments (space on line 3) +func TestFormatWhitespaceBetweenComments(t *testing.T) { + t.Parallel() + // Regression test: whitespace-only line between two single-line comments + // should not cause overlapping/duplicate edits that break formatting. + text := " const x = \"wont format\"\n//\n \n//\n" + ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{ + EditorSettings: lsutil.EditorSettings{ + TabSize: 4, + IndentSize: 4, + NewLineCharacter: "\n", + ConvertTabsToSpaces: core.TSTrue, + IndentStyle: lsutil.IndentStyleSmart, + TrimTrailingWhitespace: core.TSTrue, + }, + }, "\n") + sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: "/test.ts", + Path: "/test.ts", + }, text, core.ScriptKindTS) + edits := format.FormatDocument(ctx, sourceFile) -ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{ -EditorSettings: lsutil.EditorSettings{ -TabSize: 4, -IndentSize: 4, -NewLineCharacter: "\n", -ConvertTabsToSpaces: core.TSTrue, -IndentStyle: lsutil.IndentStyleSmart, -TrimTrailingWhitespace: core.TSTrue, -}, -}, "\n") + // Verify no overlapping or duplicate edits + for i := 1; i < len(edits); i++ { + assert.Assert(t, edits[i].Pos() >= edits[i-1].End(), + "Overlapping edits: edit %d [%d,%d) overlaps with edit %d [%d,%d)", + i-1, edits[i-1].Pos(), edits[i-1].End(), i, edits[i].Pos(), edits[i].End()) + } -t.Run("without whitespace (works)", func(t *testing.T) { -sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ -FileName: "/test.ts", -Path: "/test.ts", -}, textA, core.ScriptKindTS) -edits := format.FormatDocument(ctx, sourceFile) -t.Logf("Text: %q", textA) -for i, e := range edits { -t.Logf("Edit %d: pos=%d end=%d newText=%q [%q]", i, e.Pos(), e.End(), e.NewText, textA[e.Pos():e.End()]) -} -result := applyBulkEdits(textA, edits) -t.Logf("Result: %q", result) -}) - -t.Run("with whitespace (breaks)", func(t *testing.T) { -sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ -FileName: "/test.ts", -Path: "/test.ts", -}, textB, core.ScriptKindTS) -edits := format.FormatDocument(ctx, sourceFile) -t.Logf("Text: %q", textB) -for i, e := range edits { -t.Logf("Edit %d: pos=%d end=%d newText=%q [%q]", i, e.Pos(), e.End(), e.NewText, textB[e.Pos():e.End()]) -} -// Check for overlapping/duplicate edits -for i := 1; i < len(edits); i++ { -if edits[i].Pos() < edits[i-1].End() { -t.Errorf("Overlapping edits: edit %d [%d,%d) overlaps with edit %d [%d,%d)", i-1, edits[i-1].Pos(), edits[i-1].End(), i, edits[i].Pos(), edits[i].End()) -} -} -}) + // Verify edits can be applied without error + result := applyBulkEdits(text, edits) + assert.Assert(t, len(result) > 0) } diff --git a/internal/format/span.go b/internal/format/span.go index 1f448da00f8..052dc17e3d5 100644 --- a/internal/format/span.go +++ b/internal/format/span.go @@ -795,7 +795,9 @@ func (w *formatSpanWorker) trimTrailingWhitespacesForRemainingRange(trivias []Te w.trimTrailingWitespacesForPositions(startPos, trivia.Loc.Pos()-1, w.previousRange) } - startPos = trivia.Loc.End() + 1 + if trivia.Loc.End()+1 > startPos { + startPos = trivia.Loc.End() + 1 + } } } diff --git a/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.js b/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.js new file mode 100644 index 00000000000..cceb5717c66 --- /dev/null +++ b/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.js @@ -0,0 +1,14 @@ +//// [tests/cases/compiler/formatWhitespaceBetweenComments.ts] //// + +//// [formatWhitespaceBetweenComments.ts] + const x = "wont format" +// + +// + + +//// [formatWhitespaceBetweenComments.js] +"use strict"; +const x = "wont format"; +// +// diff --git a/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.symbols b/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.symbols new file mode 100644 index 00000000000..fca2c2315c0 --- /dev/null +++ b/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.symbols @@ -0,0 +1,10 @@ +//// [tests/cases/compiler/formatWhitespaceBetweenComments.ts] //// + +=== formatWhitespaceBetweenComments.ts === + const x = "wont format" +>x : Symbol(x, Decl(formatWhitespaceBetweenComments.ts, 0, 7)) + +// + +// + diff --git a/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.types b/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.types new file mode 100644 index 00000000000..774de070439 --- /dev/null +++ b/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.types @@ -0,0 +1,11 @@ +//// [tests/cases/compiler/formatWhitespaceBetweenComments.ts] //// + +=== formatWhitespaceBetweenComments.ts === + const x = "wont format" +>x : "wont format" +>"wont format" : "wont format" + +// + +// + diff --git a/testdata/tests/cases/compiler/formatWhitespaceBetweenComments.ts b/testdata/tests/cases/compiler/formatWhitespaceBetweenComments.ts new file mode 100644 index 00000000000..74d616d874f --- /dev/null +++ b/testdata/tests/cases/compiler/formatWhitespaceBetweenComments.ts @@ -0,0 +1,6 @@ +// @target: esnext + + const x = "wont format" +// + +// From 22e4165acee1cc862662e3e090e4e81e00c90eb9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:37:01 +0000 Subject: [PATCH 4/6] Replace compiler test with fourslash formatting test Remove the compiler test case (formatWhitespaceBetweenComments.ts and its baselines) and add a proper fourslash test that exercises FormatDocument and verifies the formatted output matches expectations. Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/a662c301-4eb8-4263-a4e2-828f2a2c87b5 Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- .../formatWhitespaceBetweenComments_test.go | 26 +++++++++++++++++++ .../formatWhitespaceBetweenComments.js | 14 ---------- .../formatWhitespaceBetweenComments.symbols | 10 ------- .../formatWhitespaceBetweenComments.types | 11 -------- .../formatWhitespaceBetweenComments.ts | 6 ----- 5 files changed, 26 insertions(+), 41 deletions(-) create mode 100644 internal/fourslash/tests/formatWhitespaceBetweenComments_test.go delete mode 100644 testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.js delete mode 100644 testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.symbols delete mode 100644 testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.types delete mode 100644 testdata/tests/cases/compiler/formatWhitespaceBetweenComments.ts diff --git a/internal/fourslash/tests/formatWhitespaceBetweenComments_test.go b/internal/fourslash/tests/formatWhitespaceBetweenComments_test.go new file mode 100644 index 00000000000..40192bdc8ab --- /dev/null +++ b/internal/fourslash/tests/formatWhitespaceBetweenComments_test.go @@ -0,0 +1,26 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestFormatWhitespaceBetweenComments(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = ` const x = "wont format" +// + +// +` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.FormatDocument(t, "") + f.VerifyCurrentFileContent(t, `const x = "wont format" +// + +// +`) +} diff --git a/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.js b/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.js deleted file mode 100644 index cceb5717c66..00000000000 --- a/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.js +++ /dev/null @@ -1,14 +0,0 @@ -//// [tests/cases/compiler/formatWhitespaceBetweenComments.ts] //// - -//// [formatWhitespaceBetweenComments.ts] - const x = "wont format" -// - -// - - -//// [formatWhitespaceBetweenComments.js] -"use strict"; -const x = "wont format"; -// -// diff --git a/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.symbols b/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.symbols deleted file mode 100644 index fca2c2315c0..00000000000 --- a/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.symbols +++ /dev/null @@ -1,10 +0,0 @@ -//// [tests/cases/compiler/formatWhitespaceBetweenComments.ts] //// - -=== formatWhitespaceBetweenComments.ts === - const x = "wont format" ->x : Symbol(x, Decl(formatWhitespaceBetweenComments.ts, 0, 7)) - -// - -// - diff --git a/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.types b/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.types deleted file mode 100644 index 774de070439..00000000000 --- a/testdata/baselines/reference/compiler/formatWhitespaceBetweenComments.types +++ /dev/null @@ -1,11 +0,0 @@ -//// [tests/cases/compiler/formatWhitespaceBetweenComments.ts] //// - -=== formatWhitespaceBetweenComments.ts === - const x = "wont format" ->x : "wont format" ->"wont format" : "wont format" - -// - -// - diff --git a/testdata/tests/cases/compiler/formatWhitespaceBetweenComments.ts b/testdata/tests/cases/compiler/formatWhitespaceBetweenComments.ts deleted file mode 100644 index 74d616d874f..00000000000 --- a/testdata/tests/cases/compiler/formatWhitespaceBetweenComments.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @target: esnext - - const x = "wont format" -// - -// From 3a62c9b84dc71c095aabb8f27a06b8e674d1369a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:34:53 +0000 Subject: [PATCH 5/6] Delete internal/format/bug_test.go per review feedback Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/8e74b134-0335-4eb7-8609-f1f715994413 Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- internal/format/bug_test.go | 45 ------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 internal/format/bug_test.go diff --git a/internal/format/bug_test.go b/internal/format/bug_test.go deleted file mode 100644 index 6e03b26f16c..00000000000 --- a/internal/format/bug_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package format_test - -import ( - "testing" - - "github.com/microsoft/typescript-go/internal/ast" - "github.com/microsoft/typescript-go/internal/core" - "github.com/microsoft/typescript-go/internal/format" - "github.com/microsoft/typescript-go/internal/ls/lsutil" - "github.com/microsoft/typescript-go/internal/parser" - "gotest.tools/v3/assert" -) - -func TestFormatWhitespaceBetweenComments(t *testing.T) { - t.Parallel() - // Regression test: whitespace-only line between two single-line comments - // should not cause overlapping/duplicate edits that break formatting. - text := " const x = \"wont format\"\n//\n \n//\n" - ctx := format.WithFormatCodeSettings(t.Context(), lsutil.FormatCodeSettings{ - EditorSettings: lsutil.EditorSettings{ - TabSize: 4, - IndentSize: 4, - NewLineCharacter: "\n", - ConvertTabsToSpaces: core.TSTrue, - IndentStyle: lsutil.IndentStyleSmart, - TrimTrailingWhitespace: core.TSTrue, - }, - }, "\n") - sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ - FileName: "/test.ts", - Path: "/test.ts", - }, text, core.ScriptKindTS) - edits := format.FormatDocument(ctx, sourceFile) - - // Verify no overlapping or duplicate edits - for i := 1; i < len(edits); i++ { - assert.Assert(t, edits[i].Pos() >= edits[i-1].End(), - "Overlapping edits: edit %d [%d,%d) overlaps with edit %d [%d,%d)", - i-1, edits[i-1].Pos(), edits[i-1].End(), i, edits[i].Pos(), edits[i].End()) - } - - // Verify edits can be applied without error - result := applyBulkEdits(text, edits) - assert.Assert(t, len(result) > 0) -} From d90bcc77196e67acc52cbec39ae68eaabdf88bdb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 20:37:26 +0000 Subject: [PATCH 6/6] Add TrimTrailingWhitespace variation test and fix content start position nit Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/82da20a1-b0ae-4365-815b-11b0bcb9cb36 Co-authored-by: DanielRosenwasser <972891+DanielRosenwasser@users.noreply.github.com> --- .../formatWhitespaceBetweenComments_test.go | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/internal/fourslash/tests/formatWhitespaceBetweenComments_test.go b/internal/fourslash/tests/formatWhitespaceBetweenComments_test.go index 40192bdc8ab..6f5f9a7bf3b 100644 --- a/internal/fourslash/tests/formatWhitespaceBetweenComments_test.go +++ b/internal/fourslash/tests/formatWhitespaceBetweenComments_test.go @@ -3,6 +3,7 @@ package fourslash_test import ( "testing" + "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/fourslash" "github.com/microsoft/typescript-go/internal/testutil" ) @@ -10,7 +11,8 @@ import ( func TestFormatWhitespaceBetweenComments(t *testing.T) { t.Parallel() defer testutil.RecoverAndFail(t, "Panic on fourslash test") - const content = ` const x = "wont format" + const content = ` + const x = "wont format" // // @@ -18,9 +20,33 @@ func TestFormatWhitespaceBetweenComments(t *testing.T) { f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() f.FormatDocument(t, "") - f.VerifyCurrentFileContent(t, `const x = "wont format" + f.VerifyCurrentFileContent(t, ` +const x = "wont format" // // `) } + +func TestFormatWhitespaceBetweenCommentsPreserveTrailingWhitespace(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = ` + const x = "wont format" +// + +// +` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + opts := f.GetOptions() + opts.FormatCodeSettings.TrimTrailingWhitespace = core.TSFalse + f.Configure(t, opts) + f.FormatDocument(t, "") + f.VerifyCurrentFileContent(t, ` +const x = "wont format" +// + +// +`) +}