From 7a9f62939d601473c38f0d3d9529663df9597198 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 1 Feb 2025 05:45:21 -0800 Subject: [PATCH] Move path exe logic into own file --- .../src/env/pathExecutables.ts | 101 ++++++++++++++++ .../terminal-suggest/src/helpers/uri.ts | 20 ++++ .../src/terminalSuggestMain.ts | 111 +----------------- 3 files changed, 127 insertions(+), 105 deletions(-) create mode 100644 extensions/terminal-suggest/src/env/pathExecutables.ts create mode 100644 extensions/terminal-suggest/src/helpers/uri.ts diff --git a/extensions/terminal-suggest/src/env/pathExecutables.ts b/extensions/terminal-suggest/src/env/pathExecutables.ts new file mode 100644 index 00000000000..98b62d345ea --- /dev/null +++ b/extensions/terminal-suggest/src/env/pathExecutables.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs/promises'; +import * as vscode from 'vscode'; +import { isExecutable } from '../helpers/executable'; +import { osIsWindows } from '../helpers/os'; +import type { ICompletionResource } from '../types'; +import { getFriendlyResourcePath } from '../helpers/uri'; + +const isWindows = osIsWindows(); +let cachedAvailableCommandsPath: string | undefined; +let cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; +const cachedWindowsExecutableExtensionsSettingId = 'terminal.integrated.suggest.windowsExecutableExtensions'; +let cachedAvailableCommands: Set | undefined; +let cachedAvailableCommandsLabels: Set | undefined; + +export function activatePathExecutables(context: vscode.ExtensionContext) { + if (isWindows) { + cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(cachedWindowsExecutableExtensionsSettingId)) { + cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); + cachedAvailableCommands = undefined; + cachedAvailableCommandsPath = undefined; + } + })); + } +} + +export async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { + // Create cache key + let pathValue: string | undefined; + if (isWindows) { + const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path'); + if (caseSensitivePathKey) { + pathValue = env[caseSensitivePathKey]; + } + } else { + pathValue = env.PATH; + } + if (pathValue === undefined) { + return; + } + + // Check cache + if (cachedAvailableCommands && cachedAvailableCommandsPath === pathValue) { + return { completionResources: cachedAvailableCommands, labels: cachedAvailableCommandsLabels }; + } + + // Extract executables from PATH + const paths = pathValue.split(isWindows ? ';' : ':'); + const pathSeparator = isWindows ? '\\' : '/'; + const promises: Promise | undefined>[] = []; + const labels: Set = new Set(); + for (const path of paths) { + promises.push(getFilesInPath(path, pathSeparator, labels)); + } + + // Merge all results + const executables = new Set(); + const resultSets = await Promise.all(promises); + for (const resultSet of resultSets) { + if (resultSet) { + for (const executable of resultSet) { + executables.add(executable); + } + } + } + + // Return + cachedAvailableCommands = executables; + cachedAvailableCommandsLabels = labels; + cachedAvailableCommandsPath = pathValue; + return { completionResources: executables, labels }; +} + +async function getFilesInPath(path: string, pathSeparator: string, labels: Set): Promise | undefined> { + try { + const dirExists = await fs.stat(path).then(stat => stat.isDirectory()).catch(() => false); + if (!dirExists) { + return undefined; + } + const result = new Set(); + const fileResource = vscode.Uri.file(path); + const files = await vscode.workspace.fs.readDirectory(fileResource); + for (const [file, fileType] of files) { + const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); + if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, cachedWindowsExecutableExtensions)) { + result.add({ label: file, detail: formattedPath }); + labels.add(file); + } + } + return result; + } catch (e) { + // Ignore errors for directories that can't be read + return undefined; + } +} diff --git a/extensions/terminal-suggest/src/helpers/uri.ts b/extensions/terminal-suggest/src/helpers/uri.ts new file mode 100644 index 00000000000..90e09dbc1c2 --- /dev/null +++ b/extensions/terminal-suggest/src/helpers/uri.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export function getFriendlyResourcePath(uri: vscode.Uri, pathSeparator: string, kind?: vscode.TerminalCompletionItemKind): string { + let path = uri.fsPath; + // Ensure drive is capitalized on Windows + if (pathSeparator === '\\' && path.match(/^[a-zA-Z]:\\/)) { + path = `${path[0].toUpperCase()}:${path.slice(2)}`; + } + if (kind === vscode.TerminalCompletionItemKind.Folder) { + if (!path.endsWith(pathSeparator)) { + path += pathSeparator; + } + } + return path; +} diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 8d278d1c002..e3f85e5d132 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -11,13 +11,14 @@ import codeCompletionSpec from './completions/code'; import cdSpec from './completions/cd'; import codeInsidersCompletionSpec from './completions/code-insiders'; import { osIsWindows } from './helpers/os'; -import { isExecutable } from './helpers/executable'; import type { ICompletionResource } from './types'; import { getBashGlobals } from './shell/bash'; import { getZshGlobals } from './shell/zsh'; import { getFishGlobals } from './shell/fish'; import { getPwshGlobals } from './shell/pwsh'; import { getTokenType, TokenType } from './tokens'; +import { activatePathExecutables, getCommandsInPath } from './env/pathExecutables'; +import { getFriendlyResourcePath } from './helpers/uri'; // TODO: remove once API is finalized export const enum TerminalShellType { @@ -37,12 +38,7 @@ export const enum TerminalShellType { } const isWindows = osIsWindows(); -let cachedAvailableCommandsPath: string | undefined; -let cachedWindowsExecutableExtensions: { [key: string]: boolean | undefined } | undefined; -const cachedWindowsExecutableExtensionsSettingId = 'terminal.integrated.suggest.windowsExecutableExtensions'; -let cachedAvailableCommands: Set | undefined; -let cachedAvailableCommandsLabels: Set | undefined; -const cachedBuiltinCommands: Map = new Map(); +const cachedGlobals: Map = new Map(); export const availableSpecs: Fig.Spec[] = [ cdSpec, @@ -63,7 +59,7 @@ const getShellSpecificGlobals: Map): Promise { try { - const cachedCommands = cachedBuiltinCommands.get(shellType); + const cachedCommands = cachedGlobals.get(shellType); if (cachedCommands) { return cachedCommands; } @@ -74,7 +70,7 @@ async function getShellGlobals(shellType: TerminalShellType, existingCommands?: const options: ExecOptionsWithStringEncoding = { encoding: 'utf-8', shell }; const mixedCommands: (string | ICompletionResource)[] | undefined = await getShellSpecificGlobals.get(shellType)?.(options, existingCommands); const normalizedCommands = mixedCommands?.map(command => typeof command === 'string' ? ({ label: command }) : command); - cachedBuiltinCommands.set(shellType, normalizedCommands); + cachedGlobals.set(shellType, normalizedCommands); return normalizedCommands; } catch (error) { @@ -121,17 +117,7 @@ export async function activate(context: vscode.ExtensionContext) { return result.items; } }, '/', '\\')); - - if (isWindows) { - cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); - context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(cachedWindowsExecutableExtensionsSettingId)) { - cachedWindowsExecutableExtensions = vscode.workspace.getConfiguration('terminal.integrated.suggest').get('windowsExecutableExtensions'); - cachedAvailableCommands = undefined; - cachedAvailableCommandsPath = undefined; - } - })); - } + activatePathExecutables(context); } /** @@ -210,77 +196,6 @@ function createCompletionItem(cursorPosition: number, prefix: string, commandRes }; } - -async function getCommandsInPath(env: { [key: string]: string | undefined } = process.env): Promise<{ completionResources: Set | undefined; labels: Set | undefined } | undefined> { - // Create cache key - let pathValue: string | undefined; - if (isWindows) { - const caseSensitivePathKey = Object.keys(env).find(key => key.toLowerCase() === 'path'); - if (caseSensitivePathKey) { - pathValue = env[caseSensitivePathKey]; - } - } else { - pathValue = env.PATH; - } - if (pathValue === undefined) { - return; - } - - // Check cache - if (cachedAvailableCommands && cachedAvailableCommandsPath === pathValue) { - return { completionResources: cachedAvailableCommands, labels: cachedAvailableCommandsLabels }; - } - - // Extract executables from PATH - const paths = pathValue.split(isWindows ? ';' : ':'); - const pathSeparator = isWindows ? '\\' : '/'; - const promises: Promise | undefined>[] = []; - const labels: Set = new Set(); - for (const path of paths) { - promises.push(getFilesInPath(path, pathSeparator, labels)); - } - - // Merge all results - const executables = new Set(); - const resultSets = await Promise.all(promises); - for (const resultSet of resultSets) { - if (resultSet) { - for (const executable of resultSet) { - executables.add(executable); - } - } - } - - // Return - cachedAvailableCommands = executables; - cachedAvailableCommandsLabels = labels; - cachedAvailableCommandsPath = pathValue; - return { completionResources: executables, labels }; -} - -async function getFilesInPath(path: string, pathSeparator: string, labels: Set): Promise | undefined> { - try { - const dirExists = await fs.stat(path).then(stat => stat.isDirectory()).catch(() => false); - if (!dirExists) { - return undefined; - } - const result = new Set(); - const fileResource = vscode.Uri.file(path); - const files = await vscode.workspace.fs.readDirectory(fileResource); - for (const [file, fileType] of files) { - const formattedPath = getFriendlyResourcePath(vscode.Uri.joinPath(fileResource, file), pathSeparator); - if (!labels.has(file) && fileType !== vscode.FileType.Unknown && fileType !== vscode.FileType.Directory && await isExecutable(formattedPath, cachedWindowsExecutableExtensions)) { - result.add({ label: file, detail: formattedPath }); - labels.add(file); - } - } - return result; - } catch (e) { - // Ignore errors for directories that can't be read - return undefined; - } -} - function getPrefix(commandLine: string, cursorPosition: number): string { // Return an empty string if the command line is empty after trimming if (commandLine.trim() === '') { @@ -519,20 +434,6 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray | undefined return { items, filesRequested, foldersRequested }; } -function getFriendlyResourcePath(uri: vscode.Uri, pathSeparator: string, kind?: vscode.TerminalCompletionItemKind): string { - let path = uri.fsPath; - // Ensure drive is capitalized on Windows - if (pathSeparator === '\\' && path.match(/^[a-zA-Z]:\\/)) { - path = `${path[0].toUpperCase()}:${path.slice(2)}`; - } - if (kind === vscode.TerminalCompletionItemKind.Folder) { - if (!path.endsWith(pathSeparator)) { - path += pathSeparator; - } - } - return path; -} - function getShell(shellType: TerminalShellType): string | undefined { switch (shellType) { case TerminalShellType.Bash: