this.props.mousedownOnHeader()}>
+ onmousedown={e => this.props.mousedownOnHeader(e)}>
{this.props.hunk.getHeader().trim()} {this.props.hunk.getSectionHeading().trim()}
@@ -62,6 +62,7 @@ export default class HunkView {
registerLineElement={this.registerLineElement}
mousedown={this.mousedownOnLine}
mousemove={this.mousemoveOnLine}
+ contextMenuOnItem={(e, clickedLine) => this.props.contextMenuOnItem(e, this.props.hunk, clickedLine)}
/>,
)}
this.props.mousedown(event, line)}
- onmousemove={event => this.props.mousemove(event, line)}>
+ onmousemove={event => this.props.mousemove(event, line)}
+ oncontextmenu={event => this.props.contextMenuOnItem(event, line)}>
{oldLineNumber}
{newLineNumber}
diff --git a/lib/views/list-selection.js b/lib/views/list-selection.js
index ce59d13e11..b5d70aff05 100644
--- a/lib/views/list-selection.js
+++ b/lib/views/list-selection.js
@@ -209,4 +209,8 @@ export default class ListSelection {
const selection = this.selections[0];
return Math.min(selection.head, selection.tail);
}
+
+ getTailIndex() {
+ return this.selections[0] ? this.selections[0].tail : null;
+ }
}
diff --git a/menus/git.cson b/menus/git.cson
index dd621fb69e..99cd8cce19 100644
--- a/menus/git.cson
+++ b/menus/git.cson
@@ -58,3 +58,15 @@
'command': 'github:open-file'
}
]
+ '.github-FilePatchView.is-staged': [
+ {
+ 'label': 'Unstage Selection'
+ 'command': 'core:confirm'
+ }
+ ]
+ '.github-FilePatchView.is-unstaged': [
+ {
+ 'label': 'Stage Selection'
+ 'command': 'core:confirm'
+ }
+ ]
diff --git a/styles/hunk-view.less b/styles/hunk-view.less
index e3337bf40d..23cbde76df 100644
--- a/styles/hunk-view.less
+++ b/styles/hunk-view.less
@@ -73,6 +73,9 @@
// Highlight hunk title
.github-HunkView {
+ &.is-selected.is-hunkMode &-header {
+ background-color: @background-color-selected;
+ }
&.is-selected.is-hunkMode &-title {
color: @text-color-selected;
}
diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js
index 854df7a9ad..8ff5b5ce87 100644
--- a/test/controllers/file-patch-controller.test.js
+++ b/test/controllers/file-patch-controller.test.js
@@ -136,7 +136,7 @@ describe('FilePatchController', function() {
let hunk = unstagedFilePatch.getHunks()[0];
let lines = hunk.getLines();
let hunkView = hunkViewsByHunk.get(hunk);
- hunkView.props.mousedownOnLine({detail: 1}, hunk, lines[1]);
+ hunkView.props.mousedownOnLine({button: 0, detail: 1}, hunk, lines[1]);
hunkView.props.mousemoveOnLine({}, hunk, lines[3]);
view.mouseup();
await hunkView.props.didClickStageButton();
@@ -169,9 +169,9 @@ describe('FilePatchController', function() {
hunk = stagedFilePatch.getHunks()[0];
lines = hunk.getLines();
hunkView = hunkViewsByHunk.get(hunk);
- hunkView.props.mousedownOnLine({detail: 1}, hunk, lines[1]);
+ hunkView.props.mousedownOnLine({button: 0, detail: 1}, hunk, lines[1]);
view.mouseup();
- hunkView.props.mousedownOnLine({detail: 1, metaKey: true}, hunk, lines[2]);
+ hunkView.props.mousedownOnLine({button: 0, detail: 1, metaKey: true}, hunk, lines[2]);
view.mouseup();
await hunkView.props.didClickStageButton();
@@ -261,7 +261,7 @@ describe('FilePatchController', function() {
let hunk = unstagedFilePatch.getHunks()[0];
let lines = hunk.getLines();
let hunkView = hunkViewsByHunk.get(hunk);
- hunkView.props.mousedownOnLine({detail: 1}, hunk, lines[1]);
+ hunkView.props.mousedownOnLine({button: 0, detail: 1}, hunk, lines[1]);
view.mouseup();
// stage lines in rapid succession
@@ -286,7 +286,7 @@ describe('FilePatchController', function() {
hunk = modifiedFilePatch.getHunks()[0];
lines = hunk.getLines();
hunkView = hunkViewsByHunk.get(hunk);
- hunkView.props.mousedownOnLine({detail: 1}, hunk, lines[2]);
+ hunkView.props.mousedownOnLine({button: 0, detail: 1}, hunk, lines[2]);
view.mouseup();
const line2StagingPromises = hunkView.props.didClickStageButton();
diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js
index 64aca20905..8ae7226600 100644
--- a/test/views/file-patch-view.test.js
+++ b/test/views/file-patch-view.test.js
@@ -16,88 +16,269 @@ describe('FilePatchView', function() {
atomEnv.destroy();
});
- it('allows lines and hunks to be selected via the mouse', async function() {
- const hunks = [
- new Hunk(1, 1, 2, 4, '', [
- new HunkLine('line-1', 'unchanged', 1, 1),
+ describe('mouse selection', () => {
+ it('allows lines and hunks to be selected via mouse drag', async function() {
+ const hunks = [
+ new Hunk(1, 1, 2, 4, '', [
+ new HunkLine('line-1', 'unchanged', 1, 1),
+ new HunkLine('line-2', 'added', -1, 2),
+ new HunkLine('line-3', 'added', -1, 3),
+ new HunkLine('line-4', 'unchanged', 2, 4),
+ ]),
+ new Hunk(5, 7, 1, 4, '', [
+ new HunkLine('line-5', 'unchanged', 5, 7),
+ new HunkLine('line-6', 'added', -1, 8),
+ new HunkLine('line-7', 'added', -1, 9),
+ new HunkLine('line-8', 'added', -1, 10),
+ ]),
+ ];
+ const hunkViews = new Map();
+ function registerHunkView(hunk, view) { hunkViews.set(hunk, view); }
+
+ const filePatchView = new FilePatchView({commandRegistry, hunks, registerHunkView});
+ const hunkView0 = hunkViews.get(hunks[0]);
+ const hunkView1 = hunkViews.get(hunks[1]);
+
+ // drag a selection
+ await hunkView0.props.mousedownOnLine({button: 0, detail: 1}, hunks[0], hunks[0].lines[2]);
+ await hunkViews.get(hunks[1]).props.mousemoveOnLine({}, hunks[1], hunks[1].lines[1]);
+ await filePatchView.mouseup();
+ assert(hunkView0.props.isSelected);
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
+ assert(hunkView1.props.isSelected);
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[1]));
+
+ // start a new selection, drag it across an existing selection
+ await hunkView1.props.mousedownOnLine({button: 0, detail: 1, metaKey: true}, hunks[1], hunks[1].lines[3]);
+ await hunkView0.props.mousemoveOnLine({}, hunks[0], hunks[0].lines[0]);
+ assert(hunkView0.props.isSelected);
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[1]));
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
+ assert(hunkView1.props.isSelected);
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[1]));
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[2]));
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[3]));
+
+ // drag back down without releasing mouse; the other selection remains intact
+ await hunkView1.props.mousemoveOnLine({}, hunks[1], hunks[1].lines[3]);
+ assert(hunkView0.props.isSelected);
+ assert(!hunkView0.props.selectedLines.has(hunks[0].lines[1]));
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
+ assert(hunkView1.props.isSelected);
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[1]));
+ assert(!hunkView1.props.selectedLines.has(hunks[1].lines[2]));
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[3]));
+
+ // drag back up so selections are adjacent, then release the mouse. selections should merge.
+ await hunkView1.props.mousemoveOnLine({}, hunks[1], hunks[1].lines[2]);
+ await filePatchView.mouseup();
+ assert(hunkView0.props.isSelected);
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
+ assert(hunkView1.props.isSelected);
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[1]));
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[2]));
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[3]));
+
+ // we detect merged selections based on the head here
+ await filePatchView.selectToNext();
+ assert(!hunkView0.props.isSelected);
+ assert(!hunkView0.props.selectedLines.has(hunks[0].lines[2]));
+
+ // double-clicking clears the existing selection and starts hunk-wise selection
+ await hunkView0.props.mousedownOnLine({button: 0, detail: 2}, hunks[0], hunks[0].lines[2]);
+ assert(hunkView0.props.isSelected);
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[1]));
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
+ assert(!hunkView1.props.isSelected);
+
+ await hunkView1.props.mousemoveOnLine({}, hunks[1], hunks[1].lines[1]);
+ assert(hunkView0.props.isSelected);
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[1]));
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
+ assert(hunkView1.props.isSelected);
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[1]));
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[2]));
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[3]));
+
+ // clicking the header clears the existing selection and starts hunk-wise selection
+ await hunkView0.props.mousedownOnHeader({button: 0, detail: 1}, hunks[0], hunks[0].lines[2]);
+ assert(hunkView0.props.isSelected);
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[1]));
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
+ assert(!hunkView1.props.isSelected);
+
+ await hunkView1.props.mousemoveOnLine({}, hunks[1], hunks[1].lines[1]);
+ assert(hunkView0.props.isSelected);
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[1]));
+ assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
+ assert(hunkView1.props.isSelected);
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[1]));
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[2]));
+ assert(hunkView1.props.selectedLines.has(hunks[1].lines[3]));
+ });
+
+ it('allows lines and hunks to be selected via cmd-clicking', async () => {
+ const hunk0 = new Hunk(1, 1, 2, 4, '', [
+ new HunkLine('line-1', 'added', -1, 1),
new HunkLine('line-2', 'added', -1, 2),
new HunkLine('line-3', 'added', -1, 3),
- new HunkLine('line-4', 'unchanged', 2, 4),
- ]),
- new Hunk(5, 7, 1, 4, '', [
- new HunkLine('line-5', 'unchanged', 5, 7),
+ ]);
+ const hunk1 = new Hunk(5, 7, 1, 4, '', [
+ new HunkLine('line-5', 'added', -1, 7),
new HunkLine('line-6', 'added', -1, 8),
new HunkLine('line-7', 'added', -1, 9),
new HunkLine('line-8', 'added', -1, 10),
- ]),
- ];
- const hunkViews = new Map();
- function registerHunkView(hunk, view) { hunkViews.set(hunk, view); }
-
- const filePatchView = new FilePatchView({commandRegistry, hunks, registerHunkView});
- const hunkView0 = hunkViews.get(hunks[0]);
- const hunkView1 = hunkViews.get(hunks[1]);
-
- // drag a selection
- await hunkView0.props.mousedownOnLine({detail: 1}, hunks[0], hunks[0].lines[2]);
- await hunkViews.get(hunks[1]).props.mousemoveOnLine({}, hunks[1], hunks[1].lines[1]);
- await filePatchView.mouseup();
- assert(hunkView0.props.isSelected);
- assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
- assert(hunkView1.props.isSelected);
- assert(hunkView1.props.selectedLines.has(hunks[1].lines[1]));
-
- // start a new selection, drag it across an existing selection
- await hunkView1.props.mousedownOnLine({detail: 1, metaKey: true}, hunks[1], hunks[1].lines[3]);
- await hunkView0.props.mousemoveOnLine({}, hunks[0], hunks[0].lines[0]);
- assert(hunkView0.props.isSelected);
- assert(hunkView0.props.selectedLines.has(hunks[0].lines[1]));
- assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
- assert(hunkView1.props.isSelected);
- assert(hunkView1.props.selectedLines.has(hunks[1].lines[1]));
- assert(hunkView1.props.selectedLines.has(hunks[1].lines[2]));
- assert(hunkView1.props.selectedLines.has(hunks[1].lines[3]));
-
- // drag back down without releasing mouse; the other selection remains intact
- await hunkView1.props.mousemoveOnLine({}, hunks[1], hunks[1].lines[3]);
- assert(hunkView0.props.isSelected);
- assert(!hunkView0.props.selectedLines.has(hunks[0].lines[1]));
- assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
- assert(hunkView1.props.isSelected);
- assert(hunkView1.props.selectedLines.has(hunks[1].lines[1]));
- assert(!hunkView1.props.selectedLines.has(hunks[1].lines[2]));
- assert(hunkView1.props.selectedLines.has(hunks[1].lines[3]));
-
- // drag back up so selections are adjacent, then release the mouse. selections should merge.
- await hunkView1.props.mousemoveOnLine({}, hunks[1], hunks[1].lines[2]);
- await filePatchView.mouseup();
- assert(hunkView0.props.isSelected);
- assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
- assert(hunkView1.props.isSelected);
- assert(hunkView1.props.selectedLines.has(hunks[1].lines[1]));
- assert(hunkView1.props.selectedLines.has(hunks[1].lines[2]));
- assert(hunkView1.props.selectedLines.has(hunks[1].lines[3]));
-
- // we detect merged selections based on the head here
- await filePatchView.selectToNext();
- assert(!hunkView0.props.isSelected);
- assert(!hunkView0.props.selectedLines.has(hunks[0].lines[2]));
-
- // double-click drag clears the existing selection and starts hunk-wise selection
- await hunkView0.props.mousedownOnLine({detail: 2}, hunks[0], hunks[0].lines[2]);
- assert(hunkView0.props.isSelected);
- assert(hunkView0.props.selectedLines.has(hunks[0].lines[1]));
- assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
- assert(!hunkView1.props.isSelected);
-
- await hunkView1.props.mousemoveOnLine({}, hunks[1], hunks[1].lines[1]);
- assert(hunkView0.props.isSelected);
- assert(hunkView0.props.selectedLines.has(hunks[0].lines[1]));
- assert(hunkView0.props.selectedLines.has(hunks[0].lines[2]));
- assert(hunkView1.props.isSelected);
- assert(hunkView1.props.selectedLines.has(hunks[1].lines[1]));
- assert(hunkView1.props.selectedLines.has(hunks[1].lines[2]));
- assert(hunkView1.props.selectedLines.has(hunks[1].lines[3]));
+ ]);
+ const hunkViews = new Map();
+ function registerHunkView(hunk, view) { hunkViews.set(hunk, view); }
+
+ const filePatchView = new FilePatchView({commandRegistry, hunks: [hunk0, hunk1], registerHunkView});
+ const hunkView0 = hunkViews.get(hunk0);
+ const hunkView1 = hunkViews.get(hunk1);
+
+ // in line selection mode, cmd-click line
+ await hunkView0.props.mousedownOnLine({button: 0, detail: 1}, hunk0, hunk0.lines[2]);
+ await filePatchView.mouseup();
+ assert.equal(filePatchView.getPatchSelectionMode(), 'line');
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([hunk0.lines[2]]));
+ await hunkView1.props.mousedownOnLine({button: 0, detail: 1, metaKey: true}, hunk1, hunk1.lines[2]);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0, hunk1]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([hunk0.lines[2], hunk1.lines[2]]));
+ await hunkView1.props.mousedownOnLine({button: 0, detail: 1, metaKey: true}, hunk1, hunk1.lines[2]);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([hunk0.lines[2]]));
+
+ // in line selection mode, cmd-click hunk header for separate hunk
+ await hunkView0.props.mousedownOnLine({button: 0, detail: 1}, hunk0, hunk0.lines[2]);
+ await filePatchView.mouseup();
+ assert.equal(filePatchView.getPatchSelectionMode(), 'line');
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([hunk0.lines[2]]));
+ await hunkView1.props.mousedownOnHeader({button: 0, metaKey: true});
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0, hunk1]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([hunk0.lines[2], ...hunk1.lines]));
+ await hunkView1.props.mousedownOnHeader({button: 0, metaKey: true});
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([hunk0.lines[2]]));
+
+ // in hunk selection mode, cmd-click line for separate hunk
+ await hunkView0.props.mousedownOnLine({button: 0, detail: 1}, hunk0, hunk0.lines[2]);
+ await filePatchView.mouseup();
+ filePatchView.togglePatchSelectionMode();
+ assert.equal(filePatchView.getPatchSelectionMode(), 'hunk');
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set(hunk0.lines));
+
+ // in hunk selection mode, cmd-click hunk header for separate hunk
+ await hunkView0.props.mousedownOnLine({button: 0, detail: 1}, hunk0, hunk0.lines[2]);
+ await filePatchView.mouseup();
+ filePatchView.togglePatchSelectionMode();
+ assert.equal(filePatchView.getPatchSelectionMode(), 'hunk');
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set(hunk0.lines));
+ });
+
+ it('allows lines and hunks to be selected via shift-clicking', async () => {
+ const hunk0 = new Hunk(1, 1, 2, 4, '', [
+ new HunkLine('line-1', 'unchanged', 1, 1, 0),
+ new HunkLine('line-2', 'added', -1, 2, 1),
+ new HunkLine('line-3', 'added', -1, 3, 2),
+ ]);
+ const hunk1 = new Hunk(5, 7, 1, 4, '', [
+ new HunkLine('line-5', 'added', -1, 7, 3),
+ new HunkLine('line-6', 'added', -1, 8, 4),
+ new HunkLine('line-7', 'added', -1, 9, 5),
+ new HunkLine('line-8', 'added', -1, 10, 6),
+ ]);
+ const hunk2 = new Hunk(15, 17, 1, 4, '', [
+ new HunkLine('line-15', 'added', -1, 15, 7),
+ new HunkLine('line-16', 'added', -1, 18, 8),
+ new HunkLine('line-17', 'added', -1, 19, 9),
+ new HunkLine('line-18', 'added', -1, 20, 10),
+ ]);
+ const hunkViews = new Map();
+ function registerHunkView(hunk, view) { hunkViews.set(hunk, view); }
+
+ const filePatchView = new FilePatchView({commandRegistry, hunks: [hunk0, hunk1, hunk2], registerHunkView});
+ const hunkView0 = hunkViews.get(hunk0);
+ const hunkView1 = hunkViews.get(hunk1);
+ const hunkView2 = hunkViews.get(hunk2);
+
+ // in line selection mode, shift-click line in separate hunk that comes after selected line
+ await hunkView0.props.mousedownOnLine({button: 0, detail: 1}, hunk0, hunk0.lines[2]);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([hunk0.lines[2]]));
+ await hunkView2.props.mousedownOnLine({button: 0, detail: 1, shiftKey: true}, hunk2, hunk2.lines[2]);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0, hunk1, hunk2]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([...hunk0.lines.slice(2), ...hunk1.lines, ...hunk2.lines.slice(0, 3)]));
+ await hunkView1.props.mousedownOnLine({button: 0, detail: 1, shiftKey: true}, hunk1, hunk1.lines[2]);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0, hunk1]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([...hunk0.lines.slice(2), ...hunk1.lines.slice(0, 3)]));
+
+ // in line selection mode, shift-click hunk header for separate hunk that comes after selected line
+ await hunkView0.props.mousedownOnLine({button: 0, detail: 1}, hunk0, hunk0.lines[2]);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([hunk0.lines[2]]));
+ await hunkView2.props.mousedownOnHeader({button: 0, shiftKey: true}, hunk2);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0, hunk1, hunk2]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([...hunk0.lines.slice(2), ...hunk1.lines, ...hunk2.lines]));
+ await hunkView1.props.mousedownOnHeader({button: 0, shiftKey: true}, hunk1);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0, hunk1]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([...hunk0.lines.slice(2), ...hunk1.lines]));
+
+ // in line selection mode, shift-click hunk header for separate hunk that comes before selected line
+ await hunkView2.props.mousedownOnLine({button: 0, detail: 1}, hunk2, hunk2.lines[2]);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk2]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([hunk2.lines[2]]));
+ await hunkView0.props.mousedownOnHeader({button: 0, shiftKey: true}, hunk0);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0, hunk1, hunk2]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([...hunk0.lines.slice(1), ...hunk1.lines, ...hunk2.lines.slice(0, 3)]));
+ await hunkView1.props.mousedownOnHeader({button: 0, shiftKey: true}, hunk1);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk1, hunk2]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([...hunk1.lines, ...hunk2.lines.slice(0, 3)]));
+
+ // in hunk selection mode, shift-click hunk header for separate hunk that comes after selected line
+ await hunkView0.props.mousedownOnHeader({button: 0}, hunk0);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set(hunk0.lines.slice(1)));
+ await hunkView2.props.mousedownOnHeader({button: 0, shiftKey: true}, hunk2);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0, hunk1, hunk2]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([...hunk0.lines.slice(1), ...hunk1.lines, ...hunk2.lines]));
+ await hunkView1.props.mousedownOnHeader({button: 0, shiftKey: true}, hunk1);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0, hunk1]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([...hunk0.lines.slice(1), ...hunk1.lines]));
+
+ // in hunk selection mode, shift-click hunk header for separate hunk that comes before selected line
+ await hunkView2.props.mousedownOnHeader({button: 0}, hunk2);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk2]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set(hunk2.lines));
+ await hunkView0.props.mousedownOnHeader({button: 0, shiftKey: true}, hunk0);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk0, hunk1, hunk2]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([...hunk0.lines.slice(1), ...hunk1.lines, ...hunk2.lines]));
+ await hunkView1.props.mousedownOnHeader({button: 0, shiftKey: true}, hunk1);
+ await filePatchView.mouseup();
+ assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk1, hunk2]));
+ assertEqualSets(filePatchView.getSelectedLines(), new Set([...hunk1.lines, ...hunk2.lines]));
+ });
});
it('scrolls off-screen lines and hunks into view when they are selected', async function() {
@@ -212,7 +393,7 @@ describe('FilePatchView', function() {
const openCurrentFile = sinon.stub();
const filePatchView = new FilePatchView({commandRegistry, hunks, openCurrentFile});
- filePatchView.mousedownOnLine({detail: 1}, hunks[0], hunks[0].lines[2]);
+ filePatchView.mousedownOnLine({button: 0, detail: 1}, hunks[0], hunks[0].lines[2]);
await filePatchView.mousemoveOnLine({}, hunks[0], hunks[0].lines[3]);
await filePatchView.mouseup();
@@ -241,7 +422,7 @@ describe('FilePatchView', function() {
const openCurrentFile = sinon.stub();
const filePatchView = new FilePatchView({commandRegistry, hunks, openCurrentFile});
- filePatchView.mousedownOnLine({detail: 1}, hunks[1], hunks[1].lines[2]);
+ filePatchView.mousedownOnLine({button: 0, detail: 1}, hunks[1], hunks[1].lines[2]);
await filePatchView.mousemoveOnLine({}, hunks[1], hunks[1].lines[3]);
await filePatchView.mouseup();
@@ -263,7 +444,7 @@ describe('FilePatchView', function() {
const openCurrentFile = sinon.stub();
const filePatchView = new FilePatchView({commandRegistry, hunks, openCurrentFile});
- filePatchView.mousedownOnLine({detail: 1}, hunks[0], hunks[0].lines[2]);
+ filePatchView.mousedownOnLine({button: 0, detail: 1}, hunks[0], hunks[0].lines[2]);
await filePatchView.mouseup();
filePatchView.openFile();