Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Bug Fixes:
- Update testing framework to fix bugs when running tests of CMake Tools without a reliable internet connection. [#4891](https://github.com/microsoft/vscode-cmake-tools/pull/4891) [@cwalther](https://github.com/cwalther)
- Fix GNU LD diagnostic regex incorrectly matching CMake status lines (e.g., Zephyr build output) as linker errors in the Problems panel. [#4910](https://github.com/microsoft/vscode-cmake-tools/issues/4910)
- Fix “Make it easier for a new developer of CMake Tools to run tests” on Windows. [#4932](https://github.com/microsoft/vscode-cmake-tools/pull/4932) [@cwalther](https://github.com/cwalther)
- Show the CMake Tools activity-bar view immediately with an "initializing" placeholder instead of hiding the entire sidebar until CMake, kit, preset, and Visual Studio developer-environment probing completes. On machines where those probes are intermittently slow (e.g. aggressive antivirus scanning of spawned processes, or extension-host file-handle exhaustion), the sidebar no longer disappears for minutes during activation. Project commands, menus, and status items remain gated on full readiness, so nothing runs against a partially-initialized project. [#5027](https://github.com/microsoft/vscode-cmake-tools/pull/5027)

## 1.23.52

Expand Down
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4392,12 +4392,17 @@
"id": "cmake-view",
"title": "CMake",
"icon": "$(cmake-tools-cmake-view-2)",
"when": "cmake:enableFullFeatureSet"
"when": "cmake:isInitializing || cmake:enableFullFeatureSet"
}
]
},
"views": {
"cmake-view": [
{
"id": "cmake.initializing",
"name": "%cmake-tools.configuration.views.cmake.initializing.description%",
"when": "cmake:isInitializing && !cmake:enableFullFeatureSet"
},
{
"id": "cmake.projectStatus",
"name": "%cmake-tools.configuration.views.cmake.projectStatus.description%",
Expand Down Expand Up @@ -4425,6 +4430,11 @@
"view": "cmake.pinnedCommands",
"contents": "%cmake-tools.configuration.views.cmake.pinnedCommandsWelcome.description%",
"when": "cmake:enableFullFeatureSet"
},
{
"view": "cmake.initializing",
"contents": "%cmake-tools.configuration.views.cmake.initializingWelcome.description%",
"when": "cmake:isInitializing && !cmake:enableFullFeatureSet"
}
],
"yamlValidation": [
Expand Down
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@
]
},
"cmake-tools.configuration.views.cmake.outline.description": "Project Outline",
"cmake-tools.configuration.views.cmake.initializing.description": "CMake",
"cmake-tools.configuration.views.cmake.initializingWelcome.description": "CMake Tools is initializing...\nThe project views will appear once startup completes.",
"cmake-tools.configuration.views.cmake.pinnedCommands.description": "Pinned Commands",
"cmake-tools.configuration.cmake.additionalKits.description": "Array of paths to custom kit files.",
"cmake-tools.configuration.cmake.revealLog.description": "Configures the settings for showing the log and focusing on the output channel.",
Expand Down
46 changes: 46 additions & 0 deletions src/activation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Pure activation policy helpers.
*
* This module is intentionally free of any `vscode` (or other heavy transitive)
* dependency so it can be exercised directly by backend unit tests
* (`yarn backendTests`).
*/

/**
* Decide whether the transient "initializing" placeholder view should be shown in
* the CMake activity-bar container during extension activation.
*
* The placeholder makes the CMake sidebar visible immediately instead of staying
* hidden for the entire (potentially slow) activation. It is only meaningful when
* there is a CMake project to initialize and the project UI is not intentionally
* hidden.
*
* @param hasCMakeProject Whether the workspace has at least one CMake project — a
* non-excluded workspace folder whose configured source directory contains a
* `CMakeLists.txt`.
* @param languageServerOnlyMode Whether the extension is running in
* language-server-only mode, where the project UI (activity-bar views,
* commands, status bar) is intentionally absent.
* @returns `true` when the placeholder should be shown.
*/
export function shouldShowInitializingView(hasCMakeProject: boolean, languageServerOnlyMode: boolean): boolean {
return hasCMakeProject && !languageServerOnlyMode;
}

/**
* Classify a filesystem error encountered while probing for a project's
* `CMakeLists.txt` during the activation preflight.
*
* Only `ENOENT` (no such file or directory) and `ENOTDIR` (a path component is not a
* directory) definitively mean the file is absent. Every other error — notably
* `EMFILE`/`ENFILE` (file-handle exhaustion) and `EACCES` — is transient or ambiguous.
* The preflight must treat those as "unknown" and fail open (show the inert
* placeholder), because they occur in exactly the resource-starved environments where
* the sidebar would otherwise stay hidden for minutes.
*
* @param code The `code` of a Node.js filesystem error (e.g. `NodeJS.ErrnoException.code`).
* @returns `true` only when the error definitively means the file is absent.
*/
export function isDefinitivelyAbsentError(code: string | undefined): boolean {
return code === 'ENOENT' || code === 'ENOTDIR';
}
111 changes: 106 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { cmakeTaskProvider, CMakeTaskProvider } from '@cmt/cmakeTaskProvider';
import * as telemetry from '@cmt/telemetry';
import { ProjectOutline, ProjectNode, TargetNode, SourceFileNode, WorkspaceFolderNode, BaseNode, DirectoryNode, CTestTestNode } from '@cmt/ui/projectOutline/projectOutline';
import { BookmarksProvider, BookmarkNode } from '@cmt/ui/bookmarks';
import { InitializingViewProvider } from '@cmt/ui/initializingView';
import { shouldShowInitializingView, isDefinitivelyAbsentError } from '@cmt/activation';
import * as util from '@cmt/util';
import { ProgressHandle, DummyDisposable, reportProgress, runCommand } from '@cmt/util';
import { DEFAULT_VARIANTS } from '@cmt/kits/variant';
Expand Down Expand Up @@ -62,6 +64,7 @@ let pinnedCommands: PinnedCommands;
const log = logging.createLogger('extension');

const multiProjectModeKey = 'cmake:multiProject';
const initializingContextKey = 'cmake:isInitializing';
export const hideLaunchCommandKey = 'cmake:hideLaunchCommand';
export const hideDebugCommandKey = 'cmake:hideDebugCommand';
export const hideBuildCommandKey = 'cmake:hideBuildCommand';
Expand Down Expand Up @@ -3027,6 +3030,78 @@ class SchemaProvider implements vscode.TextDocumentContentProvider {
}
}

/**
* Cheap, best-effort preflight used only to decide whether to show the transient
* "initializing" placeholder in the CMake activity bar during activation.
*
* Returns true when at least one non-excluded workspace folder plausibly has a CMake
* project. It performs only variable expansion and a single `CMakeLists.txt` stat per
* configured source directory — no project construction, kit/preset initialization,
* CMake resolution, subprocess spawn, or recursive scan — so it stays fast even in the
* slow environments this reveal is meant to mitigate.
*
* It is deliberately FAIL-OPEN: only a definitive "file absent" result
* (`ENOENT`/`ENOTDIR`) counts as "no project". Any transient/ambiguous filesystem error
* — notably `EMFILE` from extension-host file-handle exhaustion, which is exactly the
* condition that makes the sidebar disappear for minutes — shows the (inert) placeholder
* rather than hiding it. `${command:...}` source directories, which cannot be resolved by
* this non-interactive preflight, likewise fail open. A false positive is harmless (the
* placeholder is inert and self-clears once init settles); the authoritative end-of-init
* reveal remains the source of truth for the real project views.
*/
async function workspaceHasCMakeProjectForInitialization(): Promise<boolean> {
for (const folder of vscode.workspace.workspaceFolders ?? []) {
try {
const config = ConfigurationReader.loadConfig(folder);

// Mirror ProjectController.addFolder(): skip folders excluded from CMake project detection.
const normalizedFolder = util.normalizePath(folder.uri.fsPath, { normCase: 'always' });
const isExcluded = util.expandExcludePaths(config.exclude ?? [], folder)
.some(excluded => util.normalizePath(excluded, { normCase: 'always' }) === normalizedFolder);
if (isExcluded) {
continue;
}

const sourceDirectories = Array.isArray(config.sourceDirectory) ? config.sourceDirectory : [config.sourceDirectory];
const expansionOptions = { ...CMakeDriver.sourceDirExpansionOptions(folder.uri.fsPath), doNotSupportCommands: true };
for (const sourceDirectory of sourceDirectories) {
if (!sourceDirectory) {
continue;
}
// A ${command:...} source directory cannot be resolved by this cheap, non-interactive
// preflight. Fail open: show the placeholder (it self-clears if no project materializes).
if (sourceDirectory.includes('${command:')) {
return true;
}
let sourceDir = util.lightNormalizePath(await expandString(sourceDirectory, expansionOptions));
if (path.basename(sourceDir).toLocaleLowerCase() === 'cmakelists.txt') {
// Tolerate a sourceDirectory that already points at the CMakeLists.txt file,
// matching normalizeAndVerifySourceDir()'s behavior.
sourceDir = path.dirname(sourceDir);
}
try {
await fs.stat(path.join(sourceDir, 'CMakeLists.txt'));
return true; // A CMakeLists.txt is present — this is a CMake project.
} catch (statErr) {
if (isDefinitivelyAbsentError((statErr as NodeJS.ErrnoException)?.code)) {
continue; // Definitively no CMakeLists.txt here; keep checking other source dirs/folders.
}
// Transient/ambiguous error (e.g. EMFILE from extension-host file-handle exhaustion,
// EACCES). Fail open so the placeholder still appears in exactly the resource-starved
// environments this reveal is meant to help.
return true;
}
}
} catch (e) {
// Config/expansion error: fail open. The authoritative end-of-init reveal remains the
// source of truth for whether the real project views appear.
log.debug(localize('init.preflight.inconclusive', 'CMake initialization preflight for folder {0} was inconclusive ({1}); showing the initializing placeholder.', folder.uri.fsPath, util.errorToString(e)));
return true;
}
}
return false;
}

/**
* Starts up the extension.
* @param context The extension context
Expand Down Expand Up @@ -3090,12 +3165,38 @@ export async function activate(context: vscode.ExtensionContext): Promise<api.CM
taskProvider = vscode.tasks.registerTaskProvider(CMakeTaskProvider.CMakeScriptType, cmakeTaskProvider);
// Load a new extension manager
extensionManager = await ExtensionManager.create(context);
await extensionManager.init();

// need the extensionManager to be initialized for this.
pinnedCommands = new PinnedCommands(extensionManager.getWorkspaceConfig(), extensionManager.extensionContext);

return setup(context);
// Two-phase activation reveal: back the transient placeholder view and show the
// CMake activity-bar container with an "initializing" state as soon as the
// manager exists, so the sidebar is visible immediately instead of staying
// hidden for the entire (potentially slow) init(). The real views, commands,
// and status bar remain gated on cmake:enableFullFeatureSet, so nothing runs
// against a not-yet-ready backend.
context.subscriptions.push(vscode.window.registerTreeDataProvider('cmake.initializing', new InitializingViewProvider()));
const languageServerOnlyMode = extensionManager.getWorkspaceConfig().languageServerOnlyMode;
// Skip the filesystem preflight entirely in language-server-only mode, where the project
// UI (and therefore the placeholder) is intentionally never shown.
const hasCMakeProject = !languageServerOnlyMode && await workspaceHasCMakeProjectForInitialization();
const showInitializingView = shouldShowInitializingView(hasCMakeProject, languageServerOnlyMode);
try {
if (showInitializingView) {
// Use setContextValue directly (not setContextAndStore): cmake:isInitializing gates only
// UI visibility and is referenced by no command, so it must not trigger the active-commands
// recompute that setContextAndStore performs.
await util.setContextValue(initializingContextKey, true);
}
await extensionManager.init();

// need the extensionManager to be initialized for this.
pinnedCommands = new PinnedCommands(extensionManager.getWorkspaceConfig(), extensionManager.extensionContext);

return await setup(context);
} finally {
// Clear the placeholder once activation settles (success or failure) so the
// icon never sticks; the real views take over via cmake:enableFullFeatureSet
// when a valid CMake project is present.
await util.setContextValue(initializingContextKey, false);
}
}

// Enable all or part of the CMake Tools palette commands
Expand Down
22 changes: 22 additions & 0 deletions src/ui/initializingView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as vscode from 'vscode';

/**
* A static, inert tree data provider for the transient `cmake.initializing`
* placeholder view.
*
* It intentionally has no children — the visible content is supplied by the view's
* `viewsWelcome` contribution in `package.json`. Its only purpose is to back the
* `cmake.initializing` view so the CMake activity-bar container can appear
* immediately during activation (via the `cmake:isInitializing` context key),
* before the real project views are ready. The real views, commands, and status
* bar remain gated on `cmake:enableFullFeatureSet`.
*/
export class InitializingViewProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
getTreeItem(element: vscode.TreeItem): vscode.TreeItem {
return element;
}

getChildren(): vscode.TreeItem[] {
return [];
}
}
Loading
Loading