Skip to content

Commit 2d36152

Browse files
skyjurDonJayamanne
authored andcommitted
#764 feature request: auto import (DonJayamanne#1032)
* Auto-import command for a symbol under cursor. Using symbol provider to locate where the symbol can be imported from. To improve: Position of imported line Importing modules Relative imports * Added tests for autoimport command * Add sinon to dev-package dependencies sinon is very popular mock library (http://sinonjs.org/), also used in vscode tests.
1 parent 80b7094 commit 2d36152

File tree

8 files changed

+182
-0
lines changed

8 files changed

+182
-0
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,11 +1506,13 @@
15061506
"@types/socket.io": "^1.4.27",
15071507
"@types/socket.io-client": "^1.4.27",
15081508
"@types/uuid": "^3.3.27",
1509+
"@types/sinon": "^2.3.2",
15091510
"babel-core": "^6.14.0",
15101511
"babel-loader": "^6.2.5",
15111512
"babel-preset-es2015": "^6.14.0",
15121513
"ignore-loader": "^0.1.1",
15131514
"mocha": "^2.3.3",
1515+
"sinon": "^2.3.6",
15141516
"retyped-diff-match-patch-tsd-ambient": "^1.0.0-0",
15151517
"ts-loader": "^0.8.2",
15161518
"tslint": "^3.15.1",

src/client/autoImport.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict';
2+
3+
import * as vscode from 'vscode';
4+
import * as fs from 'fs';
5+
import * as path from 'path';
6+
import { AutoImportProvider } from './providers/autoImportProvider';
7+
8+
9+
export function activate(context: vscode.ExtensionContext, outChannel: vscode.OutputChannel) {
10+
let extension = new AutoImportProvider();
11+
context.subscriptions.push(vscode.commands.registerCommand('python.autoImportAtCursor', extension.autoImportAtCursor));
12+
}

src/client/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { PythonReferenceProvider } from './providers/referenceProvider';
88
import { PythonRenameProvider } from './providers/renameProvider';
99
import { PythonFormattingEditProvider } from './providers/formatProvider';
1010
import * as sortImports from './sortImports';
11+
import * as autoImport from './autoImport';
1112
import { LintProvider } from './providers/lintProvider';
1213
import { PythonSymbolProvider } from './providers/symbolProvider';
1314
import { PythonSignatureProvider } from './providers/signatureProvider';
@@ -58,6 +59,7 @@ export function activate(context: vscode.ExtensionContext) {
5859
}
5960

6061
sortImports.activate(context, formatOutChannel);
62+
autoImport.activate(context, formatOutChannel);
6163
context.subscriptions.push(activateSetInterpreterProvider());
6264
context.subscriptions.push(...activateExecInTerminalProvider());
6365
context.subscriptions.push(activateUpdateSparkLibraryProvider());
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import * as vscode from 'vscode';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
5+
export class AutoImportProvider {
6+
7+
constructor() {
8+
}
9+
10+
autoImportAtCursor() {
11+
let ed = vscode.window.activeTextEditor;
12+
let range : vscode.Range = ed.selection;
13+
14+
if(range.start.line !== range.end.line) {
15+
return;
16+
}
17+
18+
if(range.start.character === range.end.character) {
19+
range = ed.document.getWordRangeAtPosition(range.end);
20+
}
21+
const symbol = vscode.window.activeTextEditor.document.getText(range);
22+
23+
if(!symbol) {
24+
return;
25+
}
26+
27+
return this.autoImport(symbol);
28+
}
29+
30+
async autoImport(symbol: string) {
31+
let editor = vscode.window.activeTextEditor;
32+
let result = await this.lookupSymbol(symbol);
33+
34+
result = result.filter((s: vscode.SymbolInformation) =>
35+
s.name === symbol && // Only exact and case sensitive matches should be considered for import
36+
s.kind !== vscode.SymbolKind.Namespace // only declarations should be considered for import
37+
);
38+
39+
if(result.length === 0) {
40+
vscode.window.showInformationMessage('No matching symbols found');
41+
return;
42+
} else {
43+
var import_choices: string[] = result.map(
44+
s => `from ${pathAsPyModule(s.location)} import ${s.name}`
45+
);
46+
47+
let s = await this.showChoices(import_choices);
48+
if(s) {
49+
return addImport(editor, s);
50+
}
51+
}
52+
}
53+
54+
lookupSymbol(symbol: string) {
55+
return <Promise<vscode.SymbolInformation[]>>
56+
vscode.commands.executeCommand('vscode.executeWorkspaceSymbolProvider', symbol);
57+
}
58+
59+
showChoices(import_choices: string[]) {
60+
return vscode.window.showQuickPick(import_choices);
61+
}
62+
}
63+
64+
export function pathAsPyModule(l: vscode.Location): string {
65+
var pymodule = path.basename(l.uri.fsPath).replace(/\.py$/, '');
66+
var location = path.dirname(l.uri.fsPath);
67+
while(fs.existsSync(path.join(location, '__init__.py'))) {
68+
pymodule = path.basename(location) + '.' + pymodule;
69+
location = path.dirname(location);
70+
}
71+
return pymodule;
72+
}
73+
74+
export function addImport(ed: vscode.TextEditor, import_string: string) {
75+
return ed.edit((b: vscode.TextEditorEdit) => b.insert(
76+
getPositionForNewImport(import_string),
77+
import_string + '\n'
78+
));
79+
}
80+
81+
82+
export function getPositionForNewImport(import_string: string): vscode.Position {
83+
// TODO: figure out better position:
84+
return new vscode.Position(0, 0);
85+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import * as assert from 'assert';
2+
import * as sinon from 'sinon';
3+
import * as vscode from 'vscode';
4+
import * as path from 'path';
5+
import { AutoImportProvider } from './../client/providers/autoImportProvider';
6+
7+
let testPath = path.join(__dirname, '../../src/test');
8+
const fileOne = path.join(testPath, 'pythonFiles/autoimport/one.py');
9+
const fileThree = path.join(testPath, 'pythonFiles/autoimport/two/three.py');
10+
11+
let fileOneLoc = new vscode.Location(vscode.Uri.file(fileOne), new vscode.Position(0, 0));
12+
let fileThreeLoc = new vscode.Location(vscode.Uri.file(fileThree), new vscode.Position(0, 0));
13+
14+
15+
suite('Autoimport', () => {
16+
let autoimp = new AutoImportProvider();
17+
let lookupSymbol = sinon.stub(autoimp, 'lookupSymbol');
18+
let showChoices = sinon.stub(autoimp, 'showChoices');
19+
20+
suiteSetup(() => {
21+
});
22+
23+
setup(() => {
24+
lookupSymbol.reset();
25+
showChoices.reset();
26+
return vscode.workspace.openTextDocument(fileOne).then(vscode.window.showTextDocument);
27+
});
28+
29+
test('choices are filtered and displayed"', done => {
30+
lookupSymbol.resolves([
31+
new vscode.SymbolInformation(
32+
'TestClass', vscode.SymbolKind.Class, '', fileOneLoc
33+
),
34+
new vscode.SymbolInformation(
35+
'TestClassTwo', vscode.SymbolKind.Class, '', fileOneLoc
36+
),
37+
new vscode.SymbolInformation(
38+
'TestClass', vscode.SymbolKind.Namespace, '', fileOneLoc
39+
),
40+
]);
41+
42+
showChoices.resolves(undefined);
43+
44+
autoimp.autoImport('TestClass').then(function() {
45+
assert(lookupSymbol.calledWith('TestClass'));
46+
assert(showChoices.calledWith(['from one import TestClass']));
47+
done();
48+
});
49+
});
50+
51+
test('module under a package"', done => {
52+
lookupSymbol.resolves([
53+
new vscode.SymbolInformation(
54+
'TestClass', vscode.SymbolKind.Class, '', fileThreeLoc
55+
),
56+
]);
57+
58+
showChoices.resolves(undefined);
59+
60+
autoimp.autoImport('TestClass').then(function() {
61+
assert(showChoices.calledWith(['from two.three import TestClass']));
62+
done();
63+
});
64+
});
65+
66+
test('selection is added at line one', done => {
67+
lookupSymbol.resolves([
68+
new vscode.SymbolInformation(
69+
'TestClass', vscode.SymbolKind.Class, '', fileOneLoc
70+
),
71+
]);
72+
73+
showChoices.resolves('from one import TestClass');
74+
75+
autoimp.autoImport('TestClass').then(function() {
76+
let line0 = vscode.window.activeTextEditor.document.lineAt(0);
77+
assert.equal(line0.text, 'from one import TestClass');
78+
done();
79+
});
80+
});
81+
});

src/test/pythonFiles/autoimport/one.py

Whitespace-only changes.

src/test/pythonFiles/autoimport/two/__init__.py

Whitespace-only changes.

src/test/pythonFiles/autoimport/two/three.py

Whitespace-only changes.

0 commit comments

Comments
 (0)