UNPKG

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.

415 lines (365 loc) 15.5 kB
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 _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); 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 _atom2; function _atom() { return _atom2 = require('atom'); } var _reactForAtom2; function _reactForAtom() { return _reactForAtom2 = require('react-for-atom'); } var _commonsNodeDebounce2; function _commonsNodeDebounce() { return _commonsNodeDebounce2 = _interopRequireDefault(require('../../commons-node/debounce')); } var _commonsNodeCollection2; function _commonsNodeCollection() { return _commonsNodeCollection2 = require('../../commons-node/collection'); } var _nuclideAnalytics2; function _nuclideAnalytics() { return _nuclideAnalytics2 = require('../../nuclide-analytics'); } var _nuclideLogging2; function _nuclideLogging() { return _nuclideLogging2 = require('../../nuclide-logging'); } var _DatatipComponent2; function _DatatipComponent() { return _DatatipComponent2 = require('./DatatipComponent'); } var _PinnedDatatip2; function _PinnedDatatip() { return _PinnedDatatip2 = require('./PinnedDatatip'); } var _commonsAtomFeatureConfig2; function _commonsAtomFeatureConfig() { return _commonsAtomFeatureConfig2 = _interopRequireDefault(require('../../commons-atom/featureConfig')); } var logger = (0, (_nuclideLogging2 || _nuclideLogging()).getLogger)(); var DatatipManager = (function () { function DatatipManager() { var _this = this; _classCallCheck(this, DatatipManager); this._boundHideDatatip = this.hideDatatip.bind(this); this._subscriptions = new (_atom2 || _atom()).CompositeDisposable(); this._subscriptions.add(atom.commands.add('atom-text-editor', 'nuclide-datatip:toggle', this.toggleDatatip.bind(this))); this._subscriptions.add(atom.commands.add('atom-text-editor', 'core:cancel', this._boundHideDatatip)); this._debouncedMouseMove = function () {}; this._subscriptions.add((_commonsAtomFeatureConfig2 || _commonsAtomFeatureConfig()).default.observe('nuclide-datatip.datatipDebounceDelay', function (debounceDelay) { return _this.updateDebounceDelay(debounceDelay); })); // TODO(most): Replace with @jjiaa's mouseListenerForTextEditor introduced in D2005545. this._subscriptions.add(atom.workspace.observeTextEditors(function (editor) { // When the cursor moves the next time we do a toggle we should show the // new datatip _this._subscriptions.add(editor.onDidChangeCursorPosition(function () { _this._datatipToggle = false; })); var editorView = atom.views.getView(editor); var mouseMoveListener = function mouseMoveListener(event) { _this.handleMouseMove(event, editor, editorView); }; editorView.addEventListener('mousemove', mouseMoveListener); var mouseListenerSubscription = new (_atom2 || _atom()).Disposable(function () { return editorView.removeEventListener('mousemove', mouseMoveListener); }); var destroySubscription = editor.onDidDestroy(function () { mouseListenerSubscription.dispose(); _this._subscriptions.remove(mouseListenerSubscription); _this._subscriptions.remove(destroySubscription); }); _this._subscriptions.add(mouseListenerSubscription); _this._subscriptions.add(destroySubscription); })); this._ephemeralDatatipElement = document.createElement('div'); this._ephemeralDatatipElement.className = 'nuclide-datatip-overlay'; var datatipMouseEnter = function datatipMouseEnter(event) { return _this._handleElementMouseEnter(event); }; var datatipMouseLeave = function datatipMouseLeave(event) { return _this._handleElementMouseLeave(event); }; this._ephemeralDatatipElement.addEventListener('mouseenter', datatipMouseEnter); this._ephemeralDatatipElement.addEventListener('mouseleave', datatipMouseLeave); this._datatipProviders = []; this._marker = null; this._datatipToggle = false; this._currentRange = null; this._isHoveringDatatip = false; this._pinnedDatatips = new Set(); this._globalKeydownSubscription = null; } _createClass(DatatipManager, [{ key: 'updateDebounceDelay', value: function updateDebounceDelay(debounceDelay) { var _this2 = this; this._debouncedMouseMove = (0, (_commonsNodeDebounce2 || _commonsNodeDebounce()).default)(function (event, editor, editorView) { _this2._datatipForMouseEvent(event, editor, editorView); }, debounceDelay, /* immediate */false); } }, { key: 'handleMouseMove', value: function handleMouseMove(event, editor, editorView) { var mouseEvent = event; this._debouncedMouseMove(mouseEvent, editor, editorView); } }, { key: 'toggleDatatip', value: function toggleDatatip() { this._datatipToggle = !this._datatipToggle; if (this._datatipToggle) { var _editor = atom.workspace.getActiveTextEditor(); if (_editor != null) { var position = _editor.getCursorScreenPosition(); this._datatipInEditor(_editor, position); } } else { this.hideDatatip(); } } }, { key: 'hideDatatip', value: function hideDatatip() { if (this._marker == null) { return; } if (this._globalKeydownSubscription != null) { this._globalKeydownSubscription.dispose(); this._globalKeydownSubscription = null; } this._ephemeralDatatipElement.style.display = 'none'; this._marker.destroy(); this._marker = null; this._currentRange = null; this._isHoveringDatatip = false; } }, { key: '_handleElementMouseEnter', value: function _handleElementMouseEnter(event) { this._isHoveringDatatip = true; } }, { key: '_handleElementMouseLeave', value: function _handleElementMouseLeave(event) { this._isHoveringDatatip = false; } }, { key: '_datatipForMouseEvent', value: function _datatipForMouseEvent(e, editor, editorView) { if (!editorView.component) { // The editor was destroyed, but the destroy handler haven't yet been called to cancel // the timer. return; } var textEditorComponent = editorView.component; var screenPosition = textEditorComponent.screenPositionForMouseEvent(e); var pixelPosition = textEditorComponent.pixelPositionForMouseEvent(e); var pixelPositionFromScreenPosition = textEditorComponent.pixelPositionForScreenPosition(screenPosition); // Distance (in pixels) between screenPosition and the cursor. var horizontalDistance = pixelPosition.left - pixelPositionFromScreenPosition.left; // `screenPositionForMouseEvent.column` cannot exceed the current line length. // This is essentially a heuristic for "mouse cursor is to the left or right of text content". if (pixelPosition.left < 0 || horizontalDistance > editor.getDefaultCharWidth()) { this.hideDatatip(); return; } var bufferPosition = editor.bufferPositionForScreenPosition(screenPosition); this._datatipInEditor(editor, bufferPosition); } }, { key: '_datatipInEditor', value: _asyncToGenerator(function* (editor, position) { var _this3 = this; if (this._isHoveringDatatip) { return; } if (this._currentRange != null && this._currentRange.containsPoint(position)) { return; } if (this._marker != null) { this.hideDatatip(); } var _editor$getGrammar = editor.getGrammar(); var scopeName = _editor$getGrammar.scopeName; var providers = this._getMatchingProvidersForScopeName(scopeName); if (providers.length === 0) { return; } var datatips = (0, (_commonsNodeCollection2 || _commonsNodeCollection()).arrayCompact)((yield Promise.all(providers.map(_asyncToGenerator(function* (provider) { var name = undefined; if (provider.providerName != null) { name = provider.providerName; } else { name = 'unknown'; logger.error('Datatip provider has no name', provider); } var datatip = yield (0, (_nuclideAnalytics2 || _nuclideAnalytics()).trackOperationTiming)(name + '.datatip', function () { return provider.datatip(editor, position); }); if (!datatip || _this3._marker) { return; } var pinnable = datatip.pinnable; var component = datatip.component; var range = datatip.range; // We track the timing above, but we still want to know the number of popups that are shown. (0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)('datatip-popup', { scope: scopeName, providerName: name, rangeStartRow: String(range.start.row), rangeStartColumn: String(range.start.column), rangeEndRow: String(range.end.row), rangeEndColumn: String(range.end.column) }); _this3._currentRange = range; var action = undefined; var actionTitle = undefined; // Datatips are pinnable by default, unless explicitly specified otherwise. if (pinnable !== false) { action = (_DatatipComponent2 || _DatatipComponent()).DATATIP_ACTIONS.PIN; actionTitle = 'Pin this Datatip'; } return { range: range, component: component, pinnable: pinnable, name: name, action: action, actionTitle: actionTitle }; }))))); if (datatips.length === 0) { return; } var renderedProviders = datatips.map(function (datatip) { var ProvidedComponent = datatip.component; var name = datatip.name; var action = datatip.action; var actionTitle = datatip.actionTitle; return (_reactForAtom2 || _reactForAtom()).React.createElement( (_DatatipComponent2 || _DatatipComponent()).DatatipComponent, { action: action, actionTitle: actionTitle, onActionClick: _this3._handlePinClicked.bind(_this3, editor, datatip), key: name }, (_reactForAtom2 || _reactForAtom()).React.createElement(ProvidedComponent, null) ); }); var combinedRange = datatips[0].range; for (var i = 1; i < datatips.length; i++) { combinedRange = combinedRange.union(datatips[i].range); } // Transform the matched element range to the hint range. var marker = editor.markBufferRange(combinedRange, { invalidate: 'never' }); this._marker = marker; (_reactForAtom2 || _reactForAtom()).ReactDOM.render((_reactForAtom2 || _reactForAtom()).React.createElement( 'div', null, renderedProviders ), this._ephemeralDatatipElement); this._ephemeralDatatipElement.style.display = 'block'; editor.decorateMarker(marker, { type: 'overlay', position: 'tail', item: this._ephemeralDatatipElement }); editor.decorateMarker(marker, { type: 'highlight', 'class': 'nuclide-datatip-highlight-region' }); this._subscribeToGlobalKeydown(); }) }, { key: '_subscribeToGlobalKeydown', value: function _subscribeToGlobalKeydown() { var _this4 = this; var editor = atom.views.getView(atom.workspace); editor.addEventListener('keydown', this._boundHideDatatip); this._globalKeydownSubscription = new (_atom2 || _atom()).Disposable(function () { editor.removeEventListener('keydown', _this4._boundHideDatatip); }); } }, { key: '_handlePinClicked', value: function _handlePinClicked(editor, datatip) { var _this5 = this; this.hideDatatip(); this._pinnedDatatips.add(new (_PinnedDatatip2 || _PinnedDatatip()).PinnedDatatip(datatip, editor, function (pinnedDatatip) { _this5._pinnedDatatips.delete(pinnedDatatip); })); } }, { key: '_getMatchingProvidersForScopeName', value: function _getMatchingProvidersForScopeName(scopeName) { return this._datatipProviders.filter(function (provider) { return provider.inclusionPriority > 0 && provider.validForScope(scopeName); }).sort(function (providerA, providerB) { return providerA.inclusionPriority - providerB.inclusionPriority; }); } }, { key: 'addProvider', value: function addProvider(provider) { this._datatipProviders.push(provider); } }, { key: 'removeProvider', value: function removeProvider(provider) { (0, (_commonsNodeCollection2 || _commonsNodeCollection()).arrayRemove)(this._datatipProviders, provider); } }, { key: 'createDatatip', value: function createDatatip(component, range, pinnable) { return { component: component, range: range, pinnable: pinnable }; } }, { key: 'createPinnedDataTip', value: function createPinnedDataTip(component, range, pinnable, editor, onDispose) { var datatip = new (_PinnedDatatip2 || _PinnedDatatip()).PinnedDatatip(this.createDatatip(component, range, pinnable), editor, onDispose); this._pinnedDatatips.add(datatip); return datatip; } }, { key: 'deletePinnedDatatip', value: function deletePinnedDatatip(datatip) { this._pinnedDatatips.delete(datatip); } }, { key: 'dispose', value: function dispose() { this.hideDatatip(); (_reactForAtom2 || _reactForAtom()).ReactDOM.unmountComponentAtNode(this._ephemeralDatatipElement); this._ephemeralDatatipElement.remove(); this._pinnedDatatips.forEach(function (pinnedDatatip) { return pinnedDatatip.dispose(); }); this._subscriptions.dispose(); } }]); return DatatipManager; })(); exports.DatatipManager = DatatipManager; /** * This helps determine if we should show the datatip when toggling it via * command. The toggle command first negates this, and then if this is true * shows a datatip, otherwise it hides the current datatip. */