mirror of https://github.com/atom/atom.git
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:
parent
42f24335d6
commit
b079194478
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue