const {CompositeDisposable} = require('atom') const {repositoryForPath} = require('./helpers') const MAX_BUFFER_LENGTH_TO_DIFF = 2 * 1024 * 1024 module.exports = class GitDiffView { constructor (editor) { this.updateDiffs = this.updateDiffs.bind(this) this.editor = editor this.subscriptions = new CompositeDisposable() this.decorations = {} this.markers = [] } start () { const editorElement = this.editor.getElement() this.subscribeToRepository() this.subscriptions.add( this.editor.onDidStopChanging(this.updateDiffs), this.editor.onDidChangePath(this.updateDiffs), atom.project.onDidChangePaths(() => this.subscribeToRepository()), atom.commands.add(editorElement, 'git-diff:move-to-next-diff', () => this.moveToNextDiff()), atom.commands.add(editorElement, 'git-diff:move-to-previous-diff', () => this.moveToPreviousDiff()), atom.config.onDidChange('git-diff.showIconsInEditorGutter', () => this.updateIconDecoration()), atom.config.onDidChange('editor.showLineNumbers', () => this.updateIconDecoration()), editorElement.onDidAttach(() => this.updateIconDecoration()), this.editor.onDidDestroy(() => { this.cancelUpdate() this.removeDecorations() this.subscriptions.dispose() }) ) this.updateIconDecoration() this.scheduleUpdate() } moveToNextDiff () { const cursorLineNumber = this.editor.getCursorBufferPosition().row + 1 let nextDiffLineNumber = null let firstDiffLineNumber = null if (this.diffs) { for (const {newStart} of this.diffs) { if (newStart > cursorLineNumber) { if (nextDiffLineNumber == null) nextDiffLineNumber = newStart - 1 nextDiffLineNumber = Math.min(newStart - 1, nextDiffLineNumber) } if (firstDiffLineNumber == null) firstDiffLineNumber = newStart - 1 firstDiffLineNumber = Math.min(newStart - 1, firstDiffLineNumber) } } // Wrap around to the first diff in the file if (atom.config.get('git-diff.wrapAroundOnMoveToDiff') && nextDiffLineNumber == null) { nextDiffLineNumber = firstDiffLineNumber } this.moveToLineNumber(nextDiffLineNumber) } updateIconDecoration () { const gutter = this.editor.getElement().querySelector('.gutter') if (gutter) { if (atom.config.get('editor.showLineNumbers') && atom.config.get('git-diff.showIconsInEditorGutter')) { gutter.classList.add('git-diff-icon') } else { gutter.classList.remove('git-diff-icon') } } } moveToPreviousDiff () { const cursorLineNumber = this.editor.getCursorBufferPosition().row + 1 let previousDiffLineNumber = -1 let lastDiffLineNumber = -1 if (this.diffs) { for (const {newStart} of this.diffs) { if (newStart < cursorLineNumber) { previousDiffLineNumber = Math.max(newStart - 1, previousDiffLineNumber) } lastDiffLineNumber = Math.max(newStart - 1, lastDiffLineNumber) } } // Wrap around to the last diff in the file if (atom.config.get('git-diff.wrapAroundOnMoveToDiff') && previousDiffLineNumber === -1) { previousDiffLineNumber = lastDiffLineNumber } this.moveToLineNumber(previousDiffLineNumber) } moveToLineNumber (lineNumber) { if (lineNumber != null && lineNumber >= 0) { this.editor.setCursorBufferPosition([lineNumber, 0]) this.editor.moveToFirstCharacterOfLine() } } subscribeToRepository () { this.repository = repositoryForPath(this.editor.getPath()) if (this.repository) { this.subscriptions.add(this.repository.onDidChangeStatuses(() => { this.scheduleUpdate() })) this.subscriptions.add(this.repository.onDidChangeStatus(changedPath => { if (changedPath === this.editor.getPath()) this.scheduleUpdate() })) } } cancelUpdate () { clearImmediate(this.immediateId) } scheduleUpdate () { this.cancelUpdate() this.immediateId = setImmediate(this.updateDiffs) } updateDiffs () { if (this.editor.isDestroyed()) return this.removeDecorations() const path = this.editor && this.editor.getPath() if (path && this.editor.getBuffer().getLength() < MAX_BUFFER_LENGTH_TO_DIFF) { this.diffs = this.repository && this.repository.getLineDiffs(path, this.editor.getText()) if (this.diffs) this.addDecorations(this.diffs) } } addDecorations (diffs) { for (const {newStart, oldLines, newLines} of diffs) { const startRow = newStart - 1 const endRow = (newStart + newLines) - 1 if (oldLines === 0 && newLines > 0) { this.markRange(startRow, endRow, 'git-line-added') } else if (newLines === 0 && oldLines > 0) { if (startRow < 0) { this.markRange(0, 0, 'git-previous-line-removed') } else { this.markRange(startRow, startRow, 'git-line-removed') } } else { this.markRange(startRow, endRow, 'git-line-modified') } } } removeDecorations () { for (let marker of this.markers) marker.destroy() this.markers = [] } markRange (startRow, endRow, klass) { const marker = this.editor.markBufferRange([[startRow, 0], [endRow, 0]], {invalidate: 'never'}) this.editor.decorateMarker(marker, {type: 'line-number', class: klass}) this.markers.push(marker) } }