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
4 changes: 2 additions & 2 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
177 changes: 175 additions & 2 deletions src/extensionData.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
});
}

/**
Expand Down
Loading