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.

403 lines (350 loc) 15.7 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 _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 _atom2; function _atom() { return _atom2 = require('atom'); } var _hyperclickUtils2; function _hyperclickUtils() { return _hyperclickUtils2 = require('./hyperclick-utils'); } var _assert2; function _assert() { return _assert2 = _interopRequireDefault(require('assert')); } var _nuclideAnalytics2; function _nuclideAnalytics() { return _nuclideAnalytics2 = require('../../nuclide-analytics'); } var _nuclideLogging2; function _nuclideLogging() { return _nuclideLogging2 = require('../../nuclide-logging'); } var logger = (0, (_nuclideLogging2 || _nuclideLogging()).getLogger)(); /** * Construct this object to enable Hyperclick in a text editor. * Call `dispose` to disable the feature. */ var HyperclickForTextEditor = (function () { function HyperclickForTextEditor(textEditor, hyperclick) { var _this = this; _classCallCheck(this, HyperclickForTextEditor); this._textEditor = textEditor; this._textEditorView = atom.views.getView(textEditor); this._hyperclick = hyperclick; this._lastMouseEvent = null; this._lastPosition = null; // We store the original promise that we use to retrieve the last suggestion // so callers can also await it to know when it's available. this._lastSuggestionAtMousePromise = null; // We store the last suggestion since we must await it immediately anyway. this._lastSuggestionAtMouse = null; this._navigationMarkers = null; this._lastWordRange = null; this._subscriptions = new (_atom2 || _atom()).CompositeDisposable(); this._onMouseMove = this._onMouseMove.bind(this); this._textEditorView.addEventListener('mousemove', this._onMouseMove); this._onMouseDown = this._onMouseDown.bind(this); this._setupMouseDownListener(); this._onKeyDown = this._onKeyDown.bind(this); this._textEditorView.addEventListener('keydown', this._onKeyDown); this._onKeyUp = this._onKeyUp.bind(this); this._textEditorView.addEventListener('keyup', this._onKeyUp); this._subscriptions.add(atom.commands.add(this._textEditorView, { 'hyperclick:confirm-cursor': function hyperclickConfirmCursor() { return _this._confirmSuggestionAtCursor(); } })); this._isDestroyed = false; this._isLoading = false; this._loadingTracker = null; } _createDecoratedClass(HyperclickForTextEditor, [{ key: '_setupMouseDownListener', value: function _setupMouseDownListener() { var _this2 = this; var getLinesDomNode = function getLinesDomNode() { var component = _this2._textEditorView.component; (0, (_assert2 || _assert()).default)(component); return component.linesComponent.getDomNode(); }; var removeMouseDownListener = function removeMouseDownListener() { if (_this2._textEditorView.component == null) { return; } getLinesDomNode().removeEventListener('mousedown', _this2._onMouseDown); }; var addMouseDownListener = function addMouseDownListener() { getLinesDomNode().addEventListener('mousedown', _this2._onMouseDown); }; this._subscriptions.add(new (_atom2 || _atom()).Disposable(removeMouseDownListener)); this._subscriptions.add(this._textEditorView.onDidDetach(removeMouseDownListener)); this._subscriptions.add(this._textEditorView.onDidAttach(addMouseDownListener)); addMouseDownListener(); } }, { key: '_confirmSuggestion', value: function _confirmSuggestion(suggestion) { if (Array.isArray(suggestion.callback) && suggestion.callback.length > 0) { this._hyperclick.showSuggestionList(this._textEditor, suggestion); } else { (0, (_assert2 || _assert()).default)(typeof suggestion.callback === 'function'); suggestion.callback(); } } }, { key: '_onMouseMove', value: function _onMouseMove(event) { var mouseEvent = event; if (this._isLoading) { // Show the loading cursor. this._textEditorView.classList.add('hyperclick-loading'); } // We save the last `MouseEvent` so the user can trigger Hyperclick by // pressing the key without moving the mouse again. We only save the // relevant properties to prevent retaining a reference to the event. this._lastMouseEvent = { clientX: mouseEvent.clientX, clientY: mouseEvent.clientY }; // Don't fetch suggestions if the mouse is still in the same 'word', where // 'word' is a whitespace-delimited group of characters. // // If the last suggestion had multiple ranges, we have no choice but to // fetch suggestions because the new word might be between those ranges. // This should be ok because it will reuse that last suggestion until the // mouse moves off of it. var lastSuggestionIsNotMultiRange = !this._lastSuggestionAtMouse || !Array.isArray(this._lastSuggestionAtMouse.range); if (this._isMouseAtLastWordRange() && lastSuggestionIsNotMultiRange) { return; } var _ref = (0, (_hyperclickUtils2 || _hyperclickUtils()).getWordTextAndRange)(this._textEditor, this._getMousePositionAsBufferPosition()); var range = _ref.range; this._lastWordRange = range; if (this._isHyperclickEvent(mouseEvent)) { // Clear the suggestion if the mouse moved out of the range. if (!this._isMouseAtLastSuggestion()) { this._clearSuggestion(); } this._setSuggestionForLastMouseEvent(); } else { this._clearSuggestion(); } } }, { key: '_onMouseDown', value: function _onMouseDown(event) { var mouseEvent = event; if (!this._isHyperclickEvent(mouseEvent) || !this._isMouseAtLastSuggestion()) { return; } if (this._lastSuggestionAtMouse) { this._confirmSuggestion(this._lastSuggestionAtMouse); // Prevent the <meta-click> event from adding another cursor. event.stopPropagation(); } this._clearSuggestion(); } }, { key: '_onKeyDown', value: function _onKeyDown(event) { var mouseEvent = event; // Show the suggestion at the last known mouse position. if (this._isHyperclickEvent(mouseEvent)) { this._setSuggestionForLastMouseEvent(); } } }, { key: '_onKeyUp', value: function _onKeyUp(event) { var mouseEvent = event; if (!this._isHyperclickEvent(mouseEvent)) { this._clearSuggestion(); } } /** * Returns a `Promise` that's resolved when the latest suggestion's available. */ }, { key: 'getSuggestionAtMouse', value: function getSuggestionAtMouse() { return this._lastSuggestionAtMousePromise || Promise.resolve(null); } }, { key: '_setSuggestionForLastMouseEvent', value: _asyncToGenerator(function* () { if (!this._lastMouseEvent) { return; } var position = this._getMousePositionAsBufferPosition(); if (this._lastSuggestionAtMouse != null) { var range = this._lastSuggestionAtMouse.range; (0, (_assert2 || _assert()).default)(range, 'Hyperclick result must have a valid Range'); if (this._isPositionInRange(position, range)) { return; } } // this._lastSuggestionAtMouse will only be set if hyperclick returned a promise that // resolved to a non-null value. So, in order to not ask hyperclick for the same thing // again and again which will be anyway null, we check if the mouse position has changed. if (this._lastPosition && position.compare(this._lastPosition) === 0) { return; } this._isLoading = true; this._loadingTracker = (0, (_nuclideAnalytics2 || _nuclideAnalytics()).startTracking)('hyperclick-loading'); try { this._lastPosition = position; this._lastSuggestionAtMousePromise = this._hyperclick.getSuggestion(this._textEditor, position); this._lastSuggestionAtMouse = yield this._lastSuggestionAtMousePromise; if (this._isDestroyed) { return; } if (this._lastSuggestionAtMouse && this._isMouseAtLastSuggestion()) { // Add the hyperclick markers if there's a new suggestion and it's under the mouse. this._updateNavigationMarkers(this._lastSuggestionAtMouse.range); } else { // Remove all the markers if we've finished loading and there's no suggestion. this._updateNavigationMarkers(null); } if (this._loadingTracker != null) { this._loadingTracker.onSuccess(); } } catch (e) { if (this._loadingTracker != null) { this._loadingTracker.onError(e); } logger.error('Error getting Hyperclick suggestion:', e); } finally { this._doneLoading(); } }) }, { key: '_getMousePositionAsBufferPosition', value: function _getMousePositionAsBufferPosition() { var component = this._textEditorView.component; (0, (_assert2 || _assert()).default)(component); (0, (_assert2 || _assert()).default)(this._lastMouseEvent); var screenPosition = component.screenPositionForMouseEvent(this._lastMouseEvent); try { return this._textEditor.bufferPositionForScreenPosition(screenPosition); } catch (error) { // Fix https://github.com/facebook/nuclide/issues/292 // When navigating Atom workspace with `CMD/CTRL` down, // it triggers TextEditorElement's `mousemove` with invalid screen position. // This falls back to returning the start of the editor. logger.error('Hyperclick: Error getting buffer position for screen position:', error); return new (_atom2 || _atom()).Point(0, 0); } } }, { key: '_isMouseAtLastSuggestion', value: function _isMouseAtLastSuggestion() { if (!this._lastSuggestionAtMouse) { return false; } var range = this._lastSuggestionAtMouse.range; (0, (_assert2 || _assert()).default)(range, 'Hyperclick result must have a valid Range'); return this._isPositionInRange(this._getMousePositionAsBufferPosition(), range); } }, { key: '_isMouseAtLastWordRange', value: function _isMouseAtLastWordRange() { var lastWordRange = this._lastWordRange; if (lastWordRange == null) { return false; } return this._isPositionInRange(this._getMousePositionAsBufferPosition(), lastWordRange); } }, { key: '_isPositionInRange', value: function _isPositionInRange(position, range) { return Array.isArray(range) ? range.some(function (r) { return r.containsPoint(position); }) : range.containsPoint(position); } }, { key: '_clearSuggestion', value: function _clearSuggestion() { this._doneLoading(); this._lastSuggestionAtMousePromise = null; this._lastSuggestionAtMouse = null; this._updateNavigationMarkers(null); } }, { key: '_confirmSuggestionAtCursor', decorators: [(0, (_nuclideAnalytics2 || _nuclideAnalytics()).trackTiming)('hyperclick:confirm-cursor')], value: _asyncToGenerator(function* () { var suggestion = yield this._hyperclick.getSuggestion(this._textEditor, this._textEditor.getCursorBufferPosition()); if (suggestion) { this._confirmSuggestion(suggestion); } }) /** * Add markers for the given range(s), or clears them if `ranges` is null. */ }, { key: '_updateNavigationMarkers', value: function _updateNavigationMarkers(range) { var _this3 = this; if (this._navigationMarkers) { this._navigationMarkers.forEach(function (marker) { return marker.destroy(); }); this._navigationMarkers = null; } // Only change the cursor to a pointer if there is a suggestion ready. if (range == null) { this._textEditorView.classList.remove('hyperclick'); return; } this._textEditorView.classList.add('hyperclick'); var ranges = Array.isArray(range) ? range : [range]; this._navigationMarkers = ranges.map(function (markerRange) { var marker = _this3._textEditor.markBufferRange(markerRange, { invalidate: 'never' }); _this3._textEditor.decorateMarker(marker, { type: 'highlight', 'class': 'hyperclick' }); return marker; }); } /** * Returns whether an event should be handled by hyperclick or not. */ }, { key: '_isHyperclickEvent', value: function _isHyperclickEvent(event) { // If the user is pressing either the meta/ctrl key or the alt key. return process.platform === 'darwin' ? event.metaKey : event.ctrlKey; } }, { key: '_doneLoading', value: function _doneLoading() { this._isLoading = false; this._loadingTracker = null; this._textEditorView.classList.remove('hyperclick-loading'); } }, { key: 'dispose', value: function dispose() { this._isDestroyed = true; this._clearSuggestion(); this._textEditorView.removeEventListener('mousemove', this._onMouseMove); this._textEditorView.removeEventListener('keydown', this._onKeyDown); this._textEditorView.removeEventListener('keyup', this._onKeyUp); this._subscriptions.dispose(); } }]); return HyperclickForTextEditor; })(); exports.default = HyperclickForTextEditor; module.exports = exports.default;