From 78125e6fe4827993d2feaf87319c46625fa026f6 Mon Sep 17 00:00:00 2001 From: Ramya Achutha Rao Date: Sun, 16 Jul 2017 16:57:26 -0700 Subject: [PATCH 1/3] Use go list all to get pkgname to use in completions Fixes #647 --- src/goImport.ts | 27 ++----------- src/goMain.ts | 2 + src/goPackages.ts | 97 +++++++++++++++++++++++++++++++++++++++++++++++ src/goSuggest.ts | 78 +++++++++++++++++-------------------- 4 files changed, 137 insertions(+), 67 deletions(-) create mode 100644 src/goPackages.ts diff --git a/src/goImport.ts b/src/goImport.ts index 6a0c34b27..d9d56a92a 100644 --- a/src/goImport.ts +++ b/src/goImport.ts @@ -11,6 +11,7 @@ import { parseFilePrelude, isVendorSupported, getBinPath, getCurrentGoWorkspaceF import { documentSymbols } from './goOutline'; import { promptForMissingTool } from './goInstallTools'; import path = require('path'); +import { getRelativePackagePath } from './goPackages'; export function listPackages(excludeImportedPkgs: boolean = false): Thenable { let importsPromise = excludeImportedPkgs && vscode.window.activeTextEditor ? getImports(vscode.window.activeTextEditor.document) : Promise.resolve([]); @@ -51,30 +52,10 @@ export function listPackages(excludeImportedPkgs: boolean = false): Thenable -1) { return; } - - let magicVendorString = '/vendor/'; - let vendorIndex = pkg.indexOf(magicVendorString); - if (vendorIndex === -1) { - magicVendorString = 'vendor/'; - if (pkg.startsWith(magicVendorString)) { - vendorIndex = 0; - } + let relativePkgPath = getRelativePackagePath(currentFileDirPath, currentWorkspace, pkg); + if (relativePkgPath) { + pkgSet.add(relativePkgPath); } - // Check if current file and the vendor pkg belong to the same root project - // If yes, then vendor pkg can be replaced with its relative path to the "vendor" folder - // If not, then the vendor pkg should not be allowed to be imported. - if (vendorIndex > -1) { - let rootProjectForVendorPkg = path.join(currentWorkspace, pkg.substr(0, vendorIndex)); - let relativePathForVendorPkg = pkg.substring(vendorIndex + magicVendorString.length); - - if (relativePathForVendorPkg && currentFileDirPath.startsWith(rootProjectForVendorPkg)) { - pkgSet.add(relativePathForVendorPkg); - } - return; - } - - // pkg is not a vendor project - pkgSet.add(pkg); }); return Array.from(pkgSet).sort(); diff --git a/src/goMain.ts b/src/goMain.ts index 80f2155b8..fcf62a422 100644 --- a/src/goMain.ts +++ b/src/goMain.ts @@ -34,6 +34,7 @@ import { addTags, removeTags } from './goModifytags'; import { parseLiveFile } from './goLiveErrors'; import { GoCodeLensProvider } from './goCodelens'; import { implCursor } from './goImpl'; +import { goListAll } from './goPackages'; export let errorDiagnosticCollection: vscode.DiagnosticCollection; let warningDiagnosticCollection: vscode.DiagnosticCollection; @@ -44,6 +45,7 @@ export function activate(ctx: vscode.ExtensionContext): void { let toolsGopath = vscode.workspace.getConfiguration('go')['toolsGopath']; updateGoPathGoRootFromConfig().then(() => { + goListAll(); offerToInstallTools(); let langServerAvailable = checkLanguageServer(); if (langServerAvailable) { diff --git a/src/goPackages.ts b/src/goPackages.ts new file mode 100644 index 000000000..5000ec9df --- /dev/null +++ b/src/goPackages.ts @@ -0,0 +1,97 @@ +import vscode = require('vscode'); +import cp = require('child_process'); +import path = require('path'); +import { getGoRuntimePath } from './goPath'; +import { isVendorSupported, getCurrentGoWorkspaceFromGOPATH } from './util'; + +let allPkgs = new Map(); +let goListAllCompleted: boolean = false; + +/** + * Runs go list all + * @returns Map mapping between package import path and package name + */ +export function goListAll(): Promise> { + let goRuntimePath = getGoRuntimePath(); + + if (!goRuntimePath) { + vscode.window.showInformationMessage('Cannot find "go" binary. Update PATH or GOROOT appropriately'); + return Promise.resolve(null); + } + + if (goListAllCompleted) { + return Promise.resolve(allPkgs); + } + return new Promise>((resolve, reject) => { + cp.execFile(goRuntimePath, ['list', '-f', '{{.Name}};{{.ImportPath}}', 'all'], (err, stdout, stderr) => { + if (err) return reject(); + stdout.split('\n').forEach(pkgDetail => { + if (!pkgDetail || !pkgDetail.trim() || pkgDetail.indexOf(';') === -1) return; + let [pkgName, pkgPath] = pkgDetail.trim().split(';'); + if (pkgName !== 'main') { + allPkgs.set(pkgPath, pkgName); + } + }); + goListAllCompleted = true; + return resolve(allPkgs); + }); + }); +} + +/** + * Returns mapping of import path and package name for packages + * @param filePath. Used to determine the right relative path for vendor pkgs + * @returns Map mapping between package import path and package name + */ +export function getAllPackageDetails(filePath: string): Promise> { + + return Promise.all([isVendorSupported(), goListAll()]).then(values => { + let isVendorSupported = values[0]; + let pkgs: Map = values[1]; + let currentFileDirPath = path.dirname(filePath); + let currentWorkspace = getCurrentGoWorkspaceFromGOPATH(currentFileDirPath); + if (!isVendorSupported || !currentWorkspace) { + return pkgs; + } + + let pkgMap = new Map(); + pkgs.forEach((pkgName, pkgPath) => { + let relativePkgPath = getRelativePackagePath(currentFileDirPath, currentWorkspace, pkgPath); + if (relativePkgPath) { + pkgMap.set(relativePkgPath, pkgs.get(pkgPath)); + } + }); + return pkgMap; + }); + +} + +/** + * If given pkgPath is not vendor pkg, then the same pkgPath is returned + * Else, the import path for the vendor pkg relative to given filePath is returned. + */ +export function getRelativePackagePath(currentFileDirPath: string, currentWorkspace: string, pkgPath: string): string { + let magicVendorString = '/vendor/'; + let vendorIndex = pkgPath.indexOf(magicVendorString); + if (vendorIndex === -1) { + magicVendorString = 'vendor/'; + if (pkgPath.startsWith(magicVendorString)) { + vendorIndex = 0; + } + } + // Check if current file and the vendor pkg belong to the same root project + // If yes, then vendor pkg can be replaced with its relative path to the "vendor" folder + // If not, then the vendor pkg should not be allowed to be imported. + if (vendorIndex > -1) { + let rootProjectForVendorPkg = path.join(currentWorkspace, pkgPath.substr(0, vendorIndex)); + let relativePathForVendorPkg = pkgPath.substring(vendorIndex + magicVendorString.length); + + if (relativePathForVendorPkg && currentFileDirPath.startsWith(rootProjectForVendorPkg)) { + return relativePathForVendorPkg; + } + return ''; + } + + return pkgPath; +} + diff --git a/src/goSuggest.ts b/src/goSuggest.ts index 168611e8b..8fc58304a 100644 --- a/src/goSuggest.ts +++ b/src/goSuggest.ts @@ -10,7 +10,8 @@ import cp = require('child_process'); import { dirname, basename } from 'path'; import { getBinPath, parameters, parseFilePrelude, isPositionInString, goKeywords, getToolsEnvVars } from './util'; import { promptForMissingTool } from './goInstallTools'; -import { listPackages, getTextEditForAddImport } from './goImport'; +import { getTextEditForAddImport } from './goImport'; +import { getAllPackageDetails } from './goPackages'; function vscodeKindFromGoCodeClass(kind: string): vscode.CompletionItemKind { switch (kind) { @@ -34,15 +35,10 @@ interface GoCodeSuggestion { type: string; } -interface PackageInfo { - name: string; - path: string; -} - export class GoCompletionItemProvider implements vscode.CompletionItemProvider { private gocodeConfigurationComplete = false; - private pkgsList: PackageInfo[] = []; + private pkgsList = new Map(); public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { return this.provideCompletionItemsInternal(document, position, token, vscode.workspace.getConfiguration('go')); @@ -219,21 +215,11 @@ export class GoCompletionItemProvider implements vscode.CompletionItemProvider { } // TODO: Shouldn't lib-path also be set? private ensureGoCodeConfigured(): Thenable { - let pkgPromise = listPackages(false).then((pkgs: string[]) => { - this.pkgsList = pkgs.map(pkg => { - let index = pkg.lastIndexOf('/'); - let pkgName = index === -1 ? pkg : pkg.substr(index + 1); - // pkgs from gopkg.in will be of the form gopkg.in/user/somepkg.v3 - if (pkg.match(/gopkg\.in\/.*\.v\d+/)) { - pkgName = pkgName.substr(0, pkgName.lastIndexOf('.v')); - } - return { - name: pkgName, - path: pkg - }; - }); + getAllPackageDetails(vscode.window.activeTextEditor.document.fileName).then(pkgMap => { + this.pkgsList = pkgMap; }); - let configPromise = new Promise((resolve, reject) => { + + return new Promise((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) { @@ -244,33 +230,34 @@ export class GoCompletionItemProvider implements vscode.CompletionItemProvider { let env = getToolsEnvVars(); cp.execFile(gocode, ['set', 'propose-builtins', 'true'], { env }, (err, stdout, stderr) => { cp.execFile(gocode, ['set', 'autobuild', autobuild], {}, (err, stdout, stderr) => { - resolve(); + return resolve(); }); }); }); - return Promise.all([pkgPromise, configPromise]).then(() => { - return Promise.resolve(); - }); + } // Return importable packages that match given word as Completion Items private getMatchingPackages(word: string, suggestionSet: Set): vscode.CompletionItem[] { if (!word) return []; - let completionItems = this.pkgsList.filter((pkgInfo: PackageInfo) => { - return pkgInfo.name.startsWith(word) && !suggestionSet.has(pkgInfo.name); - }).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] - }; - // Add same sortText to the unimported packages so that they appear after the suggestions from gocode - item.sortText = 'z'; - return item; + let completionItems = []; + + this.pkgsList.forEach((pkgName: string, pkgPath: string) => { + if (pkgName.startsWith(word) && !suggestionSet.has(pkgName)) { + + let item = new vscode.CompletionItem(pkgName, vscode.CompletionItemKind.Keyword); + item.detail = pkgPath; + item.documentation = 'Imports the package'; + item.insertText = pkgName; + item.command = { + title: 'Import Package', + command: 'go.import.add', + arguments: [pkgPath] + }; + // Add same sortText to the unimported packages so that they appear after the suggestions from gocode + item.sortText = 'z'; + completionItems.push(item); + } }); return completionItems; } @@ -283,14 +270,17 @@ export class GoCompletionItemProvider implements vscode.CompletionItemProvider { return; } - let [_, pkgName] = wordmatches; + let [_, pkgNameFromWord] = wordmatches; // Word is isolated. Now check pkgsList for a match - let matchingPackages = this.pkgsList.filter(pkgInfo => { - return pkgInfo.name === pkgName; + let matchingPackages = []; + this.pkgsList.forEach((pkgName: string, pkgPath: string) => { + if (pkgNameFromWord === pkgName) { + matchingPackages.push(pkgPath); + } }); if (matchingPackages && matchingPackages.length === 1) { - return matchingPackages[0].path; + return matchingPackages[0]; } } } From b4a081cc9bf73b43419ec5d3fb2779807fd528d0 Mon Sep 17 00:00:00 2001 From: Ramya Achutha Rao Date: Sun, 16 Jul 2017 20:14:22 -0700 Subject: [PATCH 2/3] Check for version being null --- test/go.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/go.test.ts b/test/go.test.ts index 1c1ae5a55..03fe209c6 100644 --- a/test/go.test.ts +++ b/test/go.test.ts @@ -128,7 +128,7 @@ suite('Go Extension Tests', () => { 'docsTool': { value: 'gogetdoc' } }); getGoVersion().then(version => { - if (version.major > 1 || (version.major === 1 && version.minor > 5)) { + if (!version || version.major > 1 || (version.major === 1 && version.minor > 5)) { return testDefinitionProvider(config); } return Promise.resolve(); @@ -166,7 +166,7 @@ It returns the number of bytes written and any write error encountered. 'docsTool': { value: 'gogetdoc' } }); getGoVersion().then(version => { - if (version.major > 1 || (version.major === 1 && version.minor > 5)) { + if (!version || version.major > 1 || (version.major === 1 && version.minor > 5)) { return testSignatureHelpProvider(config, testCases); } return Promise.resolve(); @@ -217,7 +217,7 @@ It returns the number of bytes written and any write error encountered. 'docsTool': { value: 'gogetdoc' } }); getGoVersion().then(version => { - if (version.major > 1 || (version.major === 1 && version.minor > 5)) { + if (!version || version.major > 1 || (version.major === 1 && version.minor > 5)) { return testHoverProvider(config, testCases); } return Promise.resolve(); @@ -305,7 +305,7 @@ It returns the number of bytes written and any write error encountered. { line: 12, severity: 'error', msg: 'undefined: prin' }, ]; getGoVersion().then(version => { - if (version.major === 1 && version.minor < 6) { + if (version && version.major === 1 && version.minor < 6) { // golint is not supported in Go 1.5, so skip the test return Promise.resolve(); } @@ -326,7 +326,7 @@ It returns the number of bytes written and any write error encountered. test('Test Generate unit tests squeleton for file', (done) => { getGoVersion().then(version => { - if (version.major === 1 && version.minor < 6) { + if (version && version.major === 1 && version.minor < 6) { // gotests is not supported in Go 1.5, so skip the test return Promise.resolve(); } @@ -352,7 +352,7 @@ It returns the number of bytes written and any write error encountered. test('Test Generate unit tests squeleton for a function', (done) => { getGoVersion().then(version => { - if (version.major === 1 && version.minor < 6) { + if (version && version.major === 1 && version.minor < 6) { // gotests is not supported in Go 1.5, so skip the test return Promise.resolve(); } @@ -381,7 +381,7 @@ It returns the number of bytes written and any write error encountered. test('Test Generate unit tests squeleton for package', (done) => { getGoVersion().then(version => { - if (version.major === 1 && version.minor < 6) { + if (version && version.major === 1 && version.minor < 6) { // gotests is not supported in Go 1.5, so skip the test return Promise.resolve(); } @@ -407,7 +407,7 @@ It returns the number of bytes written and any write error encountered. test('Gometalinter error checking', (done) => { getGoVersion().then(version => { - if (version.major === 1 && version.minor < 6) { + if (version && version.major === 1 && version.minor < 6) { // golint in gometalinter is not supported in Go 1.5, so skip the test return Promise.resolve(); } From 3865feaf313f3f1476cb332fa3f87d7d07bdff11 Mon Sep 17 00:00:00 2001 From: Ramya Achutha Rao Date: Sun, 16 Jul 2017 20:29:38 -0700 Subject: [PATCH 3/3] Wait for golist before testing --- test/go.test.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/test/go.test.ts b/test/go.test.ts index 03fe209c6..3c8da19c0 100644 --- a/test/go.test.ts +++ b/test/go.test.ts @@ -21,6 +21,7 @@ import { getBinPath, getGoVersion, isVendorSupported } from '../src/util'; import { documentSymbols } from '../src/goOutline'; import { listPackages } from '../src/goImport'; import { generateTestCurrentFile, generateTestCurrentPackage, generateTestCurrentFunction } from '../src/goGenerateTests'; +import { goListAll } from '../src/goPackages'; suite('Go Extension Tests', () => { let gopath = process.env['GOPATH']; @@ -265,22 +266,26 @@ It returns the number of bytes written and any write error encountered. [new vscode.Position(12, 5), ['Abs', 'Acos', 'Asin']] ]; let uri = vscode.Uri.file(path.join(fixturePath, 'test.go')); + let goListAllPromise = goListAll(); vscode.workspace.openTextDocument(uri).then((textDocument) => { return vscode.window.showTextDocument(textDocument).then(editor => { + return editor.edit(editbuilder => { editbuilder.insert(new vscode.Position(12, 1), 'by\n'); editbuilder.insert(new vscode.Position(13, 0), 'math.\n'); }).then(() => { - let promises = testCases.map(([position, expected]) => - provider.provideCompletionItemsInternal(editor.document, position, null, config).then(items => { - let labels = items.map(x => x.label); - for (let entry of expected) { - assert.equal(labels.indexOf(entry) > -1, true, `missing expected item in completion list: ${entry} Actual: ${labels}`); - } - }) - ); - return Promise.all(promises); + return goListAllPromise.then(() => { + let promises = testCases.map(([position, expected]) => + provider.provideCompletionItemsInternal(editor.document, position, null, config).then(items => { + let labels = items.map(x => x.label); + for (let entry of expected) { + assert.equal(labels.indexOf(entry) > -1, true, `missing expected item in completion list: ${entry} Actual: ${labels}`); + } + }) + ); + return Promise.all(promises); + }); }); }).then(() => { vscode.commands.executeCommand('workbench.action.closeActiveEditor');