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.
626 lines (565 loc) • 24.8 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 _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
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 _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 _reactForAtom2;
function _reactForAtom() {
return _reactForAtom2 = require('react-for-atom');
}
var _atom2;
function _atom() {
return _atom2 = require('atom');
}
var _commonsNodePromise2;
function _commonsNodePromise() {
return _commonsNodePromise2 = require('../../commons-node/promise');
}
var _commonsNodeDebounce2;
function _commonsNodeDebounce() {
return _commonsNodeDebounce2 = _interopRequireDefault(require('../../commons-node/debounce'));
}
var _QuickSelectionDispatcher2;
function _QuickSelectionDispatcher() {
return _QuickSelectionDispatcher2 = _interopRequireDefault(require('./QuickSelectionDispatcher'));
}
var _QuickSelectionActions2;
function _QuickSelectionActions() {
return _QuickSelectionActions2 = _interopRequireDefault(require('./QuickSelectionActions'));
}
var _FileResultComponent2;
function _FileResultComponent() {
return _FileResultComponent2 = _interopRequireDefault(require('./FileResultComponent'));
}
var performance = global.performance;
function getDefaultResult() {
return {
error: null,
loading: false,
results: []
};
}
var AnalyticsEvents = Object.freeze({
QUERY_SOURCE_PROVIDER: 'quickopen-query-source-provider'
});
var RESULTS_CHANGED = 'results_changed';
var PROVIDERS_CHANGED = 'providers_changed';
var MAX_OMNI_RESULTS_PER_SERVICE = 5;
var DEFAULT_QUERY_DEBOUNCE_DELAY = 200;
var LOADING_EVENT_DELAY = 200;
var OMNISEARCH_PROVIDER = {
action: 'nuclide-quick-open:find-anything-via-omni-search',
debounceDelay: DEFAULT_QUERY_DEBOUNCE_DELAY,
name: 'OmniSearchResultProvider',
prompt: 'Search for anything...',
title: 'OmniSearch',
priority: 0
};
// Number of elements in the cache before periodic cleanup kicks in. Includes partial query strings.
var MAX_CACHED_QUERIES = 100;
var CACHE_CLEAN_DEBOUNCE_DELAY = 5000;
var UPDATE_DIRECTORIES_DEBOUNCE_DELAY = 100;
var GLOBAL_KEY = 'global';
var DIRECTORY_KEY = 'directory';
function isValidProvider(provider) {
return typeof provider.getProviderType === 'function' && typeof provider.getName === 'function' && typeof provider.getName() === 'string' && typeof provider.isRenderable === 'function' && typeof provider.executeQuery === 'function' && typeof provider.getTabTitle === 'function';
}
var searchResultManagerInstance = null;
/**
* A singleton cache for search providers and results.
*/
var SearchResultManager = (function () {
_createClass(SearchResultManager, null, [{
key: 'getInstance',
value: function getInstance() {
if (!searchResultManagerInstance) {
searchResultManagerInstance = new SearchResultManager();
}
return searchResultManagerInstance;
}
}]);
function SearchResultManager() {
var _this = this;
_classCallCheck(this, SearchResultManager);
this._isDisposed = false;
this.RESULTS_CHANGED = RESULTS_CHANGED;
this.PROVIDERS_CHANGED = PROVIDERS_CHANGED;
this._registeredProviders = {};
this._registeredProviders[DIRECTORY_KEY] = new Map();
this._registeredProviders[GLOBAL_KEY] = new Map();
this._providersByDirectory = new Map();
this._directories = [];
this._cachedResults = {};
this._lastCachedQuery = new Map();
this._debouncedCleanCache = (0, (_commonsNodeDebounce2 || _commonsNodeDebounce()).default)(function () {
return _this._cleanCache();
}, CACHE_CLEAN_DEBOUNCE_DELAY,
/* immediate */false);
// `updateDirectories` joins providers and directories, which don't know anything about each
// other. Debounce this call to reduce churn at startup, and when new providers get activated or
// a new directory gets mounted.
this._debouncedUpdateDirectories = (0, (_commonsNodeDebounce2 || _commonsNodeDebounce()).default)(function () {
return _this._updateDirectories();
}, UPDATE_DIRECTORIES_DEBOUNCE_DELAY,
/* immediate */false);
this._queryLruQueue = new Map();
this._emitter = new (_atom2 || _atom()).Emitter();
this._subscriptions = new (_atom2 || _atom()).CompositeDisposable();
this._dispatcher = (_QuickSelectionDispatcher2 || _QuickSelectionDispatcher()).default.getInstance();
// Check is required for testing.
if (atom.project) {
this._subscriptions.add(atom.project.onDidChangePaths(this._debouncedUpdateDirectories.bind(this)));
this._debouncedUpdateDirectories();
}
this._setUpFlux();
this._activeProviderName = OMNISEARCH_PROVIDER.name;
}
_createClass(SearchResultManager, [{
key: '_setUpFlux',
value: function _setUpFlux() {
var _this2 = this;
this._dispatcherToken = this._dispatcher.register(function (action) {
switch (action.actionType) {
case (_QuickSelectionDispatcher2 || _QuickSelectionDispatcher()).default.ActionType.QUERY:
_this2.executeQuery(action.query);
break;
case (_QuickSelectionDispatcher2 || _QuickSelectionDispatcher()).default.ActionType.ACTIVE_PROVIDER_CHANGED:
_this2._activeProviderName = action.providerName;
_this2._emitter.emit(PROVIDERS_CHANGED);
break;
}
});
}
}, {
key: 'getActiveProviderName',
value: function getActiveProviderName() {
return this._activeProviderName;
}
}, {
key: 'getRendererForProvider',
value: function getRendererForProvider(providerName) {
var provider = this._getProviderByName(providerName);
if (!provider || !provider.getComponentForItem) {
return (_FileResultComponent2 || _FileResultComponent()).default.getComponentForItem;
}
return provider.getComponentForItem;
}
}, {
key: 'dispose',
value: function dispose() {
this._isDisposed = true;
this._subscriptions.dispose();
}
/**
* Renew the cached list of directories, as well as the cached map of eligible providers
* for every directory.
*/
}, {
key: '_updateDirectories',
value: _asyncToGenerator(function* () {
var _this3 = this;
var newDirectories = atom.project.getDirectories();
var newProvidersByDirectories = new Map();
var eligibilities = [];
newDirectories.forEach(function (directory) {
newProvidersByDirectories.set(directory, new Set());
var _loop = function (provider) {
(0, (_assert2 || _assert()).default)(provider.isEligibleForDirectory != null, 'Directory provider ' + provider.getName() + ' must provide `isEligibleForDirectory()`.');
eligibilities.push(provider.isEligibleForDirectory(directory).then(function (isEligible) {
return {
isEligible: isEligible,
provider: provider,
directory: directory
};
}).catch(function (err) {
(0, (_nuclideLogging2 || _nuclideLogging()).getLogger)().warn('isEligibleForDirectory failed for directory provider ' + provider.getName(), err);
return {
isEligible: false,
provider: provider,
directory: directory
};
}));
};
for (var provider of _this3._registeredProviders[DIRECTORY_KEY].values()) {
_loop(provider);
}
});
var resolvedEligibilities = yield Promise.all(eligibilities);
for (var eligibility of resolvedEligibilities) {
var provider = eligibility.provider;
var isEligible = eligibility.isEligible;
var directory = eligibility.directory;
if (isEligible) {
var providersForDirectory = newProvidersByDirectories.get(directory);
(0, (_assert2 || _assert()).default)(providersForDirectory != null, 'Providers for directory ' + directory.getPath() + ' not defined');
providersForDirectory.add(provider);
}
}
this._directories = newDirectories;
this._providersByDirectory = newProvidersByDirectories;
this._emitter.emit(PROVIDERS_CHANGED);
})
}, {
key: 'on',
value: function on(name, value) {
return this._emitter.on(name, value);
}
}, {
key: 'registerProvider',
value: function registerProvider(service) {
var _this4 = this;
if (!isValidProvider(service)) {
var providerName = service.getName && service.getName() || '<unknown>';
(0, (_nuclideLogging2 || _nuclideLogging()).getLogger)().error('Quick-open provider ' + providerName + ' is not a valid provider');
}
var isRenderableProvider = typeof service.isRenderable === 'function' && service.isRenderable();
var isGlobalProvider = service.getProviderType() === 'GLOBAL';
var targetRegistry = isGlobalProvider ? this._registeredProviders[GLOBAL_KEY] : this._registeredProviders[DIRECTORY_KEY];
targetRegistry.set(service.getName(), service);
if (!isGlobalProvider) {
this._debouncedUpdateDirectories();
}
var disposable = new (_atom2 || _atom()).CompositeDisposable();
disposable.add(new (_atom2 || _atom()).Disposable(function () {
// This may be called after this package has been deactivated
// and the SearchResultManager has been disposed.
if (_this4._isDisposed) {
return;
}
var serviceName = service.getName();
targetRegistry.delete(serviceName);
_this4._providersByDirectory.forEach(function (providers, dir) {
providers.delete(service);
});
// Reset the active provider to omnisearch if the disposed service is
// the current active provider.
if (serviceName === _this4._activeProviderName) {
_this4._activeProviderName = OMNISEARCH_PROVIDER.name;
}
_this4._removeResultsForProvider(serviceName);
_this4._emitter.emit(PROVIDERS_CHANGED);
}));
// If the provider is renderable and specifies a keybinding, wire it up with the toggle command.
if (isRenderableProvider && typeof service.getAction === 'function') {
var toggleAction = service.getAction();
// TODO replace with computed property once Flow supports it.
var actionSpec = {};
actionSpec[toggleAction] = function () {
return (_QuickSelectionActions2 || _QuickSelectionActions()).default.changeActiveProvider(service.getName());
};
disposable.add(atom.commands.add('atom-workspace', actionSpec));
}
return disposable;
}
}, {
key: '_removeResultsForProvider',
value: function _removeResultsForProvider(providerName) {
if (this._cachedResults[providerName]) {
delete this._cachedResults[providerName];
this._emitter.emit(RESULTS_CHANGED);
}
this._lastCachedQuery.delete(providerName);
}
}, {
key: 'setCacheResult',
value: function setCacheResult(providerName, directory, query, result) {
var loading = arguments.length <= 4 || arguments[4] === undefined ? false : arguments[4];
var error = arguments.length <= 5 || arguments[5] === undefined ? null : arguments[5];
this.ensureCacheEntry(providerName, directory);
this._cachedResults[providerName][directory][query] = {
result: result,
loading: loading,
error: error
};
this._lastCachedQuery.set(providerName, query);
// Refresh the usage for the current query.
this._queryLruQueue.delete(query);
this._queryLruQueue.set(query, null);
setImmediate(this._debouncedCleanCache);
}
}, {
key: 'ensureCacheEntry',
value: function ensureCacheEntry(providerName, directory) {
if (!this._cachedResults[providerName]) {
this._cachedResults[providerName] = {};
}
if (!this._cachedResults[providerName][directory]) {
this._cachedResults[providerName][directory] = {};
}
}
}, {
key: 'cacheResult',
value: function cacheResult(query, result, directory, provider) {
var providerName = provider.getName();
this.setCacheResult(providerName, directory, query, result, false, null);
}
}, {
key: '_setLoading',
value: function _setLoading(query, directory, provider) {
var providerName = provider.getName();
this.ensureCacheEntry(providerName, directory);
var previousResult = this._cachedResults[providerName][directory][query];
if (!previousResult) {
this._cachedResults[providerName][directory][query] = {
result: [],
error: null,
loading: true
};
}
}
/**
* Release the oldest cached results once the cache is full.
*/
}, {
key: '_cleanCache',
value: function _cleanCache() {
var _this5 = this;
var queueSize = this._queryLruQueue.size;
if (queueSize <= MAX_CACHED_QUERIES) {
return;
}
// Figure out least recently used queries, and pop them off of the `_queryLruQueue` Map.
var expiredQueries = [];
var keyIterator = this._queryLruQueue.keys();
var entriesToRemove = queueSize - MAX_CACHED_QUERIES;
for (var i = 0; i < entriesToRemove; i++) {
var firstEntryKey = keyIterator.next().value;
expiredQueries.push(firstEntryKey);
(0, (_assert2 || _assert()).default)(firstEntryKey != null);
this._queryLruQueue.delete(firstEntryKey);
}
// For each (provider|directory) pair, remove results for all expired queries from the cache.
for (var providerName in this._cachedResults) {
var _loop2 = function (directory) {
var queryResults = _this5._cachedResults[providerName][directory];
expiredQueries.forEach(function (query) {
return delete queryResults[query];
});
};
for (var directory in this._cachedResults[providerName]) {
_loop2(directory);
}
}
this._emitter.emit(RESULTS_CHANGED);
}
}, {
key: 'processResult',
value: function processResult(query, result, directory, provider) {
this.cacheResult.apply(this, arguments);
this._emitter.emit(RESULTS_CHANGED);
}
}, {
key: 'sanitizeQuery',
value: function sanitizeQuery(query) {
return query.trim();
}
}, {
key: 'executeQuery',
value: _asyncToGenerator(function* (rawQuery) {
var _this6 = this;
var query = this.sanitizeQuery(rawQuery);
var _loop3 = function (globalProvider) {
var startTime = performance.now();
var loadingFn = function loadingFn() {
_this6._setLoading(query, GLOBAL_KEY, globalProvider);
_this6._emitter.emit(RESULTS_CHANGED);
};
(0, (_commonsNodePromise2 || _commonsNodePromise()).triggerAfterWait)(globalProvider.executeQuery(query), LOADING_EVENT_DELAY, loadingFn).then(function (result) {
(0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)(AnalyticsEvents.QUERY_SOURCE_PROVIDER, {
'quickopen-source-provider': globalProvider.getName(),
'quickopen-query-duration': (performance.now() - startTime).toString(),
'quickopen-result-count': result.length.toString()
});
_this6.processResult(query, result, GLOBAL_KEY, globalProvider);
});
};
for (var globalProvider of this._registeredProviders[GLOBAL_KEY].values()) {
_loop3(globalProvider);
}
if (this._providersByDirectory.size === 0) {
return;
}
this._directories.forEach(function (directory) {
var path = directory.getPath();
var providers = _this6._providersByDirectory.get(directory);
if (!providers) {
// Special directories like "atom://about"
return;
}
var _loop4 = function (directoryProvider) {
var startTime = performance.now();
var loadingFn = function loadingFn() {
_this6._setLoading(query, path, directoryProvider);
_this6._emitter.emit(RESULTS_CHANGED);
};
(0, (_commonsNodePromise2 || _commonsNodePromise()).triggerAfterWait)(directoryProvider.executeQuery(query, directory), LOADING_EVENT_DELAY, loadingFn).then(function (result) {
(0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)(AnalyticsEvents.QUERY_SOURCE_PROVIDER, {
'quickopen-source-provider': directoryProvider.getName(),
'quickopen-query-duration': (performance.now() - startTime).toString(),
'quickopen-result-count': result.length.toString()
});
_this6.processResult(query, result, path, directoryProvider);
});
};
for (var directoryProvider of providers) {
_loop4(directoryProvider);
}
});
})
}, {
key: '_isGlobalProvider',
value: function _isGlobalProvider(providerName) {
return this._registeredProviders[GLOBAL_KEY].has(providerName);
}
}, {
key: '_getProviderByName',
value: function _getProviderByName(providerName) {
var dirProviderName = undefined;
if (this._isGlobalProvider(providerName)) {
dirProviderName = this._registeredProviders[GLOBAL_KEY].get(providerName);
} else {
dirProviderName = this._registeredProviders[DIRECTORY_KEY].get(providerName);
}
(0, (_assert2 || _assert()).default)(dirProviderName != null, 'Provider ' + providerName + ' is not registered with quick-open.');
return dirProviderName;
}
}, {
key: '_getResultsForProvider',
value: function _getResultsForProvider(query, providerName) {
var _this7 = this;
var providerPaths = this._isGlobalProvider(providerName) ? [GLOBAL_KEY] : this._directories.map(function (d) {
return d.getPath();
});
var provider = this._getProviderByName(providerName);
var lastCachedQuery = this._lastCachedQuery.get(providerName);
return {
title: provider.getTabTitle(),
results: providerPaths.reduce(function (results, path) {
var cachedPaths = undefined;
var cachedQueries = undefined;
var cachedResult = undefined;
if (!((cachedPaths = _this7._cachedResults[providerName]) && (cachedQueries = cachedPaths[path]) && ((cachedResult = cachedQueries[query]) ||
// If the current query hasn't returned anything yet, try the last cached result.
lastCachedQuery != null && (cachedResult = cachedQueries[lastCachedQuery])))) {
cachedResult = {};
}
var defaultResult = getDefaultResult();
var resultList = cachedResult.result || defaultResult.results;
results[path] = {
results: resultList.map(function (result) {
return _extends({}, result, { sourceProvider: providerName });
}),
loading: cachedResult.loading || defaultResult.loading,
error: cachedResult.error || defaultResult.error
};
return results;
}, {})
};
}
}, {
key: 'getResults',
value: function getResults(query, activeProviderName) {
var sanitizedQuery = this.sanitizeQuery(query);
if (activeProviderName === OMNISEARCH_PROVIDER.name) {
var omniSearchResults = [{}];
for (var providerName in this._cachedResults) {
var resultForProvider = this._getResultsForProvider(sanitizedQuery, providerName);
// TODO replace this with a ranking algorithm.
for (var dir in resultForProvider.results) {
resultForProvider.results[dir].results = resultForProvider.results[dir].results.slice(0, MAX_OMNI_RESULTS_PER_SERVICE);
}
// TODO replace `partial` with computed property whenever Flow supports it.
var _partial = {};
_partial[providerName] = resultForProvider;
omniSearchResults.push(_partial);
}
return Object.assign.apply(null, omniSearchResults);
}
// TODO replace `partial` with computed property whenever Flow supports it.
var partial = {};
partial[activeProviderName] = this._getResultsForProvider(sanitizedQuery, activeProviderName);
return partial;
}
}, {
key: 'getProviderByName',
value: function getProviderByName(providerName) {
if (providerName === OMNISEARCH_PROVIDER.name) {
return _extends({}, OMNISEARCH_PROVIDER);
}
return this._bakeProvider(this._getProviderByName(providerName));
}
/**
* Turn a Provider into a plain "spec" object consumed by QuickSelectionComponent.
*/
}, {
key: '_bakeProvider',
value: function _bakeProvider(provider) {
var providerName = provider.getName();
var providerSpec = {
action: provider.getAction && provider.getAction() || '',
debounceDelay: typeof provider.getDebounceDelay === 'function' ? provider.getDebounceDelay() : DEFAULT_QUERY_DEBOUNCE_DELAY,
name: providerName,
prompt: provider.getPromptText && provider.getPromptText() || 'Search ' + providerName,
title: provider.getTabTitle && provider.getTabTitle() || providerName
};
// $FlowIssue priority property is optional
providerSpec.priority = typeof provider.getPriority === 'function' ? provider.getPriority() : Number.POSITIVE_INFINITY;
return providerSpec;
}
}, {
key: 'getRenderableProviders',
value: function getRenderableProviders() {
var _this8 = this;
// Only render tabs for providers that are eligible for at least one directory.
var eligibleDirectoryProviders = Array.from(this._registeredProviders[DIRECTORY_KEY].values()).filter(function (provider) {
for (var providers of _this8._providersByDirectory.values()) {
if (providers.has(provider)) {
return true;
}
}
return false;
});
var tabs = Array.from(this._registeredProviders[GLOBAL_KEY].values()).concat(eligibleDirectoryProviders).filter(function (provider) {
return provider.isRenderable();
}).map(this._bakeProvider).sort(function (p1, p2) {
return p1.name.localeCompare(p2.name);
});
tabs.unshift(OMNISEARCH_PROVIDER);
return tabs;
}
}]);
return SearchResultManager;
})();
exports.default = SearchResultManager;
var __test__ = {
_getOmniSearchProviderSpec: function _getOmniSearchProviderSpec() {
return OMNISEARCH_PROVIDER;
}
};
exports.__test__ = __test__;
// Cache the last query with results for each provider.
// Display cached results for the last completed query until new data arrives.
// List of most recently used query strings, used for pruning the result cache.
// Makes use of `Map`'s insertion ordering, so values are irrelevant and always set to `null`.