From e6c95a893094f9b68581581698c5c98d83e62371 Mon Sep 17 00:00:00 2001 From: hanniavalera Date: Wed, 5 Aug 2026 12:45:41 -0500 Subject: [PATCH] Show CMake sidebar immediately with an initializing placeholder during activation The CMake activity-bar container and all its views were gated on cmake:enableFullFeatureSet, which only flips true at the very end of the extension's init(), behind a serial chain of subprocess probes (cmake --version / -E capabilities, kit read, preset expansion, and the Visual Studio developer-environment bootstrap for cl.exe presets). On machines where process/file I/O is intermittently slow (e.g. antivirus scanning of spawned executables, or extension-host file-handle exhaustion / EMFILE), the entire sidebar disappears for minutes during activation. Introduce a two-phase reveal: a new cmake:isInitializing context key backs a transient, inert 'initializing' placeholder view so the activity-bar container appears immediately, while the real views, commands, and status bar remain gated on cmake:enableFullFeatureSet - nothing runs against a partially-initialized backend. The placeholder is shown when a cheap, fail-open preflight (variable expansion plus a single CMakeLists.txt stat, treating only ENOENT/ENOTDIR as absent so EMFILE and other transient filesystem errors still reveal the placeholder) finds a CMake project, and never in language-server-only mode. The key is set via setContextValue (UI-only, no command recompute) and cleared in a finally block so the icon never sticks on failure or when no project is found. --- CHANGELOG.md | 1 + package.json | 12 +- package.nls.json | 2 + src/activation.ts | 46 +++++++ src/extension.ts | 111 ++++++++++++++- src/ui/initializingView.ts | 22 +++ .../backend/activation-contributions.test.ts | 126 ++++++++++++++++++ test/unit-tests/backend/activation.test.ts | 61 +++++++++ 8 files changed, 375 insertions(+), 6 deletions(-) create mode 100644 src/activation.ts create mode 100644 src/ui/initializingView.ts create mode 100644 test/unit-tests/backend/activation-contributions.test.ts create mode 100644 test/unit-tests/backend/activation.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fc1845e8a..f1e838130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package.json b/package.json index 8aca08025..6cda5b85a 100644 --- a/package.json +++ b/package.json @@ -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%", @@ -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": [ diff --git a/package.nls.json b/package.nls.json index b0b8d2c26..f8ca67bb7 100644 --- a/package.nls.json +++ b/package.nls.json @@ -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.", diff --git a/src/activation.ts b/src/activation.ts new file mode 100644 index 000000000..8f8a1a86b --- /dev/null +++ b/src/activation.ts @@ -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'; +} diff --git a/src/extension.ts b/src/extension.ts index acf81aa10..0acfc0031 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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'; @@ -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'; @@ -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 { + 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 @@ -3090,12 +3165,38 @@ export async function activate(context: vscode.ExtensionContext): Promise