aboutsummaryrefslogblamecommitdiffhomepage
path: root/src/ide.js
blob: ac80789c1ad377ca180afd974e273081b91eaa0d (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14













                                               
                                
 
                                    
 
                                                   
 
                                 
 
                                     
 
                                             
 



                                   
 
                                                              
 
                                                              
 
                                                       
 

                                        
 
                                                                            
 
                    
 
                    
 
                                                                      
 

                                                             
 

                                         
      

                                         
                                   



                                   
                                             
 
                                                                
       

                                       
 
                        
       

      
                             
 
                                                
 


                                

                                                          


                                                                         



















                                                                                
 













                                                                   
 



                                                           
 





                                                                                                                       
 
                                                                                               




                                                      


                                                                                                                 
 































































































































                                                                                                                
'use strict';

const path = require('path');

const Promise = require('bluebird');

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

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

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

const colors = require('chalk');

const debug_ = require('debug');

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

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

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

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

const PsModuleMap = require('./PsModuleMap');

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) {
  const ideClientCommand = 'purs';

  const ideClientArgs = ['ide', 'client'].concat(dargs(options.pscIdeArgs));

  const stderr = [];

  const stdout = [];

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

  return new Promise((resolve, reject) => {
    const ideClient = spawn(ideClientCommand, ideClientArgs);

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

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

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

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

        resolve(result);
      }
    })

    ideClient.stdin.resume();

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

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

function formatIdeResult(result, options, index, length) {
  let 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) {
    const srcPath = path.relative(options.context, filename);
    const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}`

    return fs.readFileAsync(filename, 'utf8').then(source => {
      const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine)
      const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine
      const up = options.pscIdeColors ? colors.red('^') : '^'
      const down = options.pscIdeColors ? colors.red('v') : 'v'
      let 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((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
      }

      const spaces = ' '.repeat(String(pos.endLine).length)
      let snippet = trimmed.map((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(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) {
  const options = psModule.options

  const serverCommand = 'purs';

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

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

  const ideServer = spawn(serverCommand, serverArgs);

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

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

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

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

  return Promise.resolve(ideServer);
};

module.exports.load = function load(psModule) {
  const options = psModule.options

  const body = {command: 'load'};

  return spawnIdeClient(body, options);
};

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

  return retryPromise((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(() => psModule);
};

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

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

  const parseResponse = response => {
    try {
      const parsed = JSON.parse(response);

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

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

  const formatResponse = parsed => {
    const result = Array.isArray(parsed.result) ? parsed.result : [];

    return Promise.map(result, (item, i) => {
      debugVerbose('formatting result %o', item);

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

  return spawnIdeClient(body, options)
    .then(parseResponse)
    .then(formatResponse)
    .then(({ parsed, formatted, formattedMessage }) => {
      if (parsed.resultType === 'success') {
        if (options.warnings && formattedMessage.length) {
          psModule.emitWarning(formattedMessage);
        }

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

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

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

          return isModuleNotFound || isUnknownModule || isUnknownModuleImport;
      })) {
        debug('failed to rebuild because the module is unknown')

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

        return psModule;
      }
    })
  ;
};