Cache windows executables (#282265)

fixes #279262
This commit is contained in:
Megan Rogge
2025-12-10 11:59:55 -05:00
committed by GitHub
parent 0c022544b1
commit c05e0b2311
3 changed files with 116 additions and 25 deletions

View File

@@ -5,7 +5,7 @@
import * as fs from 'fs/promises';
import * as vscode from 'vscode';
import { isExecutable } from '../helpers/executable';
import { isExecutable, WindowsExecutableExtensionsCache } from '../helpers/executable';
import { osIsWindows } from '../helpers/os';
import type { ICompletionResource } from '../types';
import { getFriendlyResourcePath } from '../helpers/uri';
@@ -22,7 +22,7 @@ export interface IExecutablesInPath {
export class PathExecutableCache implements vscode.Disposable {
private _disposables: vscode.Disposable[] = [];
private _cachedWindowsExeExtensions: { [key: string]: boolean | undefined } | undefined;
private readonly _windowsExecutableExtensionsCache: WindowsExecutableExtensionsCache | undefined;
private _cachedExes: Map<string, Set<ICompletionResource> | undefined> = new Map();
private _inProgressRequest: {
@@ -33,10 +33,10 @@ export class PathExecutableCache implements vscode.Disposable {
constructor() {
if (isWindows) {
this._cachedWindowsExeExtensions = vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly);
this._windowsExecutableExtensionsCache = new WindowsExecutableExtensionsCache(this._getConfiguredWindowsExecutableExtensions());
this._disposables.push(vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(SettingsIds.CachedWindowsExecutableExtensions)) {
this._cachedWindowsExeExtensions = vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly);
this._windowsExecutableExtensionsCache?.update(this._getConfiguredWindowsExecutableExtensions());
this._cachedExes.clear();
}
}));
@@ -159,6 +159,7 @@ export class PathExecutableCache implements vscode.Disposable {
const result = new Set<ICompletionResource>();
const fileResource = vscode.Uri.file(path);
const files = await vscode.workspace.fs.readDirectory(fileResource);
const windowsExecutableExtensions = this._windowsExecutableExtensionsCache?.getExtensions();
await Promise.all(
files.map(([file, fileType]) => (async () => {
let kind: vscode.TerminalCompletionItemKind | undefined;
@@ -175,7 +176,7 @@ export class PathExecutableCache implements vscode.Disposable {
if (lstat.isSymbolicLink()) {
try {
const symlinkRealPath = await fs.realpath(resource.fsPath);
const isExec = await isExecutable(symlinkRealPath, this._cachedWindowsExeExtensions);
const isExec = await isExecutable(symlinkRealPath, windowsExecutableExtensions);
if (!isExec) {
return;
}
@@ -197,7 +198,7 @@ export class PathExecutableCache implements vscode.Disposable {
return;
}
const isExec = kind === vscode.TerminalCompletionItemKind.Method || await isExecutable(formattedPath, this._cachedWindowsExeExtensions);
const isExec = kind === vscode.TerminalCompletionItemKind.Method || await isExecutable(resource.fsPath, windowsExecutableExtensions);
if (!isExec) {
return;
}
@@ -216,6 +217,10 @@ export class PathExecutableCache implements vscode.Disposable {
return undefined;
}
}
private _getConfiguredWindowsExecutableExtensions(): { [key: string]: boolean | undefined } | undefined {
return vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly);
}
}
export type ITerminalEnvironment = { [key: string]: string | undefined };

View File

@@ -6,10 +6,10 @@
import { osIsWindows } from './os';
import * as fs from 'fs/promises';
export function isExecutable(filePath: string, configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined } | undefined): Promise<boolean> | boolean {
export function isExecutable(filePath: string, windowsExecutableExtensions?: Set<string>): Promise<boolean> | boolean {
if (osIsWindows()) {
const resolvedWindowsExecutableExtensions = resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions);
return resolvedWindowsExecutableExtensions.find(ext => filePath.endsWith(ext)) !== undefined;
const extensions = windowsExecutableExtensions ?? defaultWindowsExecutableExtensionsSet;
return hasWindowsExecutableExtension(filePath, extensions);
}
return isExecutableUnix(filePath);
}
@@ -25,22 +25,6 @@ export async function isExecutableUnix(filePath: string): Promise<boolean> {
}
}
function resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined }): string[] {
const resolvedWindowsExecutableExtensions: string[] = windowsDefaultExecutableExtensions;
const excluded = new Set<string>();
if (configuredWindowsExecutableExtensions) {
for (const [key, value] of Object.entries(configuredWindowsExecutableExtensions)) {
if (value === true) {
resolvedWindowsExecutableExtensions.push(key);
} else {
excluded.add(key);
}
}
}
return Array.from(new Set(resolvedWindowsExecutableExtensions)).filter(ext => !excluded.has(ext));
}
export const windowsDefaultExecutableExtensions: string[] = [
'.exe', // Executable file
'.bat', // Batch file
@@ -59,3 +43,65 @@ export const windowsDefaultExecutableExtensions: string[] = [
'.pl', // Perl script (requires Perl interpreter)
'.sh', // Shell script (via WSL or third-party tools)
];
const defaultWindowsExecutableExtensionsSet = new Set<string>();
for (const ext of windowsDefaultExecutableExtensions) {
defaultWindowsExecutableExtensionsSet.add(ext);
}
export class WindowsExecutableExtensionsCache {
private _rawConfig: { [key: string]: boolean | undefined } | undefined;
private _cachedExtensions: Set<string> | undefined;
constructor(rawConfig?: { [key: string]: boolean | undefined }) {
this._rawConfig = rawConfig;
}
update(rawConfig: { [key: string]: boolean | undefined } | undefined): void {
this._rawConfig = rawConfig;
this._cachedExtensions = undefined;
}
getExtensions(): Set<string> {
if (!this._cachedExtensions) {
this._cachedExtensions = resolveWindowsExecutableExtensions(this._rawConfig);
}
return this._cachedExtensions;
}
}
function hasWindowsExecutableExtension(filePath: string, extensions: Set<string>): boolean {
const fileName = filePath.slice(Math.max(filePath.lastIndexOf('\\'), filePath.lastIndexOf('/')) + 1);
for (const ext of extensions) {
if (fileName.endsWith(ext)) {
return true;
}
}
return false;
}
function resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined }): Set<string> {
const extensions = new Set<string>();
const configured = configuredWindowsExecutableExtensions ?? {};
const excluded = new Set<string>();
for (const [ext, value] of Object.entries(configured)) {
if (value !== true) {
excluded.add(ext);
}
}
for (const ext of windowsDefaultExecutableExtensions) {
if (!excluded.has(ext)) {
extensions.add(ext);
}
}
for (const [ext, value] of Object.entries(configured)) {
if (value === true) {
extensions.add(ext);
}
}
return extensions;
}

View File

@@ -7,6 +7,7 @@ import 'mocha';
import { deepStrictEqual, strictEqual } from 'node:assert';
import type { MarkdownString } from 'vscode';
import { PathExecutableCache } from '../../env/pathExecutableCache';
import { WindowsExecutableExtensionsCache, windowsDefaultExecutableExtensions } from '../../helpers/executable';
suite('PathExecutableCache', () => {
test('cache should return empty for empty PATH', async () => {
@@ -67,4 +68,43 @@ suite('PathExecutableCache', () => {
strictEqual(symlinkDoc, `${symlinkPath} -> ${realPath}`);
});
}
if (process.platform === 'win32') {
suite('WindowsExecutableExtensionsCache', () => {
test('returns default extensions when not configured', () => {
const cache = new WindowsExecutableExtensionsCache();
const extensions = cache.getExtensions();
for (const ext of windowsDefaultExecutableExtensions) {
strictEqual(extensions.has(ext), true, `expected default extension ${ext}`);
}
});
test('honors configured additions and removals', () => {
const cache = new WindowsExecutableExtensionsCache({
'.added': true,
'.bat': false
});
const extensions = cache.getExtensions();
strictEqual(extensions.has('.added'), true);
strictEqual(extensions.has('.bat'), false);
strictEqual(extensions.has('.exe'), true);
});
test('recomputes only after update is called', () => {
const cache = new WindowsExecutableExtensionsCache({ '.one': true });
const first = cache.getExtensions();
const second = cache.getExtensions();
strictEqual(first, second, 'expected cached set to be reused');
cache.update({ '.two': true });
const third = cache.getExtensions();
strictEqual(third.has('.two'), true);
strictEqual(third.has('.one'), false);
strictEqual(third === first, false, 'expected cache to recompute after update');
});
});
}
});