Skip to content

Commit 498cd18

Browse files
committed
fix #255
1 parent 2718bea commit 498cd18

16 files changed

Lines changed: 187 additions & 70 deletions

File tree

src/client/common/installer.ts

Lines changed: 116 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import * as vscode from 'vscode';
2+
import * as settings from './configSettings';
13

2-
export enum product {
4+
export enum Product {
35
pytest,
46
nosetest,
57
pylint,
@@ -8,17 +10,122 @@ export enum product {
810
prospector,
911
pydocstyle,
1012
yapf,
11-
autopep8
13+
autopep8,
14+
mypy,
15+
unittest
1216
}
1317

14-
interface installIScript {
15-
windows: string;
16-
mac: string;
17-
linux: string;
18-
}
18+
const ProductInstallScripts = new Map<Product, string>();
19+
ProductInstallScripts.set(Product.autopep8, 'pip install autopep8');
20+
ProductInstallScripts.set(Product.flake8, 'pip install flake8');
21+
ProductInstallScripts.set(Product.mypy, 'pip install mypy-lang');
22+
ProductInstallScripts.set(Product.nosetest, 'pip install nose');
23+
ProductInstallScripts.set(Product.pep8, 'pip install pep8');
24+
ProductInstallScripts.set(Product.prospector, 'pip install prospector');
25+
ProductInstallScripts.set(Product.pydocstyle, 'pip install pydocstyle');
26+
ProductInstallScripts.set(Product.pylint, 'pip install pylint');
27+
ProductInstallScripts.set(Product.pytest, 'pip install -U pytest');
28+
ProductInstallScripts.set(Product.yapf, 'pip install yapf');
29+
30+
31+
const Linters: Product[] = [Product.flake8, Product.pep8, Product.prospector, Product.pylint, Product.mypy, Product.pydocstyle];
32+
const Formatters: Product[] = [Product.autopep8, Product.yapf];
33+
const TestFrameworks: Product[] = [Product.pytest, Product.nosetest];
34+
35+
const ProductNames = new Map<Product, string>();
36+
ProductNames.set(Product.autopep8, 'autopep8');
37+
ProductNames.set(Product.flake8, 'flake8');
38+
ProductNames.set(Product.mypy, 'mypy');
39+
ProductNames.set(Product.nosetest, 'nosetest');
40+
ProductNames.set(Product.pep8, 'pep8');
41+
ProductNames.set(Product.prospector, 'prospector');
42+
ProductNames.set(Product.pydocstyle, 'pydocstyle');
43+
ProductNames.set(Product.pylint, 'pylint');
44+
ProductNames.set(Product.pytest, 'py.test');
45+
ProductNames.set(Product.yapf, 'yapf');
46+
47+
const SettingToDisableProduct = new Map<Product, string>();
48+
SettingToDisableProduct.set(Product.autopep8, '');
49+
SettingToDisableProduct.set(Product.flake8, 'linting.flake8Enabled');
50+
SettingToDisableProduct.set(Product.mypy, 'linting.mypyEnabled');
51+
SettingToDisableProduct.set(Product.nosetest, 'unitTest.nosetestsEnabled');
52+
SettingToDisableProduct.set(Product.pep8, 'linting.pep8Enabled');
53+
SettingToDisableProduct.set(Product.prospector, 'linting.prospectorEnabled');
54+
SettingToDisableProduct.set(Product.pydocstyle, 'linting.pydocstyleEnabled');
55+
SettingToDisableProduct.set(Product.pylint, 'linting.pylintEnabled');
56+
SettingToDisableProduct.set(Product.pytest, 'unitTest.pyTestEnabled');
57+
SettingToDisableProduct.set(Product.yapf, 'yapf');
58+
59+
export class Installer {
60+
private static terminal: vscode.Terminal;
61+
private disposables: vscode.Disposable[] = [];
62+
constructor() {
63+
this.disposables.push(vscode.window.onDidCloseTerminal(term => {
64+
if (term === Installer.terminal) {
65+
Installer.terminal = null;
66+
}
67+
}));
68+
}
69+
public dispose() {
70+
this.disposables.forEach(d => d.dispose());
71+
}
72+
73+
promptToInstall(product: Product) {
74+
let productType = Linters.indexOf(product) >= 0 ? 'Linter' : (Formatters.indexOf(product) >= 0 ? 'Formatter' : 'Test Framework');
75+
const productName = ProductNames.get(product);
76+
77+
const installOption = 'Install ' + productName;
78+
const disableOption = 'Disable this ' + productType;
79+
const alternateFormatter = product === Product.autopep8 ? 'yapf' : 'autopep8';
80+
const useOtherFormatter = `Use '${alternateFormatter}' formatter`;
81+
const options = [];
82+
if (Formatters.indexOf(product) === -1) {
83+
options.push(...[installOption, disableOption, 'Help']);
84+
}
85+
else {
86+
options.push(...[installOption, useOtherFormatter, 'Help']);
87+
}
88+
vscode.window.showErrorMessage(`${productType} ${productName} is not installed`, ...options).then(item => {
89+
switch (item) {
90+
case installOption: {
91+
this.installProduct(product);
92+
break;
93+
}
94+
case disableOption: {
95+
const pythonConfig = vscode.workspace.getConfiguration('python');
96+
const settingToDisable = SettingToDisableProduct.get(product);
97+
pythonConfig.update(settingToDisable, false);
98+
break;
99+
}
100+
case useOtherFormatter: {
101+
const pythonConfig = vscode.workspace.getConfiguration('python');
102+
pythonConfig.update('formatting.provider', alternateFormatter);
103+
break;
104+
}
105+
case 'Help': {
106+
break;
107+
}
108+
}
109+
});
110+
}
19111

20-
const productInstallScripts = new Map<product, installIScript>();
112+
installProduct(product: Product) {
113+
if (!Installer.terminal) {
114+
Installer.terminal = vscode.window.createTerminal('Python Installer');
115+
}
21116

22-
export function promptToInstall(prod:product){
117+
let installScript = ProductInstallScripts.get(product);
118+
if (installScript.startsWith('pip install')) {
119+
const pythonPath = settings.PythonSettings.getInstance().pythonPath;
120+
if (pythonPath.indexOf(' ') >= 0) {
121+
installScript = `"${pythonPath}" -m ${installScript}`;
122+
}
123+
else {
124+
installScript = `${pythonPath} -m ${installScript}`;
125+
}
126+
}
23127

128+
Installer.terminal.sendText(installScript);
129+
Installer.terminal.show(false);
130+
}
24131
}

src/client/formatters/autoPep8Formatter.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use strict';
22

33
import * as vscode from 'vscode';
4-
import {BaseFormatter} from './baseFormatter';
4+
import { BaseFormatter } from './baseFormatter';
55
import * as settings from '../common/configSettings';
6+
import { Product } from '../common/installer';
67

78
export class AutoPep8Formatter extends BaseFormatter {
89
constructor(protected outputChannel: vscode.OutputChannel, protected pythonSettings: settings.IPythonSettings, protected workspaceRootPath: string) {
9-
super('autopep8', outputChannel, pythonSettings, workspaceRootPath);
10+
super('autopep8', Product.autopep8, outputChannel, pythonSettings, workspaceRootPath);
1011
}
1112

1213
public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable<vscode.TextEdit[]> {

src/client/formatters/baseFormatter.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
'use strict';
22

33
import * as vscode from 'vscode';
4-
import * as path from 'path';
54
import * as fs from 'fs';
6-
import {execPythonFile} from './../common/utils';
5+
import { execPythonFile } from './../common/utils';
76
import * as settings from './../common/configSettings';
8-
import {getTextEditsFromPatch, getTempFileWithDocumentContents} from './../common/editor';
9-
import {isNotInstalledError} from '../common/helpers';
7+
import { getTextEditsFromPatch, getTempFileWithDocumentContents } from './../common/editor';
8+
import { isNotInstalledError } from '../common/helpers';
9+
import { Installer, Product } from '../common/installer';
1010

1111
export abstract class BaseFormatter {
12-
constructor(public Id: string, protected outputChannel: vscode.OutputChannel, protected pythonSettings: settings.IPythonSettings, protected workspaceRootPath: string) {
12+
private installer: Installer;
13+
constructor(public Id: string, private product: Product, protected outputChannel: vscode.OutputChannel, protected pythonSettings: settings.IPythonSettings, protected workspaceRootPath: string) {
14+
this.installer = new Installer();
1315
}
1416

1517
public abstract formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable<vscode.TextEdit[]>;
@@ -59,6 +61,7 @@ export abstract class BaseFormatter {
5961
}
6062
else {
6163
customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`;
64+
this.installer.promptToInstall(this.product);
6265
}
6366
}
6467

src/client/formatters/yapfFormatter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import * as vscode from 'vscode';
44
import {BaseFormatter} from './baseFormatter';
55
import * as settings from './../common/configSettings';
6+
import { Product } from '../common/installer';
67

78
export class YapfFormatter extends BaseFormatter {
89
constructor(protected outputChannel: vscode.OutputChannel, protected pythonSettings: settings.IPythonSettings, protected workspaceRootPath: string) {
9-
super('yapf', outputChannel, pythonSettings, workspaceRootPath);
10+
super('yapf', Product.yapf, outputChannel, pythonSettings, workspaceRootPath);
1011
}
1112

1213
public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable<vscode.TextEdit[]> {

src/client/linters/baseLinter.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
'use strict';
2-
import * as child_process from 'child_process';
3-
import * as path from 'path';
4-
import { exec } from 'child_process';
5-
import {execPythonFile} from './../common/utils';
2+
import { execPythonFile } from './../common/utils';
63
import * as settings from './../common/configSettings';
7-
import {OutputChannel, window} from 'vscode';
8-
import {isNotInstalledError} from '../common/helpers';
4+
import { OutputChannel } from 'vscode';
5+
import { isNotInstalledError } from '../common/helpers';
6+
import { Installer, Product } from '../common/installer';
97

108
let NamedRegexp = null;
119
const REGEX = '(?<line>\\d+),(?<column>\\d+),(?<type>\\w+),(?<code>\\w\\d+):(?<message>.*)\\r?(\\n|$)';
@@ -51,17 +49,18 @@ export function matchNamedRegEx(data, regex): IRegexGroup {
5149

5250
export abstract class BaseLinter {
5351
public Id: string;
52+
private installer: Installer;
5453
protected pythonSettings: settings.IPythonSettings;
55-
constructor(id: string, protected outputChannel: OutputChannel, protected workspaceRootPath: string) {
54+
constructor(id: string, private product:Product, protected outputChannel: OutputChannel, protected workspaceRootPath: string) {
5655
this.Id = id;
56+
this.installer = new Installer();
5757
this.pythonSettings = settings.PythonSettings.getInstance();
5858
}
5959
public abstract isEnabled(): Boolean;
6060
public abstract runLinter(filePath: string, txtDocumentLines: string[]): Promise<ILintMessage[]>;
6161

6262
protected run(command: string, args: string[], filePath: string, txtDocumentLines: string[], cwd: string, regEx: string = REGEX): Promise<ILintMessage[]> {
6363
let outputChannel = this.outputChannel;
64-
let linterId = this.Id;
6564

6665
return new Promise<ILintMessage[]>((resolve, reject) => {
6766
execPythonFile(command, args, cwd, true).then(data => {
@@ -83,7 +82,6 @@ export abstract class BaseLinter {
8382
if (!isNaN(match.column)) {
8483
let sourceLine = txtDocumentLines[match.line - 1];
8584
let sourceStart = sourceLine.substring(match.column - 1);
86-
let endCol = txtDocumentLines[match.line - 1].length;
8785

8886
// try to get the first word from the startig position
8987
let possibleProblemWords = sourceStart.match(/\w+/g);
@@ -105,7 +103,6 @@ export abstract class BaseLinter {
105103
catch (ex) {
106104
// Hmm, need to handle this later
107105
// TODO:
108-
let y = '';
109106
}
110107
});
111108

@@ -132,7 +129,7 @@ export abstract class BaseLinter {
132129
'For further details, please see https://github.com/DonJayamanne/pythonVSCode/wiki/Troubleshooting-Linting#2-linting-with-xxx-failed-';
133130
}
134131
else {
135-
customError += `\nYou could either install the '${this.Id}' linter or turn it off in setings.json via "python.linting.${this.Id}Enabled = false".`;
132+
customError += `\nYou could either install the '${this.Id}' linter or turn it off in setings.json via "python.linting.${this.Id}Enabled = false".`;this.installer.promptToInstall(this.product);
136133
}
137134
}
138135

src/client/linters/flake8.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
'use strict';
22

3-
import * as path from 'path';
43
import * as baseLinter from './baseLinter';
5-
import {OutputChannel, workspace} from 'vscode';
4+
import {OutputChannel} from 'vscode';
5+
import { Product } from '../common/installer';
6+
67
export class Linter extends baseLinter.BaseLinter {
78
constructor(outputChannel: OutputChannel, workspaceRootPath: string) {
8-
super('flake8', outputChannel, workspaceRootPath);
9+
super('flake8', Product.flake8, outputChannel, workspaceRootPath);
910
}
1011

1112
public isEnabled(): Boolean {

src/client/linters/mypy.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
'use strict';
22

3-
import * as path from 'path';
43
import * as baseLinter from './baseLinter';
5-
import {OutputChannel, workspace} from 'vscode';
4+
import {OutputChannel} from 'vscode';
5+
import { Product } from '../common/installer';
66

77
const REGEX = '(?<file>.py):(?<line>\\d+): (?<type>\\w+): (?<message>.*)\\r?(\\n|$)';
88

99
export class Linter extends baseLinter.BaseLinter {
1010
constructor(outputChannel: OutputChannel, workspaceRootPath: string) {
11-
super('mypy', outputChannel, workspaceRootPath);
11+
super('mypy', Product.mypy, outputChannel, workspaceRootPath);
1212
}
1313
private parseMessagesSeverity(category: string): baseLinter.LintMessageSeverity {
1414
switch (category) {

src/client/linters/pep8Linter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
'use strict';
22

3-
import * as path from 'path';
43
import * as baseLinter from './baseLinter';
5-
import {OutputChannel, workspace} from 'vscode';
4+
import {OutputChannel} from 'vscode';
5+
import { Product } from '../common/installer';
66

77
export class Linter extends baseLinter.BaseLinter {
88
constructor(outputChannel: OutputChannel, workspaceRootPath: string) {
9-
super('pep8', outputChannel, workspaceRootPath);
9+
super('pep8', Product.pep8, outputChannel, workspaceRootPath);
1010
}
1111

1212
public isEnabled(): Boolean {

src/client/linters/prospector.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use strict';
22

3-
import * as path from 'path';
43
import * as baseLinter from './baseLinter';
5-
import {OutputChannel, workspace, window} from 'vscode';
4+
import {OutputChannel} from 'vscode';
65
import {execPythonFile} from './../common/utils';
6+
import { Product } from '../common/installer';
77

88
interface IProspectorResponse {
99
messages: IProspectorMessage[];
@@ -24,7 +24,7 @@ interface IProspectorLocation {
2424

2525
export class Linter extends baseLinter.BaseLinter {
2626
constructor(outputChannel: OutputChannel, workspaceRootPath: string) {
27-
super('prospector', outputChannel, workspaceRootPath);
27+
super('prospector', Product.prospector, outputChannel, workspaceRootPath);
2828
}
2929

3030
public isEnabled(): Boolean {
@@ -37,7 +37,6 @@ export class Linter extends baseLinter.BaseLinter {
3737

3838
let prospectorPath = this.pythonSettings.linting.prospectorPath;
3939
let outputChannel = this.outputChannel;
40-
let linterId = this.Id;
4140
let prospectorArgs = Array.isArray(this.pythonSettings.linting.prospectorArgs) ? this.pythonSettings.linting.prospectorArgs : [];
4241
return new Promise<baseLinter.ILintMessage[]>((resolve, reject) => {
4342
execPythonFile(prospectorPath, prospectorArgs.concat(['--absolute-paths', '--output-format=json', filePath]), this.workspaceRootPath, false).then(data => {
@@ -56,7 +55,6 @@ export class Linter extends baseLinter.BaseLinter {
5655
let lineNumber = msg.location.line === null || isNaN(msg.location.line) ? 1 : msg.location.line;
5756
let sourceLine = txtDocumentLines[lineNumber - 1];
5857
let sourceStart = sourceLine.substring(msg.location.character);
59-
let endCol = txtDocumentLines[lineNumber - 1].length;
6058

6159
// try to get the first word from the starting position
6260
let possibleProblemWords = sourceStart.match(/\w+/g);

src/client/linters/pydocstyle.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
import * as path from 'path';
44
import * as baseLinter from './baseLinter';
5-
import {ILintMessage} from './baseLinter';
6-
import {OutputChannel, window} from 'vscode';
7-
import { exec } from 'child_process';
8-
import {execPythonFile, IS_WINDOWS} from './../common/utils';
5+
import { ILintMessage } from './baseLinter';
6+
import { OutputChannel } from 'vscode';
7+
import { execPythonFile, IS_WINDOWS } from './../common/utils';
8+
import { Product } from '../common/installer';
99

1010
export class Linter extends baseLinter.BaseLinter {
1111
constructor(outputChannel: OutputChannel, workspaceRootPath: string) {
12-
super('pydocstyle', outputChannel, workspaceRootPath);
12+
super('pydocstyle', Product.pydocstyle, outputChannel, workspaceRootPath);
1313
}
1414

1515
public isEnabled(): Boolean {
@@ -36,10 +36,8 @@ export class Linter extends baseLinter.BaseLinter {
3636

3737
protected run(commandLine: string, args: string[], filePath: string, txtDocumentLines: string[]): Promise<ILintMessage[]> {
3838
let outputChannel = this.outputChannel;
39-
let linterId = this.Id;
4039

4140
return new Promise<ILintMessage[]>((resolve, reject) => {
42-
let fileDir = path.dirname(filePath);
4341
execPythonFile(commandLine, args, this.workspaceRootPath, true).then(data => {
4442
outputChannel.append('#'.repeat(10) + 'Linting Output - ' + this.Id + '#'.repeat(10) + '\n');
4543
outputChannel.append(data);
@@ -81,7 +79,6 @@ export class Linter extends baseLinter.BaseLinter {
8179
let sourceLine = txtDocumentLines[lineNumber - 1];
8280
let trmmedSourceLine = sourceLine.trim();
8381
let sourceStart = sourceLine.indexOf(trmmedSourceLine);
84-
let endCol = sourceStart + trmmedSourceLine.length;
8582

8683
diagnostics.push({
8784
code: code,
@@ -94,7 +91,6 @@ export class Linter extends baseLinter.BaseLinter {
9491
}
9592
catch (ex) {
9693
// Hmm, need to handle this later
97-
let y = '';
9894
}
9995
});
10096
resolve(diagnostics);

0 commit comments

Comments
 (0)