apercova/atom-path-intellisense

View on GitHub
lib/atom-path-intellisense-impl.js

Summary

Maintainability
B
5 hrs
Test Coverage
'use strict';
const consts = require('./config/consts'),
  config = require('./config/config'),
  selectors = require('atom-selectors-plus'),
  logger = require('./util/logger'),
  BasePathProvider = require('./providers/base-path.provider'),
  CurrentFilePathProvider = require('./providers/current-file-path.provider'),
  CurrentFileRelativePathProvider = require('./providers/current-file-relative-path.provider'),
  NodeJSPathProvider = require('./providers/nodejs-path.provider');

/**
 *
 * @class
 * @Classdesc Autocomplete provider implementation.
 * @author
 *  [{@link https://github.com/apercova|github@apercova}],
 *  [{@link https://twitter.com/apercova|twitter@apercova}],
 *  [{@link https://www.npmjs.com/~apercova|npmjs/~apercova}]
 *
 * @since 1.2.0
 */
class AtomPathIntellisenseImpl {
  /**
   * @return {AtomPathIntellisenseImpl}  AtomPathIntellisenseImpl instance.
   */
  constructor() {
    this._logger = logger.getLogger('AtomPathIntellisenseImpl');
    this.selector = '*';
    this.suggestionPriority = 1;
    this.inclusionPriority = 2;
    this.excludeLowerPriority = false;
    this.filterSuggestions = true;
    this._subscriptions = [];
  }

  /**
   * getSuggestions - resolve suggestions using registered path providers.
   *
   * @param  {object} req Request options.
   * @return {Promise} Promise for resolving path suggestions.
   */
  getSuggestions(req) {
    let suggestions = [];
    const manualMode = config.isManualModeOn();
    if (manualMode === true) {
      if (req.activatedManually === true) {
        this._logger.debug('Manually activated.');
        suggestions = this._resolveSuggestions(req);
      }
    } else {
      this._logger.debug('Automatically activated.');
      suggestions = this._resolveSuggestions(req);
    }
    return suggestions;
  }

  /**
   * activate - Autocomplete-plus provider activation hook.
   *
   * @return {void}
   */
  activate() {
    const self = this;
    self._logger.debug('Activating auto-complete provider.');
    return new Promise((resolve, reject) => {
      self._logger.debug('Registering configuration observers.');
      self
        ._registerConfObservers()
        .then(() => {
          self
            ._registerPathProviders()
            .then(() => {
              self._logger.debug('Auto-complete provider activated.');
              resolve(true);
            })
            .catch(e => {
              self._logger.error('Error registering path providers.');
              reject(e);
            });
        })
        .catch(e => {
          self._logger.error('Error registering configuration observers.');
          reject(e);
        });
    });
  }

  /**
   * dispose - Autocomplete-plus provider disposition hook.
   *
   * @return {void}
   */
  dispose() {
    const self = this;
    return new Promise((resolve, reject) => {
      Promise.all([self._disposePathProviders()])
        .then(() => {
          self._logger.debug('Disposing subscriptions.');
          self._subscriptions.forEach(s => s.dispose());
          self._logger.debug('Subscriptions disposed.');
        })
        .catch(e => reject(e));
    });
  }

  /**
   * _resolveSuggestions - facade to resolve suggestions based on enabled strategy.
   *
   * @param  {object} req Request options.
   * @return {Promise} Promise for resolving path suggestions.
   */
  _resolveSuggestions(req) {
    const bpsAll = config.AllProvidersStrategyOn();
    if (bpsAll === true) {
      return this._resolveSuggestionsAcurracyAll(req);
    } else {
      return this._resolveSuggestionsAccuracyBest(req);
    }
  }

  /**
   * _resolveSuggestionsAcurracyAll - Resolves suggestions based on All-Provider strategy.
   * Uses all suited providers that can resolve suggestions for a given context.
   * Filters provider scope selectors.
   * Does not use provider priority.
   *
   * @param  {type}    req Request options.
   * @return {Promise}     Promise for resolving path suggestions.
   */
  _resolveSuggestionsAcurracyAll(req) {
    const self = this;
    return new Promise((resolve, reject) => {
      try {
        self._logger.debug('Using all providers strategy. Priority does not matter.');
        const pathProviders = self._resolveAllPathProviders(req);
        if (pathProviders.length > 0) {
          Promise.all(pathProviders.map(p => p.resolve(req)))
            .then(values => {
              const _suggestions = values.reduce((acum, val) => {
                let _val = val.filter(v => !acum.find(a => a.displayText === v.displayText));
                return acum.concat(_val);
              }, []);
              resolve(_suggestions);
            })
            .catch(e => reject(e));
        } else {
          this._logger.debug('No provider found');
          resolve([]);
        }
      } catch (e) {
        self._logger.error('Error using all providers strategy.');
        reject(e);
      }
    });
  }

  /**
   * _resolveSuggestionsAccuracyBest - Resolves suggestions based on Best-Provider strategy.
   * Search for best suited provider taht can resolve suggestions for a given context based
   * on provider priority.
   * Filters provider scope selectors.
   *
   * @param  {type}    req Request options.
   * @return {Promise}     Promise for resolving path suggestions.
   */
  _resolveSuggestionsAccuracyBest(req) {
    const self = this;
    return new Promise((resolve, reject) => {
      try {
        self._logger.debug('Using priority-based best provider strategy.');
        const pathProvider = this._resolveBestPathProvider(req);
        if (pathProvider) {
          self._logger.debug(`Best provider found: ${pathProvider.id}`);
          pathProvider
            .resolve(req)
            .then(suggestions => resolve(suggestions))
            .catch(e => reject(e));
        } else {
          self._logger.debug('No provider found.');
          resolve([]);
        }
      } catch (e) {
        self._logger.error('Error using priority-based best provider strategy.');
        reject(e);
      }
    });
  }

  /**
   * _registerPathProviders - Register path providers.
   *
   * @return {void}
   */
  _registerPathProviders() {
    const self = this;
    return new Promise((resolve, reject) => {
      try {
        self._logger.debug('Registering path providers.');
        self.pathProviders = [];
        self.pathProviders.push(new CurrentFilePathProvider());
        self.pathProviders.push(new CurrentFileRelativePathProvider());
        self.pathProviders.push(new NodeJSPathProvider());
        self._logger.debug(`${self.pathProviders.length} path providers registered`);

        self._logger.debug('Activating path providers.');
        Promise.all(self.pathProviders.map(p => p.activate()))
          .then(() => {
            self._logger.debug('Providers activated.');
            self._logger.debug('Providers registered.');
            resolve(self.pathProviders);
          })
          .catch(e => {
            self._logger.error('Error activating providers.');
            let _e = (e || {}).error;
            let _provider = (e || {}).provider;
            if (_e) {
              self._logger.error(`Error msg: ${_e}`);
            }
            if (_provider) {
              self._logger.debug(`Deactivating provider ${_provider.id}.`);
              self.pathProviders = self.pathProviders.filter(p => p.id !== _provider.id);
              self._logger.debug(`Provider ${_provider.id} deactivated.`);
            }
            resolve(self.pathProviders);
          });
      } catch (e) {
        self._logger.debug('Error registering providers.');
        reject(e);
      }
    });
  }

  /**
   * _providerFilter - Filter function for valid providers.
   *
   * @param  {object} req Request options
   * @return {Array}      Array containing filtered valid rpoviders.
   */
  _providerFilter(req) {
    const scopesAtCursor = req.editor.getLastCursor().getScopeDescriptor().scopes;
    this._logger.debug(`scopesAtCursor: ${scopesAtCursor}`);
    const cfScopeSelector = atom.config.get(`${consts.PACKAGE_NAME}.${consts.CF_ALLOWED_SCOPES}`);
    this._logger.debug(`cfScopeSelector: ${cfScopeSelector}`);
    return p => {
      let result =
        p instanceof BasePathProvider
          ? p.id === CurrentFileRelativePathProvider.id
            ? selectors.anySelectorMatchAllScopes(cfScopeSelector || p.scopeSelector || '', scopesAtCursor)
            : selectors.anySelectorMatchAllScopes(p.scopeSelector || '', scopesAtCursor)
          : false;
      result = result && p.canResolve(req);
      return result;
    };
  }

  /**
   * _resolveAllPathProviders - Resolve all path providers that can resolve
   * suggestions for a given context.
   *
   * @param  {object} req Request options.
   * @return {Array}      Providers.
   */
  _resolveAllPathProviders(req) {
    const providers = this.pathProviders.filter(this._providerFilter(req));
    return providers;
  }

  /**
   * _resolveBestPathProvider - Resolve bes suited path provider taht can
   * resolve suggestions for a given path.
   *
   * @param  {object}           req Request options.
   * @return {BasePathProvider}     Provider.
   */
  _resolveBestPathProvider(req) {
    /*Filter providers by scope*/
    const providers = this.pathProviders.filter(this._providerFilter(req)).sort((a, b) => a.priority - b.priority);
    let provider = null;
    providers.some(p => {
      if (p.canResolve(req)) {
        provider = p;
        return true;
      }
      return false;
    });
    return provider;
  }

  /**
   * _disposePathProviders - Dispose registered path providers.
   *
   * @return {void}
   */
  _disposePathProviders() {
    const self = this;
    return new Promise((resolve, reject) => {
      Promise.all(self.pathProviders.map(p => p.dispose()))
        .then(providers => {
          self._logger.debug(`${providers.length} providers disposed.`);
          providers.forEach(_provider => {
            self._logger.debug(`Provider ${_provider.id} disposed.`);
          });
          resolve(self.pathProviders);
        })
        .catch(e => {
          let _e = (e || {}).error;
          let _provider = (e || {}).provider;
          self._logger.error(`Error disposing provider(s). ${_provider}`);
          self._logger.error(_e);
          reject(_e);
        });
    });
  }

  /**
   * _registerConfObservers - Register configuration observers.
   *
   * @return {void}
   */
  _registerConfObservers() {
    const self = this;
    return new Promise(resolve => {
      self._subscriptions.push(
        config.addObserver(consts.CF_MANUAL_SUGGEST, () => {
          if (config.isManualModeOn()) {
            self._logger.debug('Manual suggestions: ON');
          } else {
            self._logger.debug('Manual suggestions: OFF');
          }
        })
      );
      self._subscriptions.push(
        config.addObserver(consts.CF_ENABLE_DEBUG, () => {
          if (config.isDebugEnabled()) {
            logger.enableDebug();
            self._logger.debug('Debug mode: ON');
          } else {
            self._logger.debug('Debug mode: OFF');
            logger.enableError();
          }
        })
      );
      self._subscriptions.push(
        config.addObserver(consts.CF_PROVIDER_STRATEGY_ALL, () => {
          if (config.AllProvidersStrategyOn()) {
            self._logger.debug('All providers strategy: ON');
          } else {
            self._logger.debug('All providers strategy: OFF');
          }
        })
      );
      self._subscriptions.push(
        config.addObserver(consts.CF_ALLOWED_SCOPES, () => {
          self._logger.debug(`Allowed scopes updated to: [${config.getAllowedScopes()}]`);
        })
      );

      resolve(self._subscriptions);
    });
  }
}

module.exports = AtomPathIntellisenseImpl;