diff --git a/.travis.yml b/.travis.yml index 06d569ad8..e6b357597 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,11 @@ install: - go get -u -v golang.org/x/tools/cmd/guru - go get -u -v github.com/alecthomas/gometalinter - gometalinter --install --update + - go get -u -v github.com/godoctor/godoctor script: - npm run lint - npm test --silent + +env: + - GO15VENDOREXPERIMENT=1 diff --git a/README.md b/README.md index c48d9d08b..29c5601ea 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This extension adds rich language support for the Go language to VS Code, includ - Format (using `goreturns` or `goimports` or `gofmt`) - Add Imports (using `gopkgs`) - [_partially implemented_] Debugging (using `delve`) +- Extract Method (using `godoctor`) ### IDE Features ![IDE](http://i.giphy.com/xTiTndDHV3GeIy6aNa.gif) @@ -168,6 +169,7 @@ The extension uses the following tools, installed in the current GOPATH. If any - gopkgs: `go get -u -v github.com/tpng/gopkgs` - go-symbols: `go get -u -v github.com/newhook/go-symbols` - guru: `go get -u -v golang.org/x/tools/cmd/guru` +- godoctor: `go get -u -v github.com/godoctor/godoctor` To install them just paste and run: ```bash @@ -180,8 +182,11 @@ go get -u -v golang.org/x/tools/cmd/gorename go get -u -v github.com/tpng/gopkgs go get -u -v github.com/newhook/go-symbols go get -u -v golang.org/x/tools/cmd/guru +go get -u -v github.com/godoctor/godoctor ``` +*Note*: If the version of Go you are using is less than 1.6, then `set GO15VENDOREXPERIMENT=1` to support godoctor + And for debugging: - delve: Follow the instructions at https://github.com/derekparker/delve/blob/master/Documentation/installation/README.md. diff --git a/package.json b/package.json index fdc95861d..eda48360d 100644 --- a/package.json +++ b/package.json @@ -106,8 +106,21 @@ "command": "go.import.add", "title": "Go: Add Import", "description": "Add an import declaration" + }, + { + "command": "go.method.extract", + "title": "Go: Extract Method", + "description": "Extract Method from current selection" } ], + "menus": { + "editor/context": [ + { + "when": "resourceLangId == go", + "command": "go.method.extract" + } + ] + }, "debuggers": [ { "type": "go", diff --git a/src/debugAdapter/goDebug.ts b/src/debugAdapter/goDebug.ts index cafa9392b..b06f5e547 100644 --- a/src/debugAdapter/goDebug.ts +++ b/src/debugAdapter/goDebug.ts @@ -228,17 +228,17 @@ class Delve { let str = chunk.toString(); if (this.onstderr) { this.onstderr(str); } if (!serverRunning) { - serverRunning = true; - connectClient(port, host); - } + serverRunning = true; + connectClient(port, host); + } }); this.debugProcess.stdout.on('data', chunk => { let str = chunk.toString(); if (this.onstdout) { this.onstdout(str); } if (!serverRunning) { - serverRunning = true; - connectClient(port, host); - } + serverRunning = true; + connectClient(port, host); + } }); this.debugProcess.on('close', function(code) { // TODO: Report `dlv` crash to user. diff --git a/src/goExtractMethod.ts b/src/goExtractMethod.ts new file mode 100644 index 000000000..297dffd64 --- /dev/null +++ b/src/goExtractMethod.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------*/ + +'use strict'; + +import { window, Position, Selection, Range, TextEditor } from 'vscode'; +import { getBinPath } from './goPath'; +import { EditTypes, Edit, GetEditsFromDiffs } from './util'; +import cp = require('child_process'); +import dmp = require('diff-match-patch'); + +/** + * Extracts method out of current selection and replaces the current selection with a call to the extracted method. + */ +export function extractMethod() { + + let editor = window.activeTextEditor; + if (!editor) { + window.showInformationMessage('No editor is active.'); + return; + } + if (editor.selections.length !== 1) { + window.showInformationMessage('You need to have a single selection for extracting method'); + return; + } + + let showInputBoxPromise = window.showInputBox({placeHolder: 'Please enter the name for the extracted method'}); + showInputBoxPromise.then((methodName: string) => { + extractMethodUsingGoDoctor(methodName, editor.selection, editor).then(errorMessage => { + if (errorMessage) { + window.showErrorMessage(errorMessage); + } + }); + }); +} + +/** + * Extracts method out of current selection and replaces the current selection with a call to the extracted method using godoctor. + * + * @param methodName name for the extracted method + * @param selection the editor selection from which method is to be extracted + * @param editor the editor that will be used to apply the changes from godoctor + * @returns errorMessage in case the method fails, null otherwise + */ +export function extractMethodUsingGoDoctor(methodName: string, selection: Selection, editor: TextEditor): Thenable { + let godoctor = getBinPath('godoctor'); + let position = `${selection.start.line + 1},${selection.start.character + 1}:${selection.end.line + 1},${selection.end.character + 1}`; + + return new Promise((resolve, reject) => { + let process = cp.execFile(godoctor, ['-pos', position, 'extract', methodName], {}, (err, stdout, stderr) => { + if (err) { + let errorMessageIndex = stderr.indexOf('Error:'); + return resolve(errorMessageIndex > -1 ? stderr.substr(errorMessageIndex) : stderr); + } + + let d = new dmp.diff_match_patch(); + let patchText = stdout.substr(stdout.indexOf('@@')); + let patches: dmp.Patch[]; + + try { + patches = d.patch_fromText(patchText); + } + catch (e) { + return resolve(`Failed to parse the patches from godoctor: ${e.message}`); + } + + applypatches(patches, editor).then(validEdit => { + return resolve (validEdit ? null : 'Edits could not be applied to the document'); + }); + + }); + process.stdin.end(editor.document.getText()); + }); +} + +/** + * Applies the given set of patches to the document in the given editor + * + * @param patches array of patches to be applied + * @param editor the TextEditor whose document will be updated + */ +function applypatches(patches: dmp.Patch[], editor: TextEditor): Thenable { + let totalEdits: Edit[] = []; + patches.reverse().forEach((patch: dmp.Patch) => { + // Godoctor provides a diff for each line, but the text accompanying the diff does not end with '\n' + // GetEditsFromDiffs(..) expects the '\n' to exist in the text wherever there is a new line. + // So add one for each diff from getdoctor + for (let i = 0; i < patch.diffs.length; i++) { + patch.diffs[i][1] += '\n'; + } + let edits = GetEditsFromDiffs(patch.diffs, patch.start1); + totalEdits = totalEdits.concat(edits); + }); + + return editor.edit((editBuilder) => { + totalEdits.forEach((edit) => { + switch (edit.action) { + case EditTypes.EDIT_INSERT: + editBuilder.insert(edit.start, edit.text); + break; + case EditTypes.EDIT_DELETE: + editBuilder.delete(new Range(edit.start, edit.end)); + break; + case EditTypes.EDIT_REPLACE: + editBuilder.replace(new Range(edit.start, edit.end), edit.text); + break; + } + }); + }); +} + + + diff --git a/src/goFormat.ts b/src/goFormat.ts index 3627cbf92..d0cad0701 100644 --- a/src/goFormat.ts +++ b/src/goFormat.ts @@ -11,33 +11,7 @@ import path = require('path'); import dmp = require('diff-match-patch'); import { getBinPath } from './goPath'; import { promptForMissingTool } from './goInstallTools'; - -let EDIT_DELETE = 0; -let EDIT_INSERT = 1; -let EDIT_REPLACE = 2; -class Edit { - action: number; - start: vscode.Position; - end: vscode.Position; - text: string; - - constructor(action: number, start: vscode.Position) { - this.action = action; - this.start = start; - this.text = ''; - } - - apply(): vscode.TextEdit { - switch (this.action) { - case EDIT_INSERT: - return vscode.TextEdit.insert(this.start, this.text); - case EDIT_DELETE: - return vscode.TextEdit.delete(new vscode.Range(this.start, this.end)); - case EDIT_REPLACE: - return vscode.TextEdit.replace(new vscode.Range(this.start, this.end), this.text); - } - } -} +import { EditTypes, Edit, GetEditsFromDiffs } from './util'; export class Formatter { private formatCommand = 'goreturns'; @@ -66,62 +40,18 @@ export class Formatter { let d = new dmp.diff_match_patch(); let diffs = d.diff_main(document.getText(), text); - let line = 0; - let character = 0; - let edits: vscode.TextEdit[] = []; - let edit: Edit = null; - - for (let i = 0; i < diffs.length; i++) { - let start = new vscode.Position(line, character); - - // Compute the line/character after the diff is applied. - for (let curr = 0; curr < diffs[i][1].length; curr++) { - if (diffs[i][1][curr] !== '\n') { - character++; - } else { - character = 0; - line++; - } - } - - switch (diffs[i][0]) { - case dmp.DIFF_DELETE: - if (edit == null) { - edit = new Edit(EDIT_DELETE, start); - } else if (edit.action !== EDIT_DELETE) { - return reject('cannot format due to an internal error.'); - } - edit.end = new vscode.Position(line, character); - break; - - case dmp.DIFF_INSERT: - if (edit == null) { - edit = new Edit(EDIT_INSERT, start); - } else if (edit.action === EDIT_DELETE) { - edit.action = EDIT_REPLACE; - } - // insert and replace edits are all relative to the original state - // of the document, so inserts should reset the current line/character - // position to the start. - line = start.line; - character = start.character; - edit.text += diffs[i][1]; - break; + let edits: Edit[] = GetEditsFromDiffs(diffs, 0); + let textEdits: vscode.TextEdit[] = []; - case dmp.DIFF_EQUAL: - if (edit != null) { - edits.push(edit.apply()); - edit = null; - } - break; - } + if (!edits) { + return reject('Cannot format due to internal errors'); } - if (edit != null) { - edits.push(edit.apply()); + for (let i = 0; i < edits.length; i++) { + textEdits.push(edits[i].apply()); } - return resolve(edits); + return resolve(textEdits); } catch (e) { reject(e); } diff --git a/src/goInstallTools.ts b/src/goInstallTools.ts index 0c18e2b7e..4b47d5e6f 100644 --- a/src/goInstallTools.ts +++ b/src/goInstallTools.ts @@ -23,7 +23,8 @@ let tools: { [key: string]: string } = { 'go-outline': 'github.com/lukehoban/go-outline', 'go-symbols': 'github.com/newhook/go-symbols', 'guru': 'golang.org/x/tools/cmd/guru', - 'gorename': 'golang.org/x/tools/cmd/gorename' + 'gorename': 'golang.org/x/tools/cmd/gorename', + 'godoctor': 'github.com/godoctor/godoctor' }; export function promptForMissingTool(tool: string) { @@ -109,6 +110,9 @@ export function setupGoPathAndOfferToInstallTools() { }); function promptForInstall(missing: string[]) { + // set GO15VENDOREXPERIMENT=1 to support godoctor when using Go v1.5 + process.env['GO15VENDOREXPERIMENT'] = 1; + let item = { title: 'Install', command() { diff --git a/src/goMain.ts b/src/goMain.ts index 1fbfcacc5..ff5df3ed7 100644 --- a/src/goMain.ts +++ b/src/goMain.ts @@ -26,6 +26,7 @@ import { showHideStatus } from './goStatus'; import { coverageCurrentPackage, getCodeCoverage, removeCodeCoverage } from './goCover'; import { testAtCursor, testCurrentPackage, testCurrentFile } from './goTest'; import { addImport } from './goImport'; +import { extractMethod } from './goExtractMethod'; let diagnosticCollection: vscode.DiagnosticCollection; @@ -79,6 +80,10 @@ export function activate(ctx: vscode.ExtensionContext): void { return addImport(typeof arg === 'string' ? arg : null); })); + ctx.subscriptions.push(vscode.commands.registerCommand('go.method.extract', () => { + extractMethod(); + })); + vscode.languages.setLanguageConfiguration(GO_MODE.language, { indentationRules: { // ^(.*\*/)?\s*\}.*$ diff --git a/src/util.ts b/src/util.ts index 826bcaf47..104068bed 100644 --- a/src/util.ts +++ b/src/util.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------*/ -import { TextDocument, Position } from 'vscode'; +import { TextDocument, Position, TextEdit, Range } from 'vscode'; import path = require('path'); +import dmp = require('diff-match-patch'); export function byteOffsetAt(document: TextDocument, position: Position): number { let offset = document.offsetAt(position); @@ -49,7 +50,7 @@ export function parseFilePrelude(text: string): Prelude { } // Takes a Go function signature like: -// (foo, bar string, baz number) (string, string) +// (foo, bar string, baz number) (string, string) // and returns an array of parameter strings: // ["foo", "bar string", "baz string"] // Takes care of balancing parens so to not get confused by signatures like: @@ -84,14 +85,105 @@ export function parameters(signature: string): string[] { } export function canonicalizeGOPATHPrefix(filename: string): string { - let gopath: string = process.env['GOPATH']; - if (!gopath) return filename; - let workspaces = gopath.split(path.delimiter); - let filenameLowercase = filename.toLowerCase(); - for (let workspace of workspaces) { - if (filenameLowercase.substring(0, workspace.length) === workspace.toLowerCase()) { - return workspace + filename.slice(workspace.length); + let gopath: string = process.env['GOPATH']; + if (!gopath) return filename; + let workspaces = gopath.split(path.delimiter); + let filenameLowercase = filename.toLowerCase(); + for (let workspace of workspaces) { + if (filenameLowercase.substring(0, workspace.length) === workspace.toLowerCase()) { + return workspace + filename.slice(workspace.length); + } + } + return filename; + } + +export enum EditTypes { EDIT_DELETE, EDIT_INSERT, EDIT_REPLACE}; + +export class Edit { + action: number; + start: Position; + end: Position; + text: string; + + constructor(action: number, start: Position) { + this.action = action; + this.start = start; + this.text = ''; + } + + apply(): TextEdit { + switch (this.action) { + case EditTypes.EDIT_INSERT: + return TextEdit.insert(this.start, this.text); + case EditTypes.EDIT_DELETE: + return TextEdit.delete(new Range(this.start, this.end)); + case EditTypes.EDIT_REPLACE: + return TextEdit.replace(new Range(this.start, this.end), this.text); } } - return filename; -} \ No newline at end of file +} + +/** + * Gets Edits from given diff array + * + * @param diffs The array of diffs which are translated to edits + * @param line The line number from where the edits are to be applied + * @returns Array of Edits that can be applied to the document + */ +export function GetEditsFromDiffs(diffs: dmp.Diff[], line: number): Edit[] { + let character = 0; + let edits: Edit[] = []; + let edit: Edit = null; + + for (let i = 0; i < diffs.length; i++) { + let start = new Position(line, character); + + // Compute the line/character after the diff is applied. + for (let curr = 0; curr < diffs[i][1].length; curr++) { + if (diffs[i][1][curr] !== '\n') { + character++; + } else { + character = 0; + line++; + } + } + + switch (diffs[i][0]) { + case dmp.DIFF_DELETE: + if (edit == null) { + edit = new Edit(EditTypes.EDIT_DELETE, start); + } else if (edit.action !== EditTypes.EDIT_DELETE) { + return null; + } + edit.end = new Position(line, character); + break; + + case dmp.DIFF_INSERT: + if (edit == null) { + edit = new Edit(EditTypes.EDIT_INSERT, start); + } else if (edit.action === EditTypes.EDIT_DELETE) { + edit.action = EditTypes.EDIT_REPLACE; + } + // insert and replace edits are all relative to the original state + // of the document, so inserts should reset the current line/character + // position to the start. + line = start.line; + character = start.character; + edit.text += diffs[i][1]; + break; + + case dmp.DIFF_EQUAL: + if (edit != null) { + edits.push(edit); + edit = null; + } + break; + } + } + + if (edit != null) { + edits.push(edit); + } + + return edits; +} diff --git a/test/fixtures/expectedAfterExtractMethod.go b/test/fixtures/expectedAfterExtractMethod.go new file mode 100644 index 000000000..7015d57f4 --- /dev/null +++ b/test/fixtures/expectedAfterExtractMethod.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" +) + +func print(txt string) { + fmt.Println(txt) +} +func main() { + print("Hello") + + a := 1 + b := 2 + add(a, b) +} +func add(a int, b int) { + c := a + b + fmt.Println(c) +} diff --git a/test/fixtures/test.go b/test/fixtures/test.go index d8c3d9379..71805d20e 100644 --- a/test/fixtures/test.go +++ b/test/fixtures/test.go @@ -9,4 +9,9 @@ func print(txt string) { } func main() { print("Hello") + + a:= 1 + b:=2 + c:= a+b + fmt.Println(c) } diff --git a/test/go.test.ts b/test/go.test.ts index c05c97a98..096adafae 100644 --- a/test/go.test.ts +++ b/test/go.test.ts @@ -11,6 +11,7 @@ import { GoHoverProvider } from '../src/goExtraInfo'; import { GoCompletionItemProvider } from '../src/goSuggest'; import { GoSignatureHelpProvider } from '../src/goSignature'; import { check } from '../src/goCheck'; +import { extractMethodUsingGoDoctor } from '../src/goExtractMethod'; suite('Go Extension Tests', () => { let gopath = process.env['GOPATH']; @@ -147,4 +148,45 @@ suite('Go Extension Tests', () => { assert.equal(sortedDiagnostics.length, expected.length, `too many errors ${JSON.stringify(sortedDiagnostics)}`); }).then(() => done(), done); }); + + test('Extract method fails due to invalid method name', (done) => { + let uri = vscode.Uri.file(path.join(fixtureSourcePath, 'test.go')); + let selection = new vscode.Selection(14, 0, 15, 14); + + vscode.workspace.openTextDocument(uri).then((textDocument) => { + return vscode.window.showTextDocument(textDocument).then(editor => { + return extractMethodUsingGoDoctor('a dd', selection, editor).then((retValue) => { + assert.equal(retValue.indexOf('is not a valid Go identifier') > -1, true); + }); + }); + }).then(() => done(), done); + }); + + test('Extract method fails due to invalid selection', (done) => { + let uri = vscode.Uri.file(path.join(fixtureSourcePath, 'test.go')); + let selection = new vscode.Selection(2, 0, 5, 0); + + vscode.workspace.openTextDocument(uri).then((textDocument) => { + return vscode.window.showTextDocument(textDocument).then(editor => { + return extractMethodUsingGoDoctor('add', selection, editor).then((retValue) => { + assert.equal(retValue.indexOf('invalid selection') > -1, true); + }); + }); + }).then(() => done(), done); + }); + + test('Extract method successfully', (done) => { + let uri = vscode.Uri.file(path.join(fixtureSourcePath, 'test.go')); + let expectedOutput = fs.readFileSync(path.join(fixtureSourcePath, 'expectedAfterExtractMethod.go'), 'utf8'); + let selection = new vscode.Selection(14, 0, 15, 14); + + vscode.workspace.openTextDocument(uri).then((textDocument) => { + return vscode.window.showTextDocument(textDocument).then(editor => { + return extractMethodUsingGoDoctor('add', selection, editor).then((done) => { + assert.equal(editor.document.getText(), expectedOutput); + }); + }); + }).then(() => done(), done); + }); + });