From 35bc3882abeee38569794dcc010e62d27df6ecd1 Mon Sep 17 00:00:00 2001 From: yCodeTech Date: Wed, 3 Jun 2026 00:29:33 +0100 Subject: [PATCH] feat: add path resolution for Windows built-in extensions in WSL Fixes #32. - Implemented logic to resolve the Windows built-in extensions path while running in WSL, accommodating both legacy and commit-hash directory structures. - Updated `ExtensionData` constructor to allow notification of discovery path failures only when the root extension file called it. - Enhanced error handling and logging for path resolution failures. --- src/configuration.ts | 4 +- src/extension.ts | 2 +- src/extensionData.ts | 177 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 178 insertions(+), 5 deletions(-) diff --git a/src/configuration.ts b/src/configuration.ts index d05ff85..3acb96d 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -322,8 +322,8 @@ export class Configuration { const windowsBuiltInExtensionsPath = this.extensionData.getExtensionDiscoveryPath("WindowsBuiltInExtensionsPathFromWsl"); // Read the paths and create arrays of the extensions. - const windowsBuiltInExtensions = this.readExtensionsFromDirectory(windowsBuiltInExtensionsPath); - const windowsUserExtensions = this.readExtensionsFromDirectory(windowsUserExtensionsPath); + const windowsBuiltInExtensions = windowsBuiltInExtensionsPath ? this.readExtensionsFromDirectory(windowsBuiltInExtensionsPath) : []; + const windowsUserExtensions = windowsUserExtensionsPath ? this.readExtensionsFromDirectory(windowsUserExtensionsPath) : []; // Combine the built-in and user extensions into the extensions array. extensions.push(...windowsBuiltInExtensions, ...windowsUserExtensions); diff --git a/src/extension.ts b/src/extension.ts index 6acf50b..c76915f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,7 +17,7 @@ export function activate(context: vscode.ExtensionContext) { } // Initialize extension data and configuration - const extensionData = new ExtensionData(); + const extensionData = new ExtensionData(null, true); const configuration = new Configuration(); const extensionName = extensionData.get("namespace"); const extensionDisplayName = extensionData.get("displayName"); diff --git a/src/extensionData.ts b/src/extensionData.ts index 6a37b5e..221693e 100644 --- a/src/extensionData.ts +++ b/src/extensionData.ts @@ -1,12 +1,42 @@ import * as vscode from "vscode"; import * as path from "path"; +import * as fs from "node:fs"; import isWsl from "is-wsl"; import {IPackageJson} from "package-json-type"; +import {logger} from "./logger"; import {readJsonFile} from "./utils"; import {ExtensionMetaData, ExtensionPaths, ExtensionMetaDataValue} from "./interfaces/extensionMetaData"; export class ExtensionData { + /** + * Cached result of resolving the Windows built-in extensions path while running in WSL. + * + * `undefined` means unresolved/not attempted yet. + * `null` means resolution attempted but failed. + * `string` means the resolved absolute path. + * + * @type {string | null | undefined} + */ + private static windowsBuiltInExtensionsPathFromWsl: string | null | undefined; + + /** + * Whether the unresolved Windows built-in path warning has already been shown. + * Prevents repeating the same warning message multiple times in a single session. + * + * @type {boolean} + */ + private static hasShownWindowsBuiltInExtensionsPathWarning = false; + + /** + * Whether this instance should surface discovery path failures to the user. + * + * Controlled by the constructor parameter `notifyDiscoveryPathFailures`. + * + * @type {boolean} + */ + private readonly shouldNotifyDiscoveryPathFailures: boolean; + /** * Extension data in the form of a key:value Map object. * @@ -35,7 +65,22 @@ export class ExtensionData { */ private packageJsonData: IPackageJson; - public constructor(extensionPath: string | null = null) { + /** + * Create an instance of the ExtensionData class, which retrieves and stores metadata + * about an extension. + * + * @param extensionPath Optional absolute path to the extension. If not provided, + * defaults to the path of this extension. + * This is used to allow creating ExtensionData instances for other extensions by + * providing their paths. + * + * @param notifyDiscoveryPathFailures Whether this instance should notify discovery path + * failures to the user. This should generally only be `true` for the root extension instance + * to avoid duplicate warnings. + */ + public constructor(extensionPath: string | null = null, notifyDiscoveryPathFailures: boolean = false) { + this.shouldNotifyDiscoveryPathFailures = notifyDiscoveryPathFailures; + // Set the path if provided, otherwise default to this extension's path. // // For this extension's path, we use `__dirname` and go up two levels @@ -117,8 +162,136 @@ export class ExtensionData { // Only set these if running in WSL if (isWsl) { this.extensionDiscoveryPaths.set("WindowsUserExtensionsPathFromWsl", path.dirname(process.env.VSCODE_WSL_EXT_LOCATION!)); - this.extensionDiscoveryPaths.set("WindowsBuiltInExtensionsPathFromWsl", path.join(process.env.VSCODE_CWD!, "resources/app/extensions")); + + const windowsBuiltInExtensionsPathFromWsl = this.resolveWindowsBuiltInExtensionsPathFromWsl(); + if (windowsBuiltInExtensionsPathFromWsl) { + this.extensionDiscoveryPaths.set("WindowsBuiltInExtensionsPathFromWsl", windowsBuiltInExtensionsPathFromWsl); + } + } + } + + /** + * Resolve the Windows built-in extensions path while running in WSL. + * + * VS Code now installs under a commit-hash directory (for example, + * `"C:\Users\Name\AppData\Local\Programs\Microsoft VS Code\[commit hash]\resources\app\extensions"`), + * so this checks both the old path (without a commit hash) and the new hashed path. + * + * @returns {string | undefined} The resolved Windows built-in extensions path if found. + */ + private resolveWindowsBuiltInExtensionsPathFromWsl(): string | undefined { + // Reuse cached success/failure to avoid repeated disk scans. + if (ExtensionData.windowsBuiltInExtensionsPathFromWsl !== undefined) { + return ExtensionData.windowsBuiltInExtensionsPathFromWsl ?? undefined; + } + + const vscodeCwd = process.env.VSCODE_CWD; + if (!vscodeCwd) { + this.handleWindowsBuiltInExtensionsPathResolutionFailure("The VSCODE_CWD environment variable is not set."); + // Cache failure so later calls do not repeat warnings/log scans. + ExtensionData.windowsBuiltInExtensionsPathFromWsl = null; + return; } + + // Legacy VS Code path (without a commit hash). + const legacyPath = path.join(vscodeCwd, "resources", "app", "extensions"); + + // Check the legacy path first since it's a direct path and cheaper to check + // than scanning directories. If it exists, return it. + if (fs.existsSync(legacyPath)) { + this.logWindowsBuiltInExtensionsPathResolutionSuccess(legacyPath, "legacy"); + ExtensionData.windowsBuiltInExtensionsPathFromWsl = legacyPath; + return legacyPath; + } + + // Current VS Code path with a commit-hash install folder. + let entries: fs.Dirent[] = []; + + // Read the VSCODE_CWD directory. + try { + entries = fs.readdirSync(vscodeCwd, {withFileTypes: true}); + } catch { + this.handleWindowsBuiltInExtensionsPathResolutionFailure(`Unable to read the VS Code install directory at "${vscodeCwd}".`); + // Cache failure state and skip reattempts until extension host restarts. + ExtensionData.windowsBuiltInExtensionsPathFromWsl = null; + return; + } + + // Loop through each entry in the VSCODE_CWD directory to find the commit-hash + // directory containing the built-in extensions. + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + + // Examine each child directory for the new hashed install path and check if the + // full extensions path exists within it. If it does, return it. + const candidatePath = path.join(vscodeCwd, entry.name, "resources", "app", "extensions"); + if (fs.existsSync(candidatePath)) { + this.logWindowsBuiltInExtensionsPathResolutionSuccess(candidatePath, `hashed (${entry.name})`); + ExtensionData.windowsBuiltInExtensionsPathFromWsl = candidatePath; + return candidatePath; + } + } + + this.handleWindowsBuiltInExtensionsPathResolutionFailure( + `Could not resolve the Windows built-in extensions path from VSCODE_CWD "${vscodeCwd}".` + ); + // Cache failure state (null) so subsequent calls skip re-resolution. + ExtensionData.windowsBuiltInExtensionsPathFromWsl = null; + } + + /** + * Log successful Windows built-in extension path resolution for WSL. + * + * @param {string} resolvedPath The resolved path. + * @param {string} resolvedPathType The detected VS Code install path type, + * either "legacy" or "hashed ([commit-hash])". + */ + private logWindowsBuiltInExtensionsPathResolutionSuccess(resolvedPath: string, resolvedPathType: string): void { + if (!this.shouldNotifyDiscoveryPathFailures) { + return; + } + + logger.debug("Resolved Windows built-in extensions path from WSL:", { + resolvedPathType, + resolvedPath, + VSCODE_CWD: process.env.VSCODE_CWD, + }); + } + + /** + * Log and optionally show a warning if the Windows built-in extension path cannot be resolved. + * + * @param {string} message The failure reason to log. + */ + private handleWindowsBuiltInExtensionsPathResolutionFailure(message: string): void { + if (!this.shouldNotifyDiscoveryPathFailures) { + return; + } + + logger.error(message); + logger.debug("Windows built-in extensions path resolution context:", { + VSCODE_CWD: process.env.VSCODE_CWD, + VSCODE_WSL_EXT_LOCATION: process.env.VSCODE_WSL_EXT_LOCATION, + }); + + if (ExtensionData.hasShownWindowsBuiltInExtensionsPathWarning) { + return; + } + + ExtensionData.hasShownWindowsBuiltInExtensionsPathWarning = true; + + vscode.window + .showWarningMessage( + "Auto Comment Blocks could not resolve VS Code's Windows built-in extensions path while running in WSL. Some built-in language configurations may be unavailable. Check the output channel for details.", + "Open Output Channel" + ) + .then((selection) => { + if (selection === "Open Output Channel") { + logger.showChannel(); + } + }); } /**