From 6e90dec58f18a250523c2597467d220c37ed9f14 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Thu, 23 Jul 2026 09:19:57 -0400 Subject: [PATCH] feat(compiler): add folding range kinds for comments and imports Report a FoldingRangeKind on language server folding ranges: comments fold as 'comment' and consecutive import statements fold together as an 'imports' region. Enables editor commands like Fold All Block Comments and Fold All Imports for TypeSpec files (part of #860). --- .../folding-range-kinds-2026-7-23-8-51-0.md | 7 +++ packages/compiler/src/server/serverlib.ts | 31 ++++++++-- packages/compiler/test/server/folding.test.ts | 57 +++++++++++++++++-- 3 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 .chronus/changes/folding-range-kinds-2026-7-23-8-51-0.md diff --git a/.chronus/changes/folding-range-kinds-2026-7-23-8-51-0.md b/.chronus/changes/folding-range-kinds-2026-7-23-8-51-0.md new file mode 100644 index 00000000000..8d9904e02ac --- /dev/null +++ b/.chronus/changes/folding-range-kinds-2026-7-23-8-51-0.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Language server folding ranges now report a `kind`: comments fold as `comment` and consecutive `import` statements fold together as an `imports` region. This enables editor commands such as "Fold All Block Comments" and "Fold All Imports" to work with TypeSpec files. diff --git a/packages/compiler/src/server/serverlib.ts b/packages/compiler/src/server/serverlib.ts index 2cad580d0d5..55a4379472c 100644 --- a/packages/compiler/src/server/serverlib.ts +++ b/packages/compiler/src/server/serverlib.ts @@ -17,6 +17,7 @@ import { DocumentSymbolParams, FileChangeType, FoldingRange, + FoldingRangeKind, FoldingRangeParams, Hover, HoverParams, @@ -669,13 +670,29 @@ export function createServer( rangeStartSingleLines = comment.pos; } } else if (rangeStartSingleLines !== -1) { - addRange(rangeStartSingleLines, comment.end); + addRange(rangeStartSingleLines, comment.end, FoldingRangeKind.Comment); rangeStartSingleLines = -1; } else { - addRange(comment.pos, comment.end); + addRange(comment.pos, comment.end, FoldingRangeKind.Comment); } } + addRangesForImports(); visitChildren(ast, addRangesForNode); + function addRangesForImports() { + const statements = ast!.statements; + let runStart = -1; + for (let i = 0; i <= statements.length; i++) { + const isImport = i < statements.length && statements[i].kind === SyntaxKind.ImportStatement; + if (isImport) { + if (runStart === -1) { + runStart = i; + } + } else if (runStart !== -1) { + addRange(statements[runStart].pos, statements[i - 1].end, FoldingRangeKind.Imports); + runStart = -1; + } + } + } function addRangesForNode(node: Node) { if (node.kind === SyntaxKind.Doc) { return; // fold doc comments as regular comments @@ -691,16 +708,20 @@ export function createServer( visitChildren(node, addRangesForNode); } return ranges; - function addRange(startPos: number, endPos: number) { + function addRange(startPos: number, endPos: number, kind?: FoldingRangeKind) { const start = file.getLineAndCharacterOfPosition(startPos); const end = file.getLineAndCharacterOfPosition(endPos); if (start.line !== end.line) { - ranges.push({ + const range: FoldingRange = { startLine: start.line, startCharacter: start.character, endLine: end.line, endCharacter: end.character, - }); + }; + if (kind !== undefined) { + range.kind = kind; + } + ranges.push(range); } } } diff --git a/packages/compiler/test/server/folding.test.ts b/packages/compiler/test/server/folding.test.ts index f4a1625cc89..1cc7a13415c 100644 --- a/packages/compiler/test/server/folding.test.ts +++ b/packages/compiler/test/server/folding.test.ts @@ -1,6 +1,6 @@ import { deepStrictEqual } from "assert"; import { it } from "vitest"; -import { FoldingRange } from "vscode-languageserver"; +import { FoldingRange, FoldingRangeKind } from "vscode-languageserver"; import { createTestServerHost } from "../../src/testing/test-server-host.js"; it("includes consecutive single line comments separated by whitespaces in folding range", async () => { @@ -9,7 +9,15 @@ it("includes consecutive single line comments separated by whitespaces in foldin //bar //test`); - deepStrictEqual(ranges, [{ endCharacter: 10, endLine: 4, startCharacter: 0, startLine: 0 }]); + deepStrictEqual(ranges, [ + { + endCharacter: 10, + endLine: 4, + startCharacter: 0, + startLine: 0, + kind: FoldingRangeKind.Comment, + }, + ]); }); it("doesn't fold consecutive single and multi-line comment together", async () => { @@ -21,7 +29,13 @@ it("doesn't fold consecutive single and multi-line comment together", async () = //test`); deepStrictEqual(ranges, [ - { endCharacter: 10, endLine: 6, startCharacter: 4, startLine: 4 }, + { + endCharacter: 10, + endLine: 6, + startCharacter: 4, + startLine: 4, + kind: FoldingRangeKind.Comment, + }, { endCharacter: 6, endLine: 2, startCharacter: 0, startLine: 0 }, ]); }); @@ -46,7 +60,15 @@ it("includes comments in folding range", async () => { const ranges = await getFoldingRanges(`/** description of model foo **/`); - deepStrictEqual(ranges, [{ endCharacter: 7, endLine: 2, startCharacter: 0, startLine: 0 }]); + deepStrictEqual(ranges, [ + { + endCharacter: 7, + endLine: 2, + startCharacter: 0, + startLine: 0, + kind: FoldingRangeKind.Comment, + }, + ]); }); it("does not include one line comments in folding range", async () => { @@ -126,6 +148,33 @@ it("folding range with one line", async () => { deepStrictEqual(ranges, []); }); +it("folds consecutive import statements as an imports range", async () => { + const ranges = await getFoldingRanges(`import "@typespec/http"; +import "@typespec/rest"; +import "@typespec/openapi";`); + deepStrictEqual(ranges, [ + { + endCharacter: 27, + endLine: 2, + startCharacter: 0, + startLine: 0, + kind: FoldingRangeKind.Imports, + }, + ]); +}); + +it("does not fold a single import statement", async () => { + const ranges = await getFoldingRanges(`import "@typespec/http";`); + deepStrictEqual(ranges, []); +}); + +it("does not fold imports separated by another statement", async () => { + const ranges = await getFoldingRanges(`import "@typespec/http"; +namespace Foo; +import "@typespec/rest";`); + deepStrictEqual(ranges, []); +}); + async function getFoldingRanges(source: string): Promise { const testHost = await createTestServerHost(); const textDocument = testHost.addOrUpdateDocument("test/test.tsp", source);