diff --git a/src/goImport.ts b/src/goImport.ts index 2568dad38..804b778d6 100644 --- a/src/goImport.ts +++ b/src/goImport.ts @@ -9,31 +9,91 @@ import vscode = require('vscode'); import cp = require('child_process'); import { getBinPath } from './goPath'; import { parseFilePrelude } from './util'; -import { promptForMissingTool } from './goInstallTools'; import { documentSymbols } from './goOutline'; +import { promptForMissingTool, isVendorSupported } from './goInstallTools'; +import path = require('path'); export function listPackages(excludeImportedPkgs: boolean = false): Thenable { let importsPromise = excludeImportedPkgs && vscode.window.activeTextEditor ? getImports(vscode.window.activeTextEditor.document.fileName) : Promise.resolve([]); - let pkgsPromise = new Promise((resolve, reject) => { + let vendorSupportPromise = isVendorSupported(); + let goPkgsPromise = new Promise((resolve, reject) => { cp.execFile(getBinPath('gopkgs'), [], (err, stdout, stderr) => { if (err && (err).code === 'ENOENT') { promptForMissingTool('gopkgs'); return reject(); } let lines = stdout.toString().split('\n'); - let sortedlines = lines.sort().slice(1); // Drop the empty entry from the final '\n' - return resolve(sortedlines); + if (lines[lines.length - 1] === '') { + // Drop the empty entry from the final '\n' + lines.pop(); + } + return resolve(lines); }); }); - return Promise.all([importsPromise, pkgsPromise]).then(values => { - let imports = values[0]; - let pkgs = values[1]; - if (imports.length === 0) { - return pkgs; - } - return pkgs.filter(element => { - return imports.indexOf(element) === -1; + return vendorSupportPromise.then((vendorSupport: boolean) => { + return Promise.all([goPkgsPromise, importsPromise]).then(values => { + let pkgs = values[0]; + let importedPkgs = values [1]; + + if (!vendorSupport) { + if (importedPkgs.length > 0) { + pkgs = pkgs.filter(element => { + return importedPkgs.indexOf(element) === -1; + }); + } + return pkgs.sort(); + } + + let currentFileDirPath = path.dirname(vscode.window.activeTextEditor.document.fileName); + let workspaces: string[] = process.env['GOPATH'].split(path.delimiter); + let currentWorkspace = path.join(workspaces[0], 'src'); + + // Workaround for issue in https://github.com/Microsoft/vscode/issues/9448#issuecomment-244804026 + if (process.platform === 'win32') { + currentFileDirPath = currentFileDirPath.substr(0, 1).toUpperCase() + currentFileDirPath.substr(1); + } + + // In case of multiple workspaces, find current workspace by checking if current file is + // under any of the workspaces in $GOPATH + for (let i = 1; i < workspaces.length; i++) { + let possibleCurrentWorkspace = path.join(workspaces[i], 'src'); + if (currentFileDirPath.startsWith(possibleCurrentWorkspace)) { + // In case of nested workspaces, (example: both /Users/me and /Users/me/src/a/b/c are in $GOPATH) + // both parent & child workspace in the nested workspaces pair can make it inside the above if block + // Therefore, the below check will take longer (more specific to current file) of the two + if (possibleCurrentWorkspace.length > currentWorkspace.length) { + currentWorkspace = possibleCurrentWorkspace; + } + } + } + + let pkgSet = new Set(); + pkgs.forEach(pkg => { + if (!pkg || importedPkgs.indexOf(pkg) > -1) { + return; + } + + let magicVendorString = '/vendor/'; + let vendorIndex = pkg.indexOf(magicVendorString); + + // 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 (vendorIndex > 0) { + 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 or is a vendor project not belonging to current project + pkgSet.add(pkg); + }); + + return Array.from(pkgSet).sort(); }); }); } diff --git a/src/goInstallTools.ts b/src/goInstallTools.ts index 1c44f69e1..80bd2f7b2 100644 --- a/src/goInstallTools.ts +++ b/src/goInstallTools.ts @@ -20,6 +20,7 @@ interface SemVersion { } let goVersion: SemVersion = null; +let vendorSupport: boolean = null; function getTools(): { [key: string]: string } { let goConfig = vscode.workspace.getConfiguration('go'); @@ -142,6 +143,7 @@ export function updateGoPathGoRootFromConfig() { export function setupGoPathAndOfferToInstallTools() { updateGoPathGoRootFromConfig(); + isVendorSupported(); if (!process.env['GOPATH']) { let info = 'GOPATH is not set as an environment variable or via `go.gopath` setting in Code'; @@ -193,6 +195,8 @@ function getMissingTools(): Promise { }); } + + export function getGoVersion(): Promise { if (goVersion) { return Promise.resolve(goVersion); @@ -209,4 +213,25 @@ export function getGoVersion(): Promise { return resolve(goVersion); }); }); -} \ No newline at end of file +} + +export function isVendorSupported(): Promise { + if (vendorSupport != null) { + return Promise.resolve(vendorSupport); + } + return getGoVersion().then(version => { + switch (version.major) { + case 0: + vendorSupport = false; + break; + case 1: + vendorSupport = (version.minor > 5 || (version.minor === 5 && process.env['GO15VENDOREXPERIMENT'] === '1')) ? true : false; + break; + default: + vendorSupport = true; + break; + } + return vendorSupport; + }); +} + diff --git a/test/go.test.ts b/test/go.test.ts index d37844de9..3c46d627a 100644 --- a/test/go.test.ts +++ b/test/go.test.ts @@ -19,6 +19,8 @@ import { getGoVersion } from '../src/goInstallTools'; import { documentSymbols } from '../src/goOutline'; import { listPackages } from '../src/goImport'; import { generateTestCurrentFile, generateTestCurrentPackage } from '../src/goGenerateTests'; +import { getBinPath } from '../src/goPath'; +import { isVendorSupported } from '../src/goInstallTools'; suite('Go Extension Tests', () => { let gopath = process.env['GOPATH']; @@ -83,17 +85,19 @@ encountered. ]; let uri = vscode.Uri.file(path.join(fixturePath, 'test.go')); vscode.workspace.openTextDocument(uri).then((textDocument) => { - let promises = testCases.map(([position, expected]) => - provider.provideCompletionItems(textDocument, position, null).then(items => { - let labels = items.map(x => x.label); - for (let entry of expected) { - if (labels.indexOf(entry) < 0) { - assert.fail('', entry, 'missing expected item in competion list'); + return vscode.window.showTextDocument(textDocument).then(editor => { + let promises = testCases.map(([position, expected]) => + provider.provideCompletionItems(textDocument, position, null).then(items => { + let labels = items.map(x => x.label); + for (let entry of expected) { + if (labels.indexOf(entry) < 0) { + assert.fail('', entry, 'missing expected item in competion list'); + } } - } - }) - ); - return Promise.all(promises); + }) + ); + return Promise.all(promises); + }); }, (err) => { assert.ok(false, `error in OpenTextDocument ${err}`); }).then(() => done(), done); @@ -389,4 +393,69 @@ encountered. }); }).then(() => done(), done); }); + + test('Replace vendor packages with relative path', (done) => { + // This test needs a go project that has vendor folder and vendor packages + // Since the Go extension takes a dependency on the godef tool at github.com/rogpeppe/godef + // which has vendor packages, we are using it here to test the "replace vendor packages with relative path" feature. + // If the extension ever stops depending on godef tool or if godef ever stops having vendor packages, then this test + // will fail and will have to be replaced with any other go project with vendor packages + + let vendorSupportPromise = isVendorSupported(); + let filePath = path.join(process.env['GOPATH'], 'src', 'github.com', 'rogpeppe', 'godef', 'go', 'ast', 'ast.go'); + let vendorPkgsFullPath = [ + 'github.com/rogpeppe/godef/vendor/9fans.net/go/acme', + 'github.com/rogpeppe/godef/vendor/9fans.net/go/plan9', + 'github.com/rogpeppe/godef/vendor/9fans.net/go/plan9/client' + ]; + let vendorPkgsRelativePath = [ + '9fans.net/go/acme', + '9fans.net/go/plan9', + '9fans.net/go/plan9/client' + ]; + + vendorSupportPromise.then((vendorSupport: boolean) => { + let gopkgsPromise = new Promise((resolve, reject) => { + cp.execFile(getBinPath('gopkgs'), [], (err, stdout, stderr) => { + let pkgs = stdout.split('\n').sort().slice(1); + if (vendorSupport) { + vendorPkgsFullPath.forEach(pkg => { + assert.equal(pkgs.indexOf(pkg) > -1, true, `Package not found by goPkgs: ${pkg}`); + }); + vendorPkgsRelativePath.forEach(pkg => { + assert.equal(pkgs.indexOf(pkg), -1, `Relative path to vendor package ${pkg} should not be returned by gopkgs command`); + }); + } + return resolve(pkgs); + }); + }); + + let listPkgPromise: Thenable = vscode.workspace.openTextDocument(vscode.Uri.file(filePath)).then(document => { + return vscode.window.showTextDocument(document).then(editor => { + return listPackages().then(pkgs => { + if (vendorSupport) { + vendorPkgsRelativePath.forEach(pkg => { + assert.equal(pkgs.indexOf(pkg) > -1, true, `Relative path for vendor package ${pkg} not found`); + }); + vendorPkgsFullPath.forEach(pkg => { + assert.equal(pkgs.indexOf(pkg), -1, `Full path for vendor package ${pkg} should be shown by listPackages method`); + }); + } + return Promise.resolve(pkgs); + }); + }); + }); + + return Promise.all([gopkgsPromise, listPkgPromise]).then((values: string[][]) => { + if (!vendorSupport) { + let originalPkgs = values[0]; + let updatedPkgs = values[1]; + assert.equal(originalPkgs.length, updatedPkgs.length); + for (let index = 0; index < originalPkgs.length; index++) { + assert.equal(updatedPkgs[index], originalPkgs[index]); + } + } + }); + }).then(() => done(), done); + }); });