Skip to content
This repository was archived by the owner on Jul 15, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ install:
- go get -u -v github.com/acroca/go-symbols
- go get -u -v github.com/alecthomas/gometalinter
- go get -u -v github.com/cweill/gotests/...
- go get -u -v github.com/haya14busa/goplay/cmd/goplay
- GO15VENDOREXPERIMENT=1
- if [[ "$(go version)" =~ "go version go1.5" ]]; then echo skipping gometalinter; else gometalinter --install; fi

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This extension adds rich language support for the Go language to VS Code, includ
- Show code coverage
- Generate method stubs for interfaces (using `impl`)
- [_partially implemented_] Debugging (using `delve`)
- Upload to the Go Playground (using `goplay`)

### IDE Features
![IDE](https://i.giphy.com/xTiTndDHV3GeIy6aNa.gif)
Expand Down Expand Up @@ -105,6 +106,7 @@ In addition to integrated editing features, the extension also provides several
* `Go: Add Tags` Adds configured tags to selected struct fields.
* `Go: Remove Tags` Removes configured tags from selected struct fields.
* `Go: Generate Interface Stubs` Generates method stubs for given interface
* `Go: Run on Go Playground` Upload the current selection or file to the Go Playground

You can access all of the above commands from the command pallet (`Cmd+Shift+P` or `Ctrl+Shift+P`).

Expand Down
39 changes: 38 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@
"command": "go.get.package",
"title": "Go: Get Package",
"description": "Run `go get -v` on the package on the current line."
},
{
"command": "go.playground",
"title": "Go: Run on Go Playground",
"description": "Upload the current selection or file to the Go Playground"
}
],
"debuggers": [
Expand Down Expand Up @@ -733,6 +738,32 @@
"description": "Tags and options configured here will be used by the Remove Tags command to remove tags to struct fields. If promptForTags is true, then user will be prompted for tags and options. By default, all tags and options will be removed.",
"scope": "resource"
},
"go.playground": {
"type": "object",
"properties": {
"openbrowser": {
"type": "boolean",
"default": true,
"description": "Whether to open the created Go Playground in the default browser"
},
"share": {
"type": "boolean",
"default": true,
"description": "Whether to make the created Go Playground shareable"
},
"run": {
"type": "boolean",
"default": true,
"description": "Whether to run the created Go Playground after creation"
},
"description": "The flags configured here will be passed through to command `goplay`"
},
"default": {
"openbrowser": true,
"share": true,
"run": true
}
},
"go.editorContextMenuCommands": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -790,6 +821,11 @@
"type": "boolean",
"default": true,
"description": "If true, adds command to run test coverage to the editor context menu"
},
"playground": {
"type": "boolean",
"default": true,
"description": "If true, adds command to upload the current file or selection to the Go Playground"
}
},
"default": {
Expand All @@ -803,7 +839,8 @@
"generateTestForFile": false,
"generateTestForPackage": false,
"addImport": true,
"testCoverage": true
"testCoverage": true,
"playground": true
},
"description": "Experimental Feature: Enable/Disable entries from the context menu in the editor.",
"scope": "resource"
Expand Down
2 changes: 2 additions & 0 deletions src/goInstallTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const allTools: { [key: string]: string } = {
'guru': 'golang.org/x/tools/cmd/guru',
'gorename': 'golang.org/x/tools/cmd/gorename',
'gomodifytags': 'github.com/fatih/gomodifytags',
'goplay': 'github.com/haya14busa/goplay/cmd/goplay',
'impl': 'github.com/josharian/impl',
'gotype-live': 'github.com/tylerb/gotype-live',
'godef': 'github.com/rogpeppe/godef',
Expand All @@ -51,6 +52,7 @@ function getTools(goVersion: SemVersion): string[] {
'guru',
'gorename',
'gomodifytags',
'goplay',
'impl'
];

Expand Down
3 changes: 3 additions & 0 deletions src/goMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { GoReferencesCodeLensProvider } from './goReferencesCodelens';
import { implCursor } from './goImpl';
import { browsePackages } from './goBrowsePackage';
import { goGetPackage } from './goGetPackage';
import playgroundCommand from './goPlayground';

export let errorDiagnosticCollection: vscode.DiagnosticCollection;
let warningDiagnosticCollection: vscode.DiagnosticCollection;
Expand Down Expand Up @@ -289,6 +290,8 @@ export function activate(ctx: vscode.ExtensionContext): void {

ctx.subscriptions.push(vscode.commands.registerCommand('go.get.package', goGetPackage));

ctx.subscriptions.push(vscode.commands.registerCommand('go.playground', playgroundCommand));

vscode.languages.setLanguageConfiguration(GO_MODE.language, {
indentationRules: {
decreaseIndentPattern: /^\s*(\bcase\b.*:|\bdefault\b:|}[),]?|\)[,]?)$/,
Expand Down
76 changes: 76 additions & 0 deletions src/goPlayground.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import vscode = require('vscode');
import { execFile } from 'child_process';
import { outputChannel } from './goStatus';
import { getBinPath } from './util';
import { promptForMissingTool } from './goInstallTools';

// isENOENT checks if the given error results from a missing tool installation
export const isENOENT = (err: Error): Boolean => (
!!err && (<any>err).code === 'ENOENT'
);

// flags describes the configuration toggles for the command
type flags = { [key: string]: Boolean };

// IPlaygroundUploader needs to be implemented by the uploader passed to createCommandWith
export interface IPlaygroundUploader {
upload(code: string, config: flags): Promise<string>;
}

// createCommandWith retrieves the go.playground configuration and passes
// it to the given `uploader`, together with the current editor selection
// (or the full content of the editor window if the selection is empty)
export const createCommandWith = (uploader: IPlaygroundUploader) => (): Promise<any> => {
const editor = vscode.window.activeTextEditor;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Show an error message if there is no active editor

const config: flags = vscode.workspace.getConfiguration('go', editor.document.uri).get('playground');

const selection = editor.selection;
const code = selection.isEmpty
? editor.document.getText()
: editor.document.getText(selection);

outputChannel.show();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Clear the output channel before sending any output

outputChannel.appendLine('Upload to the Go Playground in progress...\n');

return uploader.upload(code, config)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not make the call to goplay directly from here instead of going through the GoplayUploader ?

Copy link
Copy Markdown
Contributor Author

@m90 m90 Oct 19, 2017

Choose a reason for hiding this comment

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

Seeing that it's a common pattern in this extension to make multiple tools available (although maybe not necessarily in that case) for the same task, I thought it'd be a good idea to separate the editor part of the command from the actual uploading. Also, it makes writing tests a lot easier. I think I'd like to keep it that way if possible.

.then(result => outputChannel.append(result))
.catch(err => {
if ((<any>err).missingTool) {
promptForMissingTool(err.missingTool);
} else {
vscode.window.showErrorMessage(err.message);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In the absence of the goplay tool, we end up in the else block here with the error message write EPIPE. This is because we are passing the code via stdin and the connection gets closed due to ENOENT error

There are 2 ways to fix this

  1. Check BINARY_LOCATION is a valid absolute path. If not then dont run goplay, exit early. OR
  2. Call goplay directly from the createCommandWith function.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Even after installing goplay, user will have to reload VSCode for the change to take affect because the location of the binary gets calculated once in the beginning and isnt updated.

Feel free to use getBinPath for every call

}
});
};

// GoplayUploader implements `IPlaygroundUploader` using command goplay
export class GoplayUploader implements IPlaygroundUploader {
private TOOL_CMD_NAME = 'goplay';
private BINARY_LOCATION = getBinPath(this.TOOL_CMD_NAME);
static stringifyFlags(f: flags): string[] {
return Object.keys(f).map(key => `-${key}=${f[key]}`);
}
upload(code: string, config: flags): Promise<string> {
return new Promise<string>((resolve, reject) => {
execFile(this.BINARY_LOCATION, [...GoplayUploader.stringifyFlags(config), '-'], (err, stdout, stderr) => {
if (isENOENT(err)) {
(<any>err).missingTool = this.TOOL_CMD_NAME;
return reject(err);
}
if (err) {
return reject(new Error(`${this.TOOL_CMD_NAME}: ${stdout || stderr || err.message}`));
}
resolve(this.formatStdout(stdout || stderr, config));
}).stdin.end(code);
});
}
private formatStdout(result: string, config: flags) {
return `Output from the Go Playground:
${result}
Finished running tool: ${this.BINARY_LOCATION} ${GoplayUploader.stringifyFlags(config).join(' ')} -\n`;
}
}

// the default export is the function that will be registered as the handler
// for the go.playground extension command in goMain.ts
export default createCommandWith(new GoplayUploader());
1 change: 1 addition & 0 deletions test/fixtures/playground/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package main
123 changes: 123 additions & 0 deletions test/go.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { documentSymbols } from '../src/goOutline';
import { listPackages } from '../src/goImport';
import { generateTestCurrentFile, generateTestCurrentPackage, generateTestCurrentFunction } from '../src/goGenerateTests';
import { getAllPackages } from '../src/goPackages';
import { GoplayUploader, IPlaygroundUploader, createCommandWith, isENOENT } from '../src/goPlayground';
import { getImportPath } from '../src/util';

suite('Go Extension Tests', () => {
Expand All @@ -47,6 +48,7 @@ suite('Go Extension Tests', () => {
fs.copySync(path.join(fixtureSourcePath, 'diffTestData', 'file2.go'), path.join(fixturePath, 'diffTest1Data', 'file2.go'));
fs.copySync(path.join(fixtureSourcePath, 'diffTestData', 'file1.go'), path.join(fixturePath, 'diffTest2Data', 'file1.go'));
fs.copySync(path.join(fixtureSourcePath, 'diffTestData', 'file2.go'), path.join(fixturePath, 'diffTest2Data', 'file2.go'));
fs.copySync(path.join(fixtureSourcePath, 'playground', 'main.go'), path.join(fixturePath, 'playground', 'main.go'));
});

suiteTeardown(() => {
Expand Down Expand Up @@ -756,4 +758,125 @@ It returns the number of bytes written and any write error encountered.
assert.equal(run[1], getImportPath(run[0]));
});
});

suite('playground command', () => {
test('GoplayUploader#upload - success', (done) => {
const validCode = `
package main

import (
"fmt"
)

func main() {
for i := 1; i < 4; i++ {
fmt.Printf("%v ", i)
}
fmt.Print("Go!")
}`;
const uploader = new GoplayUploader();
uploader.upload(validCode, { run: true, openbrowser: false, share: false })
.then((stdout: string) => {
assert(
stdout.includes('1 2 3 Go!')
);
})
.then(() => done(), done);
});

test('GoplayUploader#upload - error', (done) => {
const invalidCode = `
package notmain

import (
"fmt"
)

func fantasy() {
fmt.Print("not a main package, sorry")
}`;
const uploader = new GoplayUploader();
uploader.upload(invalidCode, { run: true, openbrowser: false, share: false })
.then(() => done(new Error('Expected error to be returned')), () => done());
});

test('GoplayUploader#stringifyFlags', () => {
const tests: [any, string[]][] = [
[{foo: true, bar: false}, ['-foo=true', '-bar=false']],
[{}, []]
];
tests.forEach(([input, expected]) => {
assert.deepStrictEqual(
GoplayUploader.stringifyFlags(input),
expected
);
});
});

class MockPlaygroundUploader implements IPlaygroundUploader {
public code: string;
public config: any;
private result: Promise<any>;
constructor(result: Promise<any>) {
this.result = result;
}
upload(code: string, config: any): Promise<string> {
this.code = code;
this.config = config;
return this.result;
}
}

(<[string, Promise<any>][]>[
['success', Promise.resolve('OK!')],
['error', Promise.reject('Not OK!')]
]).forEach(([scenario, result]) => {
test(`createCommandWith - ${scenario}`, (done) => {
const mockUploader = new MockPlaygroundUploader(result);
const commandWithMock = createCommandWith(mockUploader);
const config = vscode.workspace.getConfiguration('go').get('playground');

let uri = vscode.Uri.file(path.join(fixturePath, 'playground', 'main.go'));
vscode.workspace.openTextDocument(uri).then((document) => {
return vscode.window.showTextDocument(document);
}).then(() => {
return commandWithMock();
})
.then(() => {
assert.strictEqual(
mockUploader.code,
'package main\n'
);
assert.deepStrictEqual(
mockUploader.config,
config
);
})
.then(() => done(), done);
});
});

test('isENOENT', () => {
const tests: [Error, Boolean][] = [
[null, false],
[new Error('Something went wrong'), false],
[(() => {
const err = new Error('Could not find the requested file');
(<any>err).code = 'ENOENT';
return err;
})(), true],
[(() => {
const err = new Error('Missing permissions');
(<any>err).code = 'EPERM';
return err;
})(), false]
];
tests.forEach(([err, expected]) => {
assert.strictEqual(
isENOENT(err),
expected
);
});
});
});
});