diff --git a/interfaces/atom.js.flow b/interfaces/atom.js.flow index 002d270e87..05fb25c2db 100644 --- a/interfaces/atom.js.flow +++ b/interfaces/atom.js.flow @@ -81,6 +81,13 @@ declare module 'atom' { destroyItem(item: any): boolean; } + declare class Range { + intersectsRow(row: number): boolean; + start: Point; + end: Point; + copy(): Range; + } + declare class TextBuffer { onDidSave(fn: Function): Disposable; onDidReload(fn: Function): Disposable; 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 b7be3808b1..3572df07f3 100644 --- a/lib/git-package.js +++ b/lib/git-package.js @@ -7,6 +7,7 @@ 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} from 'atom' import type {StatusBar, Tile} from 'status-bar' @@ -43,6 +44,10 @@ export default class GitPackage { atom.commands.add('atom-workspace', 'git:refresh-status', () => this.update('reload')) + atom.commands.add('atom-workspace', 'git:stage-or-unstage-selected-lines', () => { + stageOrUnstageSelectedLines(this.getGitStore(), this.getFileListViewModel()) + }) + this.update('reload') if (state.panelVisible) { 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', () => { + + }) + }) + }) +})