diff --git a/.vscode/launch.json b/.vscode/launch.json index 8f27b622f97..348c15f2712 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -100,27 +100,25 @@ "order": 3 } }, - { - "name": "Web Tests", - "type": "extensionHost", - "debugWebWorkerHost": true, - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", + { + "name": "Web Tests", + "type": "extensionHost", + "debugWebWorkerHost": true, + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", "--enable-proposed-api", - "--extensionDevelopmentKind=web", - "--extensionTestsPath=${workspaceFolder}/out/extension.web.bundle" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.*" - ], + "--extensionDevelopmentKind=web", + "--extensionTestsPath=${workspaceFolder}/out/extension.web.bundle" + ], + "outFiles": ["${workspaceFolder}/out/**/*.*"], "sourceMaps": true, "preLaunchTask": "compile-web-test", "presentation": { "group": "2_tests", "order": 11 } - }, + }, { // Note, for the smoke test you want to debug, you may need to copy the file, // rename it and remove a check for only smoke tests. diff --git a/news/3 Code Health/9599.md b/news/3 Code Health/9599.md new file mode 100644 index 00000000000..101a03f3ad2 --- /dev/null +++ b/news/3 Code Health/9599.md @@ -0,0 +1 @@ +Switch to using URIs wherever possible instead of strings for file paths. diff --git a/src/extension.node.ts b/src/extension.node.ts index 517016b8721..f132a47fa11 100644 --- a/src/extension.node.ts +++ b/src/extension.node.ts @@ -255,13 +255,11 @@ function addOutputChannel(context: IExtensionContext, serviceManager: IServiceMa if (!workspace.workspaceFolders || workspace.workspaceFolders.length === 0) { standardOutputChannel.appendLine(`No workspace folder opened.`); } else if (workspace.workspaceFolders.length === 1) { - standardOutputChannel.appendLine( - `Workspace folder ${getDisplayPath(workspace.workspaceFolders[0].uri.fsPath)}` - ); + standardOutputChannel.appendLine(`Workspace folder ${getDisplayPath(workspace.workspaceFolders[0].uri)}`); } else { standardOutputChannel.appendLine( `Multiple Workspace folders opened ${workspace.workspaceFolders - .map((item) => getDisplayPath(item.uri.fsPath)) + .map((item) => getDisplayPath(item.uri)) .join(', ')}` ); } diff --git a/src/intellisense/intellisenseProvider.node.ts b/src/intellisense/intellisenseProvider.node.ts index 4ba44673728..b9436cfa340 100644 --- a/src/intellisense/intellisenseProvider.node.ts +++ b/src/intellisense/intellisenseProvider.node.ts @@ -15,6 +15,7 @@ import { INotebookLanguageClientProvider, INotebookControllerManager } from '../ import { LanguageServer } from './languageServer.node'; import { IInteractiveWindowProvider } from '../interactive-window/types'; import { IVSCodeNotebookController } from '../notebooks/controllers/types'; +import { getComparisonKey } from '../platform/vscode-path/resources'; const EmptyWorkspaceKey = ''; @@ -153,16 +154,17 @@ export class IntellisenseProvider implements INotebookLanguageClientProvider, IE } private getInterpreterIdFromCache(interpreter: PythonEnvironment) { - let id = this.interpreterIdCache.get(interpreter.path); + const key = getComparisonKey(interpreter.uri); + let id = this.interpreterIdCache.get(key); if (!id) { // Making an assumption that the id for an interpreter never changes. id = getInterpreterId(interpreter); - this.interpreterIdCache.set(interpreter.path, id); + this.interpreterIdCache.set(key, id); } return id; } - private shouldAllowIntellisense(uri: Uri, interpreterId: string, _interpreterPath: string) { + private shouldAllowIntellisense(uri: Uri, interpreterId: string, _interpreterPath: Uri) { // We should allow intellisense for a URI when the interpreter matches // the controller for the uri const notebook = findAssociatedNotebookDocument(uri, this.notebooks, this.interactiveWindowProvider); diff --git a/src/intellisense/languageServer.node.ts b/src/intellisense/languageServer.node.ts index 30ca135491f..0e2b10fdeee 100644 --- a/src/intellisense/languageServer.node.ts +++ b/src/intellisense/languageServer.node.ts @@ -119,7 +119,7 @@ export class LanguageServer implements Disposable { public static async createLanguageServer( middlewareType: 'pylance' | 'jupyter', interpreter: PythonEnvironment, - shouldAllowIntellisense: (uri: Uri, interpreterId: string, interpreterPath: string) => boolean, + shouldAllowIntellisense: (uri: Uri, interpreterId: string, interpreterPath: Uri) => boolean, getNotebookHeader: (uri: Uri) => string ): Promise { const cancellationStrategy = new FileBasedCancellationStrategy(); @@ -134,15 +134,15 @@ export class LanguageServer implements Disposable { () => languageClient, () => noop, // Don't trace output. Slows things down too much NOTEBOOK_SELECTOR, - interpreter.path, - (uri) => shouldAllowIntellisense(uri, interpreterId, interpreter.path), + interpreter.uri.fsPath || interpreter.uri.path, + (uri) => shouldAllowIntellisense(uri, interpreterId, interpreter.uri), getNotebookHeader ) : createPylanceMiddleware( () => languageClient, NOTEBOOK_SELECTOR, - interpreter.path, - (uri) => shouldAllowIntellisense(uri, interpreterId, interpreter.path), + interpreter.uri.fsPath || interpreter.uri.path, + (uri) => shouldAllowIntellisense(uri, interpreterId, interpreter.uri), getNotebookHeader ); @@ -228,7 +228,7 @@ export class LanguageServer implements Disposable { const runJediPath = path.join(python.extensionPath, 'pythonFiles', 'run-jedi-language-server.py'); if (await fs.pathExists(runJediPath)) { const serverOptions: ServerOptions = { - command: interpreter.path || 'python', + command: interpreter.uri.fsPath || 'python', args: [runJediPath] }; return serverOptions; diff --git a/src/kernels/debugging/interactiveWindowDebugger.node.ts b/src/kernels/debugging/interactiveWindowDebugger.node.ts index 906e9b819e5..fcf030a22b4 100644 --- a/src/kernels/debugging/interactiveWindowDebugger.node.ts +++ b/src/kernels/debugging/interactiveWindowDebugger.node.ts @@ -56,7 +56,7 @@ export class InteractiveWindowDebugger implements IInteractiveWindowDebugger, IC // The python extension debugger tags the debug configuration with the python path used on the python property // by tagging this here (if available) we can treat IW or python extension debug session the same in knowing // which python launched them - const pythonPath = kernel.kernelConnectionMetadata.interpreter?.path; + const pythonPath = kernel.kernelConnectionMetadata.interpreter?.uri; return this.startDebugSession((c) => this.debugService.startDebugging(undefined, c), kernel, { justMyCode: settings.debugJustMyCode, diff --git a/src/kernels/helpers.node.ts b/src/kernels/helpers.node.ts index 8beddf0dcb2..787e2bb8009 100644 --- a/src/kernels/helpers.node.ts +++ b/src/kernels/helpers.node.ts @@ -4,6 +4,7 @@ 'use strict'; import * as path from '../platform/vscode-path/path'; +import * as uriPath from '../platform/vscode-path/resources'; import * as url from 'url'; import type { KernelSpec } from '@jupyterlab/services'; // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports @@ -35,7 +36,6 @@ import { traceError, traceInfo, traceInfoIfCI, traceVerbose, traceWarning } from import { getDisplayPath } from '../platform/common/platform/fs-paths'; import { IPythonExecutionFactory } from '../platform/common/process/types.node'; import { - IPathUtils, IConfigurationService, Resource, IMemento, @@ -80,6 +80,8 @@ import { IStatusProvider } from '../platform/progress/types'; import { IRawNotebookProvider } from './raw/types'; import { IVSCodeNotebookController } from '../notebooks/controllers/types'; import { isCI } from '../platform/common/constants.node'; +import { fsPathToUri } from '../platform/vscode-path/utils'; +import { deserializePythonEnvironment } from '../platform/api/pythonApi.node'; // Helper functions for dealing with kernels and kernelspecs @@ -140,9 +142,9 @@ export function getKernelId(spec: IJupyterKernelSpec, interpreter?: PythonEnviro argsForGenerationOfId = spec.argv.join('#').toLowerCase(); } const prefixForRemoteKernels = remoteBaseUrl ? `${remoteBaseUrl}.` : ''; - return `${prefixForRemoteKernels}${spec.id || ''}.${specName}.${getNormalizedInterpreterPath( - spec.interpreterPath || spec.path - )}.${getNormalizedInterpreterPath(interpreter?.path) || ''}.${argsForGenerationOfId}`; + return `${prefixForRemoteKernels}${spec.id || ''}.${specName}.${ + getNormalizedInterpreterPath(fsPathToUri(spec.interpreterPath) || spec.uri).fsPath + }.${getNormalizedInterpreterPath(interpreter?.uri).fsPath || ''}.${argsForGenerationOfId}`; } export function getSysInfoReasonHeader( @@ -228,7 +230,7 @@ function getPythonEnvironmentName(pythonEnv: PythonEnvironment) { // In such cases the envName is empty, but it has a path. let envName = pythonEnv.envName; if (pythonEnv.envPath && pythonEnv.envType === EnvironmentType.Conda && !pythonEnv.envName) { - envName = path.basename(pythonEnv.envPath); + envName = uriPath.basename(pythonEnv.envPath); } return envName; } @@ -263,7 +265,7 @@ export function getNameOfKernelConnection( : kernelConnection.kernelSpec?.name; } -export function getKernelPathFromKernelConnection(kernelConnection?: KernelConnectionMetadata): string | undefined { +export function getKernelPathFromKernelConnection(kernelConnection?: KernelConnectionMetadata): Uri | undefined { if (!kernelConnection) { return; } @@ -277,12 +279,15 @@ export function getKernelPathFromKernelConnection(kernelConnection?: KernelConne kernelConnection.kind === 'startUsingLocalKernelSpec') && kernelConnection.kernelSpec.language === PYTHON_LANGUAGE) ) { - return kernelSpec?.metadata?.interpreter?.path || kernelSpec?.interpreterPath || kernelSpec?.path; + return fsPathToUri(kernelSpec?.metadata?.interpreter?.path || kernelSpec?.interpreterPath) || kernelSpec?.uri; } else { // For non python kernels, give preference to the executable path in the kernelspec // E.g. if we have a rust kernel, we should show the path to the rust executable not the interpreter (such as conda env that owns the rust runtime). return ( - model?.path || kernelSpec?.path || kernelSpec?.metadata?.interpreter?.path || kernelSpec?.interpreterPath + model?.uri || + kernelSpec?.uri || + fsPathToUri(kernelSpec?.metadata?.interpreter?.path) || + fsPathToUri(kernelSpec?.interpreterPath) ); } } @@ -302,7 +307,6 @@ export function getRemoteKernelSessionInformation( export function getKernelConnectionPath( kernelConnection: KernelConnectionMetadata | undefined, - pathUtils: IPathUtils, workspaceService: IWorkspaceService ) { if (kernelConnection?.kind === 'connectToLiveRemoteKernel') { @@ -311,9 +315,8 @@ export function getKernelConnectionPath( const kernelPath = getKernelPathFromKernelConnection(kernelConnection); // If we have just one workspace folder opened, then ensure to use relative paths // where possible (e.g. for virtual environments). - const cwd = - workspaceService.workspaceFolders?.length === 1 ? workspaceService.workspaceFolders[0].uri.fsPath : undefined; - return kernelPath ? pathUtils.getDisplayName(kernelPath, cwd) : ''; + const folders = workspaceService.workspaceFolders ? workspaceService.workspaceFolders : []; + return kernelPath ? getDisplayPath(kernelPath, folders) : ''; } export function getInterpreterFromKernelConnectionMetadata( @@ -327,12 +330,12 @@ export function getInterpreterFromKernelConnectionMetadata( } const model = kernelConnectionMetadataHasKernelModel(kernelConnection) ? kernelConnection.kernelModel : undefined; if (model?.metadata?.interpreter) { - return model.metadata.interpreter; + return deserializePythonEnvironment(model?.metadata?.interpreter); } const kernelSpec = kernelConnectionMetadataHasKernelSpec(kernelConnection) ? kernelConnection.kernelSpec : undefined; - return kernelSpec?.metadata?.interpreter; + return deserializePythonEnvironment(kernelSpec?.metadata?.interpreter); } export function isPythonKernelConnection(kernelConnection?: KernelConnectionMetadata): boolean { if (!kernelConnection) { @@ -406,7 +409,7 @@ export function getInterpreterWorkspaceFolder( interpreter: PythonEnvironment, workspaceService: IWorkspaceService ): string | undefined { - const folder = workspaceService.getWorkspaceFolder(Uri.file(interpreter.path)); + const folder = workspaceService.getWorkspaceFolder(interpreter.uri); return folder?.uri.fsPath || workspaceService.rootPath; } /** @@ -470,8 +473,13 @@ export function getKernelRegistrationInfo( */ export function createInterpreterKernelSpec( interpreter?: PythonEnvironment, - rootKernelFilePath?: string + rootKernelFilePath?: Uri ): IJupyterKernelSpec { + const interpreterMetadata = interpreter + ? { + path: interpreter.uri.fsPath + } + : {}; // This creates a kernel spec for an interpreter. When launched, 'python' argument will map to using the interpreter // associated with the current resource for launching. const defaultSpec: KernelSpec.ISpecModel = { @@ -479,7 +487,7 @@ export function createInterpreterKernelSpec( language: 'python', display_name: interpreter?.displayName || 'Python 3', metadata: { - interpreter + interpreter: interpreterMetadata }, argv: ['python', '-m', 'ipykernel_launcher', '-f', connectionFilePlaceholder], env: {}, @@ -489,10 +497,15 @@ export function createInterpreterKernelSpec( // Generate spec file path if we know where kernel files will go const specFile = rootKernelFilePath && defaultSpec.name - ? path.join(rootKernelFilePath, defaultSpec.name, 'kernel.json') + ? uriPath.joinPath(rootKernelFilePath, defaultSpec.name, 'kernel.json') : undefined; - return new JupyterKernelSpec(defaultSpec, specFile, interpreter?.path, 'registeredByNewVersionOfExt'); + return new JupyterKernelSpec( + defaultSpec, + specFile ? specFile.fsPath : undefined, + interpreter?.uri.fsPath, + 'registeredByNewVersionOfExt' + ); } export function areKernelConnectionsEqual( @@ -573,7 +586,7 @@ export function findPreferredKernel( traceInfo( `Find preferred kernel for ${getDisplayPath(resource)} with metadata ${JSON.stringify( notebookMetadata || {} - )} & preferred interpreter ${getDisplayPath(preferredInterpreter?.path)}` + )} & preferred interpreter ${getDisplayPath(preferredInterpreter?.uri)}` ); if (kernels.length === 0) { @@ -1205,7 +1218,7 @@ function compareAgainstKernelDisplayNameInNotebookMetadata( argv: [], display_name: notebookMetadata.kernelspec.display_name, name: notebookMetadata.kernelspec.name, - path: '' + uri: Uri.file('') }); if (metadataPointsToADefaultKernelSpec) { // If we're dealing with default kernelspec names, then special handling. @@ -1298,7 +1311,7 @@ function findKernelSpecMatchingInterpreter( // if we have more than one match then something is wrong. if (result.length > 1) { - traceError(`More than one kernel spec matches the interpreter ${interpreter.path}.`, result); + traceError(`More than one kernel spec matches the interpreter ${interpreter.uri}.`, result); if (isCI) { throw new Error('More than one kernelspec matches the intererpreter'); } @@ -1313,13 +1326,16 @@ function interpreterMatchesThatInNotebookMetadata( notebookMetadata?: nbformat.INotebookMetadata ) { const interpreterHashInMetadata = getInterpreterHashInMetadata(notebookMetadata); + const interpreterHashForKernel = kernelConnection.interpreter + ? getInterpreterHash(kernelConnection.interpreter) + : undefined; return ( interpreterHashInMetadata && (kernelConnection.kind === 'startUsingLocalKernelSpec' || kernelConnection.kind === 'startUsingRemoteKernelSpec' || kernelConnection.kind === 'startUsingPythonInterpreter') && kernelConnection.interpreter && - getInterpreterHash(kernelConnection.interpreter) === interpreterHashInMetadata + interpreterHashForKernel === interpreterHashInMetadata ); } @@ -1353,10 +1369,7 @@ export async function sendTelemetryForPythonKernelExecutable( return; } const sysExecutable = concatMultilineString(output.text).trim().toLowerCase(); - const match = areInterpreterPathsSame( - kernelConnection.interpreter.path.toLowerCase(), - sysExecutable.toLowerCase() - ); + const match = areInterpreterPathsSame(kernelConnection.interpreter.uri, Uri.file(sysExecutable)); sendTelemetryEvent(Telemetry.PythonKerneExecutableMatches, undefined, { match: match ? 'true' : 'false', kernelConnectionType: kernelConnection.kind @@ -1380,7 +1393,10 @@ export async function sendTelemetryForPythonKernelExecutable( throwOnStdErr: false }); if (execOutput.stdout.trim().length > 0) { - const match = areInterpreterPathsSame(execOutput.stdout.trim().toLowerCase(), sysExecutable); + const match = areInterpreterPathsSame( + Uri.file(execOutput.stdout.trim().toLowerCase()), + Uri.file(sysExecutable) + ); sendTelemetryEvent(Telemetry.PythonKerneExecutableMatches, undefined, { match: match ? 'true' : 'false', kernelConnectionType: kernelConnection.kind @@ -1389,8 +1405,8 @@ export async function sendTelemetryForPythonKernelExecutable( if (!match) { traceError( `Interpreter started by kernel does not match expectation, expected ${getDisplayPath( - kernelConnection.interpreter?.path - )}, got ${getDisplayPath(sysExecutable)}` + kernelConnection.interpreter?.uri + )}, got ${getDisplayPath(Uri.file(sysExecutable))}` ); } } @@ -1598,7 +1614,7 @@ export async function handleKernelError( // If we failed to start the kernel, then clear cache used to track // whether we have dependencies installed or not. // Possible something is missing. - clearInstalledIntoInterpreterMemento(memento, Product.ipykernel, metadata.interpreter.path).ignoreErrors(); + clearInstalledIntoInterpreterMemento(memento, Product.ipykernel, metadata.interpreter.uri).ignoreErrors(); } const handleResult = await errorHandler.handleKernelError(error, 'start', metadata, resource); diff --git a/src/kernels/installer/condaInstaller.node.ts b/src/kernels/installer/condaInstaller.node.ts index 9ae169516ea..5d3054872da 100644 --- a/src/kernels/installer/condaInstaller.node.ts +++ b/src/kernels/installer/condaInstaller.node.ts @@ -103,13 +103,13 @@ export class CondaInstaller extends ModuleInstaller { args.push(moduleName); args.push('-y'); return { - exe: condaFile, + exe: condaFile?.fsPath, args }; } private getEnvironmentPath(interpreter: PythonEnvironment) { - const dir = path.dirname(interpreter.path); + const dir = path.dirname(interpreter.uri.fsPath); // If interpreter is in bin or Scripts, then go up one level const subDirName = path.basename(dir); diff --git a/src/kernels/installer/pipEnvInstaller.node.ts b/src/kernels/installer/pipEnvInstaller.node.ts index 5cf3b2358db..85d4a1d5963 100644 --- a/src/kernels/installer/pipEnvInstaller.node.ts +++ b/src/kernels/installer/pipEnvInstaller.node.ts @@ -50,7 +50,7 @@ export class PipEnvInstaller extends ModuleInstaller { return false; } // Install using `pipenv install` only if the active environment is related to the current folder. - return isPipenvEnvironmentRelatedToFolder(interpreter.path, workspaceFolder.uri.fsPath); + return isPipenvEnvironmentRelatedToFolder(interpreter.uri, workspaceFolder.uri); } else { return resource.envType === EnvironmentType.Pipenv; } diff --git a/src/kernels/installer/pipenv.node.ts b/src/kernels/installer/pipenv.node.ts index dad1a9ea286..9e52dee8cf0 100644 --- a/src/kernels/installer/pipenv.node.ts +++ b/src/kernels/installer/pipenv.node.ts @@ -5,6 +5,7 @@ import * as path from '../../platform/vscode-path/path'; import { traceError } from '../../platform/logging'; import { getEnvironmentVariable } from '../../platform/common/utils/platform.node'; import { pathExists, readFile, arePathsSame, normCasePath } from '../../platform/common/platform/fileUtils.node'; +import { Uri } from 'vscode'; function getSearchHeight() { // PIPENV_MAX_DEPTH tells pipenv the maximum number of directories to recursively search for @@ -49,7 +50,7 @@ export async function _getAssociatedPipfile( * otherwise return `undefined`. * @param interpreterPath Absolute path to any python interpreter. */ -async function getPipfileIfLocal(interpreterPath: string): Promise { +async function getPipfileIfLocal(interpreterPath: Uri): Promise { // Local pipenv environments are created by setting PIPENV_VENV_IN_PROJECT to 1, which always names the environment // folder '.venv': https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_VENV_IN_PROJECT // This is the layout we wish to verify. @@ -58,7 +59,7 @@ async function getPipfileIfLocal(interpreterPath: string): Promise { * If interpreter path belongs to a global pipenv environment, return associated Pipfile, otherwise return `undefined`. * @param interpreterPath Absolute path to any python interpreter. */ -async function getPipfileIfGlobal(interpreterPath: string): Promise { - const envFolder = path.dirname(path.dirname(interpreterPath)); +async function getPipfileIfGlobal(interpreterPath: Uri): Promise { + const envFolder = path.dirname(path.dirname(interpreterPath.fsPath)); const projectDir = await getProjectDir(envFolder); if (projectDir === undefined) { return undefined; @@ -121,7 +122,7 @@ async function getPipfileIfGlobal(interpreterPath: string): Promise { +export async function isPipenvEnvironment(interpreterPath: Uri): Promise { if (await getPipfileIfLocal(interpreterPath)) { return true; } @@ -136,7 +137,7 @@ export async function isPipenvEnvironment(interpreterPath: string): Promise { +export async function isPipenvEnvironmentRelatedToFolder(interpreterPath: Uri, folder: Uri): Promise { const pipFileAssociatedWithEnvironment = await getPipfileIfGlobal(interpreterPath); if (!pipFileAssociatedWithEnvironment) { return false; @@ -145,7 +146,7 @@ export async function isPipenvEnvironmentRelatedToFolder(interpreterPath: string // PIPENV_NO_INHERIT is used to tell pipenv not to look for Pipfile in parent directories // https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_NO_INHERIT const lookIntoParentDirectories = getEnvironmentVariable('PIPENV_NO_INHERIT') === undefined; - const pipFileAssociatedWithFolder = await _getAssociatedPipfile(folder, { lookIntoParentDirectories }); + const pipFileAssociatedWithFolder = await _getAssociatedPipfile(folder.fsPath, { lookIntoParentDirectories }); if (!pipFileAssociatedWithFolder) { return false; } diff --git a/src/kernels/installer/poetry.node.ts b/src/kernels/installer/poetry.node.ts index 69ce675da01..4231c0a858a 100644 --- a/src/kernels/installer/poetry.node.ts +++ b/src/kernels/installer/poetry.node.ts @@ -151,7 +151,7 @@ export class Poetry { yield 'poetry'; const home = getUserHomeDir(); if (home) { - const defaultPoetryPath = path.join(home, '.poetry', 'bin', 'poetry'); + const defaultPoetryPath = path.join(home.fsPath, '.poetry', 'bin', 'poetry'); if (pathExistsSync(defaultPoetryPath)) { yield defaultPoetryPath; } diff --git a/src/kernels/installer/poetryInstaller.node.ts b/src/kernels/installer/poetryInstaller.node.ts index a828cf5ee60..16e050cf495 100644 --- a/src/kernels/installer/poetryInstaller.node.ts +++ b/src/kernels/installer/poetryInstaller.node.ts @@ -54,7 +54,7 @@ export class PoetryInstaller extends ModuleInstaller { if (folder) { // Install using poetry CLI only if the active poetry environment is related to the current folder. return isPoetryEnvironmentRelatedToFolder( - interpreter.path, + interpreter.uri.fsPath, folder, this.configurationService.getSettings(undefined).poetryPath ); diff --git a/src/kernels/installer/productInstaller.node.ts b/src/kernels/installer/productInstaller.node.ts index 263daa15f41..86e578f93b3 100644 --- a/src/kernels/installer/productInstaller.node.ts +++ b/src/kernels/installer/productInstaller.node.ts @@ -53,12 +53,8 @@ export async function trackPackageInstalledIntoInterpreter( const key = `${getInterpreterHash(interpreter)}#${ProductNames.get(product)}`; await memento.update(key, true); } -export async function clearInstalledIntoInterpreterMemento( - memento: Memento, - product: Product, - interpreterPath: string -) { - const key = `${getInterpreterHash({ path: interpreterPath })}#${ProductNames.get(product)}`; +export async function clearInstalledIntoInterpreterMemento(memento: Memento, product: Product, interpreterPath: Uri) { + const key = `${getInterpreterHash({ uri: interpreterPath })}#${ProductNames.get(product)}`; await memento.update(key, undefined); } export function isModulePresentInEnvironmentCache(memento: Memento, product: Product, interpreter: PythonEnvironment) { diff --git a/src/kernels/installer/pyenv.node.ts b/src/kernels/installer/pyenv.node.ts index 35a4384579d..379de627d34 100644 --- a/src/kernels/installer/pyenv.node.ts +++ b/src/kernels/installer/pyenv.node.ts @@ -1,8 +1,10 @@ import * as path from '../../platform/vscode-path/path'; +import * as uriPath from '../../platform/vscode-path/resources'; import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../platform/common/utils/platform.node'; -import { arePathsSame, isParentPath, pathExists } from '../../platform/common/platform/fileUtils.node'; +import { pathExists } from '../../platform/common/platform/fileUtils.node'; +import { Uri } from 'vscode'; -export function getPyenvDir(): string { +export function getPyenvDir(): Uri { // Check if the pyenv environment variables exist: PYENV on Windows, PYENV_ROOT on Unix. // They contain the path to pyenv's installation folder. // If they don't exist, use the default path: ~/.pyenv/pyenv-win on Windows, ~/.pyenv on Unix. @@ -12,39 +14,41 @@ export function getPyenvDir(): string { let pyenvDir = getEnvironmentVariable('PYENV_ROOT') ?? getEnvironmentVariable('PYENV'); if (!pyenvDir) { - const homeDir = getUserHomeDir() || ''; + const homeDir = getUserHomeDir() || Uri.file(''); pyenvDir = - getOSType() === OSType.Windows ? path.join(homeDir, '.pyenv', 'pyenv-win') : path.join(homeDir, '.pyenv'); + getOSType() === OSType.Windows + ? path.join(homeDir.fsPath, '.pyenv', 'pyenv-win') + : path.join(homeDir.fsPath, '.pyenv'); } - return pyenvDir; + return Uri.file(pyenvDir); } /** * Checks if a given directory path is same as `pyenv` shims path. This checks * `~/.pyenv/shims` on posix and `~/.pyenv/pyenv-win/shims` on windows. - * @param {string} dirPath: Absolute path to any directory + * @param {Uri} dirPath: Absolute path to any directory * @returns {boolean}: Returns true if the patch is same as `pyenv` shims directory. */ -export function isPyenvShimDir(dirPath: string): boolean { - const shimPath = path.join(getPyenvDir(), 'shims'); - return arePathsSame(shimPath, dirPath) || arePathsSame(`${shimPath}${path.sep}`, dirPath); +export function isPyenvShimDir(dirPath: Uri): boolean { + const shimPath = uriPath.joinPath(getPyenvDir(), 'shims'); + return uriPath.isEqual(shimPath, dirPath, true); } /** * Checks if the given interpreter belongs to a pyenv based environment. - * @param {string} interpreterPath: Absolute path to the python interpreter. + * @param {Uri} interpreterPath: Absolute path to the python interpreter. * @returns {boolean}: Returns true if the interpreter belongs to a pyenv environment. */ -export async function isPyenvEnvironment(interpreterPath: string): Promise { +export async function isPyenvEnvironment(interpreterPath: Uri): Promise { const pathToCheck = interpreterPath; const pyenvDir = getPyenvDir(); - if (!(await pathExists(pyenvDir))) { + if (!(await pathExists(pyenvDir.fsPath))) { return false; } - return isParentPath(pathToCheck, pyenvDir); + return uriPath.isEqualOrParent(pathToCheck, pyenvDir); } export interface IPyenvVersionStrings { diff --git a/src/kernels/ipywidgets-message-coordination/localWidgetScriptSourceProvider.node.ts b/src/kernels/ipywidgets-message-coordination/localWidgetScriptSourceProvider.node.ts index bd1b828fdb9..3081280736b 100644 --- a/src/kernels/ipywidgets-message-coordination/localWidgetScriptSourceProvider.node.ts +++ b/src/kernels/ipywidgets-message-coordination/localWidgetScriptSourceProvider.node.ts @@ -95,8 +95,7 @@ export class LocalWidgetScriptSourceProvider implements IWidgetScriptSourceProvi if (!isPythonKernelConnection(kernelConnectionMetadata)) { return; } - const interpreterOrKernelPath = - interpreter?.path || getKernelPathFromKernelConnection(kernelConnectionMetadata); + const interpreterOrKernelPath = interpreter?.uri || getKernelPathFromKernelConnection(kernelConnectionMetadata); if (!interpreterOrKernelPath) { return; } diff --git a/src/kernels/jupyter/interpreter/jupyterInterpreterDependencyService.node.ts b/src/kernels/jupyter/interpreter/jupyterInterpreterDependencyService.node.ts index 1078935e47c..7d627f02fab 100644 --- a/src/kernels/jupyter/interpreter/jupyterInterpreterDependencyService.node.ts +++ b/src/kernels/jupyter/interpreter/jupyterInterpreterDependencyService.node.ts @@ -21,6 +21,7 @@ import { reportAction } from '../../../platform/progress/decorator.node'; import { ReportableAction } from '../../../platform/progress/types'; import { JupyterInterpreterDependencyResponse } from '../types'; import { IJupyterCommandFactory } from '../types.node'; +import { getComparisonKey } from '../../../platform/vscode-path/resources'; /** * Sorts the given list of products (in place) in the order in which they need to be installed. @@ -242,7 +243,8 @@ export class JupyterInterpreterDependencyService { token?: CancellationToken ): Promise { // If we know that all modules were available at one point in time, then use that cache. - if (this.dependenciesInstalledInInterpreter.has(interpreter.path)) { + const key = getComparisonKey(interpreter.uri); + if (this.dependenciesInstalledInInterpreter.has(key)) { return []; } @@ -271,7 +273,7 @@ export class JupyterInterpreterDependencyService { installed ? [] : [Product.kernelspec] ); if (products.length === 0) { - this.dependenciesInstalledInInterpreter.add(interpreter.path); + this.dependenciesInstalledInInterpreter.add(key); } return products; } @@ -326,7 +328,7 @@ export class JupyterInterpreterDependencyService { return JupyterInterpreterDependencyResponse.cancel; } const selectionFromError = await this.applicationShell.showErrorMessage( - DataScience.jupyterKernelSpecModuleNotFound().format(interpreter.path), + DataScience.jupyterKernelSpecModuleNotFound().format(interpreter.uri.fsPath), DataScience.selectDifferentJupyterInterpreter(), Common.cancel() ); diff --git a/src/kernels/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.node.ts b/src/kernels/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.node.ts index ec383da2bf2..bc829683758 100644 --- a/src/kernels/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.node.ts +++ b/src/kernels/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.node.ts @@ -4,6 +4,7 @@ 'use strict'; import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; import { IWorkspaceService } from '../../../platform/common/application/types'; import { IPersistentState, IPersistentStateFactory } from '../../../platform/common/types'; @@ -34,8 +35,8 @@ export class JupyterInterpreterOldCacheStateStore { private get cacheStore(): CacheInfo { return this.workspace.hasWorkspaceFolders ? this.workspaceJupyterInterpreter : this.globalJupyterInterpreter; } - public getCachedInterpreterPath(): string | undefined { - return this.cacheStore.state.value; + public getCachedInterpreterPath(): Uri | undefined { + return this.cacheStore.state.value ? Uri.file(this.cacheStore.state.value) : undefined; } public async clearCache(): Promise { await this.cacheStore.state.updateValue(undefined); diff --git a/src/kernels/jupyter/interpreter/jupyterInterpreterSelector.node.ts b/src/kernels/jupyter/interpreter/jupyterInterpreterSelector.node.ts index 0f1af8ee42a..6fc776306b2 100644 --- a/src/kernels/jupyter/interpreter/jupyterInterpreterSelector.node.ts +++ b/src/kernels/jupyter/interpreter/jupyterInterpreterSelector.node.ts @@ -8,7 +8,7 @@ import { QuickPickOptions } from 'vscode'; import { CancellationToken } from 'vscode-jsonrpc'; import { IApplicationShell, IWorkspaceService } from '../../../platform/common/application/types'; import { Cancellation } from '../../../platform/common/cancellation.node'; -import { IPathUtils } from '../../../platform/common/types'; +import { getDisplayPath } from '../../../platform/common/platform/fs-paths.node'; import { DataScience } from '../../../platform/common/utils/localize'; import { IInterpreterSelector } from '../../../platform/interpreter/configuration/types'; import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; @@ -26,8 +26,7 @@ export class JupyterInterpreterSelector { @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector, @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, @inject(JupyterInterpreterStateStore) private readonly interpreterSelectionState: JupyterInterpreterStateStore, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IPathUtils) private readonly pathUtils: IPathUtils + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService ) {} /** * Displays interpreter selector and returns the selection. @@ -37,9 +36,8 @@ export class JupyterInterpreterSelector { * @memberof JupyterInterpreterSelector */ public async selectInterpreter(token?: CancellationToken): Promise { - const workspace = this.workspace.getWorkspaceFolder(undefined); const currentPythonPath = this.interpreterSelectionState.selectedPythonPath - ? this.pathUtils.getDisplayName(this.interpreterSelectionState.selectedPythonPath, workspace?.uri.fsPath) + ? getDisplayPath(this.interpreterSelectionState.selectedPythonPath, this.workspace.workspaceFolders) : undefined; const suggestions = await this.interpreterSelector.getSuggestions(undefined); diff --git a/src/kernels/jupyter/interpreter/jupyterInterpreterService.node.ts b/src/kernels/jupyter/interpreter/jupyterInterpreterService.node.ts index 1cd355b391c..ca9c006cea5 100644 --- a/src/kernels/jupyter/interpreter/jupyterInterpreterService.node.ts +++ b/src/kernels/jupyter/interpreter/jupyterInterpreterService.node.ts @@ -4,7 +4,7 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { Event, EventEmitter } from 'vscode'; +import { Event, EventEmitter, Uri } from 'vscode'; import { CancellationToken } from 'vscode-jsonrpc'; import { createPromiseFromCancellation } from '../../../platform/common/cancellation.node'; import '../../../platform/common/extensions'; @@ -142,7 +142,7 @@ export class JupyterInterpreterService { // Check the location that we stored jupyter launch path in the old version // if it's there, return it and clear the location - private getInterpreterFromChangeOfOlderVersionOfExtension(): string | undefined { + private getInterpreterFromChangeOfOlderVersionOfExtension(): Uri | undefined { const pythonPath = this.oldVersionCacheStateStore.getCachedInterpreterPath(); if (!pythonPath) { return; @@ -156,14 +156,14 @@ export class JupyterInterpreterService { private changeSelectedInterpreterProperty(interpreter: PythonEnvironment) { this._selectedInterpreter = interpreter; this._onDidChangeInterpreter.fire(interpreter); - this.interpreterSelectionState.updateSelectedPythonPath(interpreter.path); + this.interpreterSelectionState.updateSelectedPythonPath(interpreter.uri); sendTelemetryEvent(Telemetry.SelectJupyterInterpreter, undefined, { result: 'selected' }); } // For a given python path check if it can run jupyter for us // if so, return the interpreter private async validateInterpreterPath( - pythonPath: string, + pythonPath: Uri, token?: CancellationToken ): Promise { try { diff --git a/src/kernels/jupyter/interpreter/jupyterInterpreterStateStore.node.ts b/src/kernels/jupyter/interpreter/jupyterInterpreterStateStore.node.ts index 2dd05a01fb3..a0347686f10 100644 --- a/src/kernels/jupyter/interpreter/jupyterInterpreterStateStore.node.ts +++ b/src/kernels/jupyter/interpreter/jupyterInterpreterStateStore.node.ts @@ -4,7 +4,7 @@ 'use strict'; import { inject, injectable, named } from 'inversify'; -import { Memento } from 'vscode'; +import { Memento, Uri } from 'vscode'; import { IExtensionSingleActivationService } from '../../../platform/activation/types'; import { IPythonApiProvider, IPythonExtensionChecker } from '../../../platform/api/types'; import { IMemento, GLOBAL_MEMENTO, IDisposableRegistry } from '../../../platform/common/types'; @@ -21,7 +21,7 @@ const keySelected = 'INTERPRETER_PATH_WAS_SELECTED_FOR_JUPYTER_SERVER'; */ @injectable() export class JupyterInterpreterStateStore { - private _interpreterPath?: string; + private _interpreterPath?: Uri; constructor(@inject(IMemento) @named(GLOBAL_MEMENTO) private readonly memento: Memento) {} /** @@ -33,12 +33,18 @@ export class JupyterInterpreterStateStore { public get interpreterSetAtleastOnce(): boolean { return !!this.selectedPythonPath || this.memento.get(keySelected, false); } - public get selectedPythonPath(): string | undefined { - return this._interpreterPath || this.memento.get(key, undefined); + public get selectedPythonPath(): Uri | undefined { + if (this._interpreterPath) { + return this._interpreterPath; + } + const memento = this.memento.get(key, undefined); + if (memento) { + return Uri.file(memento); + } } - public updateSelectedPythonPath(value: string | undefined) { + public updateSelectedPythonPath(value: Uri | undefined) { this._interpreterPath = value; - this.memento.update(key, value).then(noop, noop); + this.memento.update(key, value?.toString()).then(noop, noop); this.memento.update(keySelected, true).then(noop, noop); } } diff --git a/src/kernels/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.node.ts b/src/kernels/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.node.ts index 9032c9e9a9a..cccf21278d1 100644 --- a/src/kernels/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.node.ts +++ b/src/kernels/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.node.ts @@ -5,6 +5,7 @@ import { inject, injectable, named } from 'inversify'; import * as path from '../../../platform/vscode-path/path'; +import * as uriPath from '../../../platform/vscode-path/resources'; import { CancellationToken } from 'vscode'; import { traceWarning } from '../../../platform/logging'; import { @@ -13,7 +14,7 @@ import { ObservableExecutionResult, IPythonDaemonExecutionService } from '../../../platform/common/process/types.node'; -import { IOutputChannel, IPathUtils } from '../../../platform/common/types'; +import { IOutputChannel } from '../../../platform/common/types'; import { DataScience } from '../../../platform/common/utils/localize'; import { noop } from '../../../platform/common/utils/misc'; import { EXTENSION_ROOT_DIR } from '../../../platform/constants.node'; @@ -40,6 +41,7 @@ import { JupyterServerInfo } from '../types'; import { IJupyterSubCommandExecutionService } from '../types.node'; +import { getDisplayPath } from '../../../platform/common/platform/fs-paths.node'; /** * Responsible for execution of jupyter sub commands using a single/global interpreter set aside for launching jupyter server. @@ -59,7 +61,6 @@ export class JupyterInterpreterSubCommandExecutionService private readonly jupyterDependencyService: JupyterInterpreterDependencyService, @inject(IPythonExecutionFactory) private readonly pythonExecutionFactory: IPythonExecutionFactory, @inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private readonly jupyterOutputChannel: IOutputChannel, - @inject(IPathUtils) private readonly pathUtils: IPathUtils, @inject(JupyterPaths) private readonly jupyterPaths: JupyterPaths, @inject(IEnvironmentActivationService) private readonly activationHelper: IEnvironmentActivationService ) {} @@ -101,7 +102,7 @@ export class JupyterInterpreterSubCommandExecutionService } if (productsNotInstalled.length === 1 && productsNotInstalled[0] === Product.kernelspec) { - return DataScience.jupyterKernelSpecModuleNotFound().format(interpreter.path); + return DataScience.jupyterKernelSpecModuleNotFound().format(interpreter.uri.fsPath); } return getMessageForLibrariesNotInstalled(productsNotInstalled, interpreter.displayName); @@ -115,10 +116,7 @@ export class JupyterInterpreterSubCommandExecutionService ): Promise> { const interpreter = await this.getSelectedInterpreterAndThrowIfNotAvailable(options.token); this.jupyterOutputChannel.appendLine( - DataScience.startingJupyterLogMessage().format( - this.pathUtils.getDisplayName(interpreter.path), - notebookArgs.join(' ') - ) + DataScience.startingJupyterLogMessage().format(getDisplayPath(interpreter.uri), notebookArgs.join(' ')) ); const executionService = await this.pythonExecutionFactory.createDaemon({ daemonModule: JupyterDaemonModule, @@ -133,7 +131,7 @@ export class JupyterInterpreterSubCommandExecutionService const jupyterDataPaths = (process.env['JUPYTER_PATH'] || envVars['JUPYTER_PATH'] || '') .split(path.delimiter) .filter((item) => item.trim().length); - jupyterDataPaths.push(path.dirname(await this.jupyterPaths.getKernelSpecTempRegistrationFolder())); + jupyterDataPaths.push(uriPath.dirname(await this.jupyterPaths.getKernelSpecTempRegistrationFolder()).fsPath); spawnOptions.env = { ...envVars, JUPYTER_PATH: jupyterDataPaths.join(path.delimiter) diff --git a/src/kernels/jupyter/interpreter/nbconvertInterpreterDependencyChecker.node.ts b/src/kernels/jupyter/interpreter/nbconvertInterpreterDependencyChecker.node.ts index d9252b6073e..94a2ff12558 100644 --- a/src/kernels/jupyter/interpreter/nbconvertInterpreterDependencyChecker.node.ts +++ b/src/kernels/jupyter/interpreter/nbconvertInterpreterDependencyChecker.node.ts @@ -8,6 +8,7 @@ import { SemVer } from 'semver'; import { CancellationToken } from 'vscode'; import { parseSemVer } from '../../../platform/common/utils.node'; import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; +import { ResourceSet } from '../../../platform/vscode-path/map'; import { JupyterCommands } from '../../../webviews/webview-side/common/constants'; import { IInstaller, Product } from '../../installer/types'; import { INbConvertInterpreterDependencyChecker } from '../types'; @@ -16,7 +17,7 @@ import { IJupyterCommandFactory } from '../types.node'; @injectable() export class NbConvertInterpreterDependencyChecker implements INbConvertInterpreterDependencyChecker { // Track interpreters that nbconvert has been installed into - private readonly nbconvertInstalledInInterpreter = new Set(); + private readonly nbconvertInstalledInInterpreter = new ResourceSet(); constructor( @inject(IInstaller) private readonly installer: IInstaller, @inject(IJupyterCommandFactory) private readonly commandFactory: IJupyterCommandFactory @@ -25,14 +26,14 @@ export class NbConvertInterpreterDependencyChecker implements INbConvertInterpre // Check to see if nbconvert is installed in the given interpreter, note that we also need jupyter since that supplies the needed // template files for conversion public async isNbConvertInstalled(interpreter: PythonEnvironment, _token?: CancellationToken): Promise { - if (this.nbconvertInstalledInInterpreter.has(interpreter.path)) { + if (this.nbconvertInstalledInInterpreter.has(interpreter.uri)) { return true; } const isInstalled: boolean = !!(await this.installer.isInstalled(Product.nbconvert, interpreter)) && !!(await this.installer.isInstalled(Product.jupyter, interpreter)); if (isInstalled === true) { - this.nbconvertInstalledInInterpreter.add(interpreter.path); + this.nbconvertInstalledInInterpreter.add(interpreter.uri); } return isInstalled; } diff --git a/src/kernels/jupyter/jupyterKernelService.node.ts b/src/kernels/jupyter/jupyterKernelService.node.ts index 3e4d01b35e4..17eb624ad20 100644 --- a/src/kernels/jupyter/jupyterKernelService.node.ts +++ b/src/kernels/jupyter/jupyterKernelService.node.ts @@ -6,6 +6,7 @@ import type { KernelSpec } from '@jupyterlab/services'; import { inject, injectable } from 'inversify'; import * as path from '../../platform/vscode-path/path'; +import * as uriPath from '../../platform/vscode-path/resources'; import { CancellationToken } from 'vscode'; import { Cancellation } from '../../platform/common/cancellation.node'; import '../../platform/common/extensions'; @@ -37,6 +38,7 @@ import { LocalKernelConnectionMetadata } from '../types'; import { JupyterKernelSpec } from './jupyterKernelSpec.node'; +import { serializePythonEnvironment } from '../../platform/api/pythonApi.node'; /** * Responsible for registering and updating kernels @@ -128,7 +130,7 @@ export class JupyterKernelService { ) { traceInfoIfCI( `updateKernelEnvironment ${kernel.interpreter.displayName}, ${getDisplayPath( - kernel.interpreter.path + kernel.interpreter.uri )} for ${kernel.id}` ); await this.updateKernelEnvironment(resource, kernel.interpreter, kernel.kernelSpec, specFile, cancelToken); @@ -166,16 +168,19 @@ export class JupyterKernelService { } // Compute a new path for the kernelspec - const kernelSpecFilePath = path.join(root, kernel.kernelSpec.name, 'kernel.json'); + const kernelSpecFilePath = uriPath.joinPath(root, kernel.kernelSpec.name, 'kernel.json'); // If this file already exists, we can just exit - if (await this.fs.localFileExists(kernelSpecFilePath)) { - return kernelSpecFilePath; + if (await this.fs.localFileExists(kernelSpecFilePath.fsPath)) { + return kernelSpecFilePath.fsPath; } // If it doesn't exist, see if we had an original spec file that's different. const contents = { ...kernel.kernelSpec }; - if (kernel.kernelSpec.specFile && !this.fs.areLocalPathsSame(kernelSpecFilePath, kernel.kernelSpec.specFile)) { + if ( + kernel.kernelSpec.specFile && + !this.fs.areLocalPathsSame(kernelSpecFilePath.fsPath, kernel.kernelSpec.specFile) + ) { // Add extra metadata onto the contents. We'll use this // when searching for kernels later to remove duplicates. contents.metadata = contents.metadata || {}; @@ -194,7 +199,7 @@ export class JupyterKernelService { if (kernel.interpreter) { contents.metadata = { ...contents.metadata, - interpreter: kernel.interpreter + interpreter: serializePythonEnvironment(kernel.interpreter) }; } @@ -202,7 +207,7 @@ export class JupyterKernelService { // Write out the contents into the new spec file try { - await this.fs.writeLocalFile(kernelSpecFilePath, JSON.stringify(contents, undefined, 4)); + await this.fs.writeLocalFile(kernelSpecFilePath.fsPath, JSON.stringify(contents, undefined, 4)); } catch (ex) { // eslint-disable-next-line @typescript-eslint/no-explicit-any sendTelemetryEvent(Telemetry.FailedToUpdateKernelSpec, undefined, undefined, ex as any, true); @@ -216,7 +221,7 @@ export class JupyterKernelService { const originalSpecFile = contents.metadata?.vscode?.originalSpecFile || contents.metadata?.originalSpecFile; if (originalSpecFile) { const originalSpecDir = path.dirname(originalSpecFile); - const newSpecDir = path.dirname(kernelSpecFilePath); + const newSpecDir = path.dirname(kernelSpecFilePath.fsPath); const otherFiles = await this.fs.searchLocal('*.*[^json]', originalSpecDir); await Promise.all( otherFiles.map(async (f) => { @@ -228,7 +233,7 @@ export class JupyterKernelService { } sendTelemetryEvent(Telemetry.RegisterAndUseInterpreterAsKernel); - return kernelSpecFilePath; + return kernelSpecFilePath.fsPath; } private async updateKernelEnvironment( resource: Resource, @@ -245,7 +250,7 @@ export class JupyterKernelService { const kernelSpecFilePath = path.basename(specFile).toLowerCase() === kernel.name.toLowerCase() ? specFile - : path.join(kernelSpecRootPath, kernel.name, 'kernel.json'); + : uriPath.joinPath(kernelSpecRootPath, kernel.name, 'kernel.json').fsPath; // Make sure the file exists if (!(await this.fs.localFileExists(kernelSpecFilePath))) { @@ -267,9 +272,9 @@ export class JupyterKernelService { traceInfo(`Spec argv[0], not updated as it is using conda.`); } else { traceInfo( - `Spec argv[0] updated from '${specModel.argv[0]}' to '${getDisplayPath(interpreter.path)}'` + `Spec argv[0] updated from '${specModel.argv[0]}' to '${getDisplayPath(interpreter.uri)}'` ); - specModel.argv[0] = interpreter.path; + specModel.argv[0] = interpreter.uri.fsPath; } // Get the activated environment variables (as a work around for `conda run` and similar). // This ensures the code runs within the context of an activated environment. @@ -302,7 +307,7 @@ export class JupyterKernelService { // This way shell commands such as `!pip`, `!python` end up pointing to the right executables. // Also applies to `!java` where java could be an executable in the conda bin directory. if (specModel.env) { - this.envVarsService.prependPath(specModel.env as {}, path.dirname(interpreter.path)); + this.envVarsService.prependPath(specModel.env as {}, path.dirname(interpreter.uri.fsPath)); } // Ensure global site_packages are not in the path. @@ -310,7 +315,7 @@ export class JupyterKernelService { // For more details see here https://github.com/microsoft/vscode-jupyter/issues/8553#issuecomment-997144591 // https://docs.python.org/3/library/site.html#site.ENABLE_USER_SITE if (specModel.env && Object.keys(specModel.env).length > 0 && hasActivationCommands) { - traceInfo(`Adding env Variable PYTHONNOUSERSITE to ${getDisplayPath(interpreter.path)}`); + traceInfo(`Adding env Variable PYTHONNOUSERSITE to ${getDisplayPath(interpreter.uri)}`); specModel.env.PYTHONNOUSERSITE = 'True'; } else { // We don't want to inherit any such env variables from Jupyter server or the like. diff --git a/src/kernels/jupyter/jupyterKernelSpec.node.ts b/src/kernels/jupyter/jupyterKernelSpec.node.ts index ff4e9ed6965..8db2b8363af 100644 --- a/src/kernels/jupyter/jupyterKernelSpec.node.ts +++ b/src/kernels/jupyter/jupyterKernelSpec.node.ts @@ -2,21 +2,22 @@ // Licensed under the MIT License. 'use strict'; import type { KernelSpec } from '@jupyterlab/services'; -import { PythonEnvironment } from '../../platform/pythonEnvironments/info'; +import { Uri } from 'vscode'; +import { PythonEnvironment_PythonApi } from '../../platform/api/types'; import { IJupyterKernelSpec } from '../types'; export class JupyterKernelSpec implements IJupyterKernelSpec { public name: string; public originalName?: string; public language: string; - public path: string; + public uri: Uri; public readonly env: NodeJS.ProcessEnv | undefined; public display_name: string; public argv: string[]; public interrupt_mode?: 'message' | 'signal'; // eslint-disable-next-line @typescript-eslint/no-explicit-any - public metadata?: Record & { interpreter?: Partial }; + public metadata?: Record & { interpreter?: Partial }; constructor( specModel: KernelSpec.ISpecModel, public readonly specFile?: string, @@ -29,7 +30,7 @@ export class JupyterKernelSpec implements IJupyterKernelSpec { this.name = specModel.name; this.argv = specModel.argv; this.language = specModel.language; - this.path = specModel.argv && specModel.argv.length > 0 ? specModel.argv[0] : ''; + this.uri = specModel.argv && specModel.argv.length > 0 ? Uri.file(specModel.argv[0]) : Uri.file(''); this.display_name = specModel.display_name; this.metadata = specModel.metadata; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/kernels/jupyter/launcher/notebookServerProvider.node.ts b/src/kernels/jupyter/launcher/notebookServerProvider.node.ts index 99c91a6c869..26cda78a1f5 100644 --- a/src/kernels/jupyter/launcher/notebookServerProvider.node.ts +++ b/src/kernels/jupyter/launcher/notebookServerProvider.node.ts @@ -176,7 +176,7 @@ export class NotebookServerProvider implements IJupyterServerProvider { if (activeInterpreter) { const displayName = activeInterpreter.displayName ? activeInterpreter.displayName - : activeInterpreter.path; + : activeInterpreter.uri.fsPath; throw new Error( DataScience.jupyterNotSupportedBecauseOfEnvironment().format(displayName, e.toString()) ); diff --git a/src/kernels/kernelDependencyService.node.ts b/src/kernels/kernelDependencyService.node.ts index 140c003f0f0..ba940843e97 100644 --- a/src/kernels/kernelDependencyService.node.ts +++ b/src/kernels/kernelDependencyService.node.ts @@ -35,6 +35,7 @@ import { noop } from '../platform/common/utils/misc'; import { getResourceType } from '../platform/common/utils.node'; import { KernelProgressReporter } from '../platform/progress/kernelProgressReporter.node'; import { IRawNotebookSupportedService } from './raw/types'; +import { getComparisonKey } from '../platform/vscode-path/resources'; /** * Responsible for managing dependencies of a Python interpreter required to run as a Jupyter Kernel. @@ -64,9 +65,9 @@ export class KernelDependencyService implements IKernelDependencyService { ignoreCache?: boolean ): Promise { traceInfo( - `installMissingDependencies ${getDisplayPath(kernelConnection.interpreter?.path)}, ui.disabled=${ - ui.disableUI - } for resource ${getDisplayPath(resource)}` + `installMissingDependencies ${ + kernelConnection.interpreter?.uri ? getDisplayPath(kernelConnection.interpreter?.uri) : '' + }, ui.disabled=${ui.disableUI} for resource ${getDisplayPath(resource)}` ); if ( kernelConnection.kind === 'connectToLiveRemoteKernel' || @@ -88,7 +89,8 @@ export class KernelDependencyService implements IKernelDependencyService { } // Cache the install run - let promise = this.installPromises.get(kernelConnection.interpreter.path); + const key = getComparisonKey(kernelConnection.interpreter.uri); + let promise = this.installPromises.get(key); let cancelTokenSource: CancellationTokenSource | undefined; if (!promise) { const cancelTokenSource = new CancellationTokenSource(); @@ -107,7 +109,7 @@ export class KernelDependencyService implements IKernelDependencyService { cancelTokenSource.dispose(); }) .catch(noop); - this.installPromises.set(kernelConnection.interpreter.path, promise); + this.installPromises.set(key, promise); } // Get the result of the question @@ -124,7 +126,7 @@ export class KernelDependencyService implements IKernelDependencyService { dependencyResponse = KernelInterpreterDependencyResponse.failed; } finally { // Don't need to cache anymore - this.installPromises.delete(kernelConnection.interpreter.path); + this.installPromises.delete(key); } return dependencyResponse; } @@ -152,7 +154,7 @@ export class KernelDependencyService implements IKernelDependencyService { isModulePresentInEnvironmentCache(this.memento, Product.ipykernel, kernelConnection.interpreter) ) { traceInfo( - `IPyKernel found previously in this environment ${getDisplayPath(kernelConnection.interpreter.path)}` + `IPyKernel found previously in this environment ${getDisplayPath(kernelConnection.interpreter.uri)}` ); return true; } @@ -203,7 +205,7 @@ export class KernelDependencyService implements IKernelDependencyService { : DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter(); const products = isPipAvailableForNonConda === false ? [Product.ipykernel, Product.pip] : [Product.ipykernel]; const message = messageFormat.format( - interpreter.displayName || interpreter.path, + interpreter.displayName || interpreter.uri.fsPath, products.map((product) => ProductNames.get(product)!).join(` ${Common.and()} `) ); const productNameForTelemetry = products.map((product) => ProductNames.get(product)!).join(', '); diff --git a/src/kernels/raw/finder/jupyterPaths.node.ts b/src/kernels/raw/finder/jupyterPaths.node.ts index 9dd7c0ce531..3e8ce0e664c 100644 --- a/src/kernels/raw/finder/jupyterPaths.node.ts +++ b/src/kernels/raw/finder/jupyterPaths.node.ts @@ -4,20 +4,18 @@ import { inject, injectable, named } from 'inversify'; import * as path from '../../../platform/vscode-path/path'; -import { CancellationToken, Memento } from 'vscode'; +import * as uriPath from '../../../platform/vscode-path/resources'; +import { CancellationToken, Memento, Uri } from 'vscode'; import { IPlatformService } from '../../../platform/common/platform/types'; import { IFileSystem } from '../../../platform/common/platform/types.node'; import { traceError } from '../../../platform/logging'; -import { - IPathUtils, - IDisposableRegistry, - IMemento, - GLOBAL_MEMENTO, - IExtensionContext -} from '../../../platform/common/types'; +import { IDisposableRegistry, IMemento, GLOBAL_MEMENTO, IExtensionContext } from '../../../platform/common/types'; import { tryGetRealPath } from '../../../platform/common/utils.node'; import { IEnvironmentVariablesProvider } from '../../../platform/common/variables/types'; import { traceDecoratorVerbose } from '../../../platform/logging'; +import { getUserHomeDir } from '../../../platform/common/utils/platform.node'; +import { fsPathToUri } from '../../../platform/vscode-path/utils'; +import { ResourceSet } from '../../../platform/vscode-path/map'; const winJupyterPath = path.join('AppData', 'Roaming', 'jupyter', 'kernels'); const linuxJupyterPath = path.join('.local', 'share', 'jupyter', 'kernels'); @@ -31,11 +29,10 @@ const CACHE_KEY_FOR_JUPYTER_PATHS = 'CACHE_KEY_FOR_JUPYTER_PATHS_.'; @injectable() export class JupyterPaths { - private cachedKernelSpecRootPath?: Promise; - private cachedJupyterPaths?: Promise; + private cachedKernelSpecRootPath?: Promise; + private cachedJupyterPaths?: Promise; constructor( @inject(IPlatformService) private platformService: IPlatformService, - @inject(IPathUtils) private readonly pathUtils: IPathUtils, @inject(IEnvironmentVariablesProvider) private readonly envVarsProvider: IEnvironmentVariablesProvider, @inject(IDisposableRegistry) disposables: IDisposableRegistry, @inject(IMemento) @named(GLOBAL_MEMENTO) private readonly globalState: Memento, @@ -56,8 +53,8 @@ export class JupyterPaths { * (this way we don't register kernels in global path). */ public async getKernelSpecTempRegistrationFolder() { - const dir = path.join(this.context.extensionUri.fsPath, 'temp', 'jupyter', 'kernels'); - await this.fs.ensureLocalDir(dir); + const dir = uriPath.joinPath(this.context.extensionUri, 'temp', 'jupyter', 'kernels'); + await this.fs.ensureLocalDir(dir.fsPath); return dir; } /** @@ -65,27 +62,28 @@ export class JupyterPaths { * here: https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs */ @traceDecoratorVerbose('Getting Jupyter KernelSpec Root Path') - public async getKernelSpecRootPath(): Promise { + public async getKernelSpecRootPath(): Promise { this.cachedKernelSpecRootPath = this.cachedKernelSpecRootPath || (async () => { - if (this.platformService.isWindows) { - // On windows the path is not correct if we combine those variables. - // It won't point to a path that you can actually read from. - return tryGetRealPath(path.join(this.pathUtils.home, winJupyterPath)); - } else if (this.platformService.isMac) { - return path.join(this.pathUtils.home, macJupyterPath); - } else { - return path.join(this.pathUtils.home, linuxJupyterPath); + const userHomeDir = getUserHomeDir(); + if (userHomeDir) { + if (this.platformService.isWindows) { + // On windows the path is not correct if we combine those variables. + // It won't point to a path that you can actually read from. + return tryGetRealPath(uriPath.joinPath(userHomeDir, winJupyterPath)); + } else if (this.platformService.isMac) { + return uriPath.joinPath(userHomeDir, macJupyterPath); + } else { + return uriPath.joinPath(userHomeDir, linuxJupyterPath); + } } })(); void this.cachedKernelSpecRootPath.then((value) => { - if (value) { - void this.globalState.update(CACHE_KEY_FOR_JUPYTER_KERNELSPEC_ROOT_PATH, value); - } + void this.updateCachedRootPath(value); }); - if (this.globalState.get(CACHE_KEY_FOR_JUPYTER_KERNELSPEC_ROOT_PATH)) { - return this.globalState.get(CACHE_KEY_FOR_JUPYTER_KERNELSPEC_ROOT_PATH); + if (this.getCachedRootPath()) { + return this.getCachedRootPath(); } return this.cachedKernelSpecRootPath; } @@ -93,18 +91,21 @@ export class JupyterPaths { * Returns the value for `JUPYTER_RUNTIME_DIR`, location where Jupyter stores runtime files. * Such as kernel connection files. */ - public async getRuntimeDir() { - let runtimeDir: string | undefined = ''; - if (this.platformService.isWindows) { - // On windows the path is not correct if we combine those variables. - // It won't point to a path that you can actually read from. - runtimeDir = await tryGetRealPath(path.join(this.pathUtils.home, winJupyterRuntimePath)); - } else if (this.platformService.isMac) { - runtimeDir = path.join(this.pathUtils.home, macJupyterRuntimePath); - } else { - runtimeDir = process.env['$XDG_RUNTIME_DIR'] - ? path.join(process.env['$XDG_RUNTIME_DIR'], 'jupyter') - : path.join(this.pathUtils.home, '.local', 'share', 'jupyter'); + public async getRuntimeDir(): Promise { + let runtimeDir: Uri | undefined; + const userHomeDir = getUserHomeDir(); + if (userHomeDir) { + if (this.platformService.isWindows) { + // On windows the path is not correct if we combine those variables. + // It won't point to a path that you can actually read from. + runtimeDir = await tryGetRealPath(uriPath.joinPath(userHomeDir, winJupyterRuntimePath)); + } else if (this.platformService.isMac) { + runtimeDir = uriPath.joinPath(userHomeDir, macJupyterRuntimePath); + } else { + runtimeDir = process.env['$XDG_RUNTIME_DIR'] + ? fsPathToUri(path.join(process.env['$XDG_RUNTIME_DIR'], 'jupyter')) + : uriPath.joinPath(userHomeDir, '.local', 'share', 'jupyter'); + } } if (!runtimeDir) { traceError(`Failed to determine Jupyter runtime directory`); @@ -112,8 +113,9 @@ export class JupyterPaths { } try { - if (!(await this.fs.localDirectoryExists(runtimeDir))) { - await this.fs.ensureLocalDir(runtimeDir); + // Make sure the local file exists + if (!(await this.fs.localDirectoryExists(runtimeDir.fsPath))) { + await this.fs.ensureLocalDir(runtimeDir.fsPath); } return runtimeDir; } catch (ex) { @@ -125,9 +127,9 @@ export class JupyterPaths { * https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs */ @traceDecoratorVerbose('Get Kernelspec root path') - public async getKernelSpecRootPaths(cancelToken?: CancellationToken): Promise { + public async getKernelSpecRootPaths(cancelToken?: CancellationToken): Promise { // Paths specified in JUPYTER_PATH are supposed to come first in searching - const paths = new Set(await this.getJupyterPathPaths(cancelToken)); + const paths = new ResourceSet(await this.getJupyterPathPaths(cancelToken)); if (this.platformService.isWindows) { const winPath = await this.getKernelSpecRootPath(); @@ -136,15 +138,18 @@ export class JupyterPaths { } if (process.env.ALLUSERSPROFILE) { - paths.add(path.join(process.env.ALLUSERSPROFILE, 'jupyter', 'kernels')); + paths.add(Uri.file(path.join(process.env.ALLUSERSPROFILE, 'jupyter', 'kernels'))); } } else { // Unix based const secondPart = this.platformService.isMac ? macJupyterPath : linuxJupyterPath; - paths.add(path.join('/', 'usr', 'share', 'jupyter', 'kernels')); - paths.add(path.join('/', 'usr', 'local', 'share', 'jupyter', 'kernels')); - paths.add(path.join(this.pathUtils.home, secondPart)); + paths.add(Uri.file(path.join('/', 'usr', 'share', 'jupyter', 'kernels'))); + paths.add(Uri.file(path.join('/', 'usr', 'local', 'share', 'jupyter', 'kernels'))); + const userHome = getUserHomeDir(); + if (userHome) { + paths.add(uriPath.joinPath(userHome, secondPart)); + } } return Array.from(paths); @@ -156,11 +161,11 @@ export class JupyterPaths { * https://jupyter.readthedocs.io/en/latest/projects/jupyter-directories.html#envvar-JUPYTER_PATH */ @traceDecoratorVerbose('Get Jupyter Paths') - private async getJupyterPathPaths(cancelToken?: CancellationToken): Promise { + private async getJupyterPathPaths(cancelToken?: CancellationToken): Promise { this.cachedJupyterPaths = this.cachedJupyterPaths || (async () => { - const paths = new Set(); + const paths = new ResourceSet(); const vars = await this.envVarsProvider.getEnvironmentVariables(); if (cancelToken?.isCancellationRequested) { return []; @@ -173,7 +178,7 @@ export class JupyterPaths { if (jupyterPathVars.length > 0) { jupyterPathVars.forEach(async (jupyterPath) => { - const realPath = await tryGetRealPath(jupyterPath); + const realPath = await tryGetRealPath(Uri.file(jupyterPath)); if (realPath) { paths.add(realPath); } @@ -184,12 +189,37 @@ export class JupyterPaths { })(); void this.cachedJupyterPaths.then((value) => { if (value.length > 0) { - void this.globalState.update(CACHE_KEY_FOR_JUPYTER_PATHS, value); + void this.updateCachedPaths(value); + } + if (this.getCachedPaths().length > 0) { + return this.getCachedPaths(); } }); - if (this.globalState.get(CACHE_KEY_FOR_JUPYTER_PATHS, []).length > 0) { - return this.globalState.get(CACHE_KEY_FOR_JUPYTER_PATHS, []); - } return this.cachedJupyterPaths; } + + private getCachedPaths(): Uri[] { + return this.globalState.get(CACHE_KEY_FOR_JUPYTER_PATHS, []).map((s) => Uri.parse(s)); + } + + private updateCachedPaths(paths: Uri[]) { + return this.globalState.update(CACHE_KEY_FOR_JUPYTER_PATHS, paths.map(Uri.toString)); + } + + private getCachedRootPath(): Uri | undefined { + if (this.globalState.get(CACHE_KEY_FOR_JUPYTER_KERNELSPEC_ROOT_PATH)) { + const cached = this.globalState.get(CACHE_KEY_FOR_JUPYTER_KERNELSPEC_ROOT_PATH); + if (cached) { + return Uri.parse(cached); + } + } + } + + private updateCachedRootPath(path: Uri | undefined) { + if (path) { + void this.globalState.update(CACHE_KEY_FOR_JUPYTER_KERNELSPEC_ROOT_PATH, path.toString()); + } else { + void this.globalState.update(CACHE_KEY_FOR_JUPYTER_KERNELSPEC_ROOT_PATH, undefined); + } + } } diff --git a/src/kernels/raw/finder/localKernelFinder.node.ts b/src/kernels/raw/finder/localKernelFinder.node.ts index 01d208f413f..8b8c895a268 100644 --- a/src/kernels/raw/finder/localKernelFinder.node.ts +++ b/src/kernels/raw/finder/localKernelFinder.node.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; @@ -38,6 +39,29 @@ import { swallowExceptions } from '../../../platform/common/utils/decorators'; import { noop } from '../../../platform/common/utils/misc'; import { getResourceType } from '../../../platform/common/utils.node'; import { TraceOptions } from '../../../platform/logging/types'; +import { deserializePythonEnvironment, serializePythonEnvironment } from '../../../platform/api/pythonApi.node'; +import { isArray } from '../../../platform/common/utils/sysTypes'; + +function serializeKernelConnection(kernelConnection: LocalKernelConnectionMetadata) { + if (kernelConnection.interpreter) { + return { + ...kernelConnection, + interpreter: serializePythonEnvironment(kernelConnection.interpreter)! + }; + } + return kernelConnection; +} + +function deserializeKernelConnection(kernelConnection: any): LocalKernelConnectionMetadata { + if (kernelConnection.interpreter) { + return { + ...kernelConnection, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + interpreter: deserializePythonEnvironment(kernelConnection.interpreter as any)! + }; + } + return kernelConnection; +} const GlobalKernelSpecsCacheKey = 'JUPYTER_GLOBAL_KERNELSPECS_V2'; const LocalKernelSpecConnectionsCacheKey = 'LOCAL_KERNEL_SPEC_CONNECTIONS_CACHE_KEY_V2'; @@ -78,7 +102,7 @@ export class LocalKernelFinder implements ILocalKernelFinder { if ( preferredKernelFromCache && preferredKernelFromCache.interpreter && - (await this.fs.localFileExists(preferredKernelFromCache.interpreter.path)) + (await this.fs.localFileExists(preferredKernelFromCache.interpreter.uri.fsPath)) ) { traceInfo(`Preferred kernel connection found in cache ${preferredKernelFromCache.id}`); return preferredKernelFromCache; @@ -146,12 +170,17 @@ export class LocalKernelFinder implements ILocalKernelFinder { kernelsRetrievedFromCache = true; }) .catch(noop); - await Promise.race([kernelsFromCachePromise, kernelsWithoutCachePromise]); - // If we finish the cache first, and we don't have any items, in the cache, then load without cache. - if (Array.isArray(kernelsFromCache) && kernelsFromCache.length > 0) { - kernels = kernelsFromCache; - } else { - kernels = await kernelsWithoutCachePromise; + + try { + await Promise.race([kernelsFromCachePromise, kernelsWithoutCachePromise]); + // If we finish the cache first, and we don't have any items, in the cache, then load without cache. + if (Array.isArray(kernelsFromCache) && kernelsFromCache.length > 0) { + kernels = kernelsFromCache; + } else { + kernels = await kernelsWithoutCachePromise; + } + } catch (ex) { + traceError(`Exception loading kernels: ${ex}`); } } @@ -167,11 +196,11 @@ export class LocalKernelFinder implements ILocalKernelFinder { @swallowExceptions('CacheLocalKernelConnections') private async cacheLocalKernelConnections(kernels: LocalKernelConnectionMetadata[]) { - const items = this.globalState.get(LocalKernelSpecConnectionsCacheKey, []); + const items = this.getFromCache(LocalKernelSpecConnectionsCacheKey); const uniqueItems = new Map(); items.forEach((item) => uniqueItems.set(item.id, item)); kernels.forEach((item) => uniqueItems.set(item.id, item)); - await this.globalState.update(LocalKernelSpecConnectionsCacheKey, Array.from(uniqueItems.values())); + await this.updateCache(LocalKernelSpecConnectionsCacheKey, Array.from(uniqueItems.values())); } public findPreferredLocalKernelConnectionFromCache( notebookMetadata?: nbformat.INotebookMetadata @@ -183,7 +212,7 @@ export class LocalKernelFinder implements ILocalKernelFinder { if (!interpreterHash) { return; } - const items = this.globalState.get(LocalKernelSpecConnectionsCacheKey, []); + const items = this.getFromCache(LocalKernelSpecConnectionsCacheKey); const preferredKernel = items.find( (item) => item.interpreter && getInterpreterHash(item.interpreter) === interpreterHash ); @@ -205,28 +234,41 @@ export class LocalKernelFinder implements ILocalKernelFinder { const kernels = this.filterKernels(nonPythonKernelSpecs.concat(pythonRelatedKernelSpecs)); this.lastFetchedKernelsWithoutCache = kernels; - this.globalState.update(GlobalKernelSpecsCacheKey, kernels).then(noop, (ex) => { + this.updateCache(GlobalKernelSpecsCacheKey, kernels).then(noop, (ex) => { console.error('Failed to update global kernel cache', ex); }); return kernels; } + private getFromCache(cacheKey: string): LocalKernelConnectionMetadata[] { + const values = this.globalState.get(cacheKey, []); + if (values && isArray(values)) { + return values.map(deserializeKernelConnection); + } + return []; + } + + private async updateCache(cacheKey: string, values: LocalKernelConnectionMetadata[]) { + const serialized = values.map(serializeKernelConnection); + return this.globalState.update(cacheKey, serialized); + } + private async listValidKernelsFromGlobalCache( cancelToken?: CancellationToken ): Promise { const values = this.lastFetchedKernelsWithoutCache.length ? this.lastFetchedKernelsWithoutCache - : this.globalState.get(GlobalKernelSpecsCacheKey, []); + : this.getFromCache(GlobalKernelSpecsCacheKey); const validValues: LocalKernelConnectionMetadata[] = []; const promise = Promise.all( values.map(async (item) => { let somethingIsInvalid = false; const promises: Promise[] = []; - if (item.interpreter?.path) { + if (item.interpreter?.uri && item.interpreter?.uri.fsPath) { // Possible the interpreter no longer exists, in such cases, exclude this cached kernel from the list. promises.push( this.fs - .localFileExists(item.interpreter.path) + .localFileExists(item.interpreter.uri.fsPath) .then((exists) => { if (!exists) { somethingIsInvalid = true; diff --git a/src/kernels/raw/finder/localKernelSpecFinderBase.node.ts b/src/kernels/raw/finder/localKernelSpecFinderBase.node.ts index f39daa9949c..ac8670ac02e 100644 --- a/src/kernels/raw/finder/localKernelSpecFinderBase.node.ts +++ b/src/kernels/raw/finder/localKernelSpecFinderBase.node.ts @@ -3,7 +3,8 @@ 'use strict'; import * as path from '../../../platform/vscode-path/path'; -import { CancellationToken, Memento } from 'vscode'; +import * as uriPath from '../../../platform/vscode-path/resources'; +import { CancellationToken, Memento, Uri } from 'vscode'; import { IPythonExtensionChecker } from '../../../platform/api/types'; import { IWorkspaceService } from '../../../platform/common/application/types'; import { PYTHON_LANGUAGE } from '../../../platform/common/constants'; @@ -12,7 +13,7 @@ import { getDisplayPath } from '../../../platform/common/platform/fs-paths'; import { IFileSystem } from '../../../platform/common/platform/types.node'; import { ReadWrite } from '../../../platform/common/types'; import { testOnlyMethod } from '../../../platform/common/utils/decorators'; -import { noop } from '../../../platform/common/utils/misc'; +import { isUri, noop } from '../../../platform/common/utils/misc'; import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { getInterpreterKernelSpecName, getKernelRegistrationInfo } from '../../../kernels/helpers.node'; import { @@ -21,8 +22,9 @@ import { PythonKernelConnectionMetadata } from '../../../kernels/types'; import { JupyterKernelSpec } from '../../jupyter/jupyterKernelSpec.node'; +import { getComparisonKey } from '../../../platform/vscode-path/resources'; -type KernelSpecFileWithContainingInterpreter = { interpreter?: PythonEnvironment; kernelSpecFile: string }; +type KernelSpecFileWithContainingInterpreter = { interpreter?: PythonEnvironment; kernelSpecFile: Uri }; export const isDefaultPythonKernelSpecSpecName = /python\s\d*.?\d*$/; export const oldKernelsSpecFolderName = '__old_vscode_kernelspecs'; @@ -121,28 +123,29 @@ export abstract class LocalKernelSpecFinderBase { * Load the IJupyterKernelSpec for a given spec path, check the ones that we have already loaded first */ protected async getKernelSpec( - specPath: string, + specPath: Uri, interpreter?: PythonEnvironment, - globalSpecRootPath?: string, + globalSpecRootPath?: Uri, cancelToken?: CancellationToken ): Promise { // This is a backup folder for old kernels created by us. - if (specPath.includes(oldKernelsSpecFolderName)) { + if (specPath.fsPath.includes(oldKernelsSpecFolderName)) { return; } + const key = getComparisonKey(specPath); // If we have not already loaded this kernel spec, then load it - if (!this.pathToKernelSpec.has(specPath)) { - this.pathToKernelSpec.set(specPath, this.loadKernelSpec(specPath, interpreter, cancelToken)); + if (!this.pathToKernelSpec.has(key)) { + this.pathToKernelSpec.set(key, this.loadKernelSpec(specPath, interpreter, cancelToken)); } // ! as the has and set above verify that we have a return here - return this.pathToKernelSpec.get(specPath)!.then((kernelSpec) => { + return this.pathToKernelSpec.get(key)!.then((kernelSpec) => { // Delete old kernelSpecs that we created in the global kernelSpecs folder. const shouldDeleteKernelSpec = kernelSpec && globalSpecRootPath && getKernelRegistrationInfo(kernelSpec) && kernelSpec.specFile && - path.dirname(path.dirname(kernelSpec.specFile)) === globalSpecRootPath; + uriPath.isEqualOrParent(Uri.file(kernelSpec.specFile), globalSpecRootPath); if (kernelSpec && !shouldDeleteKernelSpec) { return kernelSpec; } @@ -153,8 +156,8 @@ export abstract class LocalKernelSpecFinderBase { } // If we failed to get a kernelSpec full path from our cache and loaded list - this.pathToKernelSpec.delete(specPath); - this.cache = this.cache?.filter((itemPath) => itemPath.kernelSpecFile !== specPath); + this.pathToKernelSpec.delete(key); + this.cache = this.cache?.filter((itemPath) => uriPath.isEqual(itemPath.kernelSpecFile, specPath)); return undefined; }); } @@ -174,7 +177,7 @@ export abstract class LocalKernelSpecFinderBase { * Load kernelspec json from disk */ private async loadKernelSpec( - specPath: string, + specPath: Uri, interpreter?: PythonEnvironment, cancelToken?: CancellationToken ): Promise { @@ -182,17 +185,17 @@ export abstract class LocalKernelSpecFinderBase { } // Given a set of paths, search for kernel.json files and return back the full paths of all of them that we find protected async findKernelSpecsInPaths( - paths: (string | { interpreter: PythonEnvironment; kernelSearchPath: string })[], + paths: (Uri | { interpreter: PythonEnvironment; kernelSearchPath: Uri })[], cancelToken?: CancellationToken ): Promise { const searchResults = await Promise.all( paths.map(async (searchItem) => { - const searchPath = typeof searchItem === 'string' ? searchItem : searchItem.kernelSearchPath; - if (await this.fs.localDirectoryExists(searchPath)) { - const files = await this.fs.searchLocal(`**/kernel.json`, searchPath, true); + const searchPath = isUri(searchItem) ? searchItem : searchItem.kernelSearchPath; + if (await this.fs.localDirectoryExists(searchPath.fsPath)) { + const files = await this.fs.searchLocal(`**/kernel.json`, searchPath.fsPath, true); return { - interpreter: typeof searchItem === 'string' ? undefined : searchItem.interpreter, - kernelSpecFiles: files.map((item) => path.join(searchPath, item)) + interpreter: isUri(searchItem) ? undefined : searchItem.interpreter, + kernelSpecFiles: files.map((item) => uriPath.joinPath(searchPath, item)) }; } }) @@ -217,19 +220,23 @@ export abstract class LocalKernelSpecFinderBase { * Load kernelspec json from disk */ export async function loadKernelSpec( - specPath: string, + specPath: Uri, fs: IFileSystem, interpreter?: PythonEnvironment, cancelToken?: CancellationToken ): Promise { // This is a backup folder for old kernels created by us. - if (specPath.includes(oldKernelsSpecFolderName)) { + if (specPath.fsPath.includes(oldKernelsSpecFolderName)) { return; } let kernelJson: ReadWrite; try { - traceVerbose(`Loading kernelspec from ${getDisplayPath(specPath)} for ${getDisplayPath(interpreter?.path)}`); - kernelJson = JSON.parse(await fs.readLocalFile(specPath)); + traceVerbose( + `Loading kernelspec from ${getDisplayPath(specPath)} for ${ + interpreter?.uri ? getDisplayPath(interpreter.uri) : '' + }` + ); + kernelJson = JSON.parse(await fs.readLocalFile(specPath.fsPath)); } catch (ex) { traceError(`Failed to parse kernelspec ${specPath}`, ex); return; @@ -261,7 +268,7 @@ export async function loadKernelSpec( kernelJson.metadata = kernelJson.metadata || {}; kernelJson.metadata.vscode = kernelJson.metadata.vscode || {}; if (!kernelJson.metadata.vscode.originalSpecFile) { - kernelJson.metadata.vscode.originalSpecFile = specPath; + kernelJson.metadata.vscode.originalSpecFile = specPath.fsPath; } if (!kernelJson.metadata.vscode.originalDisplayName) { kernelJson.metadata.vscode.originalDisplayName = kernelJson.display_name; @@ -274,17 +281,17 @@ export async function loadKernelSpec( const kernelSpec: IJupyterKernelSpec = new JupyterKernelSpec( // eslint-disable-next-line @typescript-eslint/no-explicit-any kernelJson as any, - specPath, + specPath.fsPath, // Interpreter information may be saved in the metadata (if this is a kernel spec created/registered by us). - interpreter?.path || kernelJson?.metadata?.interpreter?.path, + interpreter?.uri.fsPath || kernelJson?.metadata?.interpreter?.path, getKernelRegistrationInfo(kernelJson) ); // Some registered kernel specs do not have a name, in this case use the last part of the path - kernelSpec.name = kernelJson?.name || path.basename(path.dirname(specPath)); + kernelSpec.name = kernelJson?.name || path.basename(path.dirname(specPath.fsPath)); // Possible user deleted the underlying kernel. - const interpreterPath = interpreter?.path || kernelJson?.metadata?.interpreter?.path; + const interpreterPath = interpreter?.uri.fsPath || kernelJson?.metadata?.interpreter?.path; if (interpreterPath && !(await fs.localFileExists(interpreterPath))) { return; } diff --git a/src/kernels/raw/finder/localKnownPathKernelSpecFinder.node.ts b/src/kernels/raw/finder/localKnownPathKernelSpecFinder.node.ts index ad7d6b134ba..10f3d7f1d5b 100644 --- a/src/kernels/raw/finder/localKnownPathKernelSpecFinder.node.ts +++ b/src/kernels/raw/finder/localKnownPathKernelSpecFinder.node.ts @@ -122,7 +122,7 @@ export class LocalKnownPathKernelSpecFinder extends LocalKernelSpecFinderBase { const byDisplayName = new Map(); results.forEach((r) => { const existing = byDisplayName.get(r.display_name); - if (existing && existing.path !== r.path) { + if (existing && existing.uri !== r.uri) { // This item is a dupe but has a different path to start the exe unique.push(r); } else if (!existing) { diff --git a/src/kernels/raw/finder/localPythonAndRelatedNonPythonKernelSpecFinder.node.ts b/src/kernels/raw/finder/localPythonAndRelatedNonPythonKernelSpecFinder.node.ts index 905b189df26..d84eb3529b3 100644 --- a/src/kernels/raw/finder/localPythonAndRelatedNonPythonKernelSpecFinder.node.ts +++ b/src/kernels/raw/finder/localPythonAndRelatedNonPythonKernelSpecFinder.node.ts @@ -4,7 +4,8 @@ import { inject, injectable, named } from 'inversify'; import * as path from '../../../platform/vscode-path/path'; -import { CancellationToken, Memento } from 'vscode'; +import * as uriPath from '../../../platform/vscode-path/resources'; +import { CancellationToken, Memento, Uri } from 'vscode'; import { createInterpreterKernelSpec, getKernelId, getKernelRegistrationInfo } from '../../../kernels/helpers.node'; import { IJupyterKernelSpec, @@ -18,7 +19,7 @@ import { IPythonExtensionChecker } from '../../../platform/api/types'; import { IWorkspaceService } from '../../../platform/common/application/types'; import { PYTHON_LANGUAGE } from '../../../platform/common/constants'; import { traceInfoIfCI, traceVerbose, traceError } from '../../../platform/logging'; -import { getDisplayPath } from '../../../platform/common/platform/fs-paths'; +import { getDisplayPath, getDisplayPathFromLocalFile } from '../../../platform/common/platform/fs-paths.node'; import { IFileSystem } from '../../../platform/common/platform/types.node'; import { IMemento, GLOBAL_MEMENTO, Resource } from '../../../platform/common/types'; import { IInterpreterService } from '../../../platform/interpreter/contracts.node'; @@ -26,6 +27,8 @@ import { areInterpreterPathsSame } from '../../../platform/pythonEnvironments/in import { captureTelemetry } from '../../../telemetry'; import { Telemetry } from '../../../webviews/webview-side/common/constants'; import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; +import { fsPathToUri } from '../../../platform/vscode-path/utils'; +import { ResourceSet } from '../../../platform/vscode-path/map'; export const isDefaultPythonKernelSpecName = /^python\d*.?\d*$/; @@ -237,7 +240,7 @@ export class LocalPythonAndRelatedNonPythonKernelSpecFinder extends LocalKernelS traceVerbose( `Hiding default kernel spec '${kernelspec.display_name}', '${ kernelspec.name - }', ${getDisplayPath(kernelspec.argv[0])}` + }', ${getDisplayPathFromLocalFile(kernelspec.argv[0])}` ); return false; } @@ -282,17 +285,19 @@ export class LocalPythonAndRelatedNonPythonKernelSpecFinder extends LocalKernelS if ( k.language === PYTHON_LANGUAGE && k.metadata?.interpreter?.path && - !areInterpreterPathsSame(k.metadata?.interpreter?.path, activeInterpreter?.path) + !areInterpreterPathsSame(Uri.file(k.metadata?.interpreter?.path), activeInterpreter?.uri) ) { try { interpreter = await this.interpreterService.getInterpreterDetails( - k.metadata?.interpreter?.path + Uri.file(k.metadata?.interpreter?.path) ); } catch (ex) { traceError( - `Failed to get interpreter details for Kernel Spec ${getDisplayPath( + `Failed to get interpreter details for Kernel Spec ${getDisplayPathFromLocalFile( k.specFile - )} with interpreter path ${getDisplayPath(k.metadata?.interpreter?.path)}`, + )} with interpreter path ${getDisplayPath( + Uri.file(k.metadata?.interpreter?.path) + )}`, ex ); return; @@ -337,7 +342,7 @@ export class LocalPythonAndRelatedNonPythonKernelSpecFinder extends LocalKernelS if (a.kernelSpec.display_name.toUpperCase() === b.kernelSpec.display_name.toUpperCase()) { return 0; } else if ( - areInterpreterPathsSame(a.interpreter?.path, activeInterpreter?.path) && + areInterpreterPathsSame(a.interpreter?.uri, activeInterpreter?.uri) && a.kernelSpec.display_name.toUpperCase() === activeInterpreter?.displayName?.toUpperCase() ) { return -1; @@ -360,7 +365,7 @@ export class LocalPythonAndRelatedNonPythonKernelSpecFinder extends LocalKernelS const exactMatch = interpreters.find((i) => { if ( kernelSpec.metadata?.interpreter?.path && - areInterpreterPathsSame(kernelSpec.metadata?.interpreter?.path, i.path, undefined, this.fs) + areInterpreterPathsSame(Uri.file(kernelSpec.metadata?.interpreter?.path), i.uri) ) { traceVerbose(`Kernel ${kernelSpec.name} matches ${i.displayName} based on metadata path.`); return true; @@ -375,7 +380,7 @@ export class LocalPythonAndRelatedNonPythonKernelSpecFinder extends LocalKernelS kernelSpec && Array.isArray(kernelSpec.argv) && kernelSpec.argv.length > 0 ? kernelSpec.argv[0] : undefined; if (pathInArgv && path.basename(pathInArgv) !== pathInArgv) { const exactMatchBasedOnArgv = interpreters.find((i) => { - if (areInterpreterPathsSame(pathInArgv, i.path, undefined, this.fs)) { + if (areInterpreterPathsSame(Uri.file(pathInArgv), i.uri)) { traceVerbose(`Kernel ${kernelSpec.name} matches ${i.displayName} based on path in argv.`); return true; } @@ -388,7 +393,7 @@ export class LocalPythonAndRelatedNonPythonKernelSpecFinder extends LocalKernelS // 3. Sometimes we have path paths such as `/usr/bin/python3.6` in the kernel spec. // & in the list of interpreters we have `/usr/bin/python3`, they are both the same. // Hence we need to ensure we take that into account (just get the interpreter info from Python extension). - const interpreterInArgv = await this.interpreterService.getInterpreterDetails(pathInArgv); + const interpreterInArgv = await this.interpreterService.getInterpreterDetails(Uri.file(pathInArgv)); if (interpreterInArgv) { return interpreterInArgv; } @@ -397,7 +402,10 @@ export class LocalPythonAndRelatedNonPythonKernelSpecFinder extends LocalKernelS // 4. Check if `interpreterPath` is defined in kernel metadata. if (kernelSpec.interpreterPath) { const matchBasedOnInterpreterPath = interpreters.find((i) => { - if (kernelSpec.interpreterPath && this.fs.areLocalPathsSame(kernelSpec.interpreterPath, i.path)) { + if ( + kernelSpec.interpreterPath && + areInterpreterPathsSame(fsPathToUri(kernelSpec.interpreterPath), i.uri) + ) { traceVerbose(`Kernel ${kernelSpec.name} matches ${i.displayName} based on interpreter path.`); return true; } @@ -428,7 +436,7 @@ export class LocalPythonAndRelatedNonPythonKernelSpecFinder extends LocalKernelS interpreters: PythonEnvironment[], cancelToken?: CancellationToken ): Promise { - traceInfoIfCI(`Finding kernel specs for interpreters: ${interpreters.map((i) => i.path).join('\n')}`); + traceInfoIfCI(`Finding kernel specs for interpreters: ${interpreters.map((i) => i.uri).join('\n')}`); // Find all the possible places to look for this resource const [interpreterPaths, rootSpecPaths, globalSpecRootPath] = await Promise.all([ this.findKernelPathsOfAllInterpreters(interpreters), @@ -440,7 +448,9 @@ export class LocalPythonAndRelatedNonPythonKernelSpecFinder extends LocalKernelS // But we could have a kernel spec in global path that points to a completely different interpreter. // We already have a way of identifying the interpreter associated with a global kernelspec. // Hence exclude global paths from the list of interpreter specific paths (as global paths are NOT interpreter specific). - const paths = interpreterPaths.filter((item) => !rootSpecPaths.includes(item.kernelSearchPath)); + const paths = interpreterPaths.filter( + (item) => !rootSpecPaths.find((i) => uriPath.isEqual(i, item.kernelSearchPath)) + ); const searchResults = await this.findKernelSpecsInPaths(paths, cancelToken); let results: IJupyterKernelSpec[] = []; @@ -478,7 +488,7 @@ export class LocalPythonAndRelatedNonPythonKernelSpecFinder extends LocalKernelS const byDisplayName = new Map(); results.forEach((r) => { const existing = byDisplayName.get(r.display_name); - if (existing && existing.path !== r.path) { + if (existing && existing.uri !== r.uri) { // This item is a dupe but has a different path to start the exe unique.push(r); } else if (!existing) { @@ -497,13 +507,13 @@ export class LocalPythonAndRelatedNonPythonKernelSpecFinder extends LocalKernelS */ private async findKernelPathsOfAllInterpreters( interpreters: PythonEnvironment[] - ): Promise<{ interpreter: PythonEnvironment; kernelSearchPath: string }[]> { - const kernelSpecPathsAlreadyListed = new Set(); + ): Promise<{ interpreter: PythonEnvironment; kernelSearchPath: Uri }[]> { + const kernelSpecPathsAlreadyListed = new ResourceSet(); return interpreters .map((interpreter) => { return { interpreter, - kernelSearchPath: path.join(interpreter.sysPrefix, baseKernelPath) + kernelSearchPath: Uri.file(path.join(interpreter.sysPrefix, baseKernelPath)) }; }) .filter((item) => { diff --git a/src/kernels/raw/finder/remoteKernelFinder.node.ts b/src/kernels/raw/finder/remoteKernelFinder.node.ts index 62efbca1808..13359c58cb9 100644 --- a/src/kernels/raw/finder/remoteKernelFinder.node.ts +++ b/src/kernels/raw/finder/remoteKernelFinder.node.ts @@ -6,7 +6,7 @@ import { Kernel } from '@jupyterlab/services'; import type * as nbformat from '@jupyterlab/nbformat'; import * as url from 'url'; import { injectable, inject } from 'inversify'; -import { CancellationToken } from 'vscode'; +import { CancellationToken, Uri } from 'vscode'; import { findPreferredKernel, getKernelId, getLanguageInNotebookMetadata } from '../../../kernels/helpers.node'; import { IJupyterKernelSpec, @@ -194,7 +194,7 @@ export class RemoteKernelFinder implements IRemoteKernelFinder { const parsed = new url.URL(baseUrl); if (parsed.hostname.toLocaleLowerCase() === 'localhost' || parsed.hostname === '127.0.0.1') { // Interpreter is possible. Same machine as VS code - return this.interpreterService.getInterpreterDetails(spec.argv[0]); + return this.interpreterService.getInterpreterDetails(Uri.file(spec.argv[0])); } } } diff --git a/src/kernels/raw/launcher/kernelEnvVarsService.node.ts b/src/kernels/raw/launcher/kernelEnvVarsService.node.ts index f05c75c4591..98e90d9e772 100644 --- a/src/kernels/raw/launcher/kernelEnvVarsService.node.ts +++ b/src/kernels/raw/launcher/kernelEnvVarsService.node.ts @@ -12,6 +12,7 @@ import { IEnvironmentActivationService } from '../../../platform/interpreter/act import { IInterpreterService } from '../../../platform/interpreter/contracts.node'; import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { IJupyterKernelSpec } from '../../types'; +import { Uri } from 'vscode'; @injectable() export class KernelEnvironmentVariablesService { @@ -43,7 +44,7 @@ export class KernelEnvironmentVariablesService { return kernelEnv; } interpreter = await this.interpreterService - .getInterpreterDetails(kernelSpec.interpreterPath) + .getInterpreterDetails(Uri.file(kernelSpec.interpreterPath)) .catch((ex) => { traceError('Failed to fetch interpreter information for interpreter that owns a kernel', ex); return undefined; @@ -69,7 +70,7 @@ export class KernelEnvironmentVariablesService { // Also applies to `!java` where java could be an executable in the conda bin directory. if (interpreter) { const env = kernelEnv || process.env; - this.envVarsService.prependPath(env, path.dirname(interpreter.path)); + this.envVarsService.prependPath(env, path.dirname(interpreter.uri.fsPath)); return env; } return kernelEnv; @@ -114,7 +115,7 @@ export class KernelEnvironmentVariablesService { // This way shell commands such as `!pip`, `!python` end up pointing to the right executables. // Also applies to `!java` where java could be an executable in the conda bin directory. if (interpreter) { - this.envVarsService.prependPath(mergedVars, path.dirname(interpreter.path)); + this.envVarsService.prependPath(mergedVars, path.dirname(interpreter.uri.fsPath)); } // Ensure global site_packages are not in the path for non global environments @@ -122,7 +123,7 @@ export class KernelEnvironmentVariablesService { // For more details see here https://github.com/microsoft/vscode-jupyter/issues/8553#issuecomment-997144591 // https://docs.python.org/3/library/site.html#site.ENABLE_USER_SITE if (interpreter && hasInterpreterEnv && hasActivationCommands) { - traceInfo(`Adding env Variable PYTHONNOUSERSITE to ${getDisplayPath(interpreter.path)}`); + traceInfo(`Adding env Variable PYTHONNOUSERSITE to ${getDisplayPath(interpreter.uri)}`); mergedVars.PYTHONNOUSERSITE = 'True'; } else { // Ensure this is not set (nor should this be inherited). diff --git a/src/kernels/raw/launcher/kernelLauncher.node.ts b/src/kernels/raw/launcher/kernelLauncher.node.ts index f0f5b5b8732..9bb6b99682c 100644 --- a/src/kernels/raw/launcher/kernelLauncher.node.ts +++ b/src/kernels/raw/launcher/kernelLauncher.node.ts @@ -33,6 +33,7 @@ import { KernelEnvironmentVariablesService } from './kernelEnvVarsService.node'; import { KernelProcess } from './kernelProcess.node'; import { JupyterPaths } from '../finder/jupyterPaths.node'; import { isTestExecution } from '../../../platform/common/constants.node'; +import { getDisplayPathFromLocalFile } from '../../../platform/common/platform/fs-paths.node'; const PortFormatString = `kernelLauncherPortStart_{0}.tmp`; // Launches and returns a kernel process given a resource or python interpreter. @@ -141,7 +142,7 @@ export class KernelLauncher implements IKernelLauncher { ], {} ); - const displayInterpreterPath = getDisplayPath(interpreter.path); + const displayInterpreterPath = getDisplayPath(interpreter.uri); if (output.stdout) { const outputs = output.stdout .trim() @@ -150,7 +151,9 @@ export class KernelLauncher implements IKernelLauncher { .filter((s) => s.length > 0); if (outputs.length === 2) { traceInfo(`ipykernel version ${outputs[0]} for ${displayInterpreterPath}`); - traceInfo(`ipykernel location ${getDisplayPath(outputs[1])} for ${displayInterpreterPath}`); + traceInfo( + `ipykernel location ${getDisplayPathFromLocalFile(outputs[1])} for ${displayInterpreterPath}` + ); } else { traceInfo(`ipykernel version & path ${output.stdout.trim()} for ${displayInterpreterPath}`); } diff --git a/src/kernels/raw/launcher/kernelProcess.node.ts b/src/kernels/raw/launcher/kernelProcess.node.ts index 10c7cd83d46..ac678078821 100644 --- a/src/kernels/raw/launcher/kernelProcess.node.ts +++ b/src/kernels/raw/launcher/kernelProcess.node.ts @@ -369,7 +369,9 @@ export class KernelProcess implements IKernelProcess { // Note: We have to dispose the temp file and recreate it else the file // system will hold onto the file with an open handle. THis doesn't work so well when // a different process tries to open it. - const connectionFile = runtimeDir ? path.join(runtimeDir, path.basename(tempFile.filePath)) : tempFile.filePath; + const connectionFile = runtimeDir + ? path.join(runtimeDir.fsPath, path.basename(tempFile.filePath)) + : tempFile.filePath; // Ensure we dispose this, and don't maintain a handle on this file. await tempFile.dispose(); // Do not remove this line. return connectionFile; diff --git a/src/kernels/raw/session/rawJupyterSession.node.ts b/src/kernels/raw/session/rawJupyterSession.node.ts index 50468a5d248..49b77b6b142 100644 --- a/src/kernels/raw/session/rawJupyterSession.node.ts +++ b/src/kernels/raw/session/rawJupyterSession.node.ts @@ -243,7 +243,7 @@ export class RawJupyterSession extends BaseJupyterSession { traceInfo( `Starting raw kernel ${getDisplayNameOrNameOfKernelConnection( this.kernelConnectionMetadata - )} for interpreter ${getDisplayPath(this.kernelConnectionMetadata.interpreter?.path)}` + )} for interpreter ${getDisplayPath(this.kernelConnectionMetadata.interpreter?.uri)}` ); this.terminatingStatus = undefined; diff --git a/src/kernels/raw/session/rawSession.node.ts b/src/kernels/raw/session/rawSession.node.ts index 6f9ec3e69df..31a5071b8f4 100644 --- a/src/kernels/raw/session/rawSession.node.ts +++ b/src/kernels/raw/session/rawSession.node.ts @@ -201,7 +201,7 @@ export class RawSession implements ISessionWithSocket { return { id: this._id, name: this._kernel.name, - path: this.kernelProcess.kernelConnectionMetadata.interpreter?.path || 'kernel_path', + path: this.kernelProcess.kernelConnectionMetadata.interpreter?.uri.fsPath || 'kernel_path', type: 'notebook', kernel: this._kernel.model }; diff --git a/src/kernels/types.ts b/src/kernels/types.ts index 602a060c5f6..a224471fdb5 100644 --- a/src/kernels/types.ts +++ b/src/kernels/types.ts @@ -20,6 +20,7 @@ import { PythonEnvironment } from '../platform/pythonEnvironments/info'; import { IAsyncDisposable, IDisplayOptions, Resource } from '../platform/common/types'; import { WebSocketData } from '../platform/api/extension'; import { IJupyterKernel } from './jupyter/types'; +import { PythonEnvironment_PythonApi } from '../platform/api/types'; export type LiveKernelModel = IJupyterKernel & Partial & { model: Session.IModel | undefined; notebook?: { path?: string } }; @@ -319,7 +320,7 @@ export interface IJupyterKernelSpec { id?: string; name: string; language?: string; - path: string; + uri: Uri; env?: NodeJS.ProcessEnv | undefined; /** * Kernel display name. @@ -348,7 +349,7 @@ export interface IJupyterKernelSpec { */ originalDisplayName?: string; }; - interpreter?: Partial; + interpreter?: Partial; // read from disk so has to follow old format /** * @deprecated (use metadata.jupyter.originalSpecFile) */ @@ -366,7 +367,7 @@ export interface IJupyterKernelSpec { * Then you could have kernels in `\share\jupyter\kernels` * Plenty of conda packages ship kernels in this manner (beakerx, etc). */ - interpreterPath?: string; + interpreterPath?: string; // Has to be a string as old kernelspecs wrote it this way readonly interrupt_mode?: 'message' | 'signal'; /** * Whether the kernelspec is registered by VS Code diff --git a/src/notebooks/controllers/kernelFilter/kernelFilterService.node.ts b/src/notebooks/controllers/kernelFilter/kernelFilterService.node.ts index 97f0d17ff7d..df8dce8fcf1 100644 --- a/src/notebooks/controllers/kernelFilter/kernelFilterService.node.ts +++ b/src/notebooks/controllers/kernelFilter/kernelFilterService.node.ts @@ -5,8 +5,9 @@ import { ConfigurationTarget, EventEmitter } from 'vscode'; import { IWorkspaceService } from '../../../platform/common/application/types'; import { disposeAllDisposables } from '../../../platform/common/helpers'; import { traceVerbose } from '../../../platform/logging'; -import { IConfigurationService, IDisposable, IDisposableRegistry, IPathUtils } from '../../../platform/common/types'; +import { IConfigurationService, IDisposable, IDisposableRegistry } from '../../../platform/common/types'; import { KernelConnectionMetadata } from '../../../kernels/types'; +import { getDisplayPath, getDisplayPathFromLocalFile } from '../../../platform/common/platform/fs-paths.node'; import { sendTelemetryEvent } from '../../../telemetry'; import { Telemetry } from '../../../platform/common/constants'; @@ -20,8 +21,7 @@ export class KernelFilterService implements IDisposable { constructor( @inject(IConfigurationService) private readonly config: IConfigurationService, @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IDisposableRegistry) disposales: IDisposableRegistry, - @inject(IPathUtils) private readonly pathUtils: IPathUtils + @inject(IDisposableRegistry) disposales: IDisposableRegistry ) { disposales.push(this); } @@ -45,14 +45,11 @@ export class KernelFilterService implements IDisposable { ) { return ( item.path.toLowerCase() === - this.pathUtils.getDisplayName(kernelConnection.kernelSpec.specFile).toLowerCase() + getDisplayPathFromLocalFile(kernelConnection.kernelSpec.specFile).toLowerCase() ); } if (kernelConnection.kind === 'startUsingPythonInterpreter' && item.type === 'pythonEnvironment') { - return ( - item.path.toLowerCase() === - this.pathUtils.getDisplayName(kernelConnection.interpreter.path).toLowerCase() - ); + return item.path.toLowerCase() === getDisplayPath(kernelConnection.interpreter.uri).toLowerCase(); } return false; }); @@ -109,12 +106,12 @@ export class KernelFilterService implements IDisposable { } if (connection.kind === 'startUsingLocalKernelSpec' && connection.kernelSpec.specFile) { return { - path: this.pathUtils.getDisplayName(connection.kernelSpec.specFile), + path: getDisplayPathFromLocalFile(connection.kernelSpec.specFile), type: 'jupyterKernelspec' }; } else if (connection.kind === 'startUsingPythonInterpreter') { return { - path: this.pathUtils.getDisplayName(connection.interpreter.path), + path: getDisplayPath(connection.interpreter.uri), type: 'pythonEnvironment' }; } diff --git a/src/notebooks/controllers/kernelFilter/kernelFilterUI.node.ts b/src/notebooks/controllers/kernelFilter/kernelFilterUI.node.ts index 42b4fe5ca0f..9369edd20d8 100644 --- a/src/notebooks/controllers/kernelFilter/kernelFilterUI.node.ts +++ b/src/notebooks/controllers/kernelFilter/kernelFilterUI.node.ts @@ -5,7 +5,7 @@ import { QuickPickItem } from 'vscode'; import { IExtensionSyncActivationService } from '../../../platform/activation/types'; import { ICommandManager, IApplicationShell, IWorkspaceService } from '../../../platform/common/application/types'; import { disposeAllDisposables } from '../../../platform/common/helpers'; -import { IDisposable, IDisposableRegistry, IPathUtils } from '../../../platform/common/types'; +import { IDisposable, IDisposableRegistry } from '../../../platform/common/types'; import { DataScience } from '../../../platform/common/utils/localize'; import { noop } from '../../../platform/common/utils/misc'; import { @@ -28,7 +28,6 @@ export class KernelFilterUI implements IExtensionSyncActivationService, IDisposa @inject(IApplicationShell) private readonly appShell: IApplicationShell, @inject(IDisposableRegistry) disposales: IDisposableRegistry, @inject(KernelFilterService) private readonly kernelFilter: KernelFilterService, - @inject(IPathUtils) private readonly pathUtils: IPathUtils, @inject(IWorkspaceService) private readonly workspace: IWorkspaceService ) { disposales.push(this); @@ -66,7 +65,7 @@ export class KernelFilterUI implements IExtensionSyncActivationService, IDisposa return { label: getDisplayNameOrNameOfKernelConnection(item), picked: !this.kernelFilter.isKernelHidden(item), - description: getKernelConnectionPath(item, this.pathUtils, this.workspace), + description: getKernelConnectionPath(item, this.workspace), detail: item.kind === 'connectToLiveRemoteKernel' ? getRemoteKernelSessionInformation(item) diff --git a/src/notebooks/controllers/notebookControllerManager.node.ts b/src/notebooks/controllers/notebookControllerManager.node.ts index 13d85b03a91..620f6356562 100644 --- a/src/notebooks/controllers/notebookControllerManager.node.ts +++ b/src/notebooks/controllers/notebookControllerManager.node.ts @@ -5,7 +5,7 @@ import { inject, injectable } from 'inversify'; import { CancellationToken, NotebookControllerAffinity, Uri } from 'vscode'; import { CancellationTokenSource, EventEmitter, NotebookDocument } from 'vscode'; import { IExtensionSyncActivationService } from '../../platform/activation/types'; -import { IPythonExtensionChecker, IPythonApiProvider } from '../../platform/api/types'; +import { IPythonExtensionChecker } from '../../platform/api/types'; import { IVSCodeNotebook, ICommandManager, @@ -29,7 +29,6 @@ import { IExtensions, IConfigurationService, IExtensionContext, - IPathUtils, IBrowserService, Resource } from '../../platform/common/types'; @@ -143,13 +142,11 @@ export class NotebookControllerManager implements INotebookControllerManager, IE @inject(PreferredRemoteKernelIdProvider) private readonly preferredRemoteKernelIdProvider: PreferredRemoteKernelIdProvider, @inject(IRemoteKernelFinder) private readonly remoteKernelFinder: IRemoteKernelFinder, - @inject(IPathUtils) private readonly pathUtils: IPathUtils, @inject(NotebookIPyWidgetCoordinator) private readonly widgetCoordinator: NotebookIPyWidgetCoordinator, @inject(NotebookCellLanguageService) private readonly languageService: NotebookCellLanguageService, @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, @inject(IPythonExtensionChecker) private readonly extensionChecker: IPythonExtensionChecker, @inject(IDocumentManager) private readonly docManager: IDocumentManager, - @inject(IPythonApiProvider) private readonly pythonApi: IPythonApiProvider, @inject(IInterpreterService) private readonly interpreters: IInterpreterService, @inject(IApplicationShell) private readonly appShell: IApplicationShell, @inject(KernelFilterService) private readonly kernelFilter: KernelFilterService, @@ -395,15 +392,14 @@ export class NotebookControllerManager implements INotebookControllerManager, IE resource: Resource ) { // Fetch the active interpreter and use the matching controller - const api = await this.pythonApi.getApi(); - const activeInterpreter = await api.getActiveInterpreter(resource); + const activeInterpreter = await this.interpreters.getActiveInterpreter(resource); if (!activeInterpreter) { traceWarning(`Unable to create a controller for ${notebookType} without an active interpreter.`); return; } traceVerbose( - `Creating controller for ${notebookType} with interpreter ${getDisplayPath(activeInterpreter.path)}` + `Creating controller for ${notebookType} with interpreter ${getDisplayPath(activeInterpreter.uri)}` ); return this.getOrCreateControllerForActiveInterpreter(activeInterpreter, notebookType); } @@ -823,7 +819,6 @@ export class NotebookControllerManager implements INotebookControllerManager, IE this.kernelProvider, this.preferredRemoteKernelIdProvider, this.context, - this.pathUtils, this.disposables, this.languageService, this.workspace, diff --git a/src/notebooks/controllers/vscodeNotebookController.node.ts b/src/notebooks/controllers/vscodeNotebookController.node.ts index 173f026dca0..abd9b10a296 100644 --- a/src/notebooks/controllers/vscodeNotebookController.node.ts +++ b/src/notebooks/controllers/vscodeNotebookController.node.ts @@ -44,8 +44,7 @@ import { IDisplayOptions, IDisposable, IDisposableRegistry, - IExtensionContext, - IPathUtils + IExtensionContext } from '../../platform/common/types'; import { createDeferred } from '../../platform/common/utils/async'; import { DataScience, Common } from '../../platform/common/utils/localize'; @@ -145,7 +144,6 @@ export class VSCodeNotebookController implements Disposable { private readonly kernelProvider: IKernelProvider, private readonly preferredRemoteKernelIdProvider: PreferredRemoteKernelIdProvider, private readonly context: IExtensionContext, - private readonly pathUtils: IPathUtils, disposableRegistry: IDisposableRegistry, private readonly languageService: NotebookCellLanguageService, private readonly workspace: IWorkspaceService, @@ -173,7 +171,7 @@ export class VSCodeNotebookController implements Disposable { // Fill in extended info for our controller this.controller.interruptHandler = this.handleInterrupt.bind(this); - this.controller.description = getKernelConnectionPath(kernelConnection, this.pathUtils, this.workspace); + this.controller.description = getKernelConnectionPath(kernelConnection, this.workspace); this.controller.detail = kernelConnection.kind === 'connectToLiveRemoteKernel' ? getRemoteKernelSessionInformation(kernelConnection) diff --git a/src/platform/api/extension.d.ts b/src/platform/api/extension.d.ts index 07aba62093e..be87b35c5f6 100644 --- a/src/platform/api/extension.d.ts +++ b/src/platform/api/extension.d.ts @@ -57,12 +57,12 @@ export type PythonVersion = { }; export type PythonEnvironment = { displayName?: string; - path: string; + uri: Uri; version?: PythonVersion; sysPrefix: string; envType?: EnvironmentType; envName?: string; - envPath?: string; + envPath?: Uri; }; /** diff --git a/src/platform/api/pythonApi.node.ts b/src/platform/api/pythonApi.node.ts index 97f3148b5ea..099d5f189ba 100644 --- a/src/platform/api/pythonApi.node.ts +++ b/src/platform/api/pythonApi.node.ts @@ -34,10 +34,12 @@ import { IPythonExtensionChecker, IPythonProposedApi, PythonApi, + PythonEnvironment_PythonApi, RefreshInterpretersOptions } from './types'; import { traceInfo, traceVerbose, traceError, traceDecoratorVerbose } from '../logging'; import { TraceOptions } from '../logging/types'; +import { fsPathToUri } from '../vscode-path/utils'; /* eslint-disable max-classes-per-file */ @injectable() @@ -239,6 +241,31 @@ export class InterpreterSelector implements IInterpreterSelector { } } +export function deserializePythonEnvironment( + pythonVersion: Partial | undefined +): PythonEnvironment | undefined { + if (pythonVersion) { + return { + ...pythonVersion, + sysPrefix: pythonVersion.sysPrefix || '', + uri: Uri.file(pythonVersion.path || ''), + envPath: fsPathToUri(pythonVersion.envPath) + }; + } +} + +export function serializePythonEnvironment( + jupyterVersion: PythonEnvironment | undefined +): PythonEnvironment_PythonApi | undefined { + if (jupyterVersion) { + return { + ...jupyterVersion, + path: jupyterVersion.uri.fsPath, + envPath: jupyterVersion.envPath?.fsPath + }; + } +} + // eslint-disable-next-line max-classes-per-file @injectable() export class InterpreterService implements IInterpreterService { @@ -311,7 +338,10 @@ export class InterpreterService implements IInterpreterService { const workspaceId = this.workspace.getWorkspaceFolderIdentifier(resource); let promise = this.workspaceCachedActiveInterpreter.get(workspaceId); if (!promise) { - promise = this.apiProvider.getApi().then((api) => api.getActiveInterpreter(resource)); + promise = this.apiProvider + .getApi() + .then((api) => api.getActiveInterpreter(resource)) + .then(deserializePythonEnvironment); if (promise) { this.workspaceCachedActiveInterpreter.set(workspaceId, promise); @@ -326,7 +356,7 @@ export class InterpreterService implements IInterpreterService { .then((item) => traceInfo( `Active Interpreter in Python API for ${resource?.toString()} is ${getDisplayPath( - item?.path + item?.uri )}` ) ) @@ -338,10 +368,13 @@ export class InterpreterService implements IInterpreterService { } @traceDecoratorVerbose('Get Interpreter details', TraceOptions.Arguments | TraceOptions.BeforeCall) - public async getInterpreterDetails(pythonPath: string, resource?: Uri): Promise { + public async getInterpreterDetails(pythonPath: Uri, resource?: Uri): Promise { this.hookupOnDidChangeInterpreterEvent(); try { - return await this.apiProvider.getApi().then((api) => api.getInterpreterDetails(pythonPath, resource)); + return await this.apiProvider + .getApi() + .then((api) => api.getInterpreterDetails(pythonPath.fsPath, resource)) + .then(deserializePythonEnvironment); } catch { // If the python extension cannot get the details here, don't fail. Just don't use them. return undefined; @@ -361,13 +394,14 @@ export class InterpreterService implements IInterpreterService { this.apiProvider.getApi().then((api) => api.getInterpreters(f?.uri)) ) ) - : await Promise.all([this.apiProvider.getApi().then((api) => api.getInterpreters(undefined))]); + : await Promise.all([await this.apiProvider.getApi().then((api) => api.getInterpreters(undefined))]); // Remove dupes const result: PythonEnvironment[] = []; all.flat().forEach((p) => { - if (!result.find((r) => areInterpreterPathsSame(r.path, p.path))) { - result.push(p); + const translated = deserializePythonEnvironment(p); + if (translated && !result.find((r) => areInterpreterPathsSame(r.uri, translated.uri))) { + result.push(translated); } }); return result; diff --git a/src/platform/api/types.ts b/src/platform/api/types.ts index b4471737c51..01c411ff585 100644 --- a/src/platform/api/types.ts +++ b/src/platform/api/types.ts @@ -5,9 +5,8 @@ import { Disposable, Event, Uri } from 'vscode'; import * as lsp from 'vscode-languageserver-protocol'; import { InterpreterUri, Resource } from '../common/types'; import { IInterpreterQuickPickItem } from '../interpreter/configuration/types'; -import { PythonEnvironment } from '../pythonEnvironments/info'; import type { SemVer } from 'semver'; -import { IExportedKernelService } from './extension'; +import { EnvironmentType, IExportedKernelService, PythonVersion } from './extension'; export type ILanguageServerConnection = Pick< lsp.ProtocolConnection, 'sendRequest' | 'sendNotification' | 'onProgress' | 'sendProgress' | 'onNotification' | 'onRequest' @@ -53,6 +52,21 @@ export interface IInterpreterStatusbarVisibilityFilter { readonly hidden: boolean; } +// Python extension still returns strings for paths +export type InterpreterInformation_PythonApi = { + path: string; + version?: PythonVersion; + sysVersion?: string; + sysPrefix: string; +}; + +export type PythonEnvironment_PythonApi = InterpreterInformation_PythonApi & { + displayName?: string; + envType?: EnvironmentType; + envName?: string; + envPath?: string; +}; + export type PythonApi = { /** * IInterpreterService @@ -62,22 +76,22 @@ export type PythonApi = { /** * IInterpreterService */ - getInterpreters(resource?: Uri): Promise; + getInterpreters(resource?: Uri): Promise; /** * IInterpreterService */ - getActiveInterpreter(resource?: Uri): Promise; + getActiveInterpreter(resource?: Uri): Promise; /** * IInterpreterService */ - getInterpreterDetails(pythonPath: string, resource?: Uri): Promise; + getInterpreterDetails(pythonPath: string, resource?: Uri): Promise; /** * IEnvironmentActivationService */ getActivatedEnvironmentVariables( resource: Resource, - interpreter: PythonEnvironment, + interpreter: PythonEnvironment_PythonApi, allowExceptions?: boolean ): Promise; /** @@ -108,7 +122,7 @@ export type PythonApi = { getCondaFile?(): Promise; getEnvironmentActivationShellCommands?( resource: Resource, - interpreter?: PythonEnvironment + interpreter?: PythonEnvironment_PythonApi ): Promise; /** * This API will re-trigger environment discovery. Extensions can wait on the returned diff --git a/src/platform/common/application/applicationEnvironment.base.ts b/src/platform/common/application/applicationEnvironment.base.ts index b4f78ff3006..8b66cc76d7d 100644 --- a/src/platform/common/application/applicationEnvironment.base.ts +++ b/src/platform/common/application/applicationEnvironment.base.ts @@ -7,8 +7,8 @@ import * as vscode from 'vscode'; import { Channel, IApplicationEnvironment } from './types'; export abstract class BaseApplicationEnvironment implements IApplicationEnvironment { - public abstract get userSettingsFile(): string | undefined; - public abstract get userCustomKeybindingsFile(): string | undefined; + public abstract get userSettingsFile(): vscode.Uri | undefined; + public abstract get userCustomKeybindingsFile(): vscode.Uri | undefined; public get appName(): string { return vscode.env.appName; } diff --git a/src/platform/common/application/applicationEnvironment.node.ts b/src/platform/common/application/applicationEnvironment.node.ts index e52355d6969..9fcfefee509 100644 --- a/src/platform/common/application/applicationEnvironment.node.ts +++ b/src/platform/common/application/applicationEnvironment.node.ts @@ -4,28 +4,32 @@ 'use strict'; import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; import * as path from '../../../platform/vscode-path/path'; +import * as uriPath from '../../../platform/vscode-path/resources'; import { IPlatformService } from '../platform/types'; -import { IExtensionContext, IPathUtils } from '../types'; +import { IExtensionContext } from '../types'; import { OSType } from '../utils/platform'; +import { getUserHomeDir } from '../utils/platform.node'; import { BaseApplicationEnvironment } from './applicationEnvironment.base'; @injectable() export class ApplicationEnvironment extends BaseApplicationEnvironment { + private homeDir = getUserHomeDir() || Uri.file(''); + constructor( @inject(IPlatformService) private readonly platform: IPlatformService, - @inject(IPathUtils) private readonly pathUtils: IPathUtils, @inject(IExtensionContext) private readonly extensionContext: IExtensionContext ) { super(); } - public get userSettingsFile(): string | undefined { + public get userSettingsFile(): Uri | undefined { const vscodeFolderName = this.channel === 'insiders' ? 'Code - Insiders' : 'Code'; switch (this.platform.osType) { case OSType.OSX: - return path.join( - this.pathUtils.home, + return uriPath.joinPath( + this.homeDir, 'Library', 'Application Support', vscodeFolderName, @@ -33,16 +37,16 @@ export class ApplicationEnvironment extends BaseApplicationEnvironment { 'settings.json' ); case OSType.Linux: - return path.join(this.pathUtils.home, '.config', vscodeFolderName, 'User', 'settings.json'); + return uriPath.joinPath(this.homeDir, '.config', vscodeFolderName, 'User', 'settings.json'); case OSType.Windows: return process.env.APPDATA - ? path.join(process.env.APPDATA, vscodeFolderName, 'User', 'settings.json') + ? uriPath.joinPath(Uri.file(process.env.APPDATA), vscodeFolderName, 'User', 'settings.json') : undefined; default: return; } } - public get userCustomKeybindingsFile(): string | undefined { - return path.resolve(this.extensionContext.globalStorageUri.fsPath, '..', '..', 'keybindings.json'); + public get userCustomKeybindingsFile(): Uri | undefined { + return uriPath.resolvePath(this.extensionContext.globalStorageUri, path.join('..', '..', 'keybindings.json')); } } diff --git a/src/platform/common/application/applicationEnvironment.web.ts b/src/platform/common/application/applicationEnvironment.web.ts index 5d327fe0bbc..27f32524ac3 100644 --- a/src/platform/common/application/applicationEnvironment.web.ts +++ b/src/platform/common/application/applicationEnvironment.web.ts @@ -4,14 +4,15 @@ 'use strict'; import { injectable } from 'inversify'; +import { Uri } from 'vscode'; import { BaseApplicationEnvironment } from './applicationEnvironment.base'; @injectable() export class ApplicationEnvironment extends BaseApplicationEnvironment { - public get userSettingsFile(): string | undefined { + public get userSettingsFile(): Uri | undefined { return undefined; } - public get userCustomKeybindingsFile(): string | undefined { + public get userCustomKeybindingsFile(): Uri | undefined { return undefined; } } diff --git a/src/platform/common/application/types.ts b/src/platform/common/application/types.ts index 97557605126..1f254bf671a 100644 --- a/src/platform/common/application/types.ts +++ b/src/platform/common/application/types.ts @@ -999,14 +999,14 @@ export interface IApplicationEnvironment { * @type {string} * @memberof IApplicationShell */ - readonly userSettingsFile: string | undefined; + readonly userSettingsFile: Uri | undefined; /** * Gets the full path to the user custom keybindings file. (may or may not exist). * * @type {string} * @memberof IApplicationShell */ - readonly userCustomKeybindingsFile: string | undefined; + readonly userCustomKeybindingsFile: Uri | undefined; /** * The detected default shell for the extension host, this is overridden by the * `terminal.integrated.shell` setting for the extension host's platform. diff --git a/src/platform/common/platform/fileSystem.node.ts b/src/platform/common/platform/fileSystem.node.ts index 212c87c56ad..77d35319cff 100644 --- a/src/platform/common/platform/fileSystem.node.ts +++ b/src/platform/common/platform/fileSystem.node.ts @@ -7,8 +7,9 @@ import * as vscode from 'vscode'; import { traceError } from '../../logging'; import { createDirNotEmptyError, isFileNotFoundError } from './errors.node'; import { convertFileType, convertStat, getHashString } from './fileSystemUtils.node'; -import { FileSystemPathUtils } from './fs-paths.node'; -import { IFileSystemPathUtils, TemporaryFile } from './types'; +import { arePathsSame } from './fileUtils.node'; +import { getDisplayPathFromLocalFile } from './fs-paths.node'; +import { TemporaryFile } from './types'; import { FileType, IFileSystem } from './types.node'; const ENCODING = 'utf8'; @@ -20,10 +21,8 @@ const ENCODING = 'utf8'; export class FileSystem implements IFileSystem { protected vscfs: vscode.FileSystem; private globFiles: (pat: string, options?: { cwd: string; dot?: boolean }) => Promise; - private fsPathUtils: IFileSystemPathUtils; constructor() { this.globFiles = promisify(glob); - this.fsPathUtils = FileSystemPathUtils.withDefaults(); this.vscfs = vscode.workspace.fs; } @@ -32,7 +31,7 @@ export class FileSystem implements IFileSystem { } public areLocalPathsSame(path1: string, path2: string): boolean { - return this.fsPathUtils.arePathsSame(path1, path2); + return arePathsSame(path1, path2); } public async createLocalDirectory(path: string): Promise { @@ -98,7 +97,7 @@ export class FileSystem implements IFileSystem { } public getDisplayName(filename: string, cwd?: string): string { - return this.fsPathUtils.getDisplayName(filename, cwd); + return getDisplayPathFromLocalFile(filename, cwd); } public async getFileHash(filename: string): Promise { diff --git a/src/platform/common/platform/fileSystemUtils.node.ts b/src/platform/common/platform/fileSystemUtils.node.ts index e2d2dae5ed4..13a81139dda 100644 --- a/src/platform/common/platform/fileSystemUtils.node.ts +++ b/src/platform/common/platform/fileSystemUtils.node.ts @@ -7,15 +7,10 @@ import { createHash } from 'crypto'; import * as fs from 'fs-extra'; import { ReadStream, WriteStream } from 'fs-extra'; -import * as glob from 'glob'; -import { promisify } from 'util'; +import * as path from '../../vscode-path/path'; import * as vscode from 'vscode'; import '../extensions'; -import { traceError } from '../../logging'; -import { createDirNotEmptyError, isFileExistsError, isFileNotFoundError, isNoPermissionsError } from './errors.node'; -import { FileSystemPaths, FileSystemPathUtils } from './fs-paths.node'; -import { TemporaryFileSystem } from './fs-temp.node'; -import { IFileSystemPaths, IFileSystemPathUtils, ITempFileSystem } from './types'; +import { createDirNotEmptyError, isFileExistsError } from './errors.node'; import { IRawFileSystem } from './types.node'; const ENCODING = 'utf8'; @@ -50,22 +45,6 @@ export function convertStat(old: fs.Stats, filetype: vscode.FileType): vscode.Fi }; } -function filterByFileType( - files: [string, vscode.FileType][], // the files to filter - fileType: vscode.FileType // the file type to look for -): [string, vscode.FileType][] { - // We preserve the pre-existing behavior of following symlinks. - if (fileType === vscode.FileType.Unknown) { - // FileType.Unknown == 0 so we can't just use bitwise - // operations blindly here. - return files.filter(([_file, ft]) => { - return ft === vscode.FileType.Unknown || ft === (vscode.FileType.SymbolicLink & vscode.FileType.Unknown); - }); - } else { - return files.filter(([_file, ft]) => (ft & fileType) > 0); - } -} - //========================================== // "raw" filesystem @@ -97,19 +76,12 @@ interface IRawFSExtra { createWriteStream(filename: string): WriteStream; } -interface IRawPath { - dirname(path: string): string; - join(...paths: string[]): string; -} - // Later we will drop "FileSystem", switching usage to // "FileSystemUtils" and then rename "RawFileSystem" to "FileSystem". // The low-level filesystem operations used by the extension. export class RawFileSystem implements IRawFileSystem { constructor( - // the low-level FS path operations to use - protected readonly paths: IRawPath, // the VS Code FS API to use protected readonly vscfs: IVSCodeFileSystemAPI, // the node FS API to use @@ -118,12 +90,10 @@ export class RawFileSystem implements IRawFileSystem { // Create a new object using common-case default values. public static withDefaults( - paths?: IRawPath, // default: a new FileSystemPaths object (using defaults) vscfs?: IVSCodeFileSystemAPI, // default: the actual "vscode.workspace.fs" namespace fsExtra?: IRawFSExtra // default: the "fs-extra" module ): RawFileSystem { return new RawFileSystem( - paths || FileSystemPaths.withDefaults(), vscfs || vscode.workspace.fs, // The "fs-extra" module is effectively equivalent to node's "fs" // module (but is a bit more async-friendly). So we use that @@ -164,7 +134,7 @@ export class RawFileSystem implements IRawFileSystem { // otherwise). So we have to manually stat, just to be sure. // Note that this behavior was reported, but won't be changing. // See: https://github.com/microsoft/vscode/issues/84177 - await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(tgt))); + await this.vscfs.stat(vscode.Uri.file(path.dirname(tgt))); // We stick with the pre-existing behavior where files are // overwritten and directories are not. const options = { overwrite: false }; @@ -216,7 +186,7 @@ export class RawFileSystem implements IRawFileSystem { // otherwise). So we have to manually stat, just to be sure. // Note that this behavior was reported, but won't be changing. // See: https://github.com/microsoft/vscode/issues/84177 - await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(dest))); + await this.vscfs.stat(vscode.Uri.file(path.dirname(dest))); await this.vscfs.copy(srcURI, destURI, { overwrite: true }); @@ -266,7 +236,7 @@ export class RawFileSystem implements IRawFileSystem { const uri = vscode.Uri.file(dirname); const files = await this.vscfs.readDirectory(uri); return files.map(([basename, filetype]) => { - const filename = this.paths.join(dirname, basename); + const filename = path.join(dirname, basename); return [filename, filetype] as [string, vscode.FileType]; }); } @@ -308,170 +278,6 @@ export class RawFileSystem implements IRawFileSystem { } } -//========================================== -// filesystem "utils" - -// High-level filesystem operations used by the extension. -export class FileSystemUtils { - constructor( - public readonly raw: IRawFileSystem, - public readonly pathUtils: IFileSystemPathUtils, - public readonly paths: IFileSystemPaths, - public readonly tmp: ITempFileSystem, - private readonly getHash: (data: string) => string, - private readonly globFiles: (pat: string, options?: { cwd: string; dot?: boolean }) => Promise - ) {} - // Create a new object using common-case default values. - public static withDefaults( - raw?: IRawFileSystem, - pathUtils?: IFileSystemPathUtils, - tmp?: ITempFileSystem, - getHash?: (data: string) => string, - globFiles?: (pat: string, options?: { cwd: string }) => Promise - ): FileSystemUtils { - pathUtils = pathUtils || FileSystemPathUtils.withDefaults(); - return new FileSystemUtils( - raw || RawFileSystem.withDefaults(pathUtils.paths), - pathUtils, - pathUtils.paths, - tmp || TemporaryFileSystem.withDefaults(), - getHash || getHashString, - globFiles || promisify(glob) - ); - } - - //**************************** - // aliases - - public async createDirectory(directoryPath: string): Promise { - return this.raw.mkdirp(directoryPath); - } - - public async deleteDirectory(directoryPath: string): Promise { - return this.raw.rmdir(directoryPath); - } - - public async deleteFile(filename: string): Promise { - return this.raw.rmfile(filename); - } - - //**************************** - // helpers - - public async pathExists( - // the "file" to look for - filename: string, - // the file type to expect; if not provided then any file type - // matches; otherwise a mismatch results in a "false" value - fileType?: vscode.FileType - ): Promise { - let stat: vscode.FileStat; - try { - // Note that we are using stat() rather than lstat(). This - // means that any symlinks are getting resolved. - stat = await this.raw.stat(filename); - } catch (err) { - if (isFileNotFoundError(err)) { - return false; - } - traceError(`stat() failed for "${filename}"`, err); - return false; - } - - if (fileType === undefined) { - return true; - } - if (fileType === vscode.FileType.Unknown) { - // FileType.Unknown == 0, hence do not use bitwise operations. - return stat.type === vscode.FileType.Unknown; - } - return (stat.type & fileType) === fileType; - } - public async fileExists(filename: string): Promise { - return this.pathExists(filename, vscode.FileType.File); - } - public async directoryExists(dirname: string): Promise { - return this.pathExists(dirname, vscode.FileType.Directory); - } - - public async listdir(dirname: string): Promise<[string, vscode.FileType][]> { - try { - return await this.raw.listdir(dirname); - } catch (err) { - // We're only preserving pre-existng behavior here... - if (!(await this.pathExists(dirname))) { - return []; - } - throw err; // re-throw - } - } - public async getSubDirectories(dirname: string): Promise { - const files = await this.listdir(dirname); - const filtered = filterByFileType(files, vscode.FileType.Directory); - return filtered.map(([filename, _fileType]) => filename); - } - public async getFiles(dirname: string): Promise { - // Note that only "regular" files are returned. - const files = await this.listdir(dirname); - const filtered = filterByFileType(files, vscode.FileType.File); - return filtered.map(([filename, _fileType]) => filename); - } - - public async isDirReadonly(dirname: string): Promise { - const filePath = `${dirname}${this.paths.sep}___vscpTest___`; - try { - await this.raw.stat(dirname); - await this.raw.writeText(filePath, ''); - } catch (err) { - if (isNoPermissionsError(err)) { - return true; - } - throw err; // re-throw - } - this.raw - .rmfile(filePath) - // Clean resources in the background. - .ignoreErrors(); - return false; - } - - public async getFileHash(filename: string): Promise { - // The reason for lstat rather than stat is not clear... - const stat = await this.raw.lstat(filename); - const data = `${stat.ctime}-${stat.mtime}`; - return this.getHash(data); - } - - public async search(globPattern: string, cwd?: string, dot?: boolean): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let options: any; - if (cwd) { - options = { ...options, cwd }; - } - if (dot) { - options = { ...options, dot }; - } - - const found = await this.globFiles(globPattern, options); - return Array.isArray(found) ? found : []; - } - - //**************************** - // helpers (non-async) - - public fileExistsSync(filePath: string): boolean { - try { - this.raw.statSync(filePath); - } catch (err) { - if (isFileNotFoundError(err)) { - return false; - } - throw err; // re-throw - } - return true; - } -} - // We *could* use ICryptoUtils, but it's a bit overkill, issue tracked // in https://github.com/microsoft/vscode-python/issues/8438. export function getHashString(data: string): string { diff --git a/src/platform/common/platform/fs-paths.node.ts b/src/platform/common/platform/fs-paths.node.ts index 8d67b72ff1a..7524cc75a0a 100644 --- a/src/platform/common/platform/fs-paths.node.ts +++ b/src/platform/common/platform/fs-paths.node.ts @@ -3,70 +3,13 @@ import * as path from '../../vscode-path/path'; import { getOSType, OSType } from '../utils/platform'; -import { IExecutables, IFileSystemPaths, IFileSystemPathUtils } from './types'; +import { getDisplayPath as getDisplayPathCommon } from './fs-paths'; +import { Uri, WorkspaceFolder } from 'vscode'; + // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports const untildify = require('untildify'); -export const homePath = untildify('~'); - -// The parts of node's 'path' module used by FileSystemPaths. -interface INodePath { - sep: string; - join(...filenames: string[]): string; - dirname(filename: string): string; - basename(filename: string, ext?: string): string; - normalize(filename: string): string; -} - -export class FileSystemPaths implements IFileSystemPaths { - constructor( - // "true" if targeting a case-insensitive host (like Windows) - private readonly isCaseInsensitive: boolean, - // (effectively) the node "path" module to use - private readonly raw: INodePath - ) {} - // Create a new object using common-case default values. - // We do not use an alternate constructor because defaults in the - // constructor runs counter to our typical approach. - public static withDefaults( - // default: use "isWindows" - isCaseInsensitive?: boolean - ): FileSystemPaths { - if (isCaseInsensitive === undefined) { - isCaseInsensitive = getOSType() === OSType.Windows; - } - return new FileSystemPaths( - isCaseInsensitive, - // Use the actual node "path" module. - path - ); - } - - public get sep(): string { - return this.raw.sep; - } - - public join(...filenames: string[]): string { - return this.raw.join(...filenames); - } - - public dirname(filename: string): string { - return this.raw.dirname(filename); - } - - public basename(filename: string, suffix?: string): string { - return this.raw.basename(filename, suffix); - } - - public normalize(filename: string): string { - return this.raw.normalize(filename); - } - - public normCase(filename: string): string { - filename = this.raw.normalize(filename); - return this.isCaseInsensitive ? filename.toUpperCase() : filename; - } -} +export const homePath = Uri.file(untildify('~')); // This is the only thing requiring a node version export class Executables { constructor( @@ -92,55 +35,32 @@ export class Executables { } } -// The dependencies FileSystemPathUtils has on node's path module. -interface IRawPaths { - relative(relpath: string, rootpath: string): string; -} - -export class FileSystemPathUtils implements IFileSystemPathUtils { - constructor( - // the user home directory to use (and expose) - public readonly home: string, - // the low-level FS path operations to use (and expose) - public readonly paths: IFileSystemPaths, - // the low-level OS "executables" to use (and expose) - public readonly executables: IExecutables, - // other low-level FS path operations to use - private readonly raw: IRawPaths - ) {} - // Create a new object using common-case default values. - // We do not use an alternate constructor because defaults in the - // constructor runs counter to our typical approach. - public static withDefaults( - // default: a new FileSystemPaths object (using defaults) - paths?: IFileSystemPaths - ): FileSystemPathUtils { - if (paths === undefined) { - paths = FileSystemPaths.withDefaults(); +export function removeHomeFromFile(file: string | undefined) { + if (getOSType() === OSType.Windows) { + if (file && file.toLowerCase().startsWith(homePath.fsPath.toLowerCase())) { + return `~${file.slice(homePath.fsPath.length)}`; + } + } else { + if (file && file.startsWith(homePath.fsPath)) { + return `~${file.slice(homePath.fsPath.length)}`; } - return new FileSystemPathUtils( - // Use the current user's home directory. - homePath, - paths, - Executables.withDefaults(), - // Use the actual node "path" module. - path - ); } + return file || ''; +} - public arePathsSame(path1: string, path2: string): boolean { - path1 = this.paths.normCase(path1); - path2 = this.paths.normCase(path2); - return path1 === path2; - } +export function getDisplayPathFromLocalFile(file: string | undefined, cwd?: string | undefined) { + const folders: WorkspaceFolder[] = cwd + ? [ + { + uri: Uri.file(cwd), + name: '', + index: 0 + } + ] + : []; + return getDisplayPath(file ? Uri.file(file) : undefined, folders); +} - public getDisplayName(filename: string, cwd?: string): string { - if (cwd && filename.startsWith(cwd)) { - return `.${this.paths.sep}${this.raw.relative(cwd, filename)}`; - } else if (filename.startsWith(this.home)) { - return `~${this.paths.sep}${this.raw.relative(this.home, filename)}`; - } else { - return filename; - } - } +export function getDisplayPath(file?: Uri, workspaceFolders: readonly WorkspaceFolder[] | WorkspaceFolder[] = []) { + return getDisplayPathCommon(file, workspaceFolders, homePath); } diff --git a/src/platform/common/platform/fs-paths.ts b/src/platform/common/platform/fs-paths.ts index a3ad4803a00..692c4799d5d 100644 --- a/src/platform/common/platform/fs-paths.ts +++ b/src/platform/common/platform/fs-paths.ts @@ -1,12 +1,17 @@ import { Uri, WorkspaceFolder } from 'vscode'; import * as path from '../../vscode-path/path'; +import * as uriPath from '../../vscode-path/resources'; +import { getOSType, OSType } from '../utils/platform'; export function getDisplayPath( - filename?: string | Uri, - workspaceFolders: readonly WorkspaceFolder[] | WorkspaceFolder[] = [] + filename: Uri | undefined, + workspaceFolders: readonly WorkspaceFolder[] | WorkspaceFolder[] = [], + homePath: Uri | undefined = undefined ) { - const relativeToHome = getDisplayPathImpl(filename); - const relativeToWorkspaceFolders = workspaceFolders.map((folder) => getDisplayPathImpl(filename, folder.uri.path)); + const relativeToHome = getDisplayPathImpl(filename, undefined, homePath); + const relativeToWorkspaceFolders = workspaceFolders.map((folder) => + getDisplayPathImpl(filename, folder.uri, homePath) + ); // Pick the shortest path for display purposes. // As those are most likely relative to some workspace folder. let bestDisplayPath = relativeToHome; @@ -19,32 +24,35 @@ export function getDisplayPath( return bestDisplayPath; } -function getDisplayPathImpl(filename?: string | Uri, cwd?: string): string { - // Common file separator is unix based '/'. Handle mixing of paths - let cwdReplaced = cwd ? cwd.replace(/\\/g, '/') : undefined; - if (cwdReplaced?.includes(':') && cwdReplaced.startsWith('/')) { - cwdReplaced = cwdReplaced.slice(1); +function getDisplayPathImpl(file: Uri | undefined, cwd: Uri | undefined, homePath: Uri | undefined): string { + const isWindows = getOSType() === OSType.Windows; + if (file && cwd && uriPath.isEqualOrParent(file, cwd, true)) { + const relativePath = uriPath.relativePath(cwd, file); + if (relativePath) { + // On windows relative path will still use forwardslash because uriPath.relativePath is a URI path + return isWindows ? relativePath.replace(/\//g, '\\') : relativePath; + } } - let file = ''; - if (typeof filename === 'string') { - file = filename.replace(/\\/g, '/'); - } else if (!filename) { - file = ''; - } else if (filename.scheme === 'file') { - file = filename.path; - } else { - file = filename.toString().replace(/\\/g, '/'); + + if (file && homePath && uriPath.isEqualOrParent(file, homePath, true)) { + let relativePath = uriPath.relativePath(homePath, file); + if (relativePath) { + // On windows relative path will still use forwardslash because uriPath.relativePath is a URI path + relativePath = isWindows ? relativePath.replace(/\//g, '\\') : relativePath; + return `~${path.sep}${relativePath}`; + } } - if (!file) { - return ''; - } else if (cwdReplaced && file.startsWith(cwdReplaced)) { - const relativePath = `.${path.sep}${path.relative(cwdReplaced, file)}`; - // On CI the relative path might not work as expected as when testing we might have windows paths - // and the code is running on a unix machine. - return relativePath === file || relativePath.includes(cwdReplaced) - ? `.${path.sep}${file.substring(file.indexOf(cwdReplaced) + cwdReplaced.length)}` - : relativePath; - } else { - return file; + + if (file) { + // eslint-disable-next-line local-rules/dont-use-fspath + const fsPath = file.fsPath || file.path; + + // Remove separator on the front + if (fsPath && fsPath.startsWith(path.sep) && isWindows) { + return fsPath.slice(1); + } + return fsPath || ''; } + + return ''; } diff --git a/src/platform/common/platform/pathUtils.node.ts b/src/platform/common/platform/pathUtils.node.ts deleted file mode 100644 index bf096ae82d9..00000000000 --- a/src/platform/common/platform/pathUtils.node.ts +++ /dev/null @@ -1,53 +0,0 @@ -// eslint-disable-next-line -// TODO: Drop this file. -// See https://github.com/microsoft/vscode-python/issues/8542. - -import { inject, injectable } from 'inversify'; -import * as path from '../../vscode-path/path'; -import { IPathUtils, IsWindows } from '../types'; -import { OSType } from '../utils/platform'; -import { Executables, FileSystemPaths, FileSystemPathUtils, homePath } from './fs-paths.node'; - -@injectable() -export class PathUtils implements IPathUtils { - private readonly utils: FileSystemPathUtils; - constructor( - // "true" if targeting a Windows host. - @inject(IsWindows) private readonly isWindows: boolean - ) { - const osType = isWindows ? OSType.Windows : OSType.Unknown; - // We cannot just use FileSystemPathUtils.withDefaults() because - // of the isWindows arg. - this.utils = new FileSystemPathUtils( - homePath, - FileSystemPaths.withDefaults(), - new Executables(path.delimiter, osType), - path - ); - } - - public get home(): string { - return this.utils.home; - } - - public get delimiter(): string { - return this.utils.executables.delimiter; - } - - public get separator(): string { - return this.utils.paths.sep; - } - - public getDisplayName(pathValue: string, cwd?: string): string { - // Paths on windows can either contain \ or / Both work. - // Thus, C:\Python.exe is the same as C:/Python.exe - // If we're on windows ensure we convert the / in pathValue to \. - // For cases like here https://github.com/microsoft/vscode-jupyter/issues/399 - pathValue = this.isWindows ? pathValue.replace(/\//g, '\\') : pathValue; - return this.utils.getDisplayName(pathValue, cwd); - } - - public basename(pathValue: string, ext?: string): string { - return this.utils.paths.basename(pathValue, ext); - } -} diff --git a/src/platform/common/platform/types.ts b/src/platform/common/platform/types.ts index 97d605c4709..f0e4290913c 100644 --- a/src/platform/common/platform/types.ts +++ b/src/platform/common/platform/types.ts @@ -34,19 +34,6 @@ export interface ITempFileSystem { createFile(suffix: string, mode?: number): Promise; } -//=========================== -// FS paths - -// The low-level file path operations used by the extension. -export interface IFileSystemPaths { - readonly sep: string; - join(...filenames: string[]): string; - dirname(filename: string): string; - basename(filename: string, suffix?: string): string; - normalize(filename: string): string; - normCase(filename: string): string; -} - // Where to fine executables. // // In particular this class provides all the tools needed to find @@ -55,17 +42,3 @@ export interface IExecutables { delimiter: string; envVar: string; } - -export const IFileSystemPathUtils = Symbol('IFileSystemPathUtils'); -// A collection of high-level utilities related to filesystem paths. -export interface IFileSystemPathUtils { - readonly paths: IFileSystemPaths; - readonly executables: IExecutables; - readonly home: string; - // Return true if the two paths are equivalent on the current - // filesystem and false otherwise. On Windows this is significant. - // On non-Windows the filenames must always be exactly the same. - arePathsSame(path1: string, path2: string): boolean; - // Return the clean (displayable) form of the given filename. - getDisplayName(pathValue: string, cwd?: string): string; -} diff --git a/src/platform/common/process/condaService.node.ts b/src/platform/common/process/condaService.node.ts index 2a52589ceac..b327d294ca6 100644 --- a/src/platform/common/process/condaService.node.ts +++ b/src/platform/common/process/condaService.node.ts @@ -11,21 +11,22 @@ import { IPlatformService } from '../platform/types'; import { GLOBAL_MEMENTO, IDisposable, IDisposableRegistry, IMemento } from '../types'; import { createDeferredFromPromise } from '../utils/async'; import * as path from '../../../platform/vscode-path/path'; +import * as uriPath from '../../../platform/vscode-path/resources'; import { swallowExceptions } from '../utils/decorators'; import { IFileSystem } from '../platform/types.node'; import { homePath } from '../platform/fs-paths.node'; const CACHEKEY_FOR_CONDA_INFO = 'CONDA_INFORMATION_CACHE'; -const condaEnvironmentsFile = path.join(homePath, '.conda', 'environments.txt'); +const condaEnvironmentsFile = uriPath.joinPath(homePath, '.conda', 'environments.txt'); @injectable() export class CondaService { private isAvailable: boolean | undefined; - private _file?: string; - private _batchFile?: string; + private _file?: Uri; + private _batchFile?: Uri; private _version?: SemVer; private _previousVersionCall?: Promise; - private _previousFileCall?: Promise; - private _previousBatchFileCall?: Promise; + private _previousFileCall?: Promise; + private _previousBatchFileCall?: Promise; private _previousCondaEnvs: string[] = []; private readonly _onCondaEnvironmentsChanged = new EventEmitter(); public readonly onCondaEnvironmentsChanged = this._onCondaEnvironmentsChanged.event; @@ -76,7 +77,7 @@ export class CondaService { .getApi() .then((api) => (api.getCondaFile ? api.getCondaFile() : undefined)); void latestInfo.then((file) => { - this._file = file; + this._file = file ? Uri.file(file) : undefined; void this.updateCache(); }); const cachedInfo = createDeferredFromPromise(this.getCachedInformation()); @@ -84,7 +85,7 @@ export class CondaService { if (cachedInfo.completed && cachedInfo.value?.file) { return (this._file = cachedInfo.value.file); } - return latestInfo; + return latestInfo.then((v) => (v ? Uri.file(v) : undefined)); }; this._previousFileCall = promise(); return this._previousFileCall; @@ -101,12 +102,12 @@ export class CondaService { const promise = async () => { const file = await this.getCondaFile(); if (file) { - const fileDir = path.dirname(file); + const fileDir = path.dirname(file.fsPath); // Batch file depends upon OS if (this.ps.isWindows) { const possibleBatch = path.join(fileDir, '..', 'condabin', 'conda.bat'); if (await this.fs.localFileExists(possibleBatch)) { - return possibleBatch; + return Uri.file(possibleBatch); } } } @@ -133,7 +134,7 @@ export class CondaService { private async monitorCondaEnvFile() { this._previousCondaEnvs = await this.getCondaEnvsFromEnvFile(); const watcher = workspace.createFileSystemWatcher( - new RelativePattern(Uri.file(path.dirname(condaEnvironmentsFile)), path.basename(condaEnvironmentsFile)) + new RelativePattern(uriPath.dirname(condaEnvironmentsFile), uriPath.basename(condaEnvironmentsFile)) ); this.disposables.push(watcher); @@ -152,10 +153,10 @@ export class CondaService { private async getCondaEnvsFromEnvFile(): Promise { try { - const fileContents = await this.fs.readLocalFile(condaEnvironmentsFile); + const fileContents = await this.fs.readLocalFile(condaEnvironmentsFile.fsPath); return fileContents.split('\n').sort(); } catch (ex) { - if (await this.fs.localFileExists(condaEnvironmentsFile)) { + if (await this.fs.localFileExists(condaEnvironmentsFile.fsPath)) { traceError(`Failed to read file ${condaEnvironmentsFile}`, ex); } return []; @@ -165,7 +166,9 @@ export class CondaService { if (!this._file || !this._version) { return; } - const fileHash = this._file.toLowerCase() === 'conda' ? '' : await this.fs.getFileHash(this._file); + const fileHash = this._file.fsPath.toLowerCase().endsWith('conda') + ? '' + : await this.fs.getFileHash(this._file.fsPath); await this.globalState.update(CACHEKEY_FOR_CONDA_INFO, { version: this._version.raw, file: this._file, @@ -177,15 +180,17 @@ export class CondaService { * then we can assume the version is the same. * Even if not, we'll update this with the latest information. */ - private async getCachedInformation(): Promise<{ version: SemVer; file: string } | undefined> { - const cachedInfo = this.globalState.get<{ version: string; file: string; fileHash: string } | undefined>( + private async getCachedInformation(): Promise<{ version: SemVer; file: Uri } | undefined> { + const cachedInfo = this.globalState.get<{ version: string; file: Uri; fileHash: string } | undefined>( CACHEKEY_FOR_CONDA_INFO, undefined ); if (!cachedInfo) { return; } - const fileHash = cachedInfo.file.toLowerCase() === 'conda' ? '' : await this.fs.getFileHash(cachedInfo.file); + const fileHash = cachedInfo.file.fsPath.toLowerCase().endsWith('conda') + ? '' + : await this.fs.getFileHash(cachedInfo.file.fsPath); if (cachedInfo.fileHash === fileHash) { return { version: new SemVer(cachedInfo.version), diff --git a/src/platform/common/process/environmentActivationService.node.ts b/src/platform/common/process/environmentActivationService.node.ts index 40c08e281bf..8b7a396d25a 100644 --- a/src/platform/common/process/environmentActivationService.node.ts +++ b/src/platform/common/process/environmentActivationService.node.ts @@ -45,6 +45,7 @@ import { traceWarning } from '../../logging'; import { TraceOptions } from '../../logging/types'; +import { serializePythonEnvironment } from '../../api/pythonApi.node'; const ENVIRONMENT_PREFIX = 'e8b39361-0157-4923-80e1-22d70d46dee6'; const ENVIRONMENT_TIMEOUT = 30000; @@ -149,10 +150,10 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi @traceDecoratorVerbose('Getting activated env variables', TraceOptions.BeforeCall | TraceOptions.Arguments) public async getActivatedEnvironmentVariables( resource: Resource, - @logValue('path') interpreter: PythonEnvironment + @logValue('uri') interpreter: PythonEnvironment ): Promise { const title = DataScience.activatingPythonEnvironment().format( - interpreter.displayName || getDisplayPath(interpreter.path) + interpreter.displayName || getDisplayPath(interpreter.uri) ); return KernelProgressReporter.wrapAndReportProgress(resource, title, () => this.getActivatedEnvironmentVariablesImpl(resource, interpreter) @@ -161,7 +162,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi @traceDecoratorVerbose('Getting activated env variables impl', TraceOptions.BeforeCall | TraceOptions.Arguments) public async getActivatedEnvironmentVariablesImpl( resource: Resource, - @logValue('path') interpreter: PythonEnvironment + @logValue('uri') interpreter: PythonEnvironment ): Promise { const stopWatch = new StopWatch(); const envVariablesOurSelves = createDeferredFromPromise( @@ -173,10 +174,10 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi await Promise.race([envVariablesOurSelves.promise, envVariablesFromPython.promise]); void envVariablesFromPython.promise.then(() => - traceVerbose(`Got env vars with python ${getDisplayPath(interpreter?.path)} in ${stopWatch.elapsedTime}ms`) + traceVerbose(`Got env vars with python ${getDisplayPath(interpreter?.uri)} in ${stopWatch.elapsedTime}ms`) ); void envVariablesOurSelves.promise.then(() => - traceVerbose(`Got env vars ourselves ${getDisplayPath(interpreter?.path)} in ${stopWatch.elapsedTime}ms`) + traceVerbose(`Got env vars ourselves ${getDisplayPath(interpreter?.uri)} in ${stopWatch.elapsedTime}ms`) ); // If this is a conda environment and we get empty env variables from the Python extension, // Then try our approach. @@ -192,14 +193,14 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi // If we got this using our way, and we have env variables use it. if (envVariablesOurSelves.resolved) { if (envVariablesOurSelves.value) { - traceVerbose(`Got env vars ourselves faster ${getDisplayPath(interpreter?.path)}`); + traceVerbose(`Got env vars ourselves faster ${getDisplayPath(interpreter?.uri)}`); return envVariablesOurSelves.value; } else { - traceVerbose(`Got env vars ourselves faster, but empty ${getDisplayPath(interpreter?.path)}`); + traceVerbose(`Got env vars ourselves faster, but empty ${getDisplayPath(interpreter?.uri)}`); } } if (!envVariablesOurSelves.resolved) { - traceVerbose(`Got env vars with python ext faster ${getDisplayPath(interpreter?.path)}`); + traceVerbose(`Got env vars with python ext faster ${getDisplayPath(interpreter?.uri)}`); } return envVariablesFromPython.promise; } @@ -209,7 +210,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi ) public async getActivatedEnvironmentVariablesFromPython( resource: Resource, - @logValue('path') interpreter: PythonEnvironment + @logValue('uri') interpreter: PythonEnvironment ): Promise { const stopWatch = new StopWatch(); // We'll need this later. @@ -222,21 +223,23 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi | 'failedToGetCustomEnvVariables' = 'emptyVariables'; let [env, customEnvVars] = await Promise.all([ this.apiProvider.getApi().then((api) => - api.getActivatedEnvironmentVariables(resource, interpreter, false).catch((ex) => { - traceError( - `Failed to get activated env variables from Python Extension for ${getDisplayPath( - interpreter.path - )}`, - ex - ); - reasonForFailure = 'failedToGetActivatedEnvVariablesFromPython'; - return undefined; - }) + api + .getActivatedEnvironmentVariables(resource, serializePythonEnvironment(interpreter)!, false) + .catch((ex) => { + traceError( + `Failed to get activated env variables from Python Extension for ${getDisplayPath( + interpreter.uri + )}`, + ex + ); + reasonForFailure = 'failedToGetActivatedEnvVariablesFromPython'; + return undefined; + }) ), this.envVarsService.getCustomEnvironmentVariables(resource).catch((ex) => { traceError( `Failed to get activated env variables from Python Extension for ${getDisplayPath( - interpreter.path + interpreter.uri )}`, ex ); @@ -256,7 +259,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi // We must get activated env variables for Conda env, if not running stuff against conda will not work. // Hence we must log these as errors (so we can see them in jupyter logs). if (!env && envType === EnvironmentType.Conda) { - traceError(`Failed to get activated conda env variables for ${getDisplayPath(interpreter?.path)}`); + traceError(`Failed to get activated conda env variables for ${getDisplayPath(interpreter?.uri)}`); } // Store in cache if we have env vars (lets not cache if it takes <=500ms (see const) to activate an environment). @@ -285,7 +288,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi ) public async getActivatedEnvironmentVariablesOurselves( resource: Resource, - @logValue('path') interpreter: PythonEnvironment + @logValue('uri') interpreter: PythonEnvironment ): Promise { const workspaceKey = this.workspace.getWorkspaceFolderIdentifier(resource); const key = `${workspaceKey}_${interpreter && getInterpreterHash(interpreter)}`; @@ -295,7 +298,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi if (!shellInfo) { traceWarning( `Cannot get activated env variables for ${getDisplayPath( - interpreter?.path + interpreter?.uri )}, shell cannot be determined.` ); sendTelemetryEvent(Telemetry.GetActivatedEnvironmentVariables, 0, { @@ -510,7 +513,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi @testOnlyMethod() public getInterpreterEnvCacheKeyForTesting( resource: Resource, - @logValue('path') interpreter: PythonEnvironment + @logValue('uri') interpreter: PythonEnvironment ): string { const workspaceKey = this.workspace.getWorkspaceFolderIdentifier(resource); return ENVIRONMENT_ACTIVATED_ENV_VARS_KEY_PREFIX.format( @@ -530,7 +533,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi */ private getActivatedEnvVariablesFromCache( resource: Resource, - @logValue('path') interpreter: PythonEnvironment, + @logValue('uri') interpreter: PythonEnvironment, customEnvVariablesHash: string, activationCommandsForNonCondaEnvironments: string[] = [] ) { @@ -571,7 +574,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } private async storeActivatedEnvVariablesInCache( resource: Resource, - @logValue('path') interpreter: PythonEnvironment, + @logValue('uri') interpreter: PythonEnvironment, activatedEnvVariables: NodeJS.ProcessEnv, customEnvVariablesHash: string ) { @@ -658,10 +661,10 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi } const proc = new ProcessService(new BufferDecoder(), env); const service = createCondaEnv( - condaExec, + condaExec.fsPath, { name: interpreter.envName || '', - path: interpreter.path || '', + path: interpreter.uri.fsPath || '', version: condaVersion }, interpreter, @@ -674,7 +677,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi const jsonContents = await this.fs.readLocalFile(tmpFile.filePath); const envVars = await parse(jsonContents); traceInfo( - `Got activated conda env vars ourselves for ${getDisplayPath(interpreter.path)} in ${ + `Got activated conda env vars ourselves for ${getDisplayPath(interpreter.uri)} in ${ stopWatch.elapsedTime }` ); @@ -686,25 +689,25 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi @traceDecoratorVerbose('Getting env activation commands', TraceOptions.BeforeCall | TraceOptions.Arguments) private async getActivationCommands( resource: Resource, - @logValue('path') interpreter?: PythonEnvironment + @logValue('uri') interpreter?: PythonEnvironment ): Promise { - if (!interpreter?.path) { + if (!interpreter?.uri) { return; } - traceVerbose(`Getting activation commands for ${interpreter.path}`); - const key = ENVIRONMENT_ACTIVATION_COMMAND_CACHE_KEY_PREFIX.format(interpreter.path); + traceVerbose(`Getting activation commands for ${interpreter.uri}`); + const key = ENVIRONMENT_ACTIVATION_COMMAND_CACHE_KEY_PREFIX.format(interpreter.uri.fsPath); const cachedData = this.memento.get(key, []); if (cachedData && cachedData.length > 0) { - traceVerbose(`Getting activation commands for ${interpreter.path} are cached.`); + traceVerbose(`Getting activation commands for ${interpreter.uri} are cached.`); return cachedData; } if (this.envActivationCommands.has(key)) { - traceVerbose(`Getting activation commands for ${interpreter.path} are cached with a promise.`); + traceVerbose(`Getting activation commands for ${interpreter.uri} are cached with a promise.`); return this.envActivationCommands.get(key); } const shellInfo = defaultShells[this.platform.osType]; if (!shellInfo) { - traceWarning(`No activation commands for ${interpreter.path}, as the OS is unknown.`); + traceWarning(`No activation commands for ${interpreter.uri}, as the OS is unknown.`); return; } const promise = (async () => { @@ -714,7 +717,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi .then( (api) => api.getEnvironmentActivationShellCommands && - api.getEnvironmentActivationShellCommands(resource, interpreter) + api.getEnvironmentActivationShellCommands(resource, serializePythonEnvironment(interpreter)) ); if (!activationCommands || activationCommands.length === 0) { @@ -724,12 +727,12 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi void this.memento.update(key, activationCommands); return activationCommands; } catch (ex) { - traceError(`Failed to get env activation commands for ${getDisplayPath(interpreter.path)}`, ex); + traceError(`Failed to get env activation commands for ${getDisplayPath(interpreter.uri)}`, ex); return; } })(); this.envActivationCommands.set(key, promise); - traceVerbose(`Getting activation commands for ${interpreter.path} are not cached. May take a while.`); + traceVerbose(`Getting activation commands for ${interpreter.uri} are not cached. May take a while.`); return promise; } protected fixActivationCommands(commands: string[]): string[] { diff --git a/src/platform/common/process/logger.node.ts b/src/platform/common/process/logger.node.ts index d2d989e1de2..766159489c8 100644 --- a/src/platform/common/process/logger.node.ts +++ b/src/platform/common/process/logger.node.ts @@ -6,15 +6,15 @@ import { inject, injectable, named } from 'inversify'; import { isCI, isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../constants'; import { traceInfo } from '../../logging'; -import { IOutputChannel, IPathUtils } from '../types'; +import { IOutputChannel } from '../types'; import { Logging } from '../utils/localize'; import { IProcessLogger, SpawnOptions } from './types.node'; +import { removeHomeFromFile } from '../platform/fs-paths.node'; @injectable() export class ProcessLogger implements IProcessLogger { constructor( - @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel, - @inject(IPathUtils) private readonly pathUtils: IPathUtils + @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel ) {} public logProcess(file: string, args: string[], options?: SpawnOptions) { @@ -24,17 +24,17 @@ export class ProcessLogger implements IProcessLogger { return; } const argsList = args.reduce((accumulator, current, index) => { - let formattedArg = this.pathUtils.getDisplayName(current).toCommandArgument(); + let formattedArg = removeHomeFromFile(current).toCommandArgument(); if (current[0] === "'" || current[0] === '"') { - formattedArg = `${current[0]}${this.pathUtils.getDisplayName(current.substr(1))}`; + formattedArg = `${current[0]}${removeHomeFromFile(current.substr(1))}`; } return index === 0 ? formattedArg : `${accumulator} ${formattedArg}`; }, ''); - const info = [`> ${this.pathUtils.getDisplayName(file)} ${argsList}`]; + const info = [`> ${removeHomeFromFile(file)} ${argsList}`]; if (options && options.cwd) { - info.push(`${Logging.currentWorkingDirectory()} ${this.pathUtils.getDisplayName(options.cwd)}`); + info.push(`${Logging.currentWorkingDirectory()} ${removeHomeFromFile(options.cwd)}`); } info.forEach((line) => { diff --git a/src/platform/common/process/pythonDaemon.node.ts b/src/platform/common/process/pythonDaemon.node.ts index 770a3526d7c..55d94d33ee7 100644 --- a/src/platform/common/process/pythonDaemon.node.ts +++ b/src/platform/common/process/pythonDaemon.node.ts @@ -39,7 +39,7 @@ export class PythonDaemonExecutionService extends BasePythonDaemon implements IP if (response.error) { throw Error(response.error); } - return extractInterpreterInfo(this.interpreter.path, response); + return extractInterpreterInfo(this.interpreter.uri, response); } catch (ex) { traceWarning('Falling back to Python Execution Service due to failure in daemon', ex); return this.pythonExecutionService.getInterpreterInformation(); diff --git a/src/platform/common/process/pythonDaemonPool.node.ts b/src/platform/common/process/pythonDaemonPool.node.ts index d9b216ffbb7..a109bf63983 100644 --- a/src/platform/common/process/pythonDaemonPool.node.ts +++ b/src/platform/common/process/pythonDaemonPool.node.ts @@ -122,7 +122,7 @@ export class PythonDaemonExecutionServicePool extends PythonDaemonFactory implem // When using the daemon, log the message ourselves. if (daemon instanceof PythonDaemonExecutionService) { this.logger.logProcess( - `${getDisplayPath(this.interpreter.path)} (daemon)`, + `${getDisplayPath(this.interpreter.uri)} (daemon)`, daemonLogMessage.args, daemonLogMessage.options ); @@ -153,7 +153,7 @@ export class PythonDaemonExecutionServicePool extends PythonDaemonFactory implem // When using the daemon, log the message ourselves. if (daemonProc) { this.logger.logProcess( - `${getDisplayPath(this.interpreter.path)} (daemon)`, + `${getDisplayPath(this.interpreter.uri)} (daemon)`, daemonLogMessage.args, daemonLogMessage.options ); diff --git a/src/platform/common/process/pythonEnvironment.node.ts b/src/platform/common/process/pythonEnvironment.node.ts index b372fce9815..75a3cc58c4d 100644 --- a/src/platform/common/process/pythonEnvironment.node.ts +++ b/src/platform/common/process/pythonEnvironment.node.ts @@ -12,6 +12,7 @@ import { ExecutionResult, IProcessService, ShellOptions, SpawnOptions } from './ import { compare, SemVer } from 'semver'; import type { PythonEnvironment as PyEnv } from '../../pythonEnvironments/info'; import { getDisplayPath } from '../platform/fs-paths'; +import { Uri } from 'vscode'; class PythonEnvironment { private cachedInterpreterInformation: InterpreterInformation | undefined | null = null; @@ -19,9 +20,9 @@ class PythonEnvironment { protected readonly interpreter: PyEnv, // "deps" is the externally defined functionality used by the class. protected readonly deps: { - getPythonArgv(python: string): string[]; - getObservablePythonArgv(python: string): string[]; - isValidExecutable(python: string): Promise; + getPythonArgv(python: Uri): string[]; + getObservablePythonArgv(python: Uri): string[]; + isValidExecutable(python: Uri): Promise; // from ProcessService: exec(file: string, args: string[]): Promise>; shellExec(command: string, timeout: number): Promise>; @@ -29,11 +30,11 @@ class PythonEnvironment { ) {} public getExecutionInfo(pythonArgs: string[] = []): PythonExecInfo { - const python = this.deps.getPythonArgv(this.interpreter.path); + const python = this.deps.getPythonArgv(this.interpreter.uri); return buildPythonExecInfo(python, pythonArgs); } public getExecutionObservableInfo(pythonArgs: string[] = []): PythonExecInfo { - const python = this.deps.getObservablePythonArgv(this.interpreter.path); + const python = this.deps.getObservablePythonArgv(this.interpreter.uri); return buildPythonExecInfo(python, pythonArgs); } @@ -44,11 +45,11 @@ class PythonEnvironment { return this.cachedInterpreterInformation; } - public async getExecutablePath(): Promise { + public async getExecutablePath(): Promise { // If we've passed the python file, then return the file. // This is because on mac if using the interpreter /usr/bin/python2.7 we can get a different value for the path - if (await this.deps.isValidExecutable(this.interpreter.path)) { - return this.interpreter.path; + if (await this.deps.isValidExecutable(this.interpreter.uri)) { + return this.interpreter.uri; } const python = this.getExecutionInfo(); return getExecutablePath(python, this.deps.exec); @@ -71,13 +72,13 @@ class PythonEnvironment { const python = this.getExecutionInfo(); return await getInterpreterInfo(python, this.deps.shellExec, { info: traceInfo, error: traceError }); } catch (ex) { - traceError(`Failed to get interpreter information for '${getDisplayPath(this.interpreter.path)}'`, ex); + traceError(`Failed to get interpreter information for '${getDisplayPath(this.interpreter.uri)}'`, ex); } } } function createDeps( - isValidExecutable: (filename: string) => Promise, + isValidExecutable: (filename: Uri) => Promise, pythonArgv: string[] | undefined, observablePythonArgv: string[] | undefined, // from ProcessService: @@ -85,8 +86,8 @@ function createDeps( shellExec: (command: string, options?: ShellOptions) => Promise> ) { return { - getPythonArgv: (python: string) => pythonArgv || [python], - getObservablePythonArgv: (python: string) => observablePythonArgv || [python], + getPythonArgv: (python: Uri) => pythonArgv || [python.fsPath], + getObservablePythonArgv: (python: Uri) => observablePythonArgv || [python.fsPath], isValidExecutable, exec: async (cmd: string, args: string[]) => exec(cmd, args, { throwOnStdErr: true }), shellExec: async (text: string, timeout: number) => shellExec(text, { timeout }) @@ -100,7 +101,7 @@ export function createPythonEnv( fs: IFileSystem ): PythonEnvironment { const deps = createDeps( - async (filename) => fs.localFileExists(filename), + async (filename: Uri) => fs.localFileExists(filename.fsPath), // We use the default: [pythonPath]. undefined, undefined, @@ -136,7 +137,7 @@ export function createCondaEnv( } const pythonArgv = [condaFile, ...runArgs, 'python']; const deps = createDeps( - async (filename) => fs.localFileExists(filename), + async (filename) => fs.localFileExists(filename.fsPath), pythonArgv, // eslint-disable-next-line // TODO: Use pythonArgv here once 'conda run' can be @@ -163,7 +164,7 @@ export function createWindowsStoreEnv( * executable using sys.executable for windows store python * interpreters. */ - async (_f: string) => true, + async (_f: Uri) => true, // We use the default: [pythonPath]. undefined, undefined, diff --git a/src/platform/common/process/pythonExecutionFactory.node.ts b/src/platform/common/process/pythonExecutionFactory.node.ts index 0f0035ca597..bb95a314157 100644 --- a/src/platform/common/process/pythonExecutionFactory.node.ts +++ b/src/platform/common/process/pythonExecutionFactory.node.ts @@ -72,7 +72,7 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { public async createDaemon( options: DaemonExecutionFactoryCreationOptions ): Promise { - const daemonPoolKey = `${options.interpreter.path}#${options.daemonClass || ''}#${options.daemonModule || ''}`; + const daemonPoolKey = `${options.interpreter.uri}#${options.daemonClass || ''}#${options.daemonModule || ''}`; const interpreter = options.interpreter; const activatedProcPromise = this.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, @@ -81,7 +81,7 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { }); // No daemon support in Python 2.7 or during shutdown if ((interpreter?.version && interpreter.version.major < 3) || this.config.getSettings().disablePythonDaemon) { - traceInfo(`Not using daemon support for ${getDisplayPath(options.interpreter.path)}`); + traceInfo(`Not using daemon support for ${getDisplayPath(options.interpreter.uri)}`); return activatedProcPromise; } @@ -95,7 +95,7 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { if (isDaemonPoolCreationOption(options)) { traceInfo( - `Creating daemon pool for ${getDisplayPath(options.interpreter.path)} with env variables count ${ + `Creating daemon pool for ${getDisplayPath(options.interpreter.uri)} with env variables count ${ Object.keys(activatedEnvVars || {}).length }` ); @@ -112,7 +112,7 @@ export class PythonExecutionFactory implements IPythonExecutionFactory { return daemon as unknown as T; } else { traceInfo( - `Creating daemon process for ${getDisplayPath(options.interpreter.path)} with env variables count ${ + `Creating daemon process for ${getDisplayPath(options.interpreter.uri)} with env variables count ${ Object.keys(activatedEnvVars || {}).length }` ); @@ -199,7 +199,7 @@ function createPythonService( const procs = createPythonProcessService(procService, env); return { getInterpreterInformation: () => env.getInterpreterInformation(), - getExecutablePath: () => env.getExecutablePath(), + getExecutablePath: () => env.getExecutablePath().then((p) => p.fsPath), isModuleInstalled: (m) => env.isModuleInstalled(m), getExecutionInfo: (a) => env.getExecutionInfo(a), execObservable: (a, o) => procs.execObservable(a, o), diff --git a/src/platform/common/serviceRegistry.node.ts b/src/platform/common/serviceRegistry.node.ts index 6bfc8ac9bb6..574c4d39c54 100644 --- a/src/platform/common/serviceRegistry.node.ts +++ b/src/platform/common/serviceRegistry.node.ts @@ -35,7 +35,6 @@ import { FileDownloader } from './net/fileDownloader.node'; import { HttpClient } from './net/httpClient.node'; import { PersistentStateFactory } from './persistentState'; import { IS_WINDOWS } from './platform/constants.node'; -import { PathUtils } from './platform/pathUtils.node'; import { ProcessLogger } from './process/logger.node'; import { IProcessLogger } from './process/types.node'; import { @@ -44,7 +43,6 @@ import { ICryptoUtils, IExtensions, IFeatureDeprecationManager, - IPathUtils, IPersistentStateFactory, IsWindows } from './types'; @@ -63,7 +61,6 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); serviceManager.addSingleton(IExtensions, Extensions); serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); - serviceManager.addSingleton(IPathUtils, PathUtils); serviceManager.addSingleton(IVSCodeNotebook, VSCodeNotebook); serviceManager.addSingleton(IClipboard, ClipboardService); serviceManager.addSingleton(IProcessLogger, ProcessLogger); diff --git a/src/platform/common/types.ts b/src/platform/common/types.ts index fb23db41a26..b060fe592e3 100644 --- a/src/platform/common/types.ts +++ b/src/platform/common/types.ts @@ -44,23 +44,6 @@ export interface IPersistentStateFactory { createWorkspacePersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState; } -// eslint-disable-next-line -// TODO: Drop IPathUtils in favor of IFileSystemPathUtils. -// See https://github.com/microsoft/vscode-python/issues/8542. -export const IPathUtils = Symbol('IPathUtils'); -export interface IPathUtils { - readonly delimiter: string; - readonly home: string; - /** - * The platform-specific file separator. '\\' or '/'. - * @type {string} - * @memberof IPathUtils - */ - readonly separator: string; - basename(pathValue: string, ext?: string): string; - getDisplayName(pathValue: string, cwd?: string): string; -} - export const IRandom = Symbol('IRandom'); export interface IRandom { getRandomInt(min?: number, max?: number): number; diff --git a/src/platform/common/utils.node.ts b/src/platform/common/utils.node.ts index 32eb1c3cb10..112a541fe9f 100644 --- a/src/platform/common/utils.node.ts +++ b/src/platform/common/utils.node.ts @@ -20,6 +20,8 @@ import { IFileSystem } from './platform/types.node'; import { ICell, IConfigurationService, Resource } from './types'; import { DataScience } from './utils/localize'; +import { fsPathToUri } from '../vscode-path/utils'; +import { getOSType, OSType } from './utils/platform'; export async function calculateWorkingDirectory( configService: IConfigurationService, @@ -194,10 +196,17 @@ export function generateNewNotebookUri( } } -export async function tryGetRealPath(expectedPath: string): Promise { +export async function tryGetRealPath(expectedPath: Uri): Promise { try { // Real path throws if the expected path is not actually created yet. - return await fsExtra.realpath(expectedPath); + let realPath = await fsExtra.realpath(expectedPath.fsPath); + + // Make sure on linux we use the correct separator + if (getOSType() != OSType.Windows) { + realPath = realPath.replace(/\\/g, '/'); + } + + return fsPathToUri(realPath); } catch { // So if that happens, just return the original path. return expectedPath; diff --git a/src/platform/common/utils/platform.node.ts b/src/platform/common/utils/platform.node.ts index e3040ccf1db..2aba3250f81 100644 --- a/src/platform/common/utils/platform.node.ts +++ b/src/platform/common/utils/platform.node.ts @@ -3,10 +3,16 @@ 'use strict'; +import { Uri } from 'vscode'; +import { fsPathToUri } from '../../vscode-path/utils'; import { EnvironmentVariables } from '../variables/types'; import { getOSType, OSType } from './platform'; export * from './platform'; +// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports +const untildify = require('untildify'); +const homePath = untildify('~'); + export function getEnvironmentVariable(key: string): string | undefined { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (process.env as any as EnvironmentVariables)[key]; @@ -16,9 +22,12 @@ export function getPathEnvironmentVariable(): string | undefined { return getEnvironmentVariable('Path') || getEnvironmentVariable('PATH'); } -export function getUserHomeDir(): string | undefined { +export function getUserHomeDir(): Uri | undefined { if (getOSType() === OSType.Windows) { - return getEnvironmentVariable('USERPROFILE'); + return fsPathToUri(getEnvironmentVariable('USERPROFILE') || homePath); } - return getEnvironmentVariable('HOME') || getEnvironmentVariable('HOMEPATH'); + const homeVar = getEnvironmentVariable('HOME') || getEnvironmentVariable('HOMEPATH') || homePath; + + // Make sure if linux, it uses linux separators + return fsPathToUri(homeVar?.replace(/\\/g, '/')); } diff --git a/src/platform/errors/errorHandler.node.ts b/src/platform/errors/errorHandler.node.ts index a2e8468d905..7ecc8535b44 100644 --- a/src/platform/errors/errorHandler.node.ts +++ b/src/platform/errors/errorHandler.node.ts @@ -295,23 +295,23 @@ function getIPyKernelMissingErrorMessageForCell(kernelConnection: KernelConnecti ) { return; } - const displayNameOfKernel = kernelConnection.interpreter.displayName || kernelConnection.interpreter.path; + const displayNameOfKernel = kernelConnection.interpreter.displayName || kernelConnection.interpreter.uri.fsPath; const ipyKernelName = ProductNames.get(Product.ipykernel)!; const ipyKernelModuleName = translateProductToModule(Product.ipykernel); - let installerCommand = `${kernelConnection.interpreter.path.fileToCommandArgument()} -m pip install ${ipyKernelModuleName} -U --force-reinstall`; + let installerCommand = `${kernelConnection.interpreter.uri.fsPath.fileToCommandArgument()} -m pip install ${ipyKernelModuleName} -U --force-reinstall`; if (kernelConnection.interpreter?.envType === EnvironmentType.Conda) { if (kernelConnection.interpreter?.envName) { installerCommand = `conda install -n ${kernelConnection.interpreter?.envName} ${ipyKernelModuleName} --update-deps --force-reinstall`; } else if (kernelConnection.interpreter?.envPath) { - installerCommand = `conda install -p ${kernelConnection.interpreter?.envPath} ${ipyKernelModuleName} --update-deps --force-reinstall`; + installerCommand = `conda install -p ${kernelConnection.interpreter?.envPath.fsPath} ${ipyKernelModuleName} --update-deps --force-reinstall`; } } else if ( kernelConnection.interpreter?.envType === EnvironmentType.Global || kernelConnection.interpreter?.envType === EnvironmentType.WindowsStore || kernelConnection.interpreter?.envType === EnvironmentType.System ) { - installerCommand = `${kernelConnection.interpreter.path.fileToCommandArgument()} -m pip install ${ipyKernelModuleName} -U --user --force-reinstall`; + installerCommand = `${kernelConnection.interpreter.uri.fsPath.fileToCommandArgument()} -m pip install ${ipyKernelModuleName} -U --user --force-reinstall`; } const message = DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter().format( displayNameOfKernel, diff --git a/src/platform/errors/errorUtils.ts b/src/platform/errors/errorUtils.ts index b9537645cb9..cff4d87815c 100644 --- a/src/platform/errors/errorUtils.ts +++ b/src/platform/errors/errorUtils.ts @@ -1,7 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { NotebookCell, NotebookCellOutput, NotebookCellOutputItem, NotebookController, WorkspaceFolder } from 'vscode'; +import { + NotebookCell, + NotebookCellOutput, + NotebookCellOutputItem, + NotebookController, + Uri, + WorkspaceFolder +} from 'vscode'; import { CellExecutionCreator } from '../../notebooks/execution/cellExecutionCreator'; import { getDisplayPath } from '../common/platform/fs-paths'; import { DataScience } from '../common/utils/localize'; @@ -561,7 +568,7 @@ function isBuiltInModuleOverwritten( fileName, moduleName, message: DataScience.fileSeemsToBeInterferingWithKernelStartup().format( - getDisplayPath(fileName, workspaceFolders || []) + getDisplayPath(Uri.file(fileName), workspaceFolders || []) ), moreInfoLink: 'https://aka.ms/kernelFailuresOverridingBuiltInModules', telemetrySafeTags: ['import.error', 'override.modules'] diff --git a/src/platform/export/exportUtil.node.ts b/src/platform/export/exportUtil.node.ts index f4f49d589f5..b93f11b0ca2 100644 --- a/src/platform/export/exportUtil.node.ts +++ b/src/platform/export/exportUtil.node.ts @@ -45,7 +45,7 @@ export class ExportUtil { } public async removeSvgs(source: Uri) { - const model = await this.fs.readLocalFile(source.fsPath); + const model = await this.fs.readFile(source); const content = JSON.parse(model) as nbformat.INotebookContent; for (const cell of content.cells) { const outputs = 'outputs' in cell ? (cell.outputs as nbformat.IOutput[]) : undefined; diff --git a/src/platform/interpreter/contracts.node.ts b/src/platform/interpreter/contracts.node.ts index 46361cde5ee..3fba8b5a0a6 100644 --- a/src/platform/interpreter/contracts.node.ts +++ b/src/platform/interpreter/contracts.node.ts @@ -8,5 +8,5 @@ export interface IInterpreterService { refreshInterpreters(): Promise; getInterpreters(resource?: Uri): Promise; getActiveInterpreter(resource?: Uri): Promise; - getInterpreterDetails(pythonPath: string, resource?: Uri): Promise; + getInterpreterDetails(pythonPath: Uri, resource?: Uri): Promise; } diff --git a/src/platform/logging/gitHubIssueCommandListener.node.ts b/src/platform/logging/gitHubIssueCommandListener.node.ts index 730eb51b071..c2a6b3bd708 100644 --- a/src/platform/logging/gitHubIssueCommandListener.node.ts +++ b/src/platform/logging/gitHubIssueCommandListener.node.ts @@ -19,9 +19,10 @@ import { Commands, MARKDOWN_LANGUAGE } from '../common/constants'; import { traceError } from '../logging'; import { IPlatformService } from '../common/platform/types'; import { IFileSystem } from '../common/platform/types.node'; -import { IDataScienceCommandListener, IDisposableRegistry, IExtensionContext, IPathUtils } from '../common/types'; +import { IDataScienceCommandListener, IDisposableRegistry, IExtensionContext } from '../common/types'; import { GitHubIssue } from '../common/utils/localize'; import { IInterpreterService } from '../interpreter/contracts.node'; +import { getUserHomeDir } from '../common/utils/platform.node'; @injectable() export class GitHubIssueCommandListener implements IDataScienceCommandListener { @@ -31,7 +32,6 @@ export class GitHubIssueCommandListener implements IDataScienceCommandListener { private diagnosticCollection: DiagnosticCollection; constructor( @inject(IFileSystem) private filesystem: IFileSystem, - @inject(IPathUtils) private pathUtils: IPathUtils, @inject(IApplicationShell) private appShell: IApplicationShell, @inject(ICommandManager) private commandManager: ICommandManager, @inject(IApplicationEnvironment) private applicationEnvironment: IApplicationEnvironment, @@ -177,8 +177,9 @@ ${'```'} } private async getRedactedLogs() { - const pathComponents = this.pathUtils.home.split(this.pathUtils.separator); - const username = pathComponents[pathComponents.length - 1]; + const userHomePath = getUserHomeDir()?.path || ''; + const pathComponents = userHomePath.split('/'); + const username = pathComponents[pathComponents.length - 1]; // TODO: Need to verify this works. This is the same algorithm as untildify const re = RegExp(username, 'gi'); return (await this.filesystem.readLocalFile(this.logfilePath)).replace(re, '[redacted]'); } diff --git a/src/platform/pythonEnvironments/info/executable.node.ts b/src/platform/pythonEnvironments/info/executable.node.ts index a6f344afdf5..2d80fc4903e 100644 --- a/src/platform/pythonEnvironments/info/executable.node.ts +++ b/src/platform/pythonEnvironments/info/executable.node.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { Uri } from 'vscode'; import { getExecutable as getPythonExecutableCommand } from '../../common/process/internal/python.node'; import { copyPythonExecInfo, PythonExecInfo } from '../exec'; @@ -17,9 +18,9 @@ type ExecFunc = (command: string, args: string[]) => Promise; * @param python - the information to use when running Python * @param exec - the function to use to run Python */ -export async function getExecutablePath(python: PythonExecInfo, exec: ExecFunc): Promise { +export async function getExecutablePath(python: PythonExecInfo, exec: ExecFunc): Promise { const [args, parse] = getPythonExecutableCommand(); const info = copyPythonExecInfo(python, args); const result = await exec(info.command, info.args); - return parse(result.stdout); + return Uri.file(parse(result.stdout)); } diff --git a/src/platform/pythonEnvironments/info/index.ts b/src/platform/pythonEnvironments/info/index.ts index ff57d2bb5c9..b421746c99d 100644 --- a/src/platform/pythonEnvironments/info/index.ts +++ b/src/platform/pythonEnvironments/info/index.ts @@ -3,6 +3,7 @@ 'use strict'; +import { Uri } from 'vscode'; import { PythonVersion } from './pythonVersion'; type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final' | 'unknown'; @@ -40,7 +41,7 @@ export enum EnvironmentType { * @prop sysPrefix - the environment's install root (`sys.prefix`) */ export type InterpreterInformation = { - path: string; + uri: Uri; version?: PythonVersion; sysVersion?: string; sysPrefix: string; @@ -54,5 +55,5 @@ export type PythonEnvironment = InterpreterInformation & { displayName?: string; envType?: EnvironmentType; envName?: string; - envPath?: string; + envPath?: Uri; }; diff --git a/src/platform/pythonEnvironments/info/interpreter.node.ts b/src/platform/pythonEnvironments/info/interpreter.node.ts index 858f93f5d2f..87ea14374d1 100644 --- a/src/platform/pythonEnvironments/info/interpreter.node.ts +++ b/src/platform/pythonEnvironments/info/interpreter.node.ts @@ -2,8 +2,9 @@ // Licensed under the MIT License. import { sha256 } from 'hash.js'; +import { Uri } from 'vscode'; +import * as uriPath from '../../vscode-path/resources'; import { InterpreterInformation, PythonEnvironment } from '.'; -import { IFileSystem } from '../../common/platform/types.node'; import { interpreterInfo as getInterpreterInfoCommand, PythonEnvInfo } from '../../common/process/internal/scripts/index.node'; import { getOSType, OSType } from '../../common/utils/platform'; import { copyPythonExecInfo, PythonExecInfo } from '../exec'; @@ -17,10 +18,10 @@ import { parsePythonVersion } from './pythonVersion'; * @param python - the path to the Python executable * @param raw - the information returned by the `interpreterInfo.py` script */ -export function extractInterpreterInfo(python: string, raw: PythonEnvInfo): InterpreterInformation { +export function extractInterpreterInfo(python: Uri, raw: PythonEnvInfo): InterpreterInformation { const rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}-${raw.versionInfo[3]}`; return { - path: python, + uri: python, version: parsePythonVersion(rawVersion), sysVersion: raw.sysVersion, sysPrefix: raw.sysPrefix, @@ -74,16 +75,16 @@ export async function getInterpreterInfo( if (logger) { logger.info(`Found interpreter for ${argv}`); } - return extractInterpreterInfo(python.pythonExecutable, json); + return extractInterpreterInfo(Uri.file(python.pythonExecutable), json); } -export function getInterpreterHash(interpreter: PythonEnvironment | {path: string}){ - const interpreterPath = getNormalizedInterpreterPath(interpreter.path); - return sha256().update(interpreterPath).digest('hex'); +export function getInterpreterHash(interpreter: PythonEnvironment | {uri: Uri}){ + const interpreterPath = getNormalizedInterpreterPath(interpreter.uri); + return sha256().update(interpreterPath.path).digest('hex'); } export function areInterpretersSame(i1: PythonEnvironment | undefined, i2: PythonEnvironment | undefined) { - return areInterpreterPathsSame(i1?.path, i2?.path) && i1?.displayName == i2?.displayName; + return areInterpreterPathsSame(i1?.uri, i2?.uri) && i1?.displayName == i2?.displayName; } /** @@ -93,10 +94,10 @@ export function areInterpretersSame(i1: PythonEnvironment | undefined, i2: Pytho * They are both the same. * This function will take that into account. */ -export function areInterpreterPathsSame(path1: string = '', path2:string = '', ostype = getOSType(), fs?: IFileSystem){ - const norm1 = getNormalizedInterpreterPath(path1, ostype); - const norm2 = getNormalizedInterpreterPath(path2, ostype); - return norm1 === norm2 || (fs ? fs.areLocalPathsSame(norm1, norm2) : false); +export function areInterpreterPathsSame(path1: Uri = Uri.file(''), path2:Uri = Uri.file(''), ostype = getOSType(), forceLowerCase: boolean = false){ + const norm1 = getNormalizedInterpreterPath(path1, ostype, ostype == OSType.Windows || forceLowerCase); + const norm2 = getNormalizedInterpreterPath(path2, ostype, ostype == OSType.Windows || forceLowerCase); + return norm1 === norm2 || uriPath.isEqual(norm1, norm2, true); } /** * Sometimes on CI, we have paths such as (this could happen on user machines as well) @@ -105,10 +106,15 @@ export function areInterpreterPathsSame(path1: string = '', path2:string = '', o * They are both the same. * This function will take that into account. */ - export function getNormalizedInterpreterPath(path:string = '', ostype = getOSType()){ + export function getNormalizedInterpreterPath(path:Uri = Uri.file(''), ostype = getOSType(), forceLowerCase: boolean = false){ + let fsPath = path.fsPath; + if (forceLowerCase) { + fsPath = fsPath.toLowerCase(); + } + // No need to generate hashes, its unnecessarily slow. - if (!path.endsWith('/bin/python')) { - return path; + if (!fsPath.endsWith('/bin/python')) { + return Uri.file(fsPath); } // Sometimes on CI, we have paths such as (this could happen on user machines as well) // - /opt/hostedtoolcache/Python/3.8.11/x64/python @@ -117,9 +123,9 @@ export function areInterpreterPathsSame(path1: string = '', path2:string = '', o // To ensure we treat them as the same, lets drop the `bin` on unix. if ([OSType.Linux, OSType.OSX].includes(ostype)){ // We need to exclude paths such as `/usr/bin/python` - return path.endsWith('/bin/python') && path.split('/').length > 4 ? path.replace('/bin/python', '/python') : path; + return fsPath.endsWith('/bin/python') && fsPath.split('/').length > 4 ? Uri.file(fsPath.replace('/bin/python', '/python')) : Uri.file(fsPath); } - return path; + return Uri.file(fsPath); } /** diff --git a/src/platform/serviceRegistry.node.ts b/src/platform/serviceRegistry.node.ts index 30d36a51856..e9aee45c936 100644 --- a/src/platform/serviceRegistry.node.ts +++ b/src/platform/serviceRegistry.node.ts @@ -27,8 +27,6 @@ import { INotebookWatcher } from '../webviews/extension-side/variablesView/types import { IExtensionSingleActivationService, IExtensionSyncActivationService } from './activation/types'; import { ExtensionRecommendationService } from './common/extensionRecommendation.node'; import { GlobalActivation } from './common/globalActivation.node'; -import { FileSystemPathUtils } from './common/platform/fs-paths.node'; -import { IFileSystemPathUtils } from './common/platform/types'; import { PreReleaseChecker } from './common/prereleaseChecker.node'; import { IConfigurationService, IDataScienceCommandListener, IExtensionContext } from './common/types'; import { DebugLocationTrackerFactory } from './debugger/debugLocationTrackerFactory.node'; @@ -106,7 +104,6 @@ export function registerTypes(context: IExtensionContext, serviceManager: IServi serviceManager.addSingleton(IExport, ExportToPythonPlain, ExportFormat.python); serviceManager.addSingleton(ExportUtil, ExportUtil); serviceManager.addSingleton(IExportDialog, ExportDialog); - serviceManager.addSingleton(IFileSystemPathUtils, FileSystemPathUtils); serviceManager.addSingleton(INotebookWatcher, NotebookWatcher); serviceManager.addSingleton( IExtensionSyncActivationService, diff --git a/src/platform/terminals/codeExecution/helper.node.ts b/src/platform/terminals/codeExecution/helper.node.ts index ce7fca56da9..292c4430ecc 100644 --- a/src/platform/terminals/codeExecution/helper.node.ts +++ b/src/platform/terminals/codeExecution/helper.node.ts @@ -42,7 +42,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const processService = await this.processServiceFactory.create(resource); const [args, parse] = internalScripts.normalizeSelection(); - const observable = processService.execObservable(interpreter?.path || 'python', args, { + const observable = processService.execObservable(interpreter?.uri.fsPath || 'python', args, { throwOnStdErr: true }); const normalizeOutput = createDeferred(); diff --git a/src/platform/vscode-path/cache.ts b/src/platform/vscode-path/cache.ts new file mode 100644 index 00000000000..7fcc510b9d2 --- /dev/null +++ b/src/platform/vscode-path/cache.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, CancellationTokenSource, Disposable } from 'vscode'; + +export interface CacheResult extends Disposable { + promise: Promise; +} + +export class Cache { + private result: CacheResult | null = null; + constructor(private task: (ct: CancellationToken) => Promise) {} + + get(): CacheResult { + if (this.result) { + return this.result; + } + + const cts = new CancellationTokenSource(); + const promise = this.task(cts.token); + + this.result = { + promise, + dispose: () => { + this.result = null; + cts.cancel(); + cts.dispose(); + } + }; + + return this.result; + } +} + +/** + * Uses a LRU cache to make a given parametrized function cached. + * Caches just the last value. + * The key must be JSON serializable. + */ +export class LRUCachedComputed { + private lastCache: TComputed | undefined = undefined; + private lastArgKey: string | undefined = undefined; + + constructor(private readonly computeFn: (arg: TArg) => TComputed) {} + + public get(arg: TArg): TComputed { + const key = JSON.stringify(arg); + if (this.lastArgKey !== key) { + this.lastArgKey = key; + this.lastCache = this.computeFn(arg); + } + return this.lastCache!; + } +} diff --git a/src/platform/vscode-path/charCode.ts b/src/platform/vscode-path/charCode.ts new file mode 100644 index 00000000000..43de98a7fce --- /dev/null +++ b/src/platform/vscode-path/charCode.ts @@ -0,0 +1,443 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ + +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030a, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030b, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030c, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030d, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030e, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030f, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031a, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031b, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031c, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031d, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031e, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031f, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032a, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032b, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032c, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032d, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032e, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032f, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033a, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033b, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033c, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033d, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033e, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033f, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034a, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034b, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034c, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034d, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034e, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034f, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035a, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035b, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035c, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035d, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035e, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035f, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036a, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036b, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036c, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036d, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036e, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036f, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR = 0x2028, + /** + * Unicode Character 'PARAGRAPH SEPARATOR' (U+2029) + * http://www.fileformat.info/info/unicode/char/2029/index.htm + */ + PARAGRAPH_SEPARATOR = 0x2029, + /** + * Unicode Character 'NEXT LINE' (U+0085) + * http://www.fileformat.info/info/unicode/char/0085/index.htm + */ + NEXT_LINE = 0x0085, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + U_CIRCUMFLEX = 0x005e, // U+005E CIRCUMFLEX + U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT + U_DIAERESIS = 0x00a8, // U+00A8 DIAERESIS + U_MACRON = 0x00af, // U+00AF MACRON + U_ACUTE_ACCENT = 0x00b4, // U+00B4 ACUTE ACCENT + U_CEDILLA = 0x00b8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02c2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02c3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02c4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02c5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02d2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02d3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02d4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02d5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02d6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02d7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02d8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02d9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02da, // U+02DA RING ABOVE + U_OGONEK = 0x02db, // U+02DB OGONEK + U_SMALL_TILDE = 0x02dc, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02dd, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02de, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02df, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02e5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02e6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02e7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02e8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02e9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02ea, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02eb, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ed, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02ef, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02f0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02f1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02f2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02f3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02f4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02f5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02f6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02f7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02f8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02f9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02fa, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02fb, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02fc, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02fd, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02fe, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02ff, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1fbd, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1fbf, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1fc0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1fc1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1fcd, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1fce, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1fcf, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1fdd, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1fde, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1fdf, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1fed, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1fee, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1fef, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1ffd, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1ffe, // U+1FFE GREEK DASIA + + U_IDEOGRAPHIC_FULL_STOP = 0x3002, // U+3002 IDEOGRAPHIC FULL STOP + U_LEFT_CORNER_BRACKET = 0x300c, // U+300C LEFT CORNER BRACKET + U_RIGHT_CORNER_BRACKET = 0x300d, // U+300D RIGHT CORNER BRACKET + U_LEFT_BLACK_LENTICULAR_BRACKET = 0x3010, // U+3010 LEFT BLACK LENTICULAR BRACKET + U_RIGHT_BLACK_LENTICULAR_BRACKET = 0x3011, // U+3011 RIGHT BLACK LENTICULAR BRACKET + + U_OVERLINE = 0x203e, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279, + + U_FULLWIDTH_SEMICOLON = 0xff1b, // U+FF1B FULLWIDTH SEMICOLON + U_FULLWIDTH_COMMA = 0xff0c // U+FF0C FULLWIDTH COMMA +} diff --git a/src/platform/vscode-path/extpath.ts b/src/platform/vscode-path/extpath.ts new file mode 100644 index 00000000000..1d9b4457d00 --- /dev/null +++ b/src/platform/vscode-path/extpath.ts @@ -0,0 +1,405 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CharCode } from './charCode'; +import { isAbsolute, join, normalize, posix, sep } from './path'; +import { isWindows } from './platform'; +import { equalsIgnoreCase, rtrim, startsWithIgnoreCase } from './strings'; +import { isNumber } from './types'; + +export function isPathSeparator(code: number) { + return code === CharCode.Slash || code === CharCode.Backslash; +} + +/** + * Takes a Windows OS path and changes backward slashes to forward slashes. + * This should only be done for OS paths from Windows (or user provided paths potentially from Windows). + * Using it on a Linux or MaxOS path might change it. + */ +export function toSlashes(osPath: string) { + return osPath.replace(/[\\/]/g, posix.sep); +} + +/** + * Takes a Windows OS path (using backward or forward slashes) and turns it into a posix path: + * - turns backward slashes into forward slashes + * - makes it absolute if it starts with a drive letter + * This should only be done for OS paths from Windows (or user provided paths potentially from Windows). + * Using it on a Linux or MaxOS path might change it. + */ +export function toPosixPath(osPath: string) { + if (osPath.indexOf('/') === -1) { + osPath = toSlashes(osPath); + } + if (/^[a-zA-Z]:(\/|$)/.test(osPath)) { + // starts with a drive letter + osPath = '/' + osPath; + } + return osPath; +} + +/** + * Computes the _root_ this path, like `getRoot('c:\files') === c:\`, + * `getRoot('files:///files/path') === files:///`, + * or `getRoot('\\server\shares\path') === \\server\shares\` + */ +export function getRoot(path: string, sep: string = posix.sep): string { + if (!path) { + return ''; + } + + const len = path.length; + const firstLetter = path.charCodeAt(0); + if (isPathSeparator(firstLetter)) { + if (isPathSeparator(path.charCodeAt(1))) { + // UNC candidate \\localhost\shares\ddd + // ^^^^^^^^^^^^^^^^^^^ + if (!isPathSeparator(path.charCodeAt(2))) { + let pos = 3; + const start = pos; + for (; pos < len; pos++) { + if (isPathSeparator(path.charCodeAt(pos))) { + break; + } + } + if (start !== pos && !isPathSeparator(path.charCodeAt(pos + 1))) { + pos += 1; + for (; pos < len; pos++) { + if (isPathSeparator(path.charCodeAt(pos))) { + return path + .slice(0, pos + 1) // consume this separator + .replace(/[\\/]/g, sep); + } + } + } + } + } + + // /user/far + // ^ + return sep; + } else if (isWindowsDriveLetter(firstLetter)) { + // check for windows drive letter c:\ or c: + + if (path.charCodeAt(1) === CharCode.Colon) { + if (isPathSeparator(path.charCodeAt(2))) { + // C:\fff + // ^^^ + return path.slice(0, 2) + sep; + } else { + // C: + // ^^ + return path.slice(0, 2); + } + } + } + + // check for URI + // scheme://authority/path + // ^^^^^^^^^^^^^^^^^^^ + let pos = path.indexOf('://'); + if (pos !== -1) { + pos += 3; // 3 -> "://".length + for (; pos < len; pos++) { + if (isPathSeparator(path.charCodeAt(pos))) { + return path.slice(0, pos + 1); // consume this separator + } + } + } + + return ''; +} + +/** + * Check if the path follows this pattern: `\\hostname\sharename`. + * + * @see https://msdn.microsoft.com/en-us/library/gg465305.aspx + * @return A boolean indication if the path is a UNC path, on none-windows + * always false. + */ +export function isUNC(path: string): boolean { + if (!isWindows) { + // UNC is a windows concept + return false; + } + + if (!path || path.length < 5) { + // at least \\a\b + return false; + } + + let code = path.charCodeAt(0); + if (code !== CharCode.Backslash) { + return false; + } + + code = path.charCodeAt(1); + + if (code !== CharCode.Backslash) { + return false; + } + + let pos = 2; + const start = pos; + for (; pos < path.length; pos++) { + code = path.charCodeAt(pos); + if (code === CharCode.Backslash) { + break; + } + } + + if (start === pos) { + return false; + } + + code = path.charCodeAt(pos + 1); + + if (isNaN(code) || code === CharCode.Backslash) { + return false; + } + + return true; +} + +// Reference: https://en.wikipedia.org/wiki/Filename +const WINDOWS_INVALID_FILE_CHARS = /[\\/:\*\?"<>\|]/g; +const UNIX_INVALID_FILE_CHARS = /[\\/]/g; +const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])(\.(.*?))?$/i; +export function isValidBasename(name: string | null | undefined, isWindowsOS: boolean = isWindows): boolean { + const invalidFileChars = isWindowsOS ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS; + + if (!name || name.length === 0 || /^\s+$/.test(name)) { + return false; // require a name that is not just whitespace + } + + invalidFileChars.lastIndex = 0; // the holy grail of software development + if (invalidFileChars.test(name)) { + return false; // check for certain invalid file characters + } + + if (isWindowsOS && WINDOWS_FORBIDDEN_NAMES.test(name)) { + return false; // check for certain invalid file names + } + + if (name === '.' || name === '..') { + return false; // check for reserved values + } + + if (isWindowsOS && name[name.length - 1] === '.') { + return false; // Windows: file cannot end with a "." + } + + if (isWindowsOS && name.length !== name.trim().length) { + return false; // Windows: file cannot end with a whitespace + } + + if (name.length > 255) { + return false; // most file systems do not allow files > 255 length + } + + return true; +} + +/** + * @deprecated please use `IUriIdentityService.extUri.isEqual` instead. If you are + * in a context without services, consider to pass down the `extUri` from the outside + * or use `extUriBiasedIgnorePathCase` if you know what you are doing. + */ +export function isEqual(pathA: string, pathB: string, ignoreCase?: boolean): boolean { + const identityEquals = pathA === pathB; + if (!ignoreCase || identityEquals) { + return identityEquals; + } + + if (!pathA || !pathB) { + return false; + } + + return equalsIgnoreCase(pathA, pathB); +} + +/** + * @deprecated please use `IUriIdentityService.extUri.isEqualOrParent` instead. If + * you are in a context without services, consider to pass down the `extUri` from the + * outside, or use `extUriBiasedIgnorePathCase` if you know what you are doing. + */ +export function isEqualOrParent(base: string, parentCandidate: string, ignoreCase?: boolean, separator = sep): boolean { + if (base === parentCandidate) { + return true; + } + + if (!base || !parentCandidate) { + return false; + } + + if (parentCandidate.length > base.length) { + return false; + } + + if (ignoreCase) { + const beginsWith = startsWithIgnoreCase(base, parentCandidate); + if (!beginsWith) { + return false; + } + + if (parentCandidate.length === base.length) { + return true; // same path, different casing + } + + let sepOffset = parentCandidate.length; + if (parentCandidate.charAt(parentCandidate.length - 1) === separator) { + sepOffset--; // adjust the expected sep offset in case our candidate already ends in separator character + } + + return base.charAt(sepOffset) === separator; + } + + if (parentCandidate.charAt(parentCandidate.length - 1) !== separator) { + parentCandidate += separator; + } + + return base.indexOf(parentCandidate) === 0; +} + +export function isWindowsDriveLetter(char0: number): boolean { + return (char0 >= CharCode.A && char0 <= CharCode.Z) || (char0 >= CharCode.a && char0 <= CharCode.z); +} + +export function sanitizeFilePath(candidate: string, cwd: string): string { + // Special case: allow to open a drive letter without trailing backslash + if (isWindows && candidate.endsWith(':')) { + candidate += sep; + } + + // Ensure absolute + if (!isAbsolute(candidate)) { + candidate = join(cwd, candidate); + } + + // Ensure normalized + candidate = normalize(candidate); + + // Ensure no trailing slash/backslash + if (isWindows) { + candidate = rtrim(candidate, sep); + + // Special case: allow to open drive root ('C:\') + if (candidate.endsWith(':')) { + candidate += sep; + } + } else { + candidate = rtrim(candidate, sep); + + // Special case: allow to open root ('/') + if (!candidate) { + candidate = sep; + } + } + + return candidate; +} + +export function isRootOrDriveLetter(path: string): boolean { + const pathNormalized = normalize(path); + + if (isWindows) { + if (path.length > 3) { + return false; + } + + return ( + hasDriveLetter(pathNormalized) && (path.length === 2 || pathNormalized.charCodeAt(2) === CharCode.Backslash) + ); + } + + return pathNormalized === posix.sep; +} + +export function hasDriveLetter(path: string, continueAsWindows?: boolean): boolean { + const isWindowsPath: boolean = continueAsWindows !== undefined ? continueAsWindows : isWindows; + if (isWindowsPath) { + return isWindowsDriveLetter(path.charCodeAt(0)) && path.charCodeAt(1) === CharCode.Colon; + } + + return false; +} + +export function getDriveLetter(path: string): string | undefined { + return hasDriveLetter(path) ? path[0] : undefined; +} + +export function indexOfPath(path: string, candidate: string, ignoreCase?: boolean): number { + if (candidate.length > path.length) { + return -1; + } + + if (path === candidate) { + return 0; + } + + if (ignoreCase) { + path = path.toLowerCase(); + candidate = candidate.toLowerCase(); + } + + return path.indexOf(candidate); +} + +export interface IPathWithLineAndColumn { + path: string; + line?: number; + column?: number; +} + +export function parseLineAndColumnAware(rawPath: string): IPathWithLineAndColumn { + const segments = rawPath.split(':'); // C:\file.txt:: + + let path: string | undefined = undefined; + let line: number | undefined = undefined; + let column: number | undefined = undefined; + + for (const segment of segments) { + const segmentAsNumber = Number(segment); + if (!isNumber(segmentAsNumber)) { + path = !!path ? [path, segment].join(':') : segment; // a colon can well be part of a path (e.g. C:\...) + } else if (line === undefined) { + line = segmentAsNumber; + } else if (column === undefined) { + column = segmentAsNumber; + } + } + + if (!path) { + throw new Error('Format for `--goto` should be: `FILE:LINE(:COLUMN)`'); + } + + return { + path, + line: line !== undefined ? line : undefined, + column: column !== undefined ? column : line !== undefined ? 1 : undefined // if we have a line, make sure column is also set + }; +} + +const pathChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + +export function randomPath(parent?: string, prefix?: string, randomLength = 8): string { + let suffix = ''; + for (let i = 0; i < randomLength; i++) { + suffix += pathChars.charAt(Math.floor(Math.random() * pathChars.length)); + } + + let randomFileName: string; + if (prefix) { + randomFileName = `${prefix}-${suffix}`; + } else { + randomFileName = suffix; + } + + if (parent) { + return join(parent, randomFileName); + } + + return randomFileName; +} diff --git a/src/platform/vscode-path/lazy.ts b/src/platform/vscode-path/lazy.ts new file mode 100644 index 00000000000..e432b43a62b --- /dev/null +++ b/src/platform/vscode-path/lazy.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * A value that is resolved synchronously when it is first needed. + */ +export interface Lazy { + hasValue(): boolean; + + getValue(): T; + + map(f: (x: T) => R): Lazy; +} + +export class Lazy { + private _didRun: boolean = false; + private _value?: T; + private _error: Error | undefined; + + constructor(private readonly executor: () => T) {} + + /** + * True if the lazy value has been resolved. + */ + hasValue() { + return this._didRun; + } + + /** + * Get the wrapped value. + * + * This will force evaluation of the lazy value if it has not been resolved yet. Lazy values are only + * resolved once. `getValue` will re-throw exceptions that are hit while resolving the value + */ + getValue(): T { + if (!this._didRun) { + try { + this._value = this.executor(); + } catch (err) { + this._error = err; + } finally { + this._didRun = true; + } + } + if (this._error) { + throw this._error; + } + return this._value!; + } + + /** + * Get the wrapped value without forcing evaluation. + */ + get rawValue(): T | undefined { + return this._value; + } + + /** + * Create a new lazy value that is the result of applying `f` to the wrapped value. + * + * This does not force the evaluation of the current lazy value. + */ + map(f: (x: T) => R): Lazy { + return new Lazy(() => f(this.getValue())); + } +} diff --git a/src/platform/vscode-path/map.ts b/src/platform/vscode-path/map.ts new file mode 100644 index 00000000000..a7d52116b4f --- /dev/null +++ b/src/platform/vscode-path/map.ts @@ -0,0 +1,910 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Uri as URI } from 'vscode'; +import { CharCode } from './charCode'; +import { compare, compareIgnoreCase, compareSubstring, compareSubstringIgnoreCase } from './strings'; + +export function getOrSet(map: Map, key: K, value: V): V { + let result = map.get(key); + if (result === undefined) { + result = value; + map.set(key, result); + } + + return result; +} + +export function mapToString(map: Map): string { + const entries: string[] = []; + map.forEach((value, key) => { + entries.push(`${key} => ${value}`); + }); + + return `Map(${map.size}) {${entries.join(', ')}}`; +} + +export function setToString(set: Set): string { + const entries: K[] = []; + set.forEach((value) => { + entries.push(value); + }); + + return `Set(${set.size}) {${entries.join(', ')}}`; +} + +export interface IKeyIterator { + reset(key: K): this; + next(): this; + + hasNext(): boolean; + cmp(a: string): number; + value(): string; +} + +export class StringIterator implements IKeyIterator { + private _value: string = ''; + private _pos: number = 0; + + reset(key: string): this { + this._value = key; + this._pos = 0; + return this; + } + + next(): this { + this._pos += 1; + return this; + } + + hasNext(): boolean { + return this._pos < this._value.length - 1; + } + + cmp(a: string): number { + const aCode = a.charCodeAt(0); + const thisCode = this._value.charCodeAt(this._pos); + return aCode - thisCode; + } + + value(): string { + return this._value[this._pos]; + } +} + +export class ConfigKeysIterator implements IKeyIterator { + private _value!: string; + private _from!: number; + private _to!: number; + + constructor(private readonly _caseSensitive: boolean = true) {} + + reset(key: string): this { + this._value = key; + this._from = 0; + this._to = 0; + return this.next(); + } + + hasNext(): boolean { + return this._to < this._value.length; + } + + next(): this { + // this._data = key.split(/[\\/]/).filter(s => !!s); + this._from = this._to; + let justSeps = true; + for (; this._to < this._value.length; this._to++) { + const ch = this._value.charCodeAt(this._to); + if (ch === CharCode.Period) { + if (justSeps) { + this._from++; + } else { + break; + } + } else { + justSeps = false; + } + } + return this; + } + + cmp(a: string): number { + return this._caseSensitive + ? compareSubstring(a, this._value, 0, a.length, this._from, this._to) + : compareSubstringIgnoreCase(a, this._value, 0, a.length, this._from, this._to); + } + + value(): string { + return this._value.substring(this._from, this._to); + } +} + +export class PathIterator implements IKeyIterator { + private _value!: string; + private _valueLen!: number; + private _from!: number; + private _to!: number; + + constructor(private readonly _splitOnBackslash: boolean = true, private readonly _caseSensitive: boolean = true) {} + + reset(key: string): this { + this._from = 0; + this._to = 0; + this._value = key; + this._valueLen = key.length; + for (let pos = key.length - 1; pos >= 0; pos--, this._valueLen--) { + const ch = this._value.charCodeAt(pos); + if (!(ch === CharCode.Slash || (this._splitOnBackslash && ch === CharCode.Backslash))) { + break; + } + } + + return this.next(); + } + + hasNext(): boolean { + return this._to < this._valueLen; + } + + next(): this { + // this._data = key.split(/[\\/]/).filter(s => !!s); + this._from = this._to; + let justSeps = true; + for (; this._to < this._valueLen; this._to++) { + const ch = this._value.charCodeAt(this._to); + if (ch === CharCode.Slash || (this._splitOnBackslash && ch === CharCode.Backslash)) { + if (justSeps) { + this._from++; + } else { + break; + } + } else { + justSeps = false; + } + } + return this; + } + + cmp(a: string): number { + return this._caseSensitive + ? compareSubstring(a, this._value, 0, a.length, this._from, this._to) + : compareSubstringIgnoreCase(a, this._value, 0, a.length, this._from, this._to); + } + + value(): string { + return this._value.substring(this._from, this._to); + } +} + +const enum UriIteratorState { + Scheme = 1, + Authority = 2, + Path = 3, + Query = 4, + Fragment = 5 +} + +export class UriIterator implements IKeyIterator { + private _pathIterator!: PathIterator; + private _value!: URI; + private _states: UriIteratorState[] = []; + private _stateIdx: number = 0; + + constructor( + private readonly _ignorePathCasing: (uri: URI) => boolean, + private readonly _ignoreQueryAndFragment: (uri: URI) => boolean + ) {} + + reset(key: URI): this { + this._value = key; + this._states = []; + if (this._value.scheme) { + this._states.push(UriIteratorState.Scheme); + } + if (this._value.authority) { + this._states.push(UriIteratorState.Authority); + } + if (this._value.path) { + this._pathIterator = new PathIterator(false, !this._ignorePathCasing(key)); + this._pathIterator.reset(key.path); + if (this._pathIterator.value()) { + this._states.push(UriIteratorState.Path); + } + } + if (!this._ignoreQueryAndFragment(key)) { + if (this._value.query) { + this._states.push(UriIteratorState.Query); + } + if (this._value.fragment) { + this._states.push(UriIteratorState.Fragment); + } + } + this._stateIdx = 0; + return this; + } + + next(): this { + if (this._states[this._stateIdx] === UriIteratorState.Path && this._pathIterator.hasNext()) { + this._pathIterator.next(); + } else { + this._stateIdx += 1; + } + return this; + } + + hasNext(): boolean { + return ( + (this._states[this._stateIdx] === UriIteratorState.Path && this._pathIterator.hasNext()) || + this._stateIdx < this._states.length - 1 + ); + } + + cmp(a: string): number { + if (this._states[this._stateIdx] === UriIteratorState.Scheme) { + return compareIgnoreCase(a, this._value.scheme); + } else if (this._states[this._stateIdx] === UriIteratorState.Authority) { + return compareIgnoreCase(a, this._value.authority); + } else if (this._states[this._stateIdx] === UriIteratorState.Path) { + return this._pathIterator.cmp(a); + } else if (this._states[this._stateIdx] === UriIteratorState.Query) { + return compare(a, this._value.query); + } else if (this._states[this._stateIdx] === UriIteratorState.Fragment) { + return compare(a, this._value.fragment); + } + throw new Error(); + } + + value(): string { + if (this._states[this._stateIdx] === UriIteratorState.Scheme) { + return this._value.scheme; + } else if (this._states[this._stateIdx] === UriIteratorState.Authority) { + return this._value.authority; + } else if (this._states[this._stateIdx] === UriIteratorState.Path) { + return this._pathIterator.value(); + } else if (this._states[this._stateIdx] === UriIteratorState.Query) { + return this._value.query; + } else if (this._states[this._stateIdx] === UriIteratorState.Fragment) { + return this._value.fragment; + } + throw new Error(); + } +} + +interface ResourceMapKeyFn { + (resource: URI): string; +} + +class ResourceMapEntry { + constructor(readonly uri: URI, readonly value: T) {} +} + +export class ResourceMap implements Map { + private static readonly defaultToKey = (resource: URI) => resource.toString(); + + readonly [Symbol.toStringTag] = 'ResourceMap'; + + private readonly map: Map>; + private readonly toKey: ResourceMapKeyFn; + + /** + * + * @param toKey Custom uri identity function, e.g use an existing `IExtUri#getComparison`-util + */ + constructor(toKey?: ResourceMapKeyFn); + + /** + * + * @param other Another resource which this maps is created from + * @param toKey Custom uri identity function, e.g use an existing `IExtUri#getComparison`-util + */ + constructor(other?: ResourceMap, toKey?: ResourceMapKeyFn); + + constructor(mapOrKeyFn?: ResourceMap | ResourceMapKeyFn, toKey?: ResourceMapKeyFn) { + if (mapOrKeyFn instanceof ResourceMap) { + this.map = new Map(mapOrKeyFn.map); + this.toKey = toKey ?? ResourceMap.defaultToKey; + } else { + this.map = new Map(); + this.toKey = mapOrKeyFn ?? ResourceMap.defaultToKey; + } + } + + set(resource: URI, value: T): this { + this.map.set(this.toKey(resource), new ResourceMapEntry(resource, value)); + return this; + } + + get(resource: URI): T | undefined { + return this.map.get(this.toKey(resource))?.value; + } + + has(resource: URI): boolean { + return this.map.has(this.toKey(resource)); + } + + get size(): number { + return this.map.size; + } + + clear(): void { + this.map.clear(); + } + + delete(resource: URI): boolean { + return this.map.delete(this.toKey(resource)); + } + + forEach(clb: (value: T, key: URI, map: Map) => void, thisArg?: any): void { + if (typeof thisArg !== 'undefined') { + clb = clb.bind(thisArg); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (let [_, entry] of this.map) { + clb(entry.value, entry.uri, this); + } + } + + *values(): IterableIterator { + for (let entry of this.map.values()) { + yield entry.value; + } + } + + *keys(): IterableIterator { + for (let entry of this.map.values()) { + yield entry.uri; + } + } + + *entries(): IterableIterator<[URI, T]> { + for (let entry of this.map.values()) { + yield [entry.uri, entry.value]; + } + } + + *[Symbol.iterator](): IterableIterator<[URI, T]> { + for (let [, entry] of this.map) { + yield [entry.uri, entry.value]; + } + } +} + +export class ResourceSet implements Set { + readonly [Symbol.toStringTag]: string = 'ResourceSet'; + + private readonly _map: ResourceMap; + + constructor(toKey?: ResourceMapKeyFn); + constructor(entries: readonly URI[], toKey?: ResourceMapKeyFn); + constructor(entriesOrKey?: readonly URI[] | ResourceMapKeyFn, toKey?: ResourceMapKeyFn) { + if (!entriesOrKey || typeof entriesOrKey === 'function') { + this._map = new ResourceMap(entriesOrKey); + } else { + this._map = new ResourceMap(toKey); + entriesOrKey.forEach(this.add, this); + } + } + + get size(): number { + return this._map.size; + } + + add(value: URI): this { + this._map.set(value, value); + return this; + } + + clear(): void { + this._map.clear(); + } + + delete(value: URI): boolean { + return this._map.delete(value); + } + + forEach(callbackfn: (value: URI, value2: URI, set: Set) => void, thisArg?: any): void { + this._map.forEach((_value, key) => callbackfn.call(thisArg, key, key, this)); + } + + has(value: URI): boolean { + return this._map.has(value); + } + + entries(): IterableIterator<[URI, URI]> { + return this._map.entries(); + } + + keys(): IterableIterator { + return this._map.keys(); + } + + values(): IterableIterator { + return this._map.keys(); + } + + [Symbol.iterator](): IterableIterator { + return this.keys(); + } +} + +interface Item { + previous: Item | undefined; + next: Item | undefined; + key: K; + value: V; +} + +export const enum Touch { + None = 0, + AsOld = 1, + AsNew = 2 +} + +export class LinkedMap implements Map { + readonly [Symbol.toStringTag] = 'LinkedMap'; + + private _map: Map>; + private _head: Item | undefined; + private _tail: Item | undefined; + private _size: number; + + private _state: number; + + constructor() { + this._map = new Map>(); + this._head = undefined; + this._tail = undefined; + this._size = 0; + this._state = 0; + } + + clear(): void { + this._map.clear(); + this._head = undefined; + this._tail = undefined; + this._size = 0; + this._state++; + } + + isEmpty(): boolean { + return !this._head && !this._tail; + } + + get size(): number { + return this._size; + } + + get first(): V | undefined { + return this._head?.value; + } + + get last(): V | undefined { + return this._tail?.value; + } + + has(key: K): boolean { + return this._map.has(key); + } + + get(key: K, touch: Touch = Touch.None): V | undefined { + const item = this._map.get(key); + if (!item) { + return undefined; + } + if (touch !== Touch.None) { + this.touch(item, touch); + } + return item.value; + } + + set(key: K, value: V, touch: Touch = Touch.None): this { + let item = this._map.get(key); + if (item) { + item.value = value; + if (touch !== Touch.None) { + this.touch(item, touch); + } + } else { + item = { key, value, next: undefined, previous: undefined }; + switch (touch) { + case Touch.None: + this.addItemLast(item); + break; + case Touch.AsOld: + this.addItemFirst(item); + break; + case Touch.AsNew: + this.addItemLast(item); + break; + default: + this.addItemLast(item); + break; + } + this._map.set(key, item); + this._size++; + } + return this; + } + + delete(key: K): boolean { + return !!this.remove(key); + } + + remove(key: K): V | undefined { + const item = this._map.get(key); + if (!item) { + return undefined; + } + this._map.delete(key); + this.removeItem(item); + this._size--; + return item.value; + } + + shift(): V | undefined { + if (!this._head && !this._tail) { + return undefined; + } + if (!this._head || !this._tail) { + throw new Error('Invalid list'); + } + const item = this._head; + this._map.delete(item.key); + this.removeItem(item); + this._size--; + return item.value; + } + + forEach(callbackfn: (value: V, key: K, map: LinkedMap) => void, thisArg?: any): void { + const state = this._state; + let current = this._head; + while (current) { + if (thisArg) { + callbackfn.bind(thisArg)(current.value, current.key, this); + } else { + callbackfn(current.value, current.key, this); + } + if (this._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + current = current.next; + } + } + + keys(): IterableIterator { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result = { value: current.key, done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + values(): IterableIterator { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result = { value: current.value, done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + entries(): IterableIterator<[K, V]> { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator<[K, V]> = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult<[K, V]> { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result: IteratorResult<[K, V]> = { value: [current.key, current.value], done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.entries(); + } + + protected trimOld(newSize: number) { + if (newSize >= this.size) { + return; + } + if (newSize === 0) { + this.clear(); + return; + } + let current = this._head; + let currentSize = this.size; + while (current && currentSize > newSize) { + this._map.delete(current.key); + current = current.next; + currentSize--; + } + this._head = current; + this._size = currentSize; + if (current) { + current.previous = undefined; + } + this._state++; + } + + private addItemFirst(item: Item): void { + // First time Insert + if (!this._head && !this._tail) { + this._tail = item; + } else if (!this._head) { + throw new Error('Invalid list'); + } else { + item.next = this._head; + this._head.previous = item; + } + this._head = item; + this._state++; + } + + private addItemLast(item: Item): void { + // First time Insert + if (!this._head && !this._tail) { + this._head = item; + } else if (!this._tail) { + throw new Error('Invalid list'); + } else { + item.previous = this._tail; + this._tail.next = item; + } + this._tail = item; + this._state++; + } + + private removeItem(item: Item): void { + if (item === this._head && item === this._tail) { + this._head = undefined; + this._tail = undefined; + } else if (item === this._head) { + // This can only happen if size === 1 which is handled + // by the case above. + if (!item.next) { + throw new Error('Invalid list'); + } + item.next.previous = undefined; + this._head = item.next; + } else if (item === this._tail) { + // This can only happen if size === 1 which is handled + // by the case above. + if (!item.previous) { + throw new Error('Invalid list'); + } + item.previous.next = undefined; + this._tail = item.previous; + } else { + const next = item.next; + const previous = item.previous; + if (!next || !previous) { + throw new Error('Invalid list'); + } + next.previous = previous; + previous.next = next; + } + item.next = undefined; + item.previous = undefined; + this._state++; + } + + private touch(item: Item, touch: Touch): void { + if (!this._head || !this._tail) { + throw new Error('Invalid list'); + } + if (touch !== Touch.AsOld && touch !== Touch.AsNew) { + return; + } + + if (touch === Touch.AsOld) { + if (item === this._head) { + return; + } + + const next = item.next; + const previous = item.previous; + + // Unlink the item + if (item === this._tail) { + // previous must be defined since item was not head but is tail + // So there are more than on item in the map + previous!.next = undefined; + this._tail = previous; + } else { + // Both next and previous are not undefined since item was neither head nor tail. + next!.previous = previous; + previous!.next = next; + } + + // Insert the node at head + item.previous = undefined; + item.next = this._head; + this._head.previous = item; + this._head = item; + this._state++; + } else if (touch === Touch.AsNew) { + if (item === this._tail) { + return; + } + + const next = item.next; + const previous = item.previous; + + // Unlink the item. + if (item === this._head) { + // next must be defined since item was not tail but is head + // So there are more than on item in the map + next!.previous = undefined; + this._head = next; + } else { + // Both next and previous are not undefined since item was neither head nor tail. + next!.previous = previous; + previous!.next = next; + } + item.next = undefined; + item.previous = this._tail; + this._tail.next = item; + this._tail = item; + this._state++; + } + } + + toJSON(): [K, V][] { + const data: [K, V][] = []; + + this.forEach((value, key) => { + data.push([key, value]); + }); + + return data; + } + + fromJSON(data: [K, V][]): void { + this.clear(); + + for (const [key, value] of data) { + this.set(key, value); + } + } +} + +export class LRUCache extends LinkedMap { + private _limit: number; + private _ratio: number; + + constructor(limit: number, ratio: number = 1) { + super(); + this._limit = limit; + this._ratio = Math.min(Math.max(0, ratio), 1); + } + + get limit(): number { + return this._limit; + } + + set limit(limit: number) { + this._limit = limit; + this.checkTrim(); + } + + get ratio(): number { + return this._ratio; + } + + set ratio(ratio: number) { + this._ratio = Math.min(Math.max(0, ratio), 1); + this.checkTrim(); + } + + override get(key: K, touch: Touch = Touch.AsNew): V | undefined { + return super.get(key, touch); + } + + peek(key: K): V | undefined { + return super.get(key, Touch.None); + } + + override set(key: K, value: V): this { + super.set(key, value, Touch.AsNew); + this.checkTrim(); + return this; + } + + private checkTrim() { + if (this.size > this._limit) { + this.trimOld(Math.round(this._limit * this._ratio)); + } + } +} + +/** + * Wraps the map in type that only implements readonly properties. Useful + * in the extension host to prevent the consumer from making any mutations. + */ +export class ReadonlyMapView implements ReadonlyMap { + readonly #source: ReadonlyMap; + + public get size() { + return this.#source.size; + } + + constructor(source: ReadonlyMap) { + this.#source = source; + } + + forEach(callbackfn: (value: V, key: K, map: ReadonlyMap) => void, thisArg?: any): void { + this.#source.forEach(callbackfn, thisArg); + } + + get(key: K): V | undefined { + return this.#source.get(key); + } + + has(key: K): boolean { + return this.#source.has(key); + } + + entries(): IterableIterator<[K, V]> { + return this.#source.entries(); + } + + keys(): IterableIterator { + return this.#source.keys(); + } + + values(): IterableIterator { + return this.#source.values(); + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.#source.entries(); + } +} diff --git a/src/platform/vscode-path/resources.ts b/src/platform/vscode-path/resources.ts new file mode 100644 index 00000000000..2ffe3527114 --- /dev/null +++ b/src/platform/vscode-path/resources.ts @@ -0,0 +1,466 @@ +/* eslint-disable local-rules/dont-use-fspath */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Uri as URI } from 'vscode'; +import { CharCode } from './charCode'; +import * as extpath from './extpath'; +import * as paths from './path'; +import { isLinux, isWindows } from './platform'; +import { compare as strCompare, equalsIgnoreCase } from './strings'; +import { Schemas } from './utils'; + +function originalFSPath(uri: URI): string { + return uri.fsPath; +} + +//#region IExtUri + +export interface IExtUri { + // --- identity + + /** + * Compares two uris. + * + * @param uri1 Uri + * @param uri2 Uri + * @param ignoreFragment Ignore the fragment (defaults to `false`) + */ + compare(uri1: URI, uri2: URI, ignoreFragment?: boolean): number; + + /** + * Tests whether two uris are equal + * + * @param uri1 Uri + * @param uri2 Uri + * @param ignoreFragment Ignore the fragment (defaults to `false`) + */ + isEqual(uri1: URI | undefined, uri2: URI | undefined, ignoreFragment?: boolean): boolean; + + /** + * Tests whether a `candidate` URI is a parent or equal of a given `base` URI. + * + * @param base A uri which is "longer" or at least same length as `parentCandidate` + * @param parentCandidate A uri which is "shorter" or up to same length as `base` + * @param ignoreFragment Ignore the fragment (defaults to `false`) + */ + isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment?: boolean): boolean; + + /** + * Creates a key from a resource URI to be used to resource comparison and for resource maps. + * @see {@link ResourceMap} + * @param uri Uri + * @param ignoreFragment Ignore the fragment (defaults to `false`) + */ + getComparisonKey(uri: URI, ignoreFragment?: boolean): string; + + /** + * Whether the casing of the path-component of the uri should be ignored. + */ + ignorePathCasing(uri: URI): boolean; + + // --- path math + + basenameOrAuthority(resource: URI): string; + + /** + * Returns the basename of the path component of an uri. + * @param resource + */ + basename(resource: URI): string; + + /** + * Returns the extension of the path component of an uri. + * @param resource + */ + extname(resource: URI): string; + /** + * Return a URI representing the directory of a URI path. + * + * @param resource The input URI. + * @returns The URI representing the directory of the input URI. + */ + dirname(resource: URI): URI; + /** + * Join a URI path with path fragments and normalizes the resulting path. + * + * @param resource The input URI. + * @param pathFragment The path fragment to add to the URI path. + * @returns The resulting URI. + */ + joinPath(resource: URI, ...pathFragment: string[]): URI; + /** + * Normalizes the path part of a URI: Resolves `.` and `..` elements with directory names. + * + * @param resource The URI to normalize the path. + * @returns The URI with the normalized path. + */ + normalizePath(resource: URI): URI; + /** + * + * @param from + * @param to + */ + relativePath(from: URI, to: URI): string | undefined; + /** + * Resolves an absolute or relative path against a base URI. + * The path can be relative or absolute posix or a Windows path + */ + resolvePath(base: URI, path: string): URI; + + // --- misc + + /** + * Returns true if the URI path is absolute. + */ + isAbsolutePath(resource: URI): boolean; + /** + * Tests whether the two authorities are the same + */ + isEqualAuthority(a1: string, a2: string): boolean; + /** + * Returns true if the URI path has a trailing path separator + */ + hasTrailingPathSeparator(resource: URI, sep?: string): boolean; + /** + * Removes a trailing path separator, if there's one. + * Important: Doesn't remove the first slash, it would make the URI invalid + */ + removeTrailingPathSeparator(resource: URI, sep?: string): URI; + /** + * Adds a trailing path separator to the URI if there isn't one already. + * For example, c:\ would be unchanged, but c:\users would become c:\users\ + */ + addTrailingPathSeparator(resource: URI, sep?: string): URI; +} + +export class ExtUri implements IExtUri { + constructor(private _ignorePathCasing: (uri: URI) => boolean) {} + + compare(uri1: URI, uri2: URI, ignoreFragment: boolean = false): number { + if (uri1 === uri2) { + return 0; + } + return strCompare(this.getComparisonKey(uri1, ignoreFragment), this.getComparisonKey(uri2, ignoreFragment)); + } + + isEqual(uri1: URI | undefined, uri2: URI | undefined, ignoreFragment: boolean = false): boolean { + if (uri1 === uri2) { + return true; + } + if (!uri1 || !uri2) { + return false; + } + return this.getComparisonKey(uri1, ignoreFragment) === this.getComparisonKey(uri2, ignoreFragment); + } + + getComparisonKey(uri: URI, ignoreFragment: boolean = false): string { + return uri + .with({ + path: this._ignorePathCasing(uri) ? uri.path.toLowerCase() : undefined, + fragment: ignoreFragment ? undefined : undefined + }) + .toString(); + } + + ignorePathCasing(uri: URI): boolean { + return this._ignorePathCasing(uri); + } + + isEqualOrParent( + base: URI, + parentCandidate: URI, + ignoreFragment: boolean = false, + sep: '\\' | '/' = isWindows ? '\\' : '/' + ): boolean { + if (base.scheme === parentCandidate.scheme) { + if (base.scheme === Schemas.file) { + return ( + extpath.isEqualOrParent( + originalFSPath(base), + originalFSPath(parentCandidate), + this._ignorePathCasing(base), + sep + ) && + base.query === parentCandidate.query && + (ignoreFragment || base.fragment === parentCandidate.fragment) + ); + } + if (isEqualAuthority(base.authority, parentCandidate.authority)) { + return ( + extpath.isEqualOrParent(base.path, parentCandidate.path, this._ignorePathCasing(base), '/') && + base.query === parentCandidate.query && + (ignoreFragment || base.fragment === parentCandidate.fragment) + ); + } + } + return false; + } + + // --- path math + + joinPath(resource: URI, ...pathFragment: string[]): URI { + return URI.joinPath(resource, ...pathFragment); + } + + basenameOrAuthority(resource: URI): string { + return basename(resource) || resource.authority; + } + + basename(resource: URI): string { + return paths.posix.basename(resource.path); + } + + extname(resource: URI): string { + return paths.posix.extname(resource.path); + } + + dirname(resource: URI): URI { + if (resource.path.length === 0) { + return resource; + } + let dirname; + if (resource.scheme === Schemas.file) { + dirname = URI.file(paths.dirname(originalFSPath(resource))).path; + } else { + dirname = paths.posix.dirname(resource.path); + if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) { + console.error(`dirname("${resource.toString})) resulted in a relative path`); + dirname = '/'; // If a URI contains an authority component, then the path component must either be empty or begin with a CharCode.Slash ("/") character + } + } + return resource.with({ + path: dirname + }); + } + + normalizePath(resource: URI): URI { + if (!resource.path.length) { + return resource; + } + let normalizedPath: string; + if (resource.scheme === Schemas.file) { + normalizedPath = URI.file(paths.normalize(originalFSPath(resource))).path; + } else { + normalizedPath = paths.posix.normalize(resource.path); + } + return resource.with({ + path: normalizedPath + }); + } + + relativePath(from: URI, to: URI): string | undefined { + if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) { + return undefined; + } + if (from.scheme === Schemas.file) { + const relativePath = paths.relative(originalFSPath(from), originalFSPath(to)); + return isWindows ? extpath.toSlashes(relativePath) : relativePath; + } + let fromPath = from.path || '/', + toPath = to.path || '/'; + if (this._ignorePathCasing(from)) { + // make casing of fromPath match toPath + let i = 0; + for (const len = Math.min(fromPath.length, toPath.length); i < len; i++) { + if (fromPath.charCodeAt(i) !== toPath.charCodeAt(i)) { + if (fromPath.charAt(i).toLowerCase() !== toPath.charAt(i).toLowerCase()) { + break; + } + } + } + fromPath = toPath.substr(0, i) + fromPath.substr(i); + } + return paths.posix.relative(fromPath, toPath); + } + + resolvePath(base: URI, path: string): URI { + if (base.scheme === Schemas.file) { + const newURI = URI.file(paths.resolve(originalFSPath(base), path)); + return base.with({ + authority: newURI.authority, + path: newURI.path + }); + } + path = extpath.toPosixPath(path); // we allow path to be a windows path + return base.with({ + path: paths.posix.resolve(base.path, path) + }); + } + + // --- misc + + isAbsolutePath(resource: URI): boolean { + return !!resource.path && resource.path[0] === '/'; + } + + isEqualAuthority(a1: string | undefined, a2: string | undefined) { + return a1 === a2 || (a1 !== undefined && a2 !== undefined && equalsIgnoreCase(a1, a2)); + } + + hasTrailingPathSeparator(resource: URI, sep: string = paths.sep): boolean { + if (resource.scheme === Schemas.file) { + const fsp = originalFSPath(resource); + return fsp.length > extpath.getRoot(fsp).length && fsp[fsp.length - 1] === sep; + } else { + const p = resource.path; + return ( + p.length > 1 && + p.charCodeAt(p.length - 1) === CharCode.Slash && + !/^[a-zA-Z]:(\/$|\\$)/.test(resource.fsPath) + ); // ignore the slash at offset 0 + } + } + + removeTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI { + // Make sure that the path isn't a drive letter. A trailing separator there is not removable. + if (hasTrailingPathSeparator(resource, sep)) { + return resource.with({ path: resource.path.substr(0, resource.path.length - 1) }); + } + return resource; + } + + addTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI { + let isRootSep: boolean = false; + if (resource.scheme === Schemas.file) { + const fsp = originalFSPath(resource); + isRootSep = fsp !== undefined && fsp.length === extpath.getRoot(fsp).length && fsp[fsp.length - 1] === sep; + } else { + sep = '/'; + const p = resource.path; + isRootSep = p.length === 1 && p.charCodeAt(p.length - 1) === CharCode.Slash; + } + if (!isRootSep && !hasTrailingPathSeparator(resource, sep)) { + return resource.with({ path: resource.path + '/' }); + } + return resource; + } +} + +/** + * Unbiased utility that takes uris "as they are". This means it can be interchanged with + * uri#toString() usages. The following is true + * ``` + * assertEqual(aUri.toString() === bUri.toString(), exturi.isEqual(aUri, bUri)) + * ``` + */ +export const extUri = new ExtUri(() => false); + +/** + * BIASED utility that _mostly_ ignored the case of urs paths. ONLY use this util if you + * understand what you are doing. + * + * This utility is INCOMPATIBLE with `uri.toString()`-usages and both CANNOT be used interchanged. + * + * When dealing with uris from files or documents, `extUri` (the unbiased friend)is sufficient + * because those uris come from a "trustworthy source". When creating unknown uris it's always + * better to use `IUriIdentityService` which exposes an `IExtUri`-instance which knows when path + * casing matters. + */ +export const extUriBiasedIgnorePathCase = new ExtUri((uri) => { + // A file scheme resource is in the same platform as code, so ignore case for non linux platforms + // Resource can be from another platform. Lowering the case as an hack. Should come from File system provider + return uri.scheme === Schemas.file ? !isLinux : true; +}); + +/** + * BIASED utility that always ignores the casing of uris paths. ONLY use this util if you + * understand what you are doing. + * + * This utility is INCOMPATIBLE with `uri.toString()`-usages and both CANNOT be used interchanged. + * + * When dealing with uris from files or documents, `extUri` (the unbiased friend)is sufficient + * because those uris come from a "trustworthy source". When creating unknown uris it's always + * better to use `IUriIdentityService` which exposes an `IExtUri`-instance which knows when path + * casing matters. + */ +export const extUriIgnorePathCase = new ExtUri((_) => true); + +export const isEqual = extUri.isEqual.bind(extUri); +export const isEqualOrParent = extUri.isEqualOrParent.bind(extUri); +export const getComparisonKey = extUri.getComparisonKey.bind(extUri); +export const basenameOrAuthority = extUri.basenameOrAuthority.bind(extUri); +export const basename = extUri.basename.bind(extUri); +export const extname = extUri.extname.bind(extUri); +export const dirname = extUri.dirname.bind(extUri); +export const joinPath = extUri.joinPath.bind(extUri); +export const normalizePath = extUri.normalizePath.bind(extUri); +export const relativePath = extUri.relativePath.bind(extUri); +export const resolvePath = extUri.resolvePath.bind(extUri); +export const isAbsolutePath = extUri.isAbsolutePath.bind(extUri); +export const isEqualAuthority = extUri.isEqualAuthority.bind(extUri); +export const hasTrailingPathSeparator = extUri.hasTrailingPathSeparator.bind(extUri); +export const removeTrailingPathSeparator = extUri.removeTrailingPathSeparator.bind(extUri); +export const addTrailingPathSeparator = extUri.addTrailingPathSeparator.bind(extUri); + +//#endregion + +export function distinctParents(items: T[], resourceAccessor: (item: T) => URI): T[] { + const distinctParents: T[] = []; + for (let i = 0; i < items.length; i++) { + const candidateResource = resourceAccessor(items[i]); + if ( + items.some((otherItem, index) => { + if (index === i) { + return false; + } + + return isEqualOrParent(candidateResource, resourceAccessor(otherItem)); + }) + ) { + continue; + } + + distinctParents.push(items[i]); + } + + return distinctParents; +} + +/** + * Data URI related helpers. + */ +export namespace DataUri { + export const META_DATA_LABEL = 'label'; + export const META_DATA_DESCRIPTION = 'description'; + export const META_DATA_SIZE = 'size'; + export const META_DATA_MIME = 'mime'; + + export function parseMetaData(dataUri: URI): Map { + const metadata = new Map(); + + // Given a URI of: data:image/png;size:2313;label:SomeLabel;description:SomeDescription;base64,77+9UE5... + // the metadata is: size:2313;label:SomeLabel;description:SomeDescription + const meta = dataUri.path.substring(dataUri.path.indexOf(';') + 1, dataUri.path.lastIndexOf(';')); + meta.split(';').forEach((property) => { + const [key, value] = property.split(':'); + if (key && value) { + metadata.set(key, value); + } + }); + + // Given a URI of: data:image/png;size:2313;label:SomeLabel;description:SomeDescription;base64,77+9UE5... + // the mime is: image/png + const mime = dataUri.path.substring(0, dataUri.path.indexOf(';')); + if (mime) { + metadata.set(META_DATA_MIME, mime); + } + + return metadata; + } +} + +export function toLocalResource(resource: URI, authority: string | undefined, localScheme: string): URI { + if (authority) { + let path = resource.path; + if (path && path[0] !== paths.posix.sep) { + path = paths.posix.sep + path; + } + + return resource.with({ scheme: localScheme, authority, path }); + } + + return resource.with({ scheme: localScheme }); +} diff --git a/src/platform/vscode-path/strings.ts b/src/platform/vscode-path/strings.ts new file mode 100644 index 00000000000..da328157d68 --- /dev/null +++ b/src/platform/vscode-path/strings.ts @@ -0,0 +1,1196 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { LRUCachedComputed } from './cache'; +import { CharCode } from './charCode'; +import { Lazy } from './lazy'; +import { Constants } from './uint'; + +export function isFalsyOrWhitespace(str: string | undefined): boolean { + if (!str || typeof str !== 'string') { + return true; + } + return str.trim().length === 0; +} + +const _formatRegexp = /{(\d+)}/g; + +/** + * Helper to produce a string with a variable number of arguments. Insert variable segments + * into the string using the {n} notation where N is the index of the argument following the string. + * @param value string to which formatting is applied + * @param args replacements for {n}-entries + */ +export function format(value: string, ...args: any[]): string { + if (args.length === 0) { + return value; + } + return value.replace(_formatRegexp, function (match, group) { + const idx = parseInt(group, 10); + return isNaN(idx) || idx < 0 || idx >= args.length ? match : args[idx]; + }); +} + +const _format2Regexp = /{([^}]+)}/g; + +/** + * Helper to create a string from a template and a string record. + * Similar to `format` but with objects instead of positional arguments. + */ +export function format2(template: string, values: Record): string { + return template.replace(_format2Regexp, (match, group) => (values[group] ?? match) as string); +} + +/** + * Converts HTML characters inside the string to use entities instead. Makes the string safe from + * being used e.g. in HTMLElement.innerHTML. + */ +export function escape(html: string): string { + return html.replace(/[<>&]/g, function (match) { + switch (match) { + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; + default: + return match; + } + }); +} + +/** + * Escapes regular expression characters in a given string + */ +export function escapeRegExpCharacters(value: string): string { + return value.replace(/[\\\{\}\*\+\?\|\^\$\.\[\]\(\)]/g, '\\$&'); +} + +/** + * Counts how often `character` occurs inside `value`. + */ +export function count(value: string, character: string): number { + let result = 0; + const ch = character.charCodeAt(0); + for (let i = value.length - 1; i >= 0; i--) { + if (value.charCodeAt(i) === ch) { + result++; + } + } + return result; +} + +export function truncate(value: string, maxLength: number, suffix = '…'): string { + if (value.length <= maxLength) { + return value; + } + + return `${value.substr(0, maxLength)}${suffix}`; +} + +/** + * Removes all occurrences of needle from the beginning and end of haystack. + * @param haystack string to trim + * @param needle the thing to trim (default is a blank) + */ +export function trim(haystack: string, needle: string = ' '): string { + const trimmed = ltrim(haystack, needle); + return rtrim(trimmed, needle); +} + +/** + * Removes all occurrences of needle from the beginning of haystack. + * @param haystack string to trim + * @param needle the thing to trim + */ +export function ltrim(haystack: string, needle: string): string { + if (!haystack || !needle) { + return haystack; + } + + const needleLen = needle.length; + if (needleLen === 0 || haystack.length === 0) { + return haystack; + } + + let offset = 0; + + while (haystack.indexOf(needle, offset) === offset) { + offset = offset + needleLen; + } + return haystack.substring(offset); +} + +/** + * Removes all occurrences of needle from the end of haystack. + * @param haystack string to trim + * @param needle the thing to trim + */ +export function rtrim(haystack: string, needle: string): string { + if (!haystack || !needle) { + return haystack; + } + + const needleLen = needle.length, + haystackLen = haystack.length; + + if (needleLen === 0 || haystackLen === 0) { + return haystack; + } + + let offset = haystackLen, + idx = -1; + + while (true) { + idx = haystack.lastIndexOf(needle, offset - 1); + if (idx === -1 || idx + needleLen !== offset) { + break; + } + if (idx === 0) { + return ''; + } + offset = idx; + } + + return haystack.substring(0, offset); +} + +export function convertSimple2RegExpPattern(pattern: string): string { + return pattern.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&').replace(/[\*]/g, '.*'); +} + +export function stripWildcards(pattern: string): string { + return pattern.replace(/\*/g, ''); +} + +export interface RegExpOptions { + matchCase?: boolean; + wholeWord?: boolean; + multiline?: boolean; + global?: boolean; + unicode?: boolean; +} + +export function createRegExp(searchString: string, isRegex: boolean, options: RegExpOptions = {}): RegExp { + if (!searchString) { + throw new Error('Cannot create regex from empty string'); + } + if (!isRegex) { + searchString = escapeRegExpCharacters(searchString); + } + if (options.wholeWord) { + if (!/\B/.test(searchString.charAt(0))) { + searchString = '\\b' + searchString; + } + if (!/\B/.test(searchString.charAt(searchString.length - 1))) { + searchString = searchString + '\\b'; + } + } + let modifiers = ''; + if (options.global) { + modifiers += 'g'; + } + if (!options.matchCase) { + modifiers += 'i'; + } + if (options.multiline) { + modifiers += 'm'; + } + if (options.unicode) { + modifiers += 'u'; + } + + return new RegExp(searchString, modifiers); +} + +export function regExpLeadsToEndlessLoop(regexp: RegExp): boolean { + // Exit early if it's one of these special cases which are meant to match + // against an empty string + if (regexp.source === '^' || regexp.source === '^$' || regexp.source === '$' || regexp.source === '^\\s*$') { + return false; + } + + // We check against an empty string. If the regular expression doesn't advance + // (e.g. ends in an endless loop) it will match an empty string. + const match = regexp.exec(''); + return !!(match && regexp.lastIndex === 0); +} + +export function regExpContainsBackreference(regexpValue: string): boolean { + return !!regexpValue.match(/([^\\]|^)(\\\\)*\\\d+/); +} + +export function regExpFlags(regexp: RegExp): string { + return ( + (regexp.global ? 'g' : '') + + (regexp.ignoreCase ? 'i' : '') + + (regexp.multiline ? 'm' : '') + + ((regexp as any) /* standalone editor compilation */.unicode ? 'u' : '') + ); +} + +export function splitLines(str: string): string[] { + return str.split(/\r\n|\r|\n/); +} + +/** + * Returns first index of the string that is not whitespace. + * If string is empty or contains only whitespaces, returns -1 + */ +export function firstNonWhitespaceIndex(str: string): number { + for (let i = 0, len = str.length; i < len; i++) { + const chCode = str.charCodeAt(i); + if (chCode !== CharCode.Space && chCode !== CharCode.Tab) { + return i; + } + } + return -1; +} + +/** + * Returns the leading whitespace of the string. + * If the string contains only whitespaces, returns entire string + */ +export function getLeadingWhitespace(str: string, start: number = 0, end: number = str.length): string { + for (let i = start; i < end; i++) { + const chCode = str.charCodeAt(i); + if (chCode !== CharCode.Space && chCode !== CharCode.Tab) { + return str.substring(start, i); + } + } + return str.substring(start, end); +} + +/** + * Returns last index of the string that is not whitespace. + * If string is empty or contains only whitespaces, returns -1 + */ +export function lastNonWhitespaceIndex(str: string, startIndex: number = str.length - 1): number { + for (let i = startIndex; i >= 0; i--) { + const chCode = str.charCodeAt(i); + if (chCode !== CharCode.Space && chCode !== CharCode.Tab) { + return i; + } + } + return -1; +} + +export function compare(a: string, b: string): number { + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } else { + return 0; + } +} + +export function compareSubstring( + a: string, + b: string, + aStart: number = 0, + aEnd: number = a.length, + bStart: number = 0, + bEnd: number = b.length +): number { + for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) { + let codeA = a.charCodeAt(aStart); + let codeB = b.charCodeAt(bStart); + if (codeA < codeB) { + return -1; + } else if (codeA > codeB) { + return 1; + } + } + const aLen = aEnd - aStart; + const bLen = bEnd - bStart; + if (aLen < bLen) { + return -1; + } else if (aLen > bLen) { + return 1; + } + return 0; +} + +export function compareIgnoreCase(a: string, b: string): number { + return compareSubstringIgnoreCase(a, b, 0, a.length, 0, b.length); +} + +export function compareSubstringIgnoreCase( + a: string, + b: string, + aStart: number = 0, + aEnd: number = a.length, + bStart: number = 0, + bEnd: number = b.length +): number { + for (; aStart < aEnd && bStart < bEnd; aStart++, bStart++) { + let codeA = a.charCodeAt(aStart); + let codeB = b.charCodeAt(bStart); + + if (codeA === codeB) { + // equal + continue; + } + + if (codeA >= 128 || codeB >= 128) { + // not ASCII letters -> fallback to lower-casing strings + return compareSubstring(a.toLowerCase(), b.toLowerCase(), aStart, aEnd, bStart, bEnd); + } + + // mapper lower-case ascii letter onto upper-case varinats + // [97-122] (lower ascii) --> [65-90] (upper ascii) + if (isLowerAsciiLetter(codeA)) { + codeA -= 32; + } + if (isLowerAsciiLetter(codeB)) { + codeB -= 32; + } + + // compare both code points + const diff = codeA - codeB; + if (diff === 0) { + continue; + } + + return diff; + } + + const aLen = aEnd - aStart; + const bLen = bEnd - bStart; + + if (aLen < bLen) { + return -1; + } else if (aLen > bLen) { + return 1; + } + + return 0; +} + +export function isLowerAsciiLetter(code: number): boolean { + return code >= CharCode.a && code <= CharCode.z; +} + +export function isUpperAsciiLetter(code: number): boolean { + return code >= CharCode.A && code <= CharCode.Z; +} + +export function equalsIgnoreCase(a: string, b: string): boolean { + return a.length === b.length && compareSubstringIgnoreCase(a, b) === 0; +} + +export function startsWithIgnoreCase(str: string, candidate: string): boolean { + const candidateLength = candidate.length; + if (candidate.length > str.length) { + return false; + } + + return compareSubstringIgnoreCase(str, candidate, 0, candidateLength) === 0; +} + +/** + * @returns the length of the common prefix of the two strings. + */ +export function commonPrefixLength(a: string, b: string): number { + let i: number, + len = Math.min(a.length, b.length); + + for (i = 0; i < len; i++) { + if (a.charCodeAt(i) !== b.charCodeAt(i)) { + return i; + } + } + + return len; +} + +/** + * @returns the length of the common suffix of the two strings. + */ +export function commonSuffixLength(a: string, b: string): number { + let i: number, + len = Math.min(a.length, b.length); + + const aLastIndex = a.length - 1; + const bLastIndex = b.length - 1; + + for (i = 0; i < len; i++) { + if (a.charCodeAt(aLastIndex - i) !== b.charCodeAt(bLastIndex - i)) { + return i; + } + } + + return len; +} + +/** + * See http://en.wikipedia.org/wiki/Surrogate_pair + */ +export function isHighSurrogate(charCode: number): boolean { + return 0xd800 <= charCode && charCode <= 0xdbff; +} + +/** + * See http://en.wikipedia.org/wiki/Surrogate_pair + */ +export function isLowSurrogate(charCode: number): boolean { + return 0xdc00 <= charCode && charCode <= 0xdfff; +} + +/** + * See http://en.wikipedia.org/wiki/Surrogate_pair + */ +export function computeCodePoint(highSurrogate: number, lowSurrogate: number): number { + return ((highSurrogate - 0xd800) << 10) + (lowSurrogate - 0xdc00) + 0x10000; +} + +/** + * get the code point that begins at offset `offset` + */ +export function getNextCodePoint(str: string, len: number, offset: number): number { + const charCode = str.charCodeAt(offset); + if (isHighSurrogate(charCode) && offset + 1 < len) { + const nextCharCode = str.charCodeAt(offset + 1); + if (isLowSurrogate(nextCharCode)) { + return computeCodePoint(charCode, nextCharCode); + } + } + return charCode; +} + +/** + * get the code point that ends right before offset `offset` + */ +function getPrevCodePoint(str: string, offset: number): number { + const charCode = str.charCodeAt(offset - 1); + if (isLowSurrogate(charCode) && offset > 1) { + const prevCharCode = str.charCodeAt(offset - 2); + if (isHighSurrogate(prevCharCode)) { + return computeCodePoint(prevCharCode, charCode); + } + } + return charCode; +} + +export class CodePointIterator { + private readonly _str: string; + private readonly _len: number; + private _offset: number; + + public get offset(): number { + return this._offset; + } + + constructor(str: string, offset: number = 0) { + this._str = str; + this._len = str.length; + this._offset = offset; + } + + public setOffset(offset: number): void { + this._offset = offset; + } + + public prevCodePoint(): number { + const codePoint = getPrevCodePoint(this._str, this._offset); + this._offset -= codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1; + return codePoint; + } + + public nextCodePoint(): number { + const codePoint = getNextCodePoint(this._str, this._len, this._offset); + this._offset += codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1; + return codePoint; + } + + public eol(): boolean { + return this._offset >= this._len; + } +} + +export class GraphemeIterator { + private readonly _iterator: CodePointIterator; + + public get offset(): number { + return this._iterator.offset; + } + + constructor(str: string, offset: number = 0) { + this._iterator = new CodePointIterator(str, offset); + } + + public nextGraphemeLength(): number { + const graphemeBreakTree = GraphemeBreakTree.getInstance(); + const iterator = this._iterator; + const initialOffset = iterator.offset; + + let graphemeBreakType = graphemeBreakTree.getGraphemeBreakType(iterator.nextCodePoint()); + while (!iterator.eol()) { + const offset = iterator.offset; + const nextGraphemeBreakType = graphemeBreakTree.getGraphemeBreakType(iterator.nextCodePoint()); + if (breakBetweenGraphemeBreakType(graphemeBreakType, nextGraphemeBreakType)) { + // move iterator back + iterator.setOffset(offset); + break; + } + graphemeBreakType = nextGraphemeBreakType; + } + return iterator.offset - initialOffset; + } + + public prevGraphemeLength(): number { + const graphemeBreakTree = GraphemeBreakTree.getInstance(); + const iterator = this._iterator; + const initialOffset = iterator.offset; + + let graphemeBreakType = graphemeBreakTree.getGraphemeBreakType(iterator.prevCodePoint()); + while (iterator.offset > 0) { + const offset = iterator.offset; + const prevGraphemeBreakType = graphemeBreakTree.getGraphemeBreakType(iterator.prevCodePoint()); + if (breakBetweenGraphemeBreakType(prevGraphemeBreakType, graphemeBreakType)) { + // move iterator back + iterator.setOffset(offset); + break; + } + graphemeBreakType = prevGraphemeBreakType; + } + return initialOffset - iterator.offset; + } + + public eol(): boolean { + return this._iterator.eol(); + } +} + +export function nextCharLength(str: string, initialOffset: number): number { + const iterator = new GraphemeIterator(str, initialOffset); + return iterator.nextGraphemeLength(); +} + +export function prevCharLength(str: string, initialOffset: number): number { + const iterator = new GraphemeIterator(str, initialOffset); + return iterator.prevGraphemeLength(); +} + +export function getCharContainingOffset(str: string, offset: number): [number, number] { + if (offset > 0 && isLowSurrogate(str.charCodeAt(offset))) { + offset--; + } + const endOffset = offset + nextCharLength(str, offset); + const startOffset = endOffset - prevCharLength(str, endOffset); + return [startOffset, endOffset]; +} + +/** + * Generated using https://github.com/alexdima/unicode-utils/blob/main/rtl-test.js + */ +const CONTAINS_RTL = + /(?:[\u05BE\u05C0\u05C3\u05C6\u05D0-\u05F4\u0608\u060B\u060D\u061B-\u064A\u066D-\u066F\u0671-\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u0710\u0712-\u072F\u074D-\u07A5\u07B1-\u07EA\u07F4\u07F5\u07FA\u07FE-\u0815\u081A\u0824\u0828\u0830-\u0858\u085E-\u088E\u08A0-\u08C9\u200F\uFB1D\uFB1F-\uFB28\uFB2A-\uFD3D\uFD50-\uFDC7\uFDF0-\uFDFC\uFE70-\uFEFC]|\uD802[\uDC00-\uDD1B\uDD20-\uDE00\uDE10-\uDE35\uDE40-\uDEE4\uDEEB-\uDF35\uDF40-\uDFFF]|\uD803[\uDC00-\uDD23\uDE80-\uDEA9\uDEAD-\uDF45\uDF51-\uDF81\uDF86-\uDFF6]|\uD83A[\uDC00-\uDCCF\uDD00-\uDD43\uDD4B-\uDFFF]|\uD83B[\uDC00-\uDEBB])/; + +/** + * Returns true if `str` contains any Unicode character that is classified as "R" or "AL". + */ +export function containsRTL(str: string): boolean { + return CONTAINS_RTL.test(str); +} + +const IS_BASIC_ASCII = /^[\t\n\r\x20-\x7E]*$/; +/** + * Returns true if `str` contains only basic ASCII characters in the range 32 - 126 (including 32 and 126) or \n, \r, \t + */ +export function isBasicASCII(str: string): boolean { + return IS_BASIC_ASCII.test(str); +} + +export const UNUSUAL_LINE_TERMINATORS = /[\u2028\u2029]/; // LINE SEPARATOR (LS) or PARAGRAPH SEPARATOR (PS) +/** + * Returns true if `str` contains unusual line terminators, like LS or PS + */ +export function containsUnusualLineTerminators(str: string): boolean { + return UNUSUAL_LINE_TERMINATORS.test(str); +} + +export function isFullWidthCharacter(charCode: number): boolean { + // Do a cheap trick to better support wrapping of wide characters, treat them as 2 columns + // http://jrgraphix.net/research/unicode_blocks.php + // 2E80 - 2EFF CJK Radicals Supplement + // 2F00 - 2FDF Kangxi Radicals + // 2FF0 - 2FFF Ideographic Description Characters + // 3000 - 303F CJK Symbols and Punctuation + // 3040 - 309F Hiragana + // 30A0 - 30FF Katakana + // 3100 - 312F Bopomofo + // 3130 - 318F Hangul Compatibility Jamo + // 3190 - 319F Kanbun + // 31A0 - 31BF Bopomofo Extended + // 31F0 - 31FF Katakana Phonetic Extensions + // 3200 - 32FF Enclosed CJK Letters and Months + // 3300 - 33FF CJK Compatibility + // 3400 - 4DBF CJK Unified Ideographs Extension A + // 4DC0 - 4DFF Yijing Hexagram Symbols + // 4E00 - 9FFF CJK Unified Ideographs + // A000 - A48F Yi Syllables + // A490 - A4CF Yi Radicals + // AC00 - D7AF Hangul Syllables + // [IGNORE] D800 - DB7F High Surrogates + // [IGNORE] DB80 - DBFF High Private Use Surrogates + // [IGNORE] DC00 - DFFF Low Surrogates + // [IGNORE] E000 - F8FF Private Use Area + // F900 - FAFF CJK Compatibility Ideographs + // [IGNORE] FB00 - FB4F Alphabetic Presentation Forms + // [IGNORE] FB50 - FDFF Arabic Presentation Forms-A + // [IGNORE] FE00 - FE0F Variation Selectors + // [IGNORE] FE20 - FE2F Combining Half Marks + // [IGNORE] FE30 - FE4F CJK Compatibility Forms + // [IGNORE] FE50 - FE6F Small Form Variants + // [IGNORE] FE70 - FEFF Arabic Presentation Forms-B + // FF00 - FFEF Halfwidth and Fullwidth Forms + // [https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms] + // of which FF01 - FF5E fullwidth ASCII of 21 to 7E + // [IGNORE] and FF65 - FFDC halfwidth of Katakana and Hangul + // [IGNORE] FFF0 - FFFF Specials + return ( + (charCode >= 0x2e80 && charCode <= 0xd7af) || + (charCode >= 0xf900 && charCode <= 0xfaff) || + (charCode >= 0xff01 && charCode <= 0xff5e) + ); +} + +/** + * A fast function (therefore imprecise) to check if code points are emojis. + * Generated using https://github.com/alexdima/unicode-utils/blob/main/emoji-test.js + */ +export function isEmojiImprecise(x: number): boolean { + return ( + (x >= 0x1f1e6 && x <= 0x1f1ff) || + x === 8986 || + x === 8987 || + x === 9200 || + x === 9203 || + (x >= 9728 && x <= 10175) || + x === 11088 || + x === 11093 || + (x >= 127744 && x <= 128591) || + (x >= 128640 && x <= 128764) || + (x >= 128992 && x <= 129008) || + (x >= 129280 && x <= 129535) || + (x >= 129648 && x <= 129782) + ); +} + +/** + * Given a string and a max length returns a shorted version. Shorting + * happens at favorable positions - such as whitespace or punctuation characters. + */ +export function lcut(text: string, n: number) { + if (text.length < n) { + return text; + } + + const re = /\b/g; + let i = 0; + while (re.test(text)) { + if (text.length - re.lastIndex < n) { + break; + } + + i = re.lastIndex; + re.lastIndex += 1; + } + + return text.substring(i).replace(/^\s/, ''); +} + +// Escape codes +// http://en.wikipedia.org/wiki/ANSI_escape_code +const EL = /\x1B\x5B[12]?K/g; // Erase in line +const COLOR_START = /\x1b\[\d+m/g; // Color +const COLOR_END = /\x1b\[0?m/g; // Color + +export function removeAnsiEscapeCodes(str: string): string { + if (str) { + str = str.replace(EL, ''); + str = str.replace(COLOR_START, ''); + str = str.replace(COLOR_END, ''); + } + + return str; +} + +// -- UTF-8 BOM + +export const UTF8_BOM_CHARACTER = String.fromCharCode(CharCode.UTF8_BOM); + +export function startsWithUTF8BOM(str: string): boolean { + return !!(str && str.length > 0 && str.charCodeAt(0) === CharCode.UTF8_BOM); +} + +export function stripUTF8BOM(str: string): string { + return startsWithUTF8BOM(str) ? str.substr(1) : str; +} + +/** + * Checks if the characters of the provided query string are included in the + * target string. The characters do not have to be contiguous within the string. + */ +export function fuzzyContains(target: string, query: string): boolean { + if (!target || !query) { + return false; // return early if target or query are undefined + } + + if (target.length < query.length) { + return false; // impossible for query to be contained in target + } + + const queryLen = query.length; + const targetLower = target.toLowerCase(); + + let index = 0; + let lastIndexOf = -1; + while (index < queryLen) { + const indexOf = targetLower.indexOf(query[index], lastIndexOf + 1); + if (indexOf < 0) { + return false; + } + + lastIndexOf = indexOf; + + index++; + } + + return true; +} + +export function containsUppercaseCharacter(target: string, ignoreEscapedChars = false): boolean { + if (!target) { + return false; + } + + if (ignoreEscapedChars) { + target = target.replace(/\\./g, ''); + } + + return target.toLowerCase() !== target; +} + +export function uppercaseFirstLetter(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export function getNLines(str: string, n = 1): string { + if (n === 0) { + return ''; + } + + let idx = -1; + do { + idx = str.indexOf('\n', idx + 1); + n--; + } while (n > 0 && idx >= 0); + + if (idx === -1) { + return str; + } + + if (str[idx - 1] === '\r') { + idx--; + } + + return str.substr(0, idx); +} + +/** + * Produces 'a'-'z', followed by 'A'-'Z'... followed by 'a'-'z', etc. + */ +export function singleLetterHash(n: number): string { + const LETTERS_CNT = CharCode.Z - CharCode.A + 1; + + n = n % (2 * LETTERS_CNT); + + if (n < LETTERS_CNT) { + return String.fromCharCode(CharCode.a + n); + } + + return String.fromCharCode(CharCode.A + n - LETTERS_CNT); +} + +//#region Unicode Grapheme Break + +export function getGraphemeBreakType(codePoint: number): GraphemeBreakType { + const graphemeBreakTree = GraphemeBreakTree.getInstance(); + return graphemeBreakTree.getGraphemeBreakType(codePoint); +} + +function breakBetweenGraphemeBreakType(breakTypeA: GraphemeBreakType, breakTypeB: GraphemeBreakType): boolean { + // http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules + + // !!! Let's make the common case a bit faster + if (breakTypeA === GraphemeBreakType.Other) { + // see https://www.unicode.org/Public/13.0.0/ucd/auxiliary/GraphemeBreakTest-13.0.0d10.html#table + return breakTypeB !== GraphemeBreakType.Extend && breakTypeB !== GraphemeBreakType.SpacingMark; + } + + // Do not break between a CR and LF. Otherwise, break before and after controls. + // GB3 CR × LF + // GB4 (Control | CR | LF) ÷ + // GB5 ÷ (Control | CR | LF) + if (breakTypeA === GraphemeBreakType.CR) { + if (breakTypeB === GraphemeBreakType.LF) { + return false; // GB3 + } + } + if ( + breakTypeA === GraphemeBreakType.Control || + breakTypeA === GraphemeBreakType.CR || + breakTypeA === GraphemeBreakType.LF + ) { + return true; // GB4 + } + if ( + breakTypeB === GraphemeBreakType.Control || + breakTypeB === GraphemeBreakType.CR || + breakTypeB === GraphemeBreakType.LF + ) { + return true; // GB5 + } + + // Do not break Hangul syllable sequences. + // GB6 L × (L | V | LV | LVT) + // GB7 (LV | V) × (V | T) + // GB8 (LVT | T) × T + if (breakTypeA === GraphemeBreakType.L) { + if ( + breakTypeB === GraphemeBreakType.L || + breakTypeB === GraphemeBreakType.V || + breakTypeB === GraphemeBreakType.LV || + breakTypeB === GraphemeBreakType.LVT + ) { + return false; // GB6 + } + } + if (breakTypeA === GraphemeBreakType.LV || breakTypeA === GraphemeBreakType.V) { + if (breakTypeB === GraphemeBreakType.V || breakTypeB === GraphemeBreakType.T) { + return false; // GB7 + } + } + if (breakTypeA === GraphemeBreakType.LVT || breakTypeA === GraphemeBreakType.T) { + if (breakTypeB === GraphemeBreakType.T) { + return false; // GB8 + } + } + + // Do not break before extending characters or ZWJ. + // GB9 × (Extend | ZWJ) + if (breakTypeB === GraphemeBreakType.Extend || breakTypeB === GraphemeBreakType.ZWJ) { + return false; // GB9 + } + + // The GB9a and GB9b rules only apply to extended grapheme clusters: + // Do not break before SpacingMarks, or after Prepend characters. + // GB9a × SpacingMark + // GB9b Prepend × + if (breakTypeB === GraphemeBreakType.SpacingMark) { + return false; // GB9a + } + if (breakTypeA === GraphemeBreakType.Prepend) { + return false; // GB9b + } + + // Do not break within emoji modifier sequences or emoji zwj sequences. + // GB11 \p{Extended_Pictographic} Extend* ZWJ × \p{Extended_Pictographic} + if (breakTypeA === GraphemeBreakType.ZWJ && breakTypeB === GraphemeBreakType.Extended_Pictographic) { + // Note: we are not implementing the rule entirely here to avoid introducing states + return false; // GB11 + } + + // GB12 sot (RI RI)* RI × RI + // GB13 [^RI] (RI RI)* RI × RI + if (breakTypeA === GraphemeBreakType.Regional_Indicator && breakTypeB === GraphemeBreakType.Regional_Indicator) { + // Note: we are not implementing the rule entirely here to avoid introducing states + return false; // GB12 & GB13 + } + + // GB999 Any ÷ Any + return true; +} + +export const enum GraphemeBreakType { + Other = 0, + Prepend = 1, + CR = 2, + LF = 3, + Control = 4, + Extend = 5, + Regional_Indicator = 6, + SpacingMark = 7, + L = 8, + V = 9, + T = 10, + LV = 11, + LVT = 12, + ZWJ = 13, + Extended_Pictographic = 14 +} + +class GraphemeBreakTree { + private static _INSTANCE: GraphemeBreakTree | null = null; + public static getInstance(): GraphemeBreakTree { + if (!GraphemeBreakTree._INSTANCE) { + GraphemeBreakTree._INSTANCE = new GraphemeBreakTree(); + } + return GraphemeBreakTree._INSTANCE; + } + + private readonly _data: number[]; + + constructor() { + this._data = getGraphemeBreakRawData(); + } + + public getGraphemeBreakType(codePoint: number): GraphemeBreakType { + // !!! Let's make 7bit ASCII a bit faster: 0..31 + if (codePoint < 32) { + if (codePoint === CharCode.LineFeed) { + return GraphemeBreakType.LF; + } + if (codePoint === CharCode.CarriageReturn) { + return GraphemeBreakType.CR; + } + return GraphemeBreakType.Control; + } + // !!! Let's make 7bit ASCII a bit faster: 32..126 + if (codePoint < 127) { + return GraphemeBreakType.Other; + } + + const data = this._data; + const nodeCount = data.length / 3; + let nodeIndex = 1; + while (nodeIndex <= nodeCount) { + if (codePoint < data[3 * nodeIndex]) { + // go left + nodeIndex = 2 * nodeIndex; + } else if (codePoint > data[3 * nodeIndex + 1]) { + // go right + nodeIndex = 2 * nodeIndex + 1; + } else { + // hit + return data[3 * nodeIndex + 2]; + } + } + + return GraphemeBreakType.Other; + } +} + +function getGraphemeBreakRawData(): number[] { + // generated using https://github.com/alexdima/unicode-utils/blob/main/grapheme-break.js + return JSON.parse( + '[0,0,0,51229,51255,12,44061,44087,12,127462,127487,6,7083,7085,5,47645,47671,12,54813,54839,12,128678,128678,14,3270,3270,5,9919,9923,14,45853,45879,12,49437,49463,12,53021,53047,12,71216,71218,7,128398,128399,14,129360,129374,14,2519,2519,5,4448,4519,9,9742,9742,14,12336,12336,14,44957,44983,12,46749,46775,12,48541,48567,12,50333,50359,12,52125,52151,12,53917,53943,12,69888,69890,5,73018,73018,5,127990,127990,14,128558,128559,14,128759,128760,14,129653,129655,14,2027,2035,5,2891,2892,7,3761,3761,5,6683,6683,5,8293,8293,4,9825,9826,14,9999,9999,14,43452,43453,5,44509,44535,12,45405,45431,12,46301,46327,12,47197,47223,12,48093,48119,12,48989,49015,12,49885,49911,12,50781,50807,12,51677,51703,12,52573,52599,12,53469,53495,12,54365,54391,12,65279,65279,4,70471,70472,7,72145,72147,7,119173,119179,5,127799,127818,14,128240,128244,14,128512,128512,14,128652,128652,14,128721,128722,14,129292,129292,14,129445,129450,14,129734,129743,14,1476,1477,5,2366,2368,7,2750,2752,7,3076,3076,5,3415,3415,5,4141,4144,5,6109,6109,5,6964,6964,5,7394,7400,5,9197,9198,14,9770,9770,14,9877,9877,14,9968,9969,14,10084,10084,14,43052,43052,5,43713,43713,5,44285,44311,12,44733,44759,12,45181,45207,12,45629,45655,12,46077,46103,12,46525,46551,12,46973,46999,12,47421,47447,12,47869,47895,12,48317,48343,12,48765,48791,12,49213,49239,12,49661,49687,12,50109,50135,12,50557,50583,12,51005,51031,12,51453,51479,12,51901,51927,12,52349,52375,12,52797,52823,12,53245,53271,12,53693,53719,12,54141,54167,12,54589,54615,12,55037,55063,12,69506,69509,5,70191,70193,5,70841,70841,7,71463,71467,5,72330,72342,5,94031,94031,5,123628,123631,5,127763,127765,14,127941,127941,14,128043,128062,14,128302,128317,14,128465,128467,14,128539,128539,14,128640,128640,14,128662,128662,14,128703,128703,14,128745,128745,14,129004,129007,14,129329,129330,14,129402,129402,14,129483,129483,14,129686,129704,14,130048,131069,14,173,173,4,1757,1757,1,2200,2207,5,2434,2435,7,2631,2632,5,2817,2817,5,3008,3008,5,3201,3201,5,3387,3388,5,3542,3542,5,3902,3903,7,4190,4192,5,6002,6003,5,6439,6440,5,6765,6770,7,7019,7027,5,7154,7155,7,8205,8205,13,8505,8505,14,9654,9654,14,9757,9757,14,9792,9792,14,9852,9853,14,9890,9894,14,9937,9937,14,9981,9981,14,10035,10036,14,11035,11036,14,42654,42655,5,43346,43347,7,43587,43587,5,44006,44007,7,44173,44199,12,44397,44423,12,44621,44647,12,44845,44871,12,45069,45095,12,45293,45319,12,45517,45543,12,45741,45767,12,45965,45991,12,46189,46215,12,46413,46439,12,46637,46663,12,46861,46887,12,47085,47111,12,47309,47335,12,47533,47559,12,47757,47783,12,47981,48007,12,48205,48231,12,48429,48455,12,48653,48679,12,48877,48903,12,49101,49127,12,49325,49351,12,49549,49575,12,49773,49799,12,49997,50023,12,50221,50247,12,50445,50471,12,50669,50695,12,50893,50919,12,51117,51143,12,51341,51367,12,51565,51591,12,51789,51815,12,52013,52039,12,52237,52263,12,52461,52487,12,52685,52711,12,52909,52935,12,53133,53159,12,53357,53383,12,53581,53607,12,53805,53831,12,54029,54055,12,54253,54279,12,54477,54503,12,54701,54727,12,54925,54951,12,55149,55175,12,68101,68102,5,69762,69762,7,70067,70069,7,70371,70378,5,70720,70721,7,71087,71087,5,71341,71341,5,71995,71996,5,72249,72249,7,72850,72871,5,73109,73109,5,118576,118598,5,121505,121519,5,127245,127247,14,127568,127569,14,127777,127777,14,127872,127891,14,127956,127967,14,128015,128016,14,128110,128172,14,128259,128259,14,128367,128368,14,128424,128424,14,128488,128488,14,128530,128532,14,128550,128551,14,128566,128566,14,128647,128647,14,128656,128656,14,128667,128673,14,128691,128693,14,128715,128715,14,128728,128732,14,128752,128752,14,128765,128767,14,129096,129103,14,129311,129311,14,129344,129349,14,129394,129394,14,129413,129425,14,129466,129471,14,129511,129535,14,129664,129666,14,129719,129722,14,129760,129767,14,917536,917631,5,13,13,2,1160,1161,5,1564,1564,4,1807,1807,1,2085,2087,5,2307,2307,7,2382,2383,7,2497,2500,5,2563,2563,7,2677,2677,5,2763,2764,7,2879,2879,5,2914,2915,5,3021,3021,5,3142,3144,5,3263,3263,5,3285,3286,5,3398,3400,7,3530,3530,5,3633,3633,5,3864,3865,5,3974,3975,5,4155,4156,7,4229,4230,5,5909,5909,7,6078,6085,7,6277,6278,5,6451,6456,7,6744,6750,5,6846,6846,5,6972,6972,5,7074,7077,5,7146,7148,7,7222,7223,5,7416,7417,5,8234,8238,4,8417,8417,5,9000,9000,14,9203,9203,14,9730,9731,14,9748,9749,14,9762,9763,14,9776,9783,14,9800,9811,14,9831,9831,14,9872,9873,14,9882,9882,14,9900,9903,14,9929,9933,14,9941,9960,14,9974,9974,14,9989,9989,14,10006,10006,14,10062,10062,14,10160,10160,14,11647,11647,5,12953,12953,14,43019,43019,5,43232,43249,5,43443,43443,5,43567,43568,7,43696,43696,5,43765,43765,7,44013,44013,5,44117,44143,12,44229,44255,12,44341,44367,12,44453,44479,12,44565,44591,12,44677,44703,12,44789,44815,12,44901,44927,12,45013,45039,12,45125,45151,12,45237,45263,12,45349,45375,12,45461,45487,12,45573,45599,12,45685,45711,12,45797,45823,12,45909,45935,12,46021,46047,12,46133,46159,12,46245,46271,12,46357,46383,12,46469,46495,12,46581,46607,12,46693,46719,12,46805,46831,12,46917,46943,12,47029,47055,12,47141,47167,12,47253,47279,12,47365,47391,12,47477,47503,12,47589,47615,12,47701,47727,12,47813,47839,12,47925,47951,12,48037,48063,12,48149,48175,12,48261,48287,12,48373,48399,12,48485,48511,12,48597,48623,12,48709,48735,12,48821,48847,12,48933,48959,12,49045,49071,12,49157,49183,12,49269,49295,12,49381,49407,12,49493,49519,12,49605,49631,12,49717,49743,12,49829,49855,12,49941,49967,12,50053,50079,12,50165,50191,12,50277,50303,12,50389,50415,12,50501,50527,12,50613,50639,12,50725,50751,12,50837,50863,12,50949,50975,12,51061,51087,12,51173,51199,12,51285,51311,12,51397,51423,12,51509,51535,12,51621,51647,12,51733,51759,12,51845,51871,12,51957,51983,12,52069,52095,12,52181,52207,12,52293,52319,12,52405,52431,12,52517,52543,12,52629,52655,12,52741,52767,12,52853,52879,12,52965,52991,12,53077,53103,12,53189,53215,12,53301,53327,12,53413,53439,12,53525,53551,12,53637,53663,12,53749,53775,12,53861,53887,12,53973,53999,12,54085,54111,12,54197,54223,12,54309,54335,12,54421,54447,12,54533,54559,12,54645,54671,12,54757,54783,12,54869,54895,12,54981,55007,12,55093,55119,12,55243,55291,10,66045,66045,5,68325,68326,5,69688,69702,5,69817,69818,5,69957,69958,7,70089,70092,5,70198,70199,5,70462,70462,5,70502,70508,5,70750,70750,5,70846,70846,7,71100,71101,5,71230,71230,7,71351,71351,5,71737,71738,5,72000,72000,7,72160,72160,5,72273,72278,5,72752,72758,5,72882,72883,5,73031,73031,5,73461,73462,7,94192,94193,7,119149,119149,7,121403,121452,5,122915,122916,5,126980,126980,14,127358,127359,14,127535,127535,14,127759,127759,14,127771,127771,14,127792,127793,14,127825,127867,14,127897,127899,14,127945,127945,14,127985,127986,14,128000,128007,14,128021,128021,14,128066,128100,14,128184,128235,14,128249,128252,14,128266,128276,14,128335,128335,14,128379,128390,14,128407,128419,14,128444,128444,14,128481,128481,14,128499,128499,14,128526,128526,14,128536,128536,14,128543,128543,14,128556,128556,14,128564,128564,14,128577,128580,14,128643,128645,14,128649,128649,14,128654,128654,14,128660,128660,14,128664,128664,14,128675,128675,14,128686,128689,14,128695,128696,14,128705,128709,14,128717,128719,14,128725,128725,14,128736,128741,14,128747,128748,14,128755,128755,14,128762,128762,14,128981,128991,14,129009,129023,14,129160,129167,14,129296,129304,14,129320,129327,14,129340,129342,14,129356,129356,14,129388,129392,14,129399,129400,14,129404,129407,14,129432,129442,14,129454,129455,14,129473,129474,14,129485,129487,14,129648,129651,14,129659,129660,14,129671,129679,14,129709,129711,14,129728,129730,14,129751,129753,14,129776,129782,14,917505,917505,4,917760,917999,5,10,10,3,127,159,4,768,879,5,1471,1471,5,1536,1541,1,1648,1648,5,1767,1768,5,1840,1866,5,2070,2073,5,2137,2139,5,2274,2274,1,2363,2363,7,2377,2380,7,2402,2403,5,2494,2494,5,2507,2508,7,2558,2558,5,2622,2624,7,2641,2641,5,2691,2691,7,2759,2760,5,2786,2787,5,2876,2876,5,2881,2884,5,2901,2902,5,3006,3006,5,3014,3016,7,3072,3072,5,3134,3136,5,3157,3158,5,3260,3260,5,3266,3266,5,3274,3275,7,3328,3329,5,3391,3392,7,3405,3405,5,3457,3457,5,3536,3537,7,3551,3551,5,3636,3642,5,3764,3772,5,3895,3895,5,3967,3967,7,3993,4028,5,4146,4151,5,4182,4183,7,4226,4226,5,4253,4253,5,4957,4959,5,5940,5940,7,6070,6070,7,6087,6088,7,6158,6158,4,6432,6434,5,6448,6449,7,6679,6680,5,6742,6742,5,6754,6754,5,6783,6783,5,6912,6915,5,6966,6970,5,6978,6978,5,7042,7042,7,7080,7081,5,7143,7143,7,7150,7150,7,7212,7219,5,7380,7392,5,7412,7412,5,8203,8203,4,8232,8232,4,8265,8265,14,8400,8412,5,8421,8432,5,8617,8618,14,9167,9167,14,9200,9200,14,9410,9410,14,9723,9726,14,9733,9733,14,9745,9745,14,9752,9752,14,9760,9760,14,9766,9766,14,9774,9774,14,9786,9786,14,9794,9794,14,9823,9823,14,9828,9828,14,9833,9850,14,9855,9855,14,9875,9875,14,9880,9880,14,9885,9887,14,9896,9897,14,9906,9916,14,9926,9927,14,9935,9935,14,9939,9939,14,9962,9962,14,9972,9972,14,9978,9978,14,9986,9986,14,9997,9997,14,10002,10002,14,10017,10017,14,10055,10055,14,10071,10071,14,10133,10135,14,10548,10549,14,11093,11093,14,12330,12333,5,12441,12442,5,42608,42610,5,43010,43010,5,43045,43046,5,43188,43203,7,43302,43309,5,43392,43394,5,43446,43449,5,43493,43493,5,43571,43572,7,43597,43597,7,43703,43704,5,43756,43757,5,44003,44004,7,44009,44010,7,44033,44059,12,44089,44115,12,44145,44171,12,44201,44227,12,44257,44283,12,44313,44339,12,44369,44395,12,44425,44451,12,44481,44507,12,44537,44563,12,44593,44619,12,44649,44675,12,44705,44731,12,44761,44787,12,44817,44843,12,44873,44899,12,44929,44955,12,44985,45011,12,45041,45067,12,45097,45123,12,45153,45179,12,45209,45235,12,45265,45291,12,45321,45347,12,45377,45403,12,45433,45459,12,45489,45515,12,45545,45571,12,45601,45627,12,45657,45683,12,45713,45739,12,45769,45795,12,45825,45851,12,45881,45907,12,45937,45963,12,45993,46019,12,46049,46075,12,46105,46131,12,46161,46187,12,46217,46243,12,46273,46299,12,46329,46355,12,46385,46411,12,46441,46467,12,46497,46523,12,46553,46579,12,46609,46635,12,46665,46691,12,46721,46747,12,46777,46803,12,46833,46859,12,46889,46915,12,46945,46971,12,47001,47027,12,47057,47083,12,47113,47139,12,47169,47195,12,47225,47251,12,47281,47307,12,47337,47363,12,47393,47419,12,47449,47475,12,47505,47531,12,47561,47587,12,47617,47643,12,47673,47699,12,47729,47755,12,47785,47811,12,47841,47867,12,47897,47923,12,47953,47979,12,48009,48035,12,48065,48091,12,48121,48147,12,48177,48203,12,48233,48259,12,48289,48315,12,48345,48371,12,48401,48427,12,48457,48483,12,48513,48539,12,48569,48595,12,48625,48651,12,48681,48707,12,48737,48763,12,48793,48819,12,48849,48875,12,48905,48931,12,48961,48987,12,49017,49043,12,49073,49099,12,49129,49155,12,49185,49211,12,49241,49267,12,49297,49323,12,49353,49379,12,49409,49435,12,49465,49491,12,49521,49547,12,49577,49603,12,49633,49659,12,49689,49715,12,49745,49771,12,49801,49827,12,49857,49883,12,49913,49939,12,49969,49995,12,50025,50051,12,50081,50107,12,50137,50163,12,50193,50219,12,50249,50275,12,50305,50331,12,50361,50387,12,50417,50443,12,50473,50499,12,50529,50555,12,50585,50611,12,50641,50667,12,50697,50723,12,50753,50779,12,50809,50835,12,50865,50891,12,50921,50947,12,50977,51003,12,51033,51059,12,51089,51115,12,51145,51171,12,51201,51227,12,51257,51283,12,51313,51339,12,51369,51395,12,51425,51451,12,51481,51507,12,51537,51563,12,51593,51619,12,51649,51675,12,51705,51731,12,51761,51787,12,51817,51843,12,51873,51899,12,51929,51955,12,51985,52011,12,52041,52067,12,52097,52123,12,52153,52179,12,52209,52235,12,52265,52291,12,52321,52347,12,52377,52403,12,52433,52459,12,52489,52515,12,52545,52571,12,52601,52627,12,52657,52683,12,52713,52739,12,52769,52795,12,52825,52851,12,52881,52907,12,52937,52963,12,52993,53019,12,53049,53075,12,53105,53131,12,53161,53187,12,53217,53243,12,53273,53299,12,53329,53355,12,53385,53411,12,53441,53467,12,53497,53523,12,53553,53579,12,53609,53635,12,53665,53691,12,53721,53747,12,53777,53803,12,53833,53859,12,53889,53915,12,53945,53971,12,54001,54027,12,54057,54083,12,54113,54139,12,54169,54195,12,54225,54251,12,54281,54307,12,54337,54363,12,54393,54419,12,54449,54475,12,54505,54531,12,54561,54587,12,54617,54643,12,54673,54699,12,54729,54755,12,54785,54811,12,54841,54867,12,54897,54923,12,54953,54979,12,55009,55035,12,55065,55091,12,55121,55147,12,55177,55203,12,65024,65039,5,65520,65528,4,66422,66426,5,68152,68154,5,69291,69292,5,69633,69633,5,69747,69748,5,69811,69814,5,69826,69826,5,69932,69932,7,70016,70017,5,70079,70080,7,70095,70095,5,70196,70196,5,70367,70367,5,70402,70403,7,70464,70464,5,70487,70487,5,70709,70711,7,70725,70725,7,70833,70834,7,70843,70844,7,70849,70849,7,71090,71093,5,71103,71104,5,71227,71228,7,71339,71339,5,71344,71349,5,71458,71461,5,71727,71735,5,71985,71989,7,71998,71998,5,72002,72002,7,72154,72155,5,72193,72202,5,72251,72254,5,72281,72283,5,72344,72345,5,72766,72766,7,72874,72880,5,72885,72886,5,73023,73029,5,73104,73105,5,73111,73111,5,92912,92916,5,94095,94098,5,113824,113827,4,119142,119142,7,119155,119162,4,119362,119364,5,121476,121476,5,122888,122904,5,123184,123190,5,125252,125258,5,127183,127183,14,127340,127343,14,127377,127386,14,127491,127503,14,127548,127551,14,127744,127756,14,127761,127761,14,127769,127769,14,127773,127774,14,127780,127788,14,127796,127797,14,127820,127823,14,127869,127869,14,127894,127895,14,127902,127903,14,127943,127943,14,127947,127950,14,127972,127972,14,127988,127988,14,127992,127994,14,128009,128011,14,128019,128019,14,128023,128041,14,128064,128064,14,128102,128107,14,128174,128181,14,128238,128238,14,128246,128247,14,128254,128254,14,128264,128264,14,128278,128299,14,128329,128330,14,128348,128359,14,128371,128377,14,128392,128393,14,128401,128404,14,128421,128421,14,128433,128434,14,128450,128452,14,128476,128478,14,128483,128483,14,128495,128495,14,128506,128506,14,128519,128520,14,128528,128528,14,128534,128534,14,128538,128538,14,128540,128542,14,128544,128549,14,128552,128555,14,128557,128557,14,128560,128563,14,128565,128565,14,128567,128576,14,128581,128591,14,128641,128642,14,128646,128646,14,128648,128648,14,128650,128651,14,128653,128653,14,128655,128655,14,128657,128659,14,128661,128661,14,128663,128663,14,128665,128666,14,128674,128674,14,128676,128677,14,128679,128685,14,128690,128690,14,128694,128694,14,128697,128702,14,128704,128704,14,128710,128714,14,128716,128716,14,128720,128720,14,128723,128724,14,128726,128727,14,128733,128735,14,128742,128744,14,128746,128746,14,128749,128751,14,128753,128754,14,128756,128758,14,128761,128761,14,128763,128764,14,128884,128895,14,128992,129003,14,129008,129008,14,129036,129039,14,129114,129119,14,129198,129279,14,129293,129295,14,129305,129310,14,129312,129319,14,129328,129328,14,129331,129338,14,129343,129343,14,129351,129355,14,129357,129359,14,129375,129387,14,129393,129393,14,129395,129398,14,129401,129401,14,129403,129403,14,129408,129412,14,129426,129431,14,129443,129444,14,129451,129453,14,129456,129465,14,129472,129472,14,129475,129482,14,129484,129484,14,129488,129510,14,129536,129647,14,129652,129652,14,129656,129658,14,129661,129663,14,129667,129670,14,129680,129685,14,129705,129708,14,129712,129718,14,129723,129727,14,129731,129733,14,129744,129750,14,129754,129759,14,129768,129775,14,129783,129791,14,917504,917504,4,917506,917535,4,917632,917759,4,918000,921599,4,0,9,4,11,12,4,14,31,4,169,169,14,174,174,14,1155,1159,5,1425,1469,5,1473,1474,5,1479,1479,5,1552,1562,5,1611,1631,5,1750,1756,5,1759,1764,5,1770,1773,5,1809,1809,5,1958,1968,5,2045,2045,5,2075,2083,5,2089,2093,5,2192,2193,1,2250,2273,5,2275,2306,5,2362,2362,5,2364,2364,5,2369,2376,5,2381,2381,5,2385,2391,5,2433,2433,5,2492,2492,5,2495,2496,7,2503,2504,7,2509,2509,5,2530,2531,5,2561,2562,5,2620,2620,5,2625,2626,5,2635,2637,5,2672,2673,5,2689,2690,5,2748,2748,5,2753,2757,5,2761,2761,7,2765,2765,5,2810,2815,5,2818,2819,7,2878,2878,5,2880,2880,7,2887,2888,7,2893,2893,5,2903,2903,5,2946,2946,5,3007,3007,7,3009,3010,7,3018,3020,7,3031,3031,5,3073,3075,7,3132,3132,5,3137,3140,7,3146,3149,5,3170,3171,5,3202,3203,7,3262,3262,7,3264,3265,7,3267,3268,7,3271,3272,7,3276,3277,5,3298,3299,5,3330,3331,7,3390,3390,5,3393,3396,5,3402,3404,7,3406,3406,1,3426,3427,5,3458,3459,7,3535,3535,5,3538,3540,5,3544,3550,7,3570,3571,7,3635,3635,7,3655,3662,5,3763,3763,7,3784,3789,5,3893,3893,5,3897,3897,5,3953,3966,5,3968,3972,5,3981,3991,5,4038,4038,5,4145,4145,7,4153,4154,5,4157,4158,5,4184,4185,5,4209,4212,5,4228,4228,7,4237,4237,5,4352,4447,8,4520,4607,10,5906,5908,5,5938,5939,5,5970,5971,5,6068,6069,5,6071,6077,5,6086,6086,5,6089,6099,5,6155,6157,5,6159,6159,5,6313,6313,5,6435,6438,7,6441,6443,7,6450,6450,5,6457,6459,5,6681,6682,7,6741,6741,7,6743,6743,7,6752,6752,5,6757,6764,5,6771,6780,5,6832,6845,5,6847,6862,5,6916,6916,7,6965,6965,5,6971,6971,7,6973,6977,7,6979,6980,7,7040,7041,5,7073,7073,7,7078,7079,7,7082,7082,7,7142,7142,5,7144,7145,5,7149,7149,5,7151,7153,5,7204,7211,7,7220,7221,7,7376,7378,5,7393,7393,7,7405,7405,5,7415,7415,7,7616,7679,5,8204,8204,5,8206,8207,4,8233,8233,4,8252,8252,14,8288,8292,4,8294,8303,4,8413,8416,5,8418,8420,5,8482,8482,14,8596,8601,14,8986,8987,14,9096,9096,14,9193,9196,14,9199,9199,14,9201,9202,14,9208,9210,14,9642,9643,14,9664,9664,14,9728,9729,14,9732,9732,14,9735,9741,14,9743,9744,14,9746,9746,14,9750,9751,14,9753,9756,14,9758,9759,14,9761,9761,14,9764,9765,14,9767,9769,14,9771,9773,14,9775,9775,14,9784,9785,14,9787,9791,14,9793,9793,14,9795,9799,14,9812,9822,14,9824,9824,14,9827,9827,14,9829,9830,14,9832,9832,14,9851,9851,14,9854,9854,14,9856,9861,14,9874,9874,14,9876,9876,14,9878,9879,14,9881,9881,14,9883,9884,14,9888,9889,14,9895,9895,14,9898,9899,14,9904,9905,14,9917,9918,14,9924,9925,14,9928,9928,14,9934,9934,14,9936,9936,14,9938,9938,14,9940,9940,14,9961,9961,14,9963,9967,14,9970,9971,14,9973,9973,14,9975,9977,14,9979,9980,14,9982,9985,14,9987,9988,14,9992,9996,14,9998,9998,14,10000,10001,14,10004,10004,14,10013,10013,14,10024,10024,14,10052,10052,14,10060,10060,14,10067,10069,14,10083,10083,14,10085,10087,14,10145,10145,14,10175,10175,14,11013,11015,14,11088,11088,14,11503,11505,5,11744,11775,5,12334,12335,5,12349,12349,14,12951,12951,14,42607,42607,5,42612,42621,5,42736,42737,5,43014,43014,5,43043,43044,7,43047,43047,7,43136,43137,7,43204,43205,5,43263,43263,5,43335,43345,5,43360,43388,8,43395,43395,7,43444,43445,7,43450,43451,7,43454,43456,7,43561,43566,5,43569,43570,5,43573,43574,5,43596,43596,5,43644,43644,5,43698,43700,5,43710,43711,5,43755,43755,7,43758,43759,7,43766,43766,5,44005,44005,5,44008,44008,5,44012,44012,7,44032,44032,11,44060,44060,11,44088,44088,11,44116,44116,11,44144,44144,11,44172,44172,11,44200,44200,11,44228,44228,11,44256,44256,11,44284,44284,11,44312,44312,11,44340,44340,11,44368,44368,11,44396,44396,11,44424,44424,11,44452,44452,11,44480,44480,11,44508,44508,11,44536,44536,11,44564,44564,11,44592,44592,11,44620,44620,11,44648,44648,11,44676,44676,11,44704,44704,11,44732,44732,11,44760,44760,11,44788,44788,11,44816,44816,11,44844,44844,11,44872,44872,11,44900,44900,11,44928,44928,11,44956,44956,11,44984,44984,11,45012,45012,11,45040,45040,11,45068,45068,11,45096,45096,11,45124,45124,11,45152,45152,11,45180,45180,11,45208,45208,11,45236,45236,11,45264,45264,11,45292,45292,11,45320,45320,11,45348,45348,11,45376,45376,11,45404,45404,11,45432,45432,11,45460,45460,11,45488,45488,11,45516,45516,11,45544,45544,11,45572,45572,11,45600,45600,11,45628,45628,11,45656,45656,11,45684,45684,11,45712,45712,11,45740,45740,11,45768,45768,11,45796,45796,11,45824,45824,11,45852,45852,11,45880,45880,11,45908,45908,11,45936,45936,11,45964,45964,11,45992,45992,11,46020,46020,11,46048,46048,11,46076,46076,11,46104,46104,11,46132,46132,11,46160,46160,11,46188,46188,11,46216,46216,11,46244,46244,11,46272,46272,11,46300,46300,11,46328,46328,11,46356,46356,11,46384,46384,11,46412,46412,11,46440,46440,11,46468,46468,11,46496,46496,11,46524,46524,11,46552,46552,11,46580,46580,11,46608,46608,11,46636,46636,11,46664,46664,11,46692,46692,11,46720,46720,11,46748,46748,11,46776,46776,11,46804,46804,11,46832,46832,11,46860,46860,11,46888,46888,11,46916,46916,11,46944,46944,11,46972,46972,11,47000,47000,11,47028,47028,11,47056,47056,11,47084,47084,11,47112,47112,11,47140,47140,11,47168,47168,11,47196,47196,11,47224,47224,11,47252,47252,11,47280,47280,11,47308,47308,11,47336,47336,11,47364,47364,11,47392,47392,11,47420,47420,11,47448,47448,11,47476,47476,11,47504,47504,11,47532,47532,11,47560,47560,11,47588,47588,11,47616,47616,11,47644,47644,11,47672,47672,11,47700,47700,11,47728,47728,11,47756,47756,11,47784,47784,11,47812,47812,11,47840,47840,11,47868,47868,11,47896,47896,11,47924,47924,11,47952,47952,11,47980,47980,11,48008,48008,11,48036,48036,11,48064,48064,11,48092,48092,11,48120,48120,11,48148,48148,11,48176,48176,11,48204,48204,11,48232,48232,11,48260,48260,11,48288,48288,11,48316,48316,11,48344,48344,11,48372,48372,11,48400,48400,11,48428,48428,11,48456,48456,11,48484,48484,11,48512,48512,11,48540,48540,11,48568,48568,11,48596,48596,11,48624,48624,11,48652,48652,11,48680,48680,11,48708,48708,11,48736,48736,11,48764,48764,11,48792,48792,11,48820,48820,11,48848,48848,11,48876,48876,11,48904,48904,11,48932,48932,11,48960,48960,11,48988,48988,11,49016,49016,11,49044,49044,11,49072,49072,11,49100,49100,11,49128,49128,11,49156,49156,11,49184,49184,11,49212,49212,11,49240,49240,11,49268,49268,11,49296,49296,11,49324,49324,11,49352,49352,11,49380,49380,11,49408,49408,11,49436,49436,11,49464,49464,11,49492,49492,11,49520,49520,11,49548,49548,11,49576,49576,11,49604,49604,11,49632,49632,11,49660,49660,11,49688,49688,11,49716,49716,11,49744,49744,11,49772,49772,11,49800,49800,11,49828,49828,11,49856,49856,11,49884,49884,11,49912,49912,11,49940,49940,11,49968,49968,11,49996,49996,11,50024,50024,11,50052,50052,11,50080,50080,11,50108,50108,11,50136,50136,11,50164,50164,11,50192,50192,11,50220,50220,11,50248,50248,11,50276,50276,11,50304,50304,11,50332,50332,11,50360,50360,11,50388,50388,11,50416,50416,11,50444,50444,11,50472,50472,11,50500,50500,11,50528,50528,11,50556,50556,11,50584,50584,11,50612,50612,11,50640,50640,11,50668,50668,11,50696,50696,11,50724,50724,11,50752,50752,11,50780,50780,11,50808,50808,11,50836,50836,11,50864,50864,11,50892,50892,11,50920,50920,11,50948,50948,11,50976,50976,11,51004,51004,11,51032,51032,11,51060,51060,11,51088,51088,11,51116,51116,11,51144,51144,11,51172,51172,11,51200,51200,11,51228,51228,11,51256,51256,11,51284,51284,11,51312,51312,11,51340,51340,11,51368,51368,11,51396,51396,11,51424,51424,11,51452,51452,11,51480,51480,11,51508,51508,11,51536,51536,11,51564,51564,11,51592,51592,11,51620,51620,11,51648,51648,11,51676,51676,11,51704,51704,11,51732,51732,11,51760,51760,11,51788,51788,11,51816,51816,11,51844,51844,11,51872,51872,11,51900,51900,11,51928,51928,11,51956,51956,11,51984,51984,11,52012,52012,11,52040,52040,11,52068,52068,11,52096,52096,11,52124,52124,11,52152,52152,11,52180,52180,11,52208,52208,11,52236,52236,11,52264,52264,11,52292,52292,11,52320,52320,11,52348,52348,11,52376,52376,11,52404,52404,11,52432,52432,11,52460,52460,11,52488,52488,11,52516,52516,11,52544,52544,11,52572,52572,11,52600,52600,11,52628,52628,11,52656,52656,11,52684,52684,11,52712,52712,11,52740,52740,11,52768,52768,11,52796,52796,11,52824,52824,11,52852,52852,11,52880,52880,11,52908,52908,11,52936,52936,11,52964,52964,11,52992,52992,11,53020,53020,11,53048,53048,11,53076,53076,11,53104,53104,11,53132,53132,11,53160,53160,11,53188,53188,11,53216,53216,11,53244,53244,11,53272,53272,11,53300,53300,11,53328,53328,11,53356,53356,11,53384,53384,11,53412,53412,11,53440,53440,11,53468,53468,11,53496,53496,11,53524,53524,11,53552,53552,11,53580,53580,11,53608,53608,11,53636,53636,11,53664,53664,11,53692,53692,11,53720,53720,11,53748,53748,11,53776,53776,11,53804,53804,11,53832,53832,11,53860,53860,11,53888,53888,11,53916,53916,11,53944,53944,11,53972,53972,11,54000,54000,11,54028,54028,11,54056,54056,11,54084,54084,11,54112,54112,11,54140,54140,11,54168,54168,11,54196,54196,11,54224,54224,11,54252,54252,11,54280,54280,11,54308,54308,11,54336,54336,11,54364,54364,11,54392,54392,11,54420,54420,11,54448,54448,11,54476,54476,11,54504,54504,11,54532,54532,11,54560,54560,11,54588,54588,11,54616,54616,11,54644,54644,11,54672,54672,11,54700,54700,11,54728,54728,11,54756,54756,11,54784,54784,11,54812,54812,11,54840,54840,11,54868,54868,11,54896,54896,11,54924,54924,11,54952,54952,11,54980,54980,11,55008,55008,11,55036,55036,11,55064,55064,11,55092,55092,11,55120,55120,11,55148,55148,11,55176,55176,11,55216,55238,9,64286,64286,5,65056,65071,5,65438,65439,5,65529,65531,4,66272,66272,5,68097,68099,5,68108,68111,5,68159,68159,5,68900,68903,5,69446,69456,5,69632,69632,7,69634,69634,7,69744,69744,5,69759,69761,5,69808,69810,7,69815,69816,7,69821,69821,1,69837,69837,1,69927,69931,5,69933,69940,5,70003,70003,5,70018,70018,7,70070,70078,5,70082,70083,1,70094,70094,7,70188,70190,7,70194,70195,7,70197,70197,7,70206,70206,5,70368,70370,7,70400,70401,5,70459,70460,5,70463,70463,7,70465,70468,7,70475,70477,7,70498,70499,7,70512,70516,5,70712,70719,5,70722,70724,5,70726,70726,5,70832,70832,5,70835,70840,5,70842,70842,5,70845,70845,5,70847,70848,5,70850,70851,5,71088,71089,7,71096,71099,7,71102,71102,7,71132,71133,5,71219,71226,5,71229,71229,5,71231,71232,5,71340,71340,7,71342,71343,7,71350,71350,7,71453,71455,5,71462,71462,7,71724,71726,7,71736,71736,7,71984,71984,5,71991,71992,7,71997,71997,7,71999,71999,1,72001,72001,1,72003,72003,5,72148,72151,5,72156,72159,7,72164,72164,7,72243,72248,5,72250,72250,1,72263,72263,5,72279,72280,7,72324,72329,1,72343,72343,7,72751,72751,7,72760,72765,5,72767,72767,5,72873,72873,7,72881,72881,7,72884,72884,7,73009,73014,5,73020,73021,5,73030,73030,1,73098,73102,7,73107,73108,7,73110,73110,7,73459,73460,5,78896,78904,4,92976,92982,5,94033,94087,7,94180,94180,5,113821,113822,5,118528,118573,5,119141,119141,5,119143,119145,5,119150,119154,5,119163,119170,5,119210,119213,5,121344,121398,5,121461,121461,5,121499,121503,5,122880,122886,5,122907,122913,5,122918,122922,5,123566,123566,5,125136,125142,5,126976,126979,14,126981,127182,14,127184,127231,14,127279,127279,14,127344,127345,14,127374,127374,14,127405,127461,14,127489,127490,14,127514,127514,14,127538,127546,14,127561,127567,14,127570,127743,14,127757,127758,14,127760,127760,14,127762,127762,14,127766,127768,14,127770,127770,14,127772,127772,14,127775,127776,14,127778,127779,14,127789,127791,14,127794,127795,14,127798,127798,14,127819,127819,14,127824,127824,14,127868,127868,14,127870,127871,14,127892,127893,14,127896,127896,14,127900,127901,14,127904,127940,14,127942,127942,14,127944,127944,14,127946,127946,14,127951,127955,14,127968,127971,14,127973,127984,14,127987,127987,14,127989,127989,14,127991,127991,14,127995,127999,5,128008,128008,14,128012,128014,14,128017,128018,14,128020,128020,14,128022,128022,14,128042,128042,14,128063,128063,14,128065,128065,14,128101,128101,14,128108,128109,14,128173,128173,14,128182,128183,14,128236,128237,14,128239,128239,14,128245,128245,14,128248,128248,14,128253,128253,14,128255,128258,14,128260,128263,14,128265,128265,14,128277,128277,14,128300,128301,14,128326,128328,14,128331,128334,14,128336,128347,14,128360,128366,14,128369,128370,14,128378,128378,14,128391,128391,14,128394,128397,14,128400,128400,14,128405,128406,14,128420,128420,14,128422,128423,14,128425,128432,14,128435,128443,14,128445,128449,14,128453,128464,14,128468,128475,14,128479,128480,14,128482,128482,14,128484,128487,14,128489,128494,14,128496,128498,14,128500,128505,14,128507,128511,14,128513,128518,14,128521,128525,14,128527,128527,14,128529,128529,14,128533,128533,14,128535,128535,14,128537,128537,14]' + ); +} + +//#endregion + +/** + * Computes the offset after performing a left delete on the given string, + * while considering unicode grapheme/emoji rules. + */ +export function getLeftDeleteOffset(offset: number, str: string): number { + if (offset === 0) { + return 0; + } + + // Try to delete emoji part. + const emojiOffset = getOffsetBeforeLastEmojiComponent(offset, str); + if (emojiOffset !== undefined) { + return emojiOffset; + } + + // Otherwise, just skip a single code point. + const iterator = new CodePointIterator(str, offset); + iterator.prevCodePoint(); + return iterator.offset; +} + +function getOffsetBeforeLastEmojiComponent(initialOffset: number, str: string): number | undefined { + // See https://www.unicode.org/reports/tr51/tr51-14.html#EBNF_and_Regex for the + // structure of emojis. + const iterator = new CodePointIterator(str, initialOffset); + let codePoint = iterator.prevCodePoint(); + + // Skip modifiers + while ( + isEmojiModifier(codePoint) || + codePoint === CodePoint.emojiVariantSelector || + codePoint === CodePoint.enclosingKeyCap + ) { + if (iterator.offset === 0) { + // Cannot skip modifier, no preceding emoji base. + return undefined; + } + codePoint = iterator.prevCodePoint(); + } + + // Expect base emoji + if (!isEmojiImprecise(codePoint)) { + // Unexpected code point, not a valid emoji. + return undefined; + } + + let resultOffset = iterator.offset; + + if (resultOffset > 0) { + // Skip optional ZWJ code points that combine multiple emojis. + // In theory, we should check if that ZWJ actually combines multiple emojis + // to prevent deleting ZWJs in situations we didn't account for. + const optionalZwjCodePoint = iterator.prevCodePoint(); + if (optionalZwjCodePoint === CodePoint.zwj) { + resultOffset = iterator.offset; + } + } + + return resultOffset; +} + +function isEmojiModifier(codePoint: number): boolean { + return 0x1f3fb <= codePoint && codePoint <= 0x1f3ff; +} + +const enum CodePoint { + zwj = 0x200d, + + /** + * Variation Selector-16 (VS16) + */ + emojiVariantSelector = 0xfe0f, + + /** + * Combining Enclosing Keycap + */ + enclosingKeyCap = 0x20e3 +} + +export const noBreakWhitespace = '\xa0'; + +export class AmbiguousCharacters { + private static readonly ambiguousCharacterData = new Lazy< + Record ascii code point */ number[]> + >(() => { + // Generated using https://github.com/hediet/vscode-unicode-data + // Stored as key1, value1, key2, value2, ... + return JSON.parse( + '{"_common":[8232,32,8233,32,5760,32,8192,32,8193,32,8194,32,8195,32,8196,32,8197,32,8198,32,8200,32,8201,32,8202,32,8287,32,8199,32,8239,32,2042,95,65101,95,65102,95,65103,95,8208,45,8209,45,8210,45,65112,45,1748,45,8259,45,727,45,8722,45,10134,45,11450,45,1549,44,1643,44,8218,44,184,44,42233,44,894,59,2307,58,2691,58,1417,58,1795,58,1796,58,5868,58,65072,58,6147,58,6153,58,8282,58,1475,58,760,58,42889,58,8758,58,720,58,42237,58,451,33,11601,33,660,63,577,63,2429,63,5038,63,42731,63,119149,46,8228,46,1793,46,1794,46,42510,46,68176,46,1632,46,1776,46,42232,46,1373,96,65287,96,8219,96,8242,96,1370,96,1523,96,8175,96,65344,96,900,96,8189,96,8125,96,8127,96,8190,96,697,96,884,96,712,96,714,96,715,96,756,96,699,96,701,96,700,96,702,96,42892,96,1497,96,2036,96,2037,96,5194,96,5836,96,94033,96,94034,96,65339,91,10088,40,10098,40,12308,40,64830,40,65341,93,10089,41,10099,41,12309,41,64831,41,10100,123,119060,123,10101,125,65342,94,8270,42,1645,42,8727,42,66335,42,5941,47,8257,47,8725,47,8260,47,9585,47,10187,47,10744,47,119354,47,12755,47,12339,47,11462,47,20031,47,12035,47,65340,92,65128,92,8726,92,10189,92,10741,92,10745,92,119311,92,119355,92,12756,92,20022,92,12034,92,42872,38,708,94,710,94,5869,43,10133,43,66203,43,8249,60,10094,60,706,60,119350,60,5176,60,5810,60,5120,61,11840,61,12448,61,42239,61,8250,62,10095,62,707,62,119351,62,5171,62,94015,62,8275,126,732,126,8128,126,8764,126,65372,124,65293,45,120784,50,120794,50,120804,50,120814,50,120824,50,130034,50,42842,50,423,50,1000,50,42564,50,5311,50,42735,50,119302,51,120785,51,120795,51,120805,51,120815,51,120825,51,130035,51,42923,51,540,51,439,51,42858,51,11468,51,1248,51,94011,51,71882,51,120786,52,120796,52,120806,52,120816,52,120826,52,130036,52,5070,52,71855,52,120787,53,120797,53,120807,53,120817,53,120827,53,130037,53,444,53,71867,53,120788,54,120798,54,120808,54,120818,54,120828,54,130038,54,11474,54,5102,54,71893,54,119314,55,120789,55,120799,55,120809,55,120819,55,120829,55,130039,55,66770,55,71878,55,2819,56,2538,56,2666,56,125131,56,120790,56,120800,56,120810,56,120820,56,120830,56,130040,56,547,56,546,56,66330,56,2663,57,2920,57,2541,57,3437,57,120791,57,120801,57,120811,57,120821,57,120831,57,130041,57,42862,57,11466,57,71884,57,71852,57,71894,57,9082,97,65345,97,119834,97,119886,97,119938,97,119990,97,120042,97,120094,97,120146,97,120198,97,120250,97,120302,97,120354,97,120406,97,120458,97,593,97,945,97,120514,97,120572,97,120630,97,120688,97,120746,97,65313,65,119808,65,119860,65,119912,65,119964,65,120016,65,120068,65,120120,65,120172,65,120224,65,120276,65,120328,65,120380,65,120432,65,913,65,120488,65,120546,65,120604,65,120662,65,120720,65,5034,65,5573,65,42222,65,94016,65,66208,65,119835,98,119887,98,119939,98,119991,98,120043,98,120095,98,120147,98,120199,98,120251,98,120303,98,120355,98,120407,98,120459,98,388,98,5071,98,5234,98,5551,98,65314,66,8492,66,119809,66,119861,66,119913,66,120017,66,120069,66,120121,66,120173,66,120225,66,120277,66,120329,66,120381,66,120433,66,42932,66,914,66,120489,66,120547,66,120605,66,120663,66,120721,66,5108,66,5623,66,42192,66,66178,66,66209,66,66305,66,65347,99,8573,99,119836,99,119888,99,119940,99,119992,99,120044,99,120096,99,120148,99,120200,99,120252,99,120304,99,120356,99,120408,99,120460,99,7428,99,1010,99,11429,99,43951,99,66621,99,128844,67,71922,67,71913,67,65315,67,8557,67,8450,67,8493,67,119810,67,119862,67,119914,67,119966,67,120018,67,120174,67,120226,67,120278,67,120330,67,120382,67,120434,67,1017,67,11428,67,5087,67,42202,67,66210,67,66306,67,66581,67,66844,67,8574,100,8518,100,119837,100,119889,100,119941,100,119993,100,120045,100,120097,100,120149,100,120201,100,120253,100,120305,100,120357,100,120409,100,120461,100,1281,100,5095,100,5231,100,42194,100,8558,68,8517,68,119811,68,119863,68,119915,68,119967,68,120019,68,120071,68,120123,68,120175,68,120227,68,120279,68,120331,68,120383,68,120435,68,5024,68,5598,68,5610,68,42195,68,8494,101,65349,101,8495,101,8519,101,119838,101,119890,101,119942,101,120046,101,120098,101,120150,101,120202,101,120254,101,120306,101,120358,101,120410,101,120462,101,43826,101,1213,101,8959,69,65317,69,8496,69,119812,69,119864,69,119916,69,120020,69,120072,69,120124,69,120176,69,120228,69,120280,69,120332,69,120384,69,120436,69,917,69,120492,69,120550,69,120608,69,120666,69,120724,69,11577,69,5036,69,42224,69,71846,69,71854,69,66182,69,119839,102,119891,102,119943,102,119995,102,120047,102,120099,102,120151,102,120203,102,120255,102,120307,102,120359,102,120411,102,120463,102,43829,102,42905,102,383,102,7837,102,1412,102,119315,70,8497,70,119813,70,119865,70,119917,70,120021,70,120073,70,120125,70,120177,70,120229,70,120281,70,120333,70,120385,70,120437,70,42904,70,988,70,120778,70,5556,70,42205,70,71874,70,71842,70,66183,70,66213,70,66853,70,65351,103,8458,103,119840,103,119892,103,119944,103,120048,103,120100,103,120152,103,120204,103,120256,103,120308,103,120360,103,120412,103,120464,103,609,103,7555,103,397,103,1409,103,119814,71,119866,71,119918,71,119970,71,120022,71,120074,71,120126,71,120178,71,120230,71,120282,71,120334,71,120386,71,120438,71,1292,71,5056,71,5107,71,42198,71,65352,104,8462,104,119841,104,119945,104,119997,104,120049,104,120101,104,120153,104,120205,104,120257,104,120309,104,120361,104,120413,104,120465,104,1211,104,1392,104,5058,104,65320,72,8459,72,8460,72,8461,72,119815,72,119867,72,119919,72,120023,72,120179,72,120231,72,120283,72,120335,72,120387,72,120439,72,919,72,120494,72,120552,72,120610,72,120668,72,120726,72,11406,72,5051,72,5500,72,42215,72,66255,72,731,105,9075,105,65353,105,8560,105,8505,105,8520,105,119842,105,119894,105,119946,105,119998,105,120050,105,120102,105,120154,105,120206,105,120258,105,120310,105,120362,105,120414,105,120466,105,120484,105,618,105,617,105,953,105,8126,105,890,105,120522,105,120580,105,120638,105,120696,105,120754,105,1110,105,42567,105,1231,105,43893,105,5029,105,71875,105,65354,106,8521,106,119843,106,119895,106,119947,106,119999,106,120051,106,120103,106,120155,106,120207,106,120259,106,120311,106,120363,106,120415,106,120467,106,1011,106,1112,106,65322,74,119817,74,119869,74,119921,74,119973,74,120025,74,120077,74,120129,74,120181,74,120233,74,120285,74,120337,74,120389,74,120441,74,42930,74,895,74,1032,74,5035,74,5261,74,42201,74,119844,107,119896,107,119948,107,120000,107,120052,107,120104,107,120156,107,120208,107,120260,107,120312,107,120364,107,120416,107,120468,107,8490,75,65323,75,119818,75,119870,75,119922,75,119974,75,120026,75,120078,75,120130,75,120182,75,120234,75,120286,75,120338,75,120390,75,120442,75,922,75,120497,75,120555,75,120613,75,120671,75,120729,75,11412,75,5094,75,5845,75,42199,75,66840,75,1472,108,8739,73,9213,73,65512,73,1633,108,1777,73,66336,108,125127,108,120783,73,120793,73,120803,73,120813,73,120823,73,130033,73,65321,73,8544,73,8464,73,8465,73,119816,73,119868,73,119920,73,120024,73,120128,73,120180,73,120232,73,120284,73,120336,73,120388,73,120440,73,65356,108,8572,73,8467,108,119845,108,119897,108,119949,108,120001,108,120053,108,120105,73,120157,73,120209,73,120261,73,120313,73,120365,73,120417,73,120469,73,448,73,120496,73,120554,73,120612,73,120670,73,120728,73,11410,73,1030,73,1216,73,1493,108,1503,108,1575,108,126464,108,126592,108,65166,108,65165,108,1994,108,11599,73,5825,73,42226,73,93992,73,66186,124,66313,124,119338,76,8556,76,8466,76,119819,76,119871,76,119923,76,120027,76,120079,76,120131,76,120183,76,120235,76,120287,76,120339,76,120391,76,120443,76,11472,76,5086,76,5290,76,42209,76,93974,76,71843,76,71858,76,66587,76,66854,76,65325,77,8559,77,8499,77,119820,77,119872,77,119924,77,120028,77,120080,77,120132,77,120184,77,120236,77,120288,77,120340,77,120392,77,120444,77,924,77,120499,77,120557,77,120615,77,120673,77,120731,77,1018,77,11416,77,5047,77,5616,77,5846,77,42207,77,66224,77,66321,77,119847,110,119899,110,119951,110,120003,110,120055,110,120107,110,120159,110,120211,110,120263,110,120315,110,120367,110,120419,110,120471,110,1400,110,1404,110,65326,78,8469,78,119821,78,119873,78,119925,78,119977,78,120029,78,120081,78,120185,78,120237,78,120289,78,120341,78,120393,78,120445,78,925,78,120500,78,120558,78,120616,78,120674,78,120732,78,11418,78,42208,78,66835,78,3074,111,3202,111,3330,111,3458,111,2406,111,2662,111,2790,111,3046,111,3174,111,3302,111,3430,111,3664,111,3792,111,4160,111,1637,111,1781,111,65359,111,8500,111,119848,111,119900,111,119952,111,120056,111,120108,111,120160,111,120212,111,120264,111,120316,111,120368,111,120420,111,120472,111,7439,111,7441,111,43837,111,959,111,120528,111,120586,111,120644,111,120702,111,120760,111,963,111,120532,111,120590,111,120648,111,120706,111,120764,111,11423,111,4351,111,1413,111,1505,111,1607,111,126500,111,126564,111,126596,111,65259,111,65260,111,65258,111,65257,111,1726,111,64428,111,64429,111,64427,111,64426,111,1729,111,64424,111,64425,111,64423,111,64422,111,1749,111,3360,111,4125,111,66794,111,71880,111,71895,111,66604,111,1984,79,2534,79,2918,79,12295,79,70864,79,71904,79,120782,79,120792,79,120802,79,120812,79,120822,79,130032,79,65327,79,119822,79,119874,79,119926,79,119978,79,120030,79,120082,79,120134,79,120186,79,120238,79,120290,79,120342,79,120394,79,120446,79,927,79,120502,79,120560,79,120618,79,120676,79,120734,79,11422,79,1365,79,11604,79,4816,79,2848,79,66754,79,42227,79,71861,79,66194,79,66219,79,66564,79,66838,79,9076,112,65360,112,119849,112,119901,112,119953,112,120005,112,120057,112,120109,112,120161,112,120213,112,120265,112,120317,112,120369,112,120421,112,120473,112,961,112,120530,112,120544,112,120588,112,120602,112,120646,112,120660,112,120704,112,120718,112,120762,112,120776,112,11427,112,65328,80,8473,80,119823,80,119875,80,119927,80,119979,80,120031,80,120083,80,120187,80,120239,80,120291,80,120343,80,120395,80,120447,80,929,80,120504,80,120562,80,120620,80,120678,80,120736,80,11426,80,5090,80,5229,80,42193,80,66197,80,119850,113,119902,113,119954,113,120006,113,120058,113,120110,113,120162,113,120214,113,120266,113,120318,113,120370,113,120422,113,120474,113,1307,113,1379,113,1382,113,8474,81,119824,81,119876,81,119928,81,119980,81,120032,81,120084,81,120188,81,120240,81,120292,81,120344,81,120396,81,120448,81,11605,81,119851,114,119903,114,119955,114,120007,114,120059,114,120111,114,120163,114,120215,114,120267,114,120319,114,120371,114,120423,114,120475,114,43847,114,43848,114,7462,114,11397,114,43905,114,119318,82,8475,82,8476,82,8477,82,119825,82,119877,82,119929,82,120033,82,120189,82,120241,82,120293,82,120345,82,120397,82,120449,82,422,82,5025,82,5074,82,66740,82,5511,82,42211,82,94005,82,65363,115,119852,115,119904,115,119956,115,120008,115,120060,115,120112,115,120164,115,120216,115,120268,115,120320,115,120372,115,120424,115,120476,115,42801,115,445,115,1109,115,43946,115,71873,115,66632,115,65331,83,119826,83,119878,83,119930,83,119982,83,120034,83,120086,83,120138,83,120190,83,120242,83,120294,83,120346,83,120398,83,120450,83,1029,83,1359,83,5077,83,5082,83,42210,83,94010,83,66198,83,66592,83,119853,116,119905,116,119957,116,120009,116,120061,116,120113,116,120165,116,120217,116,120269,116,120321,116,120373,116,120425,116,120477,116,8868,84,10201,84,128872,84,65332,84,119827,84,119879,84,119931,84,119983,84,120035,84,120087,84,120139,84,120191,84,120243,84,120295,84,120347,84,120399,84,120451,84,932,84,120507,84,120565,84,120623,84,120681,84,120739,84,11430,84,5026,84,42196,84,93962,84,71868,84,66199,84,66225,84,66325,84,119854,117,119906,117,119958,117,120010,117,120062,117,120114,117,120166,117,120218,117,120270,117,120322,117,120374,117,120426,117,120478,117,42911,117,7452,117,43854,117,43858,117,651,117,965,117,120534,117,120592,117,120650,117,120708,117,120766,117,1405,117,66806,117,71896,117,8746,85,8899,85,119828,85,119880,85,119932,85,119984,85,120036,85,120088,85,120140,85,120192,85,120244,85,120296,85,120348,85,120400,85,120452,85,1357,85,4608,85,66766,85,5196,85,42228,85,94018,85,71864,85,8744,118,8897,118,65366,118,8564,118,119855,118,119907,118,119959,118,120011,118,120063,118,120115,118,120167,118,120219,118,120271,118,120323,118,120375,118,120427,118,120479,118,7456,118,957,118,120526,118,120584,118,120642,118,120700,118,120758,118,1141,118,1496,118,71430,118,43945,118,71872,118,119309,86,1639,86,1783,86,8548,86,119829,86,119881,86,119933,86,119985,86,120037,86,120089,86,120141,86,120193,86,120245,86,120297,86,120349,86,120401,86,120453,86,1140,86,11576,86,5081,86,5167,86,42719,86,42214,86,93960,86,71840,86,66845,86,623,119,119856,119,119908,119,119960,119,120012,119,120064,119,120116,119,120168,119,120220,119,120272,119,120324,119,120376,119,120428,119,120480,119,7457,119,1121,119,1309,119,1377,119,71434,119,71438,119,71439,119,43907,119,71919,87,71910,87,119830,87,119882,87,119934,87,119986,87,120038,87,120090,87,120142,87,120194,87,120246,87,120298,87,120350,87,120402,87,120454,87,1308,87,5043,87,5076,87,42218,87,5742,120,10539,120,10540,120,10799,120,65368,120,8569,120,119857,120,119909,120,119961,120,120013,120,120065,120,120117,120,120169,120,120221,120,120273,120,120325,120,120377,120,120429,120,120481,120,5441,120,5501,120,5741,88,9587,88,66338,88,71916,88,65336,88,8553,88,119831,88,119883,88,119935,88,119987,88,120039,88,120091,88,120143,88,120195,88,120247,88,120299,88,120351,88,120403,88,120455,88,42931,88,935,88,120510,88,120568,88,120626,88,120684,88,120742,88,11436,88,11613,88,5815,88,42219,88,66192,88,66228,88,66327,88,66855,88,611,121,7564,121,65369,121,119858,121,119910,121,119962,121,120014,121,120066,121,120118,121,120170,121,120222,121,120274,121,120326,121,120378,121,120430,121,120482,121,655,121,7935,121,43866,121,947,121,8509,121,120516,121,120574,121,120632,121,120690,121,120748,121,1199,121,4327,121,71900,121,65337,89,119832,89,119884,89,119936,89,119988,89,120040,89,120092,89,120144,89,120196,89,120248,89,120300,89,120352,89,120404,89,120456,89,933,89,978,89,120508,89,120566,89,120624,89,120682,89,120740,89,11432,89,1198,89,5033,89,5053,89,42220,89,94019,89,71844,89,66226,89,119859,122,119911,122,119963,122,120015,122,120067,122,120119,122,120171,122,120223,122,120275,122,120327,122,120379,122,120431,122,120483,122,7458,122,43923,122,71876,122,66293,90,71909,90,65338,90,8484,90,8488,90,119833,90,119885,90,119937,90,119989,90,120041,90,120197,90,120249,90,120301,90,120353,90,120405,90,120457,90,918,90,120493,90,120551,90,120609,90,120667,90,120725,90,5059,90,42204,90,71849,90,65282,34,65284,36,65285,37,65286,38,65290,42,65291,43,65294,46,65295,47,65296,48,65297,49,65298,50,65299,51,65300,52,65301,53,65302,54,65303,55,65304,56,65305,57,65308,60,65309,61,65310,62,65312,64,65316,68,65318,70,65319,71,65324,76,65329,81,65330,82,65333,85,65334,86,65335,87,65343,95,65346,98,65348,100,65350,102,65355,107,65357,109,65358,110,65361,113,65362,114,65364,116,65365,117,65367,119,65370,122,65371,123,65373,125],"_default":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],"cs":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],"de":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],"es":[8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],"fr":[65374,126,65306,58,65281,33,8216,96,8245,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],"it":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],"ja":[8211,45,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65292,44,65307,59],"ko":[8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],"pl":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],"pt-BR":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],"qps-ploc":[160,32,8211,45,65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],"ru":[65374,126,65306,58,65281,33,8216,96,8217,96,8245,96,180,96,12494,47,305,105,921,73,1009,112,215,120,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],"tr":[160,32,8211,45,65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65288,40,65289,41,65292,44,65307,59,65311,63],"zh-hans":[65374,126,65306,58,65281,33,8245,96,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65288,40,65289,41],"zh-hant":[8211,45,65374,126,180,96,12494,47,1047,51,1073,54,1072,97,1040,65,1068,98,1042,66,1089,99,1057,67,1077,101,1045,69,1053,72,305,105,1050,75,921,73,1052,77,1086,111,1054,79,1009,112,1088,112,1056,80,1075,114,1058,84,215,120,1093,120,1061,88,1091,121,1059,89,65283,35,65307,59]}' + ); + }); + + private static readonly cache = new LRUCachedComputed((locales) => { + function arrayToMap(arr: number[]): Map { + const result = new Map(); + for (let i = 0; i < arr.length; i += 2) { + result.set(arr[i], arr[i + 1]); + } + return result; + } + + function mergeMaps(map1: Map, map2: Map): Map { + const result = new Map(map1); + for (const [key, value] of map2) { + result.set(key, value); + } + return result; + } + + function intersectMaps(map1: Map | undefined, map2: Map) { + if (!map1) { + return map2; + } + const result = new Map(); + for (const [key, value] of map1) { + if (map2.has(key)) { + result.set(key, value); + } + } + return result; + } + + const data = this.ambiguousCharacterData.getValue(); + + let filteredLocales = locales.filter((l) => !l.startsWith('_') && l in data); + if (filteredLocales.length === 0) { + filteredLocales = ['_default']; + } + + let languageSpecificMap: Map | undefined = undefined; + for (const locale of filteredLocales) { + const map = arrayToMap(data[locale]); + languageSpecificMap = intersectMaps(languageSpecificMap, map); + } + + const commonMap = arrayToMap(data['_common']); + const map = mergeMaps(commonMap, languageSpecificMap!); + + return new AmbiguousCharacters(map); + }); + + public static getInstance(locales: Set): AmbiguousCharacters { + return AmbiguousCharacters.cache.get(Array.from(locales)); + } + + private static _locales = new Lazy(() => + Object.keys(AmbiguousCharacters.ambiguousCharacterData.getValue()).filter((k) => !k.startsWith('_')) + ); + public static getLocales(): string[] { + return AmbiguousCharacters._locales.getValue(); + } + + private constructor(private readonly confusableDictionary: Map) {} + + public isAmbiguous(codePoint: number): boolean { + return this.confusableDictionary.has(codePoint); + } + + /** + * Returns the non basic ASCII code point that the given code point can be confused, + * or undefined if such code point does note exist. + */ + public getPrimaryConfusable(codePoint: number): number | undefined { + return this.confusableDictionary.get(codePoint); + } + + public getConfusableCodePoints(): ReadonlySet { + return new Set(this.confusableDictionary.keys()); + } +} + +export class InvisibleCharacters { + private static getRawData(): number[] { + // Generated using https://github.com/hediet/vscode-unicode-data + return JSON.parse( + '[9,10,11,12,13,32,127,160,173,847,1564,4447,4448,6068,6069,6155,6156,6157,6158,7355,7356,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8203,8204,8205,8206,8207,8234,8235,8236,8237,8238,8239,8287,8288,8289,8290,8291,8292,8293,8294,8295,8296,8297,8298,8299,8300,8301,8302,8303,10240,12288,12644,65024,65025,65026,65027,65028,65029,65030,65031,65032,65033,65034,65035,65036,65037,65038,65039,65279,65440,65520,65521,65522,65523,65524,65525,65526,65527,65528,65532,78844,119155,119156,119157,119158,119159,119160,119161,119162,917504,917505,917506,917507,917508,917509,917510,917511,917512,917513,917514,917515,917516,917517,917518,917519,917520,917521,917522,917523,917524,917525,917526,917527,917528,917529,917530,917531,917532,917533,917534,917535,917536,917537,917538,917539,917540,917541,917542,917543,917544,917545,917546,917547,917548,917549,917550,917551,917552,917553,917554,917555,917556,917557,917558,917559,917560,917561,917562,917563,917564,917565,917566,917567,917568,917569,917570,917571,917572,917573,917574,917575,917576,917577,917578,917579,917580,917581,917582,917583,917584,917585,917586,917587,917588,917589,917590,917591,917592,917593,917594,917595,917596,917597,917598,917599,917600,917601,917602,917603,917604,917605,917606,917607,917608,917609,917610,917611,917612,917613,917614,917615,917616,917617,917618,917619,917620,917621,917622,917623,917624,917625,917626,917627,917628,917629,917630,917631,917760,917761,917762,917763,917764,917765,917766,917767,917768,917769,917770,917771,917772,917773,917774,917775,917776,917777,917778,917779,917780,917781,917782,917783,917784,917785,917786,917787,917788,917789,917790,917791,917792,917793,917794,917795,917796,917797,917798,917799,917800,917801,917802,917803,917804,917805,917806,917807,917808,917809,917810,917811,917812,917813,917814,917815,917816,917817,917818,917819,917820,917821,917822,917823,917824,917825,917826,917827,917828,917829,917830,917831,917832,917833,917834,917835,917836,917837,917838,917839,917840,917841,917842,917843,917844,917845,917846,917847,917848,917849,917850,917851,917852,917853,917854,917855,917856,917857,917858,917859,917860,917861,917862,917863,917864,917865,917866,917867,917868,917869,917870,917871,917872,917873,917874,917875,917876,917877,917878,917879,917880,917881,917882,917883,917884,917885,917886,917887,917888,917889,917890,917891,917892,917893,917894,917895,917896,917897,917898,917899,917900,917901,917902,917903,917904,917905,917906,917907,917908,917909,917910,917911,917912,917913,917914,917915,917916,917917,917918,917919,917920,917921,917922,917923,917924,917925,917926,917927,917928,917929,917930,917931,917932,917933,917934,917935,917936,917937,917938,917939,917940,917941,917942,917943,917944,917945,917946,917947,917948,917949,917950,917951,917952,917953,917954,917955,917956,917957,917958,917959,917960,917961,917962,917963,917964,917965,917966,917967,917968,917969,917970,917971,917972,917973,917974,917975,917976,917977,917978,917979,917980,917981,917982,917983,917984,917985,917986,917987,917988,917989,917990,917991,917992,917993,917994,917995,917996,917997,917998,917999]' + ); + } + + private static _data: Set | undefined = undefined; + + private static getData() { + if (!this._data) { + this._data = new Set(InvisibleCharacters.getRawData()); + } + return this._data; + } + + public static isInvisibleCharacter(codePoint: number): boolean { + return InvisibleCharacters.getData().has(codePoint); + } + + public static get codePoints(): ReadonlySet { + return InvisibleCharacters.getData(); + } +} diff --git a/src/platform/vscode-path/types.ts b/src/platform/vscode-path/types.ts new file mode 100644 index 00000000000..746bb77fe5f --- /dev/null +++ b/src/platform/vscode-path/types.ts @@ -0,0 +1,296 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * @returns whether the provided parameter is a JavaScript Array or not. + */ +export function isArray(array: any): array is any[] { + return Array.isArray(array); +} + +/** + * @returns whether the provided parameter is a JavaScript String or not. + */ +export function isString(str: unknown): str is string { + return typeof str === 'string'; +} + +/** + * @returns whether the provided parameter is a JavaScript Array and each element in the array is a string. + */ +export function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && (value).every((elem) => isString(elem)); +} + +/** + * + * @returns whether the provided parameter is of type `object` but **not** + * `null`, an `array`, a `regexp`, nor a `date`. + */ +export function isObject(obj: unknown): obj is Object { + // The method can't do a type cast since there are type (like strings) which + // are subclasses of any put not positvely matched by the function. Hence type + // narrowing results in wrong results. + return ( + typeof obj === 'object' && + obj !== null && + !Array.isArray(obj) && + !(obj instanceof RegExp) && + !(obj instanceof Date) + ); +} + +/** + * + * @returns whether the provided parameter is of type `Buffer` or Uint8Array dervived type + */ +export function isTypedArray(obj: unknown): obj is Object { + return ( + typeof obj === 'object' && + (obj instanceof Uint8Array || + obj instanceof Uint16Array || + obj instanceof Uint32Array || + obj instanceof Float32Array || + obj instanceof Float64Array || + obj instanceof Int8Array || + obj instanceof Int16Array || + obj instanceof Int32Array || + obj instanceof BigInt64Array || + obj instanceof BigUint64Array || + obj instanceof Uint8ClampedArray) + ); +} + +/** + * In **contrast** to just checking `typeof` this will return `false` for `NaN`. + * @returns whether the provided parameter is a JavaScript Number or not. + */ +export function isNumber(obj: unknown): obj is number { + return typeof obj === 'number' && !isNaN(obj); +} + +/** + * @returns whether the provided parameter is an Iterable, casting to the given generic + */ +export function isIterable(obj: unknown): obj is Iterable { + return !!obj && typeof (obj as any)[Symbol.iterator] === 'function'; +} + +/** + * @returns whether the provided parameter is a JavaScript Boolean or not. + */ +export function isBoolean(obj: unknown): obj is boolean { + return obj === true || obj === false; +} + +/** + * @returns whether the provided parameter is undefined. + */ +export function isUndefined(obj: unknown): obj is undefined { + return typeof obj === 'undefined'; +} + +/** + * @returns whether the provided parameter is defined. + */ +export function isDefined(arg: T | null | undefined): arg is T { + return !isUndefinedOrNull(arg); +} + +/** + * @returns whether the provided parameter is undefined or null. + */ +export function isUndefinedOrNull(obj: unknown): obj is undefined | null { + return isUndefined(obj) || obj === null; +} + +export function assertType(condition: unknown, type?: string): asserts condition { + if (!condition) { + throw new Error(type ? `Unexpected type, expected '${type}'` : 'Unexpected type'); + } +} + +/** + * Asserts that the argument passed in is neither undefined nor null. + */ +export function assertIsDefined(arg: T | null | undefined): T { + if (isUndefinedOrNull(arg)) { + throw new Error('Assertion Failed: argument is undefined or null'); + } + + return arg; +} + +/** + * Asserts that each argument passed in is neither undefined nor null. + */ +export function assertAllDefined(t1: T1 | null | undefined, t2: T2 | null | undefined): [T1, T2]; +export function assertAllDefined( + t1: T1 | null | undefined, + t2: T2 | null | undefined, + t3: T3 | null | undefined +): [T1, T2, T3]; +export function assertAllDefined( + t1: T1 | null | undefined, + t2: T2 | null | undefined, + t3: T3 | null | undefined, + t4: T4 | null | undefined +): [T1, T2, T3, T4]; +export function assertAllDefined(...args: (unknown | null | undefined)[]): unknown[] { + const result = []; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (isUndefinedOrNull(arg)) { + throw new Error(`Assertion Failed: argument at index ${i} is undefined or null`); + } + + result.push(arg); + } + + return result; +} + +const hasOwnProperty = Object.prototype.hasOwnProperty; + +/** + * @returns whether the provided parameter is an empty JavaScript Object or not. + */ +export function isEmptyObject(obj: unknown): obj is object { + if (!isObject(obj)) { + return false; + } + + // eslint-disable-next-line no-restricted-syntax + for (let key in obj) { + if (hasOwnProperty.call(obj, key)) { + return false; + } + } + + return true; +} + +/** + * @returns whether the provided parameter is a JavaScript Function or not. + */ +export function isFunction(obj: unknown): obj is Function { + return typeof obj === 'function'; +} + +/** + * @returns whether the provided parameters is are JavaScript Function or not. + */ +export function areFunctions(...objects: unknown[]): boolean { + return objects.length > 0 && objects.every(isFunction); +} + +export type TypeConstraint = string | Function; + +export function validateConstraints(args: unknown[], constraints: Array): void { + const len = Math.min(args.length, constraints.length); + for (let i = 0; i < len; i++) { + validateConstraint(args[i], constraints[i]); + } +} + +export function validateConstraint(arg: unknown, constraint: TypeConstraint | undefined): void { + if (isString(constraint)) { + if (typeof arg !== constraint) { + throw new Error(`argument does not match constraint: typeof ${constraint}`); + } + } else if (isFunction(constraint)) { + try { + if (arg instanceof constraint) { + return; + } + } catch { + // ignore + } + if (!isUndefinedOrNull(arg) && (arg as any).constructor === constraint) { + return; + } + if (constraint.length === 1 && constraint.call(undefined, arg) === true) { + return; + } + throw new Error( + `argument does not match one of these constraints: arg instanceof constraint, arg.constructor === constraint, nor constraint(arg) === true` + ); + } +} + +export function getAllPropertyNames(obj: object): string[] { + let res: string[] = []; + let proto = Object.getPrototypeOf(obj); + while (Object.prototype !== proto) { + res = res.concat(Object.getOwnPropertyNames(proto)); + proto = Object.getPrototypeOf(proto); + } + return res; +} + +export function getAllMethodNames(obj: object): string[] { + const methods: string[] = []; + for (const prop of getAllPropertyNames(obj)) { + if (typeof (obj as any)[prop] === 'function') { + methods.push(prop); + } + } + return methods; +} + +export function createProxyObject( + methodNames: string[], + invoke: (method: string, args: unknown[]) => unknown +): T { + const createProxyMethod = (method: string): (() => unknown) => { + return function () { + const args = Array.prototype.slice.call(arguments, 0); + return invoke(method, args); + }; + }; + + let result = {} as T; + for (const methodName of methodNames) { + (result)[methodName] = createProxyMethod(methodName); + } + return result; +} + +/** + * Converts null to undefined, passes all other values through. + */ +export function withNullAsUndefined(x: T | null): T | undefined { + return x === null ? undefined : x; +} + +/** + * Converts undefined to null, passes all other values through. + */ +export function withUndefinedAsNull(x: T | undefined): T | null { + return typeof x === 'undefined' ? null : x; +} + +type AddFirstParameterToFunction = T extends ( + ...args: any[] +) => TargetFunctionsReturnType + ? // Function: add param to function + (firstArg: FirstParameter, ...args: Parameters) => ReturnType + : // Else: just leave as is + T; + +/** + * Allows to add a first parameter to functions of a type. + */ +export type AddFirstParameterToFunctions = { + // For every property + [K in keyof Target]: AddFirstParameterToFunction; +}; + +export function assertNever(_value: never, message = 'Unreachable'): never { + throw new Error(message); +} diff --git a/src/platform/vscode-path/uint.ts b/src/platform/vscode-path/uint.ts new file mode 100644 index 00000000000..45a9c4c475b --- /dev/null +++ b/src/platform/vscode-path/uint.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const enum Constants { + /** + * MAX SMI (SMall Integer) as defined in v8. + * one bit is lost for boxing/unboxing flag. + * one bit is lost for sign flag. + * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values + */ + MAX_SAFE_SMALL_INTEGER = 1 << 30, + + /** + * MIN SMI (SMall Integer) as defined in v8. + * one bit is lost for boxing/unboxing flag. + * one bit is lost for sign flag. + * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values + */ + MIN_SAFE_SMALL_INTEGER = -(1 << 30), + + /** + * Max unsigned integer that fits on 8 bits. + */ + MAX_UINT_8 = 255, // 2^8 - 1 + + /** + * Max unsigned integer that fits on 16 bits. + */ + MAX_UINT_16 = 65535, // 2^16 - 1 + + /** + * Max unsigned integer that fits on 32 bits. + */ + MAX_UINT_32 = 4294967295, // 2^32 - 1 + + UNICODE_SUPPLEMENTARY_PLANE_BEGIN = 0x010000 +} + +export function toUint8(v: number): number { + if (v < 0) { + return 0; + } + if (v > Constants.MAX_UINT_8) { + return Constants.MAX_UINT_8; + } + return v | 0; +} + +export function toUint32(v: number): number { + if (v < 0) { + return 0; + } + if (v > Constants.MAX_UINT_32) { + return Constants.MAX_UINT_32; + } + return v | 0; +} diff --git a/src/platform/vscode-path/utils.ts b/src/platform/vscode-path/utils.ts new file mode 100644 index 00000000000..96744f7f7ee --- /dev/null +++ b/src/platform/vscode-path/utils.ts @@ -0,0 +1,103 @@ +// Miscellaneous functions from other spots in VS code + +import { Uri } from 'vscode'; + +export function fsPathToUri(path: string | undefined) { + return path ? Uri.file(path) : undefined; +} + +export namespace Schemas { + /** + * A schema that is used for models that exist in memory + * only and that have no correspondence on a server or such. + */ + export const inMemory = 'inmemory'; + + /** + * A schema that is used for setting files + */ + export const vscode = 'vscode'; + + /** + * A schema that is used for internal private files + */ + export const internal = 'private'; + + /** + * A walk-through document. + */ + export const walkThrough = 'walkThrough'; + + /** + * An embedded code snippet. + */ + export const walkThroughSnippet = 'walkThroughSnippet'; + + export const http = 'http'; + + export const https = 'https'; + + export const file = 'file'; + + export const mailto = 'mailto'; + + export const untitled = 'untitled'; + + export const data = 'data'; + + export const command = 'command'; + + export const vscodeRemote = 'vscode-remote'; + + export const vscodeRemoteResource = 'vscode-remote-resource'; + + export const vscodeUserData = 'vscode-userdata'; + + export const vscodeCustomEditor = 'vscode-custom-editor'; + + export const vscodeNotebook = 'vscode-notebook'; + + export const vscodeNotebookCell = 'vscode-notebook-cell'; + + export const vscodeNotebookCellMetadata = 'vscode-notebook-cell-metadata'; + export const vscodeNotebookCellOutput = 'vscode-notebook-cell-output'; + export const vscodeInteractive = 'vscode-interactive'; + export const vscodeInteractiveInput = 'vscode-interactive-input'; + + export const vscodeSettings = 'vscode-settings'; + + export const vscodeWorkspaceTrust = 'vscode-workspace-trust'; + + export const vscodeTerminal = 'vscode-terminal'; + + /** + * Scheme used internally for webviews that aren't linked to a resource (i.e. not custom editors) + */ + export const webviewPanel = 'webview-panel'; + + /** + * Scheme used for loading the wrapper html and script in webviews. + */ + export const vscodeWebview = 'vscode-webview'; + + /** + * Scheme used for extension pages + */ + export const extension = 'extension'; + + /** + * Scheme used as a replacement of `file` scheme to load + * files with our custom protocol handler (desktop only). + */ + export const vscodeFileResource = 'vscode-file'; + + /** + * Scheme used for temporary resources + */ + export const tmp = 'tmp'; + + /** + * Scheme used vs live share + */ + export const vsls = 'vsls'; +} diff --git a/src/telemetry/interpreterPackages.node.ts b/src/telemetry/interpreterPackages.node.ts index 7244e9088c7..951f6af9a6f 100644 --- a/src/telemetry/interpreterPackages.node.ts +++ b/src/telemetry/interpreterPackages.node.ts @@ -9,6 +9,7 @@ import { createDeferred, Deferred } from '../platform/common/utils/async'; import { isResource, noop } from '../platform/common/utils/misc'; import { IInterpreterService } from '../platform/interpreter/contracts.node'; import { PythonEnvironment } from '../platform/pythonEnvironments/info'; +import { getComparisonKey } from '../platform/vscode-path/resources'; import { getTelemetrySafeHashedString, getTelemetrySafeVersion } from './helpers'; const interestedPackages = new Set( @@ -51,10 +52,11 @@ export class InterpreterPackages { ); } public static getPackageVersions(interpreter: PythonEnvironment): Promise> { - let deferred = InterpreterPackages.interpreterInformation.get(interpreter.path); + const key = getComparisonKey(interpreter.uri); + let deferred = InterpreterPackages.interpreterInformation.get(key); if (!deferred) { deferred = createDeferred>(); - InterpreterPackages.interpreterInformation.set(interpreter.path, deferred); + InterpreterPackages.interpreterInformation.set(key, deferred); if (InterpreterPackages.instance) { InterpreterPackages.instance.trackInterpreterPackages(interpreter).catch(noop); @@ -99,7 +101,7 @@ export class InterpreterPackages { this.trackInterpreterPackages(interpreter, ignoreCache).catch(noop); } private async trackInterpreterPackages(interpreter: PythonEnvironment, ignoreCache?: boolean) { - const key = interpreter.path; + const key = getComparisonKey(interpreter.uri); if (InterpreterPackages.pendingInterpreterInformation.has(key) && !ignoreCache) { return; } @@ -147,10 +149,11 @@ export class InterpreterPackages { const version = getTelemetrySafeVersion(rawVersion); packageAndVersions.set(getTelemetrySafeHashedString(packageName), version || ''); }); - let deferred = InterpreterPackages.interpreterInformation.get(interpreter.path); + const key = getComparisonKey(interpreter.uri); + let deferred = InterpreterPackages.interpreterInformation.get(key); if (!deferred) { deferred = createDeferred>(); - InterpreterPackages.interpreterInformation.set(interpreter.path, deferred); + InterpreterPackages.interpreterInformation.set(key, deferred); } deferred.resolve(packageAndVersions); } diff --git a/src/telemetry/kernelTelemetry.node.ts b/src/telemetry/kernelTelemetry.node.ts index afe5ab4a95d..7f28037c4ef 100644 --- a/src/telemetry/kernelTelemetry.node.ts +++ b/src/telemetry/kernelTelemetry.node.ts @@ -7,6 +7,7 @@ import { EnvironmentType } from '../platform/pythonEnvironments/info'; import { KernelConnectionMetadata } from '../platform/../kernels/types'; import { Telemetry } from '../platform/common/constants'; import { sendKernelTelemetryEvent, trackKernelResourceInformation } from './telemetry.node'; +import { ResourceSet } from '../platform/vscode-path/map'; export function sendKernelListTelemetry( resource: Resource, @@ -19,7 +20,7 @@ export function sendKernelListTelemetry( kernelLiveCount: 0, condaEnvsSharingSameInterpreter: 0 }; - const uniqueCondaInterpreterPaths = new Set(); + const uniqueCondaInterpreterPaths = new ResourceSet(); kernels.forEach((item) => { switch (item.kind) { case 'connectToLiveRemoteKernel': @@ -36,10 +37,10 @@ export function sendKernelListTelemetry( // Tody we don't support such environments, lets see if people are using these, if they are then // We know kernels will not start correctly for those environments (even if started, packages might not be located correctly). if (item.interpreter.envType === EnvironmentType.Conda) { - if (uniqueCondaInterpreterPaths.has(item.interpreter.path)) { + if (uniqueCondaInterpreterPaths.has(item.interpreter.uri)) { counters.condaEnvsSharingSameInterpreter += 1; } else { - uniqueCondaInterpreterPaths.add(item.interpreter.path); + uniqueCondaInterpreterPaths.add(item.interpreter.uri); } } break; diff --git a/src/telemetry/telemetry.node.ts b/src/telemetry/telemetry.node.ts index 6f0ec95dca3..82bae28d689 100644 --- a/src/telemetry/telemetry.node.ts +++ b/src/telemetry/telemetry.node.ts @@ -234,7 +234,7 @@ export function trackKernelResourceInformation(resource: Resource, information: ); currentData.pythonEnvironmentType = interpreter.envType; currentData.pythonEnvironmentPath = getTelemetrySafeHashedString( - getNormalizedInterpreterPath(interpreter.path) + getNormalizedInterpreterPath(interpreter.uri).fsPath ); pythonEnvironmentsByHash.set(currentData.pythonEnvironmentPath, interpreter); if (interpreter.version) { diff --git a/src/telemetry/workspaceInterpreterTracker.node.ts b/src/telemetry/workspaceInterpreterTracker.node.ts index da8cebf9ddb..bd515aac31b 100644 --- a/src/telemetry/workspaceInterpreterTracker.node.ts +++ b/src/telemetry/workspaceInterpreterTracker.node.ts @@ -13,7 +13,7 @@ import { areInterpreterPathsSame } from '../platform/pythonEnvironments/info/int @injectable() export class WorkspaceInterpreterTracker implements IExtensionSyncActivationService { - private static readonly workspaceInterpreters = new Map(); + private static readonly workspaceInterpreters = new Map(); private trackingInterpreters?: boolean; private static getWorkspaceIdentifier: (resource: Resource) => string = () => ''; constructor( @@ -40,7 +40,7 @@ export class WorkspaceInterpreterTracker implements IExtensionSyncActivationServ if (!activeInterpreterPath) { return; } - return areInterpreterPathsSame(activeInterpreterPath, interpreter.path); + return areInterpreterPathsSame(activeInterpreterPath, interpreter.uri); } private trackActiveInterpreters() { if (this.trackingInterpreters || !this.pythonExtensionChecker.isPythonExtensionActive) { @@ -57,7 +57,7 @@ export class WorkspaceInterpreterTracker implements IExtensionSyncActivationServ try { const workspaceId = this.workspaceService.getWorkspaceFolderIdentifier(item); const interpreter = await this.interpreterService.getActiveInterpreter(item); - WorkspaceInterpreterTracker.workspaceInterpreters.set(workspaceId, interpreter?.path); + WorkspaceInterpreterTracker.workspaceInterpreters.set(workspaceId, interpreter?.uri); } catch (ex) { // Don't care. } diff --git a/src/test/client/api.vscode.test.ts b/src/test/client/api.vscode.test.ts index f4fbac0c462..f30ad01c31c 100644 --- a/src/test/client/api.vscode.test.ts +++ b/src/test/client/api.vscode.test.ts @@ -14,7 +14,7 @@ import { startJupyterServer, waitForTextOutput, workAroundVSCodeNotebookStartPages -} from '../datascience/notebook/helper'; +} from '../datascience/notebook/helper.node'; import { initialize } from '../initialize.node'; import * as sinon from 'sinon'; import { captureScreenShot, createEventHandler, IExtensionTestApi } from '../common.node'; diff --git a/src/test/common.node.ts b/src/test/common.node.ts index e1c2ba188ae..5191af0e32c 100644 --- a/src/test/common.node.ts +++ b/src/test/common.node.ts @@ -31,13 +31,6 @@ export const PYTHON_PATH = getPythonPath(); // Useful to see on CI (when working with conda & non-conda, virtual envs & the like). console.log(`Python used in tests is ${PYTHON_PATH}`); -export enum OSType { - Unknown = 'Unknown', - Windows = 'Windows', - OSX = 'OSX', - Linux = 'Linux' -} - export type PythonSettingKeys = | 'workspaceSymbols.enabled' | 'defaultInterpreterPath' @@ -169,19 +162,6 @@ function getPythonPath(): string { return 'python'; } -export function getOSType(): OSType { - const platform: string = process.platform; - if (/^win/.test(platform)) { - return OSType.Windows; - } else if (/^darwin/.test(platform)) { - return OSType.OSX; - } else if (/^linux/.test(platform)) { - return OSType.Linux; - } else { - return OSType.Unknown; - } -} - /** * Get the current Python interpreter version. * diff --git a/src/test/common/platform/filesystem.unit.test.ts b/src/test/common/platform/filesystem.unit.test.ts deleted file mode 100644 index c9e3e2ef325..00000000000 --- a/src/test/common/platform/filesystem.unit.test.ts +++ /dev/null @@ -1,1527 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import * as fs from 'fs'; -import * as fsextra from 'fs-extra'; -import * as TypeMoq from 'typemoq'; -import * as vscode from 'vscode'; -import { FileSystemUtils, RawFileSystem } from '../../../platform/common/platform/fileSystemUtils.node'; -import { - // These interfaces are needed for FileSystemUtils deps. - IFileSystemPaths, - IFileSystemPathUtils, - ITempFileSystem -} from '../../../platform/common/platform/types'; -import { IRawFileSystem } from '../../../platform/common/platform/types.node'; - -/* eslint-disable */ - -function Uri(filename: string): vscode.Uri { - return vscode.Uri.file(filename); -} - -function createDummyStat(filetype: vscode.FileType): vscode.FileStat { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return { type: filetype } as any; -} - -function copyStat(stat: vscode.FileStat, old: TypeMoq.IMock) { - old.setup((s) => s.size) // plug in the original value - .returns(() => stat.size); - old.setup((s) => s.ctimeMs) // plug in the original value - .returns(() => stat.ctime); - old.setup((s) => s.mtimeMs) // plug in the original value - .returns(() => stat.mtime); -} - -interface IPaths { - // fs paths (IFileSystemPaths) - sep: string; - dirname(filename: string): string; - join(...paths: string[]): string; -} - -interface IRawFS extends IPaths { - // vscode.workspace.fs - copy(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable; - createDirectory(uri: vscode.Uri): Thenable; - delete(uri: vscode.Uri, options?: { recursive: boolean; useTrash: boolean }): Thenable; - readDirectory(uri: vscode.Uri): Thenable<[string, vscode.FileType][]>; - readFile(uri: vscode.Uri): Thenable; - rename(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable; - stat(uri: vscode.Uri): Thenable; - writeFile(uri: vscode.Uri, content: Uint8Array): Thenable; - - // "fs-extra" - lstat(filename: string): Promise; - chmod(filePath: string, mode: string | number): Promise; - appendFile(filename: string, data: {}): Promise; - lstatSync(filename: string): fs.Stats; - statSync(filename: string): fs.Stats; - readFileSync(path: string, encoding: string): string; - createReadStream(filename: string): fs.ReadStream; - createWriteStream(filename: string): fs.WriteStream; -} - -suite('Raw FileSystem', () => { - let raw: TypeMoq.IMock; - let oldStats: TypeMoq.IMock[]; - let filesystem: RawFileSystem; - setup(() => { - raw = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - oldStats = []; - filesystem = new RawFileSystem( - // Since it's a mock we can just use it for all 3 values. - raw.object, - raw.object, - raw.object - ); - }); - function verifyAll() { - raw.verifyAll(); - oldStats.forEach((stat) => { - stat.verifyAll(); - }); - } - function createMockLegacyStat(): TypeMoq.IMock { - const stat = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - // This is necessary because passing "mock.object" to - // Promise.resolve() triggers the lookup. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - stat.setup((s: any) => s.then) - .returns(() => undefined) - .verifiable(TypeMoq.Times.atLeast(0)); - oldStats.push(stat); - return stat; - } - function setupStatFileType(stat: TypeMoq.IMock, filetype: vscode.FileType) { - // This mirrors the logic in convertFileType(). - if (filetype === vscode.FileType.File) { - stat.setup((s) => s.isFile()) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - } else if (filetype === vscode.FileType.Directory) { - stat.setup((s) => s.isFile()) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - stat.setup((s) => s.isDirectory()) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - } else if ((filetype & vscode.FileType.SymbolicLink) > 0) { - stat.setup((s) => s.isFile()) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - stat.setup((s) => s.isDirectory()) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - stat.setup((s) => s.isSymbolicLink()) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - } else if (filetype === vscode.FileType.Unknown) { - stat.setup((s) => s.isFile()) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - stat.setup((s) => s.isDirectory()) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - stat.setup((s) => s.isSymbolicLink()) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - } else { - throw Error(`unsupported file type ${filetype}`); - } - } - - suite('stat', () => { - test('wraps the low-level function', async () => { - const filename = 'x/y/z/spam.py'; - const expected = createDummyStat(vscode.FileType.File); - raw.setup((r) => r.stat(Uri(filename))) // expect the specific filename - .returns(() => Promise.resolve(expected)); - - const stat = await filesystem.stat(filename); - - expect(stat).to.equal(expected); - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.stat(TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - const promise = filesystem.stat('spam.py'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('lstat', () => { - [ - { kind: 'file', filetype: vscode.FileType.File }, - { kind: 'dir', filetype: vscode.FileType.Directory }, - { kind: 'symlink', filetype: vscode.FileType.SymbolicLink }, - { kind: 'unknown', filetype: vscode.FileType.Unknown } - ].forEach((testData) => { - test(`wraps the low-level function (filetype: ${testData.kind}`, async () => { - const filename = 'x/y/z/spam.py'; - const expected: vscode.FileStat = { - type: testData.filetype, - size: 10, - ctime: 101, - mtime: 102 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; - const old = createMockLegacyStat(); - setupStatFileType(old, testData.filetype); - copyStat(expected, old); - raw.setup((r) => r.lstat(filename)) // expect the specific filename - .returns(() => Promise.resolve(old.object)); - - const stat = await filesystem.lstat(filename); - - expect(stat).to.deep.equal(expected); - verifyAll(); - }); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.lstat(TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - const promise = filesystem.lstat('spam.py'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('chmod', () => { - test('passes through a string mode', async () => { - const filename = 'x/y/z/spam.py'; - const mode = '755'; - raw.setup((r) => r.chmod(filename, mode)) // expect the specific filename - .returns(() => Promise.resolve()); - - await filesystem.chmod(filename, mode); - - verifyAll(); - }); - - test('passes through an int mode', async () => { - const filename = 'x/y/z/spam.py'; - const mode = 0o755; - raw.setup((r) => r.chmod(filename, mode)) // expect the specific filename - .returns(() => Promise.resolve()); - - await filesystem.chmod(filename, mode); - - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.chmod(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - const promise = filesystem.chmod('spam.py', 755); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('move', () => { - test('move a file (target does not exist)', async () => { - const src = 'x/y/z/spam.py'; - const tgt = 'x/y/spam.py'; - raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. - .returns(() => 'x/y'); - raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. - .returns(() => Promise.resolve(undefined as unknown as vscode.FileStat)); - raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename - .returns(() => Promise.resolve()); - - await filesystem.move(src, tgt); - - verifyAll(); - }); - - test('move a file (target exists)', async () => { - const src = 'x/y/z/spam.py'; - const tgt = 'x/y/spam.py'; - raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. - .returns(() => 'x/y'); - raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. - .returns(() => Promise.resolve(undefined as unknown as vscode.FileStat)); - const err = vscode.FileSystemError.FileExists('...'); - raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename - .returns(() => Promise.reject(err)); - raw.setup((r) => r.stat(Uri(tgt))) // It's a file. - .returns(() => Promise.resolve({ type: vscode.FileType.File } as unknown as vscode.FileStat)); - raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: true })) // expect the specific filename - .returns(() => Promise.resolve()); - - await filesystem.move(src, tgt); - - verifyAll(); - }); - - test('move a directory (target does not exist)', async () => { - const src = 'x/y/z/spam'; - const tgt = 'x/y/spam'; - raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. - .returns(() => 'x/y'); - raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. - .returns(() => Promise.resolve(undefined as unknown as vscode.FileStat)); - raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename - .returns(() => Promise.resolve()); - - await filesystem.move(src, tgt); - - verifyAll(); - }); - - test('moving a directory fails if target exists', async () => { - const src = 'x/y/z/spam.py'; - const tgt = 'x/y/spam.py'; - raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. - .returns(() => 'x/y'); - raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. - .returns(() => Promise.resolve(undefined as unknown as vscode.FileStat)); - const err = vscode.FileSystemError.FileExists('...'); - raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename - .returns(() => Promise.reject(err)); - raw.setup((r) => r.stat(Uri(tgt))) // It's a directory. - .returns(() => Promise.resolve({ type: vscode.FileType.Directory } as unknown as vscode.FileStat)); - - const promise = filesystem.move(src, tgt); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - - test('move a symlink to a directory (target exists)', async () => { - const src = 'x/y/z/spam'; - const tgt = 'x/y/spam.lnk'; - raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. - .returns(() => 'x/y'); - raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. - .returns(() => Promise.resolve(undefined as unknown as vscode.FileStat)); - const err = vscode.FileSystemError.FileExists('...'); - raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename - .returns(() => Promise.reject(err)); - raw.setup((r) => r.stat(Uri(tgt))) // It's a symlink. - .returns(() => - Promise.resolve({ - type: vscode.FileType.SymbolicLink | vscode.FileType.Directory - } as unknown as vscode.FileStat) - ); - raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: true })) // expect the specific filename - .returns(() => Promise.resolve()); - - await filesystem.move(src, tgt); - - verifyAll(); - }); - - test('fails if the target parent dir does not exist', async () => { - raw.setup((r) => r.dirname(TypeMoq.It.isAny())) // Provide the target's parent. - .returns(() => ''); - const err = vscode.FileSystemError.FileNotFound('...'); - raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The parent dir does not exist. - .returns(() => Promise.reject(err)); - - const promise = filesystem.move('spam', 'eggs'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.dirname(TypeMoq.It.isAny())) // Provide the target's parent. - .returns(() => ''); - raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The parent dir exists. - .returns(() => Promise.resolve(undefined as unknown as vscode.FileStat)); - const err = new Error('oops!'); - raw.setup((r) => r.rename(TypeMoq.It.isAny(), TypeMoq.It.isAny(), { overwrite: false })) // We don't care about the filename. - .throws(err); - - const promise = filesystem.move('spam', 'eggs'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('readData', () => { - test('wraps the low-level function', async () => { - const filename = 'x/y/z/spam.py'; - const expected = Buffer.from(''); - raw.setup((r) => r.readFile(Uri(filename))) // expect the specific filename - .returns(() => Promise.resolve(expected)); - - const data = await filesystem.readData(filename); - - expect(data).to.deep.equal(expected); - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.readFile(TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - const promise = filesystem.readData('spam.py'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('readText', () => { - test('wraps the low-level function', async () => { - const filename = 'x/y/z/spam.py'; - const expected = ''; - const data = Buffer.from(expected); - raw.setup((r) => r.readFile(Uri(filename))) // expect the specific filename - .returns(() => Promise.resolve(data)); - - const text = await filesystem.readText(filename); - - expect(text).to.equal(expected); - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.readFile(TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - const promise = filesystem.readText('spam.py'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('writeText', () => { - test('wraps the low-level function', async () => { - const filename = 'x/y/z/spam.py'; - const text = ''; - const data = Buffer.from(text); - raw.setup((r) => r.writeFile(Uri(filename), data)) // expect the specific filename - .returns(() => Promise.resolve()); - - await filesystem.writeText(filename, text); - - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.writeFile(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - const promise = filesystem.writeText('spam.py', ''); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('appendText', () => { - test('wraps the low-level function', async () => { - const filename = 'x/y/z/spam.py'; - const text = ''; - raw.setup((r) => r.appendFile(filename, text)) // expect the specific filename - .returns(() => Promise.resolve()); - - await filesystem.appendText(filename, text); - - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.appendFile(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - const promise = filesystem.appendText('spam.py', ''); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('copyFile', () => { - test('wraps the low-level function', async () => { - const src = 'x/y/z/spam.py'; - const tgt = 'x/y/z/eggs.py'; - raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. - .returns(() => 'x/y/z'); - raw.setup((r) => r.stat(Uri('x/y/z'))) // The parent dir exists. - .returns(() => Promise.resolve(undefined as unknown as vscode.FileStat)); - raw.setup((r) => r.copy(Uri(src), Uri(tgt), { overwrite: true })) // Expect the specific args. - .returns(() => Promise.resolve()); - - await filesystem.copyFile(src, tgt); - - verifyAll(); - }); - - test('fails if target parent does not exist', async () => { - raw.setup((r) => r.dirname(TypeMoq.It.isAny())) // Provide the target's parent. - .returns(() => ''); - const err = vscode.FileSystemError.FileNotFound('...'); - raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The parent dir exists. - .returns(() => Promise.reject(err)); - - const promise = filesystem.copyFile('spam', 'eggs'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.dirname(TypeMoq.It.isAny())) // Provide the target's parent. - .returns(() => ''); - raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The parent dir exists. - .returns(() => Promise.resolve(undefined as unknown as vscode.FileStat)); - raw.setup((r) => r.copy(TypeMoq.It.isAny(), TypeMoq.It.isAny(), { overwrite: true })) // We don't care about the filename. - .throws(new Error('file not found')); - - const promise = filesystem.copyFile('spam', 'eggs'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('rmFile', () => { - const opts = { - recursive: false, - useTrash: false - }; - - test('wraps the low-level function', async () => { - const filename = 'x/y/z/spam.py'; - raw.setup((r) => r.delete(Uri(filename), opts)) // expect the specific filename - .returns(() => Promise.resolve()); - - await filesystem.rmfile(filename); - - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.delete(TypeMoq.It.isAny(), opts)) // We don't care about the filename. - .throws(new Error('file not found')); - - const promise = filesystem.rmfile('spam.py'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('mkdirp', () => { - test('wraps the low-level function', async () => { - const dirname = 'x/y/z/spam'; - raw.setup((r) => r.createDirectory(Uri(dirname))) // expect the specific filename - .returns(() => Promise.resolve()); - - await filesystem.mkdirp(dirname); - - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.createDirectory(TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - const promise = filesystem.mkdirp('spam'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('rmdir', () => { - const opts = { - recursive: true, - useTrash: false - }; - - test('directory is empty', async () => { - const dirname = 'x/y/z/spam'; - raw.setup((r) => r.readDirectory(Uri(dirname))) // The dir is empty. - .returns(() => Promise.resolve([])); - raw.setup((r) => r.delete(Uri(dirname), opts)) // Expect the specific args. - .returns(() => Promise.resolve()); - - await filesystem.rmdir(dirname); - - verifyAll(); - }); - - test('fails if readDirectory() fails (e.g. is a file)', async () => { - raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // It's not a directory. - .throws(new Error('is a file')); - - const promise = filesystem.rmdir('spam'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - - test('fails if not empty', async () => { - const entries: [string, vscode.FileType][] = [ - ['dev1', vscode.FileType.Unknown], - ['w', vscode.FileType.Directory], - ['spam.py', vscode.FileType.File], - ['other', vscode.FileType.SymbolicLink | vscode.FileType.File] - ]; - raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // The dir is not empty. - .returns(() => Promise.resolve(entries)); - - const promise = filesystem.rmdir('spam'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // The "file" exists. - .returns(() => Promise.resolve([])); - raw.setup((r) => r.delete(TypeMoq.It.isAny(), opts)) // We don't care about the filename. - .throws(new Error('oops!')); - - const promise = filesystem.rmdir('spam'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('rmtree', () => { - const opts = { - recursive: true, - useTrash: false - }; - - test('wraps the low-level function', async () => { - const dirname = 'x/y/z/spam'; - raw.setup((r) => r.stat(Uri(dirname))) // The dir exists. - .returns(() => Promise.resolve(undefined as unknown as vscode.FileStat)); - raw.setup((r) => r.delete(Uri(dirname), opts)) // Expect the specific dirname. - .returns(() => Promise.resolve()); - - await filesystem.rmtree(dirname); - - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The "file" exists. - .returns(() => Promise.resolve(undefined as unknown as vscode.FileStat)); - raw.setup((r) => r.delete(TypeMoq.It.isAny(), opts)) // We don't care about the filename. - .throws(new Error('file not found')); - - const promise = filesystem.rmtree('spam'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('listdir', () => { - test('mixed', async () => { - const dirname = 'x/y/z/spam'; - const actual: [string, vscode.FileType][] = [ - ['dev1', vscode.FileType.Unknown], - ['w', vscode.FileType.Directory], - ['spam.py', vscode.FileType.File], - ['other', vscode.FileType.SymbolicLink | vscode.FileType.File] - ]; - const expected = actual.map(([basename, filetype]) => { - const filename = `x/y/z/spam/${basename}`; - raw.setup((r) => r.join(dirname, basename)) // Expect the specific basename. - .returns(() => filename); - return [filename, filetype] as [string, vscode.FileType]; - }); - raw.setup((r) => r.readDirectory(Uri(dirname))) // Expect the specific filename. - .returns(() => Promise.resolve(actual)); - - const entries = await filesystem.listdir(dirname); - - expect(entries).to.deep.equal(expected); - verifyAll(); - }); - - test('empty', async () => { - const dirname = 'x/y/z/spam'; - const expected: [string, vscode.FileType][] = []; - raw.setup((r) => r.readDirectory(Uri(dirname))) // expect the specific filename - .returns(() => Promise.resolve([])); - - const entries = await filesystem.listdir(dirname); - - expect(entries).to.deep.equal(expected); - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - const promise = filesystem.listdir('spam'); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('statSync', () => { - test('wraps the low-level function (filetype: unknown)', async () => { - const filename = 'x/y/z/spam.py'; - const expected: vscode.FileStat = { - type: vscode.FileType.Unknown, - size: 10, - ctime: 101, - mtime: 102 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; - const lstat = createMockLegacyStat(); - setupStatFileType(lstat, vscode.FileType.Unknown); - copyStat(expected, lstat); - raw.setup((r) => r.lstatSync(filename)) // expect the specific filename - .returns(() => lstat.object); - - const stat = filesystem.statSync(filename); - - expect(stat).to.deep.equal(expected); - verifyAll(); - }); - - [ - { kind: 'file', filetype: vscode.FileType.File }, - { kind: 'dir', filetype: vscode.FileType.Directory } - ].forEach((testData) => { - test(`wraps the low-level function (filetype: ${testData.kind})`, async () => { - const filename = 'x/y/z/spam.py'; - const expected: vscode.FileStat = { - type: testData.filetype, - size: 10, - ctime: 101, - mtime: 102 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; - const lstat = createMockLegacyStat(); - lstat - .setup((s) => s.isSymbolicLink()) // not a symlink - .returns(() => false); - setupStatFileType(lstat, testData.filetype); - copyStat(expected, lstat); - raw.setup((r) => r.lstatSync(filename)) // expect the specific filename - .returns(() => lstat.object); - - const stat = filesystem.statSync(filename); - - expect(stat).to.deep.equal(expected); - verifyAll(); - }); - }); - - [ - { kind: 'file', filetype: vscode.FileType.File }, - { kind: 'dir', filetype: vscode.FileType.Directory }, - { kind: 'unknown', filetype: vscode.FileType.Unknown } - ].forEach((testData) => { - test(`wraps the low-level function (filetype: ${testData.kind} symlink)`, async () => { - const filename = 'x/y/z/spam.py'; - const expected: vscode.FileStat = { - type: testData.filetype | vscode.FileType.SymbolicLink, - size: 10, - ctime: 101, - mtime: 102 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; - const lstat = createMockLegacyStat(); - lstat - .setup((s) => s.isSymbolicLink()) // not a symlink - .returns(() => true); - raw.setup((r) => r.lstatSync(filename)) // expect the specific filename - .returns(() => lstat.object); - const old = createMockLegacyStat(); - setupStatFileType(old, testData.filetype); - copyStat(expected, old); - raw.setup((r) => r.statSync(filename)) // expect the specific filename - .returns(() => old.object); - - const stat = filesystem.statSync(filename); - - expect(stat).to.deep.equal(expected); - verifyAll(); - }); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.lstatSync(TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - expect(() => { - filesystem.statSync('spam.py'); - }).to.throw(); - verifyAll(); - }); - }); - - suite('readTextSync', () => { - test('wraps the low-level function', () => { - const filename = 'x/y/z/spam.py'; - const expected = ''; - raw.setup((r) => r.readFileSync(filename, 'utf8')) // expect the specific filename - .returns(() => expected); - - const text = filesystem.readTextSync(filename); - - expect(text).to.equal(expected); - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.readFileSync(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - expect(() => filesystem.readTextSync('spam.py')).to.throw(); - - verifyAll(); - }); - }); - - suite('createReadStream', () => { - test('wraps the low-level function', () => { - const filename = 'x/y/z/spam.py'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const expected = {} as any; - raw.setup((r) => r.createReadStream(filename)) // expect the specific filename - .returns(() => expected); - - const stream = filesystem.createReadStream(filename); - - expect(stream).to.equal(expected); - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.createReadStream(TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - expect(() => filesystem.createReadStream('spam.py')).to.throw(); - - verifyAll(); - }); - }); - - suite('createWriteStream', () => { - test('wraps the low-level function', () => { - const filename = 'x/y/z/spam.py'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const expected = {} as any; - raw.setup((r) => r.createWriteStream(filename)) // expect the specific filename - .returns(() => expected); - - const stream = filesystem.createWriteStream(filename); - - expect(stream).to.equal(expected); - verifyAll(); - }); - - test('fails if the low-level call fails', async () => { - raw.setup((r) => r.createWriteStream(TypeMoq.It.isAny())) // We don't care about the filename. - .throws(new Error('file not found')); - - expect(() => filesystem.createWriteStream('spam.py')).to.throw(); - - verifyAll(); - }); - }); -}); - -interface IUtilsDeps extends IRawFileSystem, IFileSystemPaths, IFileSystemPathUtils, ITempFileSystem { - // helpers - getHash(data: string): string; - globFile(pat: string, options?: { cwd: string }): Promise; -} - -suite('FileSystemUtils', () => { - let deps: TypeMoq.IMock; - let stats: TypeMoq.IMock[]; - let utils: FileSystemUtils; - setup(() => { - deps = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - - stats = []; - utils = new FileSystemUtils( - // Since it's a mock we can just use it for all 3 values. - deps.object, // rawFS - deps.object, // pathUtils - deps.object, // paths - deps.object, // tempFS - (data: string) => deps.object.getHash(data), - (pat: string, options?: { cwd: string }) => deps.object.globFile(pat, options) - ); - }); - function verifyAll() { - deps.verifyAll(); - stats.forEach((stat) => { - stat.verifyAll(); - }); - } - function createMockStat(): TypeMoq.IMock { - const stat = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - // This is necessary because passing "mock.object" to - // Promise.resolve() triggers the lookup. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - stat.setup((s: any) => s.then) - .returns(() => undefined) - .verifiable(TypeMoq.Times.atLeast(0)); - stats.push(stat); - return stat; - } - - suite('createDirectory', () => { - test('wraps the low-level function', async () => { - const dirname = 'x/y/z/spam'; - deps.setup((d) => d.mkdirp(dirname)) // expect the specific filename - .returns(() => Promise.resolve()); - - await utils.createDirectory(dirname); - - verifyAll(); - }); - }); - - suite('deleteDirectory', () => { - test('wraps the low-level function', async () => { - const dirname = 'x/y/z/spam'; - deps.setup((d) => d.rmdir(dirname)) // expect the specific filename - .returns(() => Promise.resolve()); - - await utils.deleteDirectory(dirname); - - verifyAll(); - }); - }); - - suite('deleteFile', () => { - test('wraps the low-level function', async () => { - const filename = 'x/y/z/spam.py'; - deps.setup((d) => d.rmfile(filename)) // expect the specific filename - .returns(() => Promise.resolve()); - - await utils.deleteFile(filename); - - verifyAll(); - }); - }); - - suite('pathExists', () => { - test('exists (without type)', async () => { - const filename = 'x/y/z/spam.py'; - const stat = createMockStat(); - deps.setup((d) => d.stat(filename)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.pathExists(filename); - - expect(exists).to.equal(true); - verifyAll(); - }); - - test('does not exist', async () => { - const filename = 'x/y/z/spam.py'; - const err = vscode.FileSystemError.FileNotFound(filename); - deps.setup((d) => d.stat(filename)) // The file does not exist. - .throws(err); - - const exists = await utils.pathExists(filename); - - expect(exists).to.equal(false); - verifyAll(); - }); - - test('ignores errors from stat()', async () => { - const filename = 'x/y/z/spam.py'; - deps.setup((d) => d.stat(filename)) // It's broken. - .returns(() => Promise.reject(new Error('oops!'))); - - const exists = await utils.pathExists(filename); - - expect(exists).to.equal(false); - verifyAll(); - }); - - test('matches (type: undefined)', async () => { - const filename = 'x/y/z/spam.py'; - const stat = createMockStat(); - deps.setup((d) => d.stat(filename)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.pathExists(filename); - - expect(exists).to.equal(true); - verifyAll(); - }); - - test('matches (type: file)', async () => { - const filename = 'x/y/z/spam.py'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a file. - .returns(() => vscode.FileType.File); - deps.setup((d) => d.stat(filename)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.pathExists(filename, vscode.FileType.File); - - expect(exists).to.equal(true); - verifyAll(); - }); - - test('mismatch (type: file)', async () => { - const filename = 'x/y/z/spam.py'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a directory. - .returns(() => vscode.FileType.Directory); - deps.setup((d) => d.stat(filename)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.pathExists(filename, vscode.FileType.File); - - expect(exists).to.equal(false); - verifyAll(); - }); - - test('matches (type: directory)', async () => { - const dirname = 'x/y/z/spam.py'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a directory. - .returns(() => vscode.FileType.Directory); - deps.setup((d) => d.stat(dirname)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.pathExists(dirname, vscode.FileType.Directory); - - expect(exists).to.equal(true); - verifyAll(); - }); - - test('mismatch (type: directory)', async () => { - const dirname = 'x/y/z/spam.py'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a file. - .returns(() => vscode.FileType.File); - deps.setup((d) => d.stat(dirname)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.pathExists(dirname, vscode.FileType.Directory); - - expect(exists).to.equal(false); - verifyAll(); - }); - - test('symlinks are followed', async () => { - const symlink = 'x/y/z/spam.py'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a symlink to a file. - .returns(() => vscode.FileType.File | vscode.FileType.SymbolicLink) - .verifiable(TypeMoq.Times.exactly(3)); - deps.setup((d) => d.stat(symlink)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)) - .verifiable(TypeMoq.Times.exactly(3)); - - const exists = await utils.pathExists(symlink, vscode.FileType.SymbolicLink); - const destIsFile = await utils.pathExists(symlink, vscode.FileType.File); - const destIsDir = await utils.pathExists(symlink, vscode.FileType.Directory); - - expect(exists).to.equal(true); - expect(destIsFile).to.equal(true); - expect(destIsDir).to.equal(false); - verifyAll(); - }); - - test('mismatch (type: symlink)', async () => { - const filename = 'x/y/z/spam.py'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a file. - .returns(() => vscode.FileType.File); - deps.setup((d) => d.stat(filename)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.pathExists(filename, vscode.FileType.SymbolicLink); - - expect(exists).to.equal(false); - verifyAll(); - }); - - test('matches (type: unknown)', async () => { - const sockFile = 'x/y/z/ipc.sock'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a socket. - .returns(() => vscode.FileType.Unknown); - deps.setup((d) => d.stat(sockFile)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.pathExists(sockFile, vscode.FileType.Unknown); - - expect(exists).to.equal(true); - verifyAll(); - }); - - test('mismatch (type: unknown)', async () => { - const filename = 'x/y/z/spam.py'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a file. - .returns(() => vscode.FileType.File); - deps.setup((d) => d.stat(filename)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.pathExists(filename, vscode.FileType.Unknown); - - expect(exists).to.equal(false); - verifyAll(); - }); - }); - - suite('fileExists', () => { - test('want file, got file', async () => { - const filename = 'x/y/z/spam.py'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a File. - .returns(() => vscode.FileType.File); - deps.setup((d) => d.stat(filename)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.fileExists(filename); - - expect(exists).to.equal(true); - verifyAll(); - }); - - test('want file, not file', async () => { - const filename = 'x/y/z/spam.py'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a directory. - .returns(() => vscode.FileType.Directory); - deps.setup((d) => d.stat(filename)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.fileExists(filename); - - expect(exists).to.equal(false); - verifyAll(); - }); - - test('symlink', async () => { - const symlink = 'x/y/z/spam.py'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a symlink to a File. - .returns(() => vscode.FileType.File | vscode.FileType.SymbolicLink); - deps.setup((d) => d.stat(symlink)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.fileExists(symlink); - - // This is because we currently use stat() and not lstat(). - expect(exists).to.equal(true); - verifyAll(); - }); - - test('unknown', async () => { - const sockFile = 'x/y/z/ipc.sock'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a socket. - .returns(() => vscode.FileType.Unknown); - deps.setup((d) => d.stat(sockFile)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.fileExists(sockFile); - - expect(exists).to.equal(false); - verifyAll(); - }); - }); - - suite('directoryExists', () => { - test('want directory, got directory', async () => { - const dirname = 'x/y/z/spam'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a directory. - .returns(() => vscode.FileType.Directory); - deps.setup((d) => d.stat(dirname)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.directoryExists(dirname); - - expect(exists).to.equal(true); - verifyAll(); - }); - - test('want directory, not directory', async () => { - const dirname = 'x/y/z/spam'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a file. - .returns(() => vscode.FileType.File); - deps.setup((d) => d.stat(dirname)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.directoryExists(dirname); - - expect(exists).to.equal(false); - verifyAll(); - }); - - test('symlink', async () => { - const symlink = 'x/y/z/spam'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a symlink to a directory. - .returns(() => vscode.FileType.Directory | vscode.FileType.SymbolicLink); - deps.setup((d) => d.stat(symlink)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.directoryExists(symlink); - - // This is because we currently use stat() and not lstat(). - expect(exists).to.equal(true); - verifyAll(); - }); - - test('unknown', async () => { - const sockFile = 'x/y/z/ipc.sock'; - const stat = createMockStat(); - stat.setup((s) => s.type) // It's a socket. - .returns(() => vscode.FileType.Unknown); - deps.setup((d) => d.stat(sockFile)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const exists = await utils.directoryExists(sockFile); - - expect(exists).to.equal(false); - verifyAll(); - }); - }); - - suite('listdir', () => { - test('wraps the raw call on success', async () => { - const dirname = 'x/y/z/spam'; - const expected: [string, vscode.FileType][] = [ - ['x/y/z/spam/dev1', vscode.FileType.Unknown], - ['x/y/z/spam/w', vscode.FileType.Directory], - ['x/y/z/spam/spam.py', vscode.FileType.File], - ['x/y/z/spam/other', vscode.FileType.SymbolicLink | vscode.FileType.File] - ]; - deps.setup((d) => d.listdir(dirname)) // Full results get returned from RawFileSystem.listdir(). - .returns(() => Promise.resolve(expected)); - - const entries = await utils.listdir(dirname); - - expect(entries).to.deep.equal(expected); - verifyAll(); - }); - - test('returns [] if the directory does not exist', async () => { - const dirname = 'x/y/z/spam'; - const err = vscode.FileSystemError.FileNotFound(dirname); - deps.setup((d) => d.listdir(dirname)) // The "file" does not exist. - .returns(() => Promise.reject(err)); - deps.setup((d) => d.stat(dirname)) // The "file" does not exist. - .returns(() => Promise.reject(err)); - - const entries = await utils.listdir(dirname); - - expect(entries).to.deep.equal([]); - verifyAll(); - }); - - test('fails if not a directory', async () => { - const dirname = 'x/y/z/spam'; - const err = vscode.FileSystemError.FileNotADirectory(dirname); - deps.setup((d) => d.listdir(dirname)) // Fail (async) with not-a-directory. - .returns(() => Promise.reject(err)); - const stat = createMockStat(); - deps.setup((d) => d.stat(dirname)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const promise = utils.listdir(dirname); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - - test('fails if the raw call promise fails', async () => { - const dirname = 'x/y/z/spam'; - const err = new Error('oops!'); - deps.setup((d) => d.listdir(dirname)) // Fail (async) with an arbitrary error. - .returns(() => Promise.reject(err)); - deps.setup((d) => d.stat(dirname)) // Fail with file-not-found. - .throws(vscode.FileSystemError.FileNotFound(dirname)); - - const entries = await utils.listdir(dirname); - - expect(entries).to.deep.equal([]); - verifyAll(); - }); - - test('fails if the raw call fails', async () => { - const dirname = 'x/y/z/spam'; - const err = new Error('oops!'); - deps.setup((d) => d.listdir(dirname)) // Fail with an arbirary error. - .throws(err); - const stat = createMockStat(); - deps.setup((d) => d.stat(dirname)) // The "file" exists. - .returns(() => Promise.resolve(stat.object)); - - const promise = utils.listdir(dirname); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('getSubDirectories', () => { - test('filters out non-subdirs', async () => { - const dirname = 'x/y/z/spam'; - const entries: [string, vscode.FileType][] = [ - ['x/y/z/spam/dev1', vscode.FileType.Unknown], - ['x/y/z/spam/w', vscode.FileType.Directory], - ['x/y/z/spam/spam.py', vscode.FileType.File], - ['x/y/z/spam/v', vscode.FileType.Directory], - ['x/y/z/spam/eggs.py', vscode.FileType.File], - ['x/y/z/spam/other1', vscode.FileType.SymbolicLink | vscode.FileType.File], - ['x/y/z/spam/other2', vscode.FileType.SymbolicLink | vscode.FileType.Directory] - ]; - const expected = [ - // only entries with FileType.Directory - 'x/y/z/spam/w', - 'x/y/z/spam/v', - 'x/y/z/spam/other2' - ]; - deps.setup((d) => d.listdir(dirname)) // Full results get returned from RawFileSystem.listdir(). - .returns(() => Promise.resolve(entries)); - - const filtered = await utils.getSubDirectories(dirname); - - expect(filtered).to.deep.equal(expected); - verifyAll(); - }); - }); - - suite('getFiles', () => { - test('filters out non-files', async () => { - const filename = 'x/y/z/spam'; - const entries: [string, vscode.FileType][] = [ - ['x/y/z/spam/dev1', vscode.FileType.Unknown], - ['x/y/z/spam/w', vscode.FileType.Directory], - ['x/y/z/spam/spam.py', vscode.FileType.File], - ['x/y/z/spam/v', vscode.FileType.Directory], - ['x/y/z/spam/eggs.py', vscode.FileType.File], - ['x/y/z/spam/other1', vscode.FileType.SymbolicLink | vscode.FileType.File], - ['x/y/z/spam/other2', vscode.FileType.SymbolicLink | vscode.FileType.Directory] - ]; - const expected = [ - // only entries with FileType.File - 'x/y/z/spam/spam.py', - 'x/y/z/spam/eggs.py', - 'x/y/z/spam/other1' - ]; - deps.setup((d) => d.listdir(filename)) // Full results get returned from RawFileSystem.listdir(). - .returns(() => Promise.resolve(entries)); - - const filtered = await utils.getFiles(filename); - - expect(filtered).to.deep.equal(expected); - verifyAll(); - }); - }); - - suite('isDirReadonly', () => { - setup(() => { - deps.setup((d) => d.sep) // The value really doesn't matter. - .returns(() => '/'); - }); - - test('is not readonly', async () => { - const dirname = 'x/y/z/spam'; - const filename = `${dirname}/___vscpTest___`; - deps.setup((d) => d.stat(dirname)) // Success! - .returns(() => Promise.resolve(undefined as unknown as vscode.FileStat)); - deps.setup((d) => d.writeText(filename, '')) // Success! - .returns(() => Promise.resolve()); - deps.setup((d) => d.rmfile(filename)) // Success! - .returns(() => Promise.resolve()); - - const isReadonly = await utils.isDirReadonly(dirname); - - expect(isReadonly).to.equal(false); - verifyAll(); - }); - - test('is readonly', async () => { - const dirname = 'x/y/z/spam'; - const filename = `${dirname}/___vscpTest___`; - const err = new Error('not permitted'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (err as any).code = 'EACCES'; // errno - deps.setup((d) => d.stat(dirname)) // Success! - .returns(() => Promise.resolve(undefined as unknown as vscode.FileStat)); - deps.setup((d) => d.writeText(filename, '')) // not permitted - .returns(() => Promise.reject(err)); - - const isReadonly = await utils.isDirReadonly(dirname); - - expect(isReadonly).to.equal(true); - verifyAll(); - }); - - test('fails if the directory does not exist', async () => { - const dirname = 'x/y/z/spam'; - const err = new Error('not found'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (err as any).code = 'ENOENT'; // errno - deps.setup((d) => d.stat(dirname)) // file-not-found - .returns(() => Promise.reject(err)); - - const promise = utils.isDirReadonly(dirname); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('getFileHash', () => { - test('Getting hash for a file should return non-empty string', async () => { - const filename = 'x/y/z/spam.py'; - const stat = createMockStat(); - stat.setup((s) => s.ctime) // created - .returns(() => 100); - stat.setup((s) => s.mtime) // modified - .returns(() => 120); - deps.setup((d) => d.lstat(filename)) // file exists - .returns(() => Promise.resolve(stat.object)); - deps.setup((d) => d.getHash('100-120')) // built from ctime and mtime - .returns(() => 'deadbeef'); - - const hash = await utils.getFileHash(filename); - - expect(hash).to.equal('deadbeef'); - verifyAll(); - }); - - test('Getting hash for non existent file should throw error', async () => { - const filename = 'x/y/z/spam.py'; - const err = vscode.FileSystemError.FileNotFound(filename); - deps.setup((d) => d.lstat(filename)) // file-not-found - .returns(() => Promise.reject(err)); - - const promise = utils.getFileHash(filename); - - await expect(promise).to.eventually.be.rejected; - verifyAll(); - }); - }); - - suite('search', () => { - test('found matches (without cwd)', async () => { - const pattern = `x/y/z/spam.*`; - const expected: string[] = [ - // We can pretend that there were other files - // that were ignored. - 'x/y/z/spam.py', - 'x/y/z/spam.pyc', - 'x/y/z/spam.so', - 'x/y/z/spam.data' - ]; - deps.setup((d) => d.globFile(pattern, undefined)) // found some - .returns(() => Promise.resolve(expected)); - - const files = await utils.search(pattern); - - expect(files).to.deep.equal(expected); - verifyAll(); - }); - - test('found matches (with cwd)', async () => { - const pattern = `x/y/z/spam.*`; - const cwd = 'a/b/c'; - const expected: string[] = [ - // We can pretend that there were other files - // that were ignored. - 'x/y/z/spam.py', - 'x/y/z/spam.pyc', - 'x/y/z/spam.so', - 'x/y/z/spam.data' - ]; - deps.setup((d) => d.globFile(pattern, { cwd: cwd })) // found some - .returns(() => Promise.resolve(expected)); - - const files = await utils.search(pattern, cwd); - - expect(files).to.deep.equal(expected); - verifyAll(); - }); - - test('no matches (empty)', async () => { - const pattern = `x/y/z/spam.*`; - deps.setup((d) => d.globFile(pattern, undefined)) // found none - .returns(() => Promise.resolve([])); - - const files = await utils.search(pattern); - - expect(files).to.deep.equal([]); - verifyAll(); - }); - - test('no matches (undefined)', async () => { - const pattern = `x/y/z/spam.*`; - deps.setup((d) => d.globFile(pattern, undefined)) // found none - .returns(() => Promise.resolve(undefined as unknown as string[])); - - const files = await utils.search(pattern); - - expect(files).to.deep.equal([]); - verifyAll(); - }); - }); - - suite('fileExistsSync', () => { - test('file exists', async () => { - const filename = 'x/y/z/spam.py'; - deps.setup((d) => d.statSync(filename)) // The file exists. - .returns(() => undefined as unknown as vscode.FileStat); - - const exists = utils.fileExistsSync(filename); - - expect(exists).to.equal(true); - verifyAll(); - }); - - test('file does not exist', async () => { - const filename = 'x/y/z/spam.py'; - const err = vscode.FileSystemError.FileNotFound('...'); - deps.setup((d) => d.statSync(filename)) // The file does not exist. - .throws(err); - - const exists = utils.fileExistsSync(filename); - - expect(exists).to.equal(false); - verifyAll(); - }); - - test('fails if low-level call fails', async () => { - const filename = 'x/y/z/spam.py'; - const err = new Error('oops!'); - deps.setup((d) => d.statSync(filename)) // big badda boom - .throws(err); - - expect(() => utils.fileExistsSync(filename)).to.throw(err); - verifyAll(); - }); - }); -}); diff --git a/src/test/common/platform/fs-paths.functional.test.ts b/src/test/common/platform/fs-paths.functional.test.ts deleted file mode 100644 index e93497e71cd..00000000000 --- a/src/test/common/platform/fs-paths.functional.test.ts +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -/* eslint-disable , */ - -import { expect } from 'chai'; -import * as os from 'os'; -import * as path from '../../../platform/vscode-path/path'; -import { Executables, FileSystemPaths, FileSystemPathUtils } from '../../../platform/common/platform/fs-paths.node'; -import { WINDOWS as IS_WINDOWS } from './utils'; - -suite('FileSystem - Paths', () => { - let paths: FileSystemPaths; - setup(() => { - paths = FileSystemPaths.withDefaults(); - }); - - suite('separator', () => { - test('matches node', () => { - expect(paths.sep).to.be.equal(path.sep); - }); - }); - - suite('dirname', () => { - test('with dirname', () => { - const filename = path.join('spam', 'eggs', 'spam.py'); - const expected = path.join('spam', 'eggs'); - - const basename = paths.dirname(filename); - - expect(basename).to.equal(expected); - }); - - test('without dirname', () => { - const filename = 'spam.py'; - const expected = '.'; - - const basename = paths.dirname(filename); - - expect(basename).to.equal(expected); - }); - }); - - suite('basename', () => { - test('with dirname', () => { - const filename = path.join('spam', 'eggs', 'spam.py'); - const expected = 'spam.py'; - - const basename = paths.basename(filename); - - expect(basename).to.equal(expected); - }); - - test('without dirname', () => { - const filename = 'spam.py'; - const expected = filename; - - const basename = paths.basename(filename); - - expect(basename).to.equal(expected); - }); - }); - - suite('normalize', () => { - test('noop', () => { - const filename = path.join('spam', 'eggs', 'spam.py'); - const expected = filename; - - const norm = paths.normalize(filename); - - expect(norm).to.equal(expected); - }); - - test('pathological', () => { - const filename = path.join(path.sep, 'spam', '..', 'eggs', '.', 'spam.py'); - const expected = path.join(path.sep, 'eggs', 'spam.py'); - - const norm = paths.normalize(filename); - - expect(norm).to.equal(expected); - }); - - test('relative to CWD', () => { - const filename = path.join('..', 'spam', 'eggs', 'spam.py'); - const expected = filename; - - const norm = paths.normalize(filename); - - expect(norm).to.equal(expected); - }); - - test('parent of root fails', () => { - const filename = path.join(path.sep, '..'); - const expected = filename; - - const norm = paths.normalize(filename); - - expect(norm).to.equal(expected); - }); - }); - - suite('join', () => { - test('parts get joined by path.sep', () => { - const expected = path.join('x', 'y', 'z', 'spam.py'); - - const result = paths.join( - 'x', - // Be explicit here to ensure our assumptions are correct - // about the relationship between "sep" and "join()". - path.sep === '\\' ? 'y\\z' : 'y/z', - 'spam.py' - ); - - expect(result).to.equal(expected); - }); - }); - - suite('normCase', () => { - test('forward-slash', () => { - const filename = 'X/Y/Z/SPAM.PY'; - const expected = IS_WINDOWS ? 'X\\Y\\Z\\SPAM.PY' : filename; - - const result = paths.normCase(filename); - - expect(result).to.equal(expected); - }); - - test('backslash is not changed', () => { - const filename = 'X\\Y\\Z\\SPAM.PY'; - const expected = filename; - - const result = paths.normCase(filename); - - expect(result).to.equal(expected); - }); - - test('lower-case', () => { - const filename = 'x\\y\\z\\spam.py'; - const expected = IS_WINDOWS ? 'X\\Y\\Z\\SPAM.PY' : filename; - - const result = paths.normCase(filename); - - expect(result).to.equal(expected); - }); - - test('upper-case stays upper-case', () => { - const filename = 'X\\Y\\Z\\SPAM.PY'; - const expected = 'X\\Y\\Z\\SPAM.PY'; - - const result = paths.normCase(filename); - - expect(result).to.equal(expected); - }); - }); -}); - -suite('FileSystem - Executables', () => { - let execs: Executables; - setup(() => { - execs = Executables.withDefaults(); - }); - - suite('delimiter', () => { - test('matches node', () => { - expect(execs.delimiter).to.be.equal(path.delimiter); - }); - }); - - suite('getPathVariableName', () => { - const expected = IS_WINDOWS ? 'Path' : 'PATH'; - - test('matches platform', () => { - expect(execs.envVar).to.equal(expected); - }); - }); -}); - -suite('FileSystem - Path Utils', () => { - let utils: FileSystemPathUtils; - setup(() => { - utils = FileSystemPathUtils.withDefaults(); - }); - - suite('arePathsSame', () => { - test('identical', () => { - const filename = 'x/y/z/spam.py'; - - const result = utils.arePathsSame(filename, filename); - - expect(result).to.equal(true); - }); - - test('not the same', () => { - const file1 = 'x/y/z/spam.py'; - const file2 = 'a/b/c/spam.py'; - - const result = utils.arePathsSame(file1, file2); - - expect(result).to.equal(false); - }); - - test('with different separators', () => { - const file1 = 'x/y/z/spam.py'; - const file2 = 'x\\y\\z\\spam.py'; - const expected = IS_WINDOWS; - - const result = utils.arePathsSame(file1, file2); - - expect(result).to.equal(expected); - }); - - test('with different case', () => { - const file1 = 'x/y/z/spam.py'; - const file2 = 'x/Y/z/Spam.py'; - const expected = IS_WINDOWS; - - const result = utils.arePathsSame(file1, file2); - - expect(result).to.equal(expected); - }); - }); - - suite('getDisplayName', () => { - const relname = path.join('spam', 'eggs', 'spam.py'); - const cwd = path.resolve(path.sep, 'x', 'y', 'z'); - - test('filename matches CWD', () => { - const filename = path.join(cwd, relname); - const expected = `.${path.sep}${relname}`; - - const display = utils.getDisplayName(filename, cwd); - - expect(display).to.equal(expected); - }); - - test('filename does not match CWD', () => { - const filename = path.resolve(cwd, '..', relname); - const expected = filename; - - const display = utils.getDisplayName(filename, cwd); - - expect(display).to.equal(expected); - }); - - test('filename matches home dir, not cwd', () => { - const filename = path.join(os.homedir(), relname); - const expected = path.join('~', relname); - - const display = utils.getDisplayName(filename, cwd); - - expect(display).to.equal(expected); - }); - - test('filename matches home dir', () => { - const filename = path.join(os.homedir(), relname); - const expected = path.join('~', relname); - - const display = utils.getDisplayName(filename); - - expect(display).to.equal(expected); - }); - - test('filename does not match home dir', () => { - const filename = relname; - const expected = filename; - - const display = utils.getDisplayName(filename); - - expect(display).to.equal(expected); - }); - }); -}); diff --git a/src/test/common/platform/fs-paths.unit.test.ts b/src/test/common/platform/fs-paths.unit.test.ts deleted file mode 100644 index 51d704ae6d5..00000000000 --- a/src/test/common/platform/fs-paths.unit.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -/* eslint-disable */ - -import { expect } from 'chai'; -import * as path from '../../../platform/vscode-path/path'; -import * as TypeMoq from 'typemoq'; -import { FileSystemPathUtils } from '../../../platform/common/platform/fs-paths.node'; -import { getNamesAndValues } from '../../utils/enum'; -import { OSType } from '../../../platform/common/utils/platform'; - -interface IUtilsDeps { - // executables - delimiter: string; - envVar: string; - // paths - readonly sep: string; - join(...filenames: string[]): string; - dirname(filename: string): string; - basename(filename: string, suffix?: string): string; - normalize(filename: string): string; - normCase(filename: string): string; - // node "path" - relative(relpath: string, rootpath: string): string; -} - -suite('FileSystem - Path Utils', () => { - let deps: TypeMoq.IMock; - let utils: FileSystemPathUtils; - setup(() => { - deps = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); - utils = new FileSystemPathUtils( - 'my-home', - // It's simpler to just use one mock for all 3 dependencies. - deps.object, - deps.object, - deps.object - ); - }); - function verifyAll() { - deps.verifyAll(); - } - - suite('path-related', () => { - const caseInsensitive = [OSType.Windows]; - - suite('arePathsSame', () => { - getNamesAndValues(OSType).forEach((item) => { - const osType = item.value; - - function setNormCase(filename: string, numCalls = 1): string { - let norm = filename; - if (osType === OSType.Windows) { - norm = path.normalize(filename).toUpperCase(); - } - deps.setup((d) => d.normCase(filename)) - .returns(() => norm) - .verifiable(TypeMoq.Times.exactly(numCalls)); - return filename; - } - - [ - // no upper-case - 'c:\\users\\peter smith\\my documents\\test.txt', - // some upper-case - 'c:\\USERS\\Peter Smith\\my documents\\test.TXT' - ].forEach((path1) => { - test(`True if paths are identical (type: ${item.name}) - ${path1}`, () => { - path1 = setNormCase(path1, 2); - - const areSame = utils.arePathsSame(path1, path1); - - expect(areSame).to.be.equal(true, 'file paths do not match'); - verifyAll(); - }); - }); - - test(`False if paths are completely different (type: ${item.name})`, () => { - const path1 = setNormCase('c:\\users\\Peter Smith\\my documents\\test.txt'); - const path2 = setNormCase('c:\\users\\Peter Smith\\my documents\\test.exe'); - - const areSame = utils.arePathsSame(path1, path2); - - expect(areSame).to.be.equal(false, 'file paths do not match'); - verifyAll(); - }); - - if (caseInsensitive.includes(osType)) { - test(`True if paths only differ by case (type: ${item.name})`, () => { - const path1 = setNormCase('c:\\users\\Peter Smith\\my documents\\test.txt'); - const path2 = setNormCase('c:\\USERS\\Peter Smith\\my documents\\test.TXT'); - - const areSame = utils.arePathsSame(path1, path2); - - expect(areSame).to.be.equal(true, 'file paths match'); - verifyAll(); - }); - } else { - test(`False if paths only differ by case (type: ${item.name})`, () => { - const path1 = setNormCase('c:\\users\\Peter Smith\\my documents\\test.txt'); - const path2 = setNormCase('c:\\USERS\\Peter Smith\\my documents\\test.TXT'); - - const areSame = utils.arePathsSame(path1, path2); - - expect(areSame).to.be.equal(false, 'file paths do not match'); - verifyAll(); - }); - } - - // Missing tests: - // * exercize normalization - }); - }); - }); -}); diff --git a/src/test/common/platform/pathUtils.functional.test.ts b/src/test/common/platform/pathUtils.functional.test.ts deleted file mode 100644 index 69b15b53cfb..00000000000 --- a/src/test/common/platform/pathUtils.functional.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -/* eslint-disable */ - -import { expect } from 'chai'; -import { FileSystemPathUtils } from '../../../platform/common/platform/fs-paths.node'; -import { PathUtils } from '../../../platform/common/platform/pathUtils.node'; -import { WINDOWS as IS_WINDOWS } from './utils'; - -suite('FileSystem - PathUtils', () => { - let utils: PathUtils; - let wrapped: FileSystemPathUtils; - setup(() => { - utils = new PathUtils(IS_WINDOWS); - wrapped = FileSystemPathUtils.withDefaults(); - }); - - suite('home', () => { - test('matches wrapped object', () => { - const expected = wrapped.home; - - expect(utils.home).to.equal(expected); - }); - }); - - suite('delimiter', () => { - test('matches wrapped object', () => { - const expected = wrapped.executables.delimiter; - - expect(utils.delimiter).to.be.equal(expected); - }); - }); - - suite('separator', () => { - test('matches wrapped object', () => { - const expected = wrapped.paths.sep; - - expect(utils.separator).to.be.equal(expected); - }); - }); - - suite('getDisplayName', () => { - test('matches wrapped object', () => { - const filename = 'spam.py'; - const expected = wrapped.getDisplayName(filename); - - const display = utils.getDisplayName(filename); - - expect(display).to.equal(expected); - }); - }); - - suite('basename', () => { - test('matches wrapped object', () => { - const filename = 'spam.py'; - const expected = wrapped.paths.basename(filename); - - const basename = utils.basename(filename); - - expect(basename).to.equal(expected); - }); - }); -}); diff --git a/src/test/common/process/logger.unit.test.ts b/src/test/common/process/logger.unit.test.ts index 4d3a959cb66..67adaaea559 100644 --- a/src/test/common/process/logger.unit.test.ts +++ b/src/test/common/process/logger.unit.test.ts @@ -9,21 +9,17 @@ import * as TypeMoq from 'typemoq'; // eslint-disable-next-line @typescript-eslint/no-require-imports import untildify = require('untildify'); -import { PathUtils } from '../../../platform/common/platform/pathUtils.node'; import { ProcessLogger } from '../../../platform/common/process/logger.node'; import { IOutputChannel } from '../../../platform/common/types'; import { Logging } from '../../../platform/common/utils/localize'; -import { getOSType, OSType } from '../../common.node'; /* eslint-disable */ suite('ProcessLogger suite', () => { let outputChannel: TypeMoq.IMock; - let pathUtils: PathUtils; let outputResult: string; suiteSetup(() => { outputChannel = TypeMoq.Mock.ofType(); - pathUtils = new PathUtils(getOSType() === OSType.Windows); }); setup(() => { @@ -39,7 +35,7 @@ suite('ProcessLogger suite', () => { test('Logger displays the process command, arguments and current working directory in the output channel', async () => { const options = { cwd: path.join('debug', 'path') }; - const logger = new ProcessLogger(outputChannel.object, pathUtils); + const logger = new ProcessLogger(outputChannel.object); logger.logProcess('test', ['--foo', '--bar'], options); const expectedResult = `> test --foo --bar\n${Logging.currentWorkingDirectory()} ${options.cwd}\n`; @@ -50,7 +46,7 @@ suite('ProcessLogger suite', () => { test('Logger adds quotes around arguments if they contain spaces', async () => { const options = { cwd: path.join('debug', 'path') }; - const logger = new ProcessLogger(outputChannel.object, pathUtils); + const logger = new ProcessLogger(outputChannel.object); logger.logProcess('test', ['--foo', '--bar', 'import test'], options); const expectedResult = `> test --foo --bar "import test"\n${Logging.currentWorkingDirectory()} ${path.join( @@ -62,7 +58,7 @@ suite('ProcessLogger suite', () => { test('Logger preserves quotes around arguments if they contain spaces', async () => { const options = { cwd: path.join('debug', 'path') }; - const logger = new ProcessLogger(outputChannel.object, pathUtils); + const logger = new ProcessLogger(outputChannel.object); logger.logProcess('test', ['--foo', '--bar', "'import test'"], options); const expectedResult = `> test --foo --bar \'import test\'\n${Logging.currentWorkingDirectory()} ${path.join( @@ -74,7 +70,7 @@ suite('ProcessLogger suite', () => { test('Logger replaces the path/to/home with ~ in the current working directory', async () => { const options = { cwd: path.join(untildify('~'), 'debug', 'path') }; - const logger = new ProcessLogger(outputChannel.object, pathUtils); + const logger = new ProcessLogger(outputChannel.object); logger.logProcess('test', ['--foo', '--bar'], options); const expectedResult = `> test --foo --bar\n${Logging.currentWorkingDirectory()} ${path.join( @@ -87,7 +83,7 @@ suite('ProcessLogger suite', () => { test('Logger replaces the path/to/home with ~ in the command path', async () => { const options = { cwd: path.join('debug', 'path') }; - const logger = new ProcessLogger(outputChannel.object, pathUtils); + const logger = new ProcessLogger(outputChannel.object); logger.logProcess(path.join(untildify('~'), 'test'), ['--foo', '--bar'], options); const expectedResult = `> ${path.join('~', 'test')} --foo --bar\n${Logging.currentWorkingDirectory()} ${ @@ -97,7 +93,7 @@ suite('ProcessLogger suite', () => { }); test("Logger doesn't display the working directory line if there is no options parameter", async () => { - const logger = new ProcessLogger(outputChannel.object, pathUtils); + const logger = new ProcessLogger(outputChannel.object); logger.logProcess(path.join(untildify('~'), 'test'), ['--foo', '--bar']); const expectedResult = `> ${path.join('~', 'test')} --foo --bar\n`; @@ -109,7 +105,7 @@ suite('ProcessLogger suite', () => { test("Logger doesn't display the working directory line if there is no cwd key in the options parameter", async () => { const options = {}; - const logger = new ProcessLogger(outputChannel.object, pathUtils); + const logger = new ProcessLogger(outputChannel.object); logger.logProcess(path.join(untildify('~'), 'test'), ['--foo', '--bar'], options); const expectedResult = `> ${path.join('~', 'test')} --foo --bar\n`; diff --git a/src/test/common/process/pythonDaemon.functional.test.ts b/src/test/common/process/pythonDaemon.functional.test.ts deleted file mode 100644 index 44f34ccda22..00000000000 --- a/src/test/common/process/pythonDaemon.functional.test.ts +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { assert, expect, use } from 'chai'; -import * as chaiPromised from 'chai-as-promised'; -import { ChildProcess, spawn, spawnSync } from 'child_process'; -import * as dedent from 'dedent'; -import * as fs from 'fs-extra'; -import * as os from 'os'; -import * as path from '../../../platform/vscode-path/path'; -import { instance, mock } from 'ts-mockito'; -import { - createMessageConnection, - MessageConnection, - RequestType, - StreamMessageReader, - StreamMessageWriter -} from 'vscode-jsonrpc/node'; -import { IPlatformService } from '../../../platform/common/platform/types'; -import { PythonDaemonExecutionService } from '../../../platform/common/process/pythonDaemon.node'; -import { IPythonExecutionService } from '../../../platform/common/process/types.node'; -import { IDisposable } from '../../../platform/common/types'; -import { EXTENSION_ROOT_DIR } from '../../../platform/constants.node'; -import { PythonEnvironment, PythonVersionInfo } from '../../../platform/pythonEnvironments/info'; -import { parsePythonVersion } from '../../../platform/pythonEnvironments/info/pythonVersion'; -import { isPythonVersion, PYTHON_PATH } from '../../common.node'; -import { createTemporaryFile } from '../../utils/fs'; -use(chaiPromised); - -// eslint-disable-next-line -suite('Daemon', () => { - // Set PYTHONPATH to pickup our module and the jsonrpc modules. - const envPythonPath = `${path.join(EXTENSION_ROOT_DIR, 'pythonFiles')}${path.delimiter}${path.join( - EXTENSION_ROOT_DIR, - 'pythonFiles', - 'lib', - 'python' - )}`; - const env = { PYTHONPATH: envPythonPath, PYTHONUNBUFFERED: '1' }; - let pythonProc: ChildProcess; - let connection: MessageConnection; - let fullyQualifiedPythonPath: string = PYTHON_PATH; - let pythonDaemon: PythonDaemonExecutionService; - let pythonExecutionService: IPythonExecutionService; - let platformService: IPlatformService; - let disposables: IDisposable[] = []; - suiteSetup(() => { - // When running locally. - if (PYTHON_PATH.toLowerCase() === 'python') { - fullyQualifiedPythonPath = spawnSync(PYTHON_PATH, ['-c', 'import sys;print(sys.executable)']) - .stdout.toString() - .trim(); - } - }); - setup(async function () { - if (await isPythonVersion('2.7')) { - // eslint-disable-next-line no-invalid-this - return this.skip(); - } - // Enable the following to log everything going on at pyton end. - // pythonProc = spawn(fullyQualifiedPythonPath, ['-m', 'vscode_datascience_helpers.daemon', '-v', `--log-file=${path.join(EXTENSION_ROOT_DIR, 'test.log')}`], { env }); - pythonProc = spawn(fullyQualifiedPythonPath, ['-m', 'vscode_datascience_helpers.daemon'], { env }); - connection = createMessageConnection( - new StreamMessageReader(pythonProc.stdout!), - new StreamMessageWriter(pythonProc.stdin!) - ); - connection.listen(); - pythonExecutionService = mock(); - platformService = mock(); - pythonDaemon = new PythonDaemonExecutionService( - instance(pythonExecutionService), - instance(platformService), - { path: fullyQualifiedPythonPath } as PythonEnvironment, - pythonProc, - connection - ); - }); - teardown(() => { - pythonProc?.kill(); - if (connection) { - connection.dispose(); - } - pythonDaemon?.dispose(); - disposables.forEach((item) => item.dispose()); - disposables = []; - }); - - async function createPythonFile(source: string): Promise { - const tmpFile = await createTemporaryFile('.py'); - disposables.push({ dispose: () => tmpFile.cleanupCallback() }); - await fs.writeFile(tmpFile.filePath, source, { encoding: 'utf8' }); - return tmpFile.filePath; - } - - test('Ping', async () => { - const data = 'Hello World'; - const request = new RequestType<{ data: string }, { pong: string }, void>('ping'); - const result = await connection.sendRequest(request, { data }); - assert.equal(result.pong, data); - }); - - test('Ping with Unicode', async () => { - const data = 'Hello World-₹-😄'; - const request = new RequestType<{ data: string }, { pong: string }, void>('ping'); - const result = await connection.sendRequest(request, { data }); - assert.equal(result.pong, data); - }); - - test('Interpreter Information', async () => { - type InterpreterInfo = { - versionInfo: PythonVersionInfo; - sysPrefix: string; - sysVersion: string; - }; - const json: InterpreterInfo = JSON.parse( - spawnSync(fullyQualifiedPythonPath, [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'interpreterInfo.py')]) - .stdout.toString() - .trim() - ); - const versionValue = - json.versionInfo.length === 4 - ? `${json.versionInfo.slice(0, 3).join('.')}-${json.versionInfo[3]}` - : json.versionInfo.join('.'); - const expectedVersion = { - path: fullyQualifiedPythonPath, - version: parsePythonVersion(versionValue), - sysVersion: json.sysVersion, - sysPrefix: json.sysPrefix - }; - - const version = await pythonDaemon.getInterpreterInformation(); - - assert.deepEqual(version, expectedVersion); - }); - - test('Executable path', async () => { - const execPath = await pythonDaemon.getExecutablePath(); - - assert.deepEqual(execPath, fullyQualifiedPythonPath); - }); - - async function testModuleInstalled(moduleName: string, expectedToBeInstalled: boolean) { - await assert.eventually.equal(pythonDaemon.isModuleInstalled(moduleName), expectedToBeInstalled); - } - - test("'pip' module is installed", async () => testModuleInstalled('pip', true)); - test("'unittest' module is installed", async () => testModuleInstalled('unittest', true)); - test("'VSCode-Python-Rocks' module is not Installed", async () => - testModuleInstalled('VSCode-Python-Rocks', false)); - - test('Execute a file and capture stdout (with unicode)', async () => { - const source = dedent` - import sys - sys.stdout.write("HELLO WORLD-₹-😄") - `; - const fileToExecute = await createPythonFile(source); - const output = await pythonDaemon.exec([fileToExecute], {}); - - assert.isUndefined(output.stderr); - assert.deepEqual(output.stdout, 'HELLO WORLD-₹-😄'); - }); - - test('Execute a file and capture stderr (with unicode)', async () => { - const source = dedent` - import sys - sys.stderr.write("HELLO WORLD-₹-😄") - `; - const fileToExecute = await createPythonFile(source); - const output = await pythonDaemon.exec([fileToExecute], {}); - - assert.isUndefined(output.stdout); - assert.deepEqual(output.stderr, 'HELLO WORLD-₹-😄'); - }); - - test('Execute a file with arguments', async () => { - const source = dedent` - import sys - sys.stdout.write(sys.argv[1]) - `; - const fileToExecute = await createPythonFile(source); - const output = await pythonDaemon.exec([fileToExecute, 'HELLO WORLD'], {}); - - assert.isUndefined(output.stderr); - assert.equal(output.stdout, 'HELLO WORLD'); - }); - - test('Execute a file with custom cwd', async () => { - const source = dedent` - import os - print(os.getcwd()) - `; - const fileToExecute = await createPythonFile(source); - const output1 = await pythonDaemon.exec([fileToExecute, 'HELLO WORLD'], { cwd: EXTENSION_ROOT_DIR }); - - assert.isUndefined(output1.stderr); - assert.equal(output1.stdout.trim(), EXTENSION_ROOT_DIR); - - const output2 = await pythonDaemon.exec([fileToExecute, 'HELLO WORLD'], { cwd: __dirname }); - - assert.isUndefined(output2.stderr); - assert.equal(output2.stdout.trim(), __dirname); - }); - - test('Execute a file and capture stdout & stderr', async () => { - const source = dedent` - import sys - sys.stdout.write("HELLO WORLD-₹-😄") - sys.stderr.write("FOO BAR-₹-😄") - `; - const fileToExecute = await createPythonFile(source); - const output = await pythonDaemon.exec([fileToExecute, 'HELLO WORLD'], {}); - - assert.equal(output.stdout, 'HELLO WORLD-₹-😄'); - assert.equal(output.stderr, 'FOO BAR-₹-😄'); - }); - - test('Execute a file and handle error', async () => { - const source = dedent` - import sys - raise Exception("KABOOM") - `; - const fileToExecute = await createPythonFile(source); - const promise = pythonDaemon.exec([fileToExecute], {}); - await expect(promise).to.eventually.be.rejectedWith('KABOOM'); - }); - - test('Execute a file with custom env variable', async () => { - const source = dedent` - import os - print(os.getenv("VSC_HELLO_CUSTOM", "NONE")) - `; - const fileToExecute = await createPythonFile(source); - - const output1 = await pythonDaemon.exec([fileToExecute], {}); - - // Confirm there's no custom variable. - assert.equal(output1.stdout.trim(), 'NONE'); - - // Confirm setting the varible works. - const output2 = await pythonDaemon.exec([fileToExecute], { env: { VSC_HELLO_CUSTOM: 'wow' } }); - assert.equal(output2.stdout.trim(), 'wow'); - }); - - test('Execute simple module', async () => { - const pipVersion = spawnSync(fullyQualifiedPythonPath, ['-c', 'import pip;print(pip.__version__)']) - .stdout.toString() - .trim(); - - const output = await pythonDaemon.execModule('pip', ['--version'], {}); - - assert.isUndefined(output.stderr); - assert.equal(output.stdout.trim(), pipVersion); - }); - - test('Execute a file and stream output', async () => { - const source = dedent` - import sys - import time - for i in range(5): - print(i) - time.sleep(1) - `; - const fileToExecute = await createPythonFile(source); - const output = pythonDaemon.execObservable([fileToExecute], {}); - const outputsReceived: string[] = []; - await new Promise((resolve, reject) => { - output.out.subscribe((out) => outputsReceived.push(out.out.trim()), reject, resolve); - }); - assert.deepEqual( - outputsReceived.filter((item) => item.length > 0), - ['0', '1', '2', '3', '4'] - ); - }).timeout(10_000); - - test('Execute a file and throw exception if stderr is not empty', async () => { - const fileToExecute = await createPythonFile(['import sys', 'sys.stderr.write("KABOOM")'].join(os.EOL)); - const promise = pythonDaemon.exec([fileToExecute], { throwOnStdErr: true }); - await expect(promise).to.eventually.be.rejectedWith('KABOOM'); - }); - - test('Execute a file and throw exception if stderr is not empty when streaming output', async () => { - const source = dedent` - import sys - import time - time.sleep(1) - sys.stderr.write("KABOOM") - sys.stderr.flush() - time.sleep(1) - `; - const fileToExecute = await createPythonFile(source); - const output = pythonDaemon.execObservable([fileToExecute], { throwOnStdErr: true }); - const outputsReceived: string[] = []; - const promise = new Promise((resolve, reject) => { - output.out.subscribe((out) => outputsReceived.push(out.out.trim()), reject, resolve); - }); - await expect(promise).to.eventually.be.rejectedWith('KABOOM'); - }).timeout(3_000); -}); diff --git a/src/test/common/process/pythonDaemonPool.functional.test.ts b/src/test/common/process/pythonDaemonPool.functional.test.ts deleted file mode 100644 index 11531056947..00000000000 --- a/src/test/common/process/pythonDaemonPool.functional.test.ts +++ /dev/null @@ -1,473 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { assert, expect, use } from 'chai'; -import * as chaiPromised from 'chai-as-promised'; -import { spawn, spawnSync } from 'child_process'; -import * as dedent from 'dedent'; -import { EventEmitter } from 'events'; -import * as fs from 'fs-extra'; -import * as os from 'os'; -import * as path from '../../../platform/vscode-path/path'; -import { Observable } from 'rxjs/Observable'; -import * as sinon from 'sinon'; -import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; -import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from 'vscode-jsonrpc/node'; -import { JupyterDaemonModule } from '../../../platform/common/constants'; -import { IPlatformService } from '../../../platform/common/platform/types'; -import { ProcessLogger } from '../../../platform/common/process/logger.node'; -import { PythonDaemonExecutionServicePool } from '../../../platform/common/process/pythonDaemonPool.node'; -import { - IProcessLogger, - IPythonDaemonExecutionService, - IPythonExecutionService, - ObservableExecutionResult, - Output -} from '../../../platform/common/process/types.node'; -import { IDisposable } from '../../../platform/common/types'; -import { sleep } from '../../../platform/common/utils/async'; -import { noop } from '../../../platform/common/utils/misc'; -import { EXTENSION_ROOT_DIR } from '../../../platform/constants.node'; -import { PythonEnvironment, PythonVersionInfo } from '../../../platform/pythonEnvironments/info'; -import { parsePythonVersion } from '../../../platform/pythonEnvironments/info/pythonVersion'; -import { isPythonVersion, PYTHON_PATH, waitForCondition } from '../../common.node'; -import { createTemporaryFile } from '../../utils/fs'; -use(chaiPromised); - -/* eslint-disable */ -suite('Daemon - Python Daemon Pool', () => { - // Set PYTHONPATH to pickup our module and the jsonrpc modules. - const envPythonPath = `${path.join(EXTENSION_ROOT_DIR, 'pythonFiles')}${path.delimiter}${path.join( - EXTENSION_ROOT_DIR, - 'pythonFiles', - 'lib', - 'python' - )}`; - const env = { PYTHONPATH: envPythonPath, PYTHONUNBUFFERED: '1' }; - let fullyQualifiedPythonPath: string = PYTHON_PATH; - let pythonDaemonPool: PythonDaemonExecutionServicePool; - let pythonExecutionService: IPythonExecutionService; - let platformService: IPlatformService; - let disposables: IDisposable[] = []; - let createDaemonServicesSpy: sinon.SinonSpy<[], Promise>; - let logger: IProcessLogger; - class DaemonPool extends PythonDaemonExecutionServicePool { - // eslint-disable-next-line - public createDaemonService(): Promise { - return super.createDaemonService(); - } - } - suiteSetup(() => { - // When running locally. - if (PYTHON_PATH.toLowerCase() === 'python') { - fullyQualifiedPythonPath = spawnSync(PYTHON_PATH, ['-c', 'import sys;print(sys.executable)']) - .stdout.toString() - .trim(); - } - }); - setup(async function () { - if (await isPythonVersion('2.7')) { - // eslint-disable-next-line no-invalid-this - return this.skip(); - } - logger = mock(ProcessLogger); - createDaemonServicesSpy = sinon.spy(DaemonPool.prototype, 'createDaemonService'); - pythonExecutionService = mock(); - platformService = mock(); - when( - pythonExecutionService.execModuleObservable('vscode_datascience_helpers.daemon', anything(), anything()) - ).thenCall(() => { - const pythonProc = spawn(fullyQualifiedPythonPath, ['-m', 'vscode_datascience_helpers.daemon'], { env }); - const connection = createMessageConnection( - new StreamMessageReader(pythonProc.stdout), - new StreamMessageWriter(pythonProc.stdin) - ); - connection.listen(); - disposables.push({ - dispose: () => { - pythonProc.kill(); - } - }); - disposables.push({ dispose: () => connection.dispose() }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return { proc: pythonProc, dispose: noop, out: undefined as any }; - }); - const options = { - interpreter: { path: fullyQualifiedPythonPath } as PythonEnvironment, - daemonModule: JupyterDaemonModule, - daemonCount: 2, - observableDaemonCount: 1 - }; - pythonDaemonPool = new DaemonPool( - logger, - [], - options, - instance(pythonExecutionService), - instance(platformService), - {}, - 100 - ); - await pythonDaemonPool.initialize(); - disposables.push(pythonDaemonPool); - }); - teardown(() => { - sinon.restore(); - disposables.forEach((item) => item.dispose()); - disposables = []; - }); - async function getStdOutFromObservable(output: ObservableExecutionResult) { - return new Promise((resolve, reject) => { - const data: string[] = []; - output.out.subscribe( - (out) => data.push(out.out.trim()), - reject, - () => resolve(data.join('')) - ); - }); - } - - async function createPythonFile(source: string): Promise { - const tmpFile = await createTemporaryFile('.py'); - disposables.push({ dispose: () => tmpFile.cleanupCallback() }); - await fs.writeFile(tmpFile.filePath, source, { encoding: 'utf8' }); - return tmpFile.filePath; - } - - test('Interpreter Information', async () => { - type InterpreterInfo = { - versionInfo: PythonVersionInfo; - sysPrefix: string; - sysVersion: string; - }; - const json: InterpreterInfo = JSON.parse( - spawnSync(fullyQualifiedPythonPath, [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'interpreterInfo.py')]) - .stdout.toString() - .trim() - ); - const versionValue = - json.versionInfo.length === 4 - ? `${json.versionInfo.slice(0, 3).join('.')}-${json.versionInfo[3]}` - : json.versionInfo.join('.'); - const expectedVersion = { - path: fullyQualifiedPythonPath, - version: parsePythonVersion(versionValue), - sysVersion: json.sysVersion, - sysPrefix: json.sysPrefix - }; - - const version = await pythonDaemonPool.getInterpreterInformation(); - - assert.deepEqual(version, expectedVersion); - }); - - test('Executable path', async () => { - const execPath = await pythonDaemonPool.getExecutablePath(); - - assert.deepEqual(execPath, fullyQualifiedPythonPath); - }); - - async function testModuleInstalled(moduleName: string, expectedToBeInstalled: boolean) { - await assert.eventually.equal(pythonDaemonPool.isModuleInstalled(moduleName), expectedToBeInstalled); - } - - test("'pip' module is installed", async () => testModuleInstalled('pip', true)); - test("'unittest' module is installed", async () => testModuleInstalled('unittest', true)); - test("'VSCode-Python-Rocks' module is not Installed", async () => - testModuleInstalled('VSCode-Python-Rocks', false)); - - test('Execute a file and capture stdout (with unicode)', async () => { - const source = dedent` - import sys - sys.stdout.write("HELLO WORLD-₹-😄") - `; - const fileToExecute = await createPythonFile(source); - const output = await pythonDaemonPool.exec([fileToExecute], {}); - - assert.isUndefined(output.stderr); - assert.deepEqual(output.stdout, 'HELLO WORLD-₹-😄'); - }); - - test('Execute a file and capture stderr (with unicode)', async () => { - const source = dedent` - import sys - sys.stderr.write("HELLO WORLD-₹-😄") - `; - const fileToExecute = await createPythonFile(source); - const output = await pythonDaemonPool.exec([fileToExecute], {}); - - assert.isUndefined(output.stdout); - assert.deepEqual(output.stderr, 'HELLO WORLD-₹-😄'); - }); - - test('Execute a file with arguments', async () => { - const source = dedent` - import sys - sys.stdout.write(sys.argv[1]) - `; - const fileToExecute = await createPythonFile(source); - const output = await pythonDaemonPool.exec([fileToExecute, 'HELLO WORLD'], {}); - - assert.isUndefined(output.stderr); - assert.equal(output.stdout, 'HELLO WORLD'); - }); - - test('Execute a file with custom cwd', async () => { - const source = dedent` - import os - print(os.getcwd()) - `; - const fileToExecute = await createPythonFile(source); - const output1 = await pythonDaemonPool.exec([fileToExecute, 'HELLO WORLD'], { cwd: EXTENSION_ROOT_DIR }); - - assert.isUndefined(output1.stderr); - assert.equal(output1.stdout.trim(), EXTENSION_ROOT_DIR); - - const output2 = await pythonDaemonPool.exec([fileToExecute, 'HELLO WORLD'], { cwd: __dirname }); - - assert.isUndefined(output2.stderr); - assert.equal(output2.stdout.trim(), __dirname); - }); - - test('Execute a file and capture stdout & stderr', async () => { - const source = dedent` - import sys - sys.stdout.write("HELLO WORLD-₹-😄") - sys.stderr.write("FOO BAR-₹-😄") - `; - const fileToExecute = await createPythonFile(source); - const output = await pythonDaemonPool.exec([fileToExecute, 'HELLO WORLD'], {}); - - assert.equal(output.stdout, 'HELLO WORLD-₹-😄'); - assert.equal(output.stderr, 'FOO BAR-₹-😄'); - }); - - test('Execute a file and handle error', async () => { - const source = dedent` - import sys - raise Exception("KABOOM") - `; - const fileToExecute = await createPythonFile(source); - const promise = pythonDaemonPool.exec([fileToExecute], {}); - await expect(promise).to.eventually.be.rejectedWith('KABOOM'); - }); - - test('Execute a file with custom env variable', async () => { - const source = dedent` - import os - print(os.getenv("VSC_HELLO_CUSTOM", "NONE")) - `; - const fileToExecute = await createPythonFile(source); - - const output1 = await pythonDaemonPool.exec([fileToExecute], {}); - - // Confirm there's no custom variable. - assert.equal(output1.stdout.trim(), 'NONE'); - - // Confirm setting the varible works. - const output2 = await pythonDaemonPool.exec([fileToExecute], { env: { VSC_HELLO_CUSTOM: 'wow' } }); - assert.equal(output2.stdout.trim(), 'wow'); - }); - - test('Execute simple module', async () => { - const pipVersion = spawnSync(fullyQualifiedPythonPath, ['-c', 'import pip;print(pip.__version__)']) - .stdout.toString() - .trim(); - - const output = await pythonDaemonPool.execModule('pip', ['--version'], {}); - - assert.isUndefined(output.stderr); - assert.equal(output.stdout.trim(), pipVersion); - }); - - test('Execute a file and stream output', async () => { - const source = dedent` - import sys - import time - for i in range(5): - print(i) - time.sleep(0.1) - `; - const fileToExecute = await createPythonFile(source); - const output = pythonDaemonPool.execObservable([fileToExecute], {}); - const outputsReceived: string[] = []; - await new Promise((resolve, reject) => { - output.out.subscribe((out) => outputsReceived.push(out.out.trim()), reject, resolve); - }); - assert.deepEqual( - outputsReceived.filter((item) => item.length > 0), - ['0', '1', '2', '3', '4'] - ); - }).timeout(5_000); - - test('Execute a file and throw exception if stderr is not empty', async () => { - const fileToExecute = await createPythonFile(['import sys', 'sys.stderr.write("KABOOM")'].join(os.EOL)); - const promise = pythonDaemonPool.exec([fileToExecute], { throwOnStdErr: true }); - await expect(promise).to.eventually.be.rejectedWith('KABOOM'); - }); - - test('Execute a file and throw exception if stderr is not empty when streaming output', async () => { - const source = dedent` - import sys - import time - time.sleep(0.1) - sys.stderr.write("KABOOM") - sys.stderr.flush() - time.sleep(0.1) - `; - const fileToExecute = await createPythonFile(source); - const output = pythonDaemonPool.execObservable([fileToExecute], { throwOnStdErr: true }); - const outputsReceived: string[] = []; - const promise = new Promise((resolve, reject) => { - output.out.subscribe((out) => outputsReceived.push(out.out.trim()), reject, resolve); - }); - await expect(promise).to.eventually.be.rejectedWith('KABOOM'); - }).timeout(5_000); - test('If executing a file takes time, then ensure we use another daemon', async () => { - const source = dedent` - import os - import time - time.sleep(0.2) - print(os.getpid()) - `; - const fileToExecute = await createPythonFile(source); - // When using the python execution service, return a bogus value. - when(pythonExecutionService.execObservable(deepEqual([fileToExecute]), anything())).thenCall(() => { - const observable = new Observable>((s) => { - s.next({ out: 'mypid', source: 'stdout' }); - s.complete(); - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return { proc: new EventEmitter() as any, dispose: noop, out: observable }; - }); - // This will use a damon. - const output1 = pythonDaemonPool.execObservable([fileToExecute], {}); - // These two will use a python execution service. - const output2 = pythonDaemonPool.execObservable([fileToExecute], {}); - const output3 = pythonDaemonPool.execObservable([fileToExecute], {}); - const [result1, result2, result3] = await Promise.all([ - getStdOutFromObservable(output1), - getStdOutFromObservable(output2), - getStdOutFromObservable(output3) - ]); - - // Two process ids are used to run the code (one process for a daemon, another for bogus puthon process). - expect(result1).to.not.equal('mypid'); - expect(result2).to.equal('mypid'); - expect(result3).to.equal('mypid'); - verify(pythonExecutionService.execObservable(deepEqual([fileToExecute]), anything())).twice(); - }).timeout(3_000); - test('Ensure to re-use the same daemon & it goes back into the pool (for observables)', async () => { - const source = dedent` - import os - print(os.getpid()) - `; - const fileToExecute = await createPythonFile(source); - // This will use a damon. - const output1 = await getStdOutFromObservable(pythonDaemonPool.execObservable([fileToExecute], {})); - // Wait for daemon to go into the pool. - await sleep(100); - // This will use a damon. - const output2 = await getStdOutFromObservable(pythonDaemonPool.execObservable([fileToExecute], {})); - // Wait for daemon to go into the pool. - await sleep(100); - // This will use a damon. - const output3 = await getStdOutFromObservable(pythonDaemonPool.execObservable([fileToExecute], {})); - - // The pid for all processes is the same. - // This means we're re-using the same daemon (process). - expect(output1).to.equal(output2); - expect(output1).to.equal(output3); - }).timeout(3_000); - test('Ensure two different daemons are used to execute code', async () => { - const source = dedent` - import os - import time - time.sleep(0.2) - print(os.getpid()) - `; - const fileToExecute = await createPythonFile(source); - - const [output1, output2] = await Promise.all([ - pythonDaemonPool.exec([fileToExecute], {}), - pythonDaemonPool.exec([fileToExecute], {}) - ]); - - // The pid for both processes will be different. - // This means we're running both in two separate daemons. - expect(output1.stdout).to.not.equal(output2.stdout); - }); - test('Ensure to create a new daemon if one dies', async () => { - // Get pids of the 2 daemons. - const daemonsCreated = createDaemonServicesSpy.callCount; - const source1 = dedent` - import os - import time - time.sleep(0.1) - print(os.getpid()) - `; - const fileToExecute1 = await createPythonFile(source1); - - let [pid1, pid2] = await Promise.all([ - pythonDaemonPool.exec([fileToExecute1], {}).then((out) => out.stdout.trim()), - pythonDaemonPool.exec([fileToExecute1], {}).then((out) => out.stdout.trim()) - ]); - - const processesUsedToRunCode = new Set(); - processesUsedToRunCode.add(pid1); - processesUsedToRunCode.add(pid2); - - // We should have two distinct process ids, that was used to run our code. - expect(processesUsedToRunCode.size).to.equal(2); - - // Ok, wait for daemons to go back into the pool. - await sleep(1); - - // Kill one of the daemons (let it die while running some code). - const source2 = dedent` - import os - os.kill(os.getpid(), 1) - `; - const fileToExecute2 = await createPythonFile(source2); - [pid1, pid2] = await Promise.all([ - pythonDaemonPool - .exec([fileToExecute1], {}) - .then((out) => out.stdout.trim()) - .catch(() => 'FAILED'), - pythonDaemonPool - .exec([fileToExecute2], {}) - .then((out) => out.stdout.trim()) - .catch(() => 'FAILED') - ]); - - // Confirm that one of the executions failed due to an error. - expect(pid1 === 'FAILED' ? pid1 : pid2).to.equal('FAILED'); - // Keep track of the process that worked. - processesUsedToRunCode.add(pid1 === 'FAILED' ? pid2 : pid1); - // We should still have two distinct process ids (one of the eralier processes died). - expect(processesUsedToRunCode.size).to.equal(2); - - // Wait for a new daemon to be created. - await waitForCondition( - async () => createDaemonServicesSpy.callCount - daemonsCreated === 1, - 5_000, - 'Failed to create a new daemon' - ); - - // Confirm we have two daemons by checking the Pids again. - // One of them will be new. - [pid1, pid2] = await Promise.all([ - pythonDaemonPool.exec([fileToExecute1], {}).then((out) => out.stdout.trim()), - pythonDaemonPool.exec([fileToExecute1], {}).then((out) => out.stdout.trim()) - ]); - - // Keep track of the pids. - processesUsedToRunCode.add(pid1); - processesUsedToRunCode.add(pid2); - - // Confirm we have a total of three process ids (for 3 daemons). - // 2 for earlier, then one died and a new one was created. - expect(processesUsedToRunCode.size).to.be.greaterThan(2); - }).timeout(10_000); -}); diff --git a/src/test/common/process/pythonDaemonPool.unit.test.ts b/src/test/common/process/pythonDaemonPool.unit.test.ts index ca9fc744452..c9f06f73aef 100644 --- a/src/test/common/process/pythonDaemonPool.unit.test.ts +++ b/src/test/common/process/pythonDaemonPool.unit.test.ts @@ -11,6 +11,7 @@ import { EventEmitter } from 'events'; import { Observable } from 'rxjs/Observable'; import * as sinon from 'sinon'; import { anything, instance, mock, reset, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; import { MessageConnection } from 'vscode-jsonrpc'; import { IPlatformService } from '../../../platform/common/platform/types'; import { ProcessLogger } from '../../../platform/common/process/logger.node'; @@ -95,7 +96,7 @@ suite('Daemon - Python Daemon Pool', () => { const pool = new DaemonPool( logger, [], - { interpreter: { path: 'py.exe' } as PythonEnvironment }, + { interpreter: { uri: Uri.file('py.exe') } as PythonEnvironment }, instance(pythonExecService), instance(platformService), undefined @@ -111,7 +112,11 @@ suite('Daemon - Python Daemon Pool', () => { const pool = new DaemonPool( logger, [], - { daemonCount: 5, observableDaemonCount: 3, interpreter: { path: 'py.exe' } as PythonEnvironment }, + { + daemonCount: 5, + observableDaemonCount: 3, + interpreter: { uri: Uri.file('py.exe') } as PythonEnvironment + }, instance(pythonExecService), instance(platformService), undefined @@ -130,7 +135,11 @@ suite('Daemon - Python Daemon Pool', () => { const pool = new DaemonPool( logger, [], - { daemonCount: 5, observableDaemonCount: 3, interpreter: { path: 'py.exe' } as PythonEnvironment }, + { + daemonCount: 5, + observableDaemonCount: 3, + interpreter: { uri: Uri.file('py.exe') } as PythonEnvironment + }, instance(pythonExecService), instance(platformService), undefined @@ -159,7 +168,11 @@ suite('Daemon - Python Daemon Pool', () => { const pool = new DaemonPool( logger, [], - { daemonCount: 1, observableDaemonCount: 1, interpreter: { path: 'py.exe' } as PythonEnvironment }, + { + daemonCount: 1, + observableDaemonCount: 1, + interpreter: { uri: Uri.file('py.exe') } as PythonEnvironment + }, instance(pythonExecService), instance(platformService), undefined @@ -211,7 +224,11 @@ suite('Daemon - Python Daemon Pool', () => { const pool = new DaemonPool( logger, [], - { daemonCount: 2, observableDaemonCount: 1, interpreter: { path: 'py.exe' } as PythonEnvironment }, + { + daemonCount: 2, + observableDaemonCount: 1, + interpreter: { uri: Uri.file('py.exe') } as PythonEnvironment + }, instance(pythonExecService), instance(platformService), undefined @@ -290,7 +307,11 @@ suite('Daemon - Python Daemon Pool', () => { const pool = new DaemonPool( logger, [], - { daemonCount: 1, observableDaemonCount: 1, interpreter: { path: 'py.exe' } as PythonEnvironment }, + { + daemonCount: 1, + observableDaemonCount: 1, + interpreter: { uri: Uri.file('py.exe') } as PythonEnvironment + }, instance(pythonExecService), instance(platformService), undefined diff --git a/src/test/common/process/pythonEnvironment.unit.test.ts b/src/test/common/process/pythonEnvironment.unit.test.ts index 9cbc7cbf30b..afb472f1fa5 100644 --- a/src/test/common/process/pythonEnvironment.unit.test.ts +++ b/src/test/common/process/pythonEnvironment.unit.test.ts @@ -7,6 +7,7 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; import { IFileSystem } from '../../../platform/common/platform/types.node'; import { createCondaEnv, @@ -21,7 +22,7 @@ use(chaiAsPromised); suite('PythonEnvironment', () => { let processService: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; - const pythonPath = 'path/to/python'; + const pythonPath = Uri.file('path/to/python'); setup(() => { processService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); @@ -38,15 +39,11 @@ suite('PythonEnvironment', () => { processService .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const result = await env.getInterpreterInformation(); const expectedResult = { - path: pythonPath, + uri: pythonPath, version: new SemVer('3.7.5-candidate'), sysPrefix: json.sysPrefix, sysVersion: undefined @@ -65,15 +62,11 @@ suite('PythonEnvironment', () => { processService .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const result = await env.getInterpreterInformation(); const expectedResult = { - path: pythonPath, + uri: pythonPath, version: new SemVer('3.7.5'), sysPrefix: json.sysPrefix, sysVersion: undefined @@ -95,15 +88,11 @@ suite('PythonEnvironment', () => { processService .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve({ stdout: JSON.stringify(json) })); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const result = await env.getInterpreterInformation(); const expectedResult = { - path: pythonPath, + uri: pythonPath, version: new SemVer('3.7.5-candidate'), sysPrefix: json.sysPrefix, sysVersion: undefined @@ -120,11 +109,7 @@ suite('PythonEnvironment', () => { .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // eslint-disable-next-line @typescript-eslint/no-explicit-any .returns(() => Promise.reject(new Error('timed out'))); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const result = await env.getInterpreterInformation(); @@ -138,11 +123,7 @@ suite('PythonEnvironment', () => { processService .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve({ stdout: 'bad json' })); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const result = await env.getInterpreterInformation(); @@ -150,12 +131,8 @@ suite('PythonEnvironment', () => { }); test('getExecutablePath should return pythonPath if pythonPath is a file', async () => { - fileSystem.setup((f) => f.localFileExists(pythonPath)).returns(() => Promise.resolve(true)); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + fileSystem.setup((f) => f.localFileExists(pythonPath.fsPath)).returns(() => Promise.resolve(true)); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const result = await env.getExecutablePath(); @@ -164,34 +141,29 @@ suite('PythonEnvironment', () => { test('getExecutablePath should not return pythonPath if pythonPath is not a file', async () => { const executablePath = 'path/to/dummy/executable'; - fileSystem.setup((f) => f.localFileExists(pythonPath)).returns(() => Promise.resolve(false)); + fileSystem.setup((f) => f.localFileExists(pythonPath.fsPath)).returns(() => Promise.resolve(false)); const argv = ['-c', 'import sys;print(sys.executable)']; processService - .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .setup((p) => p.exec(pythonPath.fsPath, argv, { throwOnStdErr: true })) .returns(() => Promise.resolve({ stdout: executablePath })); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const result = await env.getExecutablePath(); - expect(result).to.equal(executablePath, "getExecutablePath() sbould not return pythonPath if it's not a file"); + expect(result.path.slice(1)).to.equal( + executablePath, + "getExecutablePath() sbould not return pythonPath if it's not a file" + ); }); test('getExecutablePath should throw if the result of exec() writes to stderr', async () => { const stderr = 'bar'; - fileSystem.setup((f) => f.localFileExists(pythonPath)).returns(() => Promise.resolve(false)); + fileSystem.setup((f) => f.localFileExists(pythonPath.fsPath)).returns(() => Promise.resolve(false)); const argv = ['-c', 'import sys;print(sys.executable)']; processService - .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .setup((p) => p.exec(pythonPath.fsPath, argv, { throwOnStdErr: true })) .returns(() => Promise.reject(new StdErrError(stderr))); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const result = env.getExecutablePath(); @@ -202,14 +174,10 @@ suite('PythonEnvironment', () => { const moduleName = 'foo'; const argv = ['-c', `import ${moduleName}`]; processService - .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .setup((p) => p.exec(pythonPath.fsPath, argv, { throwOnStdErr: true })) .returns(() => Promise.resolve({ stdout: '' })) .verifiable(TypeMoq.Times.once()); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); await env.isModuleInstalled(moduleName); @@ -220,13 +188,9 @@ suite('PythonEnvironment', () => { const moduleName = 'foo'; const argv = ['-c', `import ${moduleName}`]; processService - .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .setup((p) => p.exec(pythonPath.fsPath, argv, { throwOnStdErr: true })) .returns(() => Promise.resolve({ stdout: '' })); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const result = await env.isModuleInstalled(moduleName); @@ -237,13 +201,9 @@ suite('PythonEnvironment', () => { const moduleName = 'foo'; const argv = ['-c', `import ${moduleName}`]; processService - .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .setup((p) => p.exec(pythonPath.fsPath, argv, { throwOnStdErr: true })) .returns(() => Promise.reject(new StdErrError('bar'))); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const result = await env.isModuleInstalled(moduleName); @@ -252,16 +212,12 @@ suite('PythonEnvironment', () => { test('getExecutionInfo should return pythonPath and the execution arguments as is', () => { const args = ['-a', 'b', '-c']; - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const result = env.getExecutionInfo(args); expect(result).to.deep.equal( - { command: pythonPath, args, python: [pythonPath], pythonExecutable: pythonPath }, + { command: pythonPath.fsPath, args, python: [pythonPath.fsPath], pythonExecutable: pythonPath.fsPath }, 'getExecutionInfo should return pythonPath and the command and execution arguments as is' ); }); @@ -271,7 +227,7 @@ suite('CondaEnvironment', () => { let processService: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; const args = ['-a', 'b', '-c']; - const pythonPath = 'path/to/python'; + const pythonPath = Uri.file('path/to/python'); const condaFile = 'path/to/conda'; setup(() => { @@ -284,7 +240,7 @@ suite('CondaEnvironment', () => { const env = createCondaEnv( condaFile, condaInfo, - { path: pythonPath } as PythonEnvironment, + { uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object ); @@ -304,7 +260,7 @@ suite('CondaEnvironment', () => { const env = createCondaEnv( condaFile, condaInfo, - { path: pythonPath } as PythonEnvironment, + { uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object ); @@ -320,12 +276,17 @@ suite('CondaEnvironment', () => { }); test('getExecutionObservableInfo with a named environment should return execution info using pythonPath only', () => { - const expected = { command: pythonPath, args, python: [pythonPath], pythonExecutable: pythonPath }; + const expected = { + command: pythonPath.fsPath, + args, + python: [pythonPath.fsPath], + pythonExecutable: pythonPath.fsPath + }; const condaInfo = { name: 'foo', path: 'bar', version: undefined }; const env = createCondaEnv( condaFile, condaInfo, - { path: pythonPath } as PythonEnvironment, + { uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object ); @@ -336,12 +297,17 @@ suite('CondaEnvironment', () => { }); test('getExecutionObservableInfo with a non-named environment should return execution info using pythonPath only', () => { - const expected = { command: pythonPath, args, python: [pythonPath], pythonExecutable: pythonPath }; + const expected = { + command: pythonPath.fsPath, + args, + python: [pythonPath.fsPath], + pythonExecutable: pythonPath.fsPath + }; const condaInfo = { name: '', path: 'bar', version: undefined }; const env = createCondaEnv( condaFile, condaInfo, - { path: pythonPath } as PythonEnvironment, + { uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object ); @@ -354,14 +320,14 @@ suite('CondaEnvironment', () => { suite('WindowsStoreEnvironment', () => { let processService: TypeMoq.IMock; - const pythonPath = 'foo'; + const pythonPath = Uri.file('foo'); setup(() => { processService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); }); test('Should return pythonPath if it is the path to the windows store interpreter', async () => { - const env = createWindowsStoreEnv({ path: pythonPath } as PythonEnvironment, processService.object); + const env = createWindowsStoreEnv({ uri: pythonPath } as PythonEnvironment, processService.object); const executablePath = await env.getExecutablePath(); diff --git a/src/test/common/process/pythonProcess.unit.test.ts b/src/test/common/process/pythonProcess.unit.test.ts index 4d03eb14024..10b3e5ca56f 100644 --- a/src/test/common/process/pythonProcess.unit.test.ts +++ b/src/test/common/process/pythonProcess.unit.test.ts @@ -4,6 +4,7 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; import { IFileSystem } from '../../../platform/common/platform/types.node'; import { createPythonEnv } from '../../../platform/common/process/pythonEnvironment.node'; import { createPythonProcessService } from '../../../platform/common/process/pythonProcess.node'; @@ -17,7 +18,7 @@ use(chaiAsPromised); suite('PythonProcessService', () => { let processService: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; - const pythonPath = 'path/to/python'; + const pythonPath = Uri.file('path/to/python'); setup(() => { processService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); @@ -35,17 +36,13 @@ suite('PythonProcessService', () => { noop(); } }; - processService.setup((p) => p.execObservable(pythonPath, args, options)).returns(() => observable); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + processService.setup((p) => p.execObservable(pythonPath.fsPath, args, options)).returns(() => observable); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const procs = createPythonProcessService(processService.object, env); const result = procs.execObservable(args, options); - processService.verify((p) => p.execObservable(pythonPath, args, options), TypeMoq.Times.once()); + processService.verify((p) => p.execObservable(pythonPath.fsPath, args, options), TypeMoq.Times.once()); expect(result).to.be.equal(observable, 'execObservable should return an observable'); }); @@ -62,17 +59,15 @@ suite('PythonProcessService', () => { noop(); } }; - processService.setup((p) => p.execObservable(pythonPath, expectedArgs, options)).returns(() => observable); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + processService + .setup((p) => p.execObservable(pythonPath.fsPath, expectedArgs, options)) + .returns(() => observable); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const procs = createPythonProcessService(processService.object, env); const result = procs.execModuleObservable(moduleName, args, options); - processService.verify((p) => p.execObservable(pythonPath, expectedArgs, options), TypeMoq.Times.once()); + processService.verify((p) => p.execObservable(pythonPath.fsPath, expectedArgs, options), TypeMoq.Times.once()); expect(result).to.be.equal(observable, 'execModuleObservable should return an observable'); }); @@ -80,17 +75,15 @@ suite('PythonProcessService', () => { const args = ['-a', 'b', '-c']; const options = {}; const stdout = 'foo'; - processService.setup((p) => p.exec(pythonPath, args, options)).returns(() => Promise.resolve({ stdout })); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + processService + .setup((p) => p.exec(pythonPath.fsPath, args, options)) + .returns(() => Promise.resolve({ stdout })); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const procs = createPythonProcessService(processService.object, env); const result = await procs.exec(args, options); - processService.verify((p) => p.exec(pythonPath, args, options), TypeMoq.Times.once()); + processService.verify((p) => p.exec(pythonPath.fsPath, args, options), TypeMoq.Times.once()); expect(result.stdout).to.be.equal(stdout, 'exec should return the content of stdout'); }); @@ -101,18 +94,14 @@ suite('PythonProcessService', () => { const options = {}; const stdout = 'bar'; processService - .setup((p) => p.exec(pythonPath, expectedArgs, options)) + .setup((p) => p.exec(pythonPath.fsPath, expectedArgs, options)) .returns(() => Promise.resolve({ stdout })); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const procs = createPythonProcessService(processService.object, env); const result = await procs.execModule(moduleName, args, options); - processService.verify((p) => p.exec(pythonPath, expectedArgs, options), TypeMoq.Times.once()); + processService.verify((p) => p.exec(pythonPath.fsPath, expectedArgs, options), TypeMoq.Times.once()); expect(result.stdout).to.be.equal(stdout, 'exec should return the content of stdout'); }); @@ -122,16 +111,12 @@ suite('PythonProcessService', () => { const expectedArgs = ['-m', moduleName, ...args]; const options = {}; processService - .setup((p) => p.exec(pythonPath, expectedArgs, options)) + .setup((p) => p.exec(pythonPath.fsPath, expectedArgs, options)) .returns(() => Promise.resolve({ stdout: 'bar', stderr: `Error: No module named ${moduleName}` })); processService - .setup((p) => p.exec(pythonPath, ['-c', `import ${moduleName}`], { throwOnStdErr: true })) + .setup((p) => p.exec(pythonPath.fsPath, ['-c', `import ${moduleName}`], { throwOnStdErr: true })) .returns(() => Promise.reject(new StdErrError('not installed'))); - const env = createPythonEnv( - { path: pythonPath } as PythonEnvironment, - processService.object, - fileSystem.object - ); + const env = createPythonEnv({ uri: pythonPath } as PythonEnvironment, processService.object, fileSystem.object); const procs = createPythonProcessService(processService.object, env); const result = procs.execModule(moduleName, args, options); diff --git a/src/test/common/variables/kernelEnvVarsService.unit.test.ts b/src/test/common/variables/kernelEnvVarsService.unit.test.ts index 55e0ba8c1f1..80817c0a82d 100644 --- a/src/test/common/variables/kernelEnvVarsService.unit.test.ts +++ b/src/test/common/variables/kernelEnvVarsService.unit.test.ts @@ -17,6 +17,7 @@ import { EnvironmentType, PythonEnvironment } from '../../../platform/pythonEnvi import { anything, instance, mock, when } from 'ts-mockito'; import { KernelEnvironmentVariablesService } from '../../../kernels/raw/launcher/kernelEnvVarsService.node'; import { IJupyterKernelSpec } from '../../../kernels/types'; +import { Uri } from 'vscode'; use(chaiAsPromised); @@ -27,16 +28,17 @@ suite('Kernel Environment Variables Service', () => { let variablesService: EnvironmentVariablesService; let kernelVariablesService: KernelEnvironmentVariablesService; let interpreterService: IInterpreterService; + const pathFile = Uri.file('foobar'); const interpreter: PythonEnvironment = { envType: EnvironmentType.Conda, - path: 'foobar', + uri: pathFile, sysPrefix: '0' }; const kernelSpec: IJupyterKernelSpec = { name: 'kernel', - path: 'foobar', + uri: pathFile, display_name: 'kernel', - interpreterPath: 'foobar', + interpreterPath: pathFile.fsPath, argv: [] }; @@ -67,7 +69,7 @@ suite('Kernel Environment Variables Service', () => { const processPath = Object.keys(process.env).find((k) => k.toLowerCase() == 'path'); assert.isOk(processPath); assert.isOk(vars); - assert.strictEqual(vars![processPath!], `${path.dirname(interpreter.path)}${path.delimiter}foobar`); + assert.strictEqual(vars![processPath!], `${path.dirname(interpreter.uri.fsPath)}${path.delimiter}foobar`); }); test('Paths are merged', async () => { @@ -82,14 +84,14 @@ suite('Kernel Environment Variables Service', () => { assert.isOk(vars); assert.strictEqual( vars![processPath!], - `${path.dirname(interpreter.path)}${path.delimiter}foobar${path.delimiter}foobaz` + `${path.dirname(interpreter.uri.fsPath)}${path.delimiter}foobar${path.delimiter}foobaz` ); }); test('KernelSpec interpreterPath used if interpreter is undefined', async () => { - when(interpreterService.getInterpreterDetails('foobar')).thenResolve({ + when(interpreterService.getInterpreterDetails(anything())).thenResolve({ envType: EnvironmentType.Conda, - path: 'foopath', + uri: Uri.file('foopath'), sysPrefix: 'foosysprefix' }); when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ @@ -104,7 +106,7 @@ suite('Kernel Environment Variables Service', () => { assert.isOk(vars); assert.strictEqual( vars![processPath!], - `${path.dirname(interpreter.path)}${path.delimiter}foobar${path.delimiter}foobaz` + `${path.dirname(interpreter.uri.fsPath)}${path.delimiter}foobar${path.delimiter}foobaz` ); }); @@ -113,9 +115,9 @@ suite('Kernel Environment Variables Service', () => { hasActivatedEnvVariables: boolean, hasActivationCommands: boolean ) { - when(interpreterService.getInterpreterDetails('foobar')).thenResolve({ + when(interpreterService.getInterpreterDetails(anything())).thenResolve({ envType, - path: 'foopath', + uri: Uri.file('foopath'), sysPrefix: 'foosysprefix' }); when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve( diff --git a/src/test/datascience/data-viewing/dataViewerDependencyService.unit.test.ts b/src/test/datascience/data-viewing/dataViewerDependencyService.unit.test.ts index 0db5e8e9033..fb23de1ebaf 100644 --- a/src/test/datascience/data-viewing/dataViewerDependencyService.unit.test.ts +++ b/src/test/datascience/data-viewing/dataViewerDependencyService.unit.test.ts @@ -17,6 +17,7 @@ import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { ProductInstaller } from '../../../kernels/installer/productInstaller.node'; import { IInstaller, Product } from '../../../kernels/installer/types'; import { DataViewerDependencyService } from '../../../webviews/extension-side/dataviewer/dataViewerDependencyService.node'; +import { Uri } from 'vscode'; suite('DataScience - DataViewerDependencyService', () => { let dependencyService: DataViewerDependencyService; @@ -29,7 +30,7 @@ suite('DataScience - DataViewerDependencyService', () => { setup(async () => { interpreter = { displayName: '', - path: path.join('users', 'python', 'bin', 'python.exe'), + uri: Uri.file(path.join('users', 'python', 'bin', 'python.exe')), sysPrefix: '', sysVersion: '', version: new SemVer('3.3.3') diff --git a/src/test/datascience/debugger.vscode.test.ts b/src/test/datascience/debugger.vscode.test.ts index b06bb3f9c6c..cb1948339d6 100644 --- a/src/test/datascience/debugger.vscode.test.ts +++ b/src/test/datascience/debugger.vscode.test.ts @@ -18,7 +18,7 @@ import { waitForStoppedEvent, runCell, getDebugSessionAndAdapter -} from './notebook/helper'; +} from './notebook/helper.node'; import { ITestVariableViewProvider } from './variableView/variableViewTestInterfaces'; import { traceInfo } from '../../platform/logging'; import { IDebuggingManager } from '../../platform/debugger/types'; diff --git a/src/test/datascience/errorHandler.unit.test.ts b/src/test/datascience/errorHandler.unit.test.ts index 1b56a7a3ef8..76f9c7b3a83 100644 --- a/src/test/datascience/errorHandler.unit.test.ts +++ b/src/test/datascience/errorHandler.unit.test.ts @@ -15,7 +15,6 @@ import { KernelConnectionMetadata, KernelInterpreterDependencyResponse } from '../../platform/../kernels/types'; -import { getOSType, OSType } from '../common.node'; import { PythonEnvironment, EnvironmentType } from '../../platform/pythonEnvironments/info'; import { JupyterInterpreterService } from '../../kernels/jupyter/interpreter/jupyterInterpreterService.node'; import { DataScienceErrorHandler } from '../../platform/errors/errorHandler.node'; @@ -27,6 +26,8 @@ import { IJupyterInterpreterDependencyManager, JupyterInterpreterDependencyResponse } from '../../kernels/jupyter/types'; +import { getDisplayNameOrNameOfKernelConnection } from '../../kernels/helpers.node'; +import { getOSType, OSType } from '../../platform/common/utils/platform'; suite('DataScience Error Handler Unit Tests', () => { let applicationShell: IApplicationShell; @@ -39,7 +40,7 @@ suite('DataScience Error Handler Unit Tests', () => { let kernelDependencyInstaller: IKernelDependencyService; const jupyterInterpreter: PythonEnvironment = { displayName: 'Hello', - path: 'Some Path', + uri: Uri.file('Some Path'), sysPrefix: '' }; @@ -119,7 +120,7 @@ suite('DataScience Error Handler Unit Tests', () => { id: '', kind: 'startUsingPythonInterpreter', interpreter: { - path: 'Hello There', + uri: Uri.file('Hello There'), sysPrefix: 'Something else', displayName: 'Hello (Some Path)' }, @@ -127,7 +128,7 @@ suite('DataScience Error Handler Unit Tests', () => { argv: [], display_name: '', name: '', - path: '' + uri: Uri.file('') } }; }); @@ -259,7 +260,7 @@ suite('DataScience Error Handler Unit Tests', () => { const expectedMessage = DataScience.fileSeemsToBeInterferingWithKernelStartup().format( getDisplayPath( - 'c:\\Development\\samples\\pySamples\\sample1\\kernel_issues\\start\\random.py', + Uri.file('c:\\Development\\samples\\pySamples\\sample1\\kernel_issues\\start\\random.py'), workspaceFolders ) ); @@ -281,7 +282,7 @@ suite('DataScience Error Handler Unit Tests', () => { const expectedMessage = DataScience.failedToStartKernelDueToImportFailureFromFile().format( 'Template', - getDisplayPath('/home/xyz/samples/pySamples/crap/kernel_crash/no_start/string.py', []) + '/home/xyz/samples/pySamples/crap/kernel_crash/no_start/string.py' // Not using getDisplayPath under the covers ); verifyErrorMessage(expectedMessage, 'https://aka.ms/kernelFailuresModuleImportErrFromFile'); @@ -312,7 +313,10 @@ suite('DataScience Error Handler Unit Tests', () => { ); const expectedMessage = DataScience.fileSeemsToBeInterferingWithKernelStartup().format( - getDisplayPath('/home/xyz/samples/pySamples/crap/kernel_crash/no_start/string.py', workspaceFolders) + getDisplayPath( + Uri.file('/home/xyz/samples/pySamples/crap/kernel_crash/no_start/string.py'), + workspaceFolders + ) ); verifyErrorMessage(expectedMessage, 'https://aka.ms/kernelFailuresOverridingBuiltInModules'); @@ -413,12 +417,12 @@ ImportError: No module named 'xyz' verifyErrorMessage(expectedMessage, expectedLink); } test('Failure to start Jupyter Server (unable to extract python error message)', async () => { - const envDisplayName = `${jupyterInterpreter.displayName} (${jupyterInterpreter.path})`; + const envDisplayName = getDisplayNameOrNameOfKernelConnection(kernelConnection); const expectedMessage = DataScience.failedToStartJupyter().format(envDisplayName); await verifyJupyterErrors('Kaboom', expectedMessage); }); test('Failure to start Jupyter Server (unable to extract python error message), (without failure about jupyter error, without daemon)', async () => { - const envDisplayName = `${jupyterInterpreter.displayName} (${jupyterInterpreter.path})`; + const envDisplayName = getDisplayNameOrNameOfKernelConnection(kernelConnection); const expectedMessage = DataScience.failedToStartJupyter().format(envDisplayName); await verifyJupyterErrors('kaboom', expectedMessage); }); @@ -426,14 +430,14 @@ ImportError: No module named 'xyz' const stdError = `${stdErrorMessages.failureToStartJupyter} Failed to run jupyter as observable with args notebook --no-browser --notebook-dir="/home/don/samples/pySamples/crap" --config=/tmp/40aa74ae-d668-4225-8201-4570c9a0ac4a/jupyter_notebook_config.py --NotebookApp.iopub_data_rate_limit=10000000000.0`; - const envDisplayName = `${jupyterInterpreter.displayName} (${jupyterInterpreter.path})`; + const envDisplayName = getDisplayNameOrNameOfKernelConnection(kernelConnection); const pythonError = 'NotImplementedError: subclasses must implement __call__'; const expectedMessage = DataScience.failedToStartJupyterWithErrorInfo().format(envDisplayName, pythonError); await verifyJupyterErrors(stdError, expectedMessage); }); test('Failure to start Jupyter Server (without failure about jupyter error, without daemon)', async () => { const stdError = stdErrorMessages.failureToStartJupyter; - const envDisplayName = `${jupyterInterpreter.displayName} (${jupyterInterpreter.path})`; + const envDisplayName = getDisplayNameOrNameOfKernelConnection(kernelConnection); const pythonError = 'NotImplementedError: subclasses must implement __call__'; const expectedMessage = DataScience.failedToStartJupyterWithErrorInfo().format(envDisplayName, pythonError); await verifyJupyterErrors(stdError, expectedMessage); @@ -442,7 +446,7 @@ Failed to run jupyter as observable with args notebook --no-browser --notebook-d const stdError = `${stdErrorMessages.failureToStartJupyterDueToOutdatedTraitlets} Failed to run jupyter as observable with args notebook --no-browser --notebook-dir="/home/don/samples/pySamples/crap" --config=/tmp/40aa74ae-d668-4225-8201-4570c9a0ac4a/jupyter_notebook_config.py --NotebookApp.iopub_data_rate_limit=10000000000.0`; - const envDisplayName = `${jupyterInterpreter.displayName} (${jupyterInterpreter.path})`; + const envDisplayName = getDisplayNameOrNameOfKernelConnection(kernelConnection); const pythonError = "AttributeError: 'Namespace' object has no attribute '_flags'"; const expectedMessage = DataScience.failedToStartJupyterDueToOutdatedTraitlets().format( envDisplayName, @@ -457,7 +461,7 @@ Failed to run jupyter as observable with args notebook --no-browser --notebook-d }); test('Failure to start Jupyter Server due to outdated traitlets (without failure about jupyter error, without daemon)', async () => { const stdError = stdErrorMessages.failureToStartJupyterDueToOutdatedTraitlets; - const envDisplayName = `${jupyterInterpreter.displayName} (${jupyterInterpreter.path})`; + const envDisplayName = getDisplayNameOrNameOfKernelConnection(kernelConnection); const pythonError = "AttributeError: 'Namespace' object has no attribute '_flags'"; const expectedMessage = DataScience.failedToStartJupyterDueToOutdatedTraitlets().format( envDisplayName, @@ -552,7 +556,7 @@ Failed to run jupyter as observable with args notebook --no-browser --notebook-d [ "Running cells with 'Hello (Some Path)' requires ipykernel package.", "Run the following command to install 'ipykernel' into the Python environment. ", - `Command: '"Hello There" -m pip install ipykernel -U --force-reinstall'` + `Command: '"/Hello There" -m pip install ipykernel -U --force-reinstall'` ].join('\n') ); }); diff --git a/src/test/datascience/execution.unit.test.ts b/src/test/datascience/execution.unit.test.ts index 89a938d1bf5..bfa109a12cc 100644 --- a/src/test/datascience/execution.unit.test.ts +++ b/src/test/datascience/execution.unit.test.ts @@ -11,7 +11,7 @@ import { anything, instance, match, mock, reset, when } from 'ts-mockito'; import { Matcher } from 'ts-mockito/lib/matcher/type/Matcher'; import * as TypeMoq from 'typemoq'; import * as uuid from 'uuid/v4'; -import { CancellationTokenSource, ConfigurationChangeEvent, Disposable, EventEmitter } from 'vscode'; +import { CancellationTokenSource, ConfigurationChangeEvent, Disposable, EventEmitter, Uri } from 'vscode'; import { ApplicationShell } from '../../platform/common/application/applicationShell'; import { IApplicationShell, IWorkspaceService } from '../../platform/common/application/types'; import { WorkspaceService } from '../../platform/common/application/workspace'; @@ -31,12 +31,7 @@ import { ObservableExecutionResult, Output } from '../../platform/common/process/types.node'; -import { - IAsyncDisposableRegistry, - IConfigurationService, - IOutputChannel, - IPathUtils -} from '../../platform/common/types'; +import { IAsyncDisposableRegistry, IConfigurationService, IOutputChannel } from '../../platform/common/types'; import { EXTENSION_ROOT_DIR } from '../../platform/constants.node'; import { IEnvironmentActivationService } from '../../platform/interpreter/activation/types'; import { IInterpreterService } from '../../platform/interpreter/contracts.node'; @@ -55,7 +50,6 @@ import { JupyterPaths } from '../../kernels/raw/finder/jupyterPaths.node'; import { LocalKernelFinder } from '../../kernels/raw/finder/localKernelFinder.node'; import { ILocalKernelFinder } from '../../kernels/raw/types'; import { IJupyterKernelSpec, LocalKernelConnectionMetadata } from '../../kernels/types'; -import { getOSType, OSType } from '../common.node'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants.node'; import { noop } from '../core'; import { MockOutputChannel } from '../mockClasses'; @@ -65,6 +59,7 @@ import { DisplayOptions } from '../../kernels/displayOptions.node'; import { INotebookServer } from '../../kernels/jupyter/types'; import { IJupyterSubCommandExecutionService } from '../../kernels/jupyter/types.node'; import { SystemVariables } from '../../platform/common/variables/systemVariables.node'; +import { getOSType, OSType } from '../../platform/common/utils/platform'; /* eslint-disable @typescript-eslint/no-explicit-any, , no-multi-str, */ class DisposableRegistry implements IAsyncDisposableRegistry { @@ -106,28 +101,28 @@ suite('Jupyter Execution', async () => { let ipykernelInstallCount = 0; let notebookStarter: NotebookStarter; const workingPython: PythonEnvironment = { - path: '/foo/bar/python.exe', + uri: Uri.file('/foo/bar/python.exe'), version: new SemVer('3.6.6-final'), sysVersion: '1.0.0.0', sysPrefix: 'Python' }; const missingKernelPython: PythonEnvironment = { - path: '/foo/baz/python.exe', + uri: Uri.file('/foo/baz/python.exe'), version: new SemVer('3.1.1-final'), sysVersion: '1.0.0.0', sysPrefix: 'Python' }; const missingNotebookPython: PythonEnvironment = { - path: '/bar/baz/python.exe', + uri: Uri.file('/bar/baz/python.exe'), version: new SemVer('2.1.1-final'), sysVersion: '1.0.0.0', sysPrefix: 'Python' }; const missingNotebookPython2: PythonEnvironment = { - path: '/two/baz/python.exe', + uri: Uri.file('/two/baz/python.exe'), version: new SemVer('2.1.1'), sysVersion: '1.0.0.0', sysPrefix: 'Python' @@ -143,7 +138,7 @@ suite('Jupyter Execution', async () => { }); setup(() => { - workingKernelSpec = createTempSpec(workingPython.path); + workingKernelSpec = createTempSpec(workingPython.uri.fsPath); ipykernelInstallCount = 0; // eslint-disable-next-line no-invalid-this }); @@ -546,7 +541,7 @@ suite('Jupyter Execution', async () => { // Don't mind the goofy path here. It's supposed to not find the item. It's just testing the internal regex works setupProcessServiceExecWithFunc( service, - workingPython.path, + workingPython.uri.fsPath, ['-m', 'jupyter', 'kernelspec', 'list', '--json'], () => { // Return different results after we install our kernel @@ -582,13 +577,13 @@ suite('Jupyter Execution', async () => { ]); setupProcessServiceExec( service, - workingPython.path, + workingPython.uri.fsPath, ['-m', 'jupyter', 'kernelspec', 'list', '--json'], Promise.resolve({ stdout: JSON.stringify(kernelSpecs2) }) ); setupProcessServiceExecWithFunc( service, - workingPython.path, + workingPython.uri.fsPath, [ '-m', 'ipykernel', @@ -615,20 +610,20 @@ suite('Jupyter Execution', async () => { ); setupProcessServiceExec( service, - workingPython.path, + workingPython.uri.fsPath, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' }) ); setupProcessServiceExecObservable( service, - workingPython.path, + workingPython.uri.fsPath, ['-m', 'jupyter', 'kernelspec', 'list', '--json'], [], [] ); setupProcessServiceExecObservable( service, - workingPython.path, + workingPython.uri.fsPath, [ '-m', 'jupyter', @@ -647,7 +642,7 @@ suite('Jupyter Execution', async () => { const kernelSpecs = createKernelSpecs([{ name: 'working', resourceDir: path.dirname(workingKernelSpec) }]); setupProcessServiceExec( service, - missingKernelPython.path, + missingKernelPython.uri.fsPath, ['-m', 'jupyter', 'kernelspec', 'list', '--json'], Promise.resolve({ stdout: JSON.stringify(kernelSpecs) }) ); @@ -659,20 +654,20 @@ suite('Jupyter Execution', async () => { ); setupProcessServiceExec( service, - missingKernelPython.path, + missingKernelPython.uri.fsPath, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' }) ); setupProcessServiceExecObservable( service, - missingKernelPython.path, + missingKernelPython.uri.fsPath, ['-m', 'jupyter', 'kernelspec', 'list', '--json'], [], [] ); setupProcessServiceExecObservable( service, - missingKernelPython.path, + missingKernelPython.uri.fsPath, [ '-m', 'jupyter', @@ -785,37 +780,37 @@ suite('Jupyter Execution', async () => { setupMissingKernelProcessService(processService, notebookStdErr); setupPathProcessService(jupyterOnPath, processService, notebookStdErr); when( - executionFactory.create(argThat((o) => o.interpreter && o.interpreter.path === workingPython.path)) + executionFactory.create(argThat((o) => o.interpreter && o.interpreter.uri === workingPython.uri)) ).thenResolve(workingService.object); when( - executionFactory.create(argThat((o) => o.interpreter && o.interpreter.path === missingKernelPython.path)) + executionFactory.create(argThat((o) => o.interpreter && o.interpreter.uri === missingKernelPython.uri)) ).thenResolve(missingKernelService.object); when( - executionFactory.create(argThat((o) => o.interpreter && o.interpreter.path === missingNotebookPython.path)) + executionFactory.create(argThat((o) => o.interpreter && o.interpreter.uri === missingNotebookPython.uri)) ).thenResolve(missingNotebookService.object); when( - executionFactory.create(argThat((o) => o.interpreter && o.interpreter.path === missingNotebookPython2.path)) + executionFactory.create(argThat((o) => o.interpreter && o.interpreter.uri === missingNotebookPython2.uri)) ).thenResolve(missingNotebookService2.object); when( - executionFactory.createDaemon(argThat((o) => o.interpreter && o.interpreter.path === workingPython.path)) + executionFactory.createDaemon(argThat((o) => o.interpreter && o.interpreter.uri === workingPython.uri)) ).thenResolve(workingService.object as unknown as IPythonDaemonExecutionService); when( executionFactory.createDaemon( - argThat((o) => o.interpreter && o.interpreter.path === missingKernelPython.path) + argThat((o) => o.interpreter && o.interpreter.uri === missingKernelPython.uri) ) ).thenResolve(missingKernelService.object as unknown as IPythonDaemonExecutionService); when( executionFactory.createDaemon( - argThat((o) => o.interpreter && o.interpreter.path === missingNotebookPython.path) + argThat((o) => o.interpreter && o.interpreter.uri === missingNotebookPython.uri) ) ).thenResolve(missingNotebookService.object as unknown as IPythonDaemonExecutionService); when( executionFactory.createDaemon( - argThat((o) => o.interpreter && o.interpreter.path === missingNotebookPython2.path) + argThat((o) => o.interpreter && o.interpreter.uri === missingNotebookPython2.uri) ) ).thenResolve(missingNotebookService2.object as unknown as IPythonDaemonExecutionService); @@ -833,22 +828,22 @@ suite('Jupyter Execution', async () => { ).thenResolve(activeService.object); when( executionFactory.createActivatedEnvironment( - argThat((o) => o && areInterpreterPathsSame(o.interpreter.path, workingPython.path)) + argThat((o) => o && areInterpreterPathsSame(o.interpreter.path, workingPython.uri)) ) ).thenResolve(workingService.object); when( executionFactory.createActivatedEnvironment( - argThat((o) => o && areInterpreterPathsSame(o.interpreter.path, missingKernelPython.path)) + argThat((o) => o && areInterpreterPathsSame(o.interpreter.path, missingKernelPython.uri)) ) ).thenResolve(missingKernelService.object); when( executionFactory.createActivatedEnvironment( - argThat((o) => o && areInterpreterPathsSame(o.interpreter.path, missingNotebookPython.path)) + argThat((o) => o && areInterpreterPathsSame(o.interpreter.path, missingNotebookPython.uri)) ) ).thenResolve(missingNotebookService.object); when( executionFactory.createActivatedEnvironment( - argThat((o) => o && areInterpreterPathsSame(o.interpreter.path, missingNotebookPython2.path)) + argThat((o) => o && areInterpreterPathsSame(o.interpreter.path, missingNotebookPython2.uri)) ) ).thenResolve(missingNotebookService2.object); when(processServiceFactory.create()).thenResolve(processService.object); @@ -962,7 +957,7 @@ suite('Jupyter Execution', async () => { when(jupyterInterpreterService.getSelectedInterpreter(anything())).thenResolve(activeInterpreter); const jupyterPaths = mock(); when(jupyterPaths.getKernelSpecTempRegistrationFolder()).thenResolve( - path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'temp', 'jupyter', 'kernels') + Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'temp', 'jupyter', 'kernels')) ); const envActivationService = mock(); when(envActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenResolve(); @@ -972,7 +967,6 @@ suite('Jupyter Execution', async () => { instance(dependencyService), instance(executionFactory), instance(mock()), - instance(mock()), instance(jupyterPaths), instance(envActivationService) ); @@ -988,7 +982,7 @@ suite('Jupyter Execution', async () => { const kernelFinder = mock(LocalKernelFinder); const kernelSpec: IJupyterKernelSpec = { name: 'somename', - path: 'python', + uri: Uri.file('python'), argv: ['python'], display_name: 'somename' }; @@ -1088,7 +1082,7 @@ suite('Jupyter Execution', async () => { assert.isOk(usableInterpreter, 'Usable interpreter not found'); if (usableInterpreter) { // Linter - assert.equal(usableInterpreter.path, missingKernelPython.path); + assert.equal(usableInterpreter.uri, missingKernelPython.uri); assert.equal( usableInterpreter.version!.major, missingKernelPython.version!.major, @@ -1107,4 +1101,15 @@ suite('Jupyter Execution', async () => { when(interpreterService.getActiveInterpreter(anything())).thenResolve(missingNotebookPython); await assert.eventually.equal(execution.isNotebookSupported(), false); }); + + test('Interpreter paths being the same', async () => { + assert.ok( + areInterpreterPathsSame( + Uri.file(`/opt/hostedtoolcache/Python/3.9.12/x64/bin`), + Uri.file(`/opt/hostedtoolcache/python/3.9.12/x64/bin`), + getOSType(), + true + ) + ); + }); }); diff --git a/src/test/datascience/executionServiceMock.ts b/src/test/datascience/executionServiceMock.ts index cf80460a888..c48377c9c7e 100644 --- a/src/test/datascience/executionServiceMock.ts +++ b/src/test/datascience/executionServiceMock.ts @@ -15,6 +15,7 @@ import { } from '../../platform/common/process/types.node'; import { buildPythonExecInfo } from '../../platform/pythonEnvironments/exec'; import { InterpreterInformation } from '../../platform/pythonEnvironments/info'; +import { Uri } from 'vscode'; export class MockPythonExecutionService implements IPythonExecutionService { private procService: ProcessService; @@ -26,7 +27,7 @@ export class MockPythonExecutionService implements IPythonExecutionService { public getInterpreterInformation(): Promise { return Promise.resolve({ - path: '', + uri: Uri.file(''), version: new SemVer('3.6.0-beta'), sysVersion: '1.0', sysPrefix: '1.0' diff --git a/src/test/datascience/export/exportUtil.vscode.test.ts b/src/test/datascience/export/exportUtil.vscode.test.ts index a0f584abf83..3786a81b2dc 100644 --- a/src/test/datascience/export/exportUtil.vscode.test.ts +++ b/src/test/datascience/export/exportUtil.vscode.test.ts @@ -12,7 +12,7 @@ import { ExportUtil } from '../../../platform/export/exportUtil.node'; import { IExtensionTestApi } from '../../common.node'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants.node'; import { closeActiveWindows, initialize } from '../../initialize.node'; -import { createTemporaryNotebook } from '../notebook/helper'; +import { createTemporaryNotebook } from '../notebook/helper.node'; suite('DataScience - Export Util', () => { let api: IExtensionTestApi; diff --git a/src/test/datascience/helpers.ts b/src/test/datascience/helpers.ts index 732248f6ac7..a6f261ac479 100644 --- a/src/test/datascience/helpers.ts +++ b/src/test/datascience/helpers.ts @@ -17,7 +17,7 @@ import { defaultNotebookTestTimeout, waitForCellExecutionToComplete, waitForExecutionCompletedSuccessfully -} from './notebook/helper'; +} from './notebook/helper.node'; import { initialize } from '../initialize.node'; import { IDataScienceCodeLensProvider } from '../../interactive-window/editor-integration/types'; import { IInteractiveWindowProvider, IInteractiveWindow } from '../../interactive-window/types'; @@ -113,7 +113,7 @@ export async function submitFromPythonFile( source: string, disposables: vscode.Disposable[], apiProvider?: IPythonApiProvider, - activeInterpreterPath?: string + activeInterpreterPath?: vscode.Uri ) { const tempFile = await createTemporaryFile({ contents: source, extension: '.py' }); disposables.push(tempFile); @@ -121,7 +121,7 @@ export async function submitFromPythonFile( await vscode.window.showTextDocument(untitledPythonFile); if (apiProvider && activeInterpreterPath) { const pythonApi = await apiProvider.getApi(); - await pythonApi.setActiveInterpreter(activeInterpreterPath, untitledPythonFile.uri); + await pythonApi.setActiveInterpreter(activeInterpreterPath.fsPath, untitledPythonFile.uri); } const activeInteractiveWindow = (await interactiveWindowProvider.getOrCreate( untitledPythonFile.uri @@ -134,7 +134,7 @@ export async function submitFromPythonFile( export async function submitFromPythonFileUsingCodeWatcher( source: string, disposables: vscode.Disposable[], - activeInterpreterPath?: string + activeInterpreterPath?: vscode.Uri ) { const api = await initialize(); const interactiveWindowProvider = api.serviceManager.get(IInteractiveWindowProvider); @@ -146,7 +146,7 @@ export async function submitFromPythonFileUsingCodeWatcher( if (activeInterpreterPath) { const pythonApiProvider = api.serviceManager.get(IPythonApiProvider); const pythonApi = await pythonApiProvider.getApi(); - await pythonApi.setActiveInterpreter(activeInterpreterPath, untitledPythonFile.uri); + await pythonApi.setActiveInterpreter(activeInterpreterPath.fsPath, untitledPythonFile.uri); } const activeInteractiveWindow = (await interactiveWindowProvider.getOrCreate( untitledPythonFile.uri diff --git a/src/test/datascience/interactive-common/notebookServerProvider.unit.test.ts b/src/test/datascience/interactive-common/notebookServerProvider.unit.test.ts index 5a8a5870d8d..e87f6003c99 100644 --- a/src/test/datascience/interactive-common/notebookServerProvider.unit.test.ts +++ b/src/test/datascience/interactive-common/notebookServerProvider.unit.test.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; import { SemVer } from 'semver'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; -import { CancellationTokenSource, Disposable, EventEmitter } from 'vscode'; +import { CancellationTokenSource, Disposable, EventEmitter, Uri } from 'vscode'; import { disposeAllDisposables } from '../../../platform/common/helpers'; import { IConfigurationService, IWatchableJupyterSettings } from '../../../platform/common/types'; import { IInterpreterService } from '../../../platform/interpreter/contracts.node'; @@ -33,7 +33,7 @@ suite('DataScience - NotebookServerProvider', () => { let interpreterService: IInterpreterService; let pythonSettings: IWatchableJupyterSettings; const workingPython: PythonEnvironment = { - path: '/foo/bar/python.exe', + uri: Uri.file('/foo/bar/python.exe'), version: new SemVer('3.6.6-final'), sysVersion: '1.0.0.0', sysPrefix: 'Python' diff --git a/src/test/datascience/interactiveDebugging.vscode.test.ts b/src/test/datascience/interactiveDebugging.vscode.test.ts index 8bafe59755a..fd5a6b3a2e4 100644 --- a/src/test/datascience/interactiveDebugging.vscode.test.ts +++ b/src/test/datascience/interactiveDebugging.vscode.test.ts @@ -17,7 +17,7 @@ import { waitForCodeLenses, waitForLastCellToComplete } from './helpers'; -import { closeNotebooksAndCleanUpAfterTests, defaultNotebookTestTimeout, getCellOutputs } from './notebook/helper'; +import { closeNotebooksAndCleanUpAfterTests, defaultNotebookTestTimeout, getCellOutputs } from './notebook/helper.node'; import { ITestWebviewHost } from './testInterfaces'; import { waitForVariablesToMatch } from './variableView/variableViewHelpers'; import { ITestVariableViewProvider } from './variableView/variableViewTestInterfaces'; diff --git a/src/test/datascience/interactiveWindow.vscode.test.ts b/src/test/datascience/interactiveWindow.vscode.test.ts index bc2da37786e..09daac69765 100644 --- a/src/test/datascience/interactiveWindow.vscode.test.ts +++ b/src/test/datascience/interactiveWindow.vscode.test.ts @@ -6,7 +6,6 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; import * as vscode from 'vscode'; -import { IPythonApiProvider } from '../../platform/api/types'; import { traceInfo, traceInfoIfCI } from '../../platform/logging'; import { getDisplayPath } from '../../platform/common/platform/fs-paths'; import { IDisposable } from '../../platform/common/types'; @@ -31,10 +30,12 @@ import { waitForExecutionCompletedSuccessfully, waitForExecutionCompletedWithErrors, waitForTextOutput -} from './notebook/helper'; +} from './notebook/helper.node'; import { translateCellErrorOutput, getTextOutputValue } from '../../notebooks/helpers.node'; import { INotebookControllerManager } from '../../notebooks/types'; import { IInteractiveWindowProvider } from '../../interactive-window/types'; +import { IInterpreterService } from '../../platform/interpreter/contracts.node'; +import { areInterpreterPathsSame } from '../../platform/pythonEnvironments/info/interpreter.node'; suite('Interactive window', async function () { this.timeout(120_000); @@ -72,7 +73,7 @@ suite('Interactive window', async function () { api.serviceManager.get(INotebookControllerManager); // Ensure we picked up the active interpreter for use as the kernel - const pythonApi = await api.serviceManager.get(IPythonApiProvider).getApi(); + const interpreterService = await api.serviceManager.get(IInterpreterService); // Give it a bit to warm up await sleep(500); @@ -80,10 +81,9 @@ suite('Interactive window', async function () { const controller = notebookDocument ? notebookControllerManager.getSelectedNotebookController(notebookDocument) : undefined; - const activeInterpreter = await pythonApi.getActiveInterpreter(); - assert.equal( - controller?.connection.interpreter?.path, - activeInterpreter?.path, + const activeInterpreter = await interpreterService.getActiveInterpreter(); + assert.ok( + areInterpreterPathsSame(controller?.connection.interpreter?.uri, activeInterpreter?.uri), `Controller does not match active interpreter for ${getDisplayPath(notebookDocument?.uri)}` ); @@ -115,7 +115,7 @@ suite('Interactive window', async function () { const notebookControllerManager = api.serviceManager.get(INotebookControllerManager); // Ensure we picked up the active interpreter for use as the kernel - const pythonApi = await api.serviceManager.get(IPythonApiProvider).getApi(); + const interpreterService = await api.serviceManager.get(IInterpreterService); // Give it a bit to warm up await sleep(500); @@ -123,10 +123,9 @@ suite('Interactive window', async function () { const controller = notebookDocument ? notebookControllerManager.getSelectedNotebookController(notebookDocument) : undefined; - const activeInterpreter = await pythonApi.getActiveInterpreter(); - assert.equal( - controller?.connection.interpreter?.path, - activeInterpreter?.path, + const activeInterpreter = await interpreterService.getActiveInterpreter(); + assert.ok( + areInterpreterPathsSame(controller?.connection.interpreter?.uri, activeInterpreter?.uri), `Controller does not match active interpreter for ${getDisplayPath(notebookDocument?.uri)}` ); diff --git a/src/test/datascience/interactiveWindowRemote.vscode.test.ts b/src/test/datascience/interactiveWindowRemote.vscode.test.ts index a6403081109..c4946c2a56d 100644 --- a/src/test/datascience/interactiveWindowRemote.vscode.test.ts +++ b/src/test/datascience/interactiveWindowRemote.vscode.test.ts @@ -14,7 +14,7 @@ import { startJupyterServer, waitForExecutionCompletedSuccessfully, waitForTextOutput -} from './notebook/helper'; +} from './notebook/helper.node'; suite('Interactive window (remote)', async () => { let interactiveWindowProvider: IInteractiveWindowProvider; diff --git a/src/test/datascience/interpreters/environmentActivationService.vscode.test.ts b/src/test/datascience/interpreters/environmentActivationService.vscode.test.ts index f1cfe796fa3..4d47cd91592 100644 --- a/src/test/datascience/interpreters/environmentActivationService.vscode.test.ts +++ b/src/test/datascience/interpreters/environmentActivationService.vscode.test.ts @@ -31,7 +31,7 @@ import { IEnvironmentVariablesProvider } from '../../../platform/common/variable import { IS_CONDA_TEST, IS_REMOTE_NATIVE_TEST } from '../../constants.node'; import { Disposable, Memento } from 'vscode'; import { instance, mock, verify } from 'ts-mockito'; -import { defaultNotebookTestTimeout } from '../notebook/helper'; +import { defaultNotebookTestTimeout } from '../notebook/helper.node'; import { IFileSystem } from '../../../platform/common/platform/types.node'; /* eslint-disable @typescript-eslint/no-explicit-any, no-invalid-this */ suite('DataScience - VSCode Notebook - (Conda Execution) (slow)', function () { @@ -229,7 +229,7 @@ suite('DataScience - VSCode Notebook - (Conda Execution) (slow)', function () { activeCondaInterpreter.sysPrefix, `Activated env Prefix not set ${errorMessageSuffix}` ); - const execPath = path.dirname(activeCondaInterpreter.path); + const execPath = path.dirname(activeCondaInterpreter.uri.fsPath); assert.ok( envVars[pathEnvVariableName]?.startsWith(execPath), `Path for Conda should be at the start of ENV[PATH], expected ${execPath} to be in front of ${envVars[pathEnvVariableName]} ${errorMessageSuffix}` diff --git a/src/test/datascience/ipywidgets/localWidgetScriptSourceProvider.unit.test.ts b/src/test/datascience/ipywidgets/localWidgetScriptSourceProvider.unit.test.ts index fb4240084b5..5eb0b0e0f1f 100644 --- a/src/test/datascience/ipywidgets/localWidgetScriptSourceProvider.unit.test.ts +++ b/src/test/datascience/ipywidgets/localWidgetScriptSourceProvider.unit.test.ts @@ -52,7 +52,7 @@ suite('DataScience - ipywidget - Local Widget Script Source', () => { when(kernel.kernelConnectionMetadata).thenReturn({ kernelSpec: { name: '', - path: '', + uri: Uri.file(''), display_name: '', argv: [], metadata: { interpreter: { sysPrefix: 'sysPrefix', path: 'pythonPath' } } @@ -76,7 +76,7 @@ suite('DataScience - ipywidget - Local Widget Script Source', () => { when(kernel.kernelConnectionMetadata).thenReturn({ kernelSpec: { name: '', - path: '', + uri: Uri.file(''), display_name: '', argv: [], metadata: { interpreter: { sysPrefix, path: 'pythonPath' } } @@ -95,12 +95,12 @@ suite('DataScience - ipywidget - Local Widget Script Source', () => { }); test('Look for widgets in sysPath of kernel', async () => { const sysPrefix = 'sysPrefix Of Kernel'; - const kernelPath = 'kernel Path.exe'; + const kernelPath = Uri.file('kernel Path.exe'); when(interpreterService.getInterpreterDetails(kernelPath)).thenResolve({ sysPrefix } as any); const searchDirectory = path.join(sysPrefix, 'share', 'jupyter', 'nbextensions'); when(kernel.kernelConnectionMetadata).thenReturn({ - kernelSpec: { name: '', display_name: '', argv: [], path: kernelPath, language: PYTHON_LANGUAGE }, + kernelSpec: { name: '', display_name: '', argv: [], uri: kernelPath, language: PYTHON_LANGUAGE }, id: '', kind: 'startUsingLocalKernelSpec' }); @@ -117,7 +117,7 @@ suite('DataScience - ipywidget - Local Widget Script Source', () => { when(kernel.kernelConnectionMetadata).thenReturn({ kernelSpec: { name: '', - path: '', + uri: Uri.file(''), display_name: '', argv: [], metadata: { interpreter: { sysPrefix: 'sysPrefix', path: 'pythonPath' } } @@ -143,7 +143,7 @@ suite('DataScience - ipywidget - Local Widget Script Source', () => { when(kernel.kernelConnectionMetadata).thenReturn({ kernelSpec: { name: '', - path: '', + uri: Uri.file(''), display_name: '', argv: [], metadata: { interpreter: { sysPrefix, path: 'pythonPath' } } @@ -178,7 +178,7 @@ suite('DataScience - ipywidget - Local Widget Script Source', () => { when(kernel.kernelConnectionMetadata).thenReturn({ kernelSpec: { name: '', - path: '', + uri: Uri.file(''), display_name: '', argv: [], metadata: { interpreter: { sysPrefix, path: 'pythonPath' } } @@ -211,7 +211,7 @@ suite('DataScience - ipywidget - Local Widget Script Source', () => { when(kernel.kernelConnectionMetadata).thenReturn({ kernelSpec: { name: '', - path: '', + uri: Uri.file(''), display_name: '', argv: [], metadata: { interpreter: { sysPrefix, path: 'pythonPath' } } diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts index d201141b57d..92c1c5714c3 100644 --- a/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts +++ b/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts @@ -18,6 +18,7 @@ import { import { JupyterInterpreterDependencyService } from '../../../../kernels/jupyter/interpreter/jupyterInterpreterDependencyService.node'; import { JupyterInterpreterDependencyResponse } from '../../../../kernels/jupyter/types'; import { IJupyterCommand, IJupyterCommandFactory } from '../../../../kernels/jupyter/types.node'; +import { Uri } from 'vscode'; /* eslint-disable , @typescript-eslint/no-explicit-any */ @@ -28,7 +29,7 @@ suite('DataScience - Jupyter Interpreter Configuration', () => { let commandFactory: IJupyterCommandFactory; let command: IJupyterCommand; const pythonInterpreter: PythonEnvironment = { - path: '', + uri: Uri.file(''), sysPrefix: '', sysVersion: '' }; diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterSelector.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterSelector.unit.test.ts index a585b01bc58..4750c5ac1cf 100644 --- a/src/test/datascience/jupyter/interpreter/jupyterInterpreterSelector.unit.test.ts +++ b/src/test/datascience/jupyter/interpreter/jupyterInterpreterSelector.unit.test.ts @@ -8,11 +8,11 @@ import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; import { ApplicationShell } from '../../../../platform/common/application/applicationShell'; import { IApplicationShell, IWorkspaceService } from '../../../../platform/common/application/types'; import { WorkspaceService } from '../../../../platform/common/application/workspace'; -import { PathUtils } from '../../../../platform/common/platform/pathUtils.node'; -import { IPathUtils } from '../../../../platform/common/types'; import { IInterpreterSelector } from '../../../../platform/interpreter/configuration/types'; import { JupyterInterpreterSelector } from '../../../../kernels/jupyter/interpreter/jupyterInterpreterSelector.node'; import { JupyterInterpreterStateStore } from '../../../../kernels/jupyter/interpreter/jupyterInterpreterStateStore.node'; +import { Uri } from 'vscode'; +import { getDisplayPath } from '../../../../platform/common/platform/fs-paths'; suite('DataScience - Jupyter Interpreter Picker', () => { let picker: JupyterInterpreterSelector; @@ -20,21 +20,19 @@ suite('DataScience - Jupyter Interpreter Picker', () => { let appShell: IApplicationShell; let interpreterSelectionState: JupyterInterpreterStateStore; let workspace: IWorkspaceService; - let pathUtils: IPathUtils; setup(() => { interpreterSelector = mock(); interpreterSelectionState = mock(JupyterInterpreterStateStore); appShell = mock(ApplicationShell); workspace = mock(WorkspaceService); - pathUtils = mock(PathUtils); + when(workspace.workspaceFolders).thenReturn([]); // eslint-disable-next-line @typescript-eslint/no-explicit-any picker = new JupyterInterpreterSelector( instance(interpreterSelector), instance(appShell), instance(interpreterSelectionState), - instance(workspace), - instance(pathUtils) + instance(workspace) ); }); @@ -65,9 +63,9 @@ suite('DataScience - Jupyter Interpreter Picker', () => { test('Should display current interpreter path in the picker', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const interpreters = ['something'] as any[]; - const displayPath = 'Display Path'; - when(interpreterSelectionState.selectedPythonPath).thenReturn('jupyter.exe'); - when(pathUtils.getDisplayName('jupyter.exe', anything())).thenReturn(displayPath); + const selectedPythonPath = Uri.file('jupyter.exe'); + const displayPath = getDisplayPath(selectedPythonPath); + when(interpreterSelectionState.selectedPythonPath).thenReturn(selectedPythonPath); when(interpreterSelector.getSuggestions(undefined)).thenResolve(interpreters); when(appShell.showQuickPick(anything(), anything())).thenResolve(); diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterService.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterService.unit.test.ts index abebc303534..d9cefdc826e 100644 --- a/src/test/datascience/jupyter/interpreter/jupyterInterpreterService.unit.test.ts +++ b/src/test/datascience/jupyter/interpreter/jupyterInterpreterService.unit.test.ts @@ -4,8 +4,8 @@ 'use strict'; import { assert } from 'chai'; -import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; -import { Memento } from 'vscode'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Memento, Uri } from 'vscode'; import { IInterpreterService } from '../../../../platform/interpreter/contracts.node'; import { PythonEnvironment } from '../../../../platform/pythonEnvironments/info'; import { JupyterInterpreterDependencyService } from '../../../../kernels/jupyter/interpreter/jupyterInterpreterDependencyService.node'; @@ -30,12 +30,12 @@ suite('DataScience - Jupyter Interpreter Service', () => { let oldVersionCacheStateStore: JupyterInterpreterOldCacheStateStore; const selectedJupyterInterpreter = createPythonInterpreter({ displayName: 'JupyterInterpreter' }); const pythonInterpreter: PythonEnvironment = { - path: 'some path', + uri: Uri.file('some path'), sysPrefix: '', sysVersion: '' }; const secondPythonInterpreter: PythonEnvironment = { - path: 'second interpreter path', + uri: Uri.file('second interpreter path'), sysPrefix: '', sysVersion: '' }; @@ -54,10 +54,8 @@ suite('DataScience - Jupyter Interpreter Service', () => { instance(interpreterConfiguration), instance(interpreterService) ); - when(interpreterService.getInterpreterDetails(pythonInterpreter.path, undefined)).thenResolve( - pythonInterpreter - ); - when(interpreterService.getInterpreterDetails(secondPythonInterpreter.path, undefined)).thenResolve( + when(interpreterService.getInterpreterDetails(pythonInterpreter.uri, undefined)).thenResolve(pythonInterpreter); + when(interpreterService.getInterpreterDetails(secondPythonInterpreter.uri, undefined)).thenResolve( secondPythonInterpreter ); when(memento.update(anything(), anything())).thenResolve(); @@ -118,7 +116,7 @@ suite('DataScience - Jupyter Interpreter Service', () => { assert.equal(selectedInterpreter, secondPythonInterpreter); }); test('setInitialInterpreter if older version is set should use and clear', async () => { - when(oldVersionCacheStateStore.getCachedInterpreterPath()).thenReturn(pythonInterpreter.path); + when(oldVersionCacheStateStore.getCachedInterpreterPath()).thenReturn(pythonInterpreter.uri); when(oldVersionCacheStateStore.clearCache()).thenResolve(); when(interpreterConfiguration.areDependenciesInstalled(pythonInterpreter, anything())).thenResolve(true); const initialInterpreter = await jupyterInterpreterService.setInitialInterpreter(undefined); @@ -127,14 +125,14 @@ suite('DataScience - Jupyter Interpreter Service', () => { }); test('setInitialInterpreter use saved interpreter if valid', async () => { when(oldVersionCacheStateStore.getCachedInterpreterPath()).thenReturn(undefined); - when(interpreterSelectionState.selectedPythonPath).thenReturn(pythonInterpreter.path); + when(interpreterSelectionState.selectedPythonPath).thenReturn(pythonInterpreter.uri); when(interpreterConfiguration.areDependenciesInstalled(pythonInterpreter, anything())).thenResolve(true); const initialInterpreter = await jupyterInterpreterService.setInitialInterpreter(undefined); assert.equal(initialInterpreter, pythonInterpreter); }); test('setInitialInterpreter saved interpreter invalid, clear it and use active interpreter', async () => { when(oldVersionCacheStateStore.getCachedInterpreterPath()).thenReturn(undefined); - when(interpreterSelectionState.selectedPythonPath).thenReturn(secondPythonInterpreter.path); + when(interpreterSelectionState.selectedPythonPath).thenReturn(secondPythonInterpreter.uri); when(interpreterConfiguration.areDependenciesInstalled(secondPythonInterpreter, anything())).thenResolve(false); when(interpreterService.getActiveInterpreter(anything())).thenResolve(pythonInterpreter); when(interpreterConfiguration.areDependenciesInstalled(pythonInterpreter, anything())).thenResolve(true); @@ -143,7 +141,7 @@ suite('DataScience - Jupyter Interpreter Service', () => { // Make sure we set our saved interpreter to the new active interpreter // it should have been cleared to undefined, then set to a new value verify(interpreterSelectionState.updateSelectedPythonPath(undefined)).once(); - verify(interpreterSelectionState.updateSelectedPythonPath(anyString())).once(); + verify(interpreterSelectionState.updateSelectedPythonPath(pythonInterpreter.uri)).once(); }); test('Install missing dependencies into active interpreter', async () => { when(interpreterService.getActiveInterpreter(anything())).thenResolve(pythonInterpreter); diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterStateStore.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterStateStore.unit.test.ts index 80face41e05..78ff1c353c6 100644 --- a/src/test/datascience/jupyter/interpreter/jupyterInterpreterStateStore.unit.test.ts +++ b/src/test/datascience/jupyter/interpreter/jupyterInterpreterStateStore.unit.test.ts @@ -5,7 +5,7 @@ import { assert } from 'chai'; import { anything, instance, mock, when } from 'ts-mockito'; -import { EventEmitter, Memento } from 'vscode'; +import { EventEmitter, Memento, Uri } from 'vscode'; import { PythonEnvironment } from '../../../../platform/pythonEnvironments/info'; import { JupyterInterpreterService } from '../../../../kernels/jupyter/interpreter/jupyterInterpreterService.node'; import { JupyterInterpreterStateStore } from '../../../../kernels/jupyter/interpreter/jupyterInterpreterStateStore.node'; @@ -32,13 +32,15 @@ suite('DataScience - Jupyter Interpreter State', () => { assert.isFalse(selected.interpreterSetAtleastOnce); }); test('If memento is set (for subsequent sesssions), return true', async () => { - when(memento.get(anything(), undefined)).thenReturn('jupyter.exe'); + const uri = 'jupyter.exe'; + when(memento.get(anything(), undefined)).thenReturn(uri); assert.isOk(selected.interpreterSetAtleastOnce); }); test('Get python path from memento', async () => { - when(memento.get(anything(), undefined)).thenReturn('jupyter.exe'); + const uri = 'jupyter.exe'; + when(memento.get(anything(), undefined)).thenReturn(uri); - assert.equal(selected.selectedPythonPath, 'jupyter.exe'); + assert.equal(selected.selectedPythonPath?.fsPath, Uri.file(uri).fsPath); }); }); diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.unit.test.ts index 59baea9cf7b..3d9690d470e 100644 --- a/src/test/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.unit.test.ts +++ b/src/test/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.unit.test.ts @@ -10,7 +10,6 @@ import * as fsExtra from 'fs-extra'; import * as sinon from 'sinon'; import { Subject } from 'rxjs/Subject'; import { anything, capture, deepEqual, instance, mock, when } from 'ts-mockito'; -import { PathUtils } from '../../../../platform/common/platform/pathUtils.node'; import { PythonExecutionFactory } from '../../../../platform/common/process/pythonExecutionFactory.node'; import { IPythonDaemonExecutionService, @@ -33,6 +32,7 @@ import { JupyterInterpreterSubCommandExecutionService } from '../../../../kernel import { JupyterPaths } from '../../../../kernels/raw/finder/jupyterPaths.node'; import { JupyterDaemonModule } from '../../../../platform/common/constants'; import { JupyterServerInfo } from '../../../../kernels/jupyter/types'; +import { Uri } from 'vscode'; use(chaiPromise); /* eslint-disable */ @@ -63,7 +63,6 @@ suite('DataScience - Jupyter InterpreterSubCommandExecutionService', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (instance(execService) as any).then = undefined; const output = new MockOutputChannel(''); - const pathUtils = mock(PathUtils); notebookStartResult = { dispose: noop, proc: undefined, @@ -71,7 +70,7 @@ suite('DataScience - Jupyter InterpreterSubCommandExecutionService', () => { }; const jupyterPaths = mock(); when(jupyterPaths.getKernelSpecTempRegistrationFolder()).thenResolve( - path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'temp', 'jupyter', 'kernels') + Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'temp', 'jupyter', 'kernels')) ); const envActivationService = mock(); when(envActivationService.getActivatedEnvironmentVariables(anything(), anything())).thenResolve(); @@ -81,7 +80,6 @@ suite('DataScience - Jupyter InterpreterSubCommandExecutionService', () => { instance(jupyterDependencyService), instance(execFactory), output, - instance(pathUtils), instance(jupyterPaths), instance(envActivationService) ); @@ -239,7 +237,10 @@ suite('DataScience - Jupyter InterpreterSubCommandExecutionService', () => { undefined ); - assert.equal(reason, DataScience.jupyterKernelSpecModuleNotFound().format(selectedJupyterInterpreter.path)); + assert.equal( + reason, + DataScience.jupyterKernelSpecModuleNotFound().format(selectedJupyterInterpreter.uri.fsPath) + ); }); test('Can start jupyer notebook', async () => { const output = await jupyterInterpreterExecutionService.startNotebook([], {}); diff --git a/src/test/datascience/jupyter/jupyterSession.unit.test.ts b/src/test/datascience/jupyter/jupyterSession.unit.test.ts index e3f9a6a7906..d48e94d1166 100644 --- a/src/test/datascience/jupyter/jupyterSession.unit.test.ts +++ b/src/test/datascience/jupyter/jupyterSession.unit.test.ts @@ -56,7 +56,7 @@ suite('DataScience - JupyterSession', () => { display_name: 'new kernel', language: 'python', name: 'newkernel', - path: 'path', + uri: Uri.file('path'), lastActivityTime: new Date(), numberOfConnections: 1, model: { @@ -98,7 +98,7 @@ suite('DataScience - JupyterSession', () => { argv: [], display_name: '', name: '', - path: '' + uri: Uri.file('') } }; session = mock(); diff --git a/src/test/datascience/jupyter/kernels/installationPrompts.vscode.test.ts b/src/test/datascience/jupyter/kernels/installationPrompts.vscode.test.ts index 81320244328..38b5b95c154 100644 --- a/src/test/datascience/jupyter/kernels/installationPrompts.vscode.test.ts +++ b/src/test/datascience/jupyter/kernels/installationPrompts.vscode.test.ts @@ -30,7 +30,7 @@ import { areInterpreterPathsSame, getInterpreterHash } from '../../../../platform/pythonEnvironments/info/interpreter.node'; -import { captureScreenShot, getOSType, IExtensionTestApi, OSType, waitForCondition } from '../../../common.node'; +import { captureScreenShot, IExtensionTestApi, waitForCondition } from '../../../common.node'; import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_REMOTE_NATIVE_TEST, @@ -60,11 +60,14 @@ import { WindowPromptStub, WindowPromptStubButtonClickOptions, waitForTextOutput -} from '../../notebook/helper'; +} from '../../notebook/helper.node'; import * as kernelSelector from '../../../../notebooks/controllers/kernelSelector.node'; import { noop } from '../../../core'; import { IInteractiveWindowProvider } from '../../../../interactive-window/types'; import { Commands } from '../../../../platform/common/constants'; +import { getDisplayPathFromLocalFile } from '../../../../platform/common/platform/fs-paths.node'; +import { getOSType, OSType } from '../../../../platform/common/utils/platform'; +import { isUri } from '../../../../platform/common/utils/misc'; /* eslint-disable no-invalid-this, , , @typescript-eslint/no-explicit-any */ suite('DataScience Install IPyKernel (slow) (install)', function () { @@ -75,9 +78,15 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { 'src/test/datascience/jupyter/kernels/nbWithKernel.ipynb' ); const executable = getOSType() === OSType.Windows ? 'Scripts/python.exe' : 'bin/python'; // If running locally on Windows box. - let venvPythonPath = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvnokernel', executable); - let venvNoRegPath = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvnoreg', executable); - let venvKernelPath = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvkernel', executable); + let venvPythonPath = Uri.file( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvnokernel', executable) + ); + let venvNoRegPath = Uri.file( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvnoreg', executable) + ); + let venvKernelPath = Uri.file( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvkernel', executable) + ); const expectedPromptMessageSuffix = `requires ${ProductNames.get(Product.ipykernel)!} package`; let api: IExtensionTestApi; @@ -101,7 +110,7 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { if (IS_REMOTE_NATIVE_TEST) { return this.skip(); } - if (!fs.pathExistsSync(venvPythonPath) || !fs.pathExistsSync(venvNoRegPath)) { + if (!fs.pathExistsSync(venvPythonPath.fsPath) || !fs.pathExistsSync(venvNoRegPath.fsPath)) { // Virtual env does not exist. return this.skip(); } @@ -128,9 +137,9 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { if (!interpreter1 || !interpreter2 || !interpreter3) { throw new Error('Unable to get information for interpreter 1'); } - venvPythonPath = interpreter1.path; - venvNoRegPath = interpreter2.path; - venvKernelPath = interpreter3.path; + venvPythonPath = interpreter1.uri; + venvNoRegPath = interpreter2.uri; + venvKernelPath = interpreter3.uri; }); setup(async function () { console.log(`Start test ${this.currentTest?.title}`); @@ -147,12 +156,12 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { fs .readFileSync(nbFile) .toString('utf8') - .replace('', getInterpreterHash({ path: venvPythonPath })) + .replace('', getInterpreterHash({ uri: venvPythonPath })) ); await Promise.all([ - installIPyKernel(venvKernelPath), - uninstallIPyKernel(venvPythonPath), - uninstallIPyKernel(venvNoRegPath) + installIPyKernel(venvKernelPath.fsPath), + uninstallIPyKernel(venvPythonPath.fsPath), + uninstallIPyKernel(venvNoRegPath.fsPath) ]); await closeActiveWindows(); await Promise.all([ @@ -174,8 +183,8 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { suiteTeardown(async function () { // Make sure to put ipykernel back try { - await installIPyKernel(venvPythonPath); - await uninstallIPyKernel(venvNoRegPath); + await installIPyKernel(venvPythonPath.fsPath); + await uninstallIPyKernel(venvNoRegPath.fsPath); } catch (ex) { // Don't fail test } @@ -192,10 +201,10 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { }); test(`Ensure prompt is displayed when ipykernel module is not found and it gets installed for '${path.basename( - venvPythonPath + venvPythonPath.fsPath )}'`, async () => openNotebookAndInstallIpyKernelWhenRunningCell(venvPythonPath)); test(`Ensure prompt is displayed when ipykernel module is not found and it gets installed for '${path.basename( - venvNoRegPath + venvNoRegPath.fsPath )}'`, async () => openNotebookAndInstallIpyKernelWhenRunningCell(venvPythonPath)); test('Ensure ipykernel install prompt is displayed every time you try to run a cell in a Notebook', async function () { if (IS_REMOTE_NATIVE_TEST) { @@ -298,20 +307,20 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { // The prompt should be displayed when we run a cell. await waitForCondition(async () => prompt.displayed.then(() => true), delayForUITest, 'Prompt not displayed'); - await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath); - await verifyErrorInCellOutput(notebookDocument, venvPythonPath); + await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath.fsPath); + await verifyErrorInCellOutput(notebookDocument, venvPythonPath.fsPath); // Submitting code again should display the same prompt again. prompt.reset(); await activeInteractiveWindow.addCode(source, untitledPythonFile.uri, 0).catch(noop); - await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath); - await verifyErrorInCellOutput(notebookDocument, venvPythonPath); + await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath.fsPath); + await verifyErrorInCellOutput(notebookDocument, venvPythonPath.fsPath); // Submitting code again should display the same prompt again. prompt.reset(); await activeInteractiveWindow.addCode(source, untitledPythonFile.uri, 0).catch(noop); - await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath); - await verifyErrorInCellOutput(notebookDocument, venvPythonPath); + await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath.fsPath); + await verifyErrorInCellOutput(notebookDocument, venvPythonPath.fsPath); await sleep(1_000); @@ -360,22 +369,22 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { waitForCondition( () => prompt.messages.some((message) => - message.includes(path.basename(path.dirname(path.dirname(venvPythonPath)))) + message.includes(path.basename(path.dirname(path.dirname(venvPythonPath.fsPath)))) ), delayForUITest, `Prompts '${prompt.messages}' do not include ${path.basename( - path.dirname(path.dirname(venvPythonPath)) + path.dirname(path.dirname(venvPythonPath.fsPath)) )}` ), // Verify the the name of the new env is included in the prompt displayed (instead of the old message); waitForCondition( () => prompt.messages.some((message) => - message.includes(path.basename(path.dirname(path.dirname(venvNoRegPath)))) + message.includes(path.basename(path.dirname(path.dirname(venvNoRegPath.fsPath)))) ), delayForUITest, `Prompts '${prompt.messages}' do not include ${path.basename( - path.dirname(path.dirname(venvNoRegPath)) + path.dirname(path.dirname(venvNoRegPath.fsPath)) )}` ) ]); @@ -383,14 +392,14 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { // Submitting code again should display the same prompt again. prompt.reset(); await activeInteractiveWindow.addCode(source, untitledPythonFile.uri, 0).catch(noop); - await verifyIPyKernelPromptDisplayed(prompt, venvNoRegPath); - await verifyErrorInCellOutput(notebookDocument, venvNoRegPath); + await verifyIPyKernelPromptDisplayed(prompt, venvNoRegPath.fsPath); + await verifyErrorInCellOutput(notebookDocument, venvNoRegPath.fsPath); // Submitting code again should display the same prompt again. prompt.reset(); await activeInteractiveWindow.addCode(source, untitledPythonFile.uri, 0).catch(noop); - await verifyIPyKernelPromptDisplayed(prompt, venvNoRegPath); - await verifyErrorInCellOutput(notebookDocument, venvNoRegPath); + await verifyIPyKernelPromptDisplayed(prompt, venvNoRegPath.fsPath); + await verifyErrorInCellOutput(notebookDocument, venvNoRegPath.fsPath); // Now install ipykernel and ensure we can run a cell & that it runs against the right environment. prompt.reset(); @@ -409,7 +418,7 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { const sysExecutable = getCellOutputs(lastCodeCell).trim().toLowerCase(); assert.ok( - areInterpreterPathsSame(venvNoRegPath.toLowerCase(), sysExecutable.toLowerCase()), + areInterpreterPathsSame(venvNoRegPath, Uri.file(sysExecutable)), `Python paths do not match ${venvNoRegPath}, ${sysExecutable}.` ); }); @@ -433,11 +442,11 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { (doc) => doc.uri.toString() === activeInteractiveWindow?.notebookUri?.toString() )!; - await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath); + await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath.fsPath); await sleep(500); - await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath); + await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath.fsPath); await sleep(500); - await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath); + await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath.fsPath); // Now lets install, all cells should run successfully. prompt.clickButton(Common.install()); @@ -476,20 +485,20 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { )!; // Verify and wait a few seconds, in the past we'd get a couple of prompts. - await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath); + await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath.fsPath); await sleep(500); - await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath); + await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath.fsPath); await sleep(500); - await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath); + await verifyIPyKernelPromptDisplayed(prompt, venvPythonPath.fsPath); // Verify all cells have errors. const [cell1, cell2, cell3] = notebookDocument! .getCells() .filter((cell) => cell.kind === NotebookCellKind.Code); await Promise.all([ - verifyErrorInCellOutput(notebookDocument, venvPythonPath, cell1), - verifyErrorInCellOutput(notebookDocument, venvPythonPath, cell2), - verifyErrorInCellOutput(notebookDocument, venvPythonPath, cell3) + verifyErrorInCellOutput(notebookDocument, venvPythonPath.fsPath, cell1), + verifyErrorInCellOutput(notebookDocument, venvPythonPath.fsPath, cell2), + verifyErrorInCellOutput(notebookDocument, venvPythonPath.fsPath, cell3) ]); }); @@ -503,7 +512,7 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { await closeNotebooksAndCleanUpAfterTests(); // Un-install IpyKernel - await uninstallIPyKernel(venvPythonPath); + await uninstallIPyKernel(venvPythonPath.fsPath); nbFile = await createTemporaryNotebook(templateIPynbFile, disposables); await openNotebookAndInstallIpyKernelWhenRunningCell(venvPythonPath); @@ -518,8 +527,8 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { await closeNotebooksAndCleanUpAfterTests(); // Un-install IpyKernel - await uninstallIPyKernel(venvPythonPath); - await uninstallIPyKernel(venvNoRegPath); + await uninstallIPyKernel(venvPythonPath.fsPath); + await uninstallIPyKernel(venvNoRegPath.fsPath); nbFile = await createTemporaryNotebook(templateIPynbFile, disposables); await openNotebookAndInstallIpyKernelWhenRunningCell(venvPythonPath, venvNoRegPath); @@ -534,8 +543,8 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { await closeNotebooksAndCleanUpAfterTests(); // Un-install IpyKernel - await uninstallIPyKernel(venvPythonPath); - await installIPyKernel(venvNoRegPath); + await uninstallIPyKernel(venvPythonPath.fsPath); + await installIPyKernel(venvNoRegPath.fsPath); nbFile = await createTemporaryNotebook(templateIPynbFile, disposables); await openNotebookAndInstallIpyKernelWhenRunningCell(venvPythonPath, venvNoRegPath, 'DoNotInstallIPyKernel'); @@ -551,7 +560,7 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { // Un-install IpyKernel console.log('Step2'); - await uninstallIPyKernel(venvPythonPath); + await uninstallIPyKernel(venvPythonPath.fsPath); // Now that IPyKernel is missing, if we attempt to restart a kernel, we should get a prompt. // Previously things just hang at weird spots, its not a likely scenario, but this test ensures the code works as expected. @@ -581,7 +590,7 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { await openNotebookAndInstallIpyKernelWhenRunningCell(venvPythonPath); // Un-install IpyKernel - await uninstallIPyKernel(venvPythonPath); + await uninstallIPyKernel(venvPythonPath.fsPath); // Now that IPyKernel is missing, if we attempt to restart a kernel, we should get a prompt. // Previously things just hang at weird spots, its not a likely scenario, but this test ensures the code works as expected. @@ -666,7 +675,7 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { const promptToInstall = await clickInstallFromIPyKernelPrompt(); const kernelStartSpy = sinon.spy(Kernel.prototype, 'start'); console.log('Step1'); - await uninstallIPyKernel(venvPythonPath); + await uninstallIPyKernel(venvPythonPath.fsPath); console.log('Step2'); await openNotebook(nbFile); console.log('Step3'); @@ -772,8 +781,8 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { * 3. Verify the Kernel points to the right interpreter */ async function openNotebookAndInstallIpyKernelWhenRunningCell( - interpreterPath: string, - interpreterOfNewKernelToSelect?: string, + interpreterPath: Uri, + interpreterOfNewKernelToSelect?: Uri, ipykernelInstallRequirement: 'DoNotInstallIPyKernel' | 'ShouldInstallIPYKernel' = 'ShouldInstallIPYKernel' ) { // Highjack the IPyKernel not installed prompt and click the appropriate button. @@ -824,8 +833,10 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { const output = getCellOutputs(cell).trim(); const expectedInterpreterPath = interpreterOfNewKernelToSelect || interpreterPath; assert.isTrue( - areInterpreterPathsSame(expectedInterpreterPath.toLowerCase(), output.toLocaleLowerCase()), - `Kernel points to ${getDisplayPath(output)} but expected ${getDisplayPath(expectedInterpreterPath)}` + areInterpreterPathsSame(expectedInterpreterPath, Uri.file(output)), + `Kernel points to ${getDisplayPathFromLocalFile(output)} but expected ${getDisplayPath( + expectedInterpreterPath + )}` ); // Verify ipykernel was not installed if not required && vice versa. @@ -838,7 +849,9 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { .getCalls() .map((call) => { const args: Parameters = call.args as any; - return `${ProductNames.get(args[0])} ${getDisplayPath(args[1]?.path.toString())}`; + return `${ProductNames.get(args[0])} ${getDisplayPath( + isUri(args[1]) ? args[1] : args[1]?.uri + )}`; }) .join('\n')}` ); @@ -876,7 +889,7 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { type Awaited = T extends PromiseLike ? U : T; function hookupKernelSelected( promptToInstall: Awaited>, - pythonPathToNewKernel: string, + pythonPathToNewKernel: Uri, ipykernelInstallRequirement: 'DoNotInstallIPyKernel' | 'ShouldInstallIPYKernel' = 'ShouldInstallIPYKernel' ) { // Get the controller that should be selected. @@ -887,7 +900,7 @@ suite('DataScience Install IPyKernel (slow) (install)', function () { (item) => item.controller.notebookType === JupyterNotebookView && item.connection.kind === 'startUsingPythonInterpreter' && - areInterpreterPathsSame(item.connection.interpreter.path, pythonPathToNewKernel) + areInterpreterPathsSame(item.connection.interpreter.uri, pythonPathToNewKernel) ); if (!controller) { const registeredControllers = controllerManager diff --git a/src/test/datascience/jupyter/kernels/jupyterKernelService.unit.test.ts b/src/test/datascience/jupyter/kernels/jupyterKernelService.unit.test.ts index 0709942902e..693c1d264b4 100644 --- a/src/test/datascience/jupyter/kernels/jupyterKernelService.unit.test.ts +++ b/src/test/datascience/jupyter/kernels/jupyterKernelService.unit.test.ts @@ -13,13 +13,13 @@ import { IEnvironmentActivationService } from '../../../../platform/interpreter/ import { EnvironmentType } from '../../../../platform/pythonEnvironments/info'; import { EXTENSION_ROOT_DIR } from '../../../../platform/constants.node'; import * as path from '../../../../platform/vscode-path/path'; -import { getOSType, OSType } from '../../../common.node'; -import { CancellationTokenSource } from 'vscode'; +import { CancellationTokenSource, Uri } from 'vscode'; import { EnvironmentVariablesService } from '../../../../platform/common/variables/environment.node'; import { arePathsSame } from '../../../../platform/common/platform/fileUtils.node'; import { JupyterKernelService } from '../../../../kernels/jupyter/jupyterKernelService.node'; import { JupyterPaths } from '../../../../kernels/raw/finder/jupyterPaths.node'; import { DisplayOptions } from '../../../../kernels/displayOptions.node'; +import { getOSType, OSType } from '../../../../platform/common/utils/platform'; // eslint-disable-next-line suite('DataScience - JupyterKernelService', () => { @@ -27,7 +27,7 @@ suite('DataScience - JupyterKernelService', () => { let kernelDependencyService: IKernelDependencyService; let fs: IFileSystem; let appEnv: IEnvironmentActivationService; - let testWorkspaceFolder: string; + let testWorkspaceFolder: Uri; const pathVariable = getOSType() === OSType.Windows ? 'PATH' : 'Path'; // Set of kernels. Generated this by running the localKernelFinder unit test and stringifying @@ -41,12 +41,12 @@ suite('DataScience - JupyterKernelService', () => { name: '70cbf3ad892a7619808baecec09fc6109e05177247350ed666cd97ce04371665', argv: ['python'], language: 'python', - path: 'python', + uri: Uri.file('python'), display_name: 'Python 3 Environment' }, interpreter: { displayName: 'Python 3 Environment', - path: '/usr/bin/python3', + uri: Uri.file('/usr/bin/python3'), sysPrefix: 'python', version: { major: 3, minor: 8, raw: '3.8', build: ['0'], patch: 0, prerelease: ['0'] } }, @@ -60,12 +60,12 @@ suite('DataScience - JupyterKernelService', () => { name: '92d78b5b048d9cbeebb9834099d399dea5384db6f02b0829c247cc4679e7cb5d', argv: ['python'], language: 'python', - path: 'python', + uri: Uri.file('python'), display_name: 'Conda Environment' }, interpreter: { displayName: 'Conda Environment', - path: '/usr/bin/conda/python3', + uri: Uri.file('/usr/bin/conda/python3'), sysPrefix: 'conda', envType: EnvironmentType.Conda }, @@ -78,7 +78,7 @@ suite('DataScience - JupyterKernelService', () => { name: 'python3', argv: ['/usr/bin/python3'], language: 'python', - path: '/usr/bin/python3', + uri: Uri.file('/usr/bin/python3'), display_name: 'Python 3 on Disk', metadata: { interpreter: { @@ -91,7 +91,7 @@ suite('DataScience - JupyterKernelService', () => { }, interpreter: { displayName: 'Python 3 Environment', - path: '/usr/bin/python3', + uri: Uri.file('/usr/bin/python3'), sysPrefix: 'python', version: { major: 3, minor: 8, raw: '3.8', build: ['0'], patch: 0, prerelease: ['0'] } }, @@ -104,7 +104,7 @@ suite('DataScience - JupyterKernelService', () => { name: 'julia', argv: ['/usr/bin/julia'], language: 'julia', - path: '/usr/bin/julia', + uri: Uri.file('/usr/bin/julia'), display_name: 'Julia on Disk' }, id: '3' @@ -116,12 +116,12 @@ suite('DataScience - JupyterKernelService', () => { name: 'python2', argv: ['/usr/bin/python'], language: 'python', - path: '/usr/bin/python', + uri: Uri.file('/usr/bin/python'), display_name: 'Python 2 on Disk' }, interpreter: { displayName: 'Python 2 Environment', - path: '/usr/bin/python', + uri: Uri.file('/usr/bin/python'), sysPrefix: 'python', version: { major: 2, minor: 7, raw: '2.7', build: ['0'], patch: 0, prerelease: ['0'] } }, @@ -134,7 +134,7 @@ suite('DataScience - JupyterKernelService', () => { name: 'python3', argv: ['/usr/bin/python3'], language: 'python', - path: '/usr/bin/python3', + uri: Uri.file('/usr/bin/python3'), display_name: 'Python 3 on Disk', metadata: { interpreter: { @@ -147,7 +147,7 @@ suite('DataScience - JupyterKernelService', () => { }, interpreter: { displayName: 'Python 3 Environment', - path: '/usr/bin/python3', + uri: Uri.file('/usr/bin/python3'), sysPrefix: 'python', version: { major: 3, minor: 8, raw: '3.8', build: ['0'], patch: 0, prerelease: ['0'] } }, @@ -160,7 +160,7 @@ suite('DataScience - JupyterKernelService', () => { name: 'julia', argv: ['/usr/bin/julia'], language: 'julia', - path: '/usr/bin/julia', + uri: Uri.file('/usr/bin/julia'), display_name: 'Julia on Disk' }, id: '6' @@ -172,12 +172,12 @@ suite('DataScience - JupyterKernelService', () => { name: 'python2', argv: ['/usr/bin/python'], language: 'python', - path: '/usr/bin/python', + uri: Uri.file('/usr/bin/python'), display_name: 'Python 2 on Disk' }, interpreter: { displayName: 'Python 2 Environment', - path: '/usr/bin/python', + uri: Uri.file('/usr/bin/python'), sysPrefix: 'python', version: { major: 2, minor: 7, raw: '2.7', build: ['0'], patch: 0, prerelease: ['0'] } }, @@ -190,7 +190,7 @@ suite('DataScience - JupyterKernelService', () => { name: 'python3', argv: ['/usr/bin/python3'], language: 'python', - path: '/usr/bin/python3', + uri: Uri.file('/usr/bin/python3'), display_name: 'Python 3 on Disk', metadata: { interpreter: { @@ -203,7 +203,7 @@ suite('DataScience - JupyterKernelService', () => { }, interpreter: { displayName: 'Python 3 Environment', - path: '/usr/bin/python3', + uri: Uri.file('/usr/bin/python3'), sysPrefix: 'python', version: { major: 3, minor: 8, raw: '3.8', build: ['0'], patch: 0, prerelease: ['0'] } }, @@ -216,7 +216,7 @@ suite('DataScience - JupyterKernelService', () => { name: 'julia', argv: ['/usr/bin/julia'], language: 'julia', - path: '/usr/bin/julia', + uri: Uri.file('/usr/bin/julia'), display_name: 'Julia on Disk' }, id: '9' @@ -228,12 +228,12 @@ suite('DataScience - JupyterKernelService', () => { name: 'python2', argv: ['/usr/bin/python'], language: 'python', - path: '/usr/bin/python', + uri: Uri.file('/usr/bin/python'), display_name: 'Python 2 on Disk' }, interpreter: { displayName: 'Python 2 Environment', - path: '/usr/bin/python', + uri: Uri.file('/usr/bin/python'), sysPrefix: 'python', version: { major: 2, minor: 7, raw: '2.7', build: ['0'], patch: 0, prerelease: ['0'] } }, @@ -246,7 +246,7 @@ suite('DataScience - JupyterKernelService', () => { name: 'e10e222d04b8ec3cc7034c3de1b1269b088e2bcd875030a8acab068e59af3990', argv: ['python', '-m', 'ipykernel_launcher', '-f', '{connection_file}'], language: 'python', - path: 'python', + uri: Uri.file('python'), display_name: 'Conda base environment', metadata: { interpreter: { @@ -262,7 +262,7 @@ suite('DataScience - JupyterKernelService', () => { }, interpreter: { displayName: 'Conda base environment', - path: '/usr/conda/envs/base/python', + uri: Uri.file('/usr/conda/envs/base/python'), sysPrefix: 'conda', envType: EnvironmentType.Conda }, @@ -275,7 +275,7 @@ suite('DataScience - JupyterKernelService', () => { name: 'sampleEnv', argv: ['/usr/don/home/envs/sample/bin/python'], language: 'python', - path: '/usr/don/home/envs/sample/bin/python', + uri: Uri.file('/usr/don/home/envs/sample/bin/python'), display_name: 'Kernel with custom env Variable', metadata: { interpreter: { @@ -291,7 +291,7 @@ suite('DataScience - JupyterKernelService', () => { }, interpreter: { displayName: 'Python 3 Environment', - path: '/usr/don/home/envs/sample/bin/python', + uri: Uri.file('/usr/don/home/envs/sample/bin/python'), sysPrefix: 'python', version: { major: 3, minor: 8, raw: '3.8', build: ['0'], patch: 0, prerelease: ['0'] } }, @@ -304,7 +304,7 @@ suite('DataScience - JupyterKernelService', () => { name: 'sampleEnvJulia', argv: ['/usr/don/home/envs/sample/bin/julia'], language: 'julia', - path: '/usr/don/home/envs/sample/bin/python', + uri: Uri.file('/usr/don/home/envs/sample/bin/python'), display_name: 'Julia Kernel with custom env Variable', metadata: { interpreter: { @@ -320,7 +320,7 @@ suite('DataScience - JupyterKernelService', () => { }, interpreter: { displayName: 'Python 3 Environment', - path: '/usr/don/home/envs/sample/bin/python', + uri: Uri.file('/usr/don/home/envs/sample/bin/python'), sysPrefix: 'python', version: { major: 3, minor: 8, raw: '3.8', build: ['0'], patch: 0, prerelease: ['0'] } }, @@ -333,7 +333,7 @@ suite('DataScience - JupyterKernelService', () => { name: 'nameGeneratedByUsWhenRegisteringKernelSpecs', argv: ['/usr/don/home/envs/sample/bin/julia'], language: 'julia', - path: '/usr/don/home/envs/sample/bin/python', + uri: Uri.file('/usr/don/home/envs/sample/bin/python'), display_name: 'Julia Kernel with custom env Variable', metadata: { interpreter: { @@ -349,7 +349,7 @@ suite('DataScience - JupyterKernelService', () => { }, interpreter: { displayName: 'Python 3 Environment', - path: '/usr/don/home/envs/sample/bin/python', + uri: Uri.file('/usr/don/home/envs/sample/bin/python'), sysPrefix: 'python', version: { major: 3, minor: 8, raw: '3.8', build: ['0'], patch: 0, prerelease: ['0'] } }, @@ -377,7 +377,7 @@ suite('DataScience - JupyterKernelService', () => { when(fs.searchLocal(anything(), anything())).thenResolve([]); appEnv = mock(); when(appEnv.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({}); - testWorkspaceFolder = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); + testWorkspaceFolder = Uri.file(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience')); const jupyterPaths = mock(); when(jupyterPaths.getKernelSpecTempRegistrationFolder()).thenResolve(testWorkspaceFolder); kernelService = new JupyterKernelService( @@ -413,7 +413,7 @@ suite('DataScience - JupyterKernelService', () => { assert.ok(kernelsWithInvalidName.length, 'No kernels found with invalid name'); assert.ok(kernelsWithInvalidName[0].kernelSpec?.name, 'first kernel does not have a name'); const kernelSpecPath = path.join( - testWorkspaceFolder, + testWorkspaceFolder.fsPath, kernelsWithInvalidName[0].kernelSpec?.name!, 'kernel.json' ); @@ -473,7 +473,7 @@ suite('DataScience - JupyterKernelService', () => { // Python path must be the first in PATH env variable. assert.strictEqual( kernelJson.env[pathVariable], - `${path.dirname(spec.interpreter!.path)}${path.delimiter}Path1${path.delimiter}Path2` + `${path.dirname(spec.interpreter!.uri.fsPath)}${path.delimiter}Path1${path.delimiter}Path2` ); }); test('Kernel environment preserves env variables from original non-python kernelspec', async () => { @@ -496,7 +496,7 @@ suite('DataScience - JupyterKernelService', () => { // Python path must be the first in PATH env variable. assert.strictEqual( kernelJson.env[pathVariable], - `${path.dirname(spec.interpreter!.path)}${path.delimiter}Path1${path.delimiter}Path2` + `${path.dirname(spec.interpreter!.uri.fsPath)}${path.delimiter}Path1${path.delimiter}Path2` ); }); test('Verify registration of the kernelspec', async () => { @@ -523,7 +523,7 @@ suite('DataScience - JupyterKernelService', () => { // Python path must be the first in PATH env variable. assert.strictEqual( kernelJson.env[pathVariable], - `${path.dirname(spec.interpreter!.path)}${path.delimiter}Path1${path.delimiter}Path2` + `${path.dirname(spec.interpreter!.uri.fsPath)}${path.delimiter}Path1${path.delimiter}Path2` ); // capture(fs.localFileExists) }); @@ -552,7 +552,7 @@ suite('DataScience - JupyterKernelService', () => { // Python path must be the first in PATH env variable. assert.strictEqual( kernelJson.env[pathVariable], - `${path.dirname(spec.interpreter!.path)}${path.delimiter}Path1${path.delimiter}Path2` + `${path.dirname(spec.interpreter!.uri.fsPath)}${path.delimiter}Path1${path.delimiter}Path2` ); // capture(fs.localFileExists) }); diff --git a/src/test/datascience/jupyter/kernels/kernelDependencyService.unit.test.ts b/src/test/datascience/jupyter/kernels/kernelDependencyService.unit.test.ts index 619f4fd1303..8522e4be52a 100644 --- a/src/test/datascience/jupyter/kernels/kernelDependencyService.unit.test.ts +++ b/src/test/datascience/jupyter/kernels/kernelDependencyService.unit.test.ts @@ -38,11 +38,15 @@ suite('DataScience - Kernel Dependency Service', () => { let memento: Memento; let editor: NotebookEditor; - const interpreter = createPythonInterpreter({ displayName: 'name', envType: EnvironmentType.Conda, path: 'abc' }); + const interpreter = createPythonInterpreter({ + displayName: 'name', + envType: EnvironmentType.Conda, + uri: Uri.file('abc') + }); const metadata: PythonKernelConnectionMetadata = { interpreter, kind: 'startUsingPythonInterpreter', - kernelSpec: createInterpreterKernelSpec(interpreter, ''), + kernelSpec: createInterpreterKernelSpec(interpreter, Uri.file('')), id: '1' }; setup(() => { diff --git a/src/test/datascience/jupyterServer.node.ts b/src/test/datascience/jupyterServer.node.ts index 98de24938c6..90d2a78b1f8 100644 --- a/src/test/datascience/jupyterServer.node.ts +++ b/src/test/datascience/jupyterServer.node.ts @@ -105,7 +105,7 @@ export class JupyterServer implements IAsyncDisposable { const api = await initialize(); const pythonExecFactory = api.serviceContainer.get(IPythonExecutionFactory); const pythonExecutionService = await pythonExecFactory.create({ - interpreter: { path: PYTHON_PATH } as PythonEnvironment + interpreter: { uri: Uri.file(PYTHON_PATH) } as PythonEnvironment }); const notebookArgs = [ 'notebook', diff --git a/src/test/datascience/kernel-launcher/kernelFinder.vscode.test.node.ts b/src/test/datascience/kernel-launcher/kernelFinder.vscode.test.node.ts index 93343b5aa53..f23c1f8e426 100644 --- a/src/test/datascience/kernel-launcher/kernelFinder.vscode.test.node.ts +++ b/src/test/datascience/kernel-launcher/kernelFinder.vscode.test.node.ts @@ -61,10 +61,10 @@ suite('DataScience - Kernels Finder', () => { } assert.isTrue( - areInterpreterPathsSame(kernelSpec.interpreter.path.toLowerCase(), interpreter?.path.toLocaleLowerCase()), + areInterpreterPathsSame(kernelSpec.interpreter.uri, interpreter?.uri), `No interpreter found, kernelspec interpreter is ${getDisplayPath( - kernelSpec.interpreter.path - )} but expected ${getDisplayPath(interpreter?.path)}` + kernelSpec.interpreter.uri + )} but expected ${getDisplayPath(interpreter?.uri)}` ); }); test('Interpreter kernel returned if kernelspec metadata not provided', async () => { @@ -80,10 +80,10 @@ suite('DataScience - Kernels Finder', () => { throw new Error('Kernelspec & interpreter info should not be empty'); } assert.isTrue( - areInterpreterPathsSame(kernelSpec.interpreter.path.toLowerCase(), interpreter?.path.toLocaleLowerCase()), + areInterpreterPathsSame(kernelSpec.interpreter.uri, interpreter?.uri), `No interpreter found, kernelspec interpreter is ${getDisplayPath( - kernelSpec.interpreter.path - )} but expected ${getDisplayPath(interpreter?.path)}` + kernelSpec.interpreter.uri + )} but expected ${getDisplayPath(interpreter?.uri)}` ); }); test('Can find a Python kernel based on language', async () => { diff --git a/src/test/datascience/kernel-launcher/localKernelFinder.unit.test.ts b/src/test/datascience/kernel-launcher/localKernelFinder.unit.test.ts index ec296d4c910..24568c35c4a 100644 --- a/src/test/datascience/kernel-launcher/localKernelFinder.unit.test.ts +++ b/src/test/datascience/kernel-launcher/localKernelFinder.unit.test.ts @@ -5,10 +5,10 @@ import { assert } from 'chai'; import * as path from '../../../platform/vscode-path/path'; +import * as uriPath from '../../../platform/vscode-path/resources'; import * as fsExtra from 'fs-extra'; import * as sinon from 'sinon'; import { anything, instance, mock, when, verify } from 'ts-mockito'; -import { PathUtils } from '../../../platform/common/platform/pathUtils.node'; import { IPlatformService } from '../../../platform/common/platform/types'; import { IInterpreterService } from '../../../platform/interpreter/contracts.node'; import { WorkspaceService } from '../../../platform/common/application/workspace'; @@ -27,14 +27,12 @@ import type { KernelSpec } from '@jupyterlab/services'; import { EnvironmentType, PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { IPythonExtensionChecker } from '../../../platform/api/types'; import { PYTHON_LANGUAGE } from '../../../platform/common/constants'; -import { getOSType } from '../../common.node'; +import * as platform from '../../../platform/common/utils/platform'; import { EventEmitter, Memento, Uri } from 'vscode'; import { IDisposable, IExtensionContext } from '../../../platform/common/types'; import { getInterpreterHash } from '../../../platform/pythonEnvironments/info/interpreter.node'; -import { OSType } from '../../../platform/common/utils/platform'; import { disposeAllDisposables } from '../../../platform/common/helpers'; import { KernelConnectionMetadata, LocalKernelConnectionMetadata } from '../../../platform/../kernels/types'; -import { getDisplayPath } from '../../../platform/common/platform/fs-paths'; import { arePathsSame } from '../../../platform/common/platform/fileUtils.node'; import { JupyterPaths } from '../../../kernels/raw/finder/jupyterPaths.node'; import { LocalKernelFinder } from '../../../kernels/raw/finder/localKernelFinder.node'; @@ -43,6 +41,7 @@ import { LocalKnownPathKernelSpecFinder } from '../../../kernels/raw/finder/loca import { LocalPythonAndRelatedNonPythonKernelSpecFinder } from '../../../kernels/raw/finder/localPythonAndRelatedNonPythonKernelSpecFinder.node'; import { ILocalKernelFinder } from '../../../kernels/raw/types'; import { IFileSystem } from '../../../platform/common/platform/types.node'; +import { getDisplayPathFromLocalFile } from '../../../platform/common/platform/fs-paths.node'; [false, true].forEach((isWindows) => { suite(`Local Kernel Finder ${isWindows ? 'Windows' : 'Unix'}`, () => { @@ -52,9 +51,8 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; let fs: IFileSystem; let extensionChecker: IPythonExtensionChecker; const disposables: IDisposable[] = []; - let globalSpecPath: string; - let tempDirForKernelSpecs: string; - const pathSeparator = getOSType() === OSType.Windows ? '\\' : '/'; + let globalSpecPath: Uri | undefined; + let tempDirForKernelSpecs: Uri; let jupyterPaths: JupyterPaths; type TestData = { interpreters?: ( @@ -78,6 +76,8 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; async function initialize(testData: TestData, activeInterpreter?: PythonEnvironment) { const getRealPathStub = sinon.stub(fsExtra, 'realpath'); getRealPathStub.returnsArg(0); + const getOSTypeStub = sinon.stub(platform, 'getOSType'); + getOSTypeStub.returns(isWindows ? platform.OSType.Windows : platform.OSType.Linux); interpreterService = mock(InterpreterService); // Ensure the active Interpreter is in the list of interpreters. if (activeInterpreter) { @@ -99,7 +99,6 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; fs = mock(FileSystem); when(fs.deleteLocalFile(anything())).thenResolve(); when(fs.localFileExists(anything())).thenResolve(true); - const pathUtils = new PathUtils(isWindows); const workspaceService = mock(WorkspaceService); const testWorkspaceFolder = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); @@ -121,7 +120,6 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; when(memento.update(anything(), anything())).thenResolve(); jupyterPaths = new JupyterPaths( instance(platformService), - pathUtils, instance(envVarsProvider), disposables, instance(memento), @@ -133,23 +131,23 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; (testData.interpreters || []).forEach((interpreter) => { if ('interpreter' in interpreter) { (interpreter.kernelSpecs || []).forEach((kernelSpec) => { - const jsonFile = [ + const jsonFile = path.join( interpreter.interpreter.sysPrefix, 'share', 'jupyter', 'kernels', kernelSpec.name, 'kernel.json' - ].join(pathSeparator); + ); kernelSpecsBySpecFile.set(jsonFile, kernelSpec); }); } }); - globalSpecPath = (await jupyterPaths.getKernelSpecRootPath()) as unknown as string; - tempDirForKernelSpecs = (await jupyterPaths.getKernelSpecTempRegistrationFolder()) as unknown as string; + globalSpecPath = await jupyterPaths.getKernelSpecRootPath(); + tempDirForKernelSpecs = await jupyterPaths.getKernelSpecTempRegistrationFolder(); await Promise.all( (testData.globalKernelSpecs || []).map(async (kernelSpec) => { - const jsonFile = [globalSpecPath, kernelSpec.name, 'kernel.json'].join(pathSeparator); + const jsonFile = path.join(globalSpecPath!.fsPath, kernelSpec.name, 'kernel.json'); kernelSpecsBySpecFile.set(jsonFile.replace(/\\/g, '/'), kernelSpec); }) ); @@ -161,9 +159,9 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; : Promise.reject(`File "${f}" not found.`); }); when(fs.searchLocal(anything(), anything(), true)).thenCall((_p, c: string, _d) => { - if (c === globalSpecPath) { + if (c === globalSpecPath?.fsPath) { return (testData.globalKernelSpecs || []).map((kernelSpec) => - [kernelSpec.name, 'kernel.json'].join(pathSeparator) + path.join(kernelSpec.name, 'kernel.json') ); } const interpreter = (testData.interpreters || []).find((item) => @@ -171,7 +169,7 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; ); if (interpreter && 'interpreter' in interpreter) { return (interpreter.kernelSpecs || []).map((kernelSpec) => - [kernelSpec.name, 'kernel.json'].join(pathSeparator) + path.join(kernelSpec.name, 'kernel.json') ); } return []; @@ -271,7 +269,7 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; } }; const python2Global: PythonEnvironment = { - path: isWindows ? 'C:/Python/Python2/scripts/python.exe' : '/usr/bin/python27', + uri: Uri.file(isWindows ? 'C:/Python/Python2/scripts/python.exe' : '/usr/bin/python27'), sysPrefix: isWindows ? 'C:/Python/Python2' : '/usr', displayName: 'Python 2.7', envType: EnvironmentType.Global, @@ -279,7 +277,7 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; version: { major: 2, minor: 7, patch: 0, build: [], prerelease: [], raw: '2.7.0' } }; const python36Global: PythonEnvironment = { - path: isWindows ? 'C:/Python/Python3.6/scripts/python.exe' : '/usr/bin/python36', + uri: Uri.file(isWindows ? 'C:/Python/Python3.6/scripts/python.exe' : '/usr/bin/python36'), sysPrefix: isWindows ? 'C:/Python/Python3.6' : '/usr', displayName: 'Python 3.6', envType: EnvironmentType.Global, @@ -287,7 +285,7 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; version: { major: 3, minor: 6, patch: 0, build: [], prerelease: [], raw: '3.6.0' } }; const python37Global: PythonEnvironment = { - path: isWindows ? 'C:/Python/Python3.7/scripts/python.exe' : '/usr/bin/python37', + uri: Uri.file(isWindows ? 'C:/Python/Python3.7/scripts/python.exe' : '/usr/bin/python37'), sysPrefix: isWindows ? 'C:/Python/Python3.7' : '/usr', displayName: 'Python 3.7', envType: EnvironmentType.Global, @@ -295,7 +293,9 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; version: { major: 3, minor: 7, patch: 0, build: [], prerelease: [], raw: '3.6.0' } }; const python39PyEnv_HelloWorld: PythonEnvironment = { - path: isWindows ? 'C:/pyenv/envs/temp/scripts/python.exe' : '/users/username/pyenv/envs/temp/python', + uri: Uri.file( + isWindows ? 'C:/pyenv/envs/temp/scripts/python.exe' : '/users/username/pyenv/envs/temp/python' + ), sysPrefix: isWindows ? 'C:/pyenv/envs/temp' : '/users/username/pyenv/envs/temp', displayName: 'Temporary Python 3.9', envName: 'temp', @@ -304,7 +304,9 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; version: { major: 3, minor: 9, patch: 0, build: [], prerelease: [], raw: '3.9.0' } }; const python38PyEnv_temp1: PythonEnvironment = { - path: isWindows ? 'C:/pyenv/envs/temp1/scripts/python.exe' : '/users/username/pyenv/envs/temp1/bin/python', + uri: Uri.file( + isWindows ? 'C:/pyenv/envs/temp1/scripts/python.exe' : '/users/username/pyenv/envs/temp1/bin/python' + ), sysPrefix: isWindows ? 'C:/pyenv/envs/temp1' : '/users/username/pyenv/envs/temp1', displayName: 'Temporary Python 3.8 64bit Environment', envName: 'temp1', @@ -313,7 +315,9 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; version: { major: 3, minor: 8, patch: 0, build: [], prerelease: [], raw: '3.8.0' } }; const python38PyEnv_temp2_duplicateNameAsTemp1: PythonEnvironment = { - path: isWindows ? 'C:/pyenv/envs/temp2/scripts/python.exe' : '/users/username/pyenv/envs/temp2/bin/python', + uri: Uri.file( + isWindows ? 'C:/pyenv/envs/temp2/scripts/python.exe' : '/users/username/pyenv/envs/temp2/bin/python' + ), sysPrefix: isWindows ? 'C:/pyenv/envs/temp2' : '/users/username/pyenv/envs/temp2', displayName: 'Temporary Python 3.8 64bit Environment', envName: 'temp2', @@ -322,7 +326,9 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; version: { major: 3, minor: 8, patch: 0, build: [], prerelease: [], raw: '3.8.0' } }; const python38PyEnv_temp3_duplicateNameAsTemp1: PythonEnvironment = { - path: isWindows ? 'C:/pyenv/envs/temp3/scripts/python.exe' : '/users/username/pyenv/envs/temp3/bin/python', + uri: Uri.file( + isWindows ? 'C:/pyenv/envs/temp3/scripts/python.exe' : '/users/username/pyenv/envs/temp3/bin/python' + ), sysPrefix: isWindows ? 'C:/pyenv/envs/temp3' : '/users/username/pyenv/envs/temp3', displayName: 'Temporary Python 3.8 64bit Environment', envName: 'temp3', @@ -335,7 +341,9 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; * Except on unix the executable is not in a bin folder. */ const python38PyEnv_temp4_duplicateNameAsTemp1ButNoBin: PythonEnvironment = { - path: isWindows ? 'C:/pyenv/envs/temp4/scripts/python.exe' : '/users/username/pyenv/envs/temp4/python', + uri: Uri.file( + isWindows ? 'C:/pyenv/envs/temp4/scripts/python.exe' : '/users/username/pyenv/envs/temp4/python' + ), sysPrefix: isWindows ? 'C:/pyenv/envs/temp4' : '/users/username/pyenv/envs/temp4', displayName: 'Temporary Python 3.8 64bit Environment', envName: 'temp4', @@ -345,7 +353,9 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; }; const duplicate1OfPython38PyEnv_temp1 = python38PyEnv_temp1; const python38VenvEnv: PythonEnvironment = { - path: isWindows ? 'C:/temp/venv/.venv/scripts/python.exe' : '/users/username/temp/.venv/bin/python', + uri: Uri.file( + isWindows ? 'C:/temp/venv/.venv/scripts/python.exe' : '/users/username/temp/.venv/bin/python' + ), sysPrefix: isWindows ? 'C:/temp/venv/.venv' : '/users/username/temp/.venv', displayName: 'Virtual Env Python 3.8', envName: '.venv', @@ -354,7 +364,7 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; version: { major: 3, minor: 8, patch: 0, build: [], prerelease: [], raw: '3.8.0' } }; const condaEnv1: PythonEnvironment = { - path: isWindows ? 'C:/conda/envs/env1/scripts/python.exe' : '/conda/envs/env1/bin/python', + uri: Uri.file(isWindows ? 'C:/conda/envs/env1/scripts/python.exe' : '/conda/envs/env1/bin/python'), sysPrefix: isWindows ? 'C:/conda/envs/env1' : '/conda/envs/env1', envName: 'env1', displayName: 'Conda Env1 3.6', @@ -375,13 +385,13 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; const python2spec: KernelSpec.ISpecModel = { display_name: 'Python 2 on Disk', name: 'python2Custom', - argv: [python2Global.path, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], + argv: [python2Global.uri.fsPath, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], language: 'python', resources: {} }; const fullyQualifiedPythonKernelSpec: KernelSpec.ISpecModel = { - argv: [python38VenvEnv.path, '-m', 'ipykernel_launcher', '-f', '{connection_file}', 'moreargs'], + argv: [python38VenvEnv.uri.fsPath, '-m', 'ipykernel_launcher', '-f', '{connection_file}', 'moreargs'], display_name: 'Custom .venv Kernel', language: 'python', name: 'fullyQualifiedPythonKernelSpec', @@ -389,21 +399,21 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; }; const fullyQualifiedPythonKernelSpecForGlobalPython36: KernelSpec.ISpecModel = { - argv: [python36Global.path, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], + argv: [python36Global.uri.fsPath, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], display_name: 'Custom Kernel for Global Python 36', language: 'python', name: 'fullyQualifiedPythonKernelSpecForGlobalPython36', resources: {} }; const fullyQualifiedPythonKernelSpecForGlobalPython36WithCustomEnvVars: KernelSpec.ISpecModel = { - argv: [python36Global.path, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], + argv: [python36Global.uri.fsPath, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], display_name: 'Custom Kernel for Global Python 36 with Custom Env Vars', language: 'python', name: 'fullyQualifiedPythonKernelSpecForGlobalPython36WithCustomEnvVars', resources: {} }; const fullyQualifiedPythonKernelSpecWithEnv: KernelSpec.ISpecModel = { - argv: [python38VenvEnv.path, '-m', 'ipykernel_launcher', '-f', '{connection_file}', 'moreargs'], + argv: [python38VenvEnv.uri.fsPath, '-m', 'ipykernel_launcher', '-f', '{connection_file}', 'moreargs'], display_name: 'Custom .venv Kernel with Env Vars', language: 'python', name: 'fullyQualifiedPythonKernelSpecWithEnv', @@ -413,7 +423,7 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; } }; const kernelspecRegisteredByOlderVersionOfExtension: KernelSpec.ISpecModel = { - argv: [python38VenvEnv.path, '-m', 'ipykernel_launcher', '-f', '{connection_file}', 'moreargs'], + argv: [python38VenvEnv.uri.fsPath, '-m', 'ipykernel_launcher', '-f', '{connection_file}', 'moreargs'], display_name: 'Kernelspec registered by older version of extension', language: 'python', // Most recent versions of extensions used a custom prefix in kernelnames. @@ -424,7 +434,7 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; } }; const kernelspecRegisteredByVeryOldVersionOfExtension: KernelSpec.ISpecModel = { - argv: [python38VenvEnv.path, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], + argv: [python38VenvEnv.uri.fsPath, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], display_name: 'Kernelspec registered by very old version of extension', language: 'python', // Initial versions of extensions used a GUID in kernelnames & contained the interpreter in metadata. @@ -435,7 +445,12 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; FOO: 'Bar' }, metadata: { - interpreter: { ...python38VenvEnv } + interpreter: { + displayName: python38VenvEnv.displayName, + envName: python38VenvEnv.envName, + path: python38VenvEnv.uri.fsPath, + envPath: undefined + } } }; @@ -455,11 +470,11 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; const expectedKernelSpecs: KernelConnectionMetadata[] = []; await Promise.all( expectedGlobalKernelSpecs.map(async (kernelSpec) => { - const kernelspecFile = [globalSpecPath, kernelSpec.name, 'kernel.json'].join(pathSeparator); + const kernelspecFile = path.join(globalSpecPath!.fsPath, kernelSpec.name, 'kernel.json'); const interpreter = expectedInterpreters.find( - (item) => kernelSpec.language === PYTHON_LANGUAGE && item.path === kernelSpec.argv[0] + (item) => kernelSpec.language === PYTHON_LANGUAGE && item.uri.fsPath === kernelSpec.argv[0] ); - const spec = await loadKernelSpec(kernelspecFile, instance(fs)); + const spec = await loadKernelSpec(Uri.file(kernelspecFile), instance(fs)); if (spec) { expectedKernelSpecs.push({ id: getKernelId(spec!, interpreter), @@ -472,15 +487,15 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; ); await Promise.all( expectedInterpreterKernelSpecFiles.map(async ({ interpreter, kernelspec }) => { - const kernelSpecFile = [ + const kernelSpecFile = path.join( interpreter.sysPrefix, 'share', 'jupyter', 'kernels', kernelspec.name, 'kernel.json' - ].join(pathSeparator); - const spec = await loadKernelSpec(kernelSpecFile, instance(fs), interpreter); + ); + const spec = await loadKernelSpec(Uri.file(kernelSpecFile), instance(fs), interpreter); if (spec) { expectedKernelSpecs.push({ id: getKernelId(spec!, interpreter), @@ -575,9 +590,9 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; const duplicate = ids.get(kernel.id); if (duplicate) { throw new Error( - `Duplicate kernel id found ${kernel.id} (${getDisplayPath( + `Duplicate kernel id found ${kernel.id} (${getDisplayPathFromLocalFile( kernel.kernelSpec.specFile - )}), duplicate of ${duplicate.kernelSpec.display_name} (${getDisplayPath( + )}), duplicate of ${duplicate.kernelSpec.display_name} (${getDisplayPathFromLocalFile( duplicate.kernelSpec.specFile )})` ); @@ -585,7 +600,9 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; if (!kernel.kernelSpec.specFile) { // All kernels must have a specFile defined. throw new Error( - `Kernelspec file not defined for ${kernel.id} (${getDisplayPath(kernel.kernelSpec.specFile)})` + `Kernelspec file not defined for ${kernel.id} (${getDisplayPathFromLocalFile( + kernel.kernelSpec.specFile + )})` ); } ids.set(kernel.id, kernel); @@ -647,7 +664,7 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; assert.strictEqual(actual?.kind, 'startUsingLocalKernelSpec'); assert.strictEqual( actual?.kernelSpec.specFile, - [globalSpecPath, expected.name, 'kernel.json'].join(pathSeparator) + path.join(globalSpecPath!.fsPath, expected.name, 'kernel.json') ); Object.keys(expected).forEach((key) => { // We always mess around with the names, hence don't compare names. @@ -726,21 +743,29 @@ import { IFileSystem } from '../../../platform/common/platform/types.node'; // Verify we deleted the old kernelspecs. const globalKernelSpecDir = await jupyterPaths.getKernelSpecRootPath(); const kernelSpecsToBeDeleted = [ - path.join(globalKernelSpecDir!, kernelspecRegisteredByOlderVersionOfExtension.name, 'kernel.json'), - path.join(globalKernelSpecDir!, kernelspecRegisteredByVeryOldVersionOfExtension.name, 'kernel.json') + uriPath.joinPath( + globalKernelSpecDir!, + kernelspecRegisteredByOlderVersionOfExtension.name, + 'kernel.json' + ), + uriPath.joinPath( + globalKernelSpecDir!, + kernelspecRegisteredByVeryOldVersionOfExtension.name, + 'kernel.json' + ) ]; // Verify files were copied to some other location before being deleted. - verify(fs.copyLocal(kernelSpecsToBeDeleted[0], anything())).calledBefore( - fs.deleteLocalFile(kernelSpecsToBeDeleted[0]) + verify(fs.copyLocal(kernelSpecsToBeDeleted[0].fsPath, anything())).calledBefore( + fs.deleteLocalFile(kernelSpecsToBeDeleted[0].fsPath) ); - verify(fs.copyLocal(kernelSpecsToBeDeleted[1], anything())).calledBefore( - fs.deleteLocalFile(kernelSpecsToBeDeleted[1]) + verify(fs.copyLocal(kernelSpecsToBeDeleted[1].fsPath, anything())).calledBefore( + fs.deleteLocalFile(kernelSpecsToBeDeleted[1].fsPath) ); // Verify files were deleted. - verify(fs.deleteLocalFile(kernelSpecsToBeDeleted[0])).atLeast(1); - verify(fs.deleteLocalFile(kernelSpecsToBeDeleted[1])).atLeast(1); + verify(fs.deleteLocalFile(kernelSpecsToBeDeleted[0].fsPath)).atLeast(1); + verify(fs.deleteLocalFile(kernelSpecsToBeDeleted[1].fsPath)).atLeast(1); }); [ diff --git a/src/test/datascience/kernel-launcher/remoteKernelFinder.unit.test.ts b/src/test/datascience/kernel-launcher/remoteKernelFinder.unit.test.ts index ca311eece90..5345695f8f1 100644 --- a/src/test/datascience/kernel-launcher/remoteKernelFinder.unit.test.ts +++ b/src/test/datascience/kernel-launcher/remoteKernelFinder.unit.test.ts @@ -47,28 +47,28 @@ suite(`Remote Kernel Finder`, () => { name: defaultPython3Name, argv: ['/usr/bin/python3'], language: 'python', - path: 'specFilePath' + uri: Uri.file('specFilePath') }; const python2spec: IJupyterKernelSpec = { display_name: 'Python 2 on Disk', name: 'python2', argv: ['/usr/bin/python'], language: 'python', - path: 'specFilePath' + uri: Uri.file('specFilePath') }; const juliaSpec: IJupyterKernelSpec = { display_name: 'Julia on Disk', name: 'julia', argv: ['/usr/bin/julia'], language: 'julia', - path: 'specFilePath' + uri: Uri.file('specFilePath') }; const interpreterSpec: IJupyterKernelSpec = { display_name: 'Conda interpreter kernel', name: defaultPython3Name, argv: ['python'], language: 'python', - path: 'specFilePath' + uri: Uri.file('specFilePath') }; const python3Kernels: IJupyterKernel[] = ['1', '2', '3'].map((id) => { return { diff --git a/src/test/datascience/kernelLauncher.vscode.test.ts b/src/test/datascience/kernelLauncher.vscode.test.ts index 564a6cf4985..91ff7ae29bb 100644 --- a/src/test/datascience/kernelLauncher.vscode.test.ts +++ b/src/test/datascience/kernelLauncher.vscode.test.ts @@ -18,7 +18,7 @@ import { initialize } from '../initialize.node'; import { PortAttributesProviders } from '../../platform/common/net/portAttributeProvider.node'; import { IDisposable } from '../../platform/common/types'; import { disposeAllDisposables } from '../../platform/common/helpers'; -import { CancellationTokenSource, PortAutoForwardAction } from 'vscode'; +import { CancellationTokenSource, PortAutoForwardAction, Uri } from 'vscode'; import { createRawKernel } from '../../kernels/raw/session/rawKernel.node'; import { IKernelConnection, IKernelLauncher } from '../../kernels/raw/types'; import { IJupyterKernelSpec } from '../../kernels/types'; @@ -37,7 +37,7 @@ suite('DataScience - Kernel Launcher', () => { argv: [PYTHON_PATH, '-m', 'ipykernel_launcher', '-f', `{connection_file}`], env: {}, resources: {}, - path: '' + uri: Uri.file('') }; const disposables: IDisposable[] = []; suiteSetup(async function () { @@ -96,7 +96,7 @@ suite('DataScience - Kernel Launcher', () => { const spec: IJupyterKernelSpec = { name: 'foo', language: 'python', - path: 'python', + uri: Uri.file('python'), display_name: 'foo', argv: [PYTHON_PATH, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], env: { @@ -130,7 +130,7 @@ suite('DataScience - Kernel Launcher', () => { const spec: IJupyterKernelSpec = { name: 'foo', language: 'python', - path: 'python', + uri: Uri.file('python'), display_name: 'foo', argv: [PYTHON_PATH, '-m', 'ipykernel_launcher', '-f', '{connection_file}'], env: { diff --git a/src/test/datascience/kernelProcess.unit.test.ts b/src/test/datascience/kernelProcess.unit.test.ts index 27e08bb5e87..87fcc05f15d 100644 --- a/src/test/datascience/kernelProcess.unit.test.ts +++ b/src/test/datascience/kernelProcess.unit.test.ts @@ -26,7 +26,7 @@ import { IFileSystem } from '../../platform/common/platform/types.node'; import { IPythonExtensionChecker } from '../../platform/api/types'; import { KernelEnvironmentVariablesService } from '../../kernels/raw/launcher/kernelEnvVarsService.node'; import { IDisposable, IJupyterSettings, IOutputChannel } from '../../platform/common/types'; -import { CancellationTokenSource } from 'vscode'; +import { CancellationTokenSource, Uri } from 'vscode'; import { disposeAllDisposables } from '../../platform/common/helpers'; import { noop } from '../core'; import { Observable, Subject } from 'rxjs'; @@ -143,7 +143,7 @@ suite('kernel Process', () => { argv: ['dotnet', 'csharp', '{connection_file}'], display_name: 'C# .NET', name: 'csharp', - path: 'dotnet' + uri: Uri.file('dotnet') }; const tempFile = 'temporary file.json'; when(connectionMetadata.kind).thenReturn('startUsingLocalKernelSpec'); @@ -165,7 +165,7 @@ suite('kernel Process', () => { argv: ['dotnet', 'csharp', '{connection_file}'], display_name: 'C# .NET', name: 'csharp', - path: 'dotnet' + uri: Uri.file('dotnet') }; const tempFile = 'temporary file.json'; when(connectionMetadata.kind).thenReturn('startUsingLocalKernelSpec'); @@ -184,7 +184,7 @@ suite('kernel Process', () => { argv: ['dotnet', 'csharp', '{connection_file}'], display_name: 'C# .NET', name: 'csharp', - path: 'dotnet' + uri: Uri.file('dotnet') }; const tempFile = 'temporary file.json'; when(connectionMetadata.kind).thenReturn('startUsingLocalKernelSpec'); @@ -206,11 +206,11 @@ suite('kernel Process', () => { argv: ['dotnet', 'csharp', '{connection_file}'], display_name: 'C# .NET', name: 'csharp', - path: 'dotnet' + uri: Uri.file('dotnet') }; const tempFile = path.join('tmp', 'temporary file.json'); - const jupyterRuntimeDir = path.join('hello', 'jupyter', 'runtime'); - const expectedConnectionFile = path.join(jupyterRuntimeDir, path.basename(tempFile)); + const jupyterRuntimeDir = Uri.file(path.join('hello', 'jupyter', 'runtime')); + const expectedConnectionFile = path.join(jupyterRuntimeDir.fsPath, path.basename(tempFile)); when(jupyterPaths.getRuntimeDir()).thenResolve(jupyterRuntimeDir); when(connectionMetadata.kind).thenReturn('startUsingLocalKernelSpec'); when(connectionMetadata.kernelSpec).thenReturn(kernelSpec); @@ -243,7 +243,7 @@ suite('kernel Process', () => { argv: ['dotnet', 'csharp', '{connection_file}'], display_name: 'C# .NET', name: 'csharp', - path: 'dotnet' + uri: Uri.file('dotnet') }; const tempFile = path.join('tmp', 'temporary file.json'); when(jupyterPaths.getRuntimeDir()).thenResolve(); @@ -275,11 +275,11 @@ suite('kernel Process', () => { argv: ['python', '-f', '{connection_file}'], display_name: 'Python', name: 'Python3', - path: 'python' + uri: Uri.file('python') }; const tempFile = path.join('tmp', 'temporary file.json'); - const jupyterRuntimeDir = path.join('hello', 'jupyter', 'runtime'); - const expectedConnectionFile = path.join(jupyterRuntimeDir, path.basename(tempFile)); + const jupyterRuntimeDir = Uri.file(path.join('hello', 'jupyter', 'runtime')); + const expectedConnectionFile = path.join(jupyterRuntimeDir.fsPath, path.basename(tempFile)); when(fs.createTemporaryLocalFile(deepEqual(tempFileCreationOptions))).thenResolve({ dispose: noop, filePath: tempFile @@ -324,7 +324,7 @@ suite('kernel Process', () => { argv: ['python', '-f', '{connection_file}'], display_name: 'Python', name: 'Python3', - path: 'python' + uri: Uri.file('python') }; const tempFile = path.join('tmp', 'temporary file.json'); when(fs.createTemporaryLocalFile(deepEqual(tempFileCreationOptions))).thenResolve({ @@ -371,7 +371,7 @@ suite('kernel Process', () => { argv: ['python', '-f', '{connection_file}'], display_name: 'Python', name: 'Python3', - path: 'python' + uri: Uri.file('python') }; when(pythonExecFactory.createDaemon(anything())).thenResolve(instance(pythonProcess)); when(connectionMetadata.kind).thenReturn('startUsingPythonInterpreter'); diff --git a/src/test/datascience/kernelProcess.vscode.test.node.ts b/src/test/datascience/kernelProcess.vscode.test.node.ts index c539894b371..be7059226f4 100644 --- a/src/test/datascience/kernelProcess.vscode.test.node.ts +++ b/src/test/datascience/kernelProcess.vscode.test.node.ts @@ -22,7 +22,7 @@ import { noop } from '../core'; import { EventEmitter } from 'events'; import { disposeAllDisposables } from '../../platform/common/helpers'; import { traceInfo } from '../../platform/logging'; -import { CancellationTokenSource } from 'vscode'; +import { CancellationTokenSource, Uri } from 'vscode'; import { IKernelConnection } from '../../kernels/raw/types'; import { KernelEnvironmentVariablesService } from '../../kernels/raw/launcher/kernelEnvVarsService.node'; import { KernelProcess } from '../../kernels/raw/launcher/kernelProcess.node'; @@ -125,7 +125,7 @@ suite('DataScience - Kernel Process', () => { interrupt_mode: 'message', display_name: '', name: '', - path: '' + uri: Uri.file('') }, kind: 'startUsingLocalKernelSpec' }; @@ -162,7 +162,7 @@ suite('DataScience - Kernel Process', () => { interrupt_mode: 'message', display_name: '', name: '', - path: '' + uri: Uri.file('') }, kind: 'startUsingLocalKernelSpec' }; @@ -197,7 +197,7 @@ suite('DataScience - Kernel Process', () => { interrupt_mode: 'message', display_name: '', name: '', - path: '' + uri: Uri.file('') }, kind: 'startUsingLocalKernelSpec' }; @@ -232,7 +232,7 @@ suite('DataScience - Kernel Process', () => { interrupt_mode: 'message', display_name: '', name: '', - path: '' + uri: Uri.file('') }, kind: 'startUsingLocalKernelSpec' }; diff --git a/src/test/datascience/mockPythonService.ts b/src/test/datascience/mockPythonService.node.ts similarity index 79% rename from src/test/datascience/mockPythonService.ts rename to src/test/datascience/mockPythonService.node.ts index 56259291c44..d77e350e4ed 100644 --- a/src/test/datascience/mockPythonService.ts +++ b/src/test/datascience/mockPythonService.node.ts @@ -24,7 +24,7 @@ export class MockPythonService implements IPythonExecutionService { } public getExecutablePath(): Promise { - return Promise.resolve(this.interpreter.path); + return Promise.resolve(this.interpreter.uri.fsPath); } public isModuleInstalled(_moduleName: string): Promise { @@ -32,25 +32,25 @@ export class MockPythonService implements IPythonExecutionService { } public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult { - return this.procService.execObservable(this.interpreter.path, args, options); + return this.procService.execObservable(this.interpreter.uri.fsPath, args, options); } public execModuleObservable( moduleName: string, args: string[], options: SpawnOptions ): ObservableExecutionResult { - return this.procService.execObservable(this.interpreter.path, ['-m', moduleName, ...args], options); + return this.procService.execObservable(this.interpreter.uri.fsPath, ['-m', moduleName, ...args], options); } public exec(args: string[], options: SpawnOptions): Promise> { - return this.procService.exec(this.interpreter.path, args, options); + return this.procService.exec(this.interpreter.uri.fsPath, args, options); } public execModule(moduleName: string, args: string[], options: SpawnOptions): Promise> { - return this.procService.exec(this.interpreter.path, ['-m', moduleName, ...args], options); + return this.procService.exec(this.interpreter.uri.fsPath, ['-m', moduleName, ...args], options); } public addExecResult(args: (string | RegExp)[], result: () => Promise>) { - this.procService.addExecResult(this.interpreter.path, args, result); + this.procService.addExecResult(this.interpreter.uri.fsPath, args, result); } public addExecModuleResult( @@ -58,11 +58,11 @@ export class MockPythonService implements IPythonExecutionService { args: (string | RegExp)[], result: () => Promise> ) { - this.procService.addExecResult(this.interpreter.path, ['-m', moduleName, ...args], result); + this.procService.addExecResult(this.interpreter.uri.fsPath, ['-m', moduleName, ...args], result); } public addExecObservableResult(args: (string | RegExp)[], result: () => ObservableExecutionResult) { - this.procService.addExecObservableResult(this.interpreter.path, args, result); + this.procService.addExecObservableResult(this.interpreter.uri.fsPath, args, result); } public addExecModuleObservableResult( @@ -70,7 +70,7 @@ export class MockPythonService implements IPythonExecutionService { args: (string | RegExp)[], result: () => ObservableExecutionResult ) { - this.procService.addExecObservableResult(this.interpreter.path, ['-m', moduleName, ...args], result); + this.procService.addExecObservableResult(this.interpreter.uri.fsPath, ['-m', moduleName, ...args], result); } public setDelay(timeout: number | undefined) { @@ -78,6 +78,6 @@ export class MockPythonService implements IPythonExecutionService { } public getExecutionInfo(args: string[]) { - return buildPythonExecInfo(this.interpreter.path, args); + return buildPythonExecInfo(this.interpreter.uri.fsPath, args); } } diff --git a/src/test/datascience/notebook/diagnosticProvider.vscode.test.ts b/src/test/datascience/notebook/diagnosticProvider.vscode.test.ts index 3101311d86f..021109f59b8 100644 --- a/src/test/datascience/notebook/diagnosticProvider.vscode.test.ts +++ b/src/test/datascience/notebook/diagnosticProvider.vscode.test.ts @@ -16,7 +16,7 @@ import { insertCodeCell, createEmptyPythonNotebook, workAroundVSCodeNotebookStartPages -} from './helper'; +} from './helper.node'; import { NotebookDocument, Range } from 'vscode'; import { IExtensionSyncActivationService } from '../../../platform/activation/types'; import { NotebookCellBangInstallDiagnosticsProvider } from '../../../intellisense/diagnosticsProvider.node'; diff --git a/src/test/datascience/notebook/executionService.vscode.test.ts b/src/test/datascience/notebook/executionService.vscode.test.ts index a602e8672f0..76ea9ea08c8 100644 --- a/src/test/datascience/notebook/executionService.vscode.test.ts +++ b/src/test/datascience/notebook/executionService.vscode.test.ts @@ -14,7 +14,7 @@ import { Common } from '../../../platform/common/utils/localize'; import { IVSCodeNotebook } from '../../../platform/common/application/types'; import { traceInfo, traceInfoIfCI } from '../../../platform/logging'; import { IDisposable } from '../../../platform/common/types'; -import { captureScreenShot, getOSType, IExtensionTestApi, OSType, waitForCondition } from '../../common.node'; +import { captureScreenShot, IExtensionTestApi, waitForCondition } from '../../common.node'; import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize.node'; import { closeNotebooksAndCleanUpAfterTests, @@ -43,7 +43,7 @@ import { getCellOutputs, waitForCellHavingOutput, waitForCellExecutionToComplete -} from './helper'; +} from './helper.node'; import { openNotebook } from '../helpers'; import { noop } from '../../../platform/common/utils/misc'; import { getTextOutputValue, hasErrorOutput, translateCellErrorOutput } from '../../../notebooks/helpers.node'; @@ -52,6 +52,7 @@ import { ProductNames } from '../../../kernels/installer/productNames.node'; import { Product } from '../../../kernels/installer/types'; import { IPYTHON_VERSION_CODE, IS_REMOTE_NATIVE_TEST } from '../../constants.node'; import { areInterpreterPathsSame } from '../../../platform/pythonEnvironments/info/interpreter.node'; +import { getOSType, OSType } from '../../../platform/common/utils/platform'; // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports const expectedPromptMessageSuffix = `requires ${ProductNames.get(Product.ipykernel)!} to be installed.`; @@ -399,7 +400,7 @@ suite('DataScience - VSCode Notebook - (Execution) (slow)', function () { // First path in PATH must be the directory where executable is located. assert.ok( - areInterpreterPathsSame(path.dirname(sysExecutable), pathValue[0].toLowerCase()), + areInterpreterPathsSame(Uri.file(path.dirname(sysExecutable)), Uri.file(pathValue[0]), getOSType(), true), `First entry in PATH (${pathValue[0]}) does not point to executable (${sysExecutable})` ); }); @@ -461,7 +462,7 @@ suite('DataScience - VSCode Notebook - (Execution) (slow)', function () { // First path in PATH must be the directory where executable is located. assert.ok( - areInterpreterPathsSame(shellExecutable.toLowerCase(), sysExecutable.toLowerCase()), + areInterpreterPathsSame(Uri.file(shellExecutable), Uri.file(sysExecutable)), `Python paths do not match ${shellExecutable}, ${sysExecutable}. Output is (${cell1Output}), error is ${errorOutput}` ); }); diff --git a/src/test/datascience/notebook/exportFull.vscode.test.ts b/src/test/datascience/notebook/exportFull.vscode.test.ts index f919eb0d636..95cade466fd 100644 --- a/src/test/datascience/notebook/exportFull.vscode.test.ts +++ b/src/test/datascience/notebook/exportFull.vscode.test.ts @@ -22,7 +22,7 @@ import { insertMarkdownCell, startJupyterServer, workAroundVSCodeNotebookStartPages -} from './helper'; +} from './helper.node'; import { commands, ConfigurationTarget, Uri, window, workspace } from 'vscode'; import { createDeferred } from '../../../platform/common/utils/async'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants.node'; diff --git a/src/test/datascience/notebook/helper.ts b/src/test/datascience/notebook/helper.node.ts similarity index 98% rename from src/test/datascience/notebook/helper.ts rename to src/test/datascience/notebook/helper.node.ts index 1998525d569..001ee6741eb 100644 --- a/src/test/datascience/notebook/helper.ts +++ b/src/test/datascience/notebook/helper.node.ts @@ -221,7 +221,7 @@ let waitForKernelPendingPromise: Promise | undefined; export async function waitForKernelToChange( criteria: | { labelOrId: string; isInteractiveController?: boolean } - | { interpreterPath: string; isInteractiveController?: boolean }, + | { interpreterPath: Uri; isInteractiveController?: boolean }, timeout = defaultNotebookTestTimeout ) { // Wait for the previous kernel change to finish. @@ -235,7 +235,7 @@ export async function waitForKernelToChange( async function waitForKernelToChangeImpl( criteria: | { labelOrId: string; isInteractiveController?: boolean } - | { interpreterPath: string; isInteractiveController?: boolean }, + | { interpreterPath: Uri; isInteractiveController?: boolean }, timeout = defaultNotebookTestTimeout ) { const { vscodeNotebook, notebookControllerManager } = await getServices(); @@ -272,7 +272,9 @@ async function waitForKernelToChangeImpl( id = notebookControllers ?.filter((k) => k.connection.interpreter) ?.filter((k) => (criteria.isInteractiveController ? k.id.includes(InteractiveControllerIdSuffix) : true)) - .find((k) => k.connection.interpreter!.path.toLowerCase().includes(interpreterPath.toLowerCase()))?.id; + .find((k) => + k.connection.interpreter!.uri.fsPath.toLowerCase().includes(interpreterPath.fsPath.toLowerCase()) + )?.id; } traceInfo(`Switching to kernel id ${id}`); const isRightKernel = () => { @@ -331,6 +333,7 @@ export async function waitForKernelToGetAutoSelected(expectedLanguage?: string, // Get the list of NotebookControllers for this document await notebookControllerManager.loadNotebookControllers(); + traceInfoIfCI(`Wait for kernel - got notebook controllers`); const notebookControllers = notebookControllerManager.registeredNotebookControllers(); // Make sure we don't already have a selection (this function gets run even after opening a document) @@ -357,6 +360,7 @@ export async function waitForKernelToGetAutoSelected(expectedLanguage?: string, // Do nothing for now. Just log it traceInfoIfCI(`No preferred controller found during waitForKernelToGetAutoSelected`); } + traceInfoIfCI(`Wait for kernel - got a preferred notebook controller: ${preferred?.id}`); // Find one that matches the expected language or the preferred const expectedLower = expectedLanguage?.toLowerCase(); diff --git a/src/test/datascience/notebook/intellisense/completion.vscode.test.ts b/src/test/datascience/notebook/intellisense/completion.vscode.test.ts index 0a10ed0e8c7..ab5bd735acb 100644 --- a/src/test/datascience/notebook/intellisense/completion.vscode.test.ts +++ b/src/test/datascience/notebook/intellisense/completion.vscode.test.ts @@ -22,7 +22,7 @@ import { waitForExecutionCompletedSuccessfully, prewarmNotebooks, createEmptyPythonNotebook -} from '../helper'; +} from '../helper.node'; import { IInteractiveWindowProvider } from '../../../../interactive-window/types'; import { setIntellisenseTimeout } from '../../../../intellisense/pythonKernelCompletionProvider.node'; import { Settings } from '../../../../platform/common/constants'; diff --git a/src/test/datascience/notebook/intellisense/completionProvider.vscode.test.ts b/src/test/datascience/notebook/intellisense/completionProvider.vscode.test.ts index 1c963a971cc..6b9035fa3d6 100644 --- a/src/test/datascience/notebook/intellisense/completionProvider.vscode.test.ts +++ b/src/test/datascience/notebook/intellisense/completionProvider.vscode.test.ts @@ -36,7 +36,7 @@ import { prewarmNotebooks, createEmptyPythonNotebook, getCellOutputs -} from '../helper'; +} from '../helper.node'; import { Settings } from '../../../../platform/common/constants'; /* eslint-disable @typescript-eslint/no-explicit-any, no-invalid-this */ diff --git a/src/test/datascience/notebook/intellisense/diagnostics.vscode.test.ts b/src/test/datascience/notebook/intellisense/diagnostics.vscode.test.ts index 6fb2a4212c5..b629d2a2dfd 100644 --- a/src/test/datascience/notebook/intellisense/diagnostics.vscode.test.ts +++ b/src/test/datascience/notebook/intellisense/diagnostics.vscode.test.ts @@ -15,7 +15,7 @@ import { insertCodeCell, createEmptyPythonNotebook, waitForDiagnostics -} from '../helper'; +} from '../helper.node'; import { Settings } from '../../../../platform/common/constants'; import { setIntellisenseTimeout } from '../../../../intellisense/pythonKernelCompletionProvider.node'; diff --git a/src/test/datascience/notebook/intellisense/gotodef.vscode.test.ts b/src/test/datascience/notebook/intellisense/gotodef.vscode.test.ts index b58e53b7149..e650af5a42f 100644 --- a/src/test/datascience/notebook/intellisense/gotodef.vscode.test.ts +++ b/src/test/datascience/notebook/intellisense/gotodef.vscode.test.ts @@ -16,7 +16,7 @@ import { prewarmNotebooks, createEmptyPythonNotebook, defaultNotebookTestTimeout -} from '../helper'; +} from '../helper.node'; import { setIntellisenseTimeout } from '../../../../intellisense/pythonKernelCompletionProvider.node'; import { Settings } from '../../../../platform/common/constants'; diff --git a/src/test/datascience/notebook/intellisense/hover.vscode.test.ts b/src/test/datascience/notebook/intellisense/hover.vscode.test.ts index 76225946d3c..a9113fb6a23 100644 --- a/src/test/datascience/notebook/intellisense/hover.vscode.test.ts +++ b/src/test/datascience/notebook/intellisense/hover.vscode.test.ts @@ -11,7 +11,12 @@ import { IDisposable } from '../../../../platform/common/types'; import { IExtensionTestApi } from '../../../common.node'; import { IS_REMOTE_NATIVE_TEST } from '../../../constants.node'; import { initialize } from '../../../initialize.node'; -import { closeNotebooksAndCleanUpAfterTests, insertCodeCell, createEmptyPythonNotebook, waitForHover } from '../helper'; +import { + closeNotebooksAndCleanUpAfterTests, + insertCodeCell, + createEmptyPythonNotebook, + waitForHover +} from '../helper.node'; import { setIntellisenseTimeout } from '../../../../intellisense/pythonKernelCompletionProvider.node'; import { Settings } from '../../../../platform/common/constants'; diff --git a/src/test/datascience/notebook/intellisense/interpreterSwitch.vscode.test.ts b/src/test/datascience/notebook/intellisense/interpreterSwitch.vscode.test.ts index 30b9a45ec40..bf70f760182 100644 --- a/src/test/datascience/notebook/intellisense/interpreterSwitch.vscode.test.ts +++ b/src/test/datascience/notebook/intellisense/interpreterSwitch.vscode.test.ts @@ -6,11 +6,11 @@ import { assert } from 'chai'; import * as path from '../../../../platform/vscode-path/path'; import * as fs from 'fs-extra'; import * as sinon from 'sinon'; -import { languages } from 'vscode'; +import { languages, Uri } from 'vscode'; import { traceInfo } from '../../../../platform/logging'; import { IDisposable } from '../../../../platform/common/types'; import { IInterpreterService } from '../../../../platform/interpreter/contracts.node'; -import { captureScreenShot, getOSType, IExtensionTestApi, OSType, waitForCondition } from '../../../common.node'; +import { captureScreenShot, IExtensionTestApi, waitForCondition } from '../../../common.node'; import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_REMOTE_NATIVE_TEST } from '../../../constants.node'; import { initialize, IS_CI_SERVER } from '../../../initialize.node'; import { @@ -22,26 +22,27 @@ import { waitForKernelToChange, waitForDiagnostics, defaultNotebookTestTimeout -} from '../helper'; +} from '../helper.node'; import { IVSCodeNotebook } from '../../../../platform/common/application/types'; import { IPythonExecutionFactory } from '../../../../platform/common/process/types.node'; import { PythonEnvironment } from '../../../../platform/pythonEnvironments/info'; import { setIntellisenseTimeout } from '../../../../intellisense/pythonKernelCompletionProvider.node'; import { Settings } from '../../../../platform/common/constants'; +import { getOSType, OSType } from '../../../../platform/common/utils/platform'; /* eslint-disable @typescript-eslint/no-explicit-any, no-invalid-this */ suite('DataScience - Intellisense Switch interpreters in a notebook', function () { let api: IExtensionTestApi; const disposables: IDisposable[] = []; const executable = getOSType() === OSType.Windows ? 'Scripts/python.exe' : 'bin/python'; // If running locally on Windows box. - const venvNoKernelPython = path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src/test/datascience/.venvnokernel', - executable + const venvNoKernelPython = Uri.file( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvnokernel', executable) ); - const venvKernelPython = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvkernel', executable); - let venvNoKernelPythonPath: string; - let venvKernelPythonPath: string; + const venvKernelPython = Uri.file( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvkernel', executable) + ); + let venvNoKernelPythonPath: Uri; + let venvKernelPythonPath: Uri; let vscodeNotebook: IVSCodeNotebook; this.timeout(120_000); @@ -56,8 +57,8 @@ suite('DataScience - Intellisense Switch interpreters in a notebook', function ( // These are slow tests, hence lets run only on linux on CI. if ( (IS_CI_SERVER && getOSType() !== OSType.Linux) || - !fs.pathExistsSync(venvNoKernelPython) || - !fs.pathExistsSync(venvKernelPython) + !fs.pathExistsSync(venvNoKernelPython.fsPath) || + !fs.pathExistsSync(venvKernelPython.fsPath) ) { // Virtual env does not exist. return this.skip(); @@ -74,12 +75,12 @@ suite('DataScience - Intellisense Switch interpreters in a notebook', function ( if (!activeInterpreter || !interpreter1 || !interpreter2) { throw new Error('Unable to get information for interpreter 1'); } - venvNoKernelPythonPath = interpreter1.path; - venvKernelPythonPath = interpreter2.path; + venvNoKernelPythonPath = interpreter1.uri; + venvKernelPythonPath = interpreter2.uri; // Make sure to remove pandas from the venvnokernel. This test relies on it. const factory = api.serviceContainer.get(IPythonExecutionFactory); - const process = await factory.create({ interpreter: { path: venvNoKernelPythonPath } as PythonEnvironment }); + const process = await factory.create({ interpreter: { uri: venvNoKernelPythonPath } as PythonEnvironment }); await process.execModule('pip', ['uninstall', 'pandas'], { throwOnStdErr: false }); await startJupyterServer(); diff --git a/src/test/datascience/notebook/intellisense/semanticTokens.vscode.test.ts b/src/test/datascience/notebook/intellisense/semanticTokens.vscode.test.ts index 406b642d88f..d6a791297e2 100644 --- a/src/test/datascience/notebook/intellisense/semanticTokens.vscode.test.ts +++ b/src/test/datascience/notebook/intellisense/semanticTokens.vscode.test.ts @@ -18,7 +18,7 @@ import { prewarmNotebooks, createEmptyPythonNotebook, defaultNotebookTestTimeout -} from '../helper'; +} from '../helper.node'; /* eslint-disable @typescript-eslint/no-explicit-any, no-invalid-this */ suite('DataScience - VSCode semantic token tests', function () { diff --git a/src/test/datascience/notebook/interruptRestart.vscode.test.ts b/src/test/datascience/notebook/interruptRestart.vscode.test.ts index 4f4abe7bdf4..278b328ea3a 100644 --- a/src/test/datascience/notebook/interruptRestart.vscode.test.ts +++ b/src/test/datascience/notebook/interruptRestart.vscode.test.ts @@ -28,7 +28,7 @@ import { runCell, waitForOutputs, clickOKForRestartPrompt -} from './helper'; +} from './helper.node'; import { hasErrorOutput, NotebookCellStateTracker, getTextOutputValue } from '../../../notebooks/helpers.node'; import { Commands } from '../../../platform/common/constants'; diff --git a/src/test/datascience/notebook/ipywidget.vscode.test.ts b/src/test/datascience/notebook/ipywidget.vscode.test.ts index f7af1a555fa..6da13e9759f 100644 --- a/src/test/datascience/notebook/ipywidget.vscode.test.ts +++ b/src/test/datascience/notebook/ipywidget.vscode.test.ts @@ -20,7 +20,7 @@ import { runCell, waitForExecutionCompletedSuccessfully, waitForKernelToGetAutoSelected -} from './helper'; +} from './helper.node'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants.node'; import { createDeferred, Deferred } from '../../../platform/common/utils/async'; import { InteractiveWindowMessages } from '../../../platform/messageTypes'; diff --git a/src/test/datascience/notebook/kernelCrashes.vscode.test.ts b/src/test/datascience/notebook/kernelCrashes.vscode.test.ts index bcfd84ee09d..3f9603538d5 100644 --- a/src/test/datascience/notebook/kernelCrashes.vscode.test.ts +++ b/src/test/datascience/notebook/kernelCrashes.vscode.test.ts @@ -30,7 +30,7 @@ import { defaultNotebookTestTimeout, waitForExecutionCompletedWithoutChangesToExecutionCount, getCellOutputs -} from './helper'; +} from './helper.node'; import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_NON_RAW_NATIVE_TEST, IS_REMOTE_NATIVE_TEST } from '../../constants.node'; import * as dedent from 'dedent'; import { IKernelProvider } from '../../../platform/../kernels/types'; @@ -371,7 +371,7 @@ suite('DataScience - VSCode Notebook Kernel Error Handling - (Execution) (slow)' 'src/test/datascience/notebook/kernelFailures/overrideBuiltinModule/random.py' ); const expectedErrorMessage = `${DataScience.fileSeemsToBeInterferingWithKernelStartup().format( - getDisplayPath(randomFile, workspace.workspaceFolders || []) + getDisplayPath(Uri.file(randomFile), workspace.workspaceFolders || []) )} \n${DataScience.viewJupyterLogForFurtherInfo()}`; const prompt = await hijackPrompt( diff --git a/src/test/datascience/notebook/kernelRefresh.vscode.test.ts b/src/test/datascience/notebook/kernelRefresh.vscode.test.ts index 45d557d0adb..ca9c9c90b64 100644 --- a/src/test/datascience/notebook/kernelRefresh.vscode.test.ts +++ b/src/test/datascience/notebook/kernelRefresh.vscode.test.ts @@ -21,7 +21,7 @@ import { createEmptyPythonNotebook, workAroundVSCodeNotebookStartPages, defaultNotebookTestTimeout -} from './helper'; +} from './helper.node'; import { IS_CONDA_TEST } from '../../constants.node'; import { EnvironmentType } from '../../../platform/pythonEnvironments/info'; import { JupyterNotebookView } from '../../../notebooks/constants'; diff --git a/src/test/datascience/notebook/kernelSelection.vscode.test.ts b/src/test/datascience/notebook/kernelSelection.vscode.test.ts index b3e76cc2325..27d61c3e92a 100644 --- a/src/test/datascience/notebook/kernelSelection.vscode.test.ts +++ b/src/test/datascience/notebook/kernelSelection.vscode.test.ts @@ -5,7 +5,7 @@ import { assert } from 'chai'; import * as fs from 'fs-extra'; import * as path from '../../../platform/vscode-path/path'; import * as sinon from 'sinon'; -import { commands, window } from 'vscode'; +import { commands, Uri, window } from 'vscode'; import { IPythonExtensionChecker } from '../../../platform/api/types'; import { IVSCodeNotebook } from '../../../platform/common/application/types'; import { BufferDecoder } from '../../../platform/common/process/decoder.node'; @@ -17,7 +17,7 @@ import { getInterpreterHash, getNormalizedInterpreterPath } from '../../../platform/pythonEnvironments/info/interpreter.node'; -import { createEventHandler, getOSType, IExtensionTestApi, OSType, waitForCondition } from '../../common.node'; +import { createEventHandler, IExtensionTestApi, waitForCondition } from '../../common.node'; import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_REMOTE_NATIVE_TEST } from '../../constants.node'; import { closeActiveWindows, initialize, IS_CI_SERVER } from '../../initialize.node'; import { openNotebook } from '../helpers'; @@ -34,8 +34,9 @@ import { waitForOutputs, waitForTextOutput, defaultNotebookTestTimeout -} from './helper'; +} from './helper.node'; import { getTextOutputValue } from '../../../notebooks/helpers.node'; +import { getOSType, OSType } from '../../../platform/common/utils/platform'; /* eslint-disable no-invalid-this, , , @typescript-eslint/no-explicit-any */ suite('DataScience - VSCode Notebook - Kernel Selection', function () { @@ -45,20 +46,22 @@ suite('DataScience - VSCode Notebook - Kernel Selection', function () { 'src/test/datascience/notebook/nbWithKernel.ipynb' ); const executable = getOSType() === OSType.Windows ? 'Scripts/python.exe' : 'bin/python'; // If running locally on Windows box. - const venvNoKernelPython = path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src/test/datascience/.venvnokernel', - executable + const venvNoKernelPython = Uri.file( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvnokernel', executable) + ); + const venvKernelPython = Uri.file( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvkernel', executable) + ); + const venvNoRegPath = Uri.file( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvnoreg', executable) ); - const venvKernelPython = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvkernel', executable); - const venvNoRegPath = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src/test/datascience/.venvnoreg', executable); let nbFile1: string; let api: IExtensionTestApi; - let activeInterpreterPath: string; - let venvNoKernelPythonPath: string; - let venvKernelPythonPath: string; - let venvNoRegPythonPath: string; + let activeInterpreterPath: Uri; + let venvNoKernelPythonPath: Uri; + let venvKernelPythonPath: Uri; + let venvNoRegPythonPath: Uri; let venvNoKernelDisplayName: string; let kernelProvider: IKernelProvider; const venvNoKernelSearchString = '.venvnokernel'; @@ -76,9 +79,9 @@ suite('DataScience - VSCode Notebook - Kernel Selection', function () { // These are slow tests, hence lets run only on linux on CI. if ( (IS_CI_SERVER && getOSType() !== OSType.Linux) || - !fs.pathExistsSync(venvNoKernelPython) || - !fs.pathExistsSync(venvKernelPython) || - !fs.pathExistsSync(venvNoRegPath) + !fs.pathExistsSync(venvNoKernelPython.fsPath) || + !fs.pathExistsSync(venvKernelPython.fsPath) || + !fs.pathExistsSync(venvNoRegPath.fsPath) ) { // Virtual env does not exist. return this.skip(); @@ -105,10 +108,10 @@ suite('DataScience - VSCode Notebook - Kernel Selection', function () { if (!activeInterpreter || !interpreter1 || !interpreter2 || !interpreter3) { throw new Error('Unable to get information for interpreter 1'); } - activeInterpreterPath = activeInterpreter?.path; - venvNoKernelPythonPath = interpreter1.path; - venvKernelPythonPath = interpreter2.path; - venvNoRegPythonPath = interpreter3.path; + activeInterpreterPath = activeInterpreter?.uri; + venvNoKernelPythonPath = interpreter1.uri; + venvKernelPythonPath = interpreter2.uri; + venvNoRegPythonPath = interpreter3.uri; venvNoKernelDisplayName = interpreter1.displayName || '.venvnokernel'; activeInterpreterSearchString = activeInterpreter.displayName === interpreter1.displayName @@ -117,14 +120,14 @@ suite('DataScience - VSCode Notebook - Kernel Selection', function () { ? venvKernelSearchString : activeInterpreter.displayName === interpreter3.displayName ? venvNoRegSearchString - : activeInterpreterPath; + : activeInterpreterPath.fsPath; // Ensure IPykernel is in all environments. const proc = new ProcessService(new BufferDecoder()); await Promise.all([ - proc.exec(venvNoKernelPython, ['-m', 'pip', 'install', 'ipykernel']), - proc.exec(venvKernelPython, ['-m', 'pip', 'install', 'ipykernel']), - proc.exec(venvNoRegPythonPath, ['-m', 'pip', 'install', 'ipykernel']) + proc.exec(venvNoKernelPython.fsPath, ['-m', 'pip', 'install', 'ipykernel']), + proc.exec(venvKernelPython.fsPath, ['-m', 'pip', 'install', 'ipykernel']), + proc.exec(venvNoRegPythonPath.fsPath, ['-m', 'pip', 'install', 'ipykernel']) ]); await startJupyterServer(); @@ -142,7 +145,7 @@ suite('DataScience - VSCode Notebook - Kernel Selection', function () { fs .readFileSync(nbFile1) .toString('utf8') - .replace('', getInterpreterHash({ path: venvNoKernelPythonPath })) + .replace('', getInterpreterHash({ uri: venvNoKernelPythonPath })) ); await closeActiveWindows(); sinon.restore(); @@ -171,8 +174,8 @@ suite('DataScience - VSCode Notebook - Kernel Selection', function () { const output = getTextOutputValue(cell.outputs[0]); if ( !output.includes(activeInterpreterSearchString) && - !output.includes(getNormalizedInterpreterPath(activeInterpreterPath)) && - !output.includes(activeInterpreterPath) + !output.includes(getNormalizedInterpreterPath(activeInterpreterPath).fsPath) && + !output.includes(activeInterpreterPath.fsPath) ) { assert.fail( output, diff --git a/src/test/datascience/notebook/memory.vscode.test.ts b/src/test/datascience/notebook/memory.vscode.test.ts index 351fe1c926c..32869a052e0 100644 --- a/src/test/datascience/notebook/memory.vscode.test.ts +++ b/src/test/datascience/notebook/memory.vscode.test.ts @@ -25,7 +25,7 @@ import { workAroundVSCodeNotebookStartPages, waitForTextOutput, defaultNotebookTestTimeout -} from './helper'; +} from './helper.node'; // Force GC to be available require('expose-gc'); diff --git a/src/test/datascience/notebook/nonPythonKernels.vscode.test.node.ts b/src/test/datascience/notebook/nonPythonKernels.vscode.test.node.ts index b7b3b880f75..996afc2b4db 100644 --- a/src/test/datascience/notebook/nonPythonKernels.vscode.test.node.ts +++ b/src/test/datascience/notebook/nonPythonKernels.vscode.test.node.ts @@ -29,7 +29,7 @@ import { waitForKernelToGetAutoSelected, workAroundVSCodeNotebookStartPages, waitForTextOutput -} from './helper'; +} from './helper.node'; import { PythonExtensionChecker } from '../../../platform/api/pythonApi.node'; import { NotebookCellLanguageService } from '../../../intellisense/cellLanguageService.node'; import { INotebookEditorProvider } from '../../../notebooks/types'; diff --git a/src/test/datascience/notebook/notebookControllerManager.unit.test.ts b/src/test/datascience/notebook/notebookControllerManager.unit.test.ts index 03b9b771bf6..c2242262cc8 100644 --- a/src/test/datascience/notebook/notebookControllerManager.unit.test.ts +++ b/src/test/datascience/notebook/notebookControllerManager.unit.test.ts @@ -3,6 +3,7 @@ import { assert } from 'chai'; import { when, instance, mock } from 'ts-mockito'; +import { Uri } from 'vscode'; import { getDisplayNameOrNameOfKernelConnection } from '../../../kernels/helpers.node'; import { IJupyterKernelSpec } from '../../../kernels/types'; import { EnvironmentType, PythonEnvironment } from '../../../platform/pythonEnvironments/info'; @@ -33,7 +34,7 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path' + uri: Uri.file('path') } }); @@ -47,7 +48,7 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'abc' } }); @@ -62,10 +63,10 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path' + uri: Uri.file('path') }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix' } }); @@ -79,10 +80,10 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path' + uri: Uri.file('path') }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envType: EnvironmentType.Global } @@ -97,10 +98,10 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path' + uri: Uri.file('path') }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '', displayName: 'Something', @@ -117,10 +118,10 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path' + uri: Uri.file('path') }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '', displayName: 'Something 64-bit', @@ -137,10 +138,10 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path' + uri: Uri.file('path') }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '.env', displayName: 'Something', @@ -157,10 +158,10 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path' + uri: Uri.file('path') }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '.env', displayName: 'Something 64-bit', @@ -179,7 +180,7 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'python' } }); @@ -194,11 +195,11 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'python' }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '.env', displayName: 'Something 64-bit' @@ -214,11 +215,11 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'python' }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '.env', displayName: 'Something 64-bit', @@ -235,11 +236,11 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'python' }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '', version: undefined, @@ -257,11 +258,11 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'python' }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '', displayName: 'Something 64-bit', @@ -278,11 +279,11 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'python' }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '', displayName: 'Something 64-bit', @@ -307,11 +308,11 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'python' }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '.env', displayName: 'Something 64-bit', @@ -336,11 +337,11 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'python' }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '.env', displayName: 'Something 64-bit', @@ -367,11 +368,11 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'python' }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '', displayName: 'Something 64-bit' @@ -387,11 +388,11 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'python' }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '', displayName: 'Something 64-bit', @@ -408,11 +409,11 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'python' }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '', displayName: 'Something', @@ -429,11 +430,11 @@ suite('Notebook Controller Manager', () => { argv: [], display_name: 'kspecname', name: 'kspec', - path: 'path', + uri: Uri.file('path'), language: 'python' }, interpreter: { - path: 'pyPath', + uri: Uri.file('pyPath'), sysPrefix: 'sysPrefix', envName: '', displayName: 'Something 64-bit', diff --git a/src/test/datascience/notebook/outputDisplayOrder.vscode.test.ts b/src/test/datascience/notebook/outputDisplayOrder.vscode.test.ts index 591bf5aea07..baa07784456 100644 --- a/src/test/datascience/notebook/outputDisplayOrder.vscode.test.ts +++ b/src/test/datascience/notebook/outputDisplayOrder.vscode.test.ts @@ -9,7 +9,7 @@ import { assert } from 'chai'; import { traceInfo } from '../../../platform/logging'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants.node'; import { openNotebook } from '../helpers'; -import { closeNotebooksAndCleanUpAfterTests } from './helper'; +import { closeNotebooksAndCleanUpAfterTests } from './helper.node'; import { window } from 'vscode'; import { initialize } from '../../initialize.node'; import type * as nbformat from '@jupyterlab/nbformat'; diff --git a/src/test/datascience/notebook/remoteNotebookEditor.vscode.test.ts b/src/test/datascience/notebook/remoteNotebookEditor.vscode.test.ts index 534a682224a..1e0dacb1d58 100644 --- a/src/test/datascience/notebook/remoteNotebookEditor.vscode.test.ts +++ b/src/test/datascience/notebook/remoteNotebookEditor.vscode.test.ts @@ -32,7 +32,7 @@ import { waitForTextOutput, defaultNotebookTestTimeout, createEmptyPythonNotebook -} from './helper'; +} from './helper.node'; import { openNotebook } from '../helpers'; import { PYTHON_LANGUAGE, Settings } from '../../../platform/common/constants'; import { RemoteKernelSpecConnectionMetadata } from '../../../platform/../kernels/types'; diff --git a/src/test/datascience/notebook/saving.vscode.test.ts b/src/test/datascience/notebook/saving.vscode.test.ts index 268464c24d5..bdcac46a295 100644 --- a/src/test/datascience/notebook/saving.vscode.test.ts +++ b/src/test/datascience/notebook/saving.vscode.test.ts @@ -29,7 +29,7 @@ import { waitForExecutionCompletedSuccessfully, waitForExecutionCompletedWithErrors, waitForKernelToGetAutoSelected -} from './helper'; +} from './helper.node'; /* eslint-disable @typescript-eslint/no-explicit-any, no-invalid-this */ suite('DataScience - VSCode Notebook - (Saving) (slow)', function () { diff --git a/src/test/datascience/notebookServerProvider.vscode.test.ts b/src/test/datascience/notebookServerProvider.vscode.test.ts index 411ac77ee2a..743ceb2f576 100644 --- a/src/test/datascience/notebookServerProvider.vscode.test.ts +++ b/src/test/datascience/notebookServerProvider.vscode.test.ts @@ -8,7 +8,7 @@ import { Disposable, CancellationTokenSource } from 'vscode'; import { traceInfo } from '../../platform/logging'; import { IS_NON_RAW_NATIVE_TEST } from '../constants.node'; import { initialize } from '../initialize.node'; -import { closeNotebooksAndCleanUpAfterTests, startJupyterServer } from './notebook/helper'; +import { closeNotebooksAndCleanUpAfterTests, startJupyterServer } from './notebook/helper.node'; import * as getFreePort from 'get-port'; import { IPythonExecutionFactory } from '../../platform/common/process/types.node'; import { IInterpreterService } from '../../platform/interpreter/contracts.node'; diff --git a/src/test/datascience/plotViewer/plotViewer.vscode.test.ts b/src/test/datascience/plotViewer/plotViewer.vscode.test.ts index 825318e6358..ef960379a32 100644 --- a/src/test/datascience/plotViewer/plotViewer.vscode.test.ts +++ b/src/test/datascience/plotViewer/plotViewer.vscode.test.ts @@ -18,7 +18,7 @@ import { insertCodeCell, runAllCellsInActiveNotebook, waitForExecutionCompletedSuccessfully -} from '../notebook/helper'; +} from '../notebook/helper.node'; suite('VSCode Notebook PlotViewer integration - VSCode Notebook', function () { let api: IExtensionTestApi; diff --git a/src/test/datascience/preWarmVariables.unit.test.ts b/src/test/datascience/preWarmVariables.unit.test.ts index 916c1af8018..1b0ec052fa1 100644 --- a/src/test/datascience/preWarmVariables.unit.test.ts +++ b/src/test/datascience/preWarmVariables.unit.test.ts @@ -4,7 +4,7 @@ 'use strict'; import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { EventEmitter } from 'vscode'; +import { EventEmitter, Uri } from 'vscode'; import { IExtensionSingleActivationService } from '../../platform/activation/types'; import { PythonExtensionChecker } from '../../platform/api/pythonApi.node'; import { IPythonApiProvider, IPythonExtensionChecker } from '../../platform/api/types'; @@ -29,7 +29,7 @@ suite('DataScience - PreWarm Env Vars', () => { let zmqSupported: IRawNotebookSupportedService; setup(() => { interpreter = { - path: '', + uri: Uri.file(''), sysPrefix: '', sysVersion: '' }; diff --git a/src/test/datascience/variableView/variableView.vscode.test.ts b/src/test/datascience/variableView/variableView.vscode.test.ts index 6b4ea0faf7d..11806372b8f 100644 --- a/src/test/datascience/variableView/variableView.vscode.test.ts +++ b/src/test/datascience/variableView/variableView.vscode.test.ts @@ -16,7 +16,7 @@ import { workAroundVSCodeNotebookStartPages, startJupyterServer, defaultNotebookTestTimeout -} from '../notebook/helper'; +} from '../notebook/helper.node'; import { waitForVariablesToMatch } from './variableViewHelpers'; import { ITestVariableViewProvider } from './variableViewTestInterfaces'; import { ITestWebviewHost } from '../testInterfaces'; diff --git a/src/test/datascience/variableView/variableViewHelpers.ts b/src/test/datascience/variableView/variableViewHelpers.ts index e77936a373f..6f8bd06f709 100644 --- a/src/test/datascience/variableView/variableViewHelpers.ts +++ b/src/test/datascience/variableView/variableViewHelpers.ts @@ -4,7 +4,7 @@ import * as fastDeepEqual from 'fast-deep-equal'; import { assert } from 'chai'; import { waitForCondition } from '../../common.node'; -import { defaultNotebookTestTimeout } from '../notebook/helper'; +import { defaultNotebookTestTimeout } from '../notebook/helper.node'; import { ITestWebviewHost } from '../testInterfaces'; // Basic shape of a variable result diff --git a/src/test/datascience/widgets/standard.vscode.test.ts b/src/test/datascience/widgets/standard.vscode.test.ts index cb21f1faf37..874f0cdf4db 100644 --- a/src/test/datascience/widgets/standard.vscode.test.ts +++ b/src/test/datascience/widgets/standard.vscode.test.ts @@ -26,7 +26,7 @@ import { waitForExecutionCompletedSuccessfully, waitForKernelToGetAutoSelected, workAroundVSCodeNotebookStartPages -} from '../notebook/helper'; +} from '../notebook/helper.node'; import { initializeWidgetComms, Utils } from './commUtils'; import { WidgetRenderingTimeoutForTests } from './constants'; diff --git a/src/test/index.node.ts b/src/test/index.node.ts index 33bedd7b8f7..934c21f5ae3 100644 --- a/src/test/index.node.ts +++ b/src/test/index.node.ts @@ -28,7 +28,7 @@ import { TEST_TIMEOUT } from './constants.node'; import { noop } from './core'; -import { stopJupyterServer } from './datascience/notebook/helper'; +import { stopJupyterServer } from './datascience/notebook/helper.node'; import { initialize } from './initialize.node'; import { rootHooks } from './testHooks'; diff --git a/src/test/initialize.node.ts b/src/test/initialize.node.ts index e87d6ccbec2..142bd81a675 100644 --- a/src/test/initialize.node.ts +++ b/src/test/initialize.node.ts @@ -7,7 +7,7 @@ import { clearPendingChainedUpdatesForTests } from '../notebooks/execution/noteb import { clearPendingTimers, IExtensionTestApi, PYTHON_PATH, setPythonPathInWorkspaceRoot } from './common.node'; import { IS_SMOKE_TEST, JVSC_EXTENSION_ID_FOR_TESTS } from './constants.node'; import { sleep } from './core'; -import { startJupyterServer } from './datascience/notebook/helper'; +import { startJupyterServer } from './datascience/notebook/helper.node'; import { PythonExtension, setTestExecution } from '../platform/common/constants'; export * from './constants.node'; diff --git a/src/test/interpreters/condaLocator.ts b/src/test/interpreters/condaLocator.node.ts similarity index 80% rename from src/test/interpreters/condaLocator.ts rename to src/test/interpreters/condaLocator.node.ts index 9a28f7de771..d867fc0fda9 100644 --- a/src/test/interpreters/condaLocator.ts +++ b/src/test/interpreters/condaLocator.node.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as fs from 'fs-extra'; -import * as path from '../../platform/vscode-path/path'; +import { Uri } from 'vscode'; +import * as uriPath from '../../platform/vscode-path/resources'; /** * Checks if the given interpreter path belongs to a conda environment. Using @@ -33,7 +34,7 @@ import * as path from '../../platform/vscode-path/path'; * ] * } */ -export async function isCondaEnvironment(interpreterPath: string | undefined): Promise { +export async function isCondaEnvironment(interpreterPath: Uri | undefined): Promise { // This can be undefined on some machines. if (!interpreterPath) { return false; @@ -46,7 +47,7 @@ export async function isCondaEnvironment(interpreterPath: string | undefined): P // env // |__ conda-meta <--- check if this directory exists // |__ python.exe <--- interpreterPath - const condaEnvDir1 = path.join(path.dirname(interpreterPath), condaMetaDir); + const condaEnvDir1 = uriPath.joinPath(uriPath.dirname(interpreterPath), condaMetaDir); // Check if the conda-meta directory is in the parent directory relative to the interpreter. // This layout is common on linux/Mac. @@ -54,7 +55,7 @@ export async function isCondaEnvironment(interpreterPath: string | undefined): P // |__ conda-meta <--- check if this directory exists // |__ bin // |__ python <--- interpreterPath - const condaEnvDir2 = path.join(path.dirname(path.dirname(interpreterPath)), condaMetaDir); + const condaEnvDir2 = uriPath.joinPath(uriPath.dirname(uriPath.dirname(interpreterPath)), condaMetaDir); - return [await fs.pathExists(condaEnvDir1), await fs.pathExists(condaEnvDir2)].includes(true); + return [await fs.pathExists(condaEnvDir1.fsPath), await fs.pathExists(condaEnvDir2.fsPath)].includes(true); } diff --git a/src/test/interpreters/condaService.node.ts b/src/test/interpreters/condaService.node.ts index 35825285562..33d2575ca7e 100644 --- a/src/test/interpreters/condaService.node.ts +++ b/src/test/interpreters/condaService.node.ts @@ -9,9 +9,10 @@ import { traceError, traceVerbose, traceWarning } from '../../platform/logging'; import { arePathsSame } from '../../platform/common/platform/fileUtils.node'; import { BufferDecoder } from '../../platform/common/process/decoder.node'; import { ProcessService } from '../../platform/common/process/proc.node'; -import { getOSType, OSType } from '../common.node'; import { parseCondaEnvFileContents } from './condaHelper'; -import { isCondaEnvironment } from './condaLocator'; +import { isCondaEnvironment } from './condaLocator.node'; +import { Uri } from 'vscode'; +import { getOSType, OSType } from '../../platform/common/utils/platform'; /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires @@ -179,14 +180,14 @@ async function getCondaEnvironments(): Promise { + interpreterPath: Uri | undefined +): Promise<{ name: string; path: Uri } | undefined> { const isCondaEnv = await isCondaEnvironment(interpreterPath); if (!isCondaEnv || !interpreterPath) { return; } const environments = await getCondaEnvironments(); - const dir = path.dirname(interpreterPath); + const dir = path.dirname(interpreterPath.fsPath); // If interpreter is in bin or Scripts, then go up one level const subDirName = path.basename(dir); @@ -199,7 +200,7 @@ export async function getCondaEnvironment( : []; if (matchingEnvs.length > 0) { - return { name: matchingEnvs[0].name, path: interpreterPathToMatch }; + return { name: matchingEnvs[0].name, path: Uri.file(interpreterPathToMatch) }; } // If still not available, then the user created the env after starting vs code. diff --git a/src/test/interpreters/envActivation.node.ts b/src/test/interpreters/envActivation.node.ts index 984440ec2ea..6bfd42e4575 100644 --- a/src/test/interpreters/envActivation.node.ts +++ b/src/test/interpreters/envActivation.node.ts @@ -6,6 +6,7 @@ import { getActivatedEnvVariables } from './index.node'; import { Resource } from '../../platform/common/types'; import { IEnvironmentActivationService } from '../../platform/interpreter/activation/types'; import { PythonEnvironment } from '../../platform/pythonEnvironments/info'; +import { fsPathToUri } from '../../platform/vscode-path/utils'; @injectable() export class EnvironmentActivationService implements IEnvironmentActivationService { @@ -14,7 +15,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi interpreter?: PythonEnvironment, _allowExceptions?: boolean ): Promise { - return getActivatedEnvVariables(interpreter?.path || process.env.CI_PYTHON_PATH || 'python'); + return getActivatedEnvVariables(interpreter?.uri || fsPathToUri(process.env.CI_PYTHON_PATH || 'python')!); } async hasActivationCommands(_resource: Resource, _interpreter?: PythonEnvironment): Promise { return false; diff --git a/src/test/interpreters/index.node.ts b/src/test/interpreters/index.node.ts index 85b5a71f255..701aaf1d033 100644 --- a/src/test/interpreters/index.node.ts +++ b/src/test/interpreters/index.node.ts @@ -9,10 +9,12 @@ import { PythonEnvInfo } from '../../platform/common/process/internal/scripts/in import { ProcessService } from '../../platform/common/process/proc.node'; import { PythonEnvironment } from '../../platform/pythonEnvironments/info'; import { parsePythonVersion } from '../../platform/pythonEnvironments/info/pythonVersion'; -import { getOSType, OSType } from '../common.node'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants.node'; -import { isCondaEnvironment } from './condaLocator'; +import { isCondaEnvironment } from './condaLocator.node'; import { getCondaEnvironment, getCondaFile, isCondaAvailable } from './condaService.node'; +import { getComparisonKey } from '../../platform/vscode-path/resources'; +import { Uri } from 'vscode'; +import { getOSType, OSType } from '../../platform/common/utils/platform'; const executionTimeout = 30_000; const SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'pythonFiles'); @@ -26,12 +28,13 @@ const defaultShells = { const defaultShell = defaultShells[getOSType()]; const interpreterInfoCache = new Map>(); -export async function getInterpreterInfo(pythonPath: string | undefined): Promise { +export async function getInterpreterInfo(pythonPath: Uri | undefined): Promise { if (!pythonPath) { return undefined; } - if (interpreterInfoCache.has(pythonPath)) { - return interpreterInfoCache.get(pythonPath); + const key = getComparisonKey(pythonPath); + if (interpreterInfoCache.has(key)) { + return interpreterInfoCache.get(key); } const promise = (async () => { @@ -52,7 +55,7 @@ export async function getInterpreterInfo(pythonPath: string | undefined): Promis const json: PythonEnvInfo = JSON.parse(result.stdout.trim()); const rawVersion = `${json.versionInfo.slice(0, 3).join('.')}-${json.versionInfo[3]}`; return { - path: json.exe, + uri: Uri.file(json.exe), displayName: `Python${rawVersion}`, version: parsePythonVersion(rawVersion), sysVersion: json.sysVersion, @@ -63,14 +66,15 @@ export async function getInterpreterInfo(pythonPath: string | undefined): Promis return undefined; } })(); - interpreterInfoCache.set(pythonPath, promise); + interpreterInfoCache.set(key, promise); return promise; } const envVariables = new Map>(); -export async function getActivatedEnvVariables(pythonPath: string): Promise { - if (envVariables.has(pythonPath)) { - return envVariables.get(pythonPath); +export async function getActivatedEnvVariables(pythonPath: Uri): Promise { + const key = getComparisonKey(pythonPath); + if (envVariables.has(key)) { + return envVariables.get(key); } const promise = (async () => { const cli = await getPythonCli(pythonPath); @@ -98,11 +102,11 @@ export async function getActivatedEnvVariables(pythonPath: string): Promise | undefined; @injectable() @@ -22,7 +24,7 @@ export class InterpreterService implements IInterpreterService { public readonly didChangeInterpreter = new EventEmitter(); public readonly didChangeInterpreters = new EventEmitter(); - private readonly customInterpretersPerUri = new Map(); + private readonly customInterpretersPerUri = new Map(); constructor(@inject(IPythonExtensionChecker) private readonly extensionChecker: IPythonExtensionChecker) {} public async getInterpreters(_resource?: Uri): Promise { @@ -41,17 +43,17 @@ export class InterpreterService implements IInterpreterService { this.validatePythonExtension(); const pythonPath = this.customInterpretersPerUri.has(resource?.fsPath || '') ? this.customInterpretersPerUri.get(resource?.fsPath || '')! - : process.env.CI_PYTHON_PATH || 'python'; + : fsPathToUri(process.env.CI_PYTHON_PATH || 'python'); return getInterpreterInfo(pythonPath); } - public async getInterpreterDetails(pythonPath: string, _resource?: Uri): Promise { + public async getInterpreterDetails(pythonPath: Uri, _resource?: Uri): Promise { this.validatePythonExtension(); return getInterpreterInfo(pythonPath); } - public updateInterpreter(resource: Resource, pythonPath: string): void { - if (pythonPath.trim().length > 0) { + public updateInterpreter(resource: Resource, pythonPath: Uri): void { + if (pythonPath.fsPath.trim().length > 0) { this.customInterpretersPerUri.set(resource?.fsPath || '', pythonPath); } this.didChangeInterpreter.fire(); @@ -66,15 +68,15 @@ export class InterpreterService implements IInterpreterService { async function getAllInterpreters(): Promise { const allInterpreters = await Promise.all([ - getInterpreterInfo(process.env.CI_PYTHON_PATH as string), - getInterpreterInfo(process.env.CI_PYTHON_PATH2 as string), - getInterpreterInfo('python') + getInterpreterInfo(fsPathToUri(process.env.CI_PYTHON_PATH)), + getInterpreterInfo(fsPathToUri(process.env.CI_PYTHON_PATH2)), + getInterpreterInfo(fsPathToUri('python')) ]); const interpreters: PythonEnvironment[] = []; - const items = new Set(); + const items = new ResourceSet(); allInterpreters.forEach((item) => { - if (item && !items.has(item.path)) { - items.add(item.path); + if (item && !items.has(item.uri)) { + items.add(item.uri); interpreters.push(item); } }); diff --git a/src/test/interpreters/selector.ts b/src/test/interpreters/selector.ts index 6db00cee02f..3502bc7cbc2 100644 --- a/src/test/interpreters/selector.ts +++ b/src/test/interpreters/selector.ts @@ -1,4 +1,5 @@ import { inject, injectable } from 'inversify'; +import { getDisplayPath } from '../../platform/common/platform/fs-paths'; import { Resource } from '../../platform/common/types'; // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -12,12 +13,15 @@ export class InterpreterSelector implements IInterpreterSelector { public async getSuggestions(resource: Resource): Promise { const interpreters = await this.interpreterService.getInterpreters(resource); - return interpreters.map((item) => ({ - label: item.displayName || item.path, - description: item.displayName || item.path, - detail: item.displayName || item.path, - path: item.path, - interpreter: item - })); + return interpreters.map((item) => { + const filePath = getDisplayPath(item.uri); + return { + label: item.displayName || filePath, + description: item.displayName || filePath, + detail: item.displayName || filePath, + path: filePath, + interpreter: item + }; + }); } } diff --git a/src/test/kernels/installer/channelManager.channels.unit.test.ts b/src/test/kernels/installer/channelManager.channels.unit.test.ts index 5acffe6e6ff..b0b4fc2b4b6 100644 --- a/src/test/kernels/installer/channelManager.channels.unit.test.ts +++ b/src/test/kernels/installer/channelManager.channels.unit.test.ts @@ -12,13 +12,14 @@ import { IServiceContainer } from '../../../platform/ioc/types'; import { EnvironmentType, PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { InstallationChannelManager } from '../../../kernels/installer/channelManager.node'; import { Product, IModuleInstaller } from '../../../kernels/installer/types'; +import { Uri } from 'vscode'; suite('Installation - installation channels', () => { let serviceManager: ServiceManager; let serviceContainer: IServiceContainer; const interpreter: PythonEnvironment = { envType: EnvironmentType.Conda, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; diff --git a/src/test/kernels/installer/channelManager.messages.unit.test.ts b/src/test/kernels/installer/channelManager.messages.unit.test.ts index 5703e31d3ca..bfd7c0cadf4 100644 --- a/src/test/kernels/installer/channelManager.messages.unit.test.ts +++ b/src/test/kernels/installer/channelManager.messages.unit.test.ts @@ -14,11 +14,12 @@ import { IServiceContainer } from '../../../platform/ioc/types'; import { EnvironmentType, PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { InstallationChannelManager } from '../../../kernels/installer/channelManager.node'; import { IModuleInstaller, Product } from '../../../kernels/installer/types'; +import { Uri } from 'vscode'; const info: PythonEnvironment = { displayName: '', envName: '', - path: '', + uri: Uri.file(''), envType: EnvironmentType.Unknown, version: new SemVer('0.0.0-alpha'), sysPrefix: '', @@ -139,7 +140,7 @@ suite('Installation - channel messages', () => { const activeInterpreter: PythonEnvironment = { ...info, envType: interpreterType, - path: '' + uri: Uri.file('') }; interpreters .setup((x) => x.getActiveInterpreter(TypeMoq.It.isAny())) diff --git a/src/test/kernels/installer/channelManager.unit.test.ts b/src/test/kernels/installer/channelManager.unit.test.ts index f95ab6ea449..d02067a2a9d 100644 --- a/src/test/kernels/installer/channelManager.unit.test.ts +++ b/src/test/kernels/installer/channelManager.unit.test.ts @@ -12,6 +12,7 @@ import { IServiceContainer } from '../../../platform/ioc/types'; import { EnvironmentType, PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { InstallationChannelManager } from '../../../kernels/installer/channelManager.node'; import { IModuleInstaller, Product } from '../../../kernels/installer/types'; +import { Uri } from 'vscode'; suite('InstallationChannelManager - getInstallationChannel()', () => { let serviceContainer: TypeMoq.IMock; @@ -22,7 +23,7 @@ suite('InstallationChannelManager - getInstallationChannel()', () => { let showNoInstallersMessage: sinon.SinonStub; const interpreter: PythonEnvironment = { envType: EnvironmentType.Global, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; let installChannelManager: InstallationChannelManager; @@ -92,7 +93,7 @@ suite('InstallationChannelManager - getInstallationChannels()', () => { let serviceContainer: TypeMoq.IMock; const interpreter: PythonEnvironment = { envType: EnvironmentType.Global, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; @@ -160,7 +161,7 @@ suite('InstallationChannelManager - showNoInstallersMessage()', () => { test('If active interpreter is Conda, show conda prompt', async () => { const activeInterpreter = { envType: EnvironmentType.Conda, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '' }; appShell @@ -175,7 +176,7 @@ suite('InstallationChannelManager - showNoInstallersMessage()', () => { test('If active interpreter is not Conda, show pip prompt', async () => { const activeInterpreter = { envType: EnvironmentType.Pipenv, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '' }; appShell @@ -215,7 +216,7 @@ suite('InstallationChannelManager - showNoInstallersMessage()', () => { }`, async () => { const activeInterpreter = { envType: interpreterType, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '' }; const platformService = TypeMoq.Mock.ofType(); @@ -243,7 +244,7 @@ suite('InstallationChannelManager - showNoInstallersMessage()', () => { test("If 'Search for help' is not selected in error prompt, don't open URL", async () => { const activeInterpreter = { envType: EnvironmentType.Conda, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '' }; const platformService = TypeMoq.Mock.ofType(); diff --git a/src/test/kernels/installer/condaInstaller.unit.test.ts b/src/test/kernels/installer/condaInstaller.unit.test.ts index fb512276301..e96584dc29d 100644 --- a/src/test/kernels/installer/condaInstaller.unit.test.ts +++ b/src/test/kernels/installer/condaInstaller.unit.test.ts @@ -15,6 +15,7 @@ import { EnvironmentType, PythonEnvironment } from '../../../platform/pythonEnvi import { CondaInstaller } from '../../../kernels/installer/condaInstaller.node'; import { ExecutionInstallArgs } from '../../../kernels/installer/moduleInstaller.node'; import { ModuleInstallFlags } from '../../../kernels/installer/types'; +import { Uri } from 'vscode'; suite('Common - Conda Installer', () => { let installer: CondaInstallerTest; @@ -46,7 +47,7 @@ suite('Common - Conda Installer', () => { test('Installer is not supported when conda is available variable is set to false', async () => { const interpreter: PythonEnvironment = { envType: EnvironmentType.Conda, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; @@ -59,7 +60,7 @@ suite('Common - Conda Installer', () => { test('Installer is not supported when conda is not available', async () => { const interpreter: PythonEnvironment = { envType: EnvironmentType.Conda, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; when(condaService.isCondaAvailable()).thenResolve(false); @@ -71,7 +72,7 @@ suite('Common - Conda Installer', () => { test('Installer is not supported when current env is not a conda env', async () => { const interpreter: PythonEnvironment = { envType: EnvironmentType.Global, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; const settings = mock(JupyterSettings); @@ -86,7 +87,7 @@ suite('Common - Conda Installer', () => { test('Installer is supported when current env is a conda env', async () => { const interpreter: PythonEnvironment = { envType: EnvironmentType.Conda, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; const settings = mock(JupyterSettings); @@ -101,12 +102,12 @@ suite('Common - Conda Installer', () => { test('Include name of environment', async () => { const interpreter: PythonEnvironment = { envType: EnvironmentType.Conda, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0', envName: 'baz' }; const settings = mock(JupyterSettings); - const condaPath = 'some Conda Path'; + const condaPath = Uri.file('some Conda Path'); when(configService.getSettings(undefined)).thenReturn(instance(settings)); when(condaService.getCondaFile()).thenResolve(condaPath); @@ -114,16 +115,19 @@ suite('Common - Conda Installer', () => { const execInfo = await installer.getExecutionArgs('abc', interpreter); - assert.deepEqual(execInfo, { args: ['install', '--name', interpreter.envName, 'abc', '-y'], exe: condaPath }); + assert.deepStrictEqual(execInfo, { + args: ['install', '--name', interpreter.envName, 'abc', '-y'], + exe: condaPath.fsPath + }); }); test('Include path of environment', async () => { const settings = mock(JupyterSettings); const interpreter: PythonEnvironment = { envType: EnvironmentType.Conda, - path: 'baz/foobar/python.exe', + uri: Uri.file('baz/foobar/python.exe'), sysPrefix: '0' }; - const condaPath = 'some Conda Path'; + const condaPath = Uri.file('some Conda Path'); when(configService.getSettings(undefined)).thenReturn(instance(settings)); when(condaService.getCondaFile()).thenResolve(condaPath); @@ -131,19 +135,19 @@ suite('Common - Conda Installer', () => { const execInfo = await installer.getExecutionArgs('abc', interpreter); - assert.deepEqual(execInfo, { - args: ['install', '--prefix', 'baz/foobar'.fileToCommandArgument(), 'abc', '-y'], - exe: condaPath + assert.deepStrictEqual(execInfo, { + args: ['install', '--prefix', '/baz/foobar'.fileToCommandArgument(), 'abc', '-y'], + exe: condaPath.fsPath }); }); test('Include path of environment but skip bin', async () => { const settings = mock(JupyterSettings); const interpreter: PythonEnvironment = { envType: EnvironmentType.Conda, - path: 'baz/foobar/bin/python.exe', + uri: Uri.file('baz/foobar/bin/python.exe'), sysPrefix: '0' }; - const condaPath = 'some Conda Path'; + const condaPath = Uri.file('some Conda Path'); when(configService.getSettings(undefined)).thenReturn(instance(settings)); when(condaService.getCondaFile()).thenResolve(condaPath); @@ -151,9 +155,9 @@ suite('Common - Conda Installer', () => { const execInfo = await installer.getExecutionArgs('abc', interpreter); - assert.deepEqual(execInfo, { - args: ['install', '--prefix', 'baz/foobar'.fileToCommandArgument(), 'abc', '-y'], - exe: condaPath + assert.deepStrictEqual(execInfo, { + args: ['install', '--prefix', '/baz/foobar'.fileToCommandArgument(), 'abc', '-y'], + exe: condaPath.fsPath }); }); }); diff --git a/src/test/kernels/installer/pipEnvInstaller.unit.test.ts b/src/test/kernels/installer/pipEnvInstaller.unit.test.ts index 15eb20c3caf..748cc4385cf 100644 --- a/src/test/kernels/installer/pipEnvInstaller.unit.test.ts +++ b/src/test/kernels/installer/pipEnvInstaller.unit.test.ts @@ -21,8 +21,8 @@ suite('PipEnv installer', async () => { let workspaceService: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; let pipEnvInstaller: PipEnvInstaller; - const interpreterPath = 'path/to/interpreter'; - const workspaceFolder = 'path/to/folder'; + const interpreterPath = Uri.file('path/to/interpreter'); + const workspaceFolder = Uri.file('path/to/folder'); setup(() => { serviceContainer = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); @@ -36,7 +36,7 @@ suite('PipEnv installer', async () => { isPipenvEnvironmentRelatedToFolder = sinon .stub(pipEnvHelper, 'isPipenvEnvironmentRelatedToFolder') - .callsFake((interpreter: string, folder: string) => { + .callsFake((interpreter: Uri, folder: Uri) => { return Promise.resolve(interpreterPath === interpreter && folder === workspaceFolder); }); pipEnvInstaller = new PipEnvInstaller(serviceContainer.object, workspaceService.object); @@ -77,11 +77,9 @@ suite('PipEnv installer', async () => { interpreterService .setup((p) => p.getActiveInterpreter(resource)) - .returns(() => Promise.resolve({ envType: EnvironmentType.Pipenv, path: interpreterPath } as any)); + .returns(() => Promise.resolve({ envType: EnvironmentType.Pipenv, uri: interpreterPath } as any)); - workspaceService - .setup((w) => w.getWorkspaceFolder(resource)) - .returns(() => ({ uri: { fsPath: workspaceFolder } } as any)); + workspaceService.setup((w) => w.getWorkspaceFolder(resource)).returns(() => ({ uri: workspaceFolder } as any)); const result = await pipEnvInstaller.isSupported(resource); expect(result).to.equal(true, 'Should be true'); }); @@ -90,7 +88,7 @@ suite('PipEnv installer', async () => { const resource = Uri.parse('a'); interpreterService .setup((p) => p.getActiveInterpreter(resource)) - .returns(() => Promise.resolve({ envType: EnvironmentType.Conda, path: interpreterPath } as any)); + .returns(() => Promise.resolve({ envType: EnvironmentType.Conda, uri: interpreterPath } as any)); workspaceService .setup((w) => w.getWorkspaceFolder(resource)) @@ -103,7 +101,7 @@ suite('PipEnv installer', async () => { const resource = Uri.parse('a'); interpreterService .setup((p) => p.getActiveInterpreter(resource)) - .returns(() => Promise.resolve({ envType: EnvironmentType.Pipenv, path: 'some random path' } as any)); + .returns(() => Promise.resolve({ envType: EnvironmentType.Pipenv, uri: 'some random path' } as any)); workspaceService .setup((w) => w.getWorkspaceFolder(resource)) diff --git a/src/test/kernels/installer/pipInstaller.unit.test.ts b/src/test/kernels/installer/pipInstaller.unit.test.ts index c27d4fb6042..d85652c3428 100644 --- a/src/test/kernels/installer/pipInstaller.unit.test.ts +++ b/src/test/kernels/installer/pipInstaller.unit.test.ts @@ -10,6 +10,7 @@ import { IPythonExecutionFactory, IPythonExecutionService } from '../../../platf import { IServiceContainer } from '../../../platform/ioc/types'; import { EnvironmentType, PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { PipInstaller } from '../../../kernels/installer/pipInstaller.node'; +import { Uri } from 'vscode'; suite('Pip installer', async () => { let serviceContainer: TypeMoq.IMock; @@ -55,7 +56,7 @@ suite('Pip installer', async () => { const pythonExecutionService = TypeMoq.Mock.ofType(); const interpreter: PythonEnvironment = { envType: EnvironmentType.Global, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; @@ -74,7 +75,7 @@ suite('Pip installer', async () => { const pythonExecutionService = TypeMoq.Mock.ofType(); const interpreter: PythonEnvironment = { envType: EnvironmentType.Global, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; @@ -93,7 +94,7 @@ suite('Pip installer', async () => { const pythonExecutionService = TypeMoq.Mock.ofType(); const interpreter: PythonEnvironment = { envType: EnvironmentType.Global, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; pythonExecutionFactory diff --git a/src/test/kernels/installer/pipenv.unit.test.ts b/src/test/kernels/installer/pipenv.unit.test.ts index 264dce0a433..fe6048dcbdf 100644 --- a/src/test/kernels/installer/pipenv.unit.test.ts +++ b/src/test/kernels/installer/pipenv.unit.test.ts @@ -9,6 +9,7 @@ import { isPipenvEnvironment, _getAssociatedPipfile } from '../../../kernels/installer/pipenv.node'; +import { Uri } from 'vscode'; const path = platformApis.getOSType() === platformApis.OSType.Windows ? pathModule.win32 : pathModule.posix; @@ -39,24 +40,22 @@ suite('Pipenv helper', () => { arePathsSame.restore(); getEnvVar.withArgs('PIPENV_MAX_DEPTH').returns('5'); - const expectedDotProjectFile = path.join( - TEST_LAYOUT_ROOT, - 'pipenv', - 'globalEnvironments', - 'project3-2s1eXEJ2', - '.project' + const expectedDotProjectFile = Uri.file( + path.join(TEST_LAYOUT_ROOT, 'pipenv', 'globalEnvironments', 'project3-2s1eXEJ2', '.project') ); const project = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project3'); - readFile.withArgs(expectedDotProjectFile).resolves(project); - const interpreterPath: string = path.join( - TEST_LAYOUT_ROOT, - 'pipenv', - 'globalEnvironments', - 'project3-2s1eXEJ2', - 'Scripts', - 'python.exe' + readFile.withArgs(expectedDotProjectFile.fsPath).resolves(project); + const interpreterPath = Uri.file( + path.join( + TEST_LAYOUT_ROOT, + 'pipenv', + 'globalEnvironments', + 'project3-2s1eXEJ2', + 'Scripts', + 'python.exe' + ) ); - const folder = path.join(project, 'parent', 'child', 'folder'); + const folder = Uri.file(path.join(project, 'parent', 'child', 'folder')); const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); @@ -67,9 +66,9 @@ suite('Pipenv helper', () => { const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); // Dot project file doesn't exist pathExists.withArgs(expectedDotProjectFile).resolves(false); - const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); - pathExists.withArgs(interpreterPath).resolves(true); - const folder = path.join('path', 'to', 'folder'); + const interpreterPath = Uri.file(path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe')); + pathExists.withArgs(interpreterPath.fsPath).resolves(true); + const folder = Uri.file(path.join('path', 'to', 'folder')); const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); @@ -85,10 +84,10 @@ suite('Pipenv helper', () => { const pipFileAssociatedWithEnvironment = path.join(project, 'Pipfile'); // Pipfile associated with environment exists pathExists.withArgs(pipFileAssociatedWithEnvironment).resolves(true); - const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); - pathExists.withArgs(interpreterPath).resolves(true); - const folder = path.join('path', 'to', 'folder'); - const pipFileAssociatedWithFolder = path.join(folder, 'Pipfile'); + const interpreterPath = Uri.file(path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe')); + pathExists.withArgs(interpreterPath.fsPath).resolves(true); + const folder = Uri.file(path.join('path', 'to', 'folder')); + const pipFileAssociatedWithFolder = path.join(folder.fsPath, 'Pipfile'); // Pipfile associated with folder doesn't exist pathExists.withArgs(pipFileAssociatedWithFolder).resolves(false); @@ -106,10 +105,10 @@ suite('Pipenv helper', () => { const pipFileAssociatedWithEnvironment = path.join(project, 'Pipfile'); // Pipfile associated with environment exists pathExists.withArgs(pipFileAssociatedWithEnvironment).resolves(true); - const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); - pathExists.withArgs(interpreterPath).resolves(true); - const folder = path.join('path', 'to', 'folder'); - const pipFileAssociatedWithFolder = path.join(folder, 'Pipfile'); + const interpreterPath = Uri.file(path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe')); + pathExists.withArgs(interpreterPath.fsPath).resolves(true); + const folder = Uri.file(path.join('path', 'to', 'folder')); + const pipFileAssociatedWithFolder = path.join(folder.fsPath, 'Pipfile'); // Pipfile associated with folder exists pathExists.withArgs(pipFileAssociatedWithFolder).resolves(true); // But the paths to both Pipfiles aren't the same @@ -121,22 +120,24 @@ suite('Pipenv helper', () => { }); test('If a Pipfile is associated with the environment and another is associated with the folder, and the path to both Pipfiles are same, return true', async () => { - const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); - pathExists.withArgs(expectedDotProjectFile).resolves(true); - const project = path.join('path', 'to', 'project'); - readFile.withArgs(expectedDotProjectFile).resolves(project); - pathExists.withArgs(project).resolves(true); - const pipFileAssociatedWithEnvironment = path.join(project, 'Pipfile'); + const expectedDotProjectFile = Uri.file(path.join('environments', 'project-2s1eXEJ2', '.project')); + pathExists.withArgs(expectedDotProjectFile.fsPath).resolves(true); + const project = Uri.file(path.join('path', 'to', 'project')); + readFile.withArgs(expectedDotProjectFile.fsPath).resolves(project.fsPath); + pathExists.withArgs(project.fsPath).resolves(true); + const pipFileAssociatedWithEnvironment = Uri.file(path.join(project.fsPath, 'Pipfile')); // Pipfile associated with environment exists - pathExists.withArgs(pipFileAssociatedWithEnvironment).resolves(true); - const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); - pathExists.withArgs(interpreterPath).resolves(true); - const folder = path.join('path', 'to', 'folder'); - const pipFileAssociatedWithFolder = path.join(folder, 'Pipfile'); + pathExists.withArgs(pipFileAssociatedWithEnvironment.fsPath).resolves(true); + const interpreterPath = Uri.file(path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe')); + pathExists.withArgs(interpreterPath.fsPath).resolves(true); + const folder = Uri.file(path.join('path', 'to', 'folder')); + const pipFileAssociatedWithFolder = Uri.file(path.join(folder.fsPath, 'Pipfile')); // Pipfile associated with folder exists - pathExists.withArgs(pipFileAssociatedWithFolder).resolves(true); + pathExists.withArgs(pipFileAssociatedWithFolder.fsPath).resolves(true); // The paths to both Pipfiles are also the same - arePathsSame.withArgs(pipFileAssociatedWithEnvironment, pipFileAssociatedWithFolder).resolves(true); + arePathsSame + .withArgs(pipFileAssociatedWithEnvironment.fsPath, pipFileAssociatedWithFolder.fsPath) + .resolves(true); const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); @@ -161,13 +162,13 @@ suite('Pipenv helper', () => { }); test('If the project layout matches that of a local pipenv environment, return true', async () => { - const project = path.join('path', 'to', 'project'); - pathExists.withArgs(project).resolves(true); - const pipFile = path.join(project, 'Pipfile'); + const project = Uri.file(path.join('path', 'to', 'project')); + pathExists.withArgs(project.fsPath).resolves(true); + const pipFile = Uri.file(path.join(project.fsPath, 'Pipfile')); // Pipfile associated with environment exists - pathExists.withArgs(pipFile).resolves(true); + pathExists.withArgs(pipFile.fsPath).resolves(true); // Environment is inside the project - const interpreterPath = path.join(project, '.venv', 'Scripts', 'python.exe'); + const interpreterPath = Uri.file(path.join(project.fsPath, '.venv', 'Scripts', 'python.exe')); const result = await isPipenvEnvironment(interpreterPath); @@ -175,7 +176,7 @@ suite('Pipenv helper', () => { }); test('If not local & dotProject file is missing, return false', async () => { - const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + const interpreterPath = Uri.file(path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe')); const project = path.join('path', 'to', 'project'); pathExists.withArgs(project).resolves(true); const pipFile = path.join(project, 'Pipfile'); @@ -191,7 +192,7 @@ suite('Pipenv helper', () => { }); test('If not local & dotProject contains invalid path to project, return false', async () => { - const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + const interpreterPath = Uri.file(path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe')); const project = path.join('path', 'to', 'project'); // Project doesn't exist pathExists.withArgs(project).resolves(false); @@ -207,7 +208,7 @@ suite('Pipenv helper', () => { }); test("If not local & the name of the project isn't used as a prefix in the environment folder, return false", async () => { - const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + const interpreterPath = Uri.file(path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe')); // The project name (someProjectName) isn't used as a prefix in environment folder name (project-2s1eXEJ2) const project = path.join('path', 'to', 'someProjectName'); pathExists.withArgs(project).resolves(true); @@ -224,15 +225,15 @@ suite('Pipenv helper', () => { }); test('If the project layout matches that of a global pipenv environment, return true', async () => { - const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); - const project = path.join('path', 'to', 'project'); - pathExists.withArgs(project).resolves(true); - const pipFile = path.join(project, 'Pipfile'); + const interpreterPath = Uri.file(path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe')); + const project = Uri.file(path.join('path', 'to', 'project')); + pathExists.withArgs(project.fsPath).resolves(true); + const pipFile = Uri.file(path.join(project.fsPath, 'Pipfile')); // Pipfile associated with environment exists - pathExists.withArgs(pipFile).resolves(true); - const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); - pathExists.withArgs(expectedDotProjectFile).resolves(true); - readFile.withArgs(expectedDotProjectFile).resolves(project); + pathExists.withArgs(pipFile.fsPath).resolves(true); + const expectedDotProjectFile = Uri.file(path.join('environments', 'project-2s1eXEJ2', '.project')); + pathExists.withArgs(expectedDotProjectFile.fsPath).resolves(true); + readFile.withArgs(expectedDotProjectFile.fsPath).resolves(project.fsPath); const result = await isPipenvEnvironment(interpreterPath); diff --git a/src/test/kernels/installer/poetry.unit.test.ts b/src/test/kernels/installer/poetry.unit.test.ts index e1e6b6fe4d2..dc9ec208d31 100644 --- a/src/test/kernels/installer/poetry.unit.test.ts +++ b/src/test/kernels/installer/poetry.unit.test.ts @@ -158,7 +158,7 @@ suite('Poetry binary is located correctly', async () => { }); test('When poetry is not available on PATH, try using the default poetry location if valid', async () => { - const home = platformApisNode.getUserHomeDir(); + const home = platformApisNode.getUserHomeDir()?.fsPath; if (!home) { assert(true); return; diff --git a/src/test/kernels/installer/poetryInstaller.unit.test.ts b/src/test/kernels/installer/poetryInstaller.unit.test.ts index 4ac83db95ed..89a7418b90f 100644 --- a/src/test/kernels/installer/poetryInstaller.unit.test.ts +++ b/src/test/kernels/installer/poetryInstaller.unit.test.ts @@ -88,7 +88,7 @@ suite('Module Installer - Poetry', () => { test('Is not supported when there is no workspace', async () => { const interpreter: PythonEnvironment = { envType: EnvironmentType.Poetry, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; @@ -101,7 +101,7 @@ suite('Module Installer - Poetry', () => { test('Get Executable info', async () => { const interpreter: PythonEnvironment = { envType: EnvironmentType.Poetry, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; const settings = mock(JupyterSettings); @@ -118,7 +118,7 @@ suite('Module Installer - Poetry', () => { const settings = mock(JupyterSettings); const interpreter: PythonEnvironment = { envType: EnvironmentType.Poetry, - path: path.join(project1, '.venv', 'scripts', 'python.exe'), + uri: Uri.file(path.join(project1, '.venv', 'scripts', 'python.exe')), sysPrefix: '0' }; @@ -136,7 +136,7 @@ suite('Module Installer - Poetry', () => { const settings = mock(JupyterSettings); const interpreter: PythonEnvironment = { envType: EnvironmentType.Poetry, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; @@ -154,7 +154,7 @@ suite('Module Installer - Poetry', () => { const settings = mock(JupyterSettings); const interpreter: PythonEnvironment = { envType: EnvironmentType.Conda, - path: 'foobar', + uri: Uri.file('foobar'), sysPrefix: '0' }; diff --git a/src/test/kernels/installer/productInstaller.unit.test.ts b/src/test/kernels/installer/productInstaller.unit.test.ts index 2a39cd8cfb8..8f48ec0a462 100644 --- a/src/test/kernels/installer/productInstaller.unit.test.ts +++ b/src/test/kernels/installer/productInstaller.unit.test.ts @@ -6,7 +6,7 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource } from 'vscode'; +import { CancellationTokenSource, Uri } from 'vscode'; import { IApplicationShell } from '../../../platform/common/application/types'; import { InterpreterUri, IOutputChannel } from '../../../platform/common/types'; import { IServiceContainer } from '../../../platform/ioc/types'; @@ -36,7 +36,7 @@ suite('DataScienceInstaller install', async () => { let outputChannel: TypeMoq.IMock; let tokenSource: CancellationTokenSource; - const interpreterPath = 'path/to/interpreter'; + const interpreterPath = Uri.file('path/to/interpreter'); setup(() => { tokenSource = new CancellationTokenSource(); @@ -63,7 +63,7 @@ suite('DataScienceInstaller install', async () => { envType: EnvironmentType.VirtualEnv, envName: 'test', envPath: interpreterPath, - path: interpreterPath, + uri: interpreterPath, sysPrefix: '' }; installationChannelManager @@ -78,7 +78,7 @@ suite('DataScienceInstaller install', async () => { envType: EnvironmentType.VirtualEnv, envName: 'test', envPath: interpreterPath, - path: interpreterPath, + uri: interpreterPath, sysPrefix: '' }; const testInstaller = TypeMoq.Mock.ofType(); @@ -108,7 +108,7 @@ suite('DataScienceInstaller install', async () => { envType: EnvironmentType.Conda, envName: 'test', envPath: interpreterPath, - path: interpreterPath, + uri: interpreterPath, sysPrefix: '' }; const testInstaller = TypeMoq.Mock.ofType(); @@ -138,7 +138,7 @@ suite('DataScienceInstaller install', async () => { envType: EnvironmentType.VirtualEnv, envName: 'test', envPath: interpreterPath, - path: interpreterPath, + uri: interpreterPath, sysPrefix: '' }; const testInstaller = TypeMoq.Mock.ofType(); @@ -169,7 +169,7 @@ suite('DataScienceInstaller install', async () => { envType: EnvironmentType.Poetry, envName: 'test', envPath: interpreterPath, - path: interpreterPath, + uri: interpreterPath, sysPrefix: '' }; const testInstaller = TypeMoq.Mock.ofType(); @@ -200,7 +200,7 @@ suite('DataScienceInstaller install', async () => { envType: EnvironmentType.Pipenv, envName: 'test', envPath: interpreterPath, - path: interpreterPath, + uri: interpreterPath, sysPrefix: '' }; const testInstaller = TypeMoq.Mock.ofType(); diff --git a/src/test/kernels/installer/pyenv.unit.test.ts b/src/test/kernels/installer/pyenv.unit.test.ts index 643e0e65067..ad6b44d7dc9 100644 --- a/src/test/kernels/installer/pyenv.unit.test.ts +++ b/src/test/kernels/installer/pyenv.unit.test.ts @@ -3,6 +3,7 @@ import * as assert from 'assert'; import * as path from '../../../platform/vscode-path/path'; +import * as uriPath from '../../../platform/vscode-path/resources'; import * as sinon from 'sinon'; import * as platformUtilsNode from '../../../platform/common/utils/platform.node'; import * as platformUtils from '../../../platform/common/utils/platform'; @@ -13,9 +14,10 @@ import { parsePyenvVersion, isPyenvShimDir } from '../../../kernels/installer/pyenv.node'; +import { Uri } from 'vscode'; suite('Pyenv Identifier Tests', () => { - const home = platformUtilsNode.getUserHomeDir() || ''; + const home = platformUtilsNode.getUserHomeDir() || Uri.file(''); let getEnvVariableStub: sinon.SinonStub; let pathExistsStub: sinon.SinonStub; let getOsTypeStub: sinon.SinonStub; @@ -38,7 +40,7 @@ suite('Pyenv Identifier Tests', () => { type PyenvUnitTestData = { testTitle: string; - interpreterPath: string; + interpreterPath: Uri; pyenvEnvVar?: string; osType: platformUtils.OSType; }; @@ -46,35 +48,37 @@ suite('Pyenv Identifier Tests', () => { const testData: PyenvUnitTestData[] = [ { testTitle: 'undefined', - interpreterPath: path.join(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'), + interpreterPath: uriPath.joinPath(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'), osType: platformUtils.OSType.Linux }, { testTitle: 'undefined', - interpreterPath: path.join(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), + interpreterPath: uriPath.joinPath(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), osType: platformUtils.OSType.Windows }, { testTitle: 'its default value', - interpreterPath: path.join(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'), - pyenvEnvVar: path.join(home, '.pyenv'), + interpreterPath: uriPath.joinPath(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join(home.fsPath, '.pyenv'), osType: platformUtils.OSType.Linux }, { testTitle: 'its default value', - interpreterPath: path.join(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), - pyenvEnvVar: path.join(home, '.pyenv', 'pyenv-win'), + interpreterPath: uriPath.joinPath(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join(home.fsPath, '.pyenv', 'pyenv-win'), osType: platformUtils.OSType.Windows }, { testTitle: 'a custom value', - interpreterPath: path.join('path', 'to', 'mypyenv', 'versions', '3.8.0', 'bin', 'python'), + interpreterPath: Uri.file(path.join('path', 'to', 'mypyenv', 'versions', '3.8.0', 'bin', 'python')), pyenvEnvVar: path.join('path', 'to', 'mypyenv'), osType: platformUtils.OSType.Linux }, { testTitle: 'a custom value', - interpreterPath: path.join('path', 'to', 'mypyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), + interpreterPath: Uri.file( + path.join('path', 'to', 'mypyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python') + ), pyenvEnvVar: path.join('path', 'to', 'mypyenv', 'pyenv-win'), osType: platformUtils.OSType.Windows } @@ -94,7 +98,7 @@ suite('Pyenv Identifier Tests', () => { }); test('The pyenv directory does not exist', async () => { - const interpreterPath = path.join('path', 'to', 'python'); + const interpreterPath = Uri.file(path.join('path', 'to', 'python')); pathExistsStub.resolves(false); @@ -104,7 +108,7 @@ suite('Pyenv Identifier Tests', () => { }); test('The interpreter path is not in a subfolder of the pyenv folder', async () => { - const interpreterPath = path.join('path', 'to', 'python'); + const interpreterPath = Uri.file(path.join('path', 'to', 'python')); pathExistsStub.resolves(true); @@ -302,9 +306,9 @@ suite('Pyenv Shims Dir filter tests', () => { }); test('isPyenvShimDir: valid case', () => { - assert.deepStrictEqual(isPyenvShimDir(path.join(pyenvRoot, 'shims')), true); + assert.deepStrictEqual(isPyenvShimDir(Uri.file(path.join(pyenvRoot, 'shims'))), true); }); test('isPyenvShimDir: invalid case', () => { - assert.deepStrictEqual(isPyenvShimDir(__dirname), false); + assert.deepStrictEqual(isPyenvShimDir(Uri.file(__dirname)), false); }); }); diff --git a/src/test/kernels/kernelAutoRestartMonitor.unit.test.ts b/src/test/kernels/kernelAutoRestartMonitor.unit.test.ts index 13498de2428..5cba71de294 100644 --- a/src/test/kernels/kernelAutoRestartMonitor.unit.test.ts +++ b/src/test/kernels/kernelAutoRestartMonitor.unit.test.ts @@ -3,7 +3,7 @@ import type { KernelMessage } from '@jupyterlab/services'; import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { EventEmitter } from 'vscode'; +import { EventEmitter, Uri } from 'vscode'; import { getDisplayNameOrNameOfKernelConnection } from '../../kernels/helpers.node'; import { KernelAutoRestartMonitor } from '../../kernels/kernelAutoRestartMonitor.node'; import { IJupyterSession, IKernel, IKernelProvider, KernelConnectionMetadata } from '../../kernels/types'; @@ -27,7 +27,7 @@ suite('Jupyter Execution', async () => { argv: [], display_name: 'Hello', name: 'hello', - path: 'path' + uri: Uri.file('path') }, kind: 'startUsingLocalKernelSpec' }; diff --git a/src/test/testHashPerf.ts b/src/test/testHashPerf.ts index 0a72af7afaf..bf6cbaca359 100644 --- a/src/test/testHashPerf.ts +++ b/src/test/testHashPerf.ts @@ -1,9 +1,10 @@ import * as crypto from 'crypto'; import * as path from '../platform/vscode-path/path'; import { getInterpreterHash } from '../platform/pythonEnvironments/info/interpreter.node'; +import { Uri } from 'vscode'; function doHash(p: string) { - return getInterpreterHash({ path: p }); + return getInterpreterHash({ uri: Uri.file(p) }); } function trivial(p: string) { diff --git a/src/test/testRunner.ts b/src/test/testRunner.ts index fdf685eb784..7ad95ffcadf 100644 --- a/src/test/testRunner.ts +++ b/src/test/testRunner.ts @@ -11,7 +11,7 @@ import * as Mocha from 'mocha'; import * as path from '../platform/vscode-path/path'; import { IS_SMOKE_TEST, MAX_EXTENSION_ACTIVATION_TIME } from './constants.node'; import { noop } from './core'; -import { stopJupyterServer } from './datascience/notebook/helper'; +import { stopJupyterServer } from './datascience/notebook/helper.node'; import { initialize } from './initialize.node'; // Linux: prevent a weird NPE when mocha on Linux requires the window size from the TTY. diff --git a/src/test/utils/interpreters.ts b/src/test/utils/interpreters.ts index 501e88a44e2..508d2afbe51 100644 --- a/src/test/utils/interpreters.ts +++ b/src/test/utils/interpreters.ts @@ -3,6 +3,7 @@ 'use strict'; +import { Uri } from 'vscode'; import { PythonEnvironment } from '../../platform/pythonEnvironments/info'; /** @@ -17,7 +18,7 @@ export function createPythonInterpreter(info?: Partial): Pyth const rnd = new Date().getTime().toString(); return { displayName: `Something${rnd}`, - path: `somePath${rnd}`, + uri: Uri.file(`somePath${rnd}`), sysPrefix: `someSysPrefix${rnd}`, sysVersion: `1.1.1`, ...(info || {})