Skip to content
This repository has been archived by the owner on Jul 15, 2023. It is now read-only.

Fixes #13 Rename using diffs and parse diffs using diff-parse/diff npm modules #477

Merged
merged 14 commits into from
Sep 30, 2016
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
],
"dependencies": {
"console-stamp": "^0.2.2",
"diff-match-patch": "^1.0.0",
"diff": "~3.0.0",
"json-rpc2": "^1.0.2",
"vscode-debugadapter": "^1.11.0",
"vscode-debugprotocol": "^1.11.0"
Expand Down
172 changes: 172 additions & 0 deletions src/diffUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------*/

import { TextDocument, Position, Range, TextEdit, Uri, WorkspaceEdit, TextEditorEdit } from 'vscode';
import { getBinPathFromEnvVar } from '../src/goPath';
import jsDiff = require('diff');

let diffToolAvailable: boolean = null;

export function isDiffToolAvailable(): boolean {
if (diffToolAvailable == null) {
diffToolAvailable = getBinPathFromEnvVar('diff', 'PATH', false) != null;
}
return diffToolAvailable;
}

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 = '';
}

// Creates TextEdit for current Edit
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);
}
}

// Applies Edit using given TextEditorEdit
applyUsingTextEditorEdit(editBuilder: TextEditorEdit): void {
switch (this.action) {
case EditTypes.EDIT_INSERT:
editBuilder.insert(this.start, this.text);
break;

case EditTypes.EDIT_DELETE:
editBuilder.delete(new Range(this.start, this.end));
break;

case EditTypes.EDIT_REPLACE:
editBuilder.replace(new Range(this.start, this.end), this.text);
break;
}
}

// Applies Edits to given WorkspaceEdit
applyUsingWorkspaceEdit(workspaceEdit: WorkspaceEdit, fileUri: Uri): void {
switch (this.action) {
case EditTypes.EDIT_INSERT:
workspaceEdit.insert(fileUri, this.start, this.text);
break;

case EditTypes.EDIT_DELETE:
workspaceEdit.delete(fileUri, new Range(this.start, this.end));
break;

case EditTypes.EDIT_REPLACE:
workspaceEdit.replace(fileUri, new Range(this.start, this.end), this.text);
break;
}
}
}

export interface FilePatch {
fileName: string;
edits: Edit[];
}

/**
* Uses diff module to parse given array of IUniDiff objects and returns edits for files
*
* @param diffOutput jsDiff.IUniDiff[]
*
* @returns Array of FilePatch objects, one for each file
*/
function parseUniDiffs(diffOutput: jsDiff.IUniDiff[]): FilePatch[] {
let filePatches: FilePatch[] = [];
diffOutput.forEach((uniDiff: jsDiff.IUniDiff) => {
let edit: Edit = null;
let edits: Edit[] = [];
uniDiff.hunks.forEach((hunk: jsDiff.IHunk) => {
let startLine = hunk.oldStart;
hunk.lines.forEach((line) => {
switch (line.substr(0, 1)) {
case '-':
if (edit == null) {
edit = new Edit(EditTypes.EDIT_DELETE, new Position(startLine - 1, 0));
}
edit.end = new Position(startLine, 0);
startLine++;
break;
case '+':
if (edit == null) {
edit = new Edit(EditTypes.EDIT_INSERT, new Position(startLine - 1, 0));
} else if (edit.action === EditTypes.EDIT_DELETE) {
edit.action = EditTypes.EDIT_REPLACE;
}
edit.text += line.substr(1) + '\n';
break;
case ' ':
startLine++;
if (edit != null) {
edits.push(edit);
}
edit = null;
break;
}
});
if (edit != null) {
edits.push(edit);
}
});
filePatches.push({fileName: uniDiff.oldFileName, edits: edits});
});

return filePatches;

}

/**
* Returns a FilePatch object by generating diffs between given oldStr and newStr using the diff module
*
* @param fileName string: Name of the file to which edits should be applied
* @param oldStr string
* @param newStr string
*
* @returns A single FilePatch object
*/
export function getEdits(fileName: string, oldStr: string, newStr: string): FilePatch {
if (process.platform === 'win32') {
oldStr = oldStr.split('\r\n').join('\n');
newStr = newStr.split('\r\n').join('\n');
}
let unifiedDiffs: jsDiff.IUniDiff = jsDiff.structuredPatch(fileName, fileName, oldStr, newStr, '', '');
let filePatches: FilePatch[] = parseUniDiffs([unifiedDiffs]);
return filePatches[0];
}

/**
* Uses diff module to parse given diff string and returns edits for files
*
* @param diffStr : Diff string in unified format. http://www.gnu.org/software/diffutils/manual/diffutils.html#Unified-Format
*
* @returns Array of FilePatch objects, one for each file
*/
export function getEditsFromUnifiedDiffStr(diffstr: string): FilePatch[] {
// Workaround for the bug https://github.com/kpdecker/jsdiff/issues/135
if (diffstr.startsWith('---')) {
diffstr = diffstr.split('---').join('Index\n---');
}
let unifiedDiffs: jsDiff.IUniDiff[] = jsDiff.parsePatch(diffstr);
let filePatches: FilePatch[] = parseUniDiffs(unifiedDiffs);
return filePatches;
}
98 changes: 12 additions & 86 deletions src/goFormat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,10 @@
import vscode = require('vscode');
import cp = require('child_process');
import path = require('path');
import dmp = require('diff-match-patch');
import { isDiffToolAvailable, getEdits, getEditsFromUnifiedDiffStr } from '../src/diffUtils';
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);
}
}
}

export class Formatter {
private formatCommand = 'goreturns';

Expand All @@ -55,6 +28,10 @@ export class Formatter {

let formatCommandBinPath = getBinPath(this.formatCommand);
let formatFlags = vscode.workspace.getConfiguration('go')['formatFlags'] || [];
let canFormatToolUseDiff = isDiffToolAvailable();
if (canFormatToolUseDiff) {
formatFlags.push('-d');
}

cp.execFile(formatCommandBinPath, [...formatFlags, filename], {}, (err, stdout, stderr) => {
try {
Expand All @@ -63,68 +40,17 @@ export class Formatter {
return resolve(null);
}
if (err) return reject('Cannot format due to syntax errors.');
let text = stdout.toString();
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;
let textEdits: vscode.TextEdit[] = [];
let filePatch = canFormatToolUseDiff ? getEditsFromUnifiedDiffStr(stdout)[0] : getEdits(filename, document.getText(), stdout);

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;

case dmp.DIFF_EQUAL:
if (edit != null) {
edits.push(edit.apply());
edit = null;
}
break;
}
}

if (edit != null) {
edits.push(edit.apply());
}
filePatch.edits.forEach((edit) => {
textEdits.push(edit.apply());
});

return resolve(edits);
return resolve(textEdits);
} catch (e) {
reject(e);
reject('Internal issues while getting diff from formatted content');
}
});
});
Expand Down
49 changes: 24 additions & 25 deletions src/goPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,40 @@ import os = require('os');
let binPathCache: { [bin: string]: string; } = {};
let runtimePathCache: string = null;

export function getBinPath(binname: string) {
binname = correctBinname(binname);
if (binPathCache[binname]) return binPathCache[binname];

// First search each GOPATH workspace's bin folder
if (process.env['GOPATH']) {
let workspaces = process.env['GOPATH'].split(path.delimiter);
for (let i = 0; i < workspaces.length; i++) {
let binpath = path.join(workspaces[i], 'bin', binname);
export function getBinPathFromEnvVar(toolName: string, envVar: string, appendBinToPath: boolean): string {
toolName = correctBinname(toolName);
if (process.env[envVar]) {
let paths = process.env[envVar].split(path.delimiter);
for (let i = 0; i < paths.length; i++) {
let binpath = path.join(paths[i], appendBinToPath ? 'bin' : '', toolName);
if (fs.existsSync(binpath)) {
binPathCache[binname] = binpath;
binPathCache[toolName] = binpath;
return binpath;
}
}
}
return null;
}

export function getBinPath(binname: string) {
if (binPathCache[correctBinname(binname)]) return binPathCache[correctBinname(binname)];

// First search each GOPATH workspace's bin folder
let pathFromGoPath = getBinPathFromEnvVar(binname, 'GOPATH', true);
if (pathFromGoPath) {
return pathFromGoPath;
}

// Then search PATH parts
if (process.env['PATH']) {
let pathparts = process.env['PATH'].split(path.delimiter);
for (let i = 0; i < pathparts.length; i++) {
let binpath = path.join(pathparts[i], binname);
if (fs.existsSync(binpath)) {
binPathCache[binname] = binpath;
return binpath;
}
}
let pathFromPath = getBinPathFromEnvVar(binname, 'PATH', false);
if (pathFromPath) {
return pathFromPath;
}

// Finally check GOROOT just in case
if (process.env['GOROOT']) {
let binpath = path.join(process.env['GOROOT'], 'bin', binname);
if (fs.existsSync(binpath)) {
binPathCache[binname] = binpath;
return binpath;
}
let pathFromGoRoot = getBinPathFromEnvVar(binname, 'GOROOT', true);
if (pathFromGoRoot) {
return pathFromGoRoot;
}

// Else return the binary name directly (this will likely always fail downstream)
Expand Down
Loading