/ packages / git-diff / lib / git-diff-view.js
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  };