Skip to content
This repository has been archived by the owner on Jul 15, 2023. It is now read-only.

Add completion items for unimported packages #497

Merged
merged 6 commits into from
Oct 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"vscode": "^0.11.17"
},
"engines": {
"vscode": "0.10.x"
"vscode": "^1.5.0"
},
"activationEvents": [
"onLanguage:go",
Expand Down Expand Up @@ -331,6 +331,11 @@
"type": "object",
"default": {},
"description": "Environment variables that will passed to the process that runs the Go tests"
},
"go.autocomplteUnimportedPackages": {
"type": "boolean",
"default": false,
"description": "Autocomplete members from unimported packages."
}
}
}
Expand Down
57 changes: 30 additions & 27 deletions src/goImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,36 +61,39 @@ function askUserForImport(): Thenable<string> {
});
}

export function getTextEditForAddImport(arg: string): vscode.TextEdit {
Copy link
Contributor

Choose a reason for hiding this comment

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

arg should be importPackageName?

// Import name wasn't provided
if (arg === undefined) {
return null;
}

let {imports, pkg} = parseFilePrelude(vscode.window.activeTextEditor.document.getText());
let multis = imports.filter(x => x.kind === 'multi');
if (multis.length > 0) {
// There is a multiple import declaration, add to the last one
let closeParenLine = multis[multis.length - 1].end;
return vscode.TextEdit.insert(new vscode.Position(closeParenLine, 0), '\t"' + arg + '"\n');
} else if (imports.length > 0) {
// There are only single import declarations, add after the last one
let lastSingleImport = imports[imports.length - 1].end;
return vscode.TextEdit.insert(new vscode.Position(lastSingleImport + 1, 0), 'import "' + arg + '"\n');
} else if (pkg && pkg.start >= 0) {
// There are no import declarations, but there is a package declaration
return vscode.TextEdit.insert(new vscode.Position(pkg.start + 1, 0), '\nimport (\n\t"' + arg + '"\n)\n');
} else {
// There are no imports and no package declaration - give up
return null;
}
}

export function addImport(arg: string) {
let p = arg ? Promise.resolve(arg) : askUserForImport();
p.then(imp => {
// Import name wasn't provided
if (imp === undefined) {
return null;
}

let {imports, pkg} = parseFilePrelude(vscode.window.activeTextEditor.document.getText());
let multis = imports.filter(x => x.kind === 'multi');
if (multis.length > 0) {
// There is a multiple import declaration, add to the last one
let closeParenLine = multis[multis.length - 1].end;
return vscode.window.activeTextEditor.edit(editBuilder => {
editBuilder.insert(new vscode.Position(closeParenLine, 0), '\t"' + imp + '"\n');
});
} else if (imports.length > 0) {
// There are only single import declarations, add after the last one
let lastSingleImport = imports[imports.length - 1].end;
return vscode.window.activeTextEditor.edit(editBuilder => {
editBuilder.insert(new vscode.Position(lastSingleImport + 1, 0), 'import "' + imp + '"\n');
let edit = getTextEditForAddImport(imp);
if (edit) {
vscode.window.activeTextEditor.edit(editBuilder => {
Copy link
Contributor

Choose a reason for hiding this comment

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

May not matter right now - but probably good to return the promise here so it get's chained into the .then.

editBuilder.insert(edit.range.start, edit.newText);
});
} else if (pkg && pkg.start >= 0) {
// There are no import declarations, but there is a package declaration
return vscode.window.activeTextEditor.edit(editBuilder => {
editBuilder.insert(new vscode.Position(pkg.start + 1, 0), '\nimport (\n\t"' + imp + '"\n)\n');
});
} else {
// There are no imports and no package declaration - give up
return null;
}
});
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to remove final newlines.

235 changes: 170 additions & 65 deletions src/goSuggest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import vscode = require('vscode');
import cp = require('child_process');
import { dirname, basename } from 'path';
import { getBinPath } from './goPath';
import { parameters } from './util';
import { parameters, parseFilePrelude } from './util';
import { promptForMissingTool } from './goInstallTools';
import { listPackages, getTextEditForAddImport } from './goImport';

function vscodeKindFromGoCodeClass(kind: string): vscode.CompletionItemKind {
switch (kind) {
Expand All @@ -34,15 +35,27 @@ interface GoCodeSuggestion {
type: string;
}

interface PackageInfo {
name: string;
path: string;
}

export class GoCompletionItemProvider implements vscode.CompletionItemProvider {

private gocodeConfigurationComplete = false;
private pkgsList: PackageInfo[] = [];

public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable<vscode.CompletionItem[]> {
return this.provideCompletionItemsInternal(document, position, token, vscode.workspace.getConfiguration('go'));
}

public provideCompletionItemsInternal(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, config: vscode.WorkspaceConfiguration): Thenable<vscode.CompletionItem[]> {
return this.ensureGoCodeConfigured().then(() => {
return new Promise<vscode.CompletionItem[]>((resolve, reject) => {
let filename = document.fileName;
let lineText = document.lineAt(position.line).text;
let lineTillCurrentPosition = lineText.substr(0, position.character);
let autocompleteUnimportedPackages = config['autocomplteUnimportedPackages'] === true;

if (lineText.match(/^\s*\/\//)) {
return resolve([]);
Expand All @@ -66,80 +79,130 @@ export class GoCompletionItemProvider implements vscode.CompletionItemProvider {
}

let offset = document.offsetAt(position);
let gocode = getBinPath('gocode');

// Unset GOOS and GOARCH for the `gocode` process to ensure that GOHOSTOS and GOHOSTARCH
// are used as the target operating system and architecture. `gocode` is unable to provide
// autocompletion when the Go environment is configured for cross compilation.
let env = Object.assign({}, process.env, { GOOS: '', GOARCH: '' });

// Spawn `gocode` process
let p = cp.execFile(gocode, ['-f=json', 'autocomplete', filename, 'c' + offset], { env }, (err, stdout, stderr) => {
try {
if (err && (<any>err).code === 'ENOENT') {
promptForMissingTool('gocode');
}
if (err) return reject(err);
let results = <[number, GoCodeSuggestion[]]>JSON.parse(stdout.toString());
let suggestions = [];
// 'Smart Snippet' for package clause
// TODO: Factor this out into a general mechanism
if (!document.getText().match(/package\s+(\w+)/)) {
let defaultPackageName =
basename(document.fileName) === 'main.go'
? 'main'
: basename(dirname(document.fileName));
let packageItem = new vscode.CompletionItem('package ' + defaultPackageName);
packageItem.kind = vscode.CompletionItemKind.Snippet;
packageItem.insertText = 'package ' + defaultPackageName + '\r\n\r\n';
suggestions.push(packageItem);
let inputText = document.getText();

return this.runGoCode(filename, inputText, offset, inString, position, lineText).then(suggestions => {
if (!autocompleteUnimportedPackages) {
return resolve(suggestions);
}

// Add importable packages matching currentword to suggestions
suggestions = suggestions.concat(this.getMatchingPackages(currentWord));

// If no suggestions and cursor is at a dot, then check if preceeding word is a package name
// If yes, then import the package in the inputText and run gocode again to get suggestions
if (suggestions.length === 0 && lineTillCurrentPosition.endsWith('.')) {

let pkgPath = this.getPackagePathFromLine(lineTillCurrentPosition);
if (pkgPath) {
// Now that we have the package path, import it right after the "package" statement
let {imports, pkg} = parseFilePrelude(vscode.window.activeTextEditor.document.getText());
let posToAddImport = document.offsetAt(new vscode.Position(pkg.start + 1 , 0));
let textToAdd = `import "${pkgPath}"\n`;
inputText = inputText.substr(0, posToAddImport) + textToAdd + inputText.substr(posToAddImport);
offset += textToAdd.length;

// Now that we have the package imported in the inputText, run gocode again
return this.runGoCode(filename, inputText, offset, inString, position, lineText).then(newsuggestions => {
// Since the new suggestions are due to the package that we imported,
// add additionalTextEdits to do the same in the actual document in the editor
// We use additionalTextEdits instead of command so that 'useCodeSnippetsOnFunctionSuggest' feature continues to work
newsuggestions.forEach(item => {
item.additionalTextEdits = [getTextEditForAddImport(pkgPath)];
});
resolve(newsuggestions);
});
}
if (results[1]) {
for (let suggest of results[1]) {
if (inString && suggest.class !== 'import') continue;
let item = new vscode.CompletionItem(suggest.name);
item.kind = vscodeKindFromGoCodeClass(suggest.class);
item.detail = suggest.type;
if (inString && suggest.class === 'import') {
item.textEdit = new vscode.TextEdit(
new vscode.Range(
position.line,
lineText.substring(0, position.character).lastIndexOf('"') + 1,
position.line,
position.character),
suggest.name
);
}
let conf = vscode.workspace.getConfiguration('go');
if (conf.get('useCodeSnippetsOnFunctionSuggest') && suggest.class === 'func') {
let params = parameters(suggest.type.substring(4));
let paramSnippets = [];
for (let i in params) {
let param = params[i].trim();
if (param) {
param = param.replace('{', '\\{').replace('}', '\\}');
paramSnippets.push('{{' + param + '}}');
}
}
item.insertText = suggest.name + '(' + paramSnippets.join(', ') + '){{}}';
}
suggestions.push(item);
};
}
resolve(suggestions);
} catch (e) {
reject(e);
}
resolve(suggestions);
});
p.stdin.end(document.getText());
});
});
}

private runGoCode(filename: string, inputText: string, offset: number, inString: boolean, position: vscode.Position, lineText: string): Thenable<vscode.CompletionItem[]> {
return new Promise<vscode.CompletionItem[]>((resolve, reject) => {
let gocode = getBinPath('gocode');

// Unset GOOS and GOARCH for the `gocode` process to ensure that GOHOSTOS and GOHOSTARCH
// are used as the target operating system and architecture. `gocode` is unable to provide
// autocompletion when the Go environment is configured for cross compilation.
let env = Object.assign({}, process.env, { GOOS: '', GOARCH: '' });

// Spawn `gocode` process
let p = cp.execFile(gocode, ['-f=json', 'autocomplete', filename, 'c' + offset], { env }, (err, stdout, stderr) => {
try {
if (err && (<any>err).code === 'ENOENT') {
promptForMissingTool('gocode');
}
if (err) return reject(err);
let results = <[number, GoCodeSuggestion[]]>JSON.parse(stdout.toString());
let suggestions = [];
// 'Smart Snippet' for package clause
// TODO: Factor this out into a general mechanism
if (!inputText.match(/package\s+(\w+)/)) {
let defaultPackageName =
basename(filename) === 'main.go'
? 'main'
: basename(dirname(filename));
let packageItem = new vscode.CompletionItem('package ' + defaultPackageName);
packageItem.kind = vscode.CompletionItemKind.Snippet;
packageItem.insertText = 'package ' + defaultPackageName + '\r\n\r\n';
suggestions.push(packageItem);

}
if (results[1]) {
for (let suggest of results[1]) {
if (inString && suggest.class !== 'import') continue;
let item = new vscode.CompletionItem(suggest.name);
item.kind = vscodeKindFromGoCodeClass(suggest.class);
item.detail = suggest.type;
if (inString && suggest.class === 'import') {
item.textEdit = new vscode.TextEdit(
new vscode.Range(
position.line,
lineText.substring(0, position.character).lastIndexOf('"') + 1,
position.line,
position.character),
suggest.name
);
}
let conf = vscode.workspace.getConfiguration('go');
if (conf.get('useCodeSnippetsOnFunctionSuggest') && suggest.class === 'func') {
let params = parameters(suggest.type.substring(4));
let paramSnippets = [];
for (let i in params) {
let param = params[i].trim();
if (param) {
param = param.replace('{', '\\{').replace('}', '\\}');
paramSnippets.push('{{' + param + '}}');
}
}
item.insertText = suggest.name + '(' + paramSnippets.join(', ') + ') {{}}';
}
suggestions.push(item);
};
}
resolve(suggestions);
} catch (e) {
reject(e);
}
});
p.stdin.end(inputText);
});
}
// TODO: Shouldn't lib-path also be set?
private ensureGoCodeConfigured(): Thenable<void> {
return new Promise<void>((resolve, reject) => {
let pkgPromise = listPackages(true).then((pkgs: string[]) => {
this.pkgsList = pkgs.map(pkg => {
let index = pkg.lastIndexOf('/');
return {
name: index === -1 ? pkg : pkg.substr(index + 1),
path: pkg
};
});
});
let configPromise = new Promise<void>((resolve, reject) => {
// TODO: Since the gocode daemon is shared amongst clients, shouldn't settings be
// adjusted per-invocation to avoid conflicts from other gocode-using programs?
if (this.gocodeConfigurationComplete) {
Expand All @@ -153,5 +216,47 @@ export class GoCompletionItemProvider implements vscode.CompletionItemProvider {
});
});
});
return Promise.all([pkgPromise, configPromise]).then(() => {
return Promise.resolve();
}); ;
}

// Return importable packages that match given word as Completion Items
private getMatchingPackages(word: string): vscode.CompletionItem[] {
if (!word) return [];
let completionItems = this.pkgsList.filter((pkgInfo: PackageInfo) => {
return pkgInfo.name.startsWith(word);
}).map((pkgInfo: PackageInfo) => {
let item = new vscode.CompletionItem(pkgInfo.name, vscode.CompletionItemKind.Keyword);
item.detail = pkgInfo.path;
item.documentation = 'Imports the package';
item.insertText = pkgInfo.name;
item.command = {
title: 'Import Package',
command: 'go.import.add',
arguments: [pkgInfo.path]
};
return item;
});
return completionItems;
}

// Given a line ending with dot, return the word preceeding the dot if it is a package name that can be imported
private getPackagePathFromLine(line: string): string {
let pattern = /(\w+)\.$/g;
let wordmatches = pattern.exec(line);
if (!wordmatches) {
return;
}

let [_, pkgName] = wordmatches;
// Word is isolated. Now check pkgsList for a match
let matchingPackages = this.pkgsList.filter(pkgInfo => {
return pkgInfo.name === pkgName;
});

if (matchingPackages && matchingPackages.length === 1) {
return matchingPackages[0].path;
}
}
}
Loading