From 98092644c43b1ed3474363780ec7d339bd5df435 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Wed, 18 Jan 2017 22:12:27 -0800 Subject: [PATCH 01/13] Add Stage/Unstage selection context menu options to FilePatchView --- lib/views/file-patch-view.js | 6 ++++-- menus/git.cson | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 818e1b001f..6e2bccdccb 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -4,6 +4,7 @@ import {CompositeDisposable, Disposable} from 'atom'; import etch from 'etch'; +import cx from 'classnames'; import {autobind} from 'core-decorators'; import HunkView from './hunk-view'; @@ -62,9 +63,10 @@ export default class FilePatchView { const headHunk = this.selection.getHeadHunk(); const headLine = this.selection.getHeadLine(); const hunkSelectionMode = this.selection.getMode() === 'hunk'; - const stageButtonLabelPrefix = this.props.stagingStatus === 'unstaged' ? 'Stage' : 'Unstage'; + const unstaged = this.props.stagingStatus === 'unstaged'; + const stageButtonLabelPrefix = unstaged ? 'Stage' : 'Unstage'; return ( -
{this.props.hunks.map(hunk => { 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' + } + ] From d27f450dfaf1a185aedb3a7e51ca991beacf59a0 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 23 Jan 2017 17:54:25 -0800 Subject: [PATCH 02/13] Select item on contextmenu. Retain mode when modifier keys are pressed. --- lib/views/file-patch-view.js | 76 +++++++++++++------ lib/views/hunk-view.js | 4 +- .../controllers/file-patch-controller.test.js | 10 +-- test/views/file-patch-view.test.js | 14 ++-- 4 files changed, 67 insertions(+), 37 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 6e2bccdccb..d77640a999 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -87,9 +87,10 @@ export default class FilePatchView { selectedLines={selectedLines} headLine={headLine} headHunk={headHunk} - mousedownOnHeader={() => this.mousedownOnHeader(hunk)} + mousedownOnHeader={e => this.mousedownOnHeader(e, hunk)} mousedownOnLine={this.mousedownOnLine} mousemoveOnLine={this.mousemoveOnLine} + contextMenuOnItem={this.contextMenuOnItem} didClickStageButton={() => this.didClickStageButtonForHunk(hunk)} registerView={this.props.registerHunkView} /> @@ -99,44 +100,71 @@ export default class FilePatchView { ); } - mousedownOnHeader(hunk) { - this.selection.selectHunk(hunk); - this.mouseSelectionInProgress = true; - return etch.update(this); - } - @autobind - mousedownOnLine(event, hunk, line) { - if (event.ctrlKey || event.metaKey) { - this.selection.addOrSubtractLineSelection(line); - } else if (event.shiftKey) { - if (this.selection.getMode() === 'hunk') { - this.selection.selectHunk(hunk, true); - } else { - this.selection.selectLine(line, true); + async contextMenuOnItem(event, hunk, line) { + if (this.selection.getMode() === 'hunk') { + if (!this.selection.getSelectedHunks().has(hunk)) { + event.stopPropagation(); + this.selection.selectHunk(hunk, event.shiftKey); + await etch.update(this); + const newEvent = new MouseEvent(event.type, event); + requestAnimationFrame(() => { + event.target.parentNode.dispatchEvent(newEvent); + }); } } else { - if (event.detail === 1) { - this.selection.selectLine(line, false); + if (!this.selection.getSelectedLines().has(line)) { + event.stopPropagation(); + this.selection.selectLine(line, event.shiftKey); + await etch.update(this); + const newEvent = new MouseEvent(event.type, event); + requestAnimationFrame(() => { + event.target.parentNode.dispatchEvent(newEvent); + }); + } + } + } + + async mousedownOnHeader(event, hunk) { + if (event.button === 0) { + this.selection.selectHunk(hunk); + this.mouseSelectionInProgress = true; + await etch.update(this); + } + } + + @autobind + async mousedownOnLine(event, hunk, line) { + if (event.button === 0) { + if (event.ctrlKey || event.metaKey) { + if (this.selection.getMode() === 'hunk') { + this.selection.addOrSubtractHunkSelection(hunk); + } else { + this.selection.addOrSubtractLineSelection(line); + } + } else if (event.shiftKey) { + if (this.selection.getMode() === 'hunk') { + this.selection.selectHunk(hunk, true); + } else { + this.selection.selectLine(line, true); + } } else { - this.selection.selectHunk(hunk, false); + this.selection.selectLine(line); } + this.mouseSelectionInProgress = true; + await etch.update(this); } - this.mouseSelectionInProgress = true; - return etch.update(this); } @autobind - mousemoveOnLine(event, hunk, line) { + async mousemoveOnLine(event, hunk, line) { if (this.mouseSelectionInProgress) { if (this.selection.getMode() === 'hunk') { this.selection.selectHunk(hunk, true); } else { this.selection.selectLine(line, true); } - return etch.update(this); - } else { - return null; + await etch.update(this); } } diff --git a/lib/views/hunk-view.js b/lib/views/hunk-view.js index 8566c2ae83..4520452df0 100644 --- a/lib/views/hunk-view.js +++ b/lib/views/hunk-view.js @@ -62,6 +62,7 @@ export default class HunkView { registerLineElement={this.registerLineElement} mousedown={this.mousedownOnLine} mousemove={this.mousemoveOnLine} + contextMenuOnItem={(event, line) => this.props.contextMenuOnItem(event, this.props.hunk, line)} />, )}
@@ -101,7 +102,8 @@ class LineView { return (
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/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index 854df7a9ad..743ceded40 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}, 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}, hunk, lines[1]); view.mouseup(); - hunkView.props.mousedownOnLine({detail: 1, metaKey: true}, hunk, lines[2]); + hunkView.props.mousedownOnLine({button: 0, 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}, 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}, 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..6c7b142c19 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -39,7 +39,7 @@ describe('FilePatchView', function() { const hunkView1 = hunkViews.get(hunks[1]); // drag a selection - await hunkView0.props.mousedownOnLine({detail: 1}, hunks[0], hunks[0].lines[2]); + await hunkView0.props.mousedownOnLine({button: 0}, 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); @@ -48,7 +48,7 @@ describe('FilePatchView', function() { 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 hunkView1.props.mousedownOnLine({button: 0, 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])); @@ -83,8 +83,8 @@ describe('FilePatchView', function() { 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]); + // clicking the header clears the existing selection and starts hunk-wise selection + await hunkView0.props.mousedownOnHeader({button: 0}, 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])); @@ -212,7 +212,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}, hunks[0], hunks[0].lines[2]); await filePatchView.mousemoveOnLine({}, hunks[0], hunks[0].lines[3]); await filePatchView.mouseup(); @@ -241,7 +241,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}, hunks[1], hunks[1].lines[2]); await filePatchView.mousemoveOnLine({}, hunks[1], hunks[1].lines[3]); await filePatchView.mouseup(); @@ -263,7 +263,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}, hunks[0], hunks[0].lines[2]); await filePatchView.mouseup(); filePatchView.openFile(); From 636684960abd69f100934acf78bf3ee85105bd14 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 23 Jan 2017 18:02:31 -0800 Subject: [PATCH 03/13] Pass event to mousedownOnHeader --- lib/views/hunk-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/hunk-view.js b/lib/views/hunk-view.js index 4520452df0..e6b6a514f3 100644 --- a/lib/views/hunk-view.js +++ b/lib/views/hunk-view.js @@ -43,7 +43,7 @@ export default class HunkView { return (
this.props.mousedownOnHeader()}> + onmousedown={e => this.props.mousedownOnHeader(e)}> {this.props.hunk.getHeader().trim()} {this.props.hunk.getSectionHeading().trim()} From 5109ccc97d74a49c20bd49918434d7f917f74640 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 23 Jan 2017 18:44:29 -0800 Subject: [PATCH 04/13] Add/subtract lines when clicking hunk headers w/ modifier keys --- lib/views/file-patch-view.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index d77640a999..69580f8949 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -127,7 +127,29 @@ export default class FilePatchView { async mousedownOnHeader(event, hunk) { if (event.button === 0) { - this.selection.selectHunk(hunk); + if (event.ctrlKey || event.metaKey) { + if (this.selection.getMode() === 'hunk') { + this.selection.addOrSubtractHunkSelection(hunk); + } else { + // TODO: optimize + hunk.getLines().forEach(line => { + this.selection.addOrSubtractLineSelection(line); + this.selection.coalesce(); + }); + } + } else if (event.shiftKey) { + if (this.selection.getMode() === 'hunk') { + this.selection.selectHunk(hunk, true); + } else { + // TODO: optimize + hunk.getLines().forEach(line => { + this.selection.selectLine(line, true); + this.selection.coalesce(); + }); + } + } else { + this.selection.selectHunk(hunk, false); + } this.mouseSelectionInProgress = true; await etch.update(this); } @@ -149,7 +171,7 @@ export default class FilePatchView { this.selection.selectLine(line, true); } } else { - this.selection.selectLine(line); + this.selection.selectLine(line, false); } this.mouseSelectionInProgress = true; await etch.update(this); From 35712325c6368e3b3af7d5f83fbc03d901c83ba4 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 23 Jan 2017 20:02:25 -0800 Subject: [PATCH 05/13] Highlight correct hunk while shift-clicking header based on last tail --- lib/models/hunk-line.js | 3 ++- lib/models/repository.js | 11 +++++++---- lib/views/file-patch-selection.js | 4 ++++ lib/views/file-patch-view.js | 13 ++++++++----- lib/views/list-selection.js | 4 ++++ 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/models/hunk-line.js b/lib/models/hunk-line.js index 97f09424d9..f75ce7f4b8 100644 --- a/lib/models/hunk-line.js +++ b/lib/models/hunk-line.js @@ -6,11 +6,12 @@ export default class HunkLine { '\\': 'nonewline', } - constructor(text, status, oldLineNumber, newLineNumber) { + constructor(text, status, oldLineNumber, newLineNumber, diffLineNumber) { this.text = text; this.status = status; this.oldLineNumber = oldLineNumber; this.newLineNumber = newLineNumber; + this.diffLineNumber = diffLineNumber; } copy({text, status, oldLineNumber, newLineNumber} = {}) { diff --git a/lib/models/repository.js b/lib/models/repository.js index 2b6e5ecdfe..2f17bd2d9b 100644 --- a/lib/models/repository.js +++ b/lib/models/repository.js @@ -178,6 +178,7 @@ export default class Repository { } buildFilePatchesFromRawDiffs(rawDiffs) { + let diffLineNumber = 0; return rawDiffs.map(patch => { const hunks = patch.hunks.map(hunk => { let oldLineNumber = hunk.oldStartLine; @@ -187,17 +188,19 @@ export default class Repository { const text = line.slice(1); let hunkLine; if (status === 'unchanged') { - hunkLine = new HunkLine(text, status, oldLineNumber, newLineNumber); + hunkLine = new HunkLine(text, status, oldLineNumber, newLineNumber, diffLineNumber++); oldLineNumber++; newLineNumber++; } else if (status === 'added') { - hunkLine = new HunkLine(text, status, -1, newLineNumber); + hunkLine = new HunkLine(text, status, -1, newLineNumber, diffLineNumber++); newLineNumber++; } else if (status === 'deleted') { - hunkLine = new HunkLine(text, status, oldLineNumber, -1); + hunkLine = new HunkLine(text, status, oldLineNumber, -1, diffLineNumber++); oldLineNumber++; } else if (status === 'nonewline') { - hunkLine = new HunkLine(text.substr(1), status, -1, -1); + hunkLine = new HunkLine(text.substr(1), status, -1, -1, diffLineNumber++); + } else { + throw new Error(`unknow status type: ${status}`); } return hunkLine; }); diff --git a/lib/views/file-patch-selection.js b/lib/views/file-patch-selection.js index 05060f7dbf..85fdff35a6 100644 --- a/lib/views/file-patch-selection.js +++ b/lib/views/file-patch-selection.js @@ -246,4 +246,8 @@ export default class FilePatchSelection { this.resolveNextUpdatePromise = resolve; }); } + + getLineSelectionTailIndex() { + return this.linesSelection.getTailIndex(); + } } diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 69580f8949..7f45340350 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -141,11 +141,14 @@ export default class FilePatchView { if (this.selection.getMode() === 'hunk') { this.selection.selectHunk(hunk, true); } else { - // TODO: optimize - hunk.getLines().forEach(line => { - this.selection.selectLine(line, true); - this.selection.coalesce(); - }); + const hunkLines = hunk.getLines(); + const tailIndex = this.selection.getLineSelectionTailIndex(); + const selectedHunkAfterTail = tailIndex < hunkLines[0].diffLineNumber; + if (selectedHunkAfterTail) { + this.selection.selectLine(hunkLines[hunkLines.length - 1], true); + } else { + this.selection.selectLine(hunkLines[0], true); + } } } else { this.selection.selectHunk(hunk, false); 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; + } } From 9008bd10fd5373fbd375bf52195f6add38622d87 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 23 Jan 2017 20:29:36 -0800 Subject: [PATCH 06/13] Restore behavior of selecting entire hunk when double-clicking line --- lib/views/file-patch-view.js | 4 ++- .../controllers/file-patch-controller.test.js | 10 +++---- test/views/file-patch-view.test.js | 28 +++++++++++++++---- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 7f45340350..bc227b43d3 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -173,8 +173,10 @@ export default class FilePatchView { } else { this.selection.selectLine(line, true); } - } else { + } else if (event.detail === 1) { this.selection.selectLine(line, false); + } else if (event.detail === 2) { + this.selection.selectHunk(hunk, false); } this.mouseSelectionInProgress = true; await etch.update(this); diff --git a/test/controllers/file-patch-controller.test.js b/test/controllers/file-patch-controller.test.js index 743ceded40..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({button: 0}, 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({button: 0}, hunk, lines[1]); + hunkView.props.mousedownOnLine({button: 0, detail: 1}, hunk, lines[1]); view.mouseup(); - hunkView.props.mousedownOnLine({button: 0, 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({button: 0}, 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({button: 0}, 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 6c7b142c19..1afcac19fa 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -39,7 +39,7 @@ describe('FilePatchView', function() { const hunkView1 = hunkViews.get(hunks[1]); // drag a selection - await hunkView0.props.mousedownOnLine({button: 0}, hunks[0], hunks[0].lines[2]); + 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); @@ -48,7 +48,7 @@ describe('FilePatchView', function() { 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, metaKey: true}, hunks[1], hunks[1].lines[3]); + 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])); @@ -83,8 +83,24 @@ describe('FilePatchView', function() { 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}, hunks[0], hunks[0].lines[2]); + 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])); @@ -212,7 +228,7 @@ describe('FilePatchView', function() { const openCurrentFile = sinon.stub(); const filePatchView = new FilePatchView({commandRegistry, hunks, openCurrentFile}); - filePatchView.mousedownOnLine({button: 0}, 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 +257,7 @@ describe('FilePatchView', function() { const openCurrentFile = sinon.stub(); const filePatchView = new FilePatchView({commandRegistry, hunks, openCurrentFile}); - filePatchView.mousedownOnLine({button: 0}, 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 +279,7 @@ describe('FilePatchView', function() { const openCurrentFile = sinon.stub(); const filePatchView = new FilePatchView({commandRegistry, hunks, openCurrentFile}); - filePatchView.mousedownOnLine({button: 0}, hunks[0], hunks[0].lines[2]); + filePatchView.mousedownOnLine({button: 0, detail: 1}, hunks[0], hunks[0].lines[2]); await filePatchView.mouseup(); filePatchView.openFile(); From 76cb610a86ac3bfa770b24b65a907850005a51f8 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 23 Jan 2017 21:00:59 -0800 Subject: [PATCH 07/13] Add tests for cmd/shift-clicking lines and hunk headers --- lib/views/file-patch-view.js | 12 + test/views/file-patch-view.test.js | 351 +++++++++++++++++++++-------- 2 files changed, 270 insertions(+), 93 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index bc227b43d3..b92cc0fa5e 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -206,6 +206,18 @@ export default class FilePatchView { return etch.update(this); } + getPatchSelectionMode() { + return this.selection.getMode(); + } + + getSelectedHunks() { + return this.selection.getSelectedHunks(); + } + + getSelectedLines() { + return this.selection.getSelectedLines(); + } + selectNext() { this.selection.selectNext(); return etch.update(this); diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index 1afcac19fa..062c4ac73d 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -16,104 +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({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])); + ]); + 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 line 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 after 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() { From 04046c58efe6ecafd8f7d4917487f2ce006a3598 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 23 Jan 2017 21:23:59 -0800 Subject: [PATCH 08/13] Style hunk header when in 'hunk' mode to make mode clear --- styles/hunk-view.less | 3 +++ 1 file changed, 3 insertions(+) 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; } From a38cacd9d1b3c2d76bc9aaf2010a7d9911742645 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 23 Jan 2017 21:42:30 -0800 Subject: [PATCH 09/13] :shirt: --- lib/views/hunk-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/hunk-view.js b/lib/views/hunk-view.js index e6b6a514f3..f9f74bd9ce 100644 --- a/lib/views/hunk-view.js +++ b/lib/views/hunk-view.js @@ -62,7 +62,7 @@ export default class HunkView { registerLineElement={this.registerLineElement} mousedown={this.mousedownOnLine} mousemove={this.mousemoveOnLine} - contextMenuOnItem={(event, line) => this.props.contextMenuOnItem(event, this.props.hunk, line)} + contextMenuOnItem={(e, clickedLine) => this.props.contextMenuOnItem(e, this.props.hunk, clickedLine)} />, )}
From db6212a46b88c71b8be2c0160a7d3b1afefd7578 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 24 Jan 2017 13:58:09 -0800 Subject: [PATCH 10/13] :art: contextMenuOnItem --- lib/views/file-patch-view.js | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index b92cc0fa5e..0000159590 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -102,27 +102,20 @@ export default class FilePatchView { @autobind async contextMenuOnItem(event, hunk, line) { - if (this.selection.getMode() === 'hunk') { - if (!this.selection.getSelectedHunks().has(hunk)) { - event.stopPropagation(); - this.selection.selectHunk(hunk, event.shiftKey); - await etch.update(this); - const newEvent = new MouseEvent(event.type, event); - requestAnimationFrame(() => { - event.target.parentNode.dispatchEvent(newEvent); - }); - } + const mode = this.selection.getMode(); + if (mode === 'hunk' && !this.selection.getSelectedHunks().has(hunk)) { + this.selection.selectHunk(hunk, event.shiftKey); + } else if (mode === 'line' && !this.selection.getSelectedLines().has(line)) { + this.selection.selectLine(line, event.shiftKey); } else { - if (!this.selection.getSelectedLines().has(line)) { - event.stopPropagation(); - this.selection.selectLine(line, event.shiftKey); - await etch.update(this); - const newEvent = new MouseEvent(event.type, event); - requestAnimationFrame(() => { - event.target.parentNode.dispatchEvent(newEvent); - }); - } + return null; } + event.stopPropagation(); + await etch.update(this); + const newEvent = new MouseEvent(event.type, event); + requestAnimationFrame(() => { + event.target.parentNode.dispatchEvent(newEvent); + }); } async mousedownOnHeader(event, hunk) { From 9eececb8cc4a76cb4203a5be1951876424a407ef Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 24 Jan 2017 14:08:21 -0800 Subject: [PATCH 11/13] More :art: FilePatchView --- lib/views/file-patch-view.js | 101 +++++++++++++++++------------------ 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index 0000159590..b9d8823944 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -119,73 +119,70 @@ export default class FilePatchView { } async mousedownOnHeader(event, hunk) { - if (event.button === 0) { - if (event.ctrlKey || event.metaKey) { - if (this.selection.getMode() === 'hunk') { - this.selection.addOrSubtractHunkSelection(hunk); - } else { - // TODO: optimize - hunk.getLines().forEach(line => { - this.selection.addOrSubtractLineSelection(line); - this.selection.coalesce(); - }); - } - } else if (event.shiftKey) { - if (this.selection.getMode() === 'hunk') { - this.selection.selectHunk(hunk, true); - } else { - const hunkLines = hunk.getLines(); - const tailIndex = this.selection.getLineSelectionTailIndex(); - const selectedHunkAfterTail = tailIndex < hunkLines[0].diffLineNumber; - if (selectedHunkAfterTail) { - this.selection.selectLine(hunkLines[hunkLines.length - 1], true); - } else { - this.selection.selectLine(hunkLines[0], true); - } - } + if (event.button !== 0) { return; } + if (event.ctrlKey || event.metaKey) { + if (this.selection.getMode() === 'hunk') { + this.selection.addOrSubtractHunkSelection(hunk); } else { - this.selection.selectHunk(hunk, false); - } - this.mouseSelectionInProgress = true; - await etch.update(this); - } - } - - @autobind - async mousedownOnLine(event, hunk, line) { - if (event.button === 0) { - if (event.ctrlKey || event.metaKey) { - if (this.selection.getMode() === 'hunk') { - this.selection.addOrSubtractHunkSelection(hunk); - } else { + // TODO: optimize + hunk.getLines().forEach(line => { this.selection.addOrSubtractLineSelection(line); - } - } else if (event.shiftKey) { - if (this.selection.getMode() === 'hunk') { - this.selection.selectHunk(hunk, true); + this.selection.coalesce(); + }); + } + } else if (event.shiftKey) { + if (this.selection.getMode() === 'hunk') { + this.selection.selectHunk(hunk, true); + } else { + const hunkLines = hunk.getLines(); + const tailIndex = this.selection.getLineSelectionTailIndex(); + const selectedHunkAfterTail = tailIndex < hunkLines[0].diffLineNumber; + if (selectedHunkAfterTail) { + this.selection.selectLine(hunkLines[hunkLines.length - 1], true); } else { - this.selection.selectLine(line, true); + this.selection.selectLine(hunkLines[0], true); } - } else if (event.detail === 1) { - this.selection.selectLine(line, false); - } else if (event.detail === 2) { - this.selection.selectHunk(hunk, false); } - this.mouseSelectionInProgress = true; - await etch.update(this); + } else { + this.selection.selectHunk(hunk, false); } + this.mouseSelectionInProgress = true; + await etch.update(this); } @autobind - async mousemoveOnLine(event, hunk, line) { - if (this.mouseSelectionInProgress) { + async mousedownOnLine(event, hunk, line) { + if (event.button !== 0) { return; } + if (event.ctrlKey || event.metaKey) { + if (this.selection.getMode() === 'hunk') { + this.selection.addOrSubtractHunkSelection(hunk); + } else { + this.selection.addOrSubtractLineSelection(line); + } + } else if (event.shiftKey) { if (this.selection.getMode() === 'hunk') { this.selection.selectHunk(hunk, true); } else { this.selection.selectLine(line, true); } - await etch.update(this); + } else if (event.detail === 1) { + this.selection.selectLine(line, false); + } else if (event.detail === 2) { + this.selection.selectHunk(hunk, false); } + this.mouseSelectionInProgress = true; + await etch.update(this); + } + + @autobind + async mousemoveOnLine(event, hunk, line) { + if (!this.mouseSelectionInProgress) { return; } + if (this.selection.getMode() === 'hunk') { + this.selection.selectHunk(hunk, true); + } else { + this.selection.selectLine(line, true); + } + await etch.update(this); } @autobind From 367566e536d428a41293065b73793e470a162390 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 24 Jan 2017 14:30:09 -0800 Subject: [PATCH 12/13] Fix test comments --- test/views/file-patch-view.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/views/file-patch-view.test.js b/test/views/file-patch-view.test.js index 062c4ac73d..8ae7226600 100644 --- a/test/views/file-patch-view.test.js +++ b/test/views/file-patch-view.test.js @@ -251,7 +251,7 @@ describe('FilePatchView', function() { assertEqualSets(filePatchView.getSelectedHunks(), new Set([hunk1, hunk2])); assertEqualSets(filePatchView.getSelectedLines(), new Set([...hunk1.lines, ...hunk2.lines.slice(0, 3)])); - // in line selection mode, shift-click hunk header for separate hunk that comes after selected line + // 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])); @@ -265,7 +265,7 @@ describe('FilePatchView', function() { 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 after selected line + // 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])); From c62aa9a416633927f8a32fefa2c0e8f40f1d4994 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 24 Jan 2017 14:53:55 -0800 Subject: [PATCH 13/13] :shirt: --- lib/views/file-patch-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/file-patch-view.js b/lib/views/file-patch-view.js index b9d8823944..ea22cb5a8d 100644 --- a/lib/views/file-patch-view.js +++ b/lib/views/file-patch-view.js @@ -108,7 +108,7 @@ export default class FilePatchView { } else if (mode === 'line' && !this.selection.getSelectedLines().has(line)) { this.selection.selectLine(line, event.shiftKey); } else { - return null; + return; } event.stopPropagation(); await etch.update(this);