Skip to content

Commit

Permalink
Added multi-root workspaceFolder support in path variable expansion (#…
Browse files Browse the repository at this point in the history
…7138)

Co-authored-by: Seairth Jacobs <sjacobs@wrsystems.com>
  • Loading branch information
Seairth and Seairth committed Jan 29, 2024
1 parent 419ae42 commit 12e9dd9
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 11 deletions.
7 changes: 6 additions & 1 deletion packages/pyright-internal/src/common/envVarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@

import * as os from 'os';

import { Workspace } from '../workspaceFactory';
import { Uri } from './uri/uri';

// Expands certain predefined variables supported within VS Code settings.
// Ideally, VS Code would provide an API for doing this expansion, but
// it doesn't. We'll handle the most common variables here as a convenience.
export function expandPathVariables(rootPath: Uri, path: string): string {
export function expandPathVariables(path: string, rootPath: Uri, workspaces: Workspace[]): string {
// Make sure the pathStr looks like a URI path.
let pathStr = path.replace(/\\/g, '/');

Expand All @@ -24,6 +25,10 @@ export function expandPathVariables(rootPath: Uri, path: string): string {

// Replace everything inline.
pathStr = pathStr.replace(/\$\{workspaceFolder\}/g, rootPath.getPath());
for (const workspace of workspaces) {
const ws_regexp = RegExp(`\\$\\{workspaceFolder:${workspace.workspaceName}\\}`, 'g');
pathStr = pathStr.replace(ws_regexp, workspace.rootUri.getPath());
}
if (process.env.HOME !== undefined) {
replace(/\$\{env:HOME\}/g, process.env.HOME || '');
}
Expand Down
16 changes: 10 additions & 6 deletions packages/pyright-internal/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { getRootUri } from './common/uri/uriUtils';
import { LanguageServerBase, ServerSettings } from './languageServerBase';
import { CodeActionProvider } from './languageService/codeActionProvider';
import { PyrightFileSystem } from './pyrightFileSystem';
import { Workspace } from './workspaceFactory';
import { WellKnownWorkspaceKinds, Workspace } from './workspaceFactory';

const maxAnalysisTimeInForeground = { openFilesTimeInMs: 50, noOpenFilesTimeInMs: 200 };

Expand Down Expand Up @@ -101,20 +101,22 @@ export class PyrightServer extends LanguageServerBase {
};

try {
const workspaces = this.workspaceFactory.getNonDefaultWorkspaces(WellKnownWorkspaceKinds.Regular);

const pythonSection = await this.getConfiguration(workspace.rootUri, 'python');
if (pythonSection) {
const pythonPath = pythonSection.pythonPath;
if (pythonPath && isString(pythonPath) && !isPythonBinary(pythonPath)) {
serverSettings.pythonPath = workspace.rootUri.resolvePaths(
expandPathVariables(workspace.rootUri, pythonPath)
expandPathVariables(pythonPath, workspace.rootUri, workspaces)
);
}

const venvPath = pythonSection.venvPath;

if (venvPath && isString(venvPath)) {
serverSettings.venvPath = workspace.rootUri.resolvePaths(
expandPathVariables(workspace.rootUri, venvPath)
expandPathVariables(venvPath, workspace.rootUri, workspaces)
);
}
}
Expand All @@ -126,15 +128,15 @@ export class PyrightServer extends LanguageServerBase {
const typeshedPath = typeshedPaths[0];
if (typeshedPath && isString(typeshedPath)) {
serverSettings.typeshedPath = workspace.rootUri.resolvePaths(
expandPathVariables(workspace.rootUri, typeshedPath)
expandPathVariables(typeshedPath, workspace.rootUri, workspaces)
);
}
}

const stubPath = pythonAnalysisSection.stubPath;
if (stubPath && isString(stubPath)) {
serverSettings.stubPath = workspace.rootUri.resolvePaths(
expandPathVariables(workspace.rootUri, stubPath)
expandPathVariables(stubPath, workspace.rootUri, workspaces)
);
}

Expand Down Expand Up @@ -166,7 +168,9 @@ export class PyrightServer extends LanguageServerBase {
if (extraPaths && Array.isArray(extraPaths) && extraPaths.length > 0) {
serverSettings.extraPaths = extraPaths
.filter((p) => p && isString(p))
.map((p) => workspace.rootUri.resolvePaths(expandPathVariables(workspace.rootUri, p)));
.map((p) =>
workspace.rootUri.resolvePaths(expandPathVariables(p, workspace.rootUri, workspaces))
);
}

serverSettings.includeFileSpecs = this._getStringValues(pythonAnalysisSection.include);
Expand Down
120 changes: 120 additions & 0 deletions packages/pyright-internal/src/tests/envVarUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* envVarUtils.test.ts
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*
* Unit tests for functions in envVarUtils.
*/

import * as os from 'os';

import assert from 'assert';

import { expandPathVariables } from '../common/envVarUtils';
import { Uri } from '../common/uri/uri';
import { Workspace } from '../workspaceFactory';

jest.mock('os', () => ({ __esModule: true, ...jest.requireActual('os') }));

test('expands ${workspaceFolder}', () => {
const workspaceFolderUri = Uri.parse('/src', true);
const test_path = '${workspaceFolder}/foo';
const path = `${workspaceFolderUri.getPath()}/foo`;
assert.equal(expandPathVariables(test_path, workspaceFolderUri, []), path);
});

test('expands ${workspaceFolder:sibling}', () => {
const workspaceFolderUri = Uri.parse('/src', true);
const workspace = { workspaceName: 'sibling', rootUri: workspaceFolderUri } as Workspace;
const test_path = `\${workspaceFolder:${workspace.workspaceName}}/foo`;
const path = `${workspaceFolderUri.getPath()}/foo`;
assert.equal(expandPathVariables(test_path, workspaceFolderUri, [workspace]), path);
});

describe('expandPathVariables', () => {
const OLD_ENV = process.env;

beforeEach(() => {
jest.resetModules();
process.env = { ...OLD_ENV };
});

afterAll(() => {
process.env = OLD_ENV;
});

test('expands ${env:HOME}', () => {
process.env.HOME = 'file:///home/foo';
const test_path = '${env:HOME}/bar';
const path = `${process.env.HOME}/bar`;
assert.equal(expandPathVariables(test_path, Uri.empty(), []), path);
});

test('expands ${env:USERNAME}', () => {
process.env.USERNAME = 'foo';
const test_path = 'file:///home/${env:USERNAME}/bar';
const path = `file:///home/${process.env.USERNAME}/bar`;
assert.equal(expandPathVariables(test_path, Uri.empty(), []), path);
});

test('expands ${env:VIRTUAL_ENV}', () => {
process.env.VIRTUAL_ENV = 'file:///home/foo/.venv/path';
const test_path = '${env:VIRTUAL_ENV}/bar';
const path = `${process.env.VIRTUAL_ENV}/bar`;
assert.equal(expandPathVariables(test_path, Uri.empty(), []), path);
});

test('expands ~ with os.homedir()', () => {
jest.spyOn(os, 'homedir').mockReturnValue('file:///home/foo');
process.env.HOME = '';
process.env.USERPROFILE = '';
const test_path = '~/bar';
const path = `${os.homedir()}/bar`;
assert.equal(expandPathVariables(test_path, Uri.empty(), []), path);
});

test('expands ~ with env:HOME', () => {
jest.spyOn(os, 'homedir').mockReturnValue('');
process.env.HOME = 'file:///home/foo';
process.env.USERPROFILE = '';
const test_path = '~/bar';
const path = `${process.env.HOME}/bar`;
assert.equal(expandPathVariables(test_path, Uri.empty(), []), path);
});

test('expands ~ with env:USERPROFILE', () => {
jest.spyOn(os, 'homedir').mockReturnValue('');
process.env.HOME = '';
process.env.USERPROFILE = 'file:///home/foo';
const test_path = '~/bar';
const path = `${process.env.USERPROFILE}/bar`;
assert.equal(expandPathVariables(test_path, Uri.empty(), []), path);
});

test('expands /~ with os.homedir()', () => {
jest.spyOn(os, 'homedir').mockReturnValue('file:///home/foo');
process.env.HOME = '';
process.env.USERPROFILE = '';
const test_path = '/~/bar';
const path = `${os.homedir()}/bar`;
assert.equal(expandPathVariables(test_path, Uri.empty(), []), path);
});

test('expands /~ with env:HOME', () => {
jest.spyOn(os, 'homedir').mockReturnValue('');
process.env.HOME = 'file:///home/foo';
process.env.USERPROFILE = '';
const test_path = '/~/bar';
const path = `${process.env.HOME}/bar`;
assert.equal(expandPathVariables(test_path, Uri.empty(), []), path);
});

test('expands /~ with env:USERPROFILE', () => {
jest.spyOn(os, 'homedir').mockReturnValue('');
process.env.HOME = '';
process.env.USERPROFILE = 'file:///home/foo';
const test_path = '/~/bar';
const path = `${process.env.USERPROFILE}/bar`;
assert.equal(expandPathVariables(test_path, Uri.empty(), []), path);
});
});
8 changes: 4 additions & 4 deletions packages/pyright-internal/src/tests/uri.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,14 +643,14 @@ function getHomeDirUri() {

test('resolvePath3 ~ escape', () => {
assert.equal(
resolvePaths(expandPathVariables(Uri.empty(), '~/path'), 'to', '..', 'from', 'file.ext/'),
resolvePaths(expandPathVariables('~/path', Uri.empty(), []), 'to', '..', 'from', 'file.ext/'),
`${getHomeDirUri().toString()}/path/from/file.ext`
);
});

test('resolvePath4 ~ escape in middle', () => {
assert.equal(
resolvePaths('/path', expandPathVariables(Uri.empty(), '~/file.ext/')),
resolvePaths('/path', expandPathVariables('~/file.ext/', Uri.empty(), [])),
`${getHomeDirUri().toString()}/file.ext`
);
});
Expand All @@ -661,12 +661,12 @@ function combinePaths(uri: string, ...paths: string[]) {

test('invalid ~ without root', () => {
const path = combinePaths('Library', 'Mobile Documents', 'com~apple~CloudDocs', 'Development', 'mysuperproject');
assert.equal(resolvePaths(expandPathVariables(Uri.parse('foo:///src', true), path)), path);
assert.equal(resolvePaths(expandPathVariables(path, Uri.parse('foo:///src', true), [])), path);
});

test('invalid ~ with root', () => {
const path = combinePaths('/', 'Library', 'com~apple~CloudDocs', 'Development', 'mysuperproject');
assert.equal(resolvePaths(expandPathVariables(Uri.parse('foo:///src', true), path)), path);
assert.equal(resolvePaths(expandPathVariables(path, Uri.parse('foo:///src', true), [])), path);
});

function containsPath(uri: string, child: string) {
Expand Down

0 comments on commit 12e9dd9

Please sign in to comment.