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

Commit

Permalink
Add completion items for unimported packages (#497)
Browse files Browse the repository at this point in the history
* Add completion items for unimported packages

* Use additionalTextEdits so that 'useCodeSnippetsOnFunctionSuggest' feature continues to work

* Add setting autocomplteUnimportedPackages, dont show imported pkgs
  • Loading branch information
ramya-rao-a authored Oct 6, 2016
1 parent 8246d88 commit b780d41
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 93 deletions.
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 @@ -346,6 +346,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 {
// 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 => {
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;
}
});
}
}
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

0 comments on commit b780d41

Please sign in to comment.