aboutsummaryrefslogblamecommitdiffhomepage
path: root/lib/ide.js
blob: 6087e1ca6ae3fb3630d4697f07ffdc613038435c (plain) (tree)













































































































































































































































































                                                                                                                             
'use strict';

var path = require('path');

var Promise = require('bluebird');

var fs = Promise.promisifyAll(require('fs'));

var retryPromise = require('promise-retry');

var spawn = require('cross-spawn');

var colors = require('chalk');

var debug_ = require('debug');

var debug = debug_('purs-loader');

var debugVerbose = debug_('purs-loader:verbose');

var dargs = require('./dargs');

var compile = require('./compile');

var PsModuleMap = require('./purs-module-map');

function UnknownModuleError() {
  this.name = 'UnknownModuleError';
  this.stack = new Error().stack;
}

UnknownModuleError.prototype = Object.create(Error.prototype);

UnknownModuleError.prototype.constructor = UnknownModuleError;

module.exports.UnknownModuleError = UnknownModuleError;

function spawnIdeClient(body, options) {
  var ideClientCommand = options.pscIdeClient || 'purs';

  var ideClientArgs = (options.pscIdeClient ? [] : ['ide', 'client']).concat(dargs(options.pscIdeClientArgs));

  var stderr = [];

  var stdout = [];

  debug('ide client %s %o %O', ideClientCommand, ideClientArgs, body);

  return new Promise(function (resolve, reject) {
    var ideClient = spawn(ideClientCommand, ideClientArgs);

    ideClient.stderr.on('data', function (data) {
      stderr.push(data.toString());
    });

    ideClient.stdout.on('data', function (data) {
      stdout.push(data.toString());
    });

    ideClient.on('close', function (code) {
      if (code !== 0) {
        var errorMessage = stderr.join('');

        reject(new Error('ide client failed: ' + errorMessage));
      } else {
        var result = stdout.join('');

        resolve(result);
      }
    });

    ideClient.stdin.resume();

    ideClient.stdin.write(JSON.stringify(body));

    ideClient.stdin.write('\n');
  });
}

function formatIdeResult(result, options, index, length) {
  var numAndErr = '[' + (index + 1) + '/' + length + ' ' + result.errorCode + ']';
  numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr;

  function makeResult() {
    return Promise.resolve('\n' + numAndErr + ' ' + result.message);
  }

  function makeResultSnippet(filename, pos) {
    var srcPath = path.relative(options.context, filename);
    var fileAndPos = srcPath + ':' + pos.startLine + ':' + pos.startColumn;

    return fs.readFileAsync(filename, 'utf8').then(function (source) {
      var lines = source.split('\n').slice(pos.startLine - 1, pos.endLine);
      var endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine;
      var up = options.pscIdeColors ? colors.red('^') : '^';
      var down = options.pscIdeColors ? colors.red('v') : 'v';
      var trimmed = lines.slice(0);

      if (endsOnNewline) {
        lines.splice(lines.length - 1, 1);
        pos.endLine = pos.endLine - 1;
        pos.endColumn = lines[lines.length - 1].length || 1;
      }

      // strip newlines at the end
      if (endsOnNewline) {
        trimmed = lines.reverse().reduce(function (trimmed, line, i) {
          if (i === 0 && line === '') trimmed.trimming = true;
          if (!trimmed.trimming) trimmed.push(line);
          if (trimmed.trimming && line !== '') {
            trimmed.trimming = false;
            trimmed.push(line);
          }
          return trimmed;
        }, []).reverse();
        pos.endLine = pos.endLine - (lines.length - trimmed.length);
        pos.endColumn = trimmed[trimmed.length - 1].length || 1;
      }

      var spaces = ' '.repeat(String(pos.endLine).length);
      var snippet = trimmed.map(function (line, i) {
        return '  ' + (pos.startLine + i) + '  ' + line;
      }).join('\n');

      if (trimmed.length === 1) {
        snippet += '\n  ' + spaces + '  ' + ' '.repeat(pos.startColumn - 1) + up.repeat(pos.endColumn - pos.startColumn + 1);
      } else {
        snippet = '  ' + spaces + '  ' + ' '.repeat(pos.startColumn - 1) + down + '\n' + snippet;
        snippet += '\n  ' + spaces + '  ' + ' '.repeat(pos.endColumn - 1) + up;
      }

      return Promise.resolve('\n' + numAndErr + ' ' + fileAndPos + '\n\n' + snippet + '\n\n' + result.message);
    }).catch(function (error) {
      debug('failed to format ide result: %o', error);

      return Promise.resolve('');
    });
  }

  return result.filename && result.position ? makeResultSnippet(result.filename, result.position) : makeResult();
}

module.exports.connect = function connect(psModule) {
  var options = psModule.options;

  var serverCommand = options.pscIdeServer || 'purs';

  var serverArgs = (options.pscIdeServer ? [] : ['ide', 'server']).concat(dargs(Object.assign({
    outputDirectory: options.output,
    '_': options.src
  }, options.pscIdeServerArgs)));

  debug('ide server: %s %o', serverCommand, serverArgs);

  var ideServer = spawn(serverCommand, serverArgs);

  ideServer.stdout.on('data', function (data) {
    debugVerbose('ide server stdout: %s', data.toString());
  });

  ideServer.stderr.on('data', function (data) {
    debugVerbose('ide server stderr: %s', data.toString());
  });

  ideServer.on('error', function (error) {
    debugVerbose('ide server error: %o', error);
  });

  ideServer.on('close', function (code, signal) {
    debugVerbose('ide server close: %s %s', code, signal);
  });

  return Promise.resolve(ideServer);
};

module.exports.load = function load(psModule) {
  var options = psModule.options;

  var body = { command: 'load' };

  return spawnIdeClient(body, options);
};

module.exports.loadWithRetry = function loadWithRetry(psModule) {
  var retries = 9;

  return retryPromise(function (retry, number) {
    debugVerbose('attempting to load modules (%d out of %d attempts)', number, retries);

    return module.exports.load(psModule).catch(retry);
  }, {
    retries: retries,
    factor: 1,
    minTimeout: 333,
    maxTimeout: 333
  }).then(function () {
    return psModule;
  });
};

module.exports.rebuild = function rebuild(psModule) {
  var options = psModule.options;

  var body = {
    command: 'rebuild',
    params: {
      file: psModule.srcPath
    }
  };

  var parseResponse = function parseResponse(response) {
    try {
      var parsed = JSON.parse(response);

      debugVerbose('parsed JSON response: %O', parsed);

      return Promise.resolve(parsed);
    } catch (error) {
      return Promise.reject(error);
    }
  };

  var formatResponse = function formatResponse(parsed) {
    var result = Array.isArray(parsed.result) ? parsed.result : [];

    return Promise.map(result, function (item, i) {
      debugVerbose('formatting result %O', item);

      return formatIdeResult(item, options, i, result.length);
    }).then(function (formatted) {
      return {
        parsed: parsed,
        formatted: formatted,
        formattedMessage: formatted.join('\n')
      };
    });
  };

  return spawnIdeClient(body, options).then(parseResponse).then(formatResponse).then(function (_ref) {
    var parsed = _ref.parsed,
        formatted = _ref.formatted,
        formattedMessage = _ref.formattedMessage;

    if (parsed.resultType === 'success') {
      if (options.warnings && formattedMessage.length) {
        psModule.emitWarning(formattedMessage);
      }

      return psModule;
    } else if ((parsed.result || []).some(function (item) {
      var isModuleNotFound = item.errorCode === 'ModuleNotFound';

      var isUnknownModule = item.errorCode === 'UnknownModule';

      var isUnknownModuleImport = item.errorCode === 'UnknownName' && /Unknown module/.test(item.message);

      return isModuleNotFound || isUnknownModule || isUnknownModuleImport;
    })) {
      debug('module %s was not rebuilt because the module is unknown', psModule.name);

      return Promise.reject(new UnknownModuleError());
    } else {
      if (formattedMessage.length) {
        psModule.emitError(formattedMessage);
      }

      return psModule;
    }
  });
};