diff --git a/Extension/package.json b/Extension/package.json index b6eb1bfac..dcce0856c 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -1072,6 +1072,12 @@ "description": "%c_cpp.configuration.dimInactiveRegions.description%", "scope": "resource" }, + "C_Cpp.autoFoldInactiveRegions": { + "type": "boolean", + "default": false, + "markdownDescription": "%c_cpp.configuration.autoFoldInactiveRegions.description%", + "scope": "resource" + }, "C_Cpp.inactiveRegionOpacity": { "type": "number", "default": 0.55, @@ -3999,6 +4005,16 @@ "title": "%c_cpp.command.toggleDimInactiveRegions.title%", "category": "C/C++" }, + { + "command": "C_Cpp.FoldAllInactiveRegions", + "title": "%c_cpp.command.foldAllInactiveRegions.title%", + "category": "C/C++" + }, + { + "command": "C_Cpp.UnfoldAllInactiveRegions", + "title": "%c_cpp.command.unfoldAllInactiveRegions.title%", + "category": "C/C++" + }, { "command": "C_Cpp.ResetDatabase", "title": "%c_cpp.command.resetDatabase.title%", @@ -6552,6 +6568,14 @@ "command": "C_Cpp.ToggleDimInactiveRegions", "when": "config.C_Cpp.intelliSenseEngine =~ /^[dD]efault$/" }, + { + "command": "C_Cpp.FoldAllInactiveRegions", + "when": "editorLangId =~ /^(c|(cuda-)?cpp)$/ && config.C_Cpp.intelliSenseEngine =~ /^[dD]efault$/ && config.C_Cpp.codeFolding =~ /^[eE]nabled$/" + }, + { + "command": "C_Cpp.UnfoldAllInactiveRegions", + "when": "editorLangId =~ /^(c|(cuda-)?cpp)$/ && config.C_Cpp.intelliSenseEngine =~ /^[dD]efault$/ && config.C_Cpp.codeFolding =~ /^[eE]nabled$/" + }, { "command": "C_Cpp.ResetDatabase", "when": "!(config.C_Cpp.intelliSenseEngine =~ /^[dD]isabled$/)" diff --git a/Extension/package.nls.json b/Extension/package.nls.json index d10af1bcd..57f924d7e 100644 --- a/Extension/package.nls.json +++ b/Extension/package.nls.json @@ -19,6 +19,8 @@ "c_cpp.command.enableErrorSquiggles.title": "Enable Error Squiggles", "c_cpp.command.disableErrorSquiggles.title": "Disable Error Squiggles", "c_cpp.command.toggleDimInactiveRegions.title": "Toggle Inactive Region Colorization", + "c_cpp.command.foldAllInactiveRegions.title": "Fold All Inactive Regions", + "c_cpp.command.unfoldAllInactiveRegions.title": "Unfold All Inactive Regions", "c_cpp.command.resetDatabase.title": "Reset IntelliSense Database", "c_cpp.command.takeSurvey.title": "Take Survey", "c_cpp.command.restartIntelliSenseForFile.title": "Restart IntelliSense for Active File", @@ -486,6 +488,7 @@ "c_cpp.configuration.autocomplete.disabled.description": "Uses the word-based completion provided by Visual Studio Code.", "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.", "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.", + "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`.", "c_cpp.configuration.inactiveRegionOpacity.markdownDescription": { "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.", "comment": [ diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 77ac15102..22ccc7d96 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -55,6 +55,7 @@ import { CustomConfigurationProvider1, getCustomConfigProviders, isSameProviderE import { DataBinding } from './dataBinding'; import { cachedEditorConfigSettings, getEditorConfigSettings } from './editorConfig'; import { CppSourceStr, clients, configPrefix, initializeIntervalTimer, isWritingCrashCallStack, updateLanguageConfigurations, usesCrashHandler, watchForCrashes } from './extension'; +import { InactiveRegion, InactiveRegionStore, getInactiveRegionStartLines } from './inactiveRegions'; import { LanguageClient } from './languageClient'; import { LocalizeStringParams, getLocaleId, getLocalizedString } from './localization'; import { PersistentFolderState, PersistentState, PersistentWorkspaceState } from './persistentState'; @@ -227,18 +228,13 @@ interface FileChangedParams extends WorkspaceFolderParams { uri: string; } -interface InputRegion { - startLine: number; - startColumn: number; - endLine: number; - endColumn: number; -} - interface DecorationRangesPair { decoration: vscode.TextEditorDecorationType; ranges: vscode.Range[]; } +type InactiveRegionFoldingOperation = 'fold' | 'unfold'; + interface InternalSourceFileConfiguration extends SourceFileConfiguration { compilerArgsLegacy?: string[]; } @@ -362,7 +358,7 @@ export interface IntelliSenseResult { uri: string; fileVersion: number; diagnostics: IntelliSenseDiagnostic[]; - inactiveRegions: InputRegion[]; + inactiveRegions: InactiveRegion[]; semanticTokens: SemanticToken[]; inlayHints: CppInlayHint[]; clearExistingDiagnostics: boolean; @@ -806,6 +802,8 @@ export interface Client { selectTranslationUnit(uri: vscode.Uri, translationUnit: string): Promise; updateActiveDocumentTextOptions(): void; didChangeActiveEditor(editor?: vscode.TextEditor, selection?: Range): Promise; + foldAllInactiveRegions(): Promise; + unfoldAllInactiveRegions(): Promise; restartIntelliSenseForFile(document: vscode.TextDocument): Promise; activate(): void; selectionChanged(selection: Range): Promise; @@ -872,6 +870,7 @@ export function createNullClient(): Client { } export class DefaultClient implements Client { + private static readonly autoFoldedEditors = new WeakSet(); private disposables: vscode.Disposable[] = []; private documentFormattingProviderDisposable: vscode.Disposable | undefined; private formattingRangeProviderDisposable: vscode.Disposable | undefined; @@ -886,6 +885,9 @@ export class DefaultClient implements Client { private workspaceStoragePath: string; private trackedDocuments = new Map(); private inactiveRegionsDecorations = new Map(); + private inactiveRegions = new InactiveRegionStore(); + private pendingInactiveRegionFoldingOperations = new WeakMap(); + private inactiveRegionReportingEnabled: boolean; private settingsTracker: SettingsTracker; private loggingLevel: number = 1; private configurationProvider?: string; @@ -1327,6 +1329,7 @@ export class DefaultClient implements Client { } const rootUri: vscode.Uri | undefined = this.RootUri; + this.inactiveRegionReportingEnabled = new CppSettings(rootUri).inactiveRegionReportingEnabled; this.settingsTracker = new SettingsTracker(rootUri); try { @@ -1477,6 +1480,7 @@ export class DefaultClient implements Client { intelliSenseCacheSize: settings.intelliSenseCacheSize, intelliSenseMemoryLimit: settings.intelliSenseMemoryLimit, dimInactiveRegions: settings.dimInactiveRegions, + codeFolding: settings.codeFolding, suggestSnippets: settings.suggestSnippets, legacyCompilerArgsBehavior: settings.legacyCompilerArgsBehavior, defaultSystemIncludePath: settings.defaultSystemIncludePath, @@ -1849,6 +1853,11 @@ export class DefaultClient implements Client { await this.ready; if (Object.keys(changedSettings).length > 0) { + const inactiveRegionReportingEnabled: boolean = new CppSettings(this.RootUri).inactiveRegionReportingEnabled; + if (inactiveRegionReportingEnabled !== this.inactiveRegionReportingEnabled) { + this.inactiveRegionReportingEnabled = inactiveRegionReportingEnabled; + this.inactiveRegions.clear(); + } if (this === defaultClient) { if (changedSettings.commentContinuationPatterns2 !== undefined) { updateLanguageConfigurations(); @@ -1903,6 +1912,20 @@ export class DefaultClient implements Client { void ui.ShowConfigureIntelliSenseButton(false, this, ConfigurationType.CompilerPath, showButtonSender); } } + if ((changedSettings.autoFoldInactiveRegions !== undefined || changedSettings.codeFolding !== undefined) + && vscode.window.activeTextEditor + && clients.getClientFor(vscode.window.activeTextEditor.document.uri) === this) { + if (changedSettings.codeFolding !== undefined) { + await this.languageClient.refreshFoldingRanges(vscode.window.activeTextEditor.document); + } + await this.tryApplyInactiveRegionFolding(vscode.window.activeTextEditor); + } + if (["dimInactiveRegions", + "inactiveRegionOpacity", + "inactiveRegionForegroundColor", + "inactiveRegionBackgroundColor"].some(setting => setting in changedSettings)) { + this.refreshInactiveRegionDecorations(); + } if (changedSettings.legacyCompilerArgsBehavior !== undefined) { this.configuration.handleConfigurationChange(); } @@ -1997,7 +2020,9 @@ export class DefaultClient implements Client { const oldVersion: number | undefined = openFileVersions.get(textDocumentChangeEvent.document.uri.toString()); const newVersion: number = textDocumentChangeEvent.document.version; if (oldVersion === undefined || newVersion > oldVersion) { - openFileVersions.set(textDocumentChangeEvent.document.uri.toString(), newVersion); + const uri: string = textDocumentChangeEvent.document.uri.toString(); + openFileVersions.set(uri, newVersion); + this.inactiveRegions.delete(uri); } } } @@ -2006,6 +2031,7 @@ export class DefaultClient implements Client { if (document.uri.scheme === "file") { const uri: string = document.uri.toString(); openFileVersions.set(uri, document.version); + this.inactiveRegions.delete(uri); } } @@ -2017,6 +2043,8 @@ export class DefaultClient implements Client { if (this.inlayHintsProvider) { this.inlayHintsProvider.removeFile(uri); } + this.inactiveRegions.delete(uri); + this.inactiveRegionsDecorations.get(uri)?.decoration.dispose(); this.inactiveRegionsDecorations.delete(uri); if (diagnosticsCollectionIntelliSense) { diagnosticsCollectionIntelliSense.delete(document.uri); @@ -2615,7 +2643,8 @@ export class DefaultClient implements Client { this.inlayHintsProvider.deliverInlayHints(intelliSenseResult.uri, intelliSenseResult.inlayHints, intelliSenseResult.clearExistingInlayHint); } - this.updateInactiveRegions(intelliSenseResult.uri, intelliSenseResult.inactiveRegions, intelliSenseResult.clearExistingInactiveRegions); + this.updateInactiveRegions(intelliSenseResult.uri, intelliSenseResult.inactiveRegions, + intelliSenseResult.clearExistingInactiveRegions, intelliSenseResult.isCompletePass); if (intelliSenseResult.clearExistingDiagnostics || intelliSenseResult.diagnostics.length > 0) { this.updateSquiggles(intelliSenseResult.uri, intelliSenseResult.diagnostics, intelliSenseResult.clearExistingDiagnostics); } @@ -2927,12 +2956,24 @@ export class DefaultClient implements Client { this.model.isParsingWorkspacePaused.Value = tagParseStatus.isPaused; } - private updateInactiveRegions(uriString: string, inactiveRegions: InputRegion[], startNewSet: boolean): void { + private updateInactiveRegions(uriString: string, inactiveRegions: InactiveRegion[], startNewSet: boolean, isCompletePass: boolean): void { const client: Client = clients.getClientFor(vscode.Uri.parse(uriString)); - if (!(client instanceof DefaultClient) || (!startNewSet && inactiveRegions.length === 0)) { + if (!(client instanceof DefaultClient) || (!startNewSet && inactiveRegions.length === 0 && !isCompletePass)) { return; } + client.inactiveRegions.update(uriString, inactiveRegions, startNewSet, isCompletePass); + const settings: CppSettings = new CppSettings(client.RootUri); + client.updateInactiveRegionDecorations(uriString, inactiveRegions, startNewSet, settings); + + const activeEditor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (isCompletePass && activeEditor?.document.uri.toString() === uriString) { + void client.tryApplyInactiveRegionFolding(activeEditor).catch(logAndReturn.undefined); + } + } + + private updateInactiveRegionDecorations(uriString: string, inactiveRegions: readonly InactiveRegion[], + startNewSet: boolean, settings: CppSettings): void { const dimInactiveRegions: boolean = settings.dimInactiveRegions; let currentSet: DecorationRangesPair | undefined = this.inactiveRegionsDecorations.get(uriString); if (startNewSet || !dimInactiveRegions) { @@ -2940,32 +2981,109 @@ export class DefaultClient implements Client { currentSet.decoration.dispose(); this.inactiveRegionsDecorations.delete(uriString); } - if (!dimInactiveRegions) { - return; - } currentSet = undefined; } - if (currentSet === undefined) { - const opacity: number | undefined = settings.inactiveRegionOpacity; - currentSet = { - decoration: vscode.window.createTextEditorDecorationType({ - opacity: (opacity === undefined) ? "0.55" : opacity.toString(), - backgroundColor: settings.inactiveRegionBackgroundColor, - color: settings.inactiveRegionForegroundColor, - rangeBehavior: vscode.DecorationRangeBehavior.OpenOpen - }), - ranges: [] - }; - this.inactiveRegionsDecorations.set(uriString, currentSet); + if (dimInactiveRegions) { + if (currentSet === undefined) { + const opacity: number | undefined = settings.inactiveRegionOpacity; + currentSet = { + decoration: vscode.window.createTextEditorDecorationType({ + opacity: (opacity === undefined) ? "0.55" : opacity.toString(), + backgroundColor: settings.inactiveRegionBackgroundColor, + color: settings.inactiveRegionForegroundColor, + rangeBehavior: vscode.DecorationRangeBehavior.OpenOpen + }), + ranges: [] + }; + this.inactiveRegionsDecorations.set(uriString, currentSet); + } + + Array.prototype.push.apply(currentSet.ranges, inactiveRegions.map(element => + new vscode.Range(element.startLine, element.startColumn, element.endLine, element.endColumn))); + + // Apply the decorations to all *visible* text editors + const editors: vscode.TextEditor[] = vscode.window.visibleTextEditors.filter(e => e.document.uri.toString() === uriString); + for (const e of editors) { + e.setDecorations(currentSet.decoration, currentSet.ranges); + } + } + } + + private refreshInactiveRegionDecorations(): void { + const settings: CppSettings = new CppSettings(this.RootUri); + if (!settings.dimInactiveRegions) { + this.inactiveRegionsDecorations.forEach(currentSet => currentSet.decoration.dispose()); + this.inactiveRegionsDecorations.clear(); + return; + } + + this.inactiveRegions.forEach((inactiveRegions, uriString) => + this.updateInactiveRegionDecorations(uriString, inactiveRegions, true, settings)); + } + + public foldAllInactiveRegions(): Promise { + return this.applyInactiveRegionFolding('fold'); + } + + public unfoldAllInactiveRegions(): Promise { + return this.applyInactiveRegionFolding('unfold'); + } + + private async applyInactiveRegionFolding(operation: InactiveRegionFoldingOperation): Promise { + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + if (!editor || !util.isCpp(editor.document)) { + return; + } + + this.pendingInactiveRegionFoldingOperations.set(editor, operation); + await this.tryApplyInactiveRegionFolding(editor); + } + + private async tryApplyInactiveRegionFolding(editor: vscode.TextEditor): Promise { + if (vscode.window.activeTextEditor !== editor || !util.isCpp(editor.document)) { + return; + } + + const uri: string = editor.document.uri.toString(); + const inactiveRegions: readonly InactiveRegion[] | undefined = this.inactiveRegions.getComplete(uri); + if (inactiveRegions === undefined) { + return; + } + + const settings: CppSettings = new CppSettings(editor.document.uri); + const autoFold: boolean = settings.autoFoldInactiveRegions && !DefaultClient.autoFoldedEditors.has(editor); + const pendingOperation: InactiveRegionFoldingOperation | undefined = + this.pendingInactiveRegionFoldingOperations.get(editor); + const operation: InactiveRegionFoldingOperation | undefined = pendingOperation ?? (autoFold ? 'fold' : undefined); + if (operation === undefined) { + return; + } + + this.pendingInactiveRegionFoldingOperations.delete(editor); + + if (autoFold) { + DefaultClient.autoFoldedEditors.add(editor); } - Array.prototype.push.apply(currentSet.ranges, inactiveRegions.map(element => - new vscode.Range(element.startLine, element.startColumn, element.endLine, element.endColumn))); + const selectionLines: number[] = getInactiveRegionStartLines(inactiveRegions); + if (selectionLines.length === 0) { + return; + } - // Apply the decorations to all *visible* text editors - const editors: vscode.TextEditor[] = vscode.window.visibleTextEditors.filter(e => e.document.uri.toString() === uriString); - for (const e of editors) { - e.setDecorations(currentSet.decoration, currentSet.ranges); + try { + await this.languageClient.refreshFoldingRanges(editor.document); + await vscode.commands.executeCommand(`editor.${operation}`, { + selectionLines, + direction: 'down' + }); + } catch (error) { + if (autoFold) { + DefaultClient.autoFoldedEditors.delete(editor); + } + if (pendingOperation) { + this.pendingInactiveRegionFoldingOperations.set(editor, pendingOperation); + } + throw error; } } @@ -3136,6 +3254,7 @@ export class DefaultClient implements Client { return; } + void this.tryApplyInactiveRegionFolding(editor).catch(logAndReturn.undefined); this.updateActiveDocumentTextOptions(); const params: DidChangeActiveEditorParams = { @@ -4232,6 +4351,8 @@ export class DefaultClient implements Client { } public dispose(): void { + this.inactiveRegionsDecorations.forEach(currentSet => currentSet.decoration.dispose()); + this.inactiveRegionsDecorations.clear(); this.pendingTagParsingCalls.forEach(pendingCall => { if (pendingCall.timer) { clearTimeout(pendingCall.timer); @@ -4447,6 +4568,8 @@ class NullClient implements Client { selectTranslationUnit(uri: vscode.Uri, translationUnit: string): Promise { return Promise.resolve(); } updateActiveDocumentTextOptions(): void { } didChangeActiveEditor(editor?: vscode.TextEditor): Promise { return Promise.resolve(); } + foldAllInactiveRegions(): Promise { return Promise.resolve(); } + unfoldAllInactiveRegions(): Promise { return Promise.resolve(); } restartIntelliSenseForFile(document: vscode.TextDocument): Promise { return Promise.resolve(); } activate(): void { } selectionChanged(selection: Range): Promise { return Promise.resolve(); } diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 8ae2125f4..653f620e5 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -396,6 +396,8 @@ export async function registerCommands(enabled: boolean): Promise { commandDisposables.push(vscode.commands.registerCommand('C_Cpp.EnableErrorSquiggles', enabled ? onEnableSquiggles : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.DisableErrorSquiggles', enabled ? onDisableSquiggles : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ToggleDimInactiveRegions', enabled ? onToggleDimInactiveRegions : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.FoldAllInactiveRegions', enabled ? onFoldAllInactiveRegions : onDisabledCommand)); + commandDisposables.push(vscode.commands.registerCommand('C_Cpp.UnfoldAllInactiveRegions', enabled ? onUnfoldAllInactiveRegions : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.PauseParsing', enabled ? onPauseParsing : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.ResumeParsing', enabled ? onResumeParsing : onDisabledCommand)); commandDisposables.push(vscode.commands.registerCommand('C_Cpp.PauseCodeAnalysis', enabled ? onPauseCodeAnalysis : onDisabledCommand)); @@ -925,6 +927,16 @@ function onToggleDimInactiveRegions(): void { settings.update("dimInactiveRegions", !settings.dimInactiveRegions); } +function onFoldAllInactiveRegions(): Promise { + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + return editor ? clients.getClientFor(editor.document.uri).foldAllInactiveRegions() : Promise.resolve(); +} + +function onUnfoldAllInactiveRegions(): Promise { + const editor: vscode.TextEditor | undefined = vscode.window.activeTextEditor; + return editor ? clients.getClientFor(editor.document.uri).unfoldAllInactiveRegions() : Promise.resolve(); +} + function onPauseParsing(): void { void clients.ActiveClient.pauseParsing().catch(logAndReturn.undefined); } diff --git a/Extension/src/LanguageServer/inactiveRegions.ts b/Extension/src/LanguageServer/inactiveRegions.ts new file mode 100644 index 000000000..b05d23397 --- /dev/null +++ b/Extension/src/LanguageServer/inactiveRegions.ts @@ -0,0 +1,57 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +export interface InactiveRegion { + startLine: number; + startColumn: number; + endLine: number; + endColumn: number; +} + +interface InactiveRegionSet { + regions: InactiveRegion[]; + isComplete: boolean; +} + +export class InactiveRegionStore { + private readonly regionSets = new Map(); + + public update(uri: string, regions: InactiveRegion[], startNewSet: boolean, isComplete: boolean): void { + let regionSet: InactiveRegionSet | undefined = this.regionSets.get(uri); + if (startNewSet || regionSet === undefined) { + regionSet = { + regions: [], + isComplete: false + }; + this.regionSets.set(uri, regionSet); + } + + for (const region of regions) { + regionSet.regions.push(region); + } + regionSet.isComplete = isComplete; + } + + public getComplete(uri: string): readonly InactiveRegion[] | undefined { + const regionSet: InactiveRegionSet | undefined = this.regionSets.get(uri); + return regionSet?.isComplete ? regionSet.regions : undefined; + } + + public forEach(callback: (regions: readonly InactiveRegion[], uri: string) => void): void { + this.regionSets.forEach((regionSet, uri) => callback(regionSet.regions, uri)); + } + + public clear(): void { + this.regionSets.clear(); + } + + public delete(uri: string): void { + this.regionSets.delete(uri); + } +} + +export function getInactiveRegionStartLines(regions: readonly InactiveRegion[]): number[] { + return [...new Set(regions.map(region => region.startLine))].sort((a, b) => a - b); +} diff --git a/Extension/src/LanguageServer/languageClient.ts b/Extension/src/LanguageServer/languageClient.ts index 255490fdd..63dd88b9c 100644 --- a/Extension/src/LanguageServer/languageClient.ts +++ b/Extension/src/LanguageServer/languageClient.ts @@ -4,8 +4,8 @@ * ------------------------------------------------------------------------------------------ */ 'use strict'; -import { Disposable } from 'vscode'; -import { CancellationToken, NotificationHandler, NotificationType, RequestType } from 'vscode-languageclient'; +import { Disposable, TextDocument } from 'vscode'; +import { CancellationToken, FoldingRangeRequest, NotificationHandler, NotificationType, RequestType } from 'vscode-languageclient'; import * as rpc from 'vscode-languageclient/node'; import { ManualSignal } from '../Utility/Async/manualSignal'; @@ -71,6 +71,11 @@ export class LanguageClient { return this.rpcClient.code2ProtocolConverter; } + public async refreshFoldingRanges(document: TextDocument): Promise { + await this.ready; + this.rpcClient.getFeature(FoldingRangeRequest.method).getProvider(document)?.onDidChangeFoldingRange.fire(); + } + public async sendRequest(type: RequestType, params: P, token?: CancellationToken): Promise { await this.ready; return this.rpcClient.sendRequest(type, params, token); diff --git a/Extension/src/LanguageServer/settings.ts b/Extension/src/LanguageServer/settings.ts index a18b2cd61..2a394cd62 100644 --- a/Extension/src/LanguageServer/settings.ts +++ b/Extension/src/LanguageServer/settings.ts @@ -49,6 +49,7 @@ export interface WorkspaceFolderSettingsParams { intelliSenseCacheSize: number; intelliSenseMemoryLimit: number; dimInactiveRegions: boolean; + codeFolding: boolean; suggestSnippets: boolean; legacyCompilerArgsBehavior: boolean; defaultSystemIncludePath: string[] | undefined; @@ -377,6 +378,15 @@ export class CppSettings extends Settings { public get inactiveRegionOpacity(): number { return this.getAsNumber("inactiveRegionOpacity"); } public get inactiveRegionForegroundColor(): string | undefined { return changeBlankStringToUndefined(this.getAsStringOrUndefined("inactiveRegionForegroundColor")); } public get inactiveRegionBackgroundColor(): string | undefined { return changeBlankStringToUndefined(this.getAsStringOrUndefined("inactiveRegionBackgroundColor")); } + public get autoFoldInactiveRegions(): boolean { + return this.getAsBoolean("autoFoldInactiveRegions") + && this.codeFolding + && this.intelliSenseEngine.toLowerCase() === "default"; + } + public get inactiveRegionReportingEnabled(): boolean { + return this.dimInactiveRegions + || (this.codeFolding && this.intelliSenseEngine.toLowerCase() === "default"); + } public get autocomplete(): string { return this.getAsString("autocomplete"); } public get autocompleteAddParentheses(): boolean { return this.getAsBoolean("autocompleteAddParentheses"); } public get loggingLevel(): string { return this.getAsString("loggingLevel"); } diff --git a/Extension/test/minBinaryVersion.json b/Extension/test/minBinaryVersion.json index 1a1f20762..26f354be5 100644 --- a/Extension/test/minBinaryVersion.json +++ b/Extension/test/minBinaryVersion.json @@ -1,3 +1,3 @@ { - "minBinaryVersion": "1.33.0" + "minBinaryVersion": "1.35.3" } diff --git a/Extension/test/scenarios/MultirootDeadlockTest/tests/inactiveFolding.test.ts b/Extension/test/scenarios/MultirootDeadlockTest/tests/inactiveFolding.test.ts new file mode 100644 index 000000000..f97597c64 --- /dev/null +++ b/Extension/test/scenarios/MultirootDeadlockTest/tests/inactiveFolding.test.ts @@ -0,0 +1,139 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +/* eslint-disable @typescript-eslint/triple-slash-reference */ +/// + +import * as assert from 'assert'; +import { suite } from 'mocha'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import * as api from 'vscode-cpptools'; +import * as apit from 'vscode-cpptools/out/testApi'; +import * as extension from '../../../../src/LanguageServer/extension'; +import * as testHelpers from '../../../common/testHelpers'; + +suite("Inactive region folding in a multi-root workspace", function (): void { + let testHook: apit.CppToolsTestHook; + let firstWorkspaceFolder: vscode.WorkspaceFolder; + let workspaceFolder: vscode.WorkspaceFolder; + + suiteSetup(async function (): Promise { + await testHelpers.activateCppExtension(); + const cpptools: apit.CppToolsTestApi = await apit.getCppToolsTestApi(api.Version.latest) + ?? assert.fail("Could not get CppToolsTestApi"); + testHook = cpptools.getTestHook(); + firstWorkspaceFolder = vscode.workspace.workspaceFolders?.[0] + ?? assert.fail("First workspace folder is unavailable"); + workspaceFolder = vscode.workspace.workspaceFolders?.[1] + ?? assert.fail("Second workspace folder is unavailable"); + }); + + suiteTeardown(function (): void { + testHook.dispose(); + }); + + test("routes manual folding operations by URI during a workspace client switch", async () => { + const configuration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", workspaceFolder.uri); + const previousCodeFoldingValue: string | undefined = configuration.inspect("codeFolding")?.globalValue; + await configuration.update("codeFolding", "enabled", vscode.ConfigurationTarget.Global); + let releaseEditorChange: (() => void) | undefined; + let editorChangeStub: sinon.SinonStub | undefined; + let editorChangePromise: Promise | undefined; + let editor: vscode.TextEditor | undefined; + let firstEditor: vscode.TextEditor | undefined; + try { + editor = await openFileAndWaitForIntelliSense(); + await vscode.commands.executeCommand("editor.unfoldAll"); + + const firstDocument: vscode.TextDocument = await vscode.workspace.openTextDocument( + path.join(firstWorkspaceFolder.uri.fsPath, "test.cpp")); + firstEditor = await vscode.window.showTextDocument(firstDocument); + await extension.clients.didChangeActiveEditor(firstEditor); + const firstClient = extension.clients.ActiveClient; + const owner = extension.clients.getClientFor(editor.document.uri); + assert.notStrictEqual(firstClient, owner); + + const pendingEditorChange: Promise = new Promise(resolve => releaseEditorChange = resolve); + editorChangeStub = sinon.stub(owner, "didChangeActiveEditor").returns(pendingEditorChange); + const activeEditor: vscode.TextEditor = await vscode.window.showTextDocument(editor.document, editor.viewColumn, false); + editorChangePromise = extension.clients.didChangeActiveEditor(activeEditor); + assert.strictEqual(extension.clients.ActiveClient, firstClient); + + await vscode.commands.executeCommand("C_Cpp.FoldAllInactiveRegions"); + await assertInactiveBranchIsFolded(editor); + + await vscode.commands.executeCommand("C_Cpp.UnfoldAllInactiveRegions"); + await assertInactiveBranchIsUnfolded(editor); + } finally { + releaseEditorChange?.(); + await editorChangePromise; + editorChangeStub?.restore(); + if (editor) { + await closeEditor(editor); + } + if (firstEditor) { + await closeEditor(firstEditor); + } + await testHelpers.delay(150); + await configuration.update("codeFolding", previousCodeFoldingValue, vscode.ConfigurationTarget.Global); + } + }); + + async function closeEditor(editor: vscode.TextEditor): Promise { + await vscode.window.showTextDocument(editor.document, editor.viewColumn, false); + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + } + + async function assertInactiveBranchIsFolded(editor: vscode.TextEditor): Promise { + let line: number = 24; + for (let i: number = 0; i < 20; ++i) { + const activeEditor: vscode.TextEditor = await vscode.window.showTextDocument(editor.document, editor.viewColumn, false); + activeEditor.selection = new vscode.Selection(24, 0, 24, 0); + await vscode.commands.executeCommand("cursorMove", { to: "down", by: "wrappedLine", value: 1 }); + line = activeEditor.selection.active.line; + if (line === 28) { + return; + } + await testHelpers.delay(50); + } + + assert.strictEqual(line, 28); + } + + async function assertInactiveBranchIsUnfolded(editor: vscode.TextEditor): Promise { + const activeEditor: vscode.TextEditor = await vscode.window.showTextDocument(editor.document, editor.viewColumn, false); + activeEditor.selection = new vscode.Selection(24, 0, 24, 0); + await vscode.commands.executeCommand("cursorMove", { to: "down", by: "wrappedLine", value: 1 }); + assert.strictEqual(activeEditor.selection.active.line, 25); + } + + function waitForIntelliSenseReady(fileName: string): Promise { + return new Promise((resolve, reject) => { + const timeout: NodeJS.Timeout = setTimeout(() => { + listener.dispose(); + reject(new Error(`Timed out waiting for IntelliSense for ${fileName}`)); + }, testHelpers.defaultTimeout); + const listener: vscode.Disposable = testHook.IntelliSenseStatusChanged(result => { + if (result.filename === fileName && result.status === apit.Status.IntelliSenseReady) { + clearTimeout(timeout); + listener.dispose(); + resolve(); + } + }); + }); + } + + async function openFileAndWaitForIntelliSense(): Promise { + const fileName: string = "code_folding.cpp"; + const filePath: string = path.join(workspaceFolder.uri.fsPath, fileName); + const ready: Promise = waitForIntelliSenseReady(fileName); + const document: vscode.TextDocument = await vscode.workspace.openTextDocument(filePath); + const editor: vscode.TextEditor = await vscode.window.showTextDocument(document); + await ready; + return editor; + } +}); diff --git a/Extension/test/scenarios/SingleRootProject/tests/inactiveFolding.test.ts b/Extension/test/scenarios/SingleRootProject/tests/inactiveFolding.test.ts new file mode 100644 index 000000000..f623ef848 --- /dev/null +++ b/Extension/test/scenarios/SingleRootProject/tests/inactiveFolding.test.ts @@ -0,0 +1,338 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +/* eslint-disable @typescript-eslint/triple-slash-reference */ +/// + +import * as assert from 'assert'; +import { suite } from 'mocha'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as api from 'vscode-cpptools'; +import * as apit from 'vscode-cpptools/out/testApi'; +import * as extension from '../../../../src/LanguageServer/extension'; +import * as testHelpers from '../../../common/testHelpers'; + +suite("Inactive region folding", function (): void { + let testHook: apit.CppToolsTestHook; + let workspaceFolder: vscode.WorkspaceFolder; + + suiteSetup(async function (): Promise { + await testHelpers.activateCppExtension(); + const cpptools: apit.CppToolsTestApi = await apit.getCppToolsTestApi(api.Version.latest) + ?? assert.fail("Could not get CppToolsTestApi"); + testHook = cpptools.getTestHook(); + workspaceFolder = vscode.workspace.workspaceFolders?.[0] + ?? assert.fail("No workspace folder available"); + }); + + suiteTeardown(function (): void { + testHook.dispose(); + }); + + test("folds an inactive preprocessor branch instead of its nested function", async () => { + const configuration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", workspaceFolder.uri); + const previousValue: boolean | undefined = configuration.inspect("dimInactiveRegions")?.globalValue; + await configuration.update("dimInactiveRegions", false, vscode.ConfigurationTarget.Global); + try { + const editor: vscode.TextEditor = await openFileAndWaitForIntelliSense(); + await vscode.commands.executeCommand("editor.unfoldAll"); + await vscode.commands.executeCommand("C_Cpp.FoldAllInactiveRegions"); + + await assertInactiveBranchIsFolded(editor); + } finally { + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + await configuration.update("dimInactiveRegions", previousValue, vscode.ConfigurationTarget.Global); + } + }); + + test("automatically folds inactive regions when enabled", async () => { + const configuration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", workspaceFolder.uri); + const previousValue: boolean | undefined = configuration.inspect("autoFoldInactiveRegions")?.globalValue; + await configuration.update("autoFoldInactiveRegions", true, vscode.ConfigurationTarget.Global); + try { + const editor: vscode.TextEditor = await openFileAndWaitForIntelliSense(); + await testHelpers.delay(100); + + await assertInactiveBranchIsFolded(editor); + } finally { + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + await configuration.update("autoFoldInactiveRegions", previousValue, vscode.ConfigurationTarget.Global); + } + }); + + test("manual folding does not suppress later automatic folding", async () => { + const configuration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", workspaceFolder.uri); + const previousAutoFoldValue: boolean | undefined = configuration.inspect("autoFoldInactiveRegions")?.globalValue; + const previousCodeFoldingValue: string | undefined = configuration.inspect("codeFolding")?.globalValue; + await configuration.update("autoFoldInactiveRegions", false, vscode.ConfigurationTarget.Global); + await configuration.update("codeFolding", "enabled", vscode.ConfigurationTarget.Global); + try { + const editor: vscode.TextEditor = await openFileAndWaitForIntelliSense(); + await vscode.commands.executeCommand("editor.unfoldAll"); + await vscode.commands.executeCommand("C_Cpp.FoldAllInactiveRegions"); + await assertInactiveBranchIsFolded(editor); + + await vscode.commands.executeCommand("editor.unfoldAll"); + await assertInactiveBranchIsUnfolded(editor); + await configuration.update("autoFoldInactiveRegions", true, vscode.ConfigurationTarget.Global); + + await assertInactiveBranchIsFolded(editor); + } finally { + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + await configuration.update("autoFoldInactiveRegions", previousAutoFoldValue, vscode.ConfigurationTarget.Global); + await configuration.update("codeFolding", previousCodeFoldingValue, vscode.ConfigurationTarget.Global); + } + }); + + test("unfolds all inactive regions", async () => { + const editor: vscode.TextEditor = await openFileAndWaitForIntelliSense(); + try { + await vscode.commands.executeCommand("C_Cpp.FoldAllInactiveRegions"); + await assertInactiveBranchIsFolded(editor); + + await vscode.commands.executeCommand("C_Cpp.UnfoldAllInactiveRegions"); + await assertInactiveBranchIsUnfolded(editor); + } finally { + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + } + }); + + test("applies a fold command queued before IntelliSense is ready", async () => { + const configuration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp"); + const previousUpdateDelay: number | undefined = configuration.inspect("intelliSenseUpdateDelay")?.globalValue; + await configuration.update("intelliSenseUpdateDelay", 3000, vscode.ConfigurationTarget.Global); + const editor: vscode.TextEditor = await openFileAndWaitForIntelliSense(); + try { + await vscode.commands.executeCommand("editor.unfoldAll"); + let intelliSenseReady: boolean = false; + const ready: Promise = waitForIntelliSenseReady(path.basename(editor.document.fileName)) + .then(() => { + intelliSenseReady = true; + }); + const editApplied: boolean = await editor.edit(editBuilder => + editBuilder.insert(new vscode.Position(0, 0), " ")); + assert.strictEqual(editApplied, true); + assert.strictEqual(intelliSenseReady, false); + + await vscode.commands.executeCommand("C_Cpp.FoldAllInactiveRegions"); + await ready; + await assertInactiveBranchIsFolded(editor); + } finally { + if (editor.document.isDirty) { + await vscode.window.showTextDocument(editor.document, editor.viewColumn, false); + await vscode.commands.executeCommand("undo"); + } + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + await configuration.update("intelliSenseUpdateDelay", previousUpdateDelay, vscode.ConfigurationTarget.Global); + } + }); + + test("applies a queued fold only to the editor that requested it", async () => { + const configuration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp"); + const previousAutoFoldValue: boolean | undefined = configuration.inspect("autoFoldInactiveRegions")?.globalValue; + const previousCodeFoldingValue: string | undefined = configuration.inspect("codeFolding")?.globalValue; + const previousUpdateDelay: number | undefined = configuration.inspect("intelliSenseUpdateDelay")?.globalValue; + await configuration.update("autoFoldInactiveRegions", false, vscode.ConfigurationTarget.Global); + await configuration.update("codeFolding", "enabled", vscode.ConfigurationTarget.Global); + await configuration.update("intelliSenseUpdateDelay", 3000, vscode.ConfigurationTarget.Global); + const firstEditor: vscode.TextEditor = await openFileAndWaitForIntelliSense(); + const secondEditor: vscode.TextEditor = await vscode.window.showTextDocument(firstEditor.document, { + viewColumn: vscode.ViewColumn.Beside, + preserveFocus: false, + preview: false + }); + try { + await vscode.window.showTextDocument(firstEditor.document, firstEditor.viewColumn, false); + await vscode.commands.executeCommand("editor.unfoldAll"); + firstEditor.revealRange(new vscode.Range(24, 0, 25, 0), vscode.TextEditorRevealType.InCenter); + await vscode.window.showTextDocument(secondEditor.document, secondEditor.viewColumn, false); + await vscode.commands.executeCommand("editor.unfoldAll"); + secondEditor.revealRange(new vscode.Range(24, 0, 25, 0), vscode.TextEditorRevealType.InCenter); + await assertLineVisibility(firstEditor, 25, true); + await assertLineVisibility(secondEditor, 25, true); + await vscode.window.showTextDocument(firstEditor.document, firstEditor.viewColumn, false); + + const ready: Promise = waitForIntelliSenseReady(path.basename(firstEditor.document.fileName)); + const editApplied: boolean = await firstEditor.edit(editBuilder => + editBuilder.insert(new vscode.Position(0, 0), " ")); + assert.strictEqual(editApplied, true); + await vscode.commands.executeCommand("C_Cpp.FoldAllInactiveRegions"); + await vscode.window.showTextDocument(secondEditor.document, secondEditor.viewColumn, false); + + await ready; + await assertLineVisibility(secondEditor, 25, true); + await vscode.window.showTextDocument(firstEditor.document, firstEditor.viewColumn, false); + await assertLineVisibility(firstEditor, 25, false); + await assertLineVisibility(secondEditor, 25, true); + } finally { + if (firstEditor.document.isDirty) { + await vscode.window.showTextDocument(firstEditor.document, firstEditor.viewColumn, false); + await vscode.commands.executeCommand("undo"); + } + await closeEditor(firstEditor); + await closeEditor(secondEditor); + await testHelpers.delay(150); + await configuration.update("autoFoldInactiveRegions", previousAutoFoldValue, vscode.ConfigurationTarget.Global); + await configuration.update("codeFolding", previousCodeFoldingValue, vscode.ConfigurationTarget.Global); + await configuration.update("intelliSenseUpdateDelay", previousUpdateDelay, vscode.ConfigurationTarget.Global); + } + }); + + test("does not automatically refold an editor after client recovery", async () => { + const configuration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", workspaceFolder.uri); + const previousAutoFoldValue: boolean | undefined = configuration.inspect("autoFoldInactiveRegions")?.globalValue; + const previousCodeFoldingValue: string | undefined = configuration.inspect("codeFolding")?.globalValue; + await configuration.update("autoFoldInactiveRegions", true, vscode.ConfigurationTarget.Global); + await configuration.update("codeFolding", "enabled", vscode.ConfigurationTarget.Global); + const editor: vscode.TextEditor = await openFileAndWaitForIntelliSense(); + try { + await assertInactiveBranchIsFolded(editor); + await vscode.commands.executeCommand("editor.unfoldAll"); + await assertInactiveBranchIsUnfolded(editor); + + const ready: Promise = waitForIntelliSenseReady(path.basename(editor.document.fileName)); + await extension.clients.recreateClients(); + await ready; + + await assertInactiveBranchIsUnfolded(editor); + } finally { + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + await configuration.update("autoFoldInactiveRegions", previousAutoFoldValue, vscode.ConfigurationTarget.Global); + await configuration.update("codeFolding", previousCodeFoldingValue, vscode.ConfigurationTarget.Global); + } + }); + + test("does not automatically fold regions introduced after an empty initial result", async () => { + const configuration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", workspaceFolder.uri); + const previousAutoFoldValue: boolean | undefined = configuration.inspect("autoFoldInactiveRegions")?.globalValue; + const previousCodeFoldingValue: string | undefined = configuration.inspect("codeFolding")?.globalValue; + await configuration.update("autoFoldInactiveRegions", true, vscode.ConfigurationTarget.Global); + await configuration.update("codeFolding", "enabled", vscode.ConfigurationTarget.Global); + const editor: vscode.TextEditor = await openFileAndWaitForIntelliSense("main.cpp"); + try { + const ready: Promise = waitForIntelliSenseReady(path.basename(editor.document.fileName)); + const editApplied: boolean = await editor.edit(editBuilder => editBuilder.insert( + new vscode.Position(0, 0), + "#if 0\nint inactive()\n{\n return 0;\n}\n#endif\n\n")); + assert.strictEqual(editApplied, true); + await ready; + await testHelpers.delay(100); + + const activeEditor: vscode.TextEditor = await vscode.window.showTextDocument(editor.document, editor.viewColumn, false); + activeEditor.selection = new vscode.Selection(0, 0, 0, 0); + await vscode.commands.executeCommand("cursorMove", { to: "down", by: "wrappedLine", value: 1 }); + assert.strictEqual(activeEditor.selection.active.line, 1); + } finally { + if (editor.document.isDirty) { + await vscode.window.showTextDocument(editor.document, editor.viewColumn, false); + await vscode.commands.executeCommand("undo"); + } + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + await configuration.update("autoFoldInactiveRegions", previousAutoFoldValue, vscode.ConfigurationTarget.Global); + await configuration.update("codeFolding", previousCodeFoldingValue, vscode.ConfigurationTarget.Global); + } + }); + + test("automatically folds when code folding is enabled later", async () => { + const configuration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", workspaceFolder.uri); + const previousAutoFoldValue: boolean | undefined = configuration.inspect("autoFoldInactiveRegions")?.globalValue; + const previousCodeFoldingValue: string | undefined = configuration.inspect("codeFolding")?.globalValue; + await configuration.update("autoFoldInactiveRegions", true, vscode.ConfigurationTarget.Global); + await configuration.update("codeFolding", "disabled", vscode.ConfigurationTarget.Global); + try { + const editor: vscode.TextEditor = await openFileAndWaitForIntelliSense(); + await vscode.commands.executeCommand("editor.unfoldAll"); + await testHelpers.delay(100); + + await assertInactiveBranchIsUnfolded(editor); + await configuration.update("codeFolding", "enabled", vscode.ConfigurationTarget.Global); + await assertInactiveBranchIsFolded(editor); + } finally { + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + await testHelpers.delay(150); + await configuration.update("autoFoldInactiveRegions", previousAutoFoldValue, vscode.ConfigurationTarget.Global); + await configuration.update("codeFolding", previousCodeFoldingValue, vscode.ConfigurationTarget.Global); + } + }); + + async function assertInactiveBranchIsFolded(editor: vscode.TextEditor): Promise { + let line: number = 24; + for (let i: number = 0; i < 20; ++i) { + line = await moveCursorDown(editor); + if (line === 28) { + return; + } + await testHelpers.delay(50); + } + + assert.strictEqual(line, 28); + } + + async function assertInactiveBranchIsUnfolded(editor: vscode.TextEditor): Promise { + let line: number = 24; + for (let i: number = 0; i < 20; ++i) { + line = await moveCursorDown(editor); + if (line === 25) { + return; + } + await testHelpers.delay(50); + } + + assert.strictEqual(line, 25); + } + + async function moveCursorDown(editor: vscode.TextEditor): Promise { + const activeEditor: vscode.TextEditor = await vscode.window.showTextDocument(editor.document, editor.viewColumn, false); + await vscode.commands.executeCommand("workbench.action.focusActiveEditorGroup"); + activeEditor.selection = new vscode.Selection(24, 0, 24, 0); + await vscode.commands.executeCommand("cursorMove", { to: "down", by: "wrappedLine", value: 1 }); + return activeEditor.selection.active.line; + } + + async function assertLineVisibility(editor: vscode.TextEditor, line: number, expected: boolean): Promise { + const position: vscode.Position = new vscode.Position(line, 0); + let visible: boolean = false; + for (let i: number = 0; i < 20; ++i) { + visible = editor.visibleRanges.some(range => range.contains(position)); + if (visible === expected) { + return; + } + await testHelpers.delay(50); + } + + assert.strictEqual(visible, expected); + } + + async function closeEditor(editor: vscode.TextEditor): Promise { + await vscode.window.showTextDocument(editor.document, editor.viewColumn, false); + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + } + + function waitForIntelliSenseReady(fileName: string): Promise { + return new Promise((resolve, reject) => { + const timeout: NodeJS.Timeout = setTimeout(() => { + listener.dispose(); + reject(new Error(`Timed out waiting for IntelliSense for ${fileName}`)); + }, testHelpers.defaultTimeout); + const listener: vscode.Disposable = testHook.IntelliSenseStatusChanged(result => { + if (result.filename === fileName && result.status === apit.Status.IntelliSenseReady) { + clearTimeout(timeout); + listener.dispose(); + resolve(); + } + }); + }); + } + + async function openFileAndWaitForIntelliSense(fileName: string = "code_folding.cpp"): Promise { + const filePath: string = path.join(workspaceFolder.uri.fsPath, fileName); + const ready: Promise = waitForIntelliSenseReady(fileName); + const document: vscode.TextDocument = await vscode.workspace.openTextDocument(filePath); + const editor: vscode.TextEditor = await vscode.window.showTextDocument(document); + await ready; + return editor; + } +}); diff --git a/Extension/test/unit/inactiveRegions.test.ts b/Extension/test/unit/inactiveRegions.test.ts new file mode 100644 index 000000000..bd4e81c9f --- /dev/null +++ b/Extension/test/unit/inactiveRegions.test.ts @@ -0,0 +1,75 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as assert from 'assert'; +import { suite, test } from 'mocha'; +import { getInactiveRegionStartLines, InactiveRegion, InactiveRegionStore } from '../../src/LanguageServer/inactiveRegions'; + +suite("InactiveRegionStore", () => { + const uri: string = "file:///source.cpp"; + const firstRegion: InactiveRegion = { + startLine: 2, + startColumn: 0, + endLine: 4, + endColumn: 0 + }; + const secondRegion: InactiveRegion = { + startLine: 8, + startColumn: 0, + endLine: 10, + endColumn: 0 + }; + + test("returns regions only after a complete pass", () => { + const store: InactiveRegionStore = new InactiveRegionStore(); + + store.update(uri, [firstRegion], true, false); + assert.strictEqual(store.getComplete(uri), undefined); + + store.update(uri, [secondRegion], false, true); + assert.deepStrictEqual(store.getComplete(uri), [firstRegion, secondRegion]); + }); + + test("replaces regions when a new pass starts", () => { + const store: InactiveRegionStore = new InactiveRegionStore(); + store.update(uri, [firstRegion], true, true); + + store.update(uri, [secondRegion], true, true); + + assert.deepStrictEqual(store.getComplete(uri), [secondRegion]); + }); + + test("deletes cached regions", () => { + const store: InactiveRegionStore = new InactiveRegionStore(); + store.update(uri, [firstRegion], true, true); + + store.delete(uri); + + assert.strictEqual(store.getComplete(uri), undefined); + }); + + test("iterates cached regions", () => { + const store: InactiveRegionStore = new InactiveRegionStore(); + store.update(uri, [firstRegion, secondRegion], true, true); + const entries: [string, readonly InactiveRegion[]][] = []; + + store.forEach((regions, entryUri) => entries.push([entryUri, regions])); + + assert.deepStrictEqual(entries, [[uri, [firstRegion, secondRegion]]]); + }); + + test("clears all cached regions", () => { + const store: InactiveRegionStore = new InactiveRegionStore(); + store.update(uri, [firstRegion], true, true); + + store.clear(); + + assert.strictEqual(store.getComplete(uri), undefined); + }); + + test("returns sorted unique start lines", () => { + assert.deepStrictEqual(getInactiveRegionStartLines([secondRegion, firstRegion, secondRegion]), [2, 8]); + }); +});