Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d1da48b
update to use latest api
DonJayamanne Oct 3, 2017
15d8d35
Merge branch 'master' into 1228Multiroot
DonJayamanne Oct 4, 2017
b305a72
config changes for multiroot workspace
DonJayamanne Oct 5, 2017
674da9e
linting support with multi roots
DonJayamanne Oct 5, 2017
a549fc6
multi root support for formatters
DonJayamanne Oct 6, 2017
657e20f
determine workspace root path
DonJayamanne Oct 6, 2017
d492b5a
revert change
DonJayamanne Oct 6, 2017
c0c70fb
support multiple configs per workspace folder
DonJayamanne Oct 6, 2017
86e36c1
modify formatters to use resource specific settings
DonJayamanne Oct 6, 2017
c5cd8a9
modified installer to pass resource for workspace resolution
DonJayamanne Oct 9, 2017
663b345
null test in installer
DonJayamanne Oct 9, 2017
bcca381
canges to config settings to support multiroot workspace
DonJayamanne Oct 9, 2017
e3279cb
changes to code refactoring to support workspace symbols
DonJayamanne Oct 9, 2017
965eae9
oops
DonJayamanne Oct 9, 2017
234ff78
modified to settings are resolved using document uri
DonJayamanne Oct 9, 2017
e15e627
merged 1228Multiroot
DonJayamanne Oct 9, 2017
da4e2ab
unit tests for multi root support
DonJayamanne Oct 11, 2017
b575dcc
fix unittests for multiroot
DonJayamanne Oct 11, 2017
7457c89
exclude files
DonJayamanne Oct 11, 2017
54741c1
add new line
DonJayamanne Oct 11, 2017
ef306b0
config changes for multiroot workspace
DonJayamanne Oct 5, 2017
af113a4
installer, config changes with unit tests
DonJayamanne Oct 11, 2017
23ab751
new lines and enabled multi root linter tests
DonJayamanne Oct 11, 2017
b4295ef
fix sys variables
DonJayamanne Oct 12, 2017
360e919
added unit test to resolve ${workspaceRoot} in settings.json
DonJayamanne Oct 12, 2017
b9a0360
fixed code review comments
DonJayamanne Oct 12, 2017
023b66b
fixed code review comments
DonJayamanne Oct 12, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ node_modules
*.pyc
.vscode/.ropeproject/**
src/test/.vscode/**
**/.vscode/tags
**/testFiles/**/.cache/**
*.noseids
.vscode-test
__pycache__
npm-debug.log
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@
],
"preLaunchTask": "npm"
},
{
"name": "Launch Multiroot Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"${workspaceRoot}/src/test/multiRootWkspc/multi.code-workspace",
"--extensionDevelopmentPath=${workspaceRoot}",
"--extensionTestsPath=${workspaceRoot}/out/test"
],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/out/**/*.js"
],
"preLaunchTask": "npm"
},
{
"name": "Python",
"type": "python",
Expand Down
5 changes: 3 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false, // set this to true to hide the "out" folder with the compiled JS files
"out": true, // set this to true to hide the "out" folder with the compiled JS files
"**/*.pyc": true,
"**/__pycache__": true,
"node_modules": true
"node_modules": true,
".vscode-test":true
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1593,7 +1593,7 @@
"vscode:prepublish": "tsc -p ./ && webpack",
"compile": "webpack && tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install",
"test": "node ./node_modules/vscode/bin/test"
"test": "node ./node_modules/vscode/bin/test && node ./out/test/multiRootTest.js"
},
"dependencies": {
"anser": "^1.1.0",
Expand Down
14 changes: 10 additions & 4 deletions src/client/common/configSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {

this.initializeSettings();
}
public static dispose() {
if (!IS_TEST_EXECUTION) {
throw new Error('Dispose can only be called from unit tests');
}
PythonSettings.pythonSettings.clear();
}
public static getInstance(resource?: Uri): PythonSettings {
const workspaceFolder = resource ? vscode.workspace.getWorkspaceFolder(resource) : undefined;
let workspaceFolderUri: Uri = workspaceFolder ? workspaceFolder.uri : undefined;
Expand All @@ -160,15 +166,15 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
return PythonSettings.pythonSettings.get(workspaceFolderKey);
}
private initializeSettings() {
const systemVariables: SystemVariables = new SystemVariables();
const workspaceRoot = (IS_TEST_EXECUTION || !this.workspaceRoot) ? __dirname : this.workspaceRoot.fsPath;
const workspaceRoot = this.workspaceRoot.fsPath;
const systemVariables: SystemVariables = new SystemVariables(this.workspaceRoot ? this.workspaceRoot.fsPath : undefined);
const pythonSettings = vscode.workspace.getConfiguration('python', this.workspaceRoot);
this.pythonPath = systemVariables.resolveAny(pythonSettings.get<string>('pythonPath'))!;
this.pythonPath = getAbsolutePath(this.pythonPath, IS_TEST_EXECUTION ? __dirname : workspaceRoot);
this.pythonPath = getAbsolutePath(this.pythonPath, workspaceRoot);
this.venvPath = systemVariables.resolveAny(pythonSettings.get<string>('venvPath'))!;
this.jediPath = systemVariables.resolveAny(pythonSettings.get<string>('jediPath'))!;
if (typeof this.jediPath === 'string' && this.jediPath.length > 0) {
this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), IS_TEST_EXECUTION ? __dirname : workspaceRoot);
this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot);
}
else {
this.jediPath = '';
Expand Down
4 changes: 2 additions & 2 deletions src/client/common/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function getTextEditsFromPatch(before: string, patch: string): TextEdit[]

return textEdits;
}
export function getWorkspaceEditsFromPatch(filePatches: string[]): WorkspaceEdit {
export function getWorkspaceEditsFromPatch(filePatches: string[], workspaceRoot?:string): WorkspaceEdit {
const workspaceEdit = new WorkspaceEdit();
filePatches.forEach(patch => {
const indexOfAtAt = patch.indexOf('@@');
Expand All @@ -101,7 +101,7 @@ export function getWorkspaceEditsFromPatch(filePatches: string[]): WorkspaceEdit
}

let fileName = fileNameLines[0].substring(fileNameLines[0].indexOf(' a') + 3).trim();
fileName = path.isAbsolute(fileName) ? fileName : path.resolve(vscode.workspace.rootPath, fileName);
fileName = workspaceRoot && !path.isAbsolute(fileName) ? path.resolve(workspaceRoot, fileName) : fileName;
if (!fs.existsSync(fileName)) {
return;
}
Expand Down
172 changes: 108 additions & 64 deletions src/client/common/installer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { error } from './logger';
import * as vscode from 'vscode';
import * as settings from './configSettings';
import * as os from 'os';
import { commands, ConfigurationTarget, Disposable, OutputChannel, Terminal, Uri, window, workspace } from 'vscode';
import { isNotInstalledError } from './helpers';
import { execPythonFile, getFullyQualifiedPythonInterpreterPath } from './utils';
import { Documentation } from './constants';

export enum Product {
pytest,
Expand Down Expand Up @@ -111,6 +112,9 @@ SettingToDisableProduct.set(Product.pydocstyle, 'linting.pydocstyleEnabled');
SettingToDisableProduct.set(Product.pylint, 'linting.pylintEnabled');
SettingToDisableProduct.set(Product.pytest, 'unitTest.pyTestEnabled');

const ProductInstallationPrompt = new Map<Product, string>();
ProductInstallationPrompt.set(Product.ctags, 'Install CTags to enable Python workspace symbols');

enum ProductType {
Linter,
Formatter,
Expand Down Expand Up @@ -142,6 +146,11 @@ ProductTypes.set(Product.autopep8, ProductType.Formatter);
ProductTypes.set(Product.yapf, ProductType.Formatter);
ProductTypes.set(Product.rope, ProductType.RefactoringLibrary);

export enum InstallerResponse {
Installed,
Disabled,
Ignore
}
export class Installer implements vscode.Disposable {
private static terminal: vscode.Terminal | undefined | null;
private disposables: vscode.Disposable[] = [];
Expand All @@ -155,12 +164,14 @@ export class Installer implements vscode.Disposable {
public dispose() {
this.disposables.forEach(d => d.dispose());
}
public shouldDisplayPrompt(product: Product) {
private shouldDisplayPrompt(product: Product) {
const productName = ProductNames.get(product)!;
return settings.PythonSettings.getInstance().disablePromptForFeatures.indexOf(productName) === -1;
const pythonConfig = workspace.getConfiguration('python');
const disablePromptForFeatures = pythonConfig.get('disablePromptForFeatures', [] as string[]);
return disablePromptForFeatures.indexOf(productName) === -1;
}

async promptToInstall(product: Product) {
public async promptToInstall(product: Product, resource?: Uri): Promise<InstallerResponse> {
const productType = ProductTypes.get(product)!;
const productTypeName = ProductTypeNames.get(productType);
const productName = ProductNames.get(product)!;
Expand All @@ -173,10 +184,10 @@ export class Installer implements vscode.Disposable {
else {
console.warn(message);
}
return;
return InstallerResponse.Ignore;
}

const installOption = 'Install ' + productName;
const installOption = ProductInstallationPrompt.has(product) ? ProductInstallationPrompt.get(product) : 'Install ' + productName;
const disableOption = 'Disable ' + productTypeName;
const dontShowAgain = `Don't show this prompt again`;
const alternateFormatter = product === Product.autopep8 ? 'yapf' : 'autopep8';
Expand All @@ -189,46 +200,53 @@ export class Installer implements vscode.Disposable {
if (SettingToDisableProduct.has(product)) {
options.push(...[disableOption, dontShowAgain]);
}
return vscode.window.showErrorMessage(`${productTypeName} ${productName} is not installed`, ...options).then(item => {
switch (item) {
case installOption: {
return this.install(product);
}
case disableOption: {
if (Linters.indexOf(product) >= 0) {
return disableLinter(product);
}
else {
const pythonConfig = vscode.workspace.getConfiguration('python');
const settingToDisable = SettingToDisableProduct.get(product)!;
return pythonConfig.update(settingToDisable, false);
}
}
case useOtherFormatter: {
const pythonConfig = vscode.workspace.getConfiguration('python');
return pythonConfig.update('formatting.provider', alternateFormatter);
}
case dontShowAgain: {
const pythonConfig = vscode.workspace.getConfiguration('python');
const features = pythonConfig.get('disablePromptForFeatures', [] as string[]);
features.push(productName);
return pythonConfig.update('disablePromptForFeatures', features, true);
const item = await window.showErrorMessage(`${productTypeName} ${productName} is not installed`, ...options);
switch (item) {
case installOption: {
return this.install(product, resource);
}
case disableOption: {
if (Linters.indexOf(product) >= 0) {
return this.disableLinter(product, resource).then(() => InstallerResponse.Disabled);
}
case 'Help': {
return Promise.resolve();
else {
const settingToDisable = SettingToDisableProduct.get(product)!;
return this.updateSetting(settingToDisable, false, resource).then(() => InstallerResponse.Disabled);
}
}
});
case useOtherFormatter: {
return this.updateSetting('formatting.provider', alternateFormatter, resource)
.then(() => InstallerResponse.Installed);
}
case dontShowAgain: {
const pythonConfig = workspace.getConfiguration('python');
const features = pythonConfig.get('disablePromptForFeatures', [] as string[]);
features.push(productName);
return pythonConfig.update('disablePromptForFeatures', features, true).then(() => InstallerResponse.Ignore);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does TypeScript make sure that all possible case statements are covered?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it doesn't, will add one

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

default: {
throw new Error('Invalid selection');
}
}
}

install(product: Product): Promise<any> {
public async install(product: Product, resource?: Uri): Promise<InstallerResponse> {
if (!this.outputChannel && !Installer.terminal) {
Installer.terminal = vscode.window.createTerminal('Python Installer');
Installer.terminal = window.createTerminal('Python Installer');
}

if (product === Product.ctags && os.platform() === 'win32') {
vscode.commands.executeCommand('python.displayHelp', Documentation.Workspace.InstallOnWindows);
return Promise.resolve();
if (product === Product.ctags && settings.IS_WINDOWS) {
if (this.outputChannel) {
this.outputChannel.appendLine('Install Universal Ctags Win32 to enable support for Workspace Symbols');
this.outputChannel.appendLine('Download the CTags binary from the Universal CTags site.');
this.outputChannel.appendLine('Option 1: Extract ctags.exe from the downloaded zip to any folder within your PATH so that Visual Studio Code can run it.');
this.outputChannel.appendLine('Option 2: Extract to any folder and add the path to this folder to the command setting.');
this.outputChannel.appendLine('Option 3: Extract to any folder and define that path in the python.workspaceSymbols.ctagsPath setting of your user settings file (settings.json).');
this.outputChannel.show();
}
else {
window.showInformationMessage('Install Universal Ctags and set it in your path or define the path in your python.workspaceSymbols.ctagsPath settings');
}
return InstallerResponse.Ignore;
}

let installArgs = ProductInstallScripts.get(product)!;
Expand All @@ -241,19 +259,19 @@ export class Installer implements vscode.Disposable {
installArgs.splice(2, 0, '--proxy');
}
}
let installationPromise: Promise<any>;
if (this.outputChannel && installArgs[0] === '-m') {
// Errors are just displayed to the user
this.outputChannel.show();
return execPythonFile(settings.PythonSettings.getInstance().pythonPath, installArgs, vscode.workspace.rootPath!, true, (data) => {
this.outputChannel!.append(data);
});
installationPromise = execPythonFile(settings.PythonSettings.getInstance(resource).pythonPath,
installArgs, getCwdForInstallScript(resource), true, (data) => { this.outputChannel!.append(data); });
}
else {
// When using terminal get the fully qualitified path
// Cuz people may launch vs code from terminal when they have activated the appropriate virtual env
// Problem is terminal doesn't use the currently activated virtual env
// Must have something to do with the process being launched in the terminal
return getFullyQualifiedPythonInterpreterPath()
installationPromise = getFullyQualifiedPythonInterpreterPath()
.then(pythonPath => {
let installScript = installArgs.join(' ');

Expand All @@ -269,42 +287,68 @@ export class Installer implements vscode.Disposable {
Installer.terminal!.show(false);
});
}

return installationPromise
.then(() => this.isInstalled(product))
.then(isInstalled => isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore);
}

isInstalled(product: Product): Promise<boolean | undefined> {
return isProductInstalled(product);
public isInstalled(product: Product, resource?: Uri): Promise<boolean | undefined> {
return isProductInstalled(product, resource);
}

uninstall(product: Product): Promise<any> {
return uninstallproduct(product);
public uninstall(product: Product, resource?: Uri): Promise<any> {
return uninstallproduct(product, resource);
}
public disableLinter(product: Product, resource: Uri) {
if (resource && !workspace.getWorkspaceFolder(resource)) {
const settingToDisable = SettingToDisableProduct.get(product)!;
const pythonConfig = workspace.getConfiguration('python', resource);
return pythonConfig.update(settingToDisable, false, ConfigurationTarget.Workspace);
}
else {
const pythonConfig = workspace.getConfiguration('python');
return pythonConfig.update('linting.enabledWithoutWorkspace', false, true);
}
}
private updateSetting(setting: string, value: any, resource?: Uri) {
if (resource && !workspace.getWorkspaceFolder(resource)) {
const pythonConfig = workspace.getConfiguration('python', resource);
return pythonConfig.update(setting, value, ConfigurationTarget.Workspace);
}
else {
const pythonConfig = workspace.getConfiguration('python');
return pythonConfig.update(setting, value, true);
}
}
}

export function disableLinter(product: Product, global?: boolean) {
const pythonConfig = vscode.workspace.getConfiguration('python');
const settingToDisable = SettingToDisableProduct.get(product)!;
if (vscode.workspace.rootPath) {
return pythonConfig.update(settingToDisable, false, global);
function getCwdForInstallScript(resource?: Uri) {
const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined;
if (workspaceFolder) {
return workspaceFolder.uri.fsPath;
}
else {
return pythonConfig.update('linting.enabledWithoutWorkspace', false, true);
if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) {
return workspace.workspaceFolders[0].uri.fsPath;
}
return __dirname;
}

async function isProductInstalled(product: Product): Promise<boolean | undefined> {
async function isProductInstalled(product: Product, resource?: Uri): Promise<boolean | undefined> {
if (!ProductExecutableAndArgs.has(product)) {
return;
}
const prodExec = ProductExecutableAndArgs.get(product)!;
return execPythonFile(prodExec.executable, prodExec.args.concat(['--version']), vscode.workspace.rootPath!, false)
.then(() => {
return true;
}).catch(reason => {
return !isNotInstalledError(reason);
});
const cwd = getCwdForInstallScript(resource);
return execPythonFile(prodExec.executable, prodExec.args.concat(['--version']), cwd, false)
.then(() => true)
.catch(reason => !isNotInstalledError(reason));
}

function uninstallproduct(product: Product): Promise<any> {
function uninstallproduct(product: Product, resource?: Uri): Promise<any> {
if (!ProductUninstallScripts.has(product)) {
return Promise.resolve();
}
const uninstallArgs = ProductUninstallScripts.get(product)!;
return execPythonFile('python', uninstallArgs, vscode.workspace.rootPath!, false);
}
return execPythonFile('python', uninstallArgs, getCwdForInstallScript(resource), false);
}
4 changes: 3 additions & 1 deletion src/client/common/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ class Logger {
Logger.writeLine(category, message);
}
static writeLine(category: string = "log", line: any) {
console[category](line);
if (process.env['PYTHON_DONJAYAMANNE_TEST'] !== '1') {
console[category](line);
}
if (outChannel) {
outChannel.appendLine(line);
}
Expand Down
5 changes: 2 additions & 3 deletions src/client/common/systemVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,10 @@ export abstract class AbstractSystemVariables implements ISystemVariables {
export class SystemVariables extends AbstractSystemVariables {
private _workspaceRoot: string;
private _workspaceRootFolderName: string;
private _execPath: string;

constructor() {
constructor(workspaceRoot?: string) {
super();
this._workspaceRoot = typeof vscode.workspace.rootPath === 'string' ? vscode.workspace.rootPath : __dirname;
this._workspaceRoot = typeof workspaceRoot === 'string' ? workspaceRoot : __dirname;
this._workspaceRootFolderName = Path.basename(this._workspaceRoot);
Object.keys(process.env).forEach(key => {
this[`env:${key}`] = this[`env.${key}`] = process.env[key];
Expand Down
Loading