diff --git a/.travis.yml b/.travis.yml index 48ce7281e..4f42a222d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,6 +50,7 @@ install: - go get -u -v github.com/acroca/go-symbols - go get -u -v github.com/cweill/gotests/... - go get -u -v github.com/haya14busa/goplay/cmd/goplay + - go get -u -v github.com/davidrjenni/reftools/cmd/fillstruct - GO15VENDOREXPERIMENT=1 - if [[ "$(go version)" =~ "go version go1.5" ]]; then echo skipping gometalinter; else go get -u -v github.com/alecthomas/gometalinter; fi - if [[ "$(go version)" =~ "go version go1.5" ]]; then echo skipping gometalinter; else gometalinter --install; fi diff --git a/README.md b/README.md index a15dcb917..04394e0ad 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ This extension adds rich language support for the Go language to VS Code, includ - Run Tests under the cursor, in current file, in current package, in the whole workspace (using `go test`) - Show code coverage - Generate method stubs for interfaces (using `impl`) +- Fill struct literals with default values (using `fillstruct`) - [_partially implemented_] Debugging (using `delve`) - Upload to the Go Playground (using `goplay`) @@ -106,6 +107,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: Fill Struct` Fills struct literal with default values * `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`). diff --git a/package.json b/package.json index 7df900523..9e01fd6f5 100644 --- a/package.json +++ b/package.json @@ -174,6 +174,11 @@ "title": "Go: Remove Tags From Struct Fields", "description": "Remove tags configured in go.removeTags setting from selected struct using gomodifytags" }, + { + "command": "go.fill.struct", + "title": "Go: Fill struct", + "description": "Fill a struct literal with default values" + }, { "command": "go.show.commands", "title": "Go: Show All Commands...", @@ -829,6 +834,11 @@ "default": true, "description": "If true, adds command to remove configured tags from struct fields to the editor context menu" }, + "fillStruct": { + "type": "boolean", + "default": true, + "description": "If true, adds command to fill struct literal with default values to the editor context menu" + }, "testAtCursor": { "type": "boolean", "default": true, @@ -920,6 +930,11 @@ "command": "go.remove.tags", "group": "Go group 1" }, + { + "when": "editorTextFocus && config.go.editorContextMenuCommands.fillStruct && resourceLangId == go", + "command": "go.fill.struct", + "group": "Go group 1" + }, { "when": "editorTextFocus && config.go.editorContextMenuCommands.testAtCursor && resourceLangId == go && !config.editor.codeLens", "command": "go.test.cursor", diff --git a/src/goFillStruct.ts b/src/goFillStruct.ts new file mode 100644 index 000000000..93a906f04 --- /dev/null +++ b/src/goFillStruct.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import vscode = require('vscode'); +import { byteOffsetAt, getBinPath, getFileArchive, getToolsEnvVars } from './util'; +import cp = require('child_process'); +import { promptForMissingTool } from './goInstallTools'; + +// Interface for the output from fillstruct +interface GoFillStructOutput { + start: number; + end: number; + code: string; +} + +export function runFillStruct(editor: vscode.TextEditor) { + let args = getCommonArgs(editor); + if (!args) { + return; + } + + return execFillStruct(editor, args); +} + +function getCommonArgs(editor: vscode.TextEditor): string[] { + if (!editor) { + vscode.window.showInformationMessage('No editor is active.'); + return; + } + if (!editor.document.fileName.endsWith('.go')) { + vscode.window.showInformationMessage('Current file is not a Go file.'); + return; + } + let args = ['-modified', '-file', editor.document.fileName]; + if (editor.selection.isEmpty) { + let offset = byteOffsetAt(editor.document, editor.selection.start); + args.push('-offset'); + args.push(offset.toString()); + } else { + args.push('-line'); + args.push(`${editor.selection.start.line + 1}`); + } + return args; +} + +function getTabsCount(editor: vscode.TextEditor): number { + let startline = editor.selection.start.line; + let tabs = editor.document.lineAt(startline).text.match('^\t*'); + return tabs.length; +} + +function execFillStruct(editor: vscode.TextEditor, args: string[]): Promise { + let fillstruct = getBinPath('fillstruct'); + let input = getFileArchive(editor.document); + let tabsCount = getTabsCount(editor); + + return new Promise((resolve, reject) => { + let p = cp.execFile(fillstruct, args, { env: getToolsEnvVars() }, (err, stdout, stderr) => { + try { + if (err && (err).code === 'ENOENT') { + promptForMissingTool('fillstruct'); + return reject(); + } + if (err) { + vscode.window.showInformationMessage(`Cannot fill struct: ${stderr}`); + return reject(); + } + + let output = JSON.parse(stdout); + + if (output.length === 0) { + vscode.window.showInformationMessage(`Got empty fillstruct output`); + return reject(); + } + + let indent = '\t'.repeat(tabsCount); + + editor.edit(editBuilder => { + output.forEach((structToFill) => { + const out = structToFill.code.replace(/\n/g, '\n' + indent); + const rangeToReplace = new vscode.Range(editor.document.positionAt(structToFill.start), + editor.document.positionAt(structToFill.end)); + editBuilder.replace(rangeToReplace, out); + }); + }).then(() => resolve()); + } catch (e) { + reject(e); + } + }); + p.stdin.end(input); + }); +} diff --git a/src/goInstallTools.ts b/src/goInstallTools.ts index 3f3bdb800..59ef2222a 100644 --- a/src/goInstallTools.ts +++ b/src/goInstallTools.ts @@ -38,7 +38,8 @@ const allTools: { [key: string]: string } = { 'gometalinter': 'github.com/alecthomas/gometalinter', 'megacheck': 'honnef.co/go/tools/...', 'go-langserver': 'github.com/sourcegraph/go-langserver', - 'dlv': 'github.com/derekparker/delve/cmd/dlv' + 'dlv': 'github.com/derekparker/delve/cmd/dlv', + 'fillstruct': 'github.com/davidrjenni/reftools/cmd/fillstruct' }; // Tools used explicitly by the basic features of the extension @@ -71,7 +72,8 @@ function getTools(goVersion: SemVersion): string[] { 'gorename', 'gomodifytags', 'goplay', - 'impl' + 'impl', + 'fillstruct' ]; if (goLiveErrorsEnabled()) { diff --git a/src/goMain.ts b/src/goMain.ts index 1692eed24..cc63808ed 100644 --- a/src/goMain.ts +++ b/src/goMain.ts @@ -34,6 +34,7 @@ import { isGoPathSet, getBinPath, sendTelemetryEvent, getExtensionCommands, getG import { LanguageClient } from 'vscode-languageclient'; import { clearCacheForTools } from './goPath'; import { addTags, removeTags } from './goModifytags'; +import { runFillStruct } from './goFillStruct'; import { parseLiveFile } from './goLiveErrors'; import { GoReferencesCodeLensProvider } from './goReferencesCodelens'; import { implCursor } from './goImpl'; @@ -188,6 +189,10 @@ export function activate(ctx: vscode.ExtensionContext): void { removeTags(args); })); + ctx.subscriptions.push(vscode.commands.registerCommand('go.fill.struct', () => { + runFillStruct(vscode.window.activeTextEditor); + })); + ctx.subscriptions.push(vscode.commands.registerCommand('go.impl.cursor', () => { implCursor(); })); diff --git a/test/fixtures/fillStruct/golden_1.go b/test/fixtures/fillStruct/golden_1.go new file mode 100644 index 000000000..6ac89eee9 --- /dev/null +++ b/test/fixtures/fillStruct/golden_1.go @@ -0,0 +1,19 @@ +package main + +import "time" + +type Struct struct { + String string + Number int + Float float64 + Time time.Time +} + +func main() { + myStruct := Struct{ + String: "", + Number: 0, + Float: 0.0, + Time: time.Time{}, + } +} diff --git a/test/fixtures/fillStruct/golden_2.go b/test/fixtures/fillStruct/golden_2.go new file mode 100644 index 000000000..5f5b8ed5b --- /dev/null +++ b/test/fixtures/fillStruct/golden_2.go @@ -0,0 +1,14 @@ +package main + +import ( + "net/http" +) + +func main() { + _ = http.Client{ + Transport: nil, + CheckRedirect: nil, + Jar: nil, + Timeout: 0, + } +} diff --git a/test/fixtures/fillStruct/input_1.go b/test/fixtures/fillStruct/input_1.go new file mode 100644 index 000000000..de62ea348 --- /dev/null +++ b/test/fixtures/fillStruct/input_1.go @@ -0,0 +1,14 @@ +package main + +import "time" + +type Struct struct { + String string + Number int + Float float64 + Time time.Time +} + +func main() { + myStruct := Struct{} +} diff --git a/test/fixtures/fillStruct/input_2.go b/test/fixtures/fillStruct/input_2.go new file mode 100644 index 000000000..94e71bc52 --- /dev/null +++ b/test/fixtures/fillStruct/input_2.go @@ -0,0 +1,9 @@ +package main + +import ( + "net/http" +) + +func main() { + _ = http.Client{} +} diff --git a/test/go.test.ts b/test/go.test.ts index 5fde34e94..3901388d5 100644 --- a/test/go.test.ts +++ b/test/go.test.ts @@ -25,6 +25,9 @@ import { getAllPackages } from '../src/goPackages'; import { getImportPath } from '../src/util'; import { goPlay } from '../src/goPlayground'; import { goLint } from '../src/goLint'; +import { runFillStruct } from '../src/goFillStruct'; +import { print } from 'util'; +import { TextDocument } from 'vscode-languageserver-types/lib/main'; suite('Go Extension Tests', () => { let gopath = process.env['GOPATH']; @@ -61,6 +64,11 @@ suite('Go Extension Tests', () => { fs.copySync(path.join(fixtureSourcePath, 'importTest', 'noimports.go'), path.join(fixturePath, 'importTest', 'noimports.go')); fs.copySync(path.join(fixtureSourcePath, 'importTest', 'groupImports.go'), path.join(fixturePath, 'importTest', 'groupImports.go')); fs.copySync(path.join(fixtureSourcePath, 'importTest', 'singleImports.go'), path.join(fixturePath, 'importTest', 'singleImports.go')); + fs.copySync(path.join(fixtureSourcePath, 'fillStruct', 'input_1.go'), path.join(fixturePath, 'fillStruct', 'input_1.go')); + fs.copySync(path.join(fixtureSourcePath, 'fillStruct', 'golden_1.go'), path.join(fixturePath, 'fillStruct', 'golden_1.go')); + fs.copySync(path.join(fixtureSourcePath, 'fillStruct', 'input_2.go'), path.join(fixturePath, 'fillStruct', 'input_2.go')); + fs.copySync(path.join(fixtureSourcePath, 'fillStruct', 'golden_2.go'), path.join(fixturePath, 'fillStruct', 'golden_2.go')); + fs.copySync(path.join(fixtureSourcePath, 'fillStruct', 'input_2.go'), path.join(fixturePath, 'fillStruct', 'input_3.go')); }); suiteTeardown(() => { @@ -957,4 +965,52 @@ It returns the number of bytes written and any write error encountered. }).then(() => done(), done); }); + test('Fill struct', (done) => { + let uri = vscode.Uri.file(path.join(fixturePath, 'fillStruct', 'input_1.go')); + let golden = fs.readFileSync(path.join(fixturePath, 'fillStruct', 'golden_1.go'), 'utf-8'); + + vscode.workspace.openTextDocument(uri).then((textDocument) => { + return vscode.window.showTextDocument(textDocument).then(editor => { + let selection = new vscode.Selection(12, 15, 12, 15); + editor.selection = selection; + return runFillStruct(editor).then(() => { + assert.equal(vscode.window.activeTextEditor.document.getText(), golden); + return Promise.resolve(); + }); + }); + }).then(() => done(), done); + }); + + test('Fill struct - select line', (done) => { + let uri = vscode.Uri.file(path.join(fixturePath, 'fillStruct', 'input_2.go')); + let golden = fs.readFileSync(path.join(fixturePath, 'fillStruct', 'golden_2.go'), 'utf-8'); + + vscode.workspace.openTextDocument(uri).then((textDocument) => { + return vscode.window.showTextDocument(textDocument).then(editor => { + let selection = new vscode.Selection(7, 0, 7, 10); + editor.selection = selection; + return runFillStruct(editor).then(() => { + assert.equal(vscode.window.activeTextEditor.document.getText(), golden); + return Promise.resolve(); + }); + }); + }).then(() => done(), done); + }); + + test('Fill struct – select non-struct line', (done) => { + let uri = vscode.Uri.file(path.join(fixturePath, 'fillStruct', 'input_3.go')); + // Should return same output as input. + let golden = fs.readFileSync(path.join(fixturePath, 'fillStruct', 'input_3.go'), 'utf-8'); + + vscode.workspace.openTextDocument(uri).then((textDocument) => { + return vscode.window.showTextDocument(textDocument).then(editor => { + let selection = new vscode.Selection(0, 0, 0, 0); + editor.selection = selection; + return runFillStruct(editor).then(() => { + assert.equal(vscode.window.activeTextEditor.document.getText(), golden); + return Promise.resolve(); + }); + }); + }).then(() => done(), done); + }); });