git-diff-view.js
1 const { CompositeDisposable } = require('atom'); 2 const { repositoryForPath } = require('./helpers'); 3 4 const MAX_BUFFER_LENGTH_TO_DIFF = 2 * 1024 * 1024; 5 6 module.exports = class GitDiffView { 7 constructor(editor) { 8 this.updateDiffs = this.updateDiffs.bind(this); 9 this.editor = editor; 10 this.subscriptions = new CompositeDisposable(); 11 this.decorations = {}; 12 this.markers = []; 13 } 14 15 start() { 16 const editorElement = this.editor.getElement(); 17 18 this.subscribeToRepository(); 19 20 this.subscriptions.add( 21 this.editor.onDidStopChanging(this.updateDiffs), 22 this.editor.onDidChangePath(this.updateDiffs), 23 atom.project.onDidChangePaths(() => this.subscribeToRepository()), 24 atom.commands.add(editorElement, 'git-diff:move-to-next-diff', () => 25 this.moveToNextDiff() 26 ), 27 atom.commands.add(editorElement, 'git-diff:move-to-previous-diff', () => 28 this.moveToPreviousDiff() 29 ), 30 atom.config.onDidChange('git-diff.showIconsInEditorGutter', () => 31 this.updateIconDecoration() 32 ), 33 atom.config.onDidChange('editor.showLineNumbers', () => 34 this.updateIconDecoration() 35 ), 36 editorElement.onDidAttach(() => this.updateIconDecoration()), 37 this.editor.onDidDestroy(() => { 38 this.cancelUpdate(); 39 this.removeDecorations(); 40 this.subscriptions.dispose(); 41 }) 42 ); 43 44 this.updateIconDecoration(); 45 this.scheduleUpdate(); 46 } 47 48 moveToNextDiff() { 49 const cursorLineNumber = this.editor.getCursorBufferPosition().row + 1; 50 let nextDiffLineNumber = null; 51 let firstDiffLineNumber = null; 52 if (this.diffs) { 53 for (const { newStart } of this.diffs) { 54 if (newStart > cursorLineNumber) { 55 if (nextDiffLineNumber == null) nextDiffLineNumber = newStart - 1; 56 nextDiffLineNumber = Math.min(newStart - 1, nextDiffLineNumber); 57 } 58 59 if (firstDiffLineNumber == null) firstDiffLineNumber = newStart - 1; 60 firstDiffLineNumber = Math.min(newStart - 1, firstDiffLineNumber); 61 } 62 } 63 64 // Wrap around to the first diff in the file 65 if ( 66 atom.config.get('git-diff.wrapAroundOnMoveToDiff') && 67 nextDiffLineNumber == null 68 ) { 69 nextDiffLineNumber = firstDiffLineNumber; 70 } 71 72 this.moveToLineNumber(nextDiffLineNumber); 73 } 74 75 updateIconDecoration() { 76 const gutter = this.editor.getElement().querySelector('.gutter'); 77 if (gutter) { 78 if ( 79 atom.config.get('editor.showLineNumbers') && 80 atom.config.get('git-diff.showIconsInEditorGutter') 81 ) { 82 gutter.classList.add('git-diff-icon'); 83 } else { 84 gutter.classList.remove('git-diff-icon'); 85 } 86 } 87 } 88 89 moveToPreviousDiff() { 90 const cursorLineNumber = this.editor.getCursorBufferPosition().row + 1; 91 let previousDiffLineNumber = -1; 92 let lastDiffLineNumber = -1; 93 if (this.diffs) { 94 for (const { newStart } of this.diffs) { 95 if (newStart < cursorLineNumber) { 96 previousDiffLineNumber = Math.max( 97 newStart - 1, 98 previousDiffLineNumber 99 ); 100 } 101 lastDiffLineNumber = Math.max(newStart - 1, lastDiffLineNumber); 102 } 103 } 104 105 // Wrap around to the last diff in the file 106 if ( 107 atom.config.get('git-diff.wrapAroundOnMoveToDiff') && 108 previousDiffLineNumber === -1 109 ) { 110 previousDiffLineNumber = lastDiffLineNumber; 111 } 112 113 this.moveToLineNumber(previousDiffLineNumber); 114 } 115 116 moveToLineNumber(lineNumber) { 117 if (lineNumber != null && lineNumber >= 0) { 118 this.editor.setCursorBufferPosition([lineNumber, 0]); 119 this.editor.moveToFirstCharacterOfLine(); 120 } 121 } 122 123 subscribeToRepository() { 124 this.repository = repositoryForPath(this.editor.getPath()); 125 if (this.repository) { 126 this.subscriptions.add( 127 this.repository.onDidChangeStatuses(() => { 128 this.scheduleUpdate(); 129 }) 130 ); 131 this.subscriptions.add( 132 this.repository.onDidChangeStatus(changedPath => { 133 if (changedPath === this.editor.getPath()) this.scheduleUpdate(); 134 }) 135 ); 136 } 137 } 138 139 cancelUpdate() { 140 clearImmediate(this.immediateId); 141 } 142 143 scheduleUpdate() { 144 this.cancelUpdate(); 145 this.immediateId = setImmediate(this.updateDiffs); 146 } 147 148 updateDiffs() { 149 if (this.editor.isDestroyed()) return; 150 this.removeDecorations(); 151 const path = this.editor && this.editor.getPath(); 152 if ( 153 path && 154 this.editor.getBuffer().getLength() < MAX_BUFFER_LENGTH_TO_DIFF 155 ) { 156 this.diffs = 157 this.repository && 158 this.repository.getLineDiffs(path, this.editor.getText()); 159 if (this.diffs) this.addDecorations(this.diffs); 160 } 161 } 162 163 addDecorations(diffs) { 164 for (const { newStart, oldLines, newLines } of diffs) { 165 const startRow = newStart - 1; 166 const endRow = newStart + newLines - 1; 167 if (oldLines === 0 && newLines > 0) { 168 this.markRange(startRow, endRow, 'git-line-added'); 169 } else if (newLines === 0 && oldLines > 0) { 170 if (startRow < 0) { 171 this.markRange(0, 0, 'git-previous-line-removed'); 172 } else { 173 this.markRange(startRow, startRow, 'git-line-removed'); 174 } 175 } else { 176 this.markRange(startRow, endRow, 'git-line-modified'); 177 } 178 } 179 } 180 181 removeDecorations() { 182 for (let marker of this.markers) marker.destroy(); 183 this.markers = []; 184 } 185 186 markRange(startRow, endRow, klass) { 187 const marker = this.editor.markBufferRange([[startRow, 0], [endRow, 0]], { 188 invalidate: 'never' 189 }); 190 this.editor.decorateMarker(marker, { type: 'line-number', class: klass }); 191 this.markers.push(marker); 192 } 193 };