Skip to content

Commit 0e056e5

Browse files
authored
Automatically fold inactive regions (#14796)
1 parent 26f1df0 commit 0e056e5

11 files changed

Lines changed: 822 additions & 36 deletions

File tree

‎Extension/package.json‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,12 @@
10721072
"description": "%c_cpp.configuration.dimInactiveRegions.description%",
10731073
"scope": "resource"
10741074
},
1075+
"C_Cpp.autoFoldInactiveRegions": {
1076+
"type": "boolean",
1077+
"default": false,
1078+
"markdownDescription": "%c_cpp.configuration.autoFoldInactiveRegions.description%",
1079+
"scope": "resource"
1080+
},
10751081
"C_Cpp.inactiveRegionOpacity": {
10761082
"type": "number",
10771083
"default": 0.55,
@@ -3999,6 +4005,16 @@
39994005
"title": "%c_cpp.command.toggleDimInactiveRegions.title%",
40004006
"category": "C/C++"
40014007
},
4008+
{
4009+
"command": "C_Cpp.FoldAllInactiveRegions",
4010+
"title": "%c_cpp.command.foldAllInactiveRegions.title%",
4011+
"category": "C/C++"
4012+
},
4013+
{
4014+
"command": "C_Cpp.UnfoldAllInactiveRegions",
4015+
"title": "%c_cpp.command.unfoldAllInactiveRegions.title%",
4016+
"category": "C/C++"
4017+
},
40024018
{
40034019
"command": "C_Cpp.ResetDatabase",
40044020
"title": "%c_cpp.command.resetDatabase.title%",
@@ -6552,6 +6568,14 @@
65526568
"command": "C_Cpp.ToggleDimInactiveRegions",
65536569
"when": "config.C_Cpp.intelliSenseEngine =~ /^[dD]efault$/"
65546570
},
6571+
{
6572+
"command": "C_Cpp.FoldAllInactiveRegions",
6573+
"when": "editorLangId =~ /^(c|(cuda-)?cpp)$/ && config.C_Cpp.intelliSenseEngine =~ /^[dD]efault$/ && config.C_Cpp.codeFolding =~ /^[eE]nabled$/"
6574+
},
6575+
{
6576+
"command": "C_Cpp.UnfoldAllInactiveRegions",
6577+
"when": "editorLangId =~ /^(c|(cuda-)?cpp)$/ && config.C_Cpp.intelliSenseEngine =~ /^[dD]efault$/ && config.C_Cpp.codeFolding =~ /^[eE]nabled$/"
6578+
},
65556579
{
65566580
"command": "C_Cpp.ResetDatabase",
65576581
"when": "!(config.C_Cpp.intelliSenseEngine =~ /^[dD]isabled$/)"

‎Extension/package.nls.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
"c_cpp.command.enableErrorSquiggles.title": "Enable Error Squiggles",
2020
"c_cpp.command.disableErrorSquiggles.title": "Disable Error Squiggles",
2121
"c_cpp.command.toggleDimInactiveRegions.title": "Toggle Inactive Region Colorization",
22+
"c_cpp.command.foldAllInactiveRegions.title": "Fold All Inactive Regions",
23+
"c_cpp.command.unfoldAllInactiveRegions.title": "Unfold All Inactive Regions",
2224
"c_cpp.command.resetDatabase.title": "Reset IntelliSense Database",
2325
"c_cpp.command.takeSurvey.title": "Take Survey",
2426
"c_cpp.command.restartIntelliSenseForFile.title": "Restart IntelliSense for Active File",
@@ -486,6 +488,7 @@
486488
"c_cpp.configuration.autocomplete.disabled.description": "Uses the word-based completion provided by Visual Studio Code.",
487489
"c_cpp.configuration.errorSquiggles.description": "Controls whether suspected compile errors detected by the IntelliSense engine will be reported back to the editor. It also controls whether code analysis warnings are reported if includes can't be found. This setting is ignored by the Tag Parser engine.",
488490
"c_cpp.configuration.dimInactiveRegions.description": "Controls whether inactive preprocessor blocks are colored differently than active code. This setting has no effect if IntelliSense is disabled or if using the Default High Contrast theme.",
491+
"c_cpp.configuration.autoFoldInactiveRegions.description": "Controls whether inactive preprocessor blocks are automatically folded when a file is opened. This setting only applies when `#C_Cpp.intelliSenseEngine#` is `default` and `#C_Cpp.codeFolding#` is `enabled`.",
489492
"c_cpp.configuration.inactiveRegionOpacity.markdownDescription": {
490493
"message": "Controls the opacity of inactive preprocessor blocks. Scales between `0.1` and `1.0`. This setting only applies when inactive region dimming is enabled.",
491494
"comment": [

‎Extension/src/LanguageServer/client.ts‎

Lines changed: 156 additions & 33 deletions
Large diffs are not rendered by default.

‎Extension/src/LanguageServer/extension.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ export async function registerCommands(enabled: boolean): Promise<void> {
396396
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.EnableErrorSquiggles', enabled ? onEnableSquiggles : onDisabledCommand));
397397
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.DisableErrorSquiggles', enabled ? onDisableSquiggles : onDisabledCommand));
398398
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ToggleDimInactiveRegions', enabled ? onToggleDimInactiveRegions : onDisabledCommand));
399+
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.FoldAllInactiveRegions', enabled ? onFoldAllInactiveRegions : onDisabledCommand));
400+
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.UnfoldAllInactiveRegions', enabled ? onUnfoldAllInactiveRegions : onDisabledCommand));
399401
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.PauseParsing', enabled ? onPauseParsing : onDisabledCommand));
400402
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ResumeParsing', enabled ? onResumeParsing : onDisabledCommand));
401403
commandDisposables.push(vscode.commands.registerCommand('C_Cpp.PauseCodeAnalysis', enabled ? onPauseCodeAnalysis : onDisabledCommand));
@@ -925,6 +927,16 @@ function onToggleDimInactiveRegions(): void {
925927
settings.update<boolean>("dimInactiveRegions", !settings.dimInactiveRegions);
926928
}
927929

930+
function onFoldAllInactiveRegions(): Promise<void> {
931+
const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor;
932+
return editor ? clients.getClientFor(editor.document.uri).foldAllInactiveRegions() : Promise.resolve();
933+
}
934+
935+
function onUnfoldAllInactiveRegions(): Promise<void> {
936+
const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor;
937+
return editor ? clients.getClientFor(editor.document.uri).unfoldAllInactiveRegions() : Promise.resolve();
938+
}
939+
928940
function onPauseParsing(): void {
929941
void clients.ActiveClient.pauseParsing().catch(logAndReturn.undefined);
930942
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All Rights Reserved.
3+
* See 'LICENSE' in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
6+
export interface InactiveRegion {
7+
startLine: number;
8+
startColumn: number;
9+
endLine: number;
10+
endColumn: number;
11+
}
12+
13+
interface InactiveRegionSet {
14+
regions: InactiveRegion[];
15+
isComplete: boolean;
16+
}
17+
18+
export class InactiveRegionStore {
19+
private readonly regionSets = new Map<string, InactiveRegionSet>();
20+
21+
public update(uri: string, regions: InactiveRegion[], startNewSet: boolean, isComplete: boolean): void {
22+
let regionSet: InactiveRegionSet | undefined = this.regionSets.get(uri);
23+
if (startNewSet || regionSet === undefined) {
24+
regionSet = {
25+
regions: [],
26+
isComplete: false
27+
};
28+
this.regionSets.set(uri, regionSet);
29+
}
30+
31+
for (const region of regions) {
32+
regionSet.regions.push(region);
33+
}
34+
regionSet.isComplete = isComplete;
35+
}
36+
37+
public getComplete(uri: string): readonly InactiveRegion[] | undefined {
38+
const regionSet: InactiveRegionSet | undefined = this.regionSets.get(uri);
39+
return regionSet?.isComplete ? regionSet.regions : undefined;
40+
}
41+
42+
public forEach(callback: (regions: readonly InactiveRegion[], uri: string) => void): void {
43+
this.regionSets.forEach((regionSet, uri) => callback(regionSet.regions, uri));
44+
}
45+
46+
public clear(): void {
47+
this.regionSets.clear();
48+
}
49+
50+
public delete(uri: string): void {
51+
this.regionSets.delete(uri);
52+
}
53+
}
54+
55+
export function getInactiveRegionStartLines(regions: readonly InactiveRegion[]): number[] {
56+
return [...new Set(regions.map(region => region.startLine))].sort((a, b) => a - b);
57+
}

‎Extension/src/LanguageServer/languageClient.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
* ------------------------------------------------------------------------------------------ */
55
'use strict';
66

7-
import { Disposable } from 'vscode';
8-
import { CancellationToken, NotificationHandler, NotificationType, RequestType } from 'vscode-languageclient';
7+
import { Disposable, TextDocument } from 'vscode';
8+
import { CancellationToken, FoldingRangeRequest, NotificationHandler, NotificationType, RequestType } from 'vscode-languageclient';
99
import * as rpc from 'vscode-languageclient/node';
1010
import { ManualSignal } from '../Utility/Async/manualSignal';
1111

@@ -71,6 +71,11 @@ export class LanguageClient {
7171
return this.rpcClient.code2ProtocolConverter;
7272
}
7373

74+
public async refreshFoldingRanges(document: TextDocument): Promise<void> {
75+
await this.ready;
76+
this.rpcClient.getFeature(FoldingRangeRequest.method).getProvider(document)?.onDidChangeFoldingRange.fire();
77+
}
78+
7479
public async sendRequest<P, R, E>(type: RequestType<P, R, E>, params: P, token?: CancellationToken): Promise<R> {
7580
await this.ready;
7681
return this.rpcClient.sendRequest(type, params, token);

‎Extension/src/LanguageServer/settings.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export interface WorkspaceFolderSettingsParams {
4949
intelliSenseCacheSize: number;
5050
intelliSenseMemoryLimit: number;
5151
dimInactiveRegions: boolean;
52+
codeFolding: boolean;
5253
suggestSnippets: boolean;
5354
legacyCompilerArgsBehavior: boolean;
5455
defaultSystemIncludePath: string[] | undefined;
@@ -377,6 +378,15 @@ export class CppSettings extends Settings {
377378
public get inactiveRegionOpacity(): number { return this.getAsNumber("inactiveRegionOpacity"); }
378379
public get inactiveRegionForegroundColor(): string | undefined { return changeBlankStringToUndefined(this.getAsStringOrUndefined("inactiveRegionForegroundColor")); }
379380
public get inactiveRegionBackgroundColor(): string | undefined { return changeBlankStringToUndefined(this.getAsStringOrUndefined("inactiveRegionBackgroundColor")); }
381+
public get autoFoldInactiveRegions(): boolean {
382+
return this.getAsBoolean("autoFoldInactiveRegions")
383+
&& this.codeFolding
384+
&& this.intelliSenseEngine.toLowerCase() === "default";
385+
}
386+
public get inactiveRegionReportingEnabled(): boolean {
387+
return this.dimInactiveRegions
388+
|| (this.codeFolding && this.intelliSenseEngine.toLowerCase() === "default");
389+
}
380390
public get autocomplete(): string { return this.getAsString("autocomplete"); }
381391
public get autocompleteAddParentheses(): boolean { return this.getAsBoolean("autocompleteAddParentheses"); }
382392
public get loggingLevel(): string { return this.getAsString("loggingLevel"); }
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"minBinaryVersion": "1.33.0"
2+
"minBinaryVersion": "1.35.3"
33
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/* --------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All Rights Reserved.
3+
* See 'LICENSE' in the project root for license information.
4+
* ------------------------------------------------------------------------------------------ */
5+
6+
/* eslint-disable @typescript-eslint/triple-slash-reference */
7+
/// <reference path="../../../../vscode.d.ts" />
8+
9+
import * as assert from 'assert';
10+
import { suite } from 'mocha';
11+
import * as path from 'path';
12+
import * as sinon from 'sinon';
13+
import * as vscode from 'vscode';
14+
import * as api from 'vscode-cpptools';
15+
import * as apit from 'vscode-cpptools/out/testApi';
16+
import * as extension from '../../../../src/LanguageServer/extension';
17+
import * as testHelpers from '../../../common/testHelpers';
18+
19+
suite("Inactive region folding in a multi-root workspace", function (): void {
20+
let testHook: apit.CppToolsTestHook;
21+
let firstWorkspaceFolder: vscode.WorkspaceFolder;
22+
let workspaceFolder: vscode.WorkspaceFolder;
23+
24+
suiteSetup(async function (): Promise<void> {
25+
await testHelpers.activateCppExtension();
26+
const cpptools: apit.CppToolsTestApi = await apit.getCppToolsTestApi(api.Version.latest)
27+
?? assert.fail("Could not get CppToolsTestApi");
28+
testHook = cpptools.getTestHook();
29+
firstWorkspaceFolder = vscode.workspace.workspaceFolders?.[0]
30+
?? assert.fail("First workspace folder is unavailable");
31+
workspaceFolder = vscode.workspace.workspaceFolders?.[1]
32+
?? assert.fail("Second workspace folder is unavailable");
33+
});
34+
35+
suiteTeardown(function (): void {
36+
testHook.dispose();
37+
});
38+
39+
test("routes manual folding operations by URI during a workspace client switch", async () => {
40+
const configuration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", workspaceFolder.uri);
41+
const previousCodeFoldingValue: string | undefined = configuration.inspect<string>("codeFolding")?.globalValue;
42+
await configuration.update("codeFolding", "enabled", vscode.ConfigurationTarget.Global);
43+
let releaseEditorChange: (() => void) | undefined;
44+
let editorChangeStub: sinon.SinonStub | undefined;
45+
let editorChangePromise: Promise<void> | undefined;
46+
let editor: vscode.TextEditor | undefined;
47+
let firstEditor: vscode.TextEditor | undefined;
48+
try {
49+
editor = await openFileAndWaitForIntelliSense();
50+
await vscode.commands.executeCommand("editor.unfoldAll");
51+
52+
const firstDocument: vscode.TextDocument = await vscode.workspace.openTextDocument(
53+
path.join(firstWorkspaceFolder.uri.fsPath, "test.cpp"));
54+
firstEditor = await vscode.window.showTextDocument(firstDocument);
55+
await extension.clients.didChangeActiveEditor(firstEditor);
56+
const firstClient = extension.clients.ActiveClient;
57+
const owner = extension.clients.getClientFor(editor.document.uri);
58+
assert.notStrictEqual(firstClient, owner);
59+
60+
const pendingEditorChange: Promise<void> = new Promise(resolve => releaseEditorChange = resolve);
61+
editorChangeStub = sinon.stub(owner, "didChangeActiveEditor").returns(pendingEditorChange);
62+
const activeEditor: vscode.TextEditor = await vscode.window.showTextDocument(editor.document, editor.viewColumn, false);
63+
editorChangePromise = extension.clients.didChangeActiveEditor(activeEditor);
64+
assert.strictEqual(extension.clients.ActiveClient, firstClient);
65+
66+
await vscode.commands.executeCommand("C_Cpp.FoldAllInactiveRegions");
67+
await assertInactiveBranchIsFolded(editor);
68+
69+
await vscode.commands.executeCommand("C_Cpp.UnfoldAllInactiveRegions");
70+
await assertInactiveBranchIsUnfolded(editor);
71+
} finally {
72+
releaseEditorChange?.();
73+
await editorChangePromise;
74+
editorChangeStub?.restore();
75+
if (editor) {
76+
await closeEditor(editor);
77+
}
78+
if (firstEditor) {
79+
await closeEditor(firstEditor);
80+
}
81+
await testHelpers.delay(150);
82+
await configuration.update("codeFolding", previousCodeFoldingValue, vscode.ConfigurationTarget.Global);
83+
}
84+
});
85+
86+
async function closeEditor(editor: vscode.TextEditor): Promise<void> {
87+
await vscode.window.showTextDocument(editor.document, editor.viewColumn, false);
88+
await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
89+
}
90+
91+
async function assertInactiveBranchIsFolded(editor: vscode.TextEditor): Promise<void> {
92+
let line: number = 24;
93+
for (let i: number = 0; i < 20; ++i) {
94+
const activeEditor: vscode.TextEditor = await vscode.window.showTextDocument(editor.document, editor.viewColumn, false);
95+
activeEditor.selection = new vscode.Selection(24, 0, 24, 0);
96+
await vscode.commands.executeCommand("cursorMove", { to: "down", by: "wrappedLine", value: 1 });
97+
line = activeEditor.selection.active.line;
98+
if (line === 28) {
99+
return;
100+
}
101+
await testHelpers.delay(50);
102+
}
103+
104+
assert.strictEqual(line, 28);
105+
}
106+
107+
async function assertInactiveBranchIsUnfolded(editor: vscode.TextEditor): Promise<void> {
108+
const activeEditor: vscode.TextEditor = await vscode.window.showTextDocument(editor.document, editor.viewColumn, false);
109+
activeEditor.selection = new vscode.Selection(24, 0, 24, 0);
110+
await vscode.commands.executeCommand("cursorMove", { to: "down", by: "wrappedLine", value: 1 });
111+
assert.strictEqual(activeEditor.selection.active.line, 25);
112+
}
113+
114+
function waitForIntelliSenseReady(fileName: string): Promise<void> {
115+
return new Promise<void>((resolve, reject) => {
116+
const timeout: NodeJS.Timeout = setTimeout(() => {
117+
listener.dispose();
118+
reject(new Error(`Timed out waiting for IntelliSense for ${fileName}`));
119+
}, testHelpers.defaultTimeout);
120+
const listener: vscode.Disposable = testHook.IntelliSenseStatusChanged(result => {
121+
if (result.filename === fileName && result.status === apit.Status.IntelliSenseReady) {
122+
clearTimeout(timeout);
123+
listener.dispose();
124+
resolve();
125+
}
126+
});
127+
});
128+
}
129+
130+
async function openFileAndWaitForIntelliSense(): Promise<vscode.TextEditor> {
131+
const fileName: string = "code_folding.cpp";
132+
const filePath: string = path.join(workspaceFolder.uri.fsPath, fileName);
133+
const ready: Promise<void> = waitForIntelliSenseReady(fileName);
134+
const document: vscode.TextDocument = await vscode.workspace.openTextDocument(filePath);
135+
const editor: vscode.TextEditor = await vscode.window.showTextDocument(document);
136+
await ready;
137+
return editor;
138+
}
139+
});

0 commit comments

Comments
 (0)