Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/folding-range-kinds-2026-7-23-8-51-0.md
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 26 additions & 5 deletions packages/compiler/src/server/serverlib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
DocumentSymbolParams,
FileChangeType,
FoldingRange,
FoldingRangeKind,
FoldingRangeParams,
Hover,
HoverParams,
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
}
Expand Down
57 changes: 53 additions & 4 deletions packages/compiler/test/server/folding.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 },
]);
});
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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<FoldingRange[]> {
const testHost = await createTestServerHost();
const textDocument = testHost.addOrUpdateDocument("test/test.tsp", source);
Expand Down
Loading