Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Commit 906a7c1

Browse files
authored
Merge pull request #879 from atom/ku-open-diff-from-editor
Open diff from editor
2 parents 3b29992 + b33444a commit 906a7c1

11 files changed

Lines changed: 202 additions & 6 deletions

lib/controllers/file-patch-controller.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,10 @@ export default class FilePatchController extends React.Component {
267267
return this.props.repository.hasDiscardHistory(this.props.filePatch.getPath());
268268
}
269269

270+
goToDiffLine(lineNumber) {
271+
return this.filePatchView.goToDiffLine(lineNumber);
272+
}
273+
270274
/**
271275
* Used to detect the context when this PaneItem is active
272276
*/

lib/controllers/git-tab-controller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ export default class GitTabController {
364364

365365
@autobind
366366
quietlySelectItem(filePath, stagingStatus) {
367-
return this.refs.gitTab.quitelySelectItem(filePath, stagingStatus);
367+
return this.refs.gitTab.quietlySelectItem(filePath, stagingStatus);
368368
}
369369

370370
@autobind

lib/controllers/root-controller.js

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ export default class RootController extends React.Component {
146146
<Command command="github:toggle-github-tab" callback={this.githubTabTracker.toggle} />
147147
<Command command="github:toggle-github-tab-focus" callback={this.githubTabTracker.toggleFocus} />
148148
<Command command="github:clone" callback={this.openCloneDialog} />
149+
<Command
150+
command="github:view-unstaged-changes-for-current-file"
151+
callback={this.viewUnstagedChangesForCurrentFile}
152+
/>
153+
<Command
154+
command="github:view-staged-changes-for-current-file"
155+
callback={this.viewStagedChangesForCurrentFile}
156+
/>
149157
</Commands>
150158
{this.renderStatusBarTile()}
151159
{this.renderPanels()}
@@ -201,6 +209,7 @@ export default class RootController extends React.Component {
201209
didDiveIntoFilePath={this.diveIntoFilePatchForPath}
202210
didSelectMergeConflictFile={this.showMergeConflictFileForPath}
203211
didDiveIntoMergeConflictPath={this.diveIntoMergeConflictFileForPath}
212+
showFilePatchForPath={this.showFilePatchForPath}
204213
didChangeAmending={this.didChangeAmending}
205214
focusFilePatchView={this.focusFilePatchView}
206215
ensureGitTab={this.gitTabTracker.ensureVisible}
@@ -290,6 +299,7 @@ export default class RootController extends React.Component {
290299
ref={c => { this.filePatchControllerPane = c; }}
291300
onDidCloseItem={() => { this.setState({...nullFilePatchState}); }}>
292301
<FilePatchController
302+
ref={c => { this.filePatchController = c; }}
293303
activeWorkingDirectory={this.props.activeWorkingDirectory}
294304
repository={this.props.repository}
295305
commandRegistry={this.props.commandRegistry}
@@ -474,7 +484,7 @@ export default class RootController extends React.Component {
474484
}
475485

476486
@autobind
477-
async showFilePatchForPath(filePath, stagingStatus, {activate, amending} = {}) {
487+
async showFilePatchForPath(filePath, stagingStatus, {activate, amending, lineNumber} = {}) {
478488
if (!filePath) { return null; }
479489
const repository = this.props.repository;
480490
if (!repository) { return null; }
@@ -488,6 +498,9 @@ export default class RootController extends React.Component {
488498
// TODO: can be better done w/ a prop?
489499
if (activate && this.filePatchControllerPane) {
490500
this.filePatchControllerPane.activate();
501+
if (lineNumber) {
502+
this.filePatchController.goToDiffLine(lineNumber);
503+
}
491504
}
492505
this.props.switchboard.didFinishRender('RootController.showFilePatchForPath');
493506
resolve();
@@ -502,8 +515,8 @@ export default class RootController extends React.Component {
502515
}
503516

504517
@autobind
505-
async diveIntoFilePatchForPath(filePath, stagingStatus, {amending} = {}) {
506-
await this.showFilePatchForPath(filePath, stagingStatus, {activate: true, amending});
518+
async diveIntoFilePatchForPath(filePath, stagingStatus, options = {}) {
519+
await this.showFilePatchForPath(filePath, stagingStatus, {...options, activate: true});
507520
this.focusFilePatchView();
508521
}
509522

@@ -574,6 +587,44 @@ export default class RootController extends React.Component {
574587
viewElement.focus();
575588
}
576589

590+
viewChangesForCurrentFile(stagingStatus) {
591+
const editor = this.props.workspace.getActiveTextEditor();
592+
const absFilePath = editor.getPath();
593+
const repoPath = this.props.repository.getWorkingDirectoryPath();
594+
if (absFilePath.startsWith(repoPath)) {
595+
const filePath = absFilePath.slice(repoPath.length + 1);
596+
this.quietlySelectItem(filePath, stagingStatus);
597+
const splitDirection = this.props.config.get('github.viewChangesForCurrentFileDiffPaneSplitDirection');
598+
const pane = atom.workspace.getActivePane();
599+
if (splitDirection === 'right') {
600+
pane.splitRight();
601+
} else if (splitDirection === 'down') {
602+
pane.splitDown();
603+
}
604+
return this.diveIntoFilePatchForPath(filePath, stagingStatus, {
605+
lineNumber: editor.getCursorBufferPosition().row + 1,
606+
amending: stagingStatus === 'staged' && this.state.isAmending,
607+
});
608+
} else {
609+
throw new Error(`${absFilePath} does not belong to repo ${repoPath}`);
610+
}
611+
}
612+
613+
@autobind
614+
viewUnstagedChangesForCurrentFile() {
615+
this.viewChangesForCurrentFile('unstaged');
616+
}
617+
618+
@autobind
619+
viewStagedChangesForCurrentFile() {
620+
this.viewChangesForCurrentFile('staged');
621+
}
622+
623+
@autobind
624+
goToDiffLine(lineNumber) {
625+
return this.filePatchController.goToDiffLine(lineNumber);
626+
}
627+
577628
@autobind
578629
openFiles(filePaths) {
579630
return Promise.all(filePaths.map(filePath => {

lib/views/file-patch-selection.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,29 @@ export default class FilePatchSelection {
324324
getLineSelectionTailIndex() {
325325
return this.linesSelection.getTailIndex();
326326
}
327+
328+
goToDiffLine(lineNumber) {
329+
const lines = this.linesSelection.getItems();
330+
331+
let closestLine;
332+
let closestLineDistance = Infinity;
333+
334+
for (let i = 0; i < lines.length; i++) {
335+
const line = lines[i];
336+
if (!this.linesSelection.isItemSelectable(line)) { continue; }
337+
if (line.newLineNumber === lineNumber) {
338+
return this.selectLine(line);
339+
} else {
340+
const newDistance = Math.abs(line.newLineNumber - lineNumber);
341+
if (newDistance < closestLineDistance) {
342+
closestLineDistance = newDistance;
343+
closestLine = line;
344+
} else {
345+
return this.selectLine(closestLine);
346+
}
347+
}
348+
}
349+
350+
return this.selectLine(closestLine);
351+
}
327352
}

lib/views/file-patch-view.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,4 +468,8 @@ export default class FilePatchView extends React.Component {
468468
const selectedLines = this.state.selection.getSelectedLines();
469469
return selectedLines.size ? this.props.discardLines(selectedLines) : null;
470470
}
471+
472+
goToDiffLine(lineNumber) {
473+
this.setState(prevState => ({selection: prevState.selection.goToDiffLine(lineNumber)}));
474+
}
471475
}

lib/views/git-tab-view.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export default class GitTabView {
182182
}
183183

184184
@autobind
185-
quitelySelectItem(filePath, stagingStatus) {
185+
quietlySelectItem(filePath, stagingStatus) {
186186
return this.refs.stagingView.quietlySelectItem(filePath, stagingStatus);
187187
}
188188
}

lib/views/hunk-view.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,14 @@ export default class HunkView extends React.Component {
9595
}
9696

9797
componentDidUpdate() {
98-
if (this.props.headHunk === this.props.hunk) {
98+
const selectedLine = Array.from(this.props.selectedLines)[0];
99+
if (selectedLine && this.lineElements.get(selectedLine)) {
100+
// QUESTION: why is this setTimeout needed?
101+
const element = this.lineElements.get(selectedLine);
102+
setTimeout(() => {
103+
element.scrollIntoViewIfNeeded();
104+
}, 0);
105+
} else if (this.props.headHunk === this.props.hunk) {
99106
this.element.scrollIntoViewIfNeeded();
100107
} else if (this.props.headLine && this.lineElements.has(this.props.headLine)) {
101108
this.lineElements.get(this.props.headLine).scrollIntoViewIfNeeded();

menus/git.cson

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,13 @@
132132
'command': 'github:unstage-all-changes'
133133
}
134134
]
135+
'atom-text-editor': [
136+
{
137+
'label': 'View Unstaged Changes',
138+
'command': 'github:view-unstaged-changes-for-current-file'
139+
}
140+
{
141+
'label': 'View Staged Changes',
142+
'command': 'github:view-staged-changes-for-current-file'
143+
}
144+
]

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,17 @@
111111
"default": 300,
112112
"description": "How long to wait before showing a diff view after navigating to an entry with the keyboard"
113113
},
114+
"viewChangesForCurrentFileDiffPaneSplitDirection": {
115+
"type": "string",
116+
"default": "none",
117+
"enum": [
118+
"none",
119+
"right",
120+
"down"
121+
],
122+
"title": "Direction to open diff pane",
123+
"description": "Direction to split the active pane when showing diff associated with open file. If 'none', the results will be shown in the active pane."
124+
},
114125
"gitDiagnostics": {
115126
"type": "boolean",
116127
"default": false,

test/controllers/root-controller.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,49 @@ import RootController from '../../lib/controllers/root-controller';
11581158
assert.isDefined(paneItem);
11591159
assert.equal(paneItem.getTitle(), 'Unstaged Changes: a.txt');
11601160
});
1161+
1162+
describe('viewing diffs from active editor', function() {
1163+
describe('viewUnstagedChangesForCurrentFile()', function() {
1164+
it('opens the unstaged changes diff view associated with the active editor and selects the closest hunk line according to cursor position', async function() {
1165+
const workdirPath = await cloneRepository('three-files');
1166+
const repository = await buildRepository(workdirPath);
1167+
const wrapper = mount(React.cloneElement(app, {repository}));
1168+
1169+
fs.writeFileSync(path.join(workdirPath, 'a.txt'), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].join('\n'));
1170+
1171+
const editor = await workspace.open(path.join(workdirPath, 'a.txt'));
1172+
editor.setCursorBufferPosition([7, 0]);
1173+
1174+
await wrapper.instance().viewUnstagedChangesForCurrentFile();
1175+
1176+
await assert.async.equal(workspace.getActivePaneItem().getTitle(), 'Unstaged Changes: a.txt');
1177+
const selectedLines = Array.from(workspace.getActivePaneItem().getInstance().filePatchView.getSelectedLines());
1178+
assert.equal(selectedLines.length, 1);
1179+
assert.equal(selectedLines[0].getText(), '7');
1180+
});
1181+
});
1182+
1183+
describe('viewStagedChangesForCurrentFile()', function() {
1184+
it('opens the staged changes diff view associated with the active editor and selects the closest hunk line according to cursor position', async function() {
1185+
const workdirPath = await cloneRepository('three-files');
1186+
const repository = await buildRepository(workdirPath);
1187+
const wrapper = mount(React.cloneElement(app, {repository}));
1188+
1189+
fs.writeFileSync(path.join(workdirPath, 'a.txt'), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].join('\n'));
1190+
await repository.stageFiles(['a.txt']);
1191+
1192+
const editor = await workspace.open(path.join(workdirPath, 'a.txt'));
1193+
editor.setCursorBufferPosition([7, 0]);
1194+
1195+
await wrapper.instance().viewStagedChangesForCurrentFile();
1196+
1197+
await assert.async.equal(workspace.getActivePaneItem().getTitle(), 'Staged Changes: a.txt');
1198+
const selectedLines = Array.from(workspace.getActivePaneItem().getInstance().filePatchView.getSelectedLines());
1199+
assert.equal(selectedLines.length, 1);
1200+
assert.equal(selectedLines[0].getText(), '7');
1201+
});
1202+
});
1203+
});
11611204
});
11621205
});
11631206
});

0 commit comments

Comments
 (0)