diff --git a/internal/fourslash/_scripts/convertFourslash.mts b/internal/fourslash/_scripts/convertFourslash.mts index 61b789df13..767c13b807 100644 --- a/internal/fourslash/_scripts/convertFourslash.mts +++ b/internal/fourslash/_scripts/convertFourslash.mts @@ -234,6 +234,9 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined { return [{ kind: "verifyBaselineDiagnostics" }]; case "navigateTo": return parseVerifyNavigateTo(callExpression.arguments); + case "outliningSpansInCurrentFile": + case "outliningHintSpansInCurrentFile": + return parseOutliningSpansArgs(callExpression.arguments); } } // `goTo....` @@ -2036,6 +2039,46 @@ function parseBaselineCallHierarchy(args: ts.NodeArray): Cmd { }; } +function parseOutliningSpansArgs(args: readonly ts.Expression[]): [VerifyOutliningSpansCmd] | undefined { + if (args.length === 0) { + console.error("Expected at least one argument in verify.outliningSpansInCurrentFile"); + return undefined; + } + + let spans: string = ""; + // Optional second argument for kind filter + let foldingRangeKind: string | undefined; + if (args.length > 1) { + const kindArg = getStringLiteralLike(args[1]); + if (!kindArg) { + console.error(`Expected string literal for outlining kind, got ${args[1].getText()}`); + return undefined; + } + switch (kindArg.text) { + case "comment": + foldingRangeKind = "lsproto.FoldingRangeKindComment"; + break; + case "region": + foldingRangeKind = "lsproto.FoldingRangeKindRegion"; + break; + case "imports": + foldingRangeKind = "lsproto.FoldingRangeKindImports"; + break; + case "code": + break; + default: + console.error(`Unknown folding range kind: ${kindArg.text}`); + return undefined; + } + } + + return [{ + kind: "verifyOutliningSpans", + spans, + foldingRangeKind, + }]; +} + function parseKind(expr: ts.Expression): string | undefined { if (!ts.isStringLiteral(expr)) { console.error(`Expected string literal for kind, got ${expr.getText()}`); @@ -2513,6 +2556,12 @@ interface VerifyNoSignatureHelpForTriggerReasonCmd { markers: string[]; } +interface VerifyOutliningSpansCmd { + kind: "verifyOutliningSpans"; + spans: string; + foldingRangeKind?: string; +} + type Cmd = | VerifyCompletionsCmd | VerifyApplyCodeActionFromCompletionCmd @@ -2536,7 +2585,15 @@ type Cmd = | VerifyBaselineInlayHintsCmd | VerifyImportFixAtPositionCmd | VerifyDiagnosticsCmd - | VerifyBaselineDiagnosticsCmd; + | VerifyBaselineDiagnosticsCmd + | VerifyOutliningSpansCmd; + +function generateVerifyOutliningSpans({ foldingRangeKind }: VerifyOutliningSpansCmd): string { + if (foldingRangeKind) { + return `f.VerifyOutliningSpans(t, ${foldingRangeKind})`; + } + return `f.VerifyOutliningSpans(t)`; +} function generateVerifyCompletions({ marker, args, isNewIdentifierLocation, andApplyCodeActionArgs }: VerifyCompletionsCmd): string { let expectedList: string; @@ -2830,6 +2887,8 @@ function generateCmd(cmd: Cmd): string { return generateSignatureHelpPresent(cmd); case "verifyNoSignatureHelpForTriggerReason": return generateNoSignatureHelpForTriggerReason(cmd); + case "verifyOutliningSpans": + return generateVerifyOutliningSpans(cmd); default: let neverCommand: never = cmd; throw new Error(`Unknown command kind: ${neverCommand as Cmd["kind"]}`); diff --git a/internal/fourslash/_scripts/manualTests.txt b/internal/fourslash/_scripts/manualTests.txt index 970916f313..c86bec5d29 100644 --- a/internal/fourslash/_scripts/manualTests.txt +++ b/internal/fourslash/_scripts/manualTests.txt @@ -15,3 +15,7 @@ renameForDefaultExport01 tsxCompletion12 jsDocFunctionSignatures2 jsDocFunctionSignatures12 +outliningHintSpansForFunction +getOutliningSpans +outliningForNonCompleteInterfaceDeclaration +incrementalParsingWithJsDoc \ No newline at end of file diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 0f4afc05f7..b8754a2332 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -1521,6 +1521,55 @@ func (f *FourslashTest) VerifyBaselineWorkspaceSymbol(t *testing.T, query string )) } +func (f *FourslashTest) VerifyOutliningSpans(t *testing.T, foldingRangeKind ...lsproto.FoldingRangeKind) { + params := &lsproto.FoldingRangeParams{ + TextDocument: lsproto.TextDocumentIdentifier{ + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + }, + } + result := sendRequest(t, f, lsproto.TextDocumentFoldingRangeInfo, params) + if result.FoldingRanges == nil { + t.Fatalf("Nil response received for folding range request") + } + + // Extract actual folding ranges from the result and filter by kind if specified + var actualRanges []*lsproto.FoldingRange + actualRanges = *result.FoldingRanges + if len(foldingRangeKind) > 0 { + targetKind := foldingRangeKind[0] + var filtered []*lsproto.FoldingRange + for _, r := range actualRanges { + if r.Kind != nil && *r.Kind == targetKind { + filtered = append(filtered, r) + } + } + actualRanges = filtered + } + + if len(actualRanges) != len(f.Ranges()) { + t.Fatalf("verifyOutliningSpans failed - expected total spans to be %d, but was %d", + len(f.Ranges()), len(actualRanges)) + } + + slices.SortFunc(f.Ranges(), func(a, b *RangeMarker) int { + return lsproto.ComparePositions(a.LSPos(), b.LSPos()) + }) + + for i, expectedRange := range f.Ranges() { + actualRange := actualRanges[i] + startPos := lsproto.Position{Line: actualRange.StartLine, Character: *actualRange.StartCharacter} + endPos := lsproto.Position{Line: actualRange.EndLine, Character: *actualRange.EndCharacter} + + if lsproto.ComparePositions(startPos, expectedRange.LSRange.Start) != 0 || + lsproto.ComparePositions(endPos, expectedRange.LSRange.End) != 0 { + t.Fatalf("verifyOutliningSpans failed - span %d has invalid positions:\n actual: start (%d,%d), end (%d,%d)\n expected: start (%d,%d), end (%d,%d)", + i+1, + actualRange.StartLine, *actualRange.StartCharacter, actualRange.EndLine, *actualRange.EndCharacter, + expectedRange.LSRange.Start.Line, expectedRange.LSRange.Start.Character, expectedRange.LSRange.End.Line, expectedRange.LSRange.End.Character) + } + } +} + func (f *FourslashTest) VerifyBaselineHover(t *testing.T) { markersAndItems := core.MapFiltered(f.Markers(), func(marker *Marker) (markerAndItem[*lsproto.Hover], bool) { if marker.Name == nil { diff --git a/internal/fourslash/tests/gen/correuptedTryExpressionsDontCrashGettingOutlineSpans_test.go b/internal/fourslash/tests/gen/correuptedTryExpressionsDontCrashGettingOutlineSpans_test.go new file mode 100644 index 0000000000..80921fdca2 --- /dev/null +++ b/internal/fourslash/tests/gen/correuptedTryExpressionsDontCrashGettingOutlineSpans_test.go @@ -0,0 +1,23 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestCorreuptedTryExpressionsDontCrashGettingOutlineSpans(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `try[| { + var x = [ + {% try[||] %}|][|{% except %}|] + ] +} catch (e)[| { + +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForArrayDestructuring_test.go b/internal/fourslash/tests/gen/getOutliningForArrayDestructuring_test.go new file mode 100644 index 0000000000..548b7778da --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForArrayDestructuring_test.go @@ -0,0 +1,56 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForArrayDestructuring(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `const[| [ + a, + b, + c +]|] =[| [ + 1, + 2, + 3 +]|]; +const[| [ + [|[ + [|[ + [|[ + a, + b, + c + ]|] + ]|] + ]|], + [|[ + a1, + b1, + c1 + ]|] +]|] =[| [ + [|[ + [|[ + [|[ + 1, + 2, + 3 + ]|] + ]|] + ]|], + [|[ + 1, + 2, + 3 + ]|] +]|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForBlockComments_test.go b/internal/fourslash/tests/gen/getOutliningForBlockComments_test.go new file mode 100644 index 0000000000..b9b506b0e8 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForBlockComments_test.go @@ -0,0 +1,347 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForBlockComments(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|/* + Block comment at the beginning of the file before module: + line one of the comment + line two of the comment + line three + line four + line five +*/|] +module Sayings[| { + [|/* + Comment before class: + line one of the comment + line two of the comment + line three + line four + line five + */|] + export class Greeter[| { + [|/* + Comment before a string identifier + line two of the comment + */|] + greeting: string; + [|/* + constructor + parameter message as a string + */|] + + [|/* + Multiple comments should be collapsed individually + */|] + constructor(message: string /* do not collapse this */)[| { + this.greeting = message; + }|] + [|/* + method of a class + */|] + greet()[| { + return "Hello, " + this.greeting; + }|] + }|] +}|] + +[|/* + Block comment for interface. The ending can be on the same line as the declaration. +*/|]interface IFoo[| { + [|/* + Multiple block comments + */|] + + [|/* + should be collapsed + */|] + + [|/* + individually + */|] + + [|/* + this comment has trailing space before /* and after *-/ signs + */|] + + [|/** + * + * + * + */|] + + [|/* + */|] + + [|/* + */|] + // single line comments in the middle should not have an effect + [|/* + */|] + + [|/* + */|] + + [|/* + this block comment ends + on the same line */|] [|/* where the following comment starts + should be collapsed separately + */|] + + getDist(): number; +}|] + +var x =[|{ + a:1, + b: 2, + [|/* + Over a function in an object literal + */|] + get foo()[| { + return 1; + }|] +}|] + +// Over a function expression assigned to a variable + [|/** + * Return a sum + * @param {Number} y + * @param {Number} z + * @returns {Number} the sum of y and z + */|] + const sum2 = (y, z) =>[| { + return y + z; + }|]; + +// Over a variable +[|/** + * foo + */|] +const foo = null; + +function Foo()[| { + [|/** + * Description + * + * @param {string} param + * @returns + */|] + this.method = function (param)[| { + }|] + + [|/** + * Description + * + * @param {string} param + * @returns + */|] + function method(param)[| { + }|] +}|] + +function fn1()[| { + [|/** + * comment + */|] +}|] +function fn2()[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] +}|] +function fn3()[| { + const x = 1; + + [|/** + * comment + */|] + + [|/** + * comment + */|] +}|] +function fn4()[| { + [|/** + * comment + */|] + const x = 1; + + [|/** + * comment + */|] +}|] +function fn5()[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + return 1; +}|] +function fn6()[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + const x = 1; +}|] + +[|/* +comment +*/|] + +f6(); + +class C1[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] +}|] +class C2[| { + private prop = 1; + [|/** + * comment + */|] + + [|/** + * comment + */|] +}|] +class C3[| { + [|/** + * comment + */|] + + private prop = 1; + [|/** + * comment + */|] +}|] +class C4[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + private prop = 1; +}|] + +[|/* +comment +*/|] +new C4(); + +module M1[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] +}|] +module M2[| { + export const a = 1; + [|/** + * comment + */|] + + [|/** + * comment + */|] +}|] +module M3[| { + [|/** + * comment + */|] + export const a = 1; + + [|/** + * comment + */|] +}|] +module M4[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + export const a = 1; +}|] +interface I1[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] +}|] +interface I2[| { + x: number; + [|/** + * comment + */|] + + [|/** + * comment + */|] +}|] +interface I3[| { + [|/** + * comment + */|] + x: number; + + [|/** + * comment + */|] +}|] +interface I4[| { + [|/** + * comment + */|] + + [|/** + * comment + */|] + x: number; +}|] +[|{ + [|/** + * comment + */|] + + [|/** + * comment + */|] +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForObjectDestructuring_test.go b/internal/fourslash/tests/gen/getOutliningForObjectDestructuring_test.go new file mode 100644 index 0000000000..2d8a8c48a8 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForObjectDestructuring_test.go @@ -0,0 +1,50 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForObjectDestructuring(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `const[| { + a, + b, + c +}|] =[| { + a: 1, + b: 2, + c: 3 +}|] +const[| { + a:[| { + a_1, + a_2, + a_3:[| { + a_3_1, + a_3_2, + a_3_3, + }|], + }|], + b, + c +}|] =[| { + a:[| { + a_1: 1, + a_2: 2, + a_3:[| { + a_3_1: 1, + a_3_2: 1, + a_3_3: 1 + }|], + }|], + b: 2, + c: 3 +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForObjectsInArray_test.go b/internal/fourslash/tests/gen/getOutliningForObjectsInArray_test.go new file mode 100644 index 0000000000..f19fd06936 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForObjectsInArray_test.go @@ -0,0 +1,64 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForObjectsInArray(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `const x =[| [ + [|{ a: 0 }|], + [|{ b: 1 }|], + [|{ c: 2 }|] +]|]; + +const y =[| [ + [|{ + a: 0 + }|], + [|{ + b: 1 + }|], + [|{ + c: 2 + }|] +]|]; + +const w =[| [ + [|[ 0 ]|], + [|[ 1 ]|], + [|[ 2 ]|] +]|]; + +const z =[| [ + [|[ + 0 + ]|], + [|[ + 1 + ]|], + [|[ + 2 + ]|] +]|]; + +const z =[| [ + [|[ + [|{ hello: 0 }|] + ]|], + [|[ + [|{ hello: 3 }|] + ]|], + [|[ + [|{ hello: 5 }|], + [|{ hello: 7 }|] + ]|] +]|];` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForSingleLineComments_test.go b/internal/fourslash/tests/gen/getOutliningForSingleLineComments_test.go new file mode 100644 index 0000000000..0124c74201 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForSingleLineComments_test.go @@ -0,0 +1,100 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForSingleLineComments(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|// Single line comments at the start of the file +// line 2 +// line 3 +// line 4|] +module Sayings[| { + + [|/* + */|] + [|// A sequence of + // single line|] + [|/* + and block + */|] + [|// comments + //|] + export class Sample[| { + }|] +}|] + +interface IFoo[| { + [|// all consecutive single line comments should be in one block regardless of their number or empty lines/spaces inbetween + + // comment 2 + // comment 3 + + //comment 4 + /// comment 5 + ///// comment 6 + + //comment 7 + ///comment 8 + // comment 9 + // //comment 10 + + + + + + + + + + + + + + + + + + + + + // // //comment 11 + // comment 12 + // comment 13 + // comment 14 + // comment 15 + + // comment 16 + // comment 17 + // comment 18 + // comment 19 + // comment 20 + // comment 21|] + + getDist(): number; // One single line comment should not be collapsed +}|] + +// One single line comment should not be collapsed +class WithOneSingleLineComment[| { +}|] + +function Foo()[| { + [|// comment 1 + // comment 2|] + this.method = function (param)[| { + }|] + + [|// comment 1 + // comment 2|] + function method(param)[| { + }|] +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForTupleType_test.go b/internal/fourslash/tests/gen/getOutliningForTupleType_test.go new file mode 100644 index 0000000000..8ae3491ed7 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForTupleType_test.go @@ -0,0 +1,31 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForTupleType(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `type A =[| [ + number, + number, + number +]|] + +type B =[| [ + [|[ + [|[ + number, + number, + number + ]|] + ]|] +]|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningForTypeLiteral_test.go b/internal/fourslash/tests/gen/getOutliningForTypeLiteral_test.go new file mode 100644 index 0000000000..d15f89b5d5 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningForTypeLiteral_test.go @@ -0,0 +1,36 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningForTypeLiteral(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `type A =[| { + a: number; +}|] + +type B =[| { + a:[| { + a1:[| { + a2:[| { + x: number; + y: number; + }|] + }|] + }|], + b:[| { + x: number; + }|], + c:[| { + x: number; + }|] +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansDepthChainedCalls_test.go b/internal/fourslash/tests/gen/getOutliningSpansDepthChainedCalls_test.go new file mode 100644 index 0000000000..5d270b34bb --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansDepthChainedCalls_test.go @@ -0,0 +1,126 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansDepthChainedCalls(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `declare var router: any; +router + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|] + .get[|("/", async(ctx) =>[|{ + ctx.body = "base"; + }|])|] + .post[|("/a", async(ctx) =>[|{ + //a + }|])|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansDepthElseIf_test.go b/internal/fourslash/tests/gen/getOutliningSpansDepthElseIf_test.go new file mode 100644 index 0000000000..b7e3068686 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansDepthElseIf_test.go @@ -0,0 +1,99 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansDepthElseIf(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else if (1)[| { + 1; +}|] else[| { + 1; +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForComments_test.go b/internal/fourslash/tests/gen/getOutliningSpansForComments_test.go new file mode 100644 index 0000000000..9563a425e9 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForComments_test.go @@ -0,0 +1,32 @@ +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 TestGetOutliningSpansForComments(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|/* + Block comment at the beginning of the file before module: + line one of the comment + line two of the comment + line three + line four + line five +*/|] +declare module "m"; +[|// Single line comments at the start of the file +// line 2 +// line 3 +// line 4|] +declare module "n";` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.MarkTestAsStradaServer() + f.VerifyOutliningSpans(t, lsproto.FoldingRangeKindComment) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForImports_test.go b/internal/fourslash/tests/gen/getOutliningSpansForImports_test.go new file mode 100644 index 0000000000..8bd56be4a0 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForImports_test.go @@ -0,0 +1,32 @@ +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 TestGetOutliningSpansForImports(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|import * as ns from "mod"; + +import d from "mod"; +import { a, b, c } from "mod"; + +import r = require("mod");|] + +// statement +var x = 0; + +// another set of imports +[|import * as ns from "mod"; +import d from "mod"; +import { a, b, c } from "mod"; +import r = require("mod");|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t, lsproto.FoldingRangeKindImports) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForRegionsNoSingleLineFolds_test.go b/internal/fourslash/tests/gen/getOutliningSpansForRegionsNoSingleLineFolds_test.go new file mode 100644 index 0000000000..1fcd3b24be --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForRegionsNoSingleLineFolds_test.go @@ -0,0 +1,32 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansForRegionsNoSingleLineFolds(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|//#region +function foo()[| { + +}|] +[|//these +//should|] +//#endregion not you|] +[|// be +// together|] + +[|//#region bla bla bla + +function bar()[| { }|] + +//#endregion|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.MarkTestAsStradaServer() + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForRegions_test.go b/internal/fourslash/tests/gen/getOutliningSpansForRegions_test.go new file mode 100644 index 0000000000..fc8eb75b01 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForRegions_test.go @@ -0,0 +1,65 @@ +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 TestGetOutliningSpansForRegions(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// region without label +[|// #region + +// #endregion|] + +// region without label with trailing spaces +[|// #region + +// #endregion|] + +// region with label +[|// #region label1 + +// #endregion|] + +// region with extra whitespace in all valid locations + [|// #region label2 label3 + + // #endregion|] + +// No space before directive +[|//#region label4 + +//#endregion|] + +// Nested regions +[|// #region outer + +[|// #region inner + +// #endregion inner|] + +// #endregion outer|] + +// region delimiters not valid when there is preceding text on line + test // #region invalid1 + +test // #endregion + +// region delimiters not valid when in multiline comment +/* +// #region invalid2 +*/ + +/* +// #endregion +*/` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.MarkTestAsStradaServer() + f.VerifyOutliningSpans(t, lsproto.FoldingRangeKindRegion) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForTemplateLiteral_test.go b/internal/fourslash/tests/gen/getOutliningSpansForTemplateLiteral_test.go new file mode 100644 index 0000000000..d6a03f07e7 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForTemplateLiteral_test.go @@ -0,0 +1,32 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpansForTemplateLiteral(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `declare function tag(...args: any[]): void +const a = [|` + "`" + `signal line` + "`" + `|] +const b = [|` + "`" + `multi +line` + "`" + `|] +const c = tag[|` + "`" + `signal line` + "`" + `|] +const d = tag[|` + "`" + `multi +line` + "`" + `|] +const e = [|` + "`" + `signal ${1} line` + "`" + `|] +const f = [|` + "`" + `multi +${1} +line` + "`" + `|] +const g = tag[|` + "`" + `signal ${1} line` + "`" + `|] +const h = tag[|` + "`" + `multi +${1} +line` + "`" + `|] +const i = ` + "`" + `` + "`" + `` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForUnbalancedEndRegion_test.go b/internal/fourslash/tests/gen/getOutliningSpansForUnbalancedEndRegion_test.go new file mode 100644 index 0000000000..b4da80fd87 --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForUnbalancedEndRegion_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 TestGetOutliningSpansForUnbalancedEndRegion(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// bottom-heavy region balance +[|// #region matched + +// #endregion matched|] + +// #endregion unmatched` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t, lsproto.FoldingRangeKindRegion) +} diff --git a/internal/fourslash/tests/gen/getOutliningSpansForUnbalancedRegion_test.go b/internal/fourslash/tests/gen/getOutliningSpansForUnbalancedRegion_test.go new file mode 100644 index 0000000000..69d2abffcf --- /dev/null +++ b/internal/fourslash/tests/gen/getOutliningSpansForUnbalancedRegion_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 TestGetOutliningSpansForUnbalancedRegion(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// top-heavy region balance +// #region unmatched + +[|// #region matched + +// #endregion matched|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t, lsproto.FoldingRangeKindRegion) +} diff --git a/internal/fourslash/tests/gen/outlineSpansBlockCommentsWithoutStatements_test.go b/internal/fourslash/tests/gen/outlineSpansBlockCommentsWithoutStatements_test.go new file mode 100644 index 0000000000..546f7aa02e --- /dev/null +++ b/internal/fourslash/tests/gen/outlineSpansBlockCommentsWithoutStatements_test.go @@ -0,0 +1,19 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutlineSpansBlockCommentsWithoutStatements(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|/* +/ * Some text + */|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outlineSpansTrailingBlockCommentsAfterStatements_test.go b/internal/fourslash/tests/gen/outlineSpansTrailingBlockCommentsAfterStatements_test.go new file mode 100644 index 0000000000..fe9047aed1 --- /dev/null +++ b/internal/fourslash/tests/gen/outlineSpansTrailingBlockCommentsAfterStatements_test.go @@ -0,0 +1,20 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutlineSpansTrailingBlockCommentsAfterStatements(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `console.log(0); +[|/* +/ * Some text + */|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansForArguments_test.go b/internal/fourslash/tests/gen/outliningSpansForArguments_test.go new file mode 100644 index 0000000000..c60a256de7 --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansForArguments_test.go @@ -0,0 +1,30 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansForArguments(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `console.log(123, 456)l; +console.log( +); +console.log[|( + 123, 456 +)|]; +console.log[|( + 123, + 456 +)|]; +() =>[| console.log[|( + 123, + 456 +)|]|];` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansForArrowFunctionBody_test.go b/internal/fourslash/tests/gen/outliningSpansForArrowFunctionBody_test.go new file mode 100644 index 0000000000..86a492a098 --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansForArrowFunctionBody_test.go @@ -0,0 +1,27 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansForArrowFunctionBody(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `() => 42; +() => ( 42 ); +() =>[| { + 42 +}|]; +() => [|( + 42 +)|]; +() =>[| "foo" + + "bar" + + "baz"|];` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansForFunction_test.go b/internal/fourslash/tests/gen/outliningSpansForFunction_test.go new file mode 100644 index 0000000000..6b72692a70 --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansForFunction_test.go @@ -0,0 +1,96 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansForFunction(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|( + a: number, + b: number +) => { + return a + b; +}|]; + +(a: number, b: number) =>[| { + return a + b; +}|] + +const f1 = function[| ( + a: number + b: number +) { + return a + b; +}|] + +const f2 = function (a: number, b: number)[| { + return a + b; +}|] + +function f3[| ( + a: number + b: number +) { + return a + b; +}|] + +function f4(a: number, b: number)[| { + return a + b; +}|] + +class Foo[| { + constructor[|( + a: number, + b: number + ) { + this.a = a; + this.b = b; + }|] + + m1[|( + a: number, + b: number + ) { + return a + b; + }|] + + m1(a: number, b: number)[| { + return a + b; + }|] +}|] + +declare function foo(props: any): void; +foo[|( + a =>[| { + + }|] +)|] + +foo[|( + (a) =>[| { + + }|] +)|] + +foo[|( + (a, b, c) =>[| { + + }|] +)|] + +foo[|([| + (a, + b, + c) => { + + }|] +)|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansForImportAndExportAttributes_test.go b/internal/fourslash/tests/gen/outliningSpansForImportAndExportAttributes_test.go new file mode 100644 index 0000000000..31b8eb76f1 --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansForImportAndExportAttributes_test.go @@ -0,0 +1,60 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansForImportAndExportAttributes(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `import { a1, a2 } from "a"; +; +import { +} from "a"; +; +import [|{ + b1, + b2, +}|] from "b"; +; +import j1 from "./j" with { type: "json" }; +; +import j2 from "./j" with { +}; +; +import j3 from "./j" with [|{ + type: "json" +}|]; +; +[|import { a5, a6 } from "a"; +import [|{ + a7, + a8, +}|] from "a";|] +export { a1, a2 }; +; +export { a3, a4 } from "a"; +; +export { +}; +; +export [|{ + b1, + b2, +}|]; +; +export { +} from "b"; +; +export [|{ + b3, + b4, +}|] from "b"; +;` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansForImportsAndExports_test.go b/internal/fourslash/tests/gen/outliningSpansForImportsAndExports_test.go new file mode 100644 index 0000000000..724db92ffc --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansForImportsAndExports_test.go @@ -0,0 +1,60 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansForImportsAndExports(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `import { a1, a2 } from "a"; +; +import { +} from "a"; +; +import [|{ + b1, + b2, +}|] from "b"; +; +import j1 from "./j" assert { type: "json" }; +; +import j2 from "./j" assert { +}; +; +import j3 from "./j" assert [|{ + type: "json" +}|]; +; +[|import { a5, a6 } from "a"; +import [|{ + a7, + a8, +}|] from "a";|] +export { a1, a2 }; +; +export { a3, a4 } from "a"; +; +export { +}; +; +export [|{ + b1, + b2, +}|]; +; +export { +} from "b"; +; +export [|{ + b3, + b4, +}|] from "b"; +;` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansForParenthesizedExpression_test.go b/internal/fourslash/tests/gen/outliningSpansForParenthesizedExpression_test.go new file mode 100644 index 0000000000..bc037cb518 --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansForParenthesizedExpression_test.go @@ -0,0 +1,45 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansForParenthesizedExpression(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `const a = [|( + true + ? true + : false + ? true + : false +)|]; + +const b = ( 1 ); + +const c = [|( + 1 +)|]; + +( 1 ); + +[|( + [|( + [|( + 1 + )|] + )|] +)|]; + +[|( + [|( + ( 1 ) + )|] +)|];` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/gen/outliningSpansSwitchCases_test.go b/internal/fourslash/tests/gen/outliningSpansSwitchCases_test.go new file mode 100644 index 0000000000..761a78296c --- /dev/null +++ b/internal/fourslash/tests/gen/outliningSpansSwitchCases_test.go @@ -0,0 +1,49 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningSpansSwitchCases(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `switch (undefined)[| { + case 0:[| + console.log(1) + console.log(2) + break; + console.log(3);|] + case 1:[| + break;|] + case 2:[| + break; + console.log(3);|] + case 3:[| + console.log(4);|] + + case 4: + case 5: + case 6:[| + + + console.log(5);|] + + case 7:[| console.log(6);|] + + case 8:[| [|{ + console.log(8); + break; + }|] + console.log(8);|] + + default:[| + console.log(7); + console.log(8);|] +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/manual/getOutliningSpans_test.go b/internal/fourslash/tests/manual/getOutliningSpans_test.go new file mode 100644 index 0000000000..3b06adf2e7 --- /dev/null +++ b/internal/fourslash/tests/manual/getOutliningSpans_test.go @@ -0,0 +1,140 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestGetOutliningSpans(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// interface +interface IFoo[| { + getDist(): number; +}|] + +// class members +class Foo[| { + constructor()[| { + }|] + + public foo(): number[| { + return 0; + }|] + + public get X()[| { + return 1; + }|] + + public set X(v: number)[| { + }|] + + public member = function f()[| { + + }|] +}|] +// class expressions + [|(new class[| { + bla()[| { + + }|] + }|])|] +switch(1)[| { + case 1:[| break;|] +}|] + +var array =[| [ + 1, + 2 +]|] + +// modules +module m1[| { + module m2[| { }|] + module m3[| { + function foo()[| { + + }|] + + interface IFoo2[| { + + }|] + + class foo2 implements IFoo2[| { + + }|] + }|] +}|] + +// function declaration +function foo(): number[| { + return 0; +}|] + +// function expressions +[|(function f()[| { + +}|])|] + +// trivia handeling +class ClassFooWithTrivia[| /* some comments */ + /* more trivia */ { + + + [|/*some trailing trivia */|] +}|] /* even more */ + +// object literals +var x =[|{ + a:1, + b:2, + get foo()[| { + return 1; + }|] +}|] +//outline with deep nesting +var nest =[| [[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[[|[ + [|[ + [ + [ + [ + [ + 1,2,3 + ] + ] + ] + ] + ]|] +]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]]|]; + +//outline after a deeply nested node +class AfterNestedNodes[| { +}|] +// function arguments +function f(x: number[], y: number[])[| { + return 3; +}|] +f[|( +// single line array literal span won't render in VS + [|[0]|], + [|[ + 1, + 2 + ]|] +)|]; + +class C[| { + foo: T; +}|] + +class D extends C[| { + constructor(x)[| { + super(x); + }|] +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/manual/incrementalParsingWithJsDoc_test.go b/internal/fourslash/tests/manual/incrementalParsingWithJsDoc_test.go new file mode 100644 index 0000000000..e8aeb4f7f1 --- /dev/null +++ b/internal/fourslash/tests/manual/incrementalParsingWithJsDoc_test.go @@ -0,0 +1,24 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestIncrementalParsingWithJsDoc(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `[|import a from 'a/aaaaaaa/aaaaaaa/aaaaaa/aaaaaaa'; +/**/import b from 'b'; +import c from 'c';|] +[|/** @internal */|] +export class LanguageIdentifier[| { }|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) + f.GoToMarker(t, "") + f.Backspace(t, 1) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/manual/outliningForNonCompleteInterfaceDeclaration_test.go b/internal/fourslash/tests/manual/outliningForNonCompleteInterfaceDeclaration_test.go new file mode 100644 index 0000000000..69e3351747 --- /dev/null +++ b/internal/fourslash/tests/manual/outliningForNonCompleteInterfaceDeclaration_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 TestOutliningForNonCompleteInterfaceDeclaration(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `interface I` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/fourslash/tests/manual/outliningHintSpansForFunction_test.go b/internal/fourslash/tests/manual/outliningHintSpansForFunction_test.go new file mode 100644 index 0000000000..761cb0a2d5 --- /dev/null +++ b/internal/fourslash/tests/manual/outliningHintSpansForFunction_test.go @@ -0,0 +1,28 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestOutliningHintSpansForFunction(t *testing.T) { + t.Parallel() + + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `namespace NS[| { + function f(x: number, y: number)[| { + return x + y; + }|] + + function g[|( + x: number, + y: number, + ): number { + return x + y; + }|] +}|]` + f := fourslash.NewFourslash(t, nil /*capabilities*/, content) + f.VerifyOutliningSpans(t) +} diff --git a/internal/ls/folding.go b/internal/ls/folding.go new file mode 100644 index 0000000000..d37548c7c3 --- /dev/null +++ b/internal/ls/folding.go @@ -0,0 +1,533 @@ +package ls + +import ( + "cmp" + "context" + "slices" + "strings" + "unicode" + + "github.com/microsoft/typescript-go/internal/ast" + "github.com/microsoft/typescript-go/internal/astnav" + "github.com/microsoft/typescript-go/internal/debug" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/printer" + "github.com/microsoft/typescript-go/internal/scanner" +) + +func (l *LanguageService) ProvideFoldingRange(ctx context.Context, documentURI lsproto.DocumentUri) (lsproto.FoldingRangeResponse, error) { + _, sourceFile := l.getProgramAndFile(documentURI) + res := l.addNodeOutliningSpans(ctx, sourceFile) + res = append(res, l.addRegionOutliningSpans(sourceFile)...) + slices.SortFunc(res, func(a, b *lsproto.FoldingRange) int { + if c := cmp.Compare(a.StartLine, b.StartLine); c != 0 { + return c + } + return cmp.Compare(*a.StartCharacter, *b.StartCharacter) + }) + return lsproto.FoldingRangesOrNull{FoldingRanges: &res}, nil +} + +func (l *LanguageService) addNodeOutliningSpans(ctx context.Context, sourceFile *ast.SourceFile) []*lsproto.FoldingRange { + depthRemaining := 40 + current := 0 + + statements := sourceFile.Statements + n := len(statements.Nodes) + foldingRange := make([]*lsproto.FoldingRange, 0, 40) + for current < n { + for current < n && !ast.IsAnyImportSyntax(statements.Nodes[current]) { + foldingRange = append(foldingRange, visitNode(ctx, statements.Nodes[current], depthRemaining, sourceFile, l)...) + current++ + } + if current == n { + break + } + firstImport := current + for current < n && ast.IsAnyImportSyntax(statements.Nodes[current]) { + foldingRange = append(foldingRange, visitNode(ctx, statements.Nodes[current], depthRemaining, sourceFile, l)...) + current++ + } + lastImport := current - 1 + if lastImport != firstImport { + foldingRangeKind := lsproto.FoldingRangeKindImports + foldingRange = append(foldingRange, createFoldingRangeFromBounds( + astnav.GetStartOfNode(astnav.FindChildOfKind(statements.Nodes[firstImport], + ast.KindImportKeyword, sourceFile), sourceFile, false /*includeJSDoc*/), + statements.Nodes[lastImport].End(), + foldingRangeKind, + sourceFile, + l)) + } + } + + // Visit the EOF Token so that comments which aren't attached to statements are included. + foldingRange = append(foldingRange, visitNode(ctx, sourceFile.EndOfFileToken, depthRemaining, sourceFile, l)...) + return foldingRange +} + +func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) []*lsproto.FoldingRange { + regions := make([]*lsproto.FoldingRange, 0, 40) + out := make([]*lsproto.FoldingRange, 0, 40) + lineStarts := scanner.GetECMALineStarts(sourceFile) + for _, currentLineStart := range lineStarts { + lineEnd := getLineEndOfPosition(sourceFile, int(currentLineStart)) + lineText := sourceFile.Text()[currentLineStart:lineEnd] + result := parseRegionDelimiter(lineText) + if result == nil || isInComment(sourceFile, int(currentLineStart), astnav.GetTokenAtPosition(sourceFile, int(currentLineStart))) != nil { + continue + } + + if result.isStart { + commentStart := l.createLspPosition(strings.Index(sourceFile.Text()[currentLineStart:lineEnd], "//")+int(currentLineStart), sourceFile) + foldingRangeKindRegion := lsproto.FoldingRangeKindRegion + collapsedText := "#region" + if result.name != "" { + collapsedText = result.name + } + // Our spans start out with some initial data. + // On every `#endregion`, we'll come back to these `FoldingRange`s + // and fill in their EndLine/EndCharacter. + regions = append(regions, &lsproto.FoldingRange{ + StartLine: commentStart.Line, + StartCharacter: &commentStart.Character, + Kind: &foldingRangeKindRegion, + CollapsedText: &collapsedText, + }) + } else { + if len(regions) > 0 { + region := regions[len(regions)-1] + regions = regions[:len(regions)-1] + endingPosition := l.createLspPosition(lineEnd, sourceFile) + region.EndLine = endingPosition.Line + region.EndCharacter = &endingPosition.Character + out = append(out, region) + } + } + } + return out +} + +func visitNode(ctx context.Context, n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { + if depthRemaining == 0 { + return nil + } + if ctx.Err() != nil { + return nil + } + foldingRange := make([]*lsproto.FoldingRange, 0, 40) + if (!ast.IsBinaryExpression(n) && ast.IsDeclaration(n)) || ast.IsVariableStatement(n) || ast.IsReturnStatement(n) || ast.IsCallOrNewExpression(n) || n.Kind == ast.KindEndOfFile { + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(ctx, n, sourceFile, l)...) + } + if ast.IsFunctionLike(n) && n.Parent != nil && ast.IsBinaryExpression(n.Parent) && n.Parent.AsBinaryExpression().Left != nil && ast.IsPropertyAccessExpression(n.Parent.AsBinaryExpression().Left) { + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForNode(ctx, n.Parent.AsBinaryExpression().Left, sourceFile, l)...) + } + if ast.IsBlock(n) { + statements := n.AsBlock().Statements + if statements != nil { + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, statements.End(), sourceFile, l)...) + } + } + if ast.IsModuleBlock(n) { + statements := n.AsModuleBlock().Statements + if statements != nil { + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, statements.End(), sourceFile, l)...) + } + } + if ast.IsClassLike(n) || ast.IsInterfaceDeclaration(n) { + var members *ast.NodeList + if ast.IsClassDeclaration(n) { + members = n.AsClassDeclaration().Members + } else if ast.IsClassExpression(n) { + members = n.AsClassExpression().Members + } else { + members = n.AsInterfaceDeclaration().Members + } + if members != nil { + foldingRange = append(foldingRange, addOutliningForLeadingCommentsForPos(ctx, members.End(), sourceFile, l)...) + } + } + + span := getOutliningSpanForNode(n, sourceFile, l) + if span != nil { + foldingRange = append(foldingRange, span) + } + + depthRemaining-- + if ast.IsCallExpression(n) { + depthRemaining++ + expressionNodes := visitNode(ctx, n.Expression(), depthRemaining, sourceFile, l) + if expressionNodes != nil { + foldingRange = append(foldingRange, expressionNodes...) + } + depthRemaining-- + for _, arg := range n.Arguments() { + if arg != nil { + foldingRange = append(foldingRange, visitNode(ctx, arg, depthRemaining, sourceFile, l)...) + } + } + typeArguments := n.TypeArguments() + for _, typeArg := range typeArguments { + if typeArg != nil { + foldingRange = append(foldingRange, visitNode(ctx, typeArg, depthRemaining, sourceFile, l)...) + } + } + } else if ast.IsIfStatement(n) && n.AsIfStatement().ElseStatement != nil && ast.IsIfStatement(n.AsIfStatement().ElseStatement) { + // Consider an 'else if' to be on the same depth as the 'if'. + ifStatement := n.AsIfStatement() + expressionNodes := visitNode(ctx, n.Expression(), depthRemaining, sourceFile, l) + if expressionNodes != nil { + foldingRange = append(foldingRange, expressionNodes...) + } + thenNode := visitNode(ctx, ifStatement.ThenStatement, depthRemaining, sourceFile, l) + if thenNode != nil { + foldingRange = append(foldingRange, thenNode...) + } + depthRemaining++ + elseNode := visitNode(ctx, ifStatement.ElseStatement, depthRemaining, sourceFile, l) + if elseNode != nil { + foldingRange = append(foldingRange, elseNode...) + } + depthRemaining-- + } else { + visit := func(node *ast.Node) bool { + childNode := visitNode(ctx, node, depthRemaining, sourceFile, l) + if childNode != nil { + foldingRange = append(foldingRange, childNode...) + } + return false + } + n.ForEachChild(visit) + } + depthRemaining++ + return foldingRange +} + +func addOutliningForLeadingCommentsForNode(ctx context.Context, n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { + if ast.IsJsxText(n) { + return nil + } + return addOutliningForLeadingCommentsForPos(ctx, n.Pos(), sourceFile, l) +} + +func addOutliningForLeadingCommentsForPos(ctx context.Context, pos int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange { + p := &printer.EmitContext{} + foldingRange := make([]*lsproto.FoldingRange, 0, 40) + firstSingleLineCommentStart := -1 + lastSingleLineCommentEnd := -1 + singleLineCommentCount := 0 + foldingRangeKindComment := lsproto.FoldingRangeKindComment + + combineAndAddMultipleSingleLineComments := func() *lsproto.FoldingRange { + // Only outline spans of two or more consecutive single line comments + if singleLineCommentCount > 1 { + return createFoldingRangeFromBounds(firstSingleLineCommentStart, lastSingleLineCommentEnd, foldingRangeKindComment, sourceFile, l) + } + return nil + } + + sourceText := sourceFile.Text() + for comment := range scanner.GetLeadingCommentRanges(&printer.NewNodeFactory(p).NodeFactory, sourceText, pos) { + commentPos := comment.Pos() + commentEnd := comment.End() + + if ctx.Err() != nil { + return nil + } + switch comment.Kind { + case ast.KindSingleLineCommentTrivia: + // never fold region delimiters into single-line comment regions + commentText := sourceText[commentPos:commentEnd] + if parseRegionDelimiter(commentText) != nil { + comments := combineAndAddMultipleSingleLineComments() + if comments != nil { + foldingRange = append(foldingRange, comments) + } + singleLineCommentCount = 0 + break + } + + // For single line comments, combine consecutive ones (2 or more) into + // a single span from the start of the first till the end of the last + if singleLineCommentCount == 0 { + firstSingleLineCommentStart = commentPos + } + lastSingleLineCommentEnd = commentEnd + singleLineCommentCount++ + break + case ast.KindMultiLineCommentTrivia: + comments := combineAndAddMultipleSingleLineComments() + if comments != nil { + foldingRange = append(foldingRange, comments) + } + foldingRange = append(foldingRange, createFoldingRangeFromBounds(commentPos, commentEnd, foldingRangeKindComment, sourceFile, l)) + singleLineCommentCount = 0 + break + default: + debug.AssertNever(comment.Kind) + } + } + addedComments := combineAndAddMultipleSingleLineComments() + if addedComments != nil { + foldingRange = append(foldingRange, addedComments) + } + return foldingRange +} + +type regionDelimiterResult struct { + isStart bool + name string +} + +func parseRegionDelimiter(lineText string) *regionDelimiterResult { + // We trim the leading whitespace and // without the regex since the + // multiple potential whitespace matches can make for some gnarly backtracking behavior + lineText = strings.TrimLeftFunc(lineText, unicode.IsSpace) + if !strings.HasPrefix(lineText, "//") { + return nil + } + lineText = strings.TrimSpace(lineText[2:]) + lineText = strings.TrimSuffix(lineText, "\r") + if !strings.HasPrefix(lineText, "#") { + return nil + } + lineText = lineText[1:] + isStart := true + if strings.HasPrefix(lineText, "end") { + isStart = false + lineText = lineText[3:] + } + if !strings.HasPrefix(lineText, "region") { + return nil + } + lineText = lineText[6:] + return ®ionDelimiterResult{ + isStart: isStart, + name: strings.TrimSpace(lineText), + } +} + +func getOutliningSpanForNode(n *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + switch n.Kind { + case ast.KindBlock: + if ast.IsFunctionLike(n.Parent) { + return functionSpan(n.Parent, n, sourceFile, l) + } + // Check if the block is standalone, or 'attached' to some parent statement. + // If the latter, we want to collapse the block, but consider its hint span + // to be the entire span of the parent. + switch n.Parent.Kind { + case ast.KindDoStatement, ast.KindForInStatement, ast.KindForOfStatement, ast.KindForStatement, ast.KindIfStatement, ast.KindWhileStatement, ast.KindWithStatement, ast.KindCatchClause: + return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + case ast.KindTryStatement: + // Could be the try-block, or the finally-block. + tryStatement := n.Parent.AsTryStatement() + if tryStatement.TryBlock == n { + return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + } else if tryStatement.FinallyBlock == n { + if span := spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l); span != nil { + return span + } + } + fallthrough + default: + // Block was a standalone block. In this case we want to only collapse + // the span of the block, independent of any parent span. + return createFoldingRange(l.createLspRangeFromNode(n, sourceFile), "", "") + } + case ast.KindModuleBlock: + return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + case ast.KindClassDeclaration, ast.KindClassExpression, ast.KindInterfaceDeclaration, ast.KindEnumDeclaration, ast.KindCaseBlock, ast.KindTypeLiteral, ast.KindObjectBindingPattern: + return spanForNode(n, ast.KindOpenBraceToken, true /*useFullStart*/, sourceFile, l) + case ast.KindTupleType: + return spanForNode(n, ast.KindOpenBracketToken, !ast.IsTupleTypeNode(n.Parent) /*useFullStart*/, sourceFile, l) + case ast.KindCaseClause, ast.KindDefaultClause: + return spanForNodeArray(n.AsCaseOrDefaultClause().Statements, sourceFile, l) + case ast.KindObjectLiteralExpression: + return spanForNode(n, ast.KindOpenBraceToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart*/, sourceFile, l) + case ast.KindArrayLiteralExpression: + return spanForNode(n, ast.KindOpenBracketToken, !ast.IsArrayLiteralExpression(n.Parent) && !ast.IsCallExpression(n.Parent) /*useFullStart*/, sourceFile, l) + case ast.KindJsxElement, ast.KindJsxFragment: + return spanForJSXElement(n, sourceFile, l) + case ast.KindJsxSelfClosingElement, ast.KindJsxOpeningElement: + return spanForJSXAttributes(n, sourceFile, l) + case ast.KindTemplateExpression, ast.KindNoSubstitutionTemplateLiteral: + return spanForTemplateLiteral(n, sourceFile, l) + case ast.KindArrayBindingPattern: + return spanForNode(n, ast.KindOpenBracketToken, !ast.IsBindingElement(n.Parent) /*useFullStart*/, sourceFile, l) + case ast.KindArrowFunction: + return spanForArrowFunction(n, sourceFile, l) + case ast.KindCallExpression: + return spanForCallExpression(n, sourceFile, l) + case ast.KindParenthesizedExpression: + return spanForParenthesizedExpression(n, sourceFile, l) + case ast.KindNamedImports, ast.KindNamedExports, ast.KindImportAttributes: + return spanForImportExportElements(n, sourceFile, l) + } + return nil +} + +func spanForImportExportElements(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + var elements *ast.NodeList + switch node.Kind { + case ast.KindNamedImports: + elements = node.AsNamedImports().Elements + case ast.KindNamedExports: + elements = node.AsNamedExports().Elements + case ast.KindImportAttributes: + elements = node.AsImportAttributes().Attributes + } + if elements == nil || len(elements.Nodes) == 0 { + return nil + } + openToken := astnav.FindChildOfKind(node, ast.KindOpenBraceToken, sourceFile) + closeToken := astnav.FindChildOfKind(node, ast.KindCloseBraceToken, sourceFile) + if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) { + return nil + } + return rangeBetweenTokens(openToken, closeToken, sourceFile, false /*useFullStart*/, l) +} + +func spanForParenthesizedExpression(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + start := astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/) + if printer.PositionsAreOnSameLine(start, node.End(), sourceFile) { + return nil + } + textRange := l.createLspRangeFromBounds(start, node.End(), sourceFile) + return createFoldingRange(textRange, "", "") +} + +func spanForCallExpression(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + if node.AsCallExpression().Arguments == nil || len(node.AsCallExpression().Arguments.Nodes) == 0 { + return nil + } + openToken := astnav.FindChildOfKind(node, ast.KindOpenParenToken, sourceFile) + closeToken := astnav.FindChildOfKind(node, ast.KindCloseParenToken, sourceFile) + if openToken == nil || closeToken == nil || printer.PositionsAreOnSameLine(openToken.Pos(), closeToken.Pos(), sourceFile) { + return nil + } + + return rangeBetweenTokens(openToken, closeToken, sourceFile, true /*useFullStart*/, l) +} + +func spanForArrowFunction(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + arrowFunctionNode := node.AsArrowFunction() + if ast.IsBlock(arrowFunctionNode.Body) || ast.IsParenthesizedExpression(arrowFunctionNode.Body) || printer.PositionsAreOnSameLine(arrowFunctionNode.Body.Pos(), arrowFunctionNode.Body.End(), sourceFile) { + return nil + } + textRange := l.createLspRangeFromBounds(arrowFunctionNode.Body.Pos(), arrowFunctionNode.Body.End(), sourceFile) + return createFoldingRange(textRange, "", "") +} + +func spanForTemplateLiteral(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + if node.Kind == ast.KindNoSubstitutionTemplateLiteral && len(node.Text()) == 0 { + return nil + } + return createFoldingRangeFromBounds(astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l) +} + +func spanForJSXElement(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + if node.Kind == ast.KindJsxElement { + jsxElement := node.AsJsxElement() + textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(jsxElement.OpeningElement, sourceFile, false /*includeJSDoc*/), jsxElement.ClosingElement.End(), sourceFile) + tagName := jsxElement.OpeningElement.TagName().Text() + bannerText := "<" + tagName + ">..." + return createFoldingRange(textRange, "", bannerText) + } + // JsxFragment + jsxFragment := node.AsJsxFragment() + textRange := l.createLspRangeFromBounds(astnav.GetStartOfNode(jsxFragment.OpeningFragment, sourceFile, false /*includeJSDoc*/), jsxFragment.ClosingFragment.End(), sourceFile) + return createFoldingRange(textRange, "", "<>...") +} + +func spanForJSXAttributes(node *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + var attributes *ast.JsxAttributesNode + if node.Kind == ast.KindJsxSelfClosingElement { + attributes = node.AsJsxSelfClosingElement().Attributes + } else { + attributes = node.AsJsxOpeningElement().Attributes + } + if len(attributes.Properties()) == 0 { + return nil + } + return createFoldingRangeFromBounds(astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/), node.End(), "", sourceFile, l) +} + +func spanForNodeArray(statements *ast.NodeList, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + if statements != nil && len(statements.Nodes) != 0 { + return createFoldingRange(l.createLspRangeFromBounds(statements.Pos(), statements.End(), sourceFile), "", "") + } + return nil +} + +func spanForNode(node *ast.Node, open ast.Kind, useFullStart bool, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + closeBrace := ast.KindCloseBraceToken + if open != ast.KindOpenBraceToken { + closeBrace = ast.KindCloseBracketToken + } + openToken := astnav.FindChildOfKind(node, open, sourceFile) + closeToken := astnav.FindChildOfKind(node, closeBrace, sourceFile) + if openToken != nil && closeToken != nil { + return rangeBetweenTokens(openToken, closeToken, sourceFile, useFullStart, l) + } + return nil +} + +func rangeBetweenTokens(openToken *ast.Node, closeToken *ast.Node, sourceFile *ast.SourceFile, useFullStart bool, l *LanguageService) *lsproto.FoldingRange { + var textRange *lsproto.Range + if useFullStart { + textRange = l.createLspRangeFromBounds(openToken.Pos(), closeToken.End(), sourceFile) + } else { + textRange = l.createLspRangeFromBounds(astnav.GetStartOfNode(openToken, sourceFile, false /*includeJSDoc*/), closeToken.End(), sourceFile) + } + return createFoldingRange(textRange, "", "") +} + +func createFoldingRange(textRange *lsproto.Range, foldingRangeKind lsproto.FoldingRangeKind, collapsedText string) *lsproto.FoldingRange { + if collapsedText == "" { + defaultText := "..." + collapsedText = defaultText + } + var kind *lsproto.FoldingRangeKind + if foldingRangeKind != "" { + kind = &foldingRangeKind + } + return &lsproto.FoldingRange{ + StartLine: textRange.Start.Line, + StartCharacter: &textRange.Start.Character, + EndLine: textRange.End.Line, + EndCharacter: &textRange.End.Character, + Kind: kind, + CollapsedText: &collapsedText, + } +} + +func createFoldingRangeFromBounds(pos int, end int, foldingRangeKind lsproto.FoldingRangeKind, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + return createFoldingRange(l.createLspRangeFromBounds(pos, end, sourceFile), foldingRangeKind, "") +} + +func functionSpan(node *ast.Node, body *ast.Node, sourceFile *ast.SourceFile, l *LanguageService) *lsproto.FoldingRange { + openToken := tryGetFunctionOpenToken(node, body, sourceFile) + closeToken := astnav.FindChildOfKind(body, ast.KindCloseBraceToken, sourceFile) + if openToken != nil && closeToken != nil { + return rangeBetweenTokens(openToken, closeToken, sourceFile, true /*useFullStart*/, l) + } + return nil +} + +func tryGetFunctionOpenToken(node *ast.SignatureDeclaration, body *ast.Node, sourceFile *ast.SourceFile) *ast.Node { + if isNodeArrayMultiLine(node.Parameters(), sourceFile) { + openParenToken := astnav.FindChildOfKind(node, ast.KindOpenParenToken, sourceFile) + if openParenToken != nil { + return openParenToken + } + } + return astnav.FindChildOfKind(body, ast.KindOpenBraceToken, sourceFile) +} + +func isNodeArrayMultiLine(list []*ast.Node, sourceFile *ast.SourceFile) bool { + if len(list) == 0 { + return false + } + return !printer.PositionsAreOnSameLine(list[0].Pos(), list[len(list)-1].End(), sourceFile) +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index e9e33669ad..319c6175af 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -519,6 +519,7 @@ var handlers = sync.OnceValue(func() handlerMap { registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentCodeLensInfo, (*Server).handleCodeLens) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentCodeActionInfo, (*Server).handleCodeAction) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentPrepareCallHierarchyInfo, (*Server).handlePrepareCallHierarchy) + registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentFoldingRangeInfo, (*Server).handleFoldingRange) registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*Server).handleReferences, combineReferences) registerMultiProjectReferenceRequestHandler(handlers, lsproto.TextDocumentRenameInfo, (*Server).handleRename, combineRenameResponse) @@ -526,6 +527,9 @@ var handlers = sync.OnceValue(func() handlerMap { registerRequestHandler(handlers, lsproto.CallHierarchyIncomingCallsInfo, (*Server).handleCallHierarchyIncomingCalls) registerRequestHandler(handlers, lsproto.CallHierarchyOutgoingCallsInfo, (*Server).handleCallHierarchyOutgoingCalls) + registerRequestHandler(handlers, lsproto.CallHierarchyIncomingCallsInfo, (*Server).handleCallHierarchyIncomingCalls) + registerRequestHandler(handlers, lsproto.CallHierarchyOutgoingCallsInfo, (*Server).handleCallHierarchyOutgoingCalls) + registerRequestHandler(handlers, lsproto.WorkspaceSymbolInfo, (*Server).handleWorkspaceSymbol) registerRequestHandler(handlers, lsproto.CompletionItemResolveInfo, (*Server).handleCompletionItemResolve) registerRequestHandler(handlers, lsproto.CodeLensResolveInfo, (*Server).handleCodeLensResolve) @@ -939,6 +943,9 @@ func (s *Server) handleInitialize(ctx context.Context, params *lsproto.Initializ DocumentSymbolProvider: &lsproto.BooleanOrDocumentSymbolOptions{ Boolean: ptrTo(true), }, + FoldingRangeProvider: &lsproto.BooleanOrFoldingRangeOptionsOrFoldingRangeRegistrationOptions{ + Boolean: ptrTo(true), + }, RenameProvider: &lsproto.BooleanOrRenameOptions{ Boolean: ptrTo(true), }, @@ -1133,6 +1140,10 @@ func (s *Server) handleSignatureHelp(ctx context.Context, languageService *ls.La ) } +func (s *Server) handleFoldingRange(ctx context.Context, ls *ls.LanguageService, params *lsproto.FoldingRangeParams) (lsproto.FoldingRangeResponse, error) { + return ls.ProvideFoldingRange(ctx, params.TextDocument.Uri) +} + func (s *Server) handleDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.DefinitionParams) (lsproto.DefinitionResponse, error) { return ls.ProvideDefinition(ctx, params.TextDocument.Uri, params.Position) } diff --git a/internal/printer/printer.go b/internal/printer/printer.go index 2a212b6be3..8c4579bde0 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -5059,7 +5059,7 @@ func (p *Printer) emitCommentsBeforeToken(token ast.Kind, pos int, contextNode * if contextNode.Pos() != startPos { indentLeading := flags&tefIndentLeadingComments != 0 - needsIndent := indentLeading && p.currentSourceFile != nil && !positionsAreOnSameLine(startPos, pos, p.currentSourceFile) + needsIndent := indentLeading && p.currentSourceFile != nil && !PositionsAreOnSameLine(startPos, pos, p.currentSourceFile) p.increaseIndentIf(needsIndent) p.emitLeadingComments(startPos, false /*elided*/) p.decreaseIndentIf(needsIndent) diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index 5716e75d6c..44956a8611 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -335,7 +335,7 @@ func rangeIsOnSingleLine(r core.TextRange, sourceFile *ast.SourceFile) bool { } func rangeStartPositionsAreOnSameLine(range1 core.TextRange, range2 core.TextRange, sourceFile *ast.SourceFile) bool { - return positionsAreOnSameLine( + return PositionsAreOnSameLine( getStartPositionOfRange(range1, sourceFile, false /*includeComments*/), getStartPositionOfRange(range2, sourceFile, false /*includeComments*/), sourceFile, @@ -343,15 +343,15 @@ func rangeStartPositionsAreOnSameLine(range1 core.TextRange, range2 core.TextRan } func rangeEndPositionsAreOnSameLine(range1 core.TextRange, range2 core.TextRange, sourceFile *ast.SourceFile) bool { - return positionsAreOnSameLine(range1.End(), range2.End(), sourceFile) + return PositionsAreOnSameLine(range1.End(), range2.End(), sourceFile) } func rangeStartIsOnSameLineAsRangeEnd(range1 core.TextRange, range2 core.TextRange, sourceFile *ast.SourceFile) bool { - return positionsAreOnSameLine(getStartPositionOfRange(range1, sourceFile, false /*includeComments*/), range2.End(), sourceFile) + return PositionsAreOnSameLine(getStartPositionOfRange(range1, sourceFile, false /*includeComments*/), range2.End(), sourceFile) } func rangeEndIsOnSameLineAsRangeStart(range1 core.TextRange, range2 core.TextRange, sourceFile *ast.SourceFile) bool { - return positionsAreOnSameLine(range1.End(), getStartPositionOfRange(range2, sourceFile, false /*includeComments*/), sourceFile) + return PositionsAreOnSameLine(range1.End(), getStartPositionOfRange(range2, sourceFile, false /*includeComments*/), sourceFile) } func getStartPositionOfRange(r core.TextRange, sourceFile *ast.SourceFile, includeComments bool) int { @@ -361,7 +361,7 @@ func getStartPositionOfRange(r core.TextRange, sourceFile *ast.SourceFile, inclu return scanner.SkipTriviaEx(sourceFile.Text(), r.Pos(), &scanner.SkipTriviaOptions{StopAtComments: includeComments}) } -func positionsAreOnSameLine(pos1 int, pos2 int, sourceFile *ast.SourceFile) bool { +func PositionsAreOnSameLine(pos1 int, pos2 int, sourceFile *ast.SourceFile) bool { return GetLinesBetweenPositions(sourceFile, pos1, pos2) == 0 }