Skip to content

Commit 35bc388

Browse files
committed
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.
1 parent 60c6b65 commit 35bc388

3 files changed

Lines changed: 178 additions & 5 deletions

File tree

src/configuration.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,8 @@ export class Configuration {
322322
const windowsBuiltInExtensionsPath = this.extensionData.getExtensionDiscoveryPath("WindowsBuiltInExtensionsPathFromWsl");
323323

324324
// Read the paths and create arrays of the extensions.
325-
const windowsBuiltInExtensions = this.readExtensionsFromDirectory(windowsBuiltInExtensionsPath);
326-
const windowsUserExtensions = this.readExtensionsFromDirectory(windowsUserExtensionsPath);
325+
const windowsBuiltInExtensions = windowsBuiltInExtensionsPath ? this.readExtensionsFromDirectory(windowsBuiltInExtensionsPath) : [];
326+
const windowsUserExtensions = windowsUserExtensionsPath ? this.readExtensionsFromDirectory(windowsUserExtensionsPath) : [];
327327

328328
// Combine the built-in and user extensions into the extensions array.
329329
extensions.push(...windowsBuiltInExtensions, ...windowsUserExtensions);

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function activate(context: vscode.ExtensionContext) {
1717
}
1818

1919
// Initialize extension data and configuration
20-
const extensionData = new ExtensionData();
20+
const extensionData = new ExtensionData(null, true);
2121
const configuration = new Configuration();
2222
const extensionName = extensionData.get("namespace");
2323
const extensionDisplayName = extensionData.get("displayName");

src/extensionData.ts

Lines changed: 175 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,42 @@
11
import * as vscode from "vscode";
22
import * as path from "path";
3+
import * as fs from "node:fs";
34
import isWsl from "is-wsl";
45
import {IPackageJson} from "package-json-type";
56

7+
import {logger} from "./logger";
68
import {readJsonFile} from "./utils";
79
import {ExtensionMetaData, ExtensionPaths, ExtensionMetaDataValue} from "./interfaces/extensionMetaData";
810

911
export class ExtensionData {
12+
/**
13+
* Cached result of resolving the Windows built-in extensions path while running in WSL.
14+
*
15+
* `undefined` means unresolved/not attempted yet.
16+
* `null` means resolution attempted but failed.
17+
* `string` means the resolved absolute path.
18+
*
19+
* @type {string | null | undefined}
20+
*/
21+
private static windowsBuiltInExtensionsPathFromWsl: string | null | undefined;
22+
23+
/**
24+
* Whether the unresolved Windows built-in path warning has already been shown.
25+
* Prevents repeating the same warning message multiple times in a single session.
26+
*
27+
* @type {boolean}
28+
*/
29+
private static hasShownWindowsBuiltInExtensionsPathWarning = false;
30+
31+
/**
32+
* Whether this instance should surface discovery path failures to the user.
33+
*
34+
* Controlled by the constructor parameter `notifyDiscoveryPathFailures`.
35+
*
36+
* @type {boolean}
37+
*/
38+
private readonly shouldNotifyDiscoveryPathFailures: boolean;
39+
1040
/**
1141
* Extension data in the form of a key:value Map object.
1242
*
@@ -35,7 +65,22 @@ export class ExtensionData {
3565
*/
3666
private packageJsonData: IPackageJson;
3767

38-
public constructor(extensionPath: string | null = null) {
68+
/**
69+
* Create an instance of the ExtensionData class, which retrieves and stores metadata
70+
* about an extension.
71+
*
72+
* @param extensionPath Optional absolute path to the extension. If not provided,
73+
* defaults to the path of this extension.
74+
* This is used to allow creating ExtensionData instances for other extensions by
75+
* providing their paths.
76+
*
77+
* @param notifyDiscoveryPathFailures Whether this instance should notify discovery path
78+
* failures to the user. This should generally only be `true` for the root extension instance
79+
* to avoid duplicate warnings.
80+
*/
81+
public constructor(extensionPath: string | null = null, notifyDiscoveryPathFailures: boolean = false) {
82+
this.shouldNotifyDiscoveryPathFailures = notifyDiscoveryPathFailures;
83+
3984
// Set the path if provided, otherwise default to this extension's path.
4085
//
4186
// For this extension's path, we use `__dirname` and go up two levels
@@ -117,8 +162,136 @@ export class ExtensionData {
117162
// Only set these if running in WSL
118163
if (isWsl) {
119164
this.extensionDiscoveryPaths.set("WindowsUserExtensionsPathFromWsl", path.dirname(process.env.VSCODE_WSL_EXT_LOCATION!));
120-
this.extensionDiscoveryPaths.set("WindowsBuiltInExtensionsPathFromWsl", path.join(process.env.VSCODE_CWD!, "resources/app/extensions"));
165+
166+
const windowsBuiltInExtensionsPathFromWsl = this.resolveWindowsBuiltInExtensionsPathFromWsl();
167+
if (windowsBuiltInExtensionsPathFromWsl) {
168+
this.extensionDiscoveryPaths.set("WindowsBuiltInExtensionsPathFromWsl", windowsBuiltInExtensionsPathFromWsl);
169+
}
170+
}
171+
}
172+
173+
/**
174+
* Resolve the Windows built-in extensions path while running in WSL.
175+
*
176+
* VS Code now installs under a commit-hash directory (for example,
177+
* `"C:\Users\Name\AppData\Local\Programs\Microsoft VS Code\[commit hash]\resources\app\extensions"`),
178+
* so this checks both the old path (without a commit hash) and the new hashed path.
179+
*
180+
* @returns {string | undefined} The resolved Windows built-in extensions path if found.
181+
*/
182+
private resolveWindowsBuiltInExtensionsPathFromWsl(): string | undefined {
183+
// Reuse cached success/failure to avoid repeated disk scans.
184+
if (ExtensionData.windowsBuiltInExtensionsPathFromWsl !== undefined) {
185+
return ExtensionData.windowsBuiltInExtensionsPathFromWsl ?? undefined;
186+
}
187+
188+
const vscodeCwd = process.env.VSCODE_CWD;
189+
if (!vscodeCwd) {
190+
this.handleWindowsBuiltInExtensionsPathResolutionFailure("The VSCODE_CWD environment variable is not set.");
191+
// Cache failure so later calls do not repeat warnings/log scans.
192+
ExtensionData.windowsBuiltInExtensionsPathFromWsl = null;
193+
return;
121194
}
195+
196+
// Legacy VS Code path (without a commit hash).
197+
const legacyPath = path.join(vscodeCwd, "resources", "app", "extensions");
198+
199+
// Check the legacy path first since it's a direct path and cheaper to check
200+
// than scanning directories. If it exists, return it.
201+
if (fs.existsSync(legacyPath)) {
202+
this.logWindowsBuiltInExtensionsPathResolutionSuccess(legacyPath, "legacy");
203+
ExtensionData.windowsBuiltInExtensionsPathFromWsl = legacyPath;
204+
return legacyPath;
205+
}
206+
207+
// Current VS Code path with a commit-hash install folder.
208+
let entries: fs.Dirent[] = [];
209+
210+
// Read the VSCODE_CWD directory.
211+
try {
212+
entries = fs.readdirSync(vscodeCwd, {withFileTypes: true});
213+
} catch {
214+
this.handleWindowsBuiltInExtensionsPathResolutionFailure(`Unable to read the VS Code install directory at "${vscodeCwd}".`);
215+
// Cache failure state and skip reattempts until extension host restarts.
216+
ExtensionData.windowsBuiltInExtensionsPathFromWsl = null;
217+
return;
218+
}
219+
220+
// Loop through each entry in the VSCODE_CWD directory to find the commit-hash
221+
// directory containing the built-in extensions.
222+
for (const entry of entries) {
223+
if (!entry.isDirectory()) {
224+
continue;
225+
}
226+
227+
// Examine each child directory for the new hashed install path and check if the
228+
// full extensions path exists within it. If it does, return it.
229+
const candidatePath = path.join(vscodeCwd, entry.name, "resources", "app", "extensions");
230+
if (fs.existsSync(candidatePath)) {
231+
this.logWindowsBuiltInExtensionsPathResolutionSuccess(candidatePath, `hashed (${entry.name})`);
232+
ExtensionData.windowsBuiltInExtensionsPathFromWsl = candidatePath;
233+
return candidatePath;
234+
}
235+
}
236+
237+
this.handleWindowsBuiltInExtensionsPathResolutionFailure(
238+
`Could not resolve the Windows built-in extensions path from VSCODE_CWD "${vscodeCwd}".`
239+
);
240+
// Cache failure state (null) so subsequent calls skip re-resolution.
241+
ExtensionData.windowsBuiltInExtensionsPathFromWsl = null;
242+
}
243+
244+
/**
245+
* Log successful Windows built-in extension path resolution for WSL.
246+
*
247+
* @param {string} resolvedPath The resolved path.
248+
* @param {string} resolvedPathType The detected VS Code install path type,
249+
* either "legacy" or "hashed ([commit-hash])".
250+
*/
251+
private logWindowsBuiltInExtensionsPathResolutionSuccess(resolvedPath: string, resolvedPathType: string): void {
252+
if (!this.shouldNotifyDiscoveryPathFailures) {
253+
return;
254+
}
255+
256+
logger.debug("Resolved Windows built-in extensions path from WSL:", {
257+
resolvedPathType,
258+
resolvedPath,
259+
VSCODE_CWD: process.env.VSCODE_CWD,
260+
});
261+
}
262+
263+
/**
264+
* Log and optionally show a warning if the Windows built-in extension path cannot be resolved.
265+
*
266+
* @param {string} message The failure reason to log.
267+
*/
268+
private handleWindowsBuiltInExtensionsPathResolutionFailure(message: string): void {
269+
if (!this.shouldNotifyDiscoveryPathFailures) {
270+
return;
271+
}
272+
273+
logger.error(message);
274+
logger.debug("Windows built-in extensions path resolution context:", {
275+
VSCODE_CWD: process.env.VSCODE_CWD,
276+
VSCODE_WSL_EXT_LOCATION: process.env.VSCODE_WSL_EXT_LOCATION,
277+
});
278+
279+
if (ExtensionData.hasShownWindowsBuiltInExtensionsPathWarning) {
280+
return;
281+
}
282+
283+
ExtensionData.hasShownWindowsBuiltInExtensionsPathWarning = true;
284+
285+
vscode.window
286+
.showWarningMessage(
287+
"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.",
288+
"Open Output Channel"
289+
)
290+
.then((selection) => {
291+
if (selection === "Open Output Channel") {
292+
logger.showChannel();
293+
}
294+
});
122295
}
123296

124297
/**

0 commit comments

Comments
 (0)