atom-nuclide
Version:
A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.
314 lines (267 loc) • 13.2 kB
JavaScript
Object.defineProperty(exports, '__esModule', {
value: true
});
/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })();
var _createDecoratedClass = (function () { function defineProperties(target, descriptors, initializers) { for (var i = 0; i < descriptors.length; i++) { var descriptor = descriptors[i]; var decorators = descriptor.decorators; var key = descriptor.key; delete descriptor.key; delete descriptor.decorators; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor || descriptor.initializer) descriptor.writable = true; if (decorators) { for (var f = 0; f < decorators.length; f++) { var decorator = decorators[f]; if (typeof decorator === 'function') { descriptor = decorator(target, key, descriptor) || descriptor; } else { throw new TypeError('The decorator for method ' + descriptor.key + ' is of the invalid type ' + typeof decorator); } } if (descriptor.initializer !== undefined) { initializers[key] = descriptor; continue; } } Object.defineProperty(target, key, descriptor); } } return function (Constructor, protoProps, staticProps, protoInitializers, staticInitializers) { if (protoProps) defineProperties(Constructor.prototype, protoProps, protoInitializers); if (staticProps) defineProperties(Constructor, staticProps, staticInitializers); return Constructor; }; })();
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { var callNext = step.bind(null, 'next'); var callThrow = step.bind(null, 'throw'); function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(callNext, callThrow); } } callNext(); }); }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _nuclideAnalytics2;
function _nuclideAnalytics() {
return _nuclideAnalytics2 = require('../../nuclide-analytics');
}
var _atom2;
function _atom() {
return _atom2 = require('atom');
}
var _assert2;
function _assert() {
return _assert2 = _interopRequireDefault(require('assert'));
}
var _electron2;
function _electron() {
return _electron2 = require('electron');
}
var MS_TO_WAIT_BEFORE_SPINNER = 2000;
var CHANGESET_CSS_CLASS = 'nuclide-blame-hash';
var CLICKABLE_CHANGESET_CSS_CLASS = 'nuclide-blame-hash-clickable';
var HG_CHANGESET_DATA_ATTRIBUTE = 'hgChangeset';
var BLAME_DECORATION_CLASS = 'blame-decoration';
var BlameGutter = (function () {
/**
* @param gutterName A name for this gutter. Must not be used by any another
* gutter in this TextEditor.
* @param editor The TextEditor this BlameGutter should create UI for.
* @param blameProvider The BlameProvider that provides the appropriate blame
* information for this BlameGutter.
*/
function BlameGutter(gutterName, editor, blameProvider) {
var _this = this;
_classCallCheck(this, BlameGutter);
this._isDestroyed = false;
this._isEditorDestroyed = false;
this._subscriptions = new (_atom2 || _atom()).CompositeDisposable();
this._editor = editor;
this._blameProvider = blameProvider;
this._changesetSpanClassName = CHANGESET_CSS_CLASS;
this._bufferLineToDecoration = new Map();
this._gutter = editor.addGutter({ name: gutterName });
var gutterView = atom.views.getView(this._gutter);
gutterView.classList.add('nuclide-blame');
// If getUrlForRevision() is available, add a single, top-level click handler for the gutter.
if (typeof blameProvider.getUrlForRevision === 'function') {
(function () {
// We also want to style the changeset differently if it is clickable.
_this._changesetSpanClassName += ' ' + CLICKABLE_CHANGESET_CSS_CLASS;
var onClick = _this._onClick.bind(_this);
gutterView.addEventListener('click', onClick);
_this._subscriptions.add(_this._gutter.onDidDestroy(function () {
return gutterView.removeEventListener('click', onClick);
}));
})();
}
this._subscriptions.add(editor.onDidDestroy(function () {
_this._isEditorDestroyed = true;
}));
this._fetchAndDisplayBlame();
}
/**
* If the user clicked on a ChangeSet ID, extract it from the DOM element via the data- attribute
* and find the corresponding Differential revision. If successful, open the URL for the revision.
*/
_createDecoratedClass(BlameGutter, [{
key: '_onClick',
value: _asyncToGenerator(function* (e) {
var target = e.target;
if (!target) {
return;
}
var dataset = target.dataset;
var changeset = dataset[HG_CHANGESET_DATA_ATTRIBUTE];
if (!changeset) {
return;
}
var blameProvider = this._blameProvider;
(0, (_assert2 || _assert()).default)(typeof blameProvider.getUrlForRevision === 'function');
var url = yield blameProvider.getUrlForRevision(this._editor, changeset);
if (url) {
// Note that 'shell' is not the public 'shell' package on npm but an Atom built-in.
(_electron2 || _electron()).shell.openExternal(url);
} else {
atom.notifications.addWarning('No URL found for ' + changeset + '.');
}
(0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)('blame-gutter-click-revision', {
editorPath: this._editor.getPath() || '',
url: url || ''
});
})
}, {
key: '_fetchAndDisplayBlame',
value: _asyncToGenerator(function* () {
// Add a loading spinner while we fetch the blame.
this._addLoadingSpinner();
var newBlame = undefined;
try {
newBlame = yield this._blameProvider.getBlameForEditor(this._editor);
} catch (error) {
atom.notifications.addError('Failed to fetch blame to display. ' + 'The file is empty or untracked or the repository cannot be reached.', error);
atom.commands.dispatch(atom.views.getView(this._editor), 'nuclide-blame:hide-blame');
return;
}
// The BlameGutter could have been destroyed while blame was being fetched.
if (this._isDestroyed) {
return;
}
// Remove the loading spinner before setting the contents of the blame gutter.
this._cleanUpLoadingSpinner();
this._updateBlame(newBlame);
})
}, {
key: '_addLoadingSpinner',
value: function _addLoadingSpinner() {
var _this2 = this;
if (this._loadingSpinnerIsPending) {
return;
}
this._loadingSpinnerIsPending = true;
this._loadingSpinnerTimeoutId = window.setTimeout(function () {
_this2._loadingSpinnerIsPending = false;
_this2._loadingSpinnerDiv = document.createElement('div');
_this2._loadingSpinnerDiv.className = 'nuclide-blame-spinner';
var gutterView = atom.views.getView(_this2._gutter);
// $FlowFixMe
gutterView.appendChild(_this2._loadingSpinnerDiv);
}, MS_TO_WAIT_BEFORE_SPINNER);
}
}, {
key: '_cleanUpLoadingSpinner',
value: function _cleanUpLoadingSpinner() {
if (this._loadingSpinnerIsPending) {
window.clearTimeout(this._loadingSpinnerTimeoutId);
this._loadingSpinnerIsPending = false;
}
if (this._loadingSpinnerDiv) {
this._loadingSpinnerDiv.remove();
this._loadingSpinnerDiv = null;
}
}
}, {
key: 'destroy',
value: function destroy() {
this._isDestroyed = true;
this._cleanUpLoadingSpinner();
if (!this._isEditorDestroyed) {
// Due to a bug in the Gutter API, destroying a Gutter after the editor
// has been destroyed results in an exception.
this._gutter.destroy();
}
for (var decoration of this._bufferLineToDecoration.values()) {
decoration.getMarker().destroy();
}
}
// The BlameForEditor completely replaces any previous blame information.
}, {
key: '_updateBlame',
decorators: [(0, (_nuclideAnalytics2 || _nuclideAnalytics()).trackTiming)('blame-ui.blame-gutter.updateBlame')],
value: function _updateBlame(blameForEditor) {
if (blameForEditor.size === 0) {
atom.notifications.addInfo('Found no blame to display. Is this file empty or untracked?\n If not, check for errors in the Nuclide logs local to your repo.');
}
var allPreviousBlamedLines = new Set(this._bufferLineToDecoration.keys());
var longestBlame = 0;
for (var blameInfo of blameForEditor.values()) {
var blameLength = blameInfo.author.length;
if (blameInfo.changeset) {
blameLength += blameInfo.changeset.length + 1;
}
if (blameLength > longestBlame) {
longestBlame = blameLength;
}
}
for (var _ref3 of blameForEditor) {
var _ref2 = _slicedToArray(_ref3, 2);
var bufferLine = _ref2[0];
var blameInfo = _ref2[1];
this._setBlameLine(bufferLine, blameInfo, longestBlame);
allPreviousBlamedLines.delete(bufferLine);
}
// Any lines that weren't in the new blameForEditor are outdated.
for (var oldLine of allPreviousBlamedLines) {
this._removeBlameLine(oldLine);
}
// Update the width of the gutter according to the new contents.
this._updateGutterWidthToCharacterLength(longestBlame);
}
}, {
key: '_updateGutterWidthToCharacterLength',
value: function _updateGutterWidthToCharacterLength(characters) {
var gutterView = atom.views.getView(this._gutter);
gutterView.style.width = characters + 'ch';
}
}, {
key: '_setBlameLine',
value: function _setBlameLine(bufferLine, blameInfo, longestBlame) {
var item = this._createGutterItem(blameInfo, longestBlame);
var decorationProperties = {
type: 'gutter',
gutterName: this._gutter.name,
'class': BLAME_DECORATION_CLASS,
item: item
};
var decoration = this._bufferLineToDecoration.get(bufferLine);
if (!decoration) {
var bufferLineHeadPoint = [bufferLine, 0];
// The range of this Marker doesn't matter, only the line it is on, because
// the Decoration is for a Gutter.
var marker = this._editor.markBufferRange([bufferLineHeadPoint, bufferLineHeadPoint]);
decoration = this._editor.decorateMarker(marker, decorationProperties);
this._bufferLineToDecoration.set(bufferLine, decoration);
} else {
decoration.setProperties(decorationProperties);
}
}
}, {
key: '_removeBlameLine',
value: function _removeBlameLine(bufferLine) {
var blameDecoration = this._bufferLineToDecoration.get(bufferLine);
if (!blameDecoration) {
return;
}
// The recommended way of destroying a decoration is by destroying its marker.
blameDecoration.getMarker().destroy();
this._bufferLineToDecoration.delete(bufferLine);
}
}, {
key: '_createGutterItem',
value: function _createGutterItem(blameInfo, longestBlame) {
var doc = window.document;
var item = doc.createElement('div');
var authorSpan = doc.createElement('span');
authorSpan.innerText = blameInfo.author;
item.appendChild(authorSpan);
if (blameInfo.changeset) {
var numSpaces = longestBlame - blameInfo.author.length - blameInfo.changeset.length;
// Insert non-breaking spaces to ensure the changeset is right-aligned.
// Admittedly, this is a little gross, but it seems better than setting style.width on every
// item that we create and having to give it a special flexbox layout. Hooray monospace!
item.appendChild(doc.createTextNode(' '.repeat(numSpaces)));
var changesetSpan = doc.createElement('span');
changesetSpan.className = this._changesetSpanClassName;
changesetSpan.dataset[HG_CHANGESET_DATA_ATTRIBUTE] = blameInfo.changeset;
changesetSpan.innerText = blameInfo.changeset;
item.appendChild(changesetSpan);
}
return item;
}
}]);
return BlameGutter;
})();
exports.default = BlameGutter;
module.exports = exports.default;