Fix git diff subscriptions (#21968)

The startup script now uses a `Set` to manage `GitDiffView`s held in memory and destroy them when `deactivate` is called.
There are now four major subscription blocks. 
1. The outer subscriptions held by `activate`.
2. The per-editor subscriptions held within `activate`.
3. The per-editor repository event subscriptions held within each `GitDIffView` instance.
4. The per-editor modification event subscriptions held within each `GitDiffView` are only active when the editor content is bound to a valid git repository.

Teardowns of any editor or the module now result in `disposal` of the respective editor's subscriptions or all subscriptions authored within the module.

I removed some of `GitDiffView`'s unnecessary methods such as the `start`, `cancleUpdate`, `addDecoration` and `removeDecorations`;
The last two methods were combined into the body of `updateDiffs`.
`scheduleUpdate` now calls `requestAnimationFrame` instead of `setImmediate` because it's native, standard, and yields
to other more important browser processes. I know Atom Core implements setImmediate, but rAF seems to work just as fast if not faster.
The memory management of the editor markers and diffs have been joined using a WeakMap. When the diffs are destroyed,
so too are the editor markers.
Finally, I added the `destroy` method to handle the teardown of subscriptions and other destroyable objects contained within the `GitDiffViews` before object release.
This commit is contained in:
Ruby Allison Rose 2021-03-08 10:12:07 -08:00 committed by GitHub
parent 42f24335d6
commit b079194478
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 461 additions and 267 deletions

View File

@ -1,7 +1,9 @@
const SelectListView = require('atom-select-list');
const { repositoryForPath } = require('./helpers');
'use babel';
module.exports = class DiffListView {
import SelectListView from 'atom-select-list';
import repositoryForPath from './helpers';
export default class DiffListView {
constructor() {
this.selectListView = new SelectListView({
emptyMessage: 'No diffs in file',
@ -71,7 +73,7 @@ module.exports = class DiffListView {
this.cancel();
} else if (editor) {
this.editor = editor;
const repository = repositoryForPath(this.editor.getPath());
const repository = await repositoryForPath(this.editor.getPath());
let diffs = repository
? repository.getLineDiffs(this.editor.getPath(), this.editor.getText())
: [];
@ -86,4 +88,4 @@ module.exports = class DiffListView {
this.attach();
}
}
};
}

View File

@ -1,64 +1,157 @@
const { CompositeDisposable } = require('atom');
const { repositoryForPath } = require('./helpers');
'use babel';
import { CompositeDisposable } from 'atom';
import repositoryForPath from './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;
/**
* @describe Handles per-editor event and repository subscriptions.
* @param editor {Atom.TextEditor} - The editor this view will manage.
*/
export default class GitDiffView {
constructor(editor, editorElement) {
// These are the only members guaranteed to exist.
this.subscriptions = new CompositeDisposable();
this.decorations = {};
this.markers = [];
}
this.editor = editor;
this.editorElement = editorElement;
this.repository = null;
this.markers = new Map();
start() {
const editorElement = this.editor.getElement();
// Assign `null` to all possible child vars here so the JS engine doesn't
// have to re-evaluate the microcode when we do eventually need them.
this.releaseChildren();
this.subscribeToRepository();
// I know this looks janky but it works. Class methods are available
// before the constructor is executed. It's a micro-opt above lambdas.
const subscribeToRepository = this.subscribeToRepository.bind(this);
// WARNING: This gets handed to requestAnimationFrame, so it must be bound.
this.updateDiffs = this.updateDiffs.bind(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();
})
atom.project.onDidChangePaths(subscribeToRepository)
);
}
this.updateIconDecoration();
this.scheduleUpdate();
/**
* @describe Handles tear down of destructables and subscriptions.
* Does not handle release of memory. This method should only be called
* just before this object is freed, and should only tear down the main
* object components that are guarunteed to exist at all times.
*/
destroy() {
this.subscriptions.dispose();
this.destroyChildren();
this.markers.clear();
}
/**
* @describe Destroys this objects children (non-freeing), it's intended
* to be an ease-of use function for maintaing this object. This method
* should only tear down objects that are selectively allocated upon
* repository discovery.
*
* Example: this.diffs only exists when we have a repository.
*/
destroyChildren() {
if (this._animationId) cancelAnimationFrame(this._animationId);
if (this.diffs)
for (const diff of this.diffs) this.markers.get(diff).destroy();
}
/**
* @describe The memory releasing complement function of `destroyChildren`.
* frees the memory allocated at all child object storage locations
* when there is no repository.
*/
releaseChildren() {
this.diffs = null;
this._repoSubs = null;
this._animationId = null;
this.editorPath = null;
this.buffer = null;
}
/**
* @describe handles all subscriptions based on the repository in focus
*/
async subscribeToRepository() {
if (this._repoSubs !== null) {
this._repoSubs.dispose();
this.subscriptions.remove(this._repoSubs);
}
// Don't cache the path unless we know we need it.
let editorPath = this.editor.getPath();
this.repository = await repositoryForPath(editorPath);
if (this.repository !== null) {
this.editorPath = editorPath;
this.buffer = this.editor.getBuffer();
const subscribeToRepository = this.subscribeToRepository.bind(this);
const updateIconDecoration = this.updateIconDecoration.bind(this);
const scheduleUpdate = this.scheduleUpdate.bind(this);
this._repoSubs = new CompositeDisposable(
this.repository.onDidDestroy(subscribeToRepository),
this.repository.onDidChangeStatuses(scheduleUpdate),
this.repository.onDidChangeStatus(changedPath => {
if (changedPath === this.editorPath) scheduleUpdate();
}),
this.editor.onDidStopChanging(scheduleUpdate),
this.editor.onDidChangePath(() => {
this.editorPath = this.edtior.getPath();
this.buffer = this.editor.getBuffer();
scheduleUpdate();
}),
atom.commands.add(
this.editorElement,
'git-diff:move-to-next-diff',
this.moveToNextDiff.bind(this)
),
atom.commands.add(
this.editorElement,
'git-diff:move-to-previous-diff',
this.moveToPreviousDiff.bind(this)
),
atom.config.onDidChange(
'git-diff.showIconsInEditorGutter',
updateIconDecoration
),
atom.config.onDidChange('editor.showLineNumbers', updateIconDecoration),
this.editorElement.onDidAttach(updateIconDecoration)
);
// Every time the repo is changed, the editor needs to be reinitialized.
this.subscriptions.add(this._repoSubs);
updateIconDecoration();
scheduleUpdate();
} else {
this.destroyChildren();
this.releaseChildren();
}
}
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);
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
@ -72,8 +165,30 @@ module.exports = class GitDiffView {
this.moveToLineNumber(nextDiffLineNumber);
}
moveToPreviousDiff() {
const cursorLineNumber = this.editor.getCursorBufferPosition().row + 1;
let previousDiffLineNumber = null;
let lastDiffLineNumber = null;
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 === null
) {
previousDiffLineNumber = lastDiffLineNumber;
}
this.moveToLineNumber(previousDiffLineNumber);
}
updateIconDecoration() {
const gutter = this.editor.getElement().querySelector('.gutter');
const gutter = this.editorElement.querySelector('.gutter');
if (gutter) {
if (
atom.config.get('editor.showLineNumbers') &&
@ -86,108 +201,67 @@ module.exports = class GitDiffView {
}
}
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) {
if (lineNumber !== null) {
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);
// Use Chromium native requestAnimationFrame because it yields
// to the browser, is standard and doesn't involve extra JS overhead.
if (this._animationId) cancelAnimationFrame(this._animationId);
this._animationId = requestAnimationFrame(this.updateDiffs);
}
/**
* @describe Uses text markers in the target editor to visualize
* git modifications, additions, and deletions. The current algorithm
* just redraws the markers each call.
*/
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);
}
}
if (this.buffer.getLength() < MAX_BUFFER_LENGTH_TO_DIFF) {
// Before we redraw the diffs, tear down the old markers.
if (this.diffs)
for (const diff of this.diffs) this.markers.get(diff).destroy();
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');
this.markers.clear();
const text = this.buffer.getText();
this.diffs = this.repository.getLineDiffs(this.editorPath, text);
this.diffs = this.diffs || []; // Sanitize type to array.
for (const diff of this.diffs) {
const { newStart, oldLines, newLines } = diff;
const startRow = newStart - 1;
const endRow = newStart + newLines - 1;
let mark;
if (oldLines === 0 && newLines > 0) {
mark = this.markRange(startRow, endRow, 'git-line-added');
} else if (newLines === 0 && oldLines > 0) {
if (startRow < 0) {
mark = this.markRange(0, 0, 'git-previous-line-removed');
} else {
mark = this.markRange(startRow, startRow, 'git-line-removed');
}
} else {
this.markRange(startRow, startRow, 'git-line-removed');
mark = this.markRange(startRow, endRow, 'git-line-modified');
}
} else {
this.markRange(startRow, endRow, 'git-line-modified');
this.markers.set(diff, mark);
}
}
}
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);
return marker;
}
};
}

View File

@ -1,11 +1,10 @@
exports.repositoryForPath = function(goalPath) {
const directories = atom.project.getDirectories();
const repositories = atom.project.getRepositories();
for (let i = 0; i < directories.length; i++) {
const directory = directories[i];
'use babel';
export default async function(goalPath) {
for (const directory of atom.project.getDirectories()) {
if (goalPath === directory.getPath() || directory.contains(goalPath)) {
return repositories[i];
return atom.project.repositoryForDirectory(directory);
}
}
return null;
};
}

View File

@ -1,32 +1,52 @@
const GitDiffView = require('./git-diff-view');
const DiffListView = require('./diff-list-view');
'use babel';
import { CompositeDisposable } from 'atom';
import GitDiffView from './git-diff-view';
import DiffListView from './diff-list-view';
let diffListView = null;
let diffViews = new Set();
let subscriptions = null;
module.exports = {
activate() {
const watchedEditors = new WeakSet();
export default {
activate(state) {
subscriptions = new CompositeDisposable();
atom.workspace.observeTextEditors(editor => {
if (watchedEditors.has(editor)) return;
subscriptions.add(
atom.workspace.observeTextEditors(editor => {
const editorElement = atom.views.getView(editor);
const diffView = new GitDiffView(editor, editorElement);
new GitDiffView(editor).start();
atom.commands.add(
atom.views.getView(editor),
'git-diff:toggle-diff-list',
() => {
if (diffListView == null) diffListView = new DiffListView();
diffListView.toggle();
}
);
diffViews.add(diffView);
watchedEditors.add(editor);
editor.onDidDestroy(() => watchedEditors.delete(editor));
});
const listViewCommand = 'git-diff:toggle-diff-list';
const editorSubs = new CompositeDisposable(
atom.commands.add(editorElement, listViewCommand, () => {
if (diffListView == null) diffListView = new DiffListView();
diffListView.toggle();
}),
editor.onDidDestroy(() => {
diffView.destroy();
diffViews.delete(diffView);
editorSubs.dispose();
subscriptions.remove(editorSubs);
})
);
subscriptions.add(editorSubs);
})
);
},
deactivate() {
if (diffListView) diffListView.destroy();
diffListView = null;
for (const diffView of diffViews) diffView.destroy();
diffViews.clear();
subscriptions.dispose();
subscriptions = null;
}
};

View File

@ -9,27 +9,11 @@
"atom": "*"
},
"dependencies": {
"atom-select-list": "^0.7.0",
"fs-plus": "^3.0.0",
"temp": "~0.8.1"
"atom-select-list": "^0.7.0"
},
"devDependencies": {
"standard": "^11.0.0"
},
"standard": {
"ignore": [
"spec/fixtures/working-dir/sample.js"
],
"env": {
"atomtest": true,
"browser": true,
"jasmine": true,
"node": true
},
"globals": [
"atom",
"snapshotResult"
]
"fs-plus": "^3.0.0",
"temp": "~0.8.1"
},
"configSchema": {
"showIconsInEditorGutter": {

View File

@ -1,6 +1,6 @@
const path = require('path');
const fs = require('fs-plus');
const temp = require('temp');
const temp = require('temp').track();
describe('git-diff:toggle-diff-list', () => {
let diffListView, editor;

View File

@ -1,12 +1,17 @@
const path = require('path');
const fs = require('fs-plus');
const temp = require('temp');
const temp = require('temp').track();
describe('GitDiff package', () => {
let editor, editorElement, projectPath;
let editor, editorElement, projectPath, screenUpdates;
beforeEach(() => {
spyOn(window, 'setImmediate').andCallFake(fn => fn());
screenUpdates = 0;
spyOn(window, 'requestAnimationFrame').andCallFake(fn => {
fn();
screenUpdates++;
});
spyOn(window, 'cancelAnimationFrame').andCallFake(i => null);
projectPath = temp.mkdirSync('git-diff-spec-');
const otherPath = temp.mkdirSync('some-other-path-');
@ -20,16 +25,26 @@ describe('GitDiff package', () => {
jasmine.attachToDOM(atom.workspace.getElement());
waitsForPromise(() =>
atom.workspace.open(path.join(projectPath, 'sample.js'))
);
waitsForPromise(async () => {
await atom.workspace.open(path.join(projectPath, 'sample.js'));
await atom.packages.activatePackage('git-diff');
});
runs(() => {
editor = atom.workspace.getActiveTextEditor();
editorElement = editor.getElement();
editorElement = atom.views.getView(editor);
});
});
waitsForPromise(() => atom.packages.activatePackage('git-diff'));
afterEach(() => {
temp.cleanup();
});
describe('when the editor has no changes', () => {
it("doesn't mark the editor", () => {
waitsFor(() => screenUpdates > 0);
runs(() => expect(editor.getMarkers().length).toBe(0));
});
});
describe('when the editor has modified lines', () => {
@ -39,13 +54,17 @@ describe('GitDiff package', () => {
);
editor.insertText('a');
advanceClock(editor.getBuffer().stoppedChangingDelay);
expect(editorElement.querySelectorAll('.git-line-modified').length).toBe(
1
);
expect(editorElement.querySelector('.git-line-modified')).toHaveData(
'buffer-row',
0
);
waitsFor(() => editor.getMarkers().length > 0);
runs(() => {
expect(
editorElement.querySelectorAll('.git-line-modified').length
).toBe(1);
expect(editorElement.querySelector('.git-line-modified')).toHaveData(
'buffer-row',
0
);
});
});
});
@ -56,11 +75,16 @@ describe('GitDiff package', () => {
editor.insertNewline();
editor.insertText('a');
advanceClock(editor.getBuffer().stoppedChangingDelay);
expect(editorElement.querySelectorAll('.git-line-added').length).toBe(1);
expect(editorElement.querySelector('.git-line-added')).toHaveData(
'buffer-row',
1
);
waitsFor(() => editor.getMarkers().length > 0);
runs(() => {
expect(editorElement.querySelectorAll('.git-line-added').length).toBe(
1
);
expect(editorElement.querySelector('.git-line-added')).toHaveData(
'buffer-row',
1
);
});
});
});
@ -70,13 +94,16 @@ describe('GitDiff package', () => {
editor.setCursorBufferPosition([5]);
editor.deleteLine();
advanceClock(editor.getBuffer().stoppedChangingDelay);
expect(editorElement.querySelectorAll('.git-line-removed').length).toBe(
1
);
expect(editorElement.querySelector('.git-line-removed')).toHaveData(
'buffer-row',
4
);
waitsFor(() => editor.getMarkers().length > 0);
runs(() => {
expect(editorElement.querySelectorAll('.git-line-removed').length).toBe(
1
);
expect(editorElement.querySelector('.git-line-removed')).toHaveData(
'buffer-row',
4
);
});
});
});
@ -86,12 +113,15 @@ describe('GitDiff package', () => {
editor.setCursorBufferPosition([0, 0]);
editor.deleteLine();
advanceClock(editor.getBuffer().stoppedChangingDelay);
expect(
editorElement.querySelectorAll('.git-previous-line-removed').length
).toBe(1);
expect(
editorElement.querySelector('.git-previous-line-removed')
).toHaveData('buffer-row', 0);
waitsFor(() => editor.getMarkers().length > 0);
runs(() => {
expect(
editorElement.querySelectorAll('.git-previous-line-removed').length
).toBe(1);
expect(
editorElement.querySelector('.git-previous-line-removed')
).toHaveData('buffer-row', 0);
});
});
});
@ -102,14 +132,24 @@ describe('GitDiff package', () => {
);
editor.insertText('a');
advanceClock(editor.getBuffer().stoppedChangingDelay);
expect(editorElement.querySelectorAll('.git-line-modified').length).toBe(
1
waitsFor(
() => editorElement.querySelectorAll('.git-line-modified').length > 0
);
editor.backspace();
advanceClock(editor.getBuffer().stoppedChangingDelay);
expect(editorElement.querySelectorAll('.git-line-modified').length).toBe(
0
runs(() => {
expect(
editorElement.querySelectorAll('.git-line-modified').length
).toBe(1);
editor.backspace();
advanceClock(editor.getBuffer().stoppedChangingDelay);
});
waitsFor(
() => editorElement.querySelectorAll('.git-line-modified').length < 1
);
runs(() => {
expect(
editorElement.querySelectorAll('.git-line-modified').length
).toBe(0);
});
});
});
@ -119,21 +159,17 @@ describe('GitDiff package', () => {
path.join(projectPath, 'sample.txt'),
'Some different text.'
);
let nextTick = false;
waitsForPromise(() =>
atom.workspace.open(path.join(projectPath, 'sample.txt'))
);
runs(() => {
editorElement = atom.workspace.getActiveTextEditor().getElement();
editor = atom.workspace.getActiveTextEditor();
editorElement = editor.getElement();
});
setImmediate(() => {
nextTick = true;
});
waitsFor(() => nextTick);
waitsFor(() => editor.getMarkers().length > 0);
runs(() => {
expect(
@ -152,39 +188,49 @@ describe('GitDiff package', () => {
editor.deleteLine();
atom.project.setPaths([temp.mkdirSync('no-repository')]);
advanceClock(editor.getBuffer().stoppedChangingDelay);
waitsFor(() => editor.getMarkers().length === 0);
runs(() => {
expect(editor.getMarkers().length).toBe(0);
});
});
});
describe('move-to-next-diff/move-to-previous-diff events', () => {
it('moves the cursor to first character of the next/previous diff line', () => {
editor.insertText('a');
editor.setCursorBufferPosition([5]);
editor.deleteLine();
advanceClock(editor.getBuffer().stoppedChangingDelay);
waitsFor(() => editor.getMarkers().length > 0);
runs(() => {
editor.setCursorBufferPosition([5]);
editor.deleteLine();
advanceClock(editor.getBuffer().stoppedChangingDelay);
editor.setCursorBufferPosition([0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
editor.setCursorBufferPosition([0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff');
expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff');
expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
});
});
it('wraps around to the first/last diff in the file', () => {
editor.insertText('a');
editor.setCursorBufferPosition([5]);
editor.deleteLine();
advanceClock(editor.getBuffer().stoppedChangingDelay);
waitsFor(() => editor.getMarkers().length > 0);
runs(() => {
editor.setCursorBufferPosition([5]);
editor.deleteLine();
advanceClock(editor.getBuffer().stoppedChangingDelay);
editor.setCursorBufferPosition([0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
editor.setCursorBufferPosition([0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition().toArray()).toEqual([4, 4]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition().toArray()).toEqual([0, 0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff');
expect(editor.getCursorBufferPosition().toArray()).toEqual([4, 4]);
});
});
describe('when the wrapAroundOnMoveToDiff config option is false', () => {
@ -197,19 +243,28 @@ describe('GitDiff package', () => {
editor.setCursorBufferPosition([5]);
editor.deleteLine();
advanceClock(editor.getBuffer().stoppedChangingDelay);
waitsFor(() => editor.getMarkers().length > 0);
editor.setCursorBufferPosition([0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
runs(() => {
editor.setCursorBufferPosition([0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-next-diff');
expect(editor.getCursorBufferPosition()).toEqual([4, 4]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff');
expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
atom.commands.dispatch(
editorElement,
'git-diff:move-to-previous-diff'
);
expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
atom.commands.dispatch(editorElement, 'git-diff:move-to-previous-diff');
expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
atom.commands.dispatch(
editorElement,
'git-diff:move-to-previous-diff'
);
expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
});
});
});
});
@ -219,28 +274,40 @@ describe('GitDiff package', () => {
atom.config.set('git-diff.showIconsInEditorGutter', true);
});
it('the gutter has a git-diff-icon class', () =>
expect(editorElement.querySelector('.gutter')).toHaveClass(
'git-diff-icon'
));
it('the gutter has a git-diff-icon class', () => {
waitsFor(() => screenUpdates > 0);
runs(() => {
expect(editorElement.querySelector('.gutter')).toHaveClass(
'git-diff-icon'
);
});
});
it('keeps the git-diff-icon class when editor.showLineNumbers is toggled', () => {
atom.config.set('editor.showLineNumbers', false);
expect(editorElement.querySelector('.gutter')).not.toHaveClass(
'git-diff-icon'
);
waitsFor(() => screenUpdates > 0);
atom.config.set('editor.showLineNumbers', true);
expect(editorElement.querySelector('.gutter')).toHaveClass(
'git-diff-icon'
);
runs(() => {
atom.config.set('editor.showLineNumbers', false);
expect(editorElement.querySelector('.gutter')).not.toHaveClass(
'git-diff-icon'
);
atom.config.set('editor.showLineNumbers', true);
expect(editorElement.querySelector('.gutter')).toHaveClass(
'git-diff-icon'
);
});
});
it('removes the git-diff-icon class when the showIconsInEditorGutter config option set to false', () => {
atom.config.set('git-diff.showIconsInEditorGutter', false);
expect(editorElement.querySelector('.gutter')).not.toHaveClass(
'git-diff-icon'
);
waitsFor(() => screenUpdates > 0);
runs(() => {
atom.config.set('git-diff.showIconsInEditorGutter', false);
expect(editorElement.querySelector('.gutter')).not.toHaveClass(
'git-diff-icon'
);
});
});
});
});

View File

@ -0,0 +1,48 @@
const path = require('path');
const fs = require('fs-plus');
const temp = require('temp').track();
const commands = [
'git-diff:toggle-diff-list',
'git-diff:move-to-next-diff',
'git-diff:move-to-previous-diff'
];
describe('git-diff', () => {
let editor, element;
beforeEach(() => {
const projectPath = temp.mkdirSync('git-diff-spec-');
fs.copySync(path.join(__dirname, 'fixtures', 'working-dir'), projectPath);
fs.moveSync(
path.join(projectPath, 'git.git'),
path.join(projectPath, '.git')
);
atom.project.setPaths([projectPath]);
jasmine.attachToDOM(atom.workspace.getElement());
waitsForPromise(() => atom.workspace.open('sample.js'));
runs(() => {
editor = atom.workspace.getActiveTextEditor();
element = atom.views.getView(editor);
});
});
describe('When the module is deactivated', () => {
it('removes all registered command hooks after deactivation.', () => {
waitsForPromise(() => atom.packages.activatePackage('git-diff'));
waitsForPromise(() => atom.packages.deactivatePackage('git-diff'));
runs(() => {
// NOTE: don't use enable and disable from the Public API.
expect(atom.packages.isPackageActive('git-diff')).toBe(false);
atom.commands
.findCommands({ target: element })
.filter(({ name }) => commands.includes(name))
.forEach(command => expect(commands).not.toContain(command.name));
});
});
});
});