From 5993cad57bc906664827f3ac48dccaf42f57ce4b Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 15 Mar 2016 20:13:33 -0700 Subject: [PATCH 1/7] Stage lines from editor Signed-off-by: Michelle Tilley --- interfaces/atom.js.flow | 4 +++ lib/common.js | 9 +++++++ lib/git-package.js | 54 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/interfaces/atom.js.flow b/interfaces/atom.js.flow index c9460a6921..6d1ab32624 100644 --- a/interfaces/atom.js.flow +++ b/interfaces/atom.js.flow @@ -79,6 +79,10 @@ declare module 'atom' { destroyItem(item: any): boolean; } + declare class Range { + intersectsRow(row: number): boolean; + } + declare class TextBuffer { onDidSave(fn: Function): Disposable; onDidReload(fn: Function): Disposable; diff --git a/lib/common.js b/lib/common.js index 183a406563..99409d2bd3 100644 --- a/lib/common.js +++ b/lib/common.js @@ -30,6 +30,15 @@ export function createObjectsFromString (diffString: string, return objects } +export function any (arr: Array, predicate: (item: T, idx: number, arr: Array) => boolean): boolean { + for (let i = 0; i < arr.length; i++) { + if (predicate(arr[i], i, arr)) { + return true + } + } + return false +} + export type ObjectMap = { [key: string]: V } export function ifNotNull (a: ?T, fn: (a: T) => U): ?U { diff --git a/lib/git-package.js b/lib/git-package.js index 0989399e3a..eba810693f 100644 --- a/lib/git-package.js +++ b/lib/git-package.js @@ -7,9 +7,9 @@ import DiffViewModel from './diff-view-model' import StatusBarViewModel from './status-bar-view-model' import FileDiffViewModel from './file-diff-view-model' import GitService from './git-service' -import {DiffURI} from './common' +import {any, DiffURI} from './common' -import type {Panel, Pane} from 'atom' +import type {Panel, Pane, Range} from 'atom' import type {StatusBar, Tile} from 'status-bar' type GitState = {panelVisible: boolean} @@ -45,6 +45,10 @@ export default class GitPackage { this.update() }) + atom.commands.add('atom-workspace', 'git:stage-selected-lines', () => { + this.stageSelectedLines() + }) + this.update() if (state.panelVisible) { @@ -63,6 +67,52 @@ export default class GitPackage { }) } + stageSelectedLines (): void { + const editor = atom.workspace.getActiveTextEditor() + if (editor) { + const selectedRanges = editor.getSelectedBufferRanges() + const [_projectPath, relativePath] = editor.project.relativizePath(editor.getPath()) + this.stageBufferRanges(selectedRanges, relativePath) + } + } + + stageBufferRanges (ranges: Array, filePath: string): void { + let lineOffsetSoFar = 0 + + ranges = ranges.map(range => { + if (range.end.column === 0 && range.start.row !== range.end.row) { + range = range.copy() + range.end.row = range.end.row - 1 + } + return range + }) + + const rangeIntersectsRow = (range, row) => range.intersectsRow(row) + const anyRangesIntersectLine = (line) => { + let row + if (line.isAddition()) { + row = line.getNewLineNumber() - 1 + lineOffsetSoFar++ + } else { + row = line.getOldLineNumber() + lineOffsetSoFar-- + } + if (line.isDeletion()) row += lineOffsetSoFar + return any(ranges, (range) => rangeIntersectsRow(range, row)) + } + + this.getFileListViewModel().update().then(() => { + const fileDiff = this.getFileListViewModel().getDiffForPathName(filePath) + fileDiff.transact(() => { + fileDiff.getHunks() + .map(hunk => hunk.getLines()) + .reduce((acc, lines) => acc.concat(lines), []) + .filter(line => line.isChanged() && anyRangesIntersectLine(line)) + .forEach(line => line.stage()) + }) + }) + } + hasRepository (): boolean { return atom.project.getRepositories().length > 0 && atom.project.getRepositories()[0] } From 22954cd6d73365b39104fffb9079f842fc46879c Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 28 Mar 2016 15:26:37 -0700 Subject: [PATCH 2/7] Use Array::some instead of custom `any` function Signed-off-by: Michelle Tilley --- lib/common.js | 9 --------- lib/git-package.js | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/common.js b/lib/common.js index 99409d2bd3..183a406563 100644 --- a/lib/common.js +++ b/lib/common.js @@ -30,15 +30,6 @@ export function createObjectsFromString (diffString: string, return objects } -export function any (arr: Array, predicate: (item: T, idx: number, arr: Array) => boolean): boolean { - for (let i = 0; i < arr.length; i++) { - if (predicate(arr[i], i, arr)) { - return true - } - } - return false -} - export type ObjectMap = { [key: string]: V } export function ifNotNull (a: ?T, fn: (a: T) => U): ?U { diff --git a/lib/git-package.js b/lib/git-package.js index eba810693f..f3c07edeb5 100644 --- a/lib/git-package.js +++ b/lib/git-package.js @@ -98,7 +98,7 @@ export default class GitPackage { lineOffsetSoFar-- } if (line.isDeletion()) row += lineOffsetSoFar - return any(ranges, (range) => rangeIntersectsRow(range, row)) + return ranges.some(range => rangeIntersectsRow(range, row)) } this.getFileListViewModel().update().then(() => { From 26fcdedfcdc7d7e9e46d3dc4f74d7b4d629775ca Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 28 Mar 2016 18:23:42 -0700 Subject: [PATCH 3/7] Use GitStore for staging lines from editor Signed-off-by: Michelle Tilley --- lib/git-package.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/git-package.js b/lib/git-package.js index 7f369c30b8..3f4f6fe5e6 100644 --- a/lib/git-package.js +++ b/lib/git-package.js @@ -74,10 +74,13 @@ export default class GitPackage { } } - stageBufferRanges (ranges: Array, filePath: string): void { + async stageBufferRanges (ranges: Array, filePath: string): void { let lineOffsetSoFar = 0 ranges = ranges.map(range => { + // If a range spans at least one line, and ends on column zero we don't + // want to stage the last line (since no text on that line is selected). + // So, copy the range and rewind the ending row by 1. if (range.end.column === 0 && range.start.row !== range.end.row) { range = range.copy() range.end.row = range.end.row - 1 @@ -85,8 +88,8 @@ export default class GitPackage { return range }) - const rangeIntersectsRow = (range, row) => range.intersectsRow(row) - const anyRangesIntersectLine = (line) => { + const anyRangesIntersectLine = (ranges, line) => { + // Added/deleted lines mess up the row offset; account for them let row if (line.isAddition()) { row = line.getNewLineNumber() - 1 @@ -96,19 +99,16 @@ export default class GitPackage { lineOffsetSoFar-- } if (line.isDeletion()) row += lineOffsetSoFar - return ranges.some(range => rangeIntersectsRow(range, row)) + return ranges.some(range => range.intersectsRow(row)) } - this.getFileListViewModel().update().then(() => { - const fileDiff = this.getFileListViewModel().getDiffForPathName(filePath) - fileDiff.transact(() => { - fileDiff.getHunks() - .map(hunk => hunk.getLines()) - .reduce((acc, lines) => acc.concat(lines), []) - .filter(line => line.isChanged() && anyRangesIntersectLine(line)) - .forEach(line => line.stage()) - }) - }) + await this.update() + const fileDiff = this.getFileListViewModel().getDiffForPathName(filePath) + const linesToStage = fileDiff.getHunks() + .map(hunk => hunk.getLines()) + .reduce((acc, lines) => acc.concat(lines), []) + .filter(line => line.isChanged() && !line.isStaged() && anyRangesIntersectLine(ranges, line)) + await this.getGitStore().stageLines(linesToStage, true) } hasRepository (): boolean { From 3f795f50128ce7643da35741c6b9eae37f547ffa Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Mon, 28 Mar 2016 19:39:14 -0700 Subject: [PATCH 4/7] Unstage lines if all selected lines are staged Signed-off-by: Michelle Tilley --- lib/git-package.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/git-package.js b/lib/git-package.js index 3f4f6fe5e6..a7b5231505 100644 --- a/lib/git-package.js +++ b/lib/git-package.js @@ -104,11 +104,17 @@ export default class GitPackage { await this.update() const fileDiff = this.getFileListViewModel().getDiffForPathName(filePath) - const linesToStage = fileDiff.getHunks() + const linesChangedInRange = fileDiff.getHunks() .map(hunk => hunk.getLines()) .reduce((acc, lines) => acc.concat(lines), []) - .filter(line => line.isChanged() && !line.isStaged() && anyRangesIntersectLine(ranges, line)) - await this.getGitStore().stageLines(linesToStage, true) + .filter(line => line.isChanged() && anyRangesIntersectLine(ranges, line)) + const linesToStage = linesChangedInRange.filter(line => !line.isStaged()) + + if (linesToStage.length) { + await this.getGitStore().stageLines(linesToStage, true) + } else { + await this.getGitStore().stageLines(linesChangedInRange, false) + } } hasRepository (): boolean { From a0d5c7ae749a5fb63ad7846f6eedac539a45ce99 Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 29 Mar 2016 22:22:38 -0700 Subject: [PATCH 5/7] Add Range property types Signed-off-by: Michelle Tilley --- interfaces/atom.js.flow | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interfaces/atom.js.flow b/interfaces/atom.js.flow index 5acddfeceb..05fb25c2db 100644 --- a/interfaces/atom.js.flow +++ b/interfaces/atom.js.flow @@ -83,6 +83,9 @@ declare module 'atom' { declare class Range { intersectsRow(row: number): boolean; + start: Point; + end: Point; + copy(): Range; } declare class TextBuffer { From 90089d1d9f4098ce2a9ccfb653b310fd14d9d97d Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 29 Mar 2016 22:25:32 -0700 Subject: [PATCH 6/7] Extract selection staging logic to own module Signed-off-by: Michelle Tilley --- lib/editor-ops.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++ lib/git-package.js | 59 +++---------------------------------------- 2 files changed, 66 insertions(+), 55 deletions(-) create mode 100644 lib/editor-ops.js diff --git a/lib/editor-ops.js b/lib/editor-ops.js new file mode 100644 index 0000000000..90d4831ec1 --- /dev/null +++ b/lib/editor-ops.js @@ -0,0 +1,62 @@ +/* @flow */ + +import GitStore from './git-store' +import FileListViewModel from './file-list-view-model' +import FileDiff from './file-diff' +import HunkLine from './hunk-line' + +import type {Range} from 'atom' + +export function stageOrUnstageSelectedLines (gitStore: GitStore, fileListViewModel: FileListViewModel): void { + const editor = atom.workspace.getActiveTextEditor() + if (editor) { + const selectedRanges = editor.getSelectedBufferRanges() + const [_projectPath, relativePath] = editor.project.relativizePath(editor.getPath()) + stageBufferRanges(selectedRanges, relativePath, gitStore, fileListViewModel) + } +} + +export async function stageBufferRanges (ranges: Array, filePath: string, gitStore: GitStore, fileListViewModel: FileListViewModel): Promise { + await gitStore.loadFromGit() + const fileDiff = fileListViewModel.getDiffForPathName(filePath) + const [changedLines, stageOrUnstage] = getChangedLinesAndStagingType(fileDiff, ranges) + await gitStore.stageLines(changedLines, stageOrUnstage) +} + +export function getChangedLinesAndStagingType (fileDiff: FileDiff, ranges: Array): [Array, boolean] { + let lineOffsetSoFar = 0 + + ranges = ranges.map(range => { + // If a range spans at least one line, and ends on column zero we don't + // want to stage the last line (since no text on that line is selected). + // So, copy the range and rewind the ending row by 1. + if (range.end.column === 0 && range.start.row !== range.end.row) { + range = range.copy() + range.end.row = range.end.row - 1 + } + return range + }) + + const anyRangesIntersectLine = (ranges, line) => { + // Added/deleted lines mess up the row offset; account for them + let row + if (line.isAddition()) { + row = line.getNewLineNumber() - 1 + lineOffsetSoFar++ + } else { + row = line.getOldLineNumber() + lineOffsetSoFar-- + } + if (line.isDeletion()) row += lineOffsetSoFar + return ranges.some(range => range.intersectsRow(row)) + } + + const linesChangedInRange = fileDiff.getHunks() + .map(hunk => hunk.getLines()) + .reduce((acc, lines) => acc.concat(lines), []) + .filter(line => line.isChanged() && anyRangesIntersectLine(ranges, line)) + + const linesToStage = linesChangedInRange.filter(line => !line.isStaged()) + + return linesToStage.length ? [linesToStage, true] : [linesChangedInRange, false] +} diff --git a/lib/git-package.js b/lib/git-package.js index a7b5231505..3572df07f3 100644 --- a/lib/git-package.js +++ b/lib/git-package.js @@ -7,8 +7,9 @@ import DiffViewModel from './diff-view-model' import StatusBarViewModel from './status-bar-view-model' import GitService from './git-service' import {DiffURI} from './common' +import {stageOrUnstageSelectedLines} from './editor-ops' -import type {Panel, Pane, Range} from 'atom' +import type {Panel, Pane} from 'atom' import type {StatusBar, Tile} from 'status-bar' import type {ChangeType} from './git-store' @@ -43,8 +44,8 @@ export default class GitPackage { atom.commands.add('atom-workspace', 'git:refresh-status', () => this.update('reload')) - atom.commands.add('atom-workspace', 'git:stage-selected-lines', () => { - this.stageSelectedLines() + atom.commands.add('atom-workspace', 'git:stage-or-unstage-selected-lines', () => { + stageOrUnstageSelectedLines(this.getGitStore(), this.getFileListViewModel()) }) this.update('reload') @@ -65,58 +66,6 @@ export default class GitPackage { }) } - stageSelectedLines (): void { - const editor = atom.workspace.getActiveTextEditor() - if (editor) { - const selectedRanges = editor.getSelectedBufferRanges() - const [_projectPath, relativePath] = editor.project.relativizePath(editor.getPath()) - this.stageBufferRanges(selectedRanges, relativePath) - } - } - - async stageBufferRanges (ranges: Array, filePath: string): void { - let lineOffsetSoFar = 0 - - ranges = ranges.map(range => { - // If a range spans at least one line, and ends on column zero we don't - // want to stage the last line (since no text on that line is selected). - // So, copy the range and rewind the ending row by 1. - if (range.end.column === 0 && range.start.row !== range.end.row) { - range = range.copy() - range.end.row = range.end.row - 1 - } - return range - }) - - const anyRangesIntersectLine = (ranges, line) => { - // Added/deleted lines mess up the row offset; account for them - let row - if (line.isAddition()) { - row = line.getNewLineNumber() - 1 - lineOffsetSoFar++ - } else { - row = line.getOldLineNumber() - lineOffsetSoFar-- - } - if (line.isDeletion()) row += lineOffsetSoFar - return ranges.some(range => range.intersectsRow(row)) - } - - await this.update() - const fileDiff = this.getFileListViewModel().getDiffForPathName(filePath) - const linesChangedInRange = fileDiff.getHunks() - .map(hunk => hunk.getLines()) - .reduce((acc, lines) => acc.concat(lines), []) - .filter(line => line.isChanged() && anyRangesIntersectLine(ranges, line)) - const linesToStage = linesChangedInRange.filter(line => !line.isStaged()) - - if (linesToStage.length) { - await this.getGitStore().stageLines(linesToStage, true) - } else { - await this.getGitStore().stageLines(linesChangedInRange, false) - } - } - hasRepository (): boolean { return atom.project.getRepositories().length > 0 && atom.project.getRepositories()[0] } From aaee6e9f247850f814a1919e9a72c644de78048e Mon Sep 17 00:00:00 2001 From: Katrina Uychaco Date: Tue, 29 Mar 2016 22:25:59 -0700 Subject: [PATCH 7/7] (WIP) Add tests for staging from editor Signed-off-by: Michelle Tilley --- spec/editor-ops-spec.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 spec/editor-ops-spec.js diff --git a/spec/editor-ops-spec.js b/spec/editor-ops-spec.js new file mode 100644 index 0000000000..7dd559293c --- /dev/null +++ b/spec/editor-ops-spec.js @@ -0,0 +1,23 @@ +/** @babel */ + +import {it} from './async-spec-helpers' + +describe('Editor ops', function () { + describe('staging and unstaging lines', () => { + describe('when no highlighted lines are staged', () => { + it('stages changed lines', () => { + + }) + }) + describe('when some highlighted lines are staged and some are unstaged', () => { + it('stages all unstaged changed lines', () => { + + }) + }) + describe('when all highlighted lines are staged', () => { + it('unstages all staged lines', () => { + + }) + }) + }) +})