<commit_before>const _ = require('underscore');
const createMitm = require('mitm-papandreou');
const http = require('http');
const https = require('https');
const messy = require('messy');
const semver = require('semver');
const stream = require('stream');
const unexpected = require('unexpected');
const unexpectedMessy = require('unexpected-messy');

const consumeReadableStream = require('./consumeReadableStream');
const createSerializedRequestHandler = require('./createSerializedRequestHandler');
const errors = require('./errors');
const formatHeaderObj = require('./formatHeaderObj');
const isBodyJson = require('./isBodyJson');
const resolveExpectedRequestProperties = require('./resolveExpectedRequestProperties');
const trimHeadersLower = require('./trimHeadersLower');

const expect = unexpected.clone().use(unexpectedMessy);

const NODE_10_AND_LATER = semver.satisfies(process.version, '>= 10');

function calculateBodyByteCount(chunk) {
  const trailerIdx = findHeaderSeparator(chunk);
  if (trailerIdx !== -1) {
    return chunk.slice(trailerIdx + 4).length;
  }

  return 0;
}

function createMockResponse(responseProperties) {
  const mockResponse = new messy.HttpResponse(responseProperties);
  mockResponse.statusCode = mockResponse.statusCode || 200;
  mockResponse.protocolName = mockResponse.protocolName || 'HTTP';
  mockResponse.protocolVersion = mockResponse.protocolVersion || '1.1';
  mockResponse.statusMessage =
    mockResponse.statusMessage || http.STATUS_CODES[mockResponse.statusCode];
  return mockResponse;
}

function getMockResponse(responseProperties) {
  let mockResponse;
  let mockResponseError;
  if (Object.prototype.toString.call(responseProperties) === '[object Error]') {
    mockResponseError = responseProperties;
  } else {
    mockResponse = createMockResponse(responseProperties);
  }

  return expect
    .promise(() => {
      if (
        !mockResponseError &&
        mockResponse &&
        mockResponse.body &&
        typeof mockResponse.body.pipe === 'function'
      ) {
        return consumeReadableStream(mockResponse.body).then(result => {
          if (result.error) {
            mockResponseError = result.error;
          }
          if (result.body) {
            mockResponse.unchunkedBody = result.body;
          }
        });
      }
    })
    .then(() => {
      if (mockResponse && !mockResponseError && isBodyJson(mockResponse.body)) {
        if (!mockResponse.headers.has('Content-Type')) {
          mockResponse.headers.set('Content-Type', 'application/json');
        }
      }
      return {
        response: mockResponse,
        error: mockResponseError
      };
    });
}

function findHeaderSeparator(chunk) {
  return chunk.toString().indexOf('\r\n\r\n');
}

function isTrailerChunk(chunk) {
  return chunk.slice(-5).toString() === '0\r\n\r\n';
}

function attachConnectionHook(connection, callback) {
  let bodyByteCount = 0;
  let contentLengthByteCount = 0;
  let hasContentLength = false;
  let hasTransferEncoding = false;
  const rawChunks = [];
  let sawHeaders = false;
  const _write = connection._write;

  // Wrap low level write to gather response data and return
  // raw data to the callback when entire response is written.
  connection._write = (chunk, encoding, cb) => {
    chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);

    rawChunks.push(chunk);

    if (!sawHeaders) {
      const chunkString = chunk.toString();
      let match;

      match = chunkString.match(/^Transfer-Encoding: /im);
      if (match) {
        hasTransferEncoding = true;
        sawHeaders = true;
      }

      match = chunkString.match(/^Content-Length: (\d+)/im);
      if (match && !hasTransferEncoding) {
        // handle the current chunk containing body bytes
        bodyByteCount = calculateBodyByteCount(chunk);
        contentLengthByteCount = +match[1];
        hasContentLength = true;
        sawHeaders = true;
      }
    } else {
      bodyByteCount += chunk.length;
    }

    if (
      (isTrailerChunk(chunk) && !hasContentLength) ||
      (hasContentLength && bodyByteCount === contentLengthByteCount)
    ) {
      // Explicitly execute the callback returning raw data here
      // to ensure it is run before issuing the final write which
      // will complete the request on the wire.
      callback(null, Buffer.concat(rawChunks));
    }

    _write.call(connection, chunk, encoding, cb);
  };
}

function attachResponseHooks(res, callback) {
  let hasEnded = false; // keep track of whether res.end() has been called

  const connection = NODE_10_AND_LATER
    ? res.connection._handle
    : res.connection;

  attachConnectionHook(connection, (_err, rawBuffer) => {
    if (hasEnded) {
      callback(null, rawBuffer);
    }
  });

  ['end', 'destroy'].forEach(methodName => {
    const orig = res[methodName];
    res[methodName] = function(chunk, encoding) {
      if (methodName === 'end') {
        hasEnded = true;
      }

      const returnValue = orig.apply(this, arguments);

      if (methodName === 'destroy') {
        hasEnded = true;
        callback(null);
      }

      // unconditionally return value given we do not wrap write
      return returnValue;
    };
  });
}

function observeResponse(res) {
  return new Promise(resolve => {
    attachResponseHooks(res, (_err, rawBuffer) => {
      resolve(rawBuffer);
    });
  });
}

function trimMockResponse(mockResponse) {
  const responseProperties = {};
  responseProperties.statusCode = mockResponse.statusCode;
  responseProperties.statusMessage = mockResponse.statusMessage;
  responseProperties.protocolName = mockResponse.protocolName;
  responseProperties.protocolVersion = mockResponse.protocolVersion;
  responseProperties.headers = formatHeaderObj(
    mockResponse.headers.valuesByName
  );

  if (mockResponse.body) {
    // read out the messy decoded body
    responseProperties.body = mockResponse.body;
  }

  return responseProperties;
}

class UnexpectedMitmMocker {
  constructor(options) {
    this.requestDescriptions = options.requestDescriptions || [];
    this.timeline = null;
    this.fulfilmentValue = null;
  }

  mock(consumptionFunction) {
    const that = this;
    const requestDescriptions = this.requestDescriptions;
    const mitm = createMitm();

    // Keep track of the current requestDescription
    let nextRequestDescriptionIndex = 0;

    // Keep track of the http/https agents that we have seen
    // during the test so we can clean up afterwards:
    const seenAgents = [];

    // accumulator for events
    const timeline = [];

    function cleanup() {
      seenAgents.forEach(agent => {
        if (agent.freeSockets) {
          Object.keys(agent.freeSockets).forEach(key => {
            agent.freeSockets[key] = [];
          });
        }
      });
      mitm.disable();
    }

    return new Promise((resolve, reject) => {
      mitm.on(
        'request',
        createSerializedRequestHandler((req, res) => {
          if (!res.connection) {
            // I've observed some cases where keep-alive is
            // being used and we end up with an extra "phantom
            // request event" even though only requeest is being
            // issued. Seems like a bug in mitm.
            // It goes without saying that we should try to get
            // rid of this. Hopefully something will happen
            // on https://github.com/moll/node-mitm/pull/36
            return;
          }
          const clientSocket = req.connection._mitm.client;
          let clientSocketOptions = req.connection._mitm.opts;
          if (typeof clientSocketOptions.port === 'string') {
            // The port could have been defined as a string in a 3rdparty library doing the http(s) call, and that seems to be valid use of the http(s) module
            clientSocketOptions = _.defaults(
              {
                port: parseInt(clientSocketOptions.port, 10)
              },
              clientSocketOptions
            );
          }
          const agent =
            clientSocketOptions.agent ||
            (res.connection.encrypted ? https : http).globalAgent;
          if (seenAgents.indexOf(agent) === -1) {
            seenAgents.push(agent);
          }

          const metadata = _.defaults(
            { encrypted: Boolean(res.connection.encrypted) },
            _.pick(
              clientSocketOptions,
              messy.HttpRequest.metadataPropertyNames
            ),
            _.pick(
              clientSocketOptions &&
                clientSocketOptions.agent &&
                clientSocketOptions.agent.options,
              messy.HttpRequest.metadataPropertyNames
            )
          );

          const requestDescription =
            requestDescriptions[nextRequestDescriptionIndex];
          nextRequestDescriptionIndex += 1;
          const hasRequestDescription = !!requestDescription;

          let requestProperties;
          let expectedRequestProperties;

          const responseProperties = hasRequestDescription
            ? requestDescription.response
            : undefined;

          Promise.resolve()
            .then(() => {
              expectedRequestProperties = resolveExpectedRequestProperties(
                requestDescription && requestDescription.request
              );
            })
            .then(() => consumeReadableStream(req, { skipConcat: true }))
            .then(result => {
              if (result.error) {
                // TODO: Consider adding support for recording this (the request erroring out while we're consuming it)
                throw result.error;
              }
              requestProperties = _.extend(
                {
                  method: req.method,
                  path: req.url,
                  protocolName: 'HTTP',
                  protocolVersion: req.httpVersion,
                  headers: req.headers,
                  unchunkedBody: Buffer.concat(result.body)
                },
                metadata
              );

              if (!hasRequestDescription) {
                // there was no mock so arrange "<no response>"
                assertMockResponse(null);

                // We wish to cause the generation of a diff
                // so we arrange to enter our 'success' path
                // but ensure the delegated assertion fully
                // completes by signalling an error condition.
                //
                // Note the use of the single reject/resolve
                // behaviour of promises: while the delegated
                // assertion is reject()ed, we have already
                // resolve()d thus the rejection of the former
                // is effectively ignored and we proceed with
                // our output.

                // cancel the delegated assertion
                throw new errors.SawUnexpectedRequestsError(
                  'unexpected-mitm: Saw unexpected requests.'
                );
              }

              if (typeof responseProperties === 'function') {
                // reset the readable req stream state
                stream.Readable.call(req);

                // read stream data from the buffered chunks
                req._read = function() {
                  this.push(result.body.shift() || null);
                };

                // call response function inside a promise to catch exceptions
                return Promise.resolve()
                  .then(() => {
                    responseProperties(req, res);
                  })
                  .then(() => ({
                    response: null,
                    error: null
                  }))
                  .catch((
                    err // ensure delivery of the caught error to the underlying socket
                  ) => ({
                    response: null,
                    error: err
                  }));
              } else {
                return getMockResponse(responseProperties);
              }
            })
            .then(result => {
              const mockResponse = result.response;
              const mockResponseError = result.error;

              if (!(mockResponse || mockResponseError)) {
                return;
              }

              return Promise.resolve()
                .then(() => {
                  const assertionMockRequest = new messy.HttpRequest(
                    requestProperties
                  );
                  trimHeadersLower(assertionMockRequest);

                  expect.errorMode = 'default';
                  return expect(
                    assertionMockRequest,
                    'to satisfy',
                    expectedRequestProperties
                  );
                })
                .then(() => {
                  deliverMockResponse(mockResponse, mockResponseError);
                })
                .catch(e => {
                  assertMockResponse(mockResponse, mockResponseError);
                  throw new errors.EarlyExitError(
                    'Seen request did not match the expected request.'
                  );
                });
            })
            .catch(e => {
              // Given an error occurs, the deferred assertion
              // will still be pending and must be completed. We
              // do this by signalling the error on the socket.
              try {
                clientSocket.emit('error', e);
              } catch (e) {
                // If an something was thrown trying to signal
                // an error we have little choice but to simply
                // reject the assertion. This is only safe with
                // Unexpected 10.15.x and above.
              }

              timeline.push(e);
              reject(e);
            });

          function assertMockResponse(mockResponse, mockResponseError) {
            const mockRequest = new messy.HttpRequest(requestProperties);
            const exchange = new messy.HttpExchange({ request: mockRequest });
            if (mockResponse) {
              exchange.response = mockResponse;
            } else if (mockResponseError) {
              exchange.response = mockResponseError;
            }

            let spec = null;
            // only attempt request validation with a mock
            if (hasRequestDescription) {
              spec = { request: expectedRequestProperties };
            }

            // We explicitly do not complete the promise
            // when the last request for which we have a
            // mock is seen.
            //
            // Instead simply record any exchanges that
            // passed through and defer the resolution or
            // rejection to the handler functions attached
            // to the delegated assertion execution below.
            timeline.push({
              exchange,
              spec
            });
          }

          function deliverMockResponse(mockResponse, mockResponseError) {
            setImmediate(() => {
              let nonEmptyMockResponse = false;
              if (mockResponse) {
                res.statusCode = mockResponse.statusCode;
                mockResponse.headers.getNames().forEach(headerName => {
                  mockResponse.headers.getAll(headerName).forEach(value => {
                    nonEmptyMockResponse = true;
                    res.setHeader(headerName, value);
                  });
                });
                const unchunkedBody = mockResponse.unchunkedBody;
                if (unchunkedBody === null) {
                  nonEmptyMockResponse = true;
                  res.write(Buffer.from('null'));
                } else if (
                  typeof unchunkedBody !== 'undefined' &&
                  unchunkedBody.length > 0
                ) {
                  nonEmptyMockResponse = true;
                  res.write(unchunkedBody);
                } else if (nonEmptyMockResponse) {
                  res.writeHead(res.statusCode || 200);
                }
              }
              if (mockResponseError) {
                setImmediate(() => {
                  clientSocket.emit('error', mockResponseError);
                  assertMockResponse(mockResponse, mockResponseError);
                });
              } else {
                res.end();
              }
            });
          }

          // Hook the final write and immediately assert the request.
          // Note this occurs prior to it being written on the wire.
          observeResponse(res).then(rawBuffer => {
            const mockResponse = createMockResponse(rawBuffer);
            assertMockResponse(mockResponse);
          });
        })
      );

      // handle synchronous throws
      let consumer;
      try {
        const consumerResult = consumptionFunction();
        // ensure consumption function result is a promise
        consumer = Promise.resolve(consumerResult);
      } catch (e) {
        timeline.push(e);
        return reject(e);
      }

      consumer
        .then(fulfilmentValue => {
          // Handle the case of specified but unprocessed mocks.
          // If there were none remaining immediately complete.
          //
          // Where the driving assertion resolves we must check
          // if any mocks still exist. If so, we add them to the
          // set of expected exchanges and resolve the promise.
          const hasRemainingRequestDescriptions =
            nextRequestDescriptionIndex < requestDescriptions.length;
          if (hasRemainingRequestDescriptions) {
            // exhaust remaining mocks using a promises chain
            return (function nextItem() {
              const remainingDescription =
                requestDescriptions[nextRequestDescriptionIndex];
              nextRequestDescriptionIndex += 1;
              if (remainingDescription) {
                let expectedRequestProperties;

                return Promise.resolve()
                  .then(() => {
                    expectedRequestProperties = resolveExpectedRequestProperties(
                      remainingDescription.request
                    );
                  })
                  .then(() => getMockResponse(remainingDescription.response))
                  .then(result => {
                    const spec = {
                      request: expectedRequestProperties,
                      response:
                        result.error || trimMockResponse(result.response)
                    };

                    timeline.push({ spec });

                    return nextItem();
                  });
              } else {
                throw new errors.UnexercisedMocksError();
              }
            })();
          } else {
            resolve(fulfilmentValue);
          }
        })
        .catch(e => {
          timeline.push(e);
          reject(e);
        });
    })
      .then(fulfilmentValue => {
        cleanup();

        that.timeline = timeline;
        that.fulfilmentValue = fulfilmentValue;

        return {
          timeline,
          fulfilmentValue
        };
      })
      .catch(() => {
        cleanup();

        that.timeline = timeline;
        that.fulfilmentValue = null;

        // given we will later fail on "to satisfy" pass a null fulfilment value
        return {
          timeline,
          fulfilmentValue: null
        };
      });
  }
}

module.exports = UnexpectedMitmMocker;
<commit_msg>Use objects for internal request and response state within Mocker.<commit_after>const _ = require('underscore');
const createMitm = require('mitm-papandreou');
const http = require('http');
const https = require('https');
const messy = require('messy');
const semver = require('semver');
const stream = require('stream');
const unexpected = require('unexpected');
const unexpectedMessy = require('unexpected-messy');

const consumeReadableStream = require('./consumeReadableStream');
const createSerializedRequestHandler = require('./createSerializedRequestHandler');
const errors = require('./errors');
const formatHeaderObj = require('./formatHeaderObj');
const isBodyJson = require('./isBodyJson');
const resolveExpectedRequestProperties = require('./resolveExpectedRequestProperties');
const trimHeadersLower = require('./trimHeadersLower');

const expect = unexpected.clone().use(unexpectedMessy);

const NODE_10_AND_LATER = semver.satisfies(process.version, '>= 10');

function calculateBodyByteCount(chunk) {
  const trailerIdx = findHeaderSeparator(chunk);
  if (trailerIdx !== -1) {
    return chunk.slice(trailerIdx + 4).length;
  }

  return 0;
}

function createMockResponse(responseProperties) {
  const mockResponse = new messy.HttpResponse(responseProperties);
  mockResponse.statusCode = mockResponse.statusCode || 200;
  mockResponse.protocolName = mockResponse.protocolName || 'HTTP';
  mockResponse.protocolVersion = mockResponse.protocolVersion || '1.1';
  mockResponse.statusMessage =
    mockResponse.statusMessage || http.STATUS_CODES[mockResponse.statusCode];
  return mockResponse;
}

function getMockResponse(responseProperties) {
  let mockResponse;
  let mockResponseError;
  if (Object.prototype.toString.call(responseProperties) === '[object Error]') {
    mockResponseError = responseProperties;
  } else {
    mockResponse = createMockResponse(responseProperties);
  }

  return expect
    .promise(() => {
      if (
        !mockResponseError &&
        mockResponse &&
        mockResponse.body &&
        typeof mockResponse.body.pipe === 'function'
      ) {
        return consumeReadableStream(mockResponse.body).then(result => {
          if (result.error) {
            mockResponseError = result.error;
          }
          if (result.body) {
            mockResponse.unchunkedBody = result.body;
          }
        });
      }
    })
    .then(() => {
      if (mockResponse && !mockResponseError && isBodyJson(mockResponse.body)) {
        if (!mockResponse.headers.has('Content-Type')) {
          mockResponse.headers.set('Content-Type', 'application/json');
        }
      }
      return {
        response: mockResponse,
        error: mockResponseError
      };
    });
}

function findHeaderSeparator(chunk) {
  return chunk.toString().indexOf('\r\n\r\n');
}

function isTrailerChunk(chunk) {
  return chunk.slice(-5).toString() === '0\r\n\r\n';
}

function attachConnectionHook(connection, callback) {
  let bodyByteCount = 0;
  let contentLengthByteCount = 0;
  let hasContentLength = false;
  let hasTransferEncoding = false;
  const rawChunks = [];
  let sawHeaders = false;
  const _write = connection._write;

  // Wrap low level write to gather response data and return
  // raw data to the callback when entire response is written.
  connection._write = (chunk, encoding, cb) => {
    chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);

    rawChunks.push(chunk);

    if (!sawHeaders) {
      const chunkString = chunk.toString();
      let match;

      match = chunkString.match(/^Transfer-Encoding: /im);
      if (match) {
        hasTransferEncoding = true;
        sawHeaders = true;
      }

      match = chunkString.match(/^Content-Length: (\d+)/im);
      if (match && !hasTransferEncoding) {
        // handle the current chunk containing body bytes
        bodyByteCount = calculateBodyByteCount(chunk);
        contentLengthByteCount = +match[1];
        hasContentLength = true;
        sawHeaders = true;
      }
    } else {
      bodyByteCount += chunk.length;
    }

    if (
      (isTrailerChunk(chunk) && !hasContentLength) ||
      (hasContentLength && bodyByteCount === contentLengthByteCount)
    ) {
      // Explicitly execute the callback returning raw data here
      // to ensure it is run before issuing the final write which
      // will complete the request on the wire.
      callback(null, Buffer.concat(rawChunks));
    }

    _write.call(connection, chunk, encoding, cb);
  };
}

function attachResponseHooks(res, callback) {
  let hasEnded = false; // keep track of whether res.end() has been called

  const connection = NODE_10_AND_LATER
    ? res.connection._handle
    : res.connection;

  attachConnectionHook(connection, (_err, rawBuffer) => {
    if (hasEnded) {
      callback(null, rawBuffer);
    }
  });

  ['end', 'destroy'].forEach(methodName => {
    const orig = res[methodName];
    res[methodName] = function(chunk, encoding) {
      if (methodName === 'end') {
        hasEnded = true;
      }

      const returnValue = orig.apply(this, arguments);

      if (methodName === 'destroy') {
        hasEnded = true;
        callback(null);
      }

      // unconditionally return value given we do not wrap write
      return returnValue;
    };
  });
}

function observeResponse(res) {
  return new Promise(resolve => {
    attachResponseHooks(res, (_err, rawBuffer) => {
      resolve(rawBuffer);
    });
  });
}

function trimMockResponse(mockResponse) {
  const responseProperties = {};
  responseProperties.statusCode = mockResponse.statusCode;
  responseProperties.statusMessage = mockResponse.statusMessage;
  responseProperties.protocolName = mockResponse.protocolName;
  responseProperties.protocolVersion = mockResponse.protocolVersion;
  responseProperties.headers = formatHeaderObj(
    mockResponse.headers.valuesByName
  );

  if (mockResponse.body) {
    // read out the messy decoded body
    responseProperties.body = mockResponse.body;
  }

  return responseProperties;
}

class UnexpectedMitmMocker {
  constructor(options) {
    this.requestDescriptions = options.requestDescriptions || [];
    this.timeline = null;
    this.fulfilmentValue = null;
  }

  mock(consumptionFunction) {
    const that = this;
    const requestDescriptions = this.requestDescriptions;
    const mitm = createMitm();

    // Keep track of the current requestDescription
    let nextRequestDescriptionIndex = 0;

    // Keep track of the http/https agents that we have seen
    // during the test so we can clean up afterwards:
    const seenAgents = [];

    // accumulator for events
    const timeline = [];

    function cleanup() {
      seenAgents.forEach(agent => {
        if (agent.freeSockets) {
          Object.keys(agent.freeSockets).forEach(key => {
            agent.freeSockets[key] = [];
          });
        }
      });
      mitm.disable();
    }

    function handleRequest(req, metadata, spec) {
      return consumeReadableStream(req, { skipConcat: true }).then(result => {
        const properties = Object.assign(
          {
            method: req.method,
            path: req.url,
            protocolName: 'HTTP',
            protocolVersion: req.httpVersion,
            headers: req.headers,
            unchunkedBody: Buffer.concat(result.body)
          },
          metadata
        );
        const request = new messy.HttpRequest(properties);

        return {
          request,
          error: result.error,
          chunks: result.body,
          properties,
          spec
        };
      });
    }

    function assertExchange(requestStruct, responseStruct) {
      const mockRequest = requestStruct.request;
      const exchange = new messy.HttpExchange({ request: mockRequest });

      if (responseStruct === null) {
        // leave everything unset for "<no response>"
      } else if (responseStruct.response) {
        exchange.response = responseStruct.response;
      } else if (responseStruct.error) {
        exchange.response = responseStruct.error;
      }

      let spec = null;
      // only attempt request validation with a mock
      if (requestStruct.spec) {
        spec = { request: requestStruct.spec };
      }

      // We explicitly do not complete the promise
      // when the last request for which we have a
      // mock is seen.
      //
      // Instead simply record any exchanges that
      // passed through and defer the resolution or
      // rejection to the handler functions attached
      // to the delegated assertion execution below.
      timeline.push({
        exchange,
        spec
      });
    }

    return new Promise((resolve, reject) => {
      mitm.on(
        'request',
        createSerializedRequestHandler((req, res) => {
          if (!res.connection) {
            // I've observed some cases where keep-alive is
            // being used and we end up with an extra "phantom
            // request event" even though only requeest is being
            // issued. Seems like a bug in mitm.
            // It goes without saying that we should try to get
            // rid of this. Hopefully something will happen
            // on https://github.com/moll/node-mitm/pull/36
            return;
          }
          const clientSocket = req.connection._mitm.client;
          let clientSocketOptions = req.connection._mitm.opts;
          if (typeof clientSocketOptions.port === 'string') {
            // The port could have been defined as a string in a 3rdparty library doing the http(s) call, and that seems to be valid use of the http(s) module
            clientSocketOptions = _.defaults(
              {
                port: parseInt(clientSocketOptions.port, 10)
              },
              clientSocketOptions
            );
          }
          const agent =
            clientSocketOptions.agent ||
            (res.connection.encrypted ? https : http).globalAgent;
          if (seenAgents.indexOf(agent) === -1) {
            seenAgents.push(agent);
          }

          const metadata = _.defaults(
            { encrypted: Boolean(res.connection.encrypted) },
            _.pick(
              clientSocketOptions,
              messy.HttpRequest.metadataPropertyNames
            ),
            _.pick(
              clientSocketOptions &&
                clientSocketOptions.agent &&
                clientSocketOptions.agent.options,
              messy.HttpRequest.metadataPropertyNames
            )
          );

          const requestDescription =
            requestDescriptions[nextRequestDescriptionIndex];
          nextRequestDescriptionIndex += 1;
          const hasRequestDescription = !!requestDescription;

          let requestStruct;
          let expectedRequestProperties;

          const responseProperties = hasRequestDescription
            ? requestDescription.response
            : undefined;

          Promise.resolve()
            .then(() => {
              if (!hasRequestDescription) {
                return;
              }

              expectedRequestProperties = resolveExpectedRequestProperties(
                requestDescription && requestDescription.request
              );
            })
            .then(() => handleRequest(req, metadata, expectedRequestProperties))
            .then(result => {
              // make available for use further down the promise chain
              requestStruct = result;

              if (requestStruct.error) {
                // TODO: Consider adding support for recording this (the request erroring out while we're consuming it)
                throw requestStruct.error;
              }

              if (!hasRequestDescription) {
                // there was no mock so arrange "<no response>"
                assertExchange(requestStruct, null);

                // We wish to cause the generation of a diff
                // so we arrange to enter our 'success' path
                // but ensure the delegated assertion fully
                // completes by signalling an error condition.
                //
                // Note the use of the single reject/resolve
                // behaviour of promises: while the delegated
                // assertion is reject()ed, we have already
                // resolve()d thus the rejection of the former
                // is effectively ignored and we proceed with
                // our output.

                // cancel the delegated assertion
                throw new errors.SawUnexpectedRequestsError(
                  'unexpected-mitm: Saw unexpected requests.'
                );
              }

              if (typeof responseProperties === 'function') {
                // reset the readable req stream state
                stream.Readable.call(req);

                // read stream data from the buffered chunks
                req._read = function() {
                  this.push(requestStruct.chunks.shift() || null);
                };

                // call response function inside a promise to catch exceptions
                return Promise.resolve()
                  .then(() => {
                    responseProperties(req, res);
                  })
                  .then(() => ({
                    response: null,
                    error: null
                  }))
                  .catch((
                    err // ensure delivery of the caught error to the underlying socket
                  ) => ({
                    response: null,
                    error: err
                  }));
              } else {
                return getMockResponse(responseProperties);
              }
            })
            .then(responseStruct => {
              if (!(responseStruct.response || responseStruct.error)) {
                return;
              }

              return Promise.resolve()
                .then(() => {
                  const assertionMockRequest = new messy.HttpRequest(
                    requestStruct.properties
                  );
                  trimHeadersLower(assertionMockRequest);

                  expect.errorMode = 'default';
                  return expect(
                    assertionMockRequest,
                    'to satisfy',
                    requestStruct.spec
                  );
                })
                .then(() => deliverMockResponse(responseStruct))
                .catch(e => {
                  assertExchange(requestStruct, responseStruct);
                  throw new errors.EarlyExitError(
                    'Seen request did not match the expected request.'
                  );
                });
            })
            .catch(e => {
              // Given an error occurs, the deferred assertion
              // will still be pending and must be completed. We
              // do this by signalling the error on the socket.
              try {
                clientSocket.emit('error', e);
              } catch (e) {
                // If an something was thrown trying to signal
                // an error we have little choice but to simply
                // reject the assertion. This is only safe with
                // Unexpected 10.15.x and above.
              }

              timeline.push(e);
              reject(e);
            });

          function deliverMockResponse(responseStruct) {
            const mockResponse = responseStruct.response;
            const mockResponseError = responseStruct.error;
            setImmediate(() => {
              let nonEmptyMockResponse = false;
              if (mockResponse) {
                res.statusCode = mockResponse.statusCode;
                mockResponse.headers.getNames().forEach(headerName => {
                  mockResponse.headers.getAll(headerName).forEach(value => {
                    nonEmptyMockResponse = true;
                    res.setHeader(headerName, value);
                  });
                });
                const unchunkedBody = mockResponse.unchunkedBody;
                if (unchunkedBody === null) {
                  nonEmptyMockResponse = true;
                  res.write(Buffer.from('null'));
                } else if (
                  typeof unchunkedBody !== 'undefined' &&
                  unchunkedBody.length > 0
                ) {
                  nonEmptyMockResponse = true;
                  res.write(unchunkedBody);
                } else if (nonEmptyMockResponse) {
                  res.writeHead(res.statusCode || 200);
                }
              }
              if (mockResponseError) {
                setImmediate(() => {
                  clientSocket.emit('error', mockResponseError);
                  assertExchange(requestStruct, responseStruct);
                });
              } else {
                res.end();
              }
            });
          }

          // Hook the final write and immediately assert the request.
          // Note this occurs prior to it being written on the wire.
          observeResponse(res).then(rawBuffer => {
            const response = createMockResponse(rawBuffer);
            assertExchange(requestStruct, { response });
          });
        })
      );

      // handle synchronous throws
      let consumer;
      try {
        const consumerResult = consumptionFunction();
        // ensure consumption function result is a promise
        consumer = Promise.resolve(consumerResult);
      } catch (e) {
        timeline.push(e);
        return reject(e);
      }

      consumer
        .then(fulfilmentValue => {
          // Handle the case of specified but unprocessed mocks.
          // If there were none remaining immediately complete.
          //
          // Where the driving assertion resolves we must check
          // if any mocks still exist. If so, we add them to the
          // set of expected exchanges and resolve the promise.
          const hasRemainingRequestDescriptions =
            nextRequestDescriptionIndex < requestDescriptions.length;
          if (hasRemainingRequestDescriptions) {
            // exhaust remaining mocks using a promises chain
            return (function nextItem() {
              const remainingDescription =
                requestDescriptions[nextRequestDescriptionIndex];
              nextRequestDescriptionIndex += 1;
              if (remainingDescription) {
                let expectedRequestProperties;

                return Promise.resolve()
                  .then(() => {
                    expectedRequestProperties = resolveExpectedRequestProperties(
                      remainingDescription.request
                    );
                  })
                  .then(() => getMockResponse(remainingDescription.response))
                  .then(result => {
                    const spec = {
                      request: expectedRequestProperties,
                      response:
                        result.error || trimMockResponse(result.response)
                    };

                    timeline.push({ spec });

                    return nextItem();
                  });
              } else {
                throw new errors.UnexercisedMocksError();
              }
            })();
          } else {
            resolve(fulfilmentValue);
          }
        })
        .catch(e => {
          timeline.push(e);
          reject(e);
        });
    })
      .then(fulfilmentValue => {
        cleanup();

        that.timeline = timeline;
        that.fulfilmentValue = fulfilmentValue;

        return {
          timeline,
          fulfilmentValue
        };
      })
      .catch(() => {
        cleanup();

        that.timeline = timeline;
        that.fulfilmentValue = null;

        // given we will later fail on "to satisfy" pass a null fulfilment value
        return {
          timeline,
          fulfilmentValue: null
        };
      });
  }
}

module.exports = UnexpectedMitmMocker;
<|endoftext|>


<commit_before>"use strict";

var _path = require("path");

var _tape = require("tape");

var _tape2 = _interopRequireDefault(_tape);

var _memoryFs = require("memory-fs");

var _memoryFs2 = _interopRequireDefault(_memoryFs);

var _webpack = require("webpack");

var _webpack2 = _interopRequireDefault(_webpack);

var _index = require("../index");

var _index2 = _interopRequireDefault(_index);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

(0, _tape2.default)("NpmAutoInstallWebpackPlugin normal resolve", function (t) {
  var compiler = (0, _webpack2.default)({
    context: (0, _path.resolve)(__dirname, "../../"),
    entry: {
      client: (0, _path.resolve)(__dirname, "../__fixtures__/client.fixture.js")
    },
    output: {
      path: __dirname },
    // It will be in MemoryFS :)
    module: {
      loaders: [{
        test: /\.js(x?)$/,
        exclude: /node_modules/,
        loader: "babel"
      }]
    },
    plugins: [new _index2.default()]
  });
  compiler.outputFileSystem = new _memoryFs2.default();

  compiler.run(function (err, stats) {
    t.equal(err, null);

    t.end();
  });
});

(0, _tape2.default)("NpmAutoInstallWebpackPlugin nested resolve", function (t) {
  var compiler = (0, _webpack2.default)({
    context: (0, _path.resolve)(__dirname, "../../"),
    entry: {
      client: (0, _path.resolve)(__dirname, "../__fixtures__/nested.fixture.js")
    },
    output: {
      path: __dirname },
    // It will be in MemoryFS :)
    module: {
      loaders: [{
        test: /\.js(x?)$/,
        exclude: /node_modules/,
        loader: "babel"
      }]
    },
    plugins: [new _index2.default()]
  });
  compiler.outputFileSystem = new _memoryFs2.default();

  compiler.run(function (err, stats) {
    t.equal(err, null);

    t.end();
  });
});<commit_msg>chore(lib): remove test case<commit_after><|endoftext|>


<commit_before>"use babel";

let Function = require('loophole').Function;
let _ = require('underscore-plus');

const REGEXP_LINE = /(([\$\w]+[\w-]*)|([.:;'"[{( ]+))$/g;

export default class Provider {

  constructor(manager) {

    this.manager = undefined;
    this.force = false;

    // automcomplete-plus
    this.selector = '.source.js';
    this.disableForSelector = '.source.js .comment';
    this.inclusionPriority = 1;
    this.excludeLowerPriority = false;

    this.line = undefined;
    this.lineMatchResult = undefined;
    this.tempPrefix = undefined;
    this.suggestionsArr = undefined;
    this.suggestion = undefined;
    this.suggestionClone = undefined;
  }

  init(manager) {

    this.manager = manager;
    this.excludeLowerPriority = this.manager.packageConfig.options.excludeLowerPriorityProviders;

    if (this.manager.packageConfig.options.displayAboveSnippets) {

      this.suggestionPriority = 2;
    }
  }

  isValidPrefix(prefix, prefixLast) {

    if (prefixLast === undefined) {

      return false;
    }

    if (prefixLast === '\.') {

      return true;
    }

    if (prefixLast.match(/;|\s/)) {

      return false;
    }

    if (prefix.length > 1) {

      prefix = `_${prefix}`;
    }

    try {

      (new Function(`var ${prefix}`))();

    } catch (e) {

      return false;
    }

    return true;
  }

  checkPrefix(prefix) {

    if (prefix.match(/(\s|;|\.|\"|\')$/) || prefix.replace(/\s/g, '').length === 0) {

      return '';
    }

    return prefix;
  }

  getPrefix(editor, bufferPosition) {

    this.line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition]);
    this.lineMatchResult = this.line.match(REGEXP_LINE);

    if (this.lineMatchResult) {

      return this.lineMatchResult[0];
    }
  }

  getSuggestions({editor, bufferPosition, scopeDescriptor, prefix, activatedManually}) {

    if (!this.manager.client) {

      return [];
    }

    this.tempPrefix = this.getPrefix(editor, bufferPosition) || prefix;

    if (!this.isValidPrefix(this.tempPrefix, this.tempPrefix[this.tempPrefix.length - 1]) && !this.force && !activatedManually) {

      return [];
    }

    prefix = this.checkPrefix(this.tempPrefix);

    return new Promise((resolve) => {

      this.manager.client.update(editor).then((data) => {

        if (data.isQueried) {

          return;
        }

        this.manager.client.completions(atom.project.relativizePath(editor.getURI())[1], {

          line: bufferPosition.row,
          ch: bufferPosition.column

        }).then((data) => {

          if (!data) {

            return resolve([]);
          }

          if (!data.completions.length) {

            return resolve([]);
          }

          this.suggestionsArr = [];

          for (let obj of data.completions) {

            obj = this.manager.helper.formatTypeCompletion(obj);

            this.suggestion = {

              text: obj.name,
              replacementPrefix: prefix,
              className: null,
              type: obj._typeSelf,
              leftLabel: obj.leftLabel,
              snippet: obj._snippet,
              displayText: obj._displayText,
              description: obj.doc || null,
              descriptionMoreURL: obj.url || null
            };

            if (this.manager.packageConfig.options.useSnippetsAndFunction && obj._hasParams) {

              this.suggestionClone = _.clone(this.suggestion);
              this.suggestionClone.type = 'snippet';

              if (obj._hasParams) {

                this.suggestion.snippet = `${obj.name}($\{0:\})`;

              } else {

                this.suggestion.snippet = `${obj.name}()`;
              }

              this.suggestionsArr.push(this.suggestion);
              this.suggestionsArr.push(this.suggestionClone);

            } else {

              this.suggestionsArr.push(this.suggestion);
            }
          }

          resolve(this.suggestionsArr);
        });
      });
    });
  }

  forceCompletion() {

    this.force = true;
    atom.commands.dispatch(atom.views.getView(atom.workspace.getActiveTextEditor()), 'autocomplete-plus:activate');
    this.force = false;
  }
}
<commit_msg>return a promise in any case<commit_after>"use babel";

let Function = require('loophole').Function;
let _ = require('underscore-plus');

const REGEXP_LINE = /(([\$\w]+[\w-]*)|([.:;'"[{( ]+))$/g;

export default class Provider {

  constructor(manager) {

    this.manager = undefined;
    this.force = false;

    // automcomplete-plus
    this.selector = '.source.js';
    this.disableForSelector = '.source.js .comment';
    this.inclusionPriority = 1;
    this.excludeLowerPriority = false;

    this.line = undefined;
    this.lineMatchResult = undefined;
    this.tempPrefix = undefined;
    this.suggestionsArr = undefined;
    this.suggestion = undefined;
    this.suggestionClone = undefined;
  }

  init(manager) {

    this.manager = manager;
    this.excludeLowerPriority = this.manager.packageConfig.options.excludeLowerPriorityProviders;

    if (this.manager.packageConfig.options.displayAboveSnippets) {

      this.suggestionPriority = 2;
    }
  }

  isValidPrefix(prefix, prefixLast) {

    if (prefixLast === undefined) {

      return false;
    }

    if (prefixLast === '\.') {

      return true;
    }

    if (prefixLast.match(/;|\s/)) {

      return false;
    }

    if (prefix.length > 1) {

      prefix = `_${prefix}`;
    }

    try {

      (new Function(`var ${prefix}`))();

    } catch (e) {

      return false;
    }

    return true;
  }

  checkPrefix(prefix) {

    if (prefix.match(/(\s|;|\.|\"|\')$/) || prefix.replace(/\s/g, '').length === 0) {

      return '';
    }

    return prefix;
  }

  getPrefix(editor, bufferPosition) {

    this.line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition]);
    this.lineMatchResult = this.line.match(REGEXP_LINE);

    if (this.lineMatchResult) {

      return this.lineMatchResult[0];
    }
  }

  getSuggestions({editor, bufferPosition, scopeDescriptor, prefix, activatedManually}) {

    return new Promise((resolve) => {

      if (!this.manager.client) {

        return resolve([]);
      }

      this.tempPrefix = this.getPrefix(editor, bufferPosition) || prefix;

      if (!this.isValidPrefix(this.tempPrefix, this.tempPrefix[this.tempPrefix.length - 1]) && !this.force && !activatedManually) {

        return resolve([]);
      }

      prefix = this.checkPrefix(this.tempPrefix);

      this.manager.client.update(editor).then((data) => {

        if (data.isQueried) {

          return resolve([]);
        }

        this.manager.client.completions(atom.project.relativizePath(editor.getURI())[1], {

          line: bufferPosition.row,
          ch: bufferPosition.column

        }).then((data) => {

          if (!data) {

            return resolve([]);
          }

          if (!data.completions.length) {

            return resolve([]);
          }

          this.suggestionsArr = [];

          for (let obj of data.completions) {

            obj = this.manager.helper.formatTypeCompletion(obj);

            this.suggestion = {

              text: obj.name,
              replacementPrefix: prefix,
              className: null,
              type: obj._typeSelf,
              leftLabel: obj.leftLabel,
              snippet: obj._snippet,
              displayText: obj._displayText,
              description: obj.doc || null,
              descriptionMoreURL: obj.url || null
            };

            if (this.manager.packageConfig.options.useSnippetsAndFunction && obj._hasParams) {

              this.suggestionClone = _.clone(this.suggestion);
              this.suggestionClone.type = 'snippet';

              if (obj._hasParams) {

                this.suggestion.snippet = `${obj.name}($\{0:\})`;

              } else {

                this.suggestion.snippet = `${obj.name}()`;
              }

              this.suggestionsArr.push(this.suggestion);
              this.suggestionsArr.push(this.suggestionClone);

            } else {

              this.suggestionsArr.push(this.suggestion);
            }
          }

          resolve(this.suggestionsArr);
        });
      });
    });
  }

  forceCompletion() {

    this.force = true;
    atom.commands.dispatch(atom.views.getView(atom.workspace.getActiveTextEditor()), 'autocomplete-plus:activate');
    this.force = false;
  }
}
<|endoftext|>


<commit_before>// Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import AxiomError from 'axiom/core/error';

import JsFileSystem from 'axiom/fs/js_file_system';
import JsEntry from 'axiom/fs/js_entry';
import Path from 'axiom/fs/path';

import Termcap from 'axiom_shell/util/termcap';
import WashBuiltins from 'axiom_shell/exe/wash_builtins';
import environment from 'axiom_shell/environment';

/**
 * The shell read-eval-print loop.
 */
export var Shell = function(executeContext) {
  this.executeContext = executeContext;
  executeContext.onSignal.addListener(this.onSignal_.bind(this));

  this.fileSystem = environment.getServiceBinding('filesystems@axiom');

  if (!this.executeContext.getEnv('$PWD'))
    this.executeContext.setEnv('$PWD', '/');

  this.inputHistory = [];

  // The list of currently active jobs.
  this.executeContextList_ = [];

  // The job in the foreground (receives keyboard input).
  this.foregroundContext_ = null;

  this.tc_ = new Termcap();
  this.promptString_ = this.tc_.output(
      '%set-attr(FG_BOLD, FG_CYAN)wash$ %set-attr()');

  this.builtins = new WashBuiltins(this);
  this.builtinsFS = new JsFileSystem();
  this.builtinsFS.rootDirectory.install(this.builtins.callbacks);
};

/**
 * The main entrypoint when invoked as a JsExecutable.
 */
Shell.main = function(executeContext) {
  var shell = new Shell(executeContext);
  window.wash_ = shell;  // Console debugging aid.
  executeContext.ready();

  return shell.readEvalPrintLoop();
};

export default Shell.main;

Shell.prototype.absPath = function(path) {
  return Path.abs(this.executeContext.getEnv('$PWD', '/'), path);
};

Shell.prototype.findExecutable = function(path) {
  var searchList;

  var envPath = this.executeContext.getEnv('@PATH', []);
  if (path.substr(0, 1) == '/') {
    searchList = [path];
  } else {
    searchList = envPath.map(function(p) { return p + '/' + path });
  }

  var searchNextPath = function() {
    if (!searchList.length)
      return Promise.reject(new AxiomError.NotFound('path', path));

    var currentPath = searchList.shift();
    return this.fileSystem.stat(currentPath).then(
      function(result) {
        if (result.mode & Path.mode.x)
          return currentPath;

        return searchNextPath();
      }
    ).catch(function(value) {
      if (AxiomError.NotFound.test(value))
        return searchNextPath();

      return Promise.reject(value);
    });
  }.bind(this);

  return searchNextPath();
};

Shell.prototype.read = function() {
  return this.findExecutable('readline').then(
    function(readlinePath) {
      return this.executeContext.callPromise(
          this.fileSystem,
          readlinePath,
          { promptString: this.promptString_,
            inputHistory: this.inputHistory
          });
    }.bind(this)
  );
};

/**
 * Parse and evaluate a line of shell input.
 */
Shell.prototype.evaluate = function(value) {
  var str = value.trim();

  if (!str)
    return Promise.resolve();

  if (str != this.inputHistory[0])
    this.inputHistory.unshift(str);

  var ary = this.parseShellInput(str);
  return this.dispatch(ary[0], ary[1]);
};

Shell.prototype.readEvalPrint = function() {
  return this.read().then(
    function(result) {
      if (result == null) {
        // EOF from readline.
        return Promise.resolve(null);
      }

      if (typeof result != 'string') {
        return Promise.reject(new AxiomError.Runtime(
            'Unexpected type from readline: ' + (typeof value)));
      }

      return this.evaluate(result).then(
        function(value) {
          if (typeof value != 'undefined' && typeof value != 'number' &&
              value != null) {
            this.println(JSON.stringify(value));
          }

          return Promise.resolve(value);
        }.bind(this)
      ).catch(
        function(error) {
          this.printErrorValue(error);
          return Promise.reject(error);
        }.bind(this)
      );
    }.bind(this)
  );
};

/**
 * Read-eval-print-loop.
 */
Shell.prototype.readEvalPrintLoop = function() {
  return this.readEvalPrint().then(
    function(value) {
      if (this.executeContext.isReadyState('READY'))
        return this.readEvalPrintLoop();

      return Promise.resolve(value);
    }.bind(this)
  ).catch(
    function(value) {
      if (this.executeContext.isReadyState('READY'))
        return this.readEvalPrintLoop();

      return Promise.reject(value);
    }.bind(this)
  );
};

Shell.prototype.parseShellInput = function(str) {
  var pos = str.indexOf(' ');
  var path, argv;
  if (pos > -1) {
    path = str.substr(0, pos);
    argv = this.parseArgv(str.substr(pos + 1).trim());
  } else {
    path = str;
    argv = null;
  }

  if (path.substr(0, 2) == './')
    path = this.executeContext.getEnv('$PWD', '/') + path.substr(2);

  return [path, argv];
};

Shell.prototype.dispatch = function(path, argv) {
  if (this.builtins.callbacks.hasOwnProperty(path)) {
    return this.builtinFS.createContext('execute', path, argv).then(
      function(cx) {
        return this.dispatchExecuteContext(cx);
      });
  }

  return this.findExecutable(path).then(
    function(abspath) {
      return this.fileSystem.createContext(
          'execute', abspath, argv).then(
         function(cx) {
           return this.dispatchExecuteContext(cx);
         }.bind(this));
    }.bind(this)
  );
};

Shell.prototype.dispatchExecuteContext = function(cx) {
  this.executeContext.setCallee(cx);
  return cx.executePromise();
};

Shell.prototype.parseArgv = function(argv) {
  if (!argv)
    return null;

  if (/[\{\[\"\']/.test(argv.substr(0, 1))) {
    // argv starts with {, [, ", or '... parse it as JSON.
    try {
      return JSON.parse(argv);
    } catch (ex) {
      throw AxiomError.Runtime('Error parsing arguments: ' + ex);
    }

  } else {
    return argv.split(/\s+/g);
  }
};

Shell.prototype.printErrorValue = function(value) {
  var args = [];
  for (var key in value.errorValue) {
    args.push(key + ': ' + JSON.stringify(value.errorValue[key]));
  }

  var str = this.tc_.output('%set-attr(FG_BOLD, FG_RED)' + value.errorName +
                            '%set-attr()');
  if (args.length)
    str += ': ' + args.join(', ');

  this.errorln(str);
};

Shell.prototype.println = function(str) {
  this.executeContext.stdout(str + '\n');
};

Shell.prototype.errorln = function(str) {
  this.executeContext.stderr(str + '\n');
};

Shell.prototype.onSignal_ = function(name) {
  console.log('Caught signal: ' + name);
  if (name == 'interrupt')
    this.readEvalPrintLoop();
};
<commit_msg>Fix dispatch of wash builtins, better error reporting<commit_after>// Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import AxiomError from 'axiom/core/error';

import JsFileSystem from 'axiom/fs/js_file_system';
import JsEntry from 'axiom/fs/js_entry';
import Path from 'axiom/fs/path';

import Termcap from 'axiom_shell/util/termcap';
import WashBuiltins from 'axiom_shell/exe/wash_builtins';
import environment from 'axiom_shell/environment';

/**
 * The shell read-eval-print loop.
 */
export var Shell = function(executeContext) {
  this.executeContext = executeContext;
  executeContext.onSignal.addListener(this.onSignal_.bind(this));

  this.fileSystem = environment.getServiceBinding('filesystems@axiom');

  if (!this.executeContext.getEnv('$PWD'))
    this.executeContext.setEnv('$PWD', '/');

  this.inputHistory = [];

  // The list of currently active jobs.
  this.executeContextList_ = [];

  // The job in the foreground (receives keyboard input).
  this.foregroundContext_ = null;

  this.tc_ = new Termcap();
  this.promptString_ = this.tc_.output(
      '%set-attr(FG_BOLD, FG_CYAN)wash$ %set-attr()');

  this.builtins = new WashBuiltins(this);
  this.builtinsFS = new JsFileSystem();
  this.builtinsFS.rootDirectory.install(this.builtins.callbacks);
};

/**
 * The main entrypoint when invoked as a JsExecutable.
 */
Shell.main = function(executeContext) {
  var shell = new Shell(executeContext);
  window.wash_ = shell;  // Console debugging aid.
  executeContext.ready();

  return shell.readEvalPrintLoop();
};

export default Shell.main;

Shell.prototype.absPath = function(path) {
  return Path.abs(this.executeContext.getEnv('$PWD', '/'), path);
};

Shell.prototype.findExecutable = function(path) {
  var searchList;

  var envPath = this.executeContext.getEnv('@PATH', []);
  if (path.substr(0, 1) == '/') {
    searchList = [path];
  } else {
    searchList = envPath.map(function(p) { return p + '/' + path });
  }

  var searchNextPath = function() {
    if (!searchList.length)
      return Promise.reject(new AxiomError.NotFound('path', path));

    var currentPath = searchList.shift();
    return this.fileSystem.stat(currentPath).then(
      function(result) {
        if (result.mode & Path.mode.x)
          return currentPath;

        return searchNextPath();
      }
    ).catch(function(value) {
      if (AxiomError.NotFound.test(value))
        return searchNextPath();

      return Promise.reject(value);
    });
  }.bind(this);

  return searchNextPath();
};

Shell.prototype.read = function() {
  return this.findExecutable('readline').then(
    function(readlinePath) {
      return this.executeContext.callPromise(
          this.fileSystem,
          readlinePath,
          { promptString: this.promptString_,
            inputHistory: this.inputHistory
          });
    }.bind(this)
  );
};

/**
 * Parse and evaluate a line of shell input.
 */
Shell.prototype.evaluate = function(value) {
  var str = value.trim();

  if (!str)
    return Promise.resolve();

  if (str != this.inputHistory[0])
    this.inputHistory.unshift(str);

  var ary = this.parseShellInput(str);
  return this.dispatch(ary[0], ary[1]);
};

Shell.prototype.readEvalPrint = function() {
  return this.read().then(
    function(result) {
      if (result == null) {
        // EOF from readline.
        return Promise.resolve(null);
      }

      if (typeof result != 'string') {
        return Promise.reject(new AxiomError.Runtime(
            'Unexpected type from readline: ' + (typeof value)));
      }

      return this.evaluate(result).then(
        function(value) {
          if (typeof value != 'undefined' && typeof value != 'number' &&
              value != null) {
            this.println(JSON.stringify(value));
          }

          return Promise.resolve(value);
        }.bind(this));
    }.bind(this)
  ).catch(
    function(error) {
      this.printErrorValue(error);
      return Promise.reject(error);
    }.bind(this)
  );
};

/**
 * Read-eval-print-loop.
 */
Shell.prototype.readEvalPrintLoop = function() {
  return this.readEvalPrint().then(
    function(value) {
      if (this.executeContext.isReadyState('READY'))
        return this.readEvalPrintLoop();

      return Promise.resolve(value);
    }.bind(this)
  ).catch(
    function(value) {
      if (this.executeContext.isReadyState('READY'))
        return this.readEvalPrintLoop();

      return Promise.reject(value);
    }.bind(this)
  );
};

Shell.prototype.parseShellInput = function(str) {
  var pos = str.indexOf(' ');
  var path, argv;
  if (pos > -1) {
    path = str.substr(0, pos);
    argv = this.parseArgv(str.substr(pos + 1).trim());
  } else {
    path = str;
    argv = null;
  }

  if (path.substr(0, 2) == './')
    path = this.executeContext.getEnv('$PWD', '/') + path.substr(2);

  return [path, argv];
};

Shell.prototype.dispatch = function(path, argv) {
  if (this.builtins.callbacks.hasOwnProperty(path)) {
    return this.builtinsFS.binding.createContext('execute', path, argv).then(
      function(cx) {
        return this.dispatchExecuteContext(cx);
      }.bind(this));
  }

  return this.findExecutable(path).then(
    function(abspath) {
      return this.fileSystem.createContext(
          'execute', abspath, argv).then(
         function(cx) {
           return this.dispatchExecuteContext(cx);
         }.bind(this));
    }.bind(this)
  );
};

Shell.prototype.dispatchExecuteContext = function(cx) {
  this.executeContext.setCallee(cx);
  return cx.executePromise();
};

Shell.prototype.parseArgv = function(argv) {
  if (!argv)
    return null;

  if (/[\{\[\"\']/.test(argv.substr(0, 1))) {
    // argv starts with {, [, ", or '... parse it as JSON.
    try {
      return JSON.parse(argv);
    } catch (ex) {
      throw AxiomError.Runtime('Error parsing arguments: ' + ex);
    }

  } else {
    return argv.split(/\s+/g);
  }
};

Shell.prototype.printErrorValue = function(value) {
  var args = [];
  if (!(value instanceof AxiomError)) {
    if (value instanceof Error) {
      console.log('printErrorValue:',value);
      value = new AxiomError.Runtime(value.message);
    } else if (value instanceof Object) {
      value = new AxiomError.Runtime(value.toString());
    } else {
      value = new AxiomError.Runtime(JSON.stringify(value));
    }
  }

  for (var key in value.errorValue) {
    args.push(key + ': ' + JSON.stringify(value.errorValue[key]));
  }

  var str = this.tc_.output('%set-attr(FG_BOLD, FG_RED)' + value.errorName +
                            '%set-attr()');
  if (args.length)
    str += ': ' + args.join(', ');

  this.errorln(str);
};

Shell.prototype.println = function(str) {
  this.executeContext.stdout(str + '\n');
};

Shell.prototype.errorln = function(str) {
  this.executeContext.stderr(str + '\n');
};

Shell.prototype.onSignal_ = function(name) {
  console.log('Caught signal: ' + name);
  if (name == 'interrupt')
    this.readEvalPrintLoop();
};
<|endoftext|>


<commit_before>/* jshint browser: true, jquery: true */
/* global Panel:false, console:false, server:false, addWidget:false */
/*
 * bannerManager.js
 *
 * Copyright (c) 2015 James G Jenner
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

function BannerManager(options) {
  this.banners = [];
  this.bannersIndex = [];
  this.server = null;
  this.displayBanner = true;

  this.bannerDialog = new BannerDialog();
};

BannerManager.prototype.preferencesChanged = function(systemPreferences) {
  var column;
  var insertBeforeBanner = false;

  if(this.dispalyBanner === systemPreferences.displayBanner) {
    return;
  }

  if(this.displayBanner && !systemPreferences.displayBanner) {
    this._removeBannersFromMain();
  }

  if(!this.displayBanner && systemPreferences.displayBanner) {
    this._removeBannersToMain();
  }

  this.displayBanner = systemPreferences.displayBanner;
};

BannerManager.prototype.getByName = function(name) {
  var banner = null;

  for(var i = 0; i < this.banners.length; i++) {
    if(this.banners[i].name === name) {
      banner = this.banners[i];
      break;
    }
  }

  return banner;
};

BannerManager.prototype.getById = function(id) {
  var banner = null;

  var idx = this.bannersIndex.indexOf(id);

  if(idx > -1) {
    banner = this.banners[idx];
  }

  return banner;
};

BannerManager.prototype.add = function(banner) {
  this._sendAdd(banner);
};

BannerManager.prototype.update = function(banner) {
  this._sendUpdate(banner);
};

BannerManager.prototype.processMessages = function(server, id, content) {
  var processed = false;

  switch (id) {
    case Banner.MESSAGE_BANNERS:
      this._receivedContent(server, content);
      processed = true;
      break;

    case Banner.MESSAGE_ADD_BANNER:
      this._receivedAdd(server, new Banner(content));
      processed = true;
      break;

    case Banner.MESSAGE_DELETE_BANNER:
      this._receivedDelete(server, content);
      processed = true;
      break;

    case Banner.MESSAGE_UPDATE_BANNER:
      this._receivedUpdate(server, new Banner(content));
      processed = true;
      break;
  }

  return processed;
};

BannerManager.prototype._receivedAdd = function(server, d) {
  this._addToGrid(d);
  displayStatusMessage("The Banner Content has been added", "Banner Contents");
};

BannerManager.prototype._receivedDelete = function(server, id) {
  this._removeFromGrid(id);
  displayStatusMessage("The Banner Content has been been removed", "Banner Contents");
};

BannerManager.prototype._receivedUpdate = function(server, banner) {
  var idx = this.bannersIndex.indexOf(banner.id);
  var currentBanner = null;

  if(idx < 0) {
    this.banners.push(banner);
    this.bannersIndex.push(banner.id);
  } else {
    currentBanner = this.banners[idx];
    this.banners[idx] = banner;

    if(currentBanner.headline !== banner.headline) {
      $('#bannerRowHeadline' + banner.id).text(banner.headline);
    }
    if(currentBanner.subHeadLine !== banner.subHeadLine) {
      $('#bannerRowSubHeadLine'  + banner.id).text(banner.subHeadLine);
    }
  }
};

BannerManager.prototype._receivedContent = function(server, d) {
  $("#bannerGrid").empty();

  this.banners = [];
  var banner = null;

  for (var item in d) {
    banner = new Banner(d[item]);
    this.banners.push(banner);
    this.bannersIndex.push(banner.id);
    this._addToGrid(banner);
  }
};

BannerManager.prototype._sendAdd = function(banner) {
  if(this.server !== null) {
    this.server.sendMessage(Banner.MESSAGE_ADD_BANNER, banner);
  }
};

BannerManager.prototype._sendDelete = function(id) {
  if(this.server !== null) {
    this.server.sendMessage(Banner.MESSAGE_DELETE_BANNER, id);
  }
};

BannerManager.prototype._sendUpdate = function(banner) {
  if(this.server !== null) {
    this.server.sendMessage(Banner.MESSAGE_UPDATE_BANNER, banner);
  }
};

BannerManager.prototype._addToGrid = function(banner) {
  $("#bannerGrid").append(
    '
' +
    '  ' + banner.headline + '	' +
    '  ' + banner.subHeadline + '	' +
    '  ' +
    '
' +
    '      <button id="bannerBtnDelete' + banner.id + '" type="button" class="grid-row-buttons btn btn-default ">' +
    '        ' +
    '      </button>' +
    '
' +
    '  	' +
    '
'
  );

  $('#banner' + banner.id).hide().fadeIn('fast');

  $('#bannerPane .rowlink').rowlink();
  $('#bannerRow' + banner.id).on('click', function() {
    this.bannerDialog.display(BannerDialog.UPDATE, banner);
    return false;
  }.bind(this));

  $('#bannerBtnDelete' + banner.id).on('click', function() {
    this._sendDelete(banner.id);
    return false;
  }.bind(this));
};

BannerManager.prototype._removeFromGrid = function(id) {
  $('#bannerRow' + id)
    .animate({height: 0, padding: 0, margin: 0}, {easing: "linear", duration: "slow", queue: false})
    .fadeOut('slow', function() {
    this.remove();
  });
};

BannerManager.prototype.setup = function() {
  $(".bannerRow").on('click', function() {
    return false;
  });

  $("#addBanner").on('click', function() {
    this.bannerDialog.display(BannerDialog.ADD);
    return false;
  }.bind(this));
};

BannerManager.prototype._removeBannersFromMain = function() {
};

BannerManager.prototype._removeBannersToMain = function() {
};
<commit_msg>Implemented setting the carousel for the banner based on banner data<commit_after>/* jshint browser: true, jquery: true */
/* global Panel:false, console:false, server:false, addWidget:false */
/*
 * bannerManager.js
 *
 * Copyright (c) 2015 James G Jenner
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

function BannerManager(options) {
  this.banners = [];
  this.bannersIndex = [];
  this.server = null;
  this.displayBanner = true;

  this.bannerDialog = new BannerDialog();
}

BannerManager.prototype.preferencesChanged = function(systemPreferences) {
  var column;
  var insertBeforeBanner = false;

  if(this.dispalyBanner === systemPreferences.displayBanner) {
    return;
  }

  if(this.displayBanner && !systemPreferences.displayBanner) {
    this._removeBannersFromMain();
  }

  if(!this.displayBanner && systemPreferences.displayBanner) {
    this._removeBannersToMain();
  }

  this.displayBanner = systemPreferences.displayBanner;
};

BannerManager.prototype.getByName = function(name) {
  var banner = null;

  for(var i = 0; i < this.banners.length; i++) {
    if(this.banners[i].name === name) {
      banner = this.banners[i];
      break;
    }
  }

  return banner;
};

BannerManager.prototype.getById = function(id) {
  var banner = null;

  var idx = this.bannersIndex.indexOf(id);

  if(idx > -1) {
    banner = this.banners[idx];
  }

  return banner;
};

BannerManager.prototype.add = function(banner) {
  this._sendAdd(banner);
};

BannerManager.prototype.update = function(banner) {
  this._sendUpdate(banner);
};

BannerManager.prototype.processMessages = function(server, id, content) {
  var processed = false;

  switch (id) {
    case Banner.MESSAGE_BANNERS:
      this._receivedContent(server, content);
      processed = true;
      break;

    case Banner.MESSAGE_ADD_BANNER:
      this._receivedAdd(server, new Banner(content));
      processed = true;
      break;

    case Banner.MESSAGE_DELETE_BANNER:
      this._receivedDelete(server, content);
      processed = true;
      break;

    case Banner.MESSAGE_UPDATE_BANNER:
      this._receivedUpdate(server, new Banner(content));
      processed = true;
      break;
  }

  return processed;
};

BannerManager.prototype._receivedAdd = function(server, d) {
  this.banners.push(d);
  this.bannersIndex.push(d.id);
  this._addToGrid(d);
  this._addToCarousel(d);
  this._initialiseCarousel();
  displayStatusMessage("The Banner Content has been added", "Banner Contents");
};

BannerManager.prototype._receivedDelete = function(server, id) {
  this._removeFromGrid(id);
  this._removeFromCarousel(id);
  this._initialiseCarousel();

  var idx = this.bannersIndex.indexOf(id);
  if(idx > -1) {
    this.bannersIndex.splice(idx, 1);
    this.banners.splice(idx, 1);
  }

  displayStatusMessage("The Banner Content has been been removed", "Banner Contents");
};

BannerManager.prototype._receivedUpdate = function(server, banner) {
  var idx = this.bannersIndex.indexOf(banner.id);

  if(idx < 0) {
    this.banners.push(banner);
    this.bannersIndex.push(banner.id);
    this._addToGrid(banner);
    this._addToCarousel(banner);
  } else {
    this._updateGrid(this.banners[idx], banner);
    this._updateCarousel(this.banners[idx], banner);

    this.banners[idx] = banner;
  }

  this._initialiseCarousel();
  displayStatusMessage("The Banner Content has been updated", "Banner Contents");
};

BannerManager.prototype._receivedContent = function(server, d) {
  $("#bannerGrid").empty();

  this.banners = [];
  var banner = null;

  for (var item in d) {
    banner = new Banner(d[item]);
    this.banners.push(banner);
    this.bannersIndex.push(banner.id);
    this._addToGrid(banner);
    this._addToCarousel(banner);
  }
  this._initialiseCarousel();
  displayStatusMessage("The Banner Content has been initialised", "Banner Contents");
};

BannerManager.prototype._sendAdd = function(banner) {
  if(this.server !== null) {
    this.server.sendMessage(Banner.MESSAGE_ADD_BANNER, banner);
  }
};

BannerManager.prototype._sendDelete = function(id) {
  if(this.server !== null) {
    this.server.sendMessage(Banner.MESSAGE_DELETE_BANNER, id);
  }
};

BannerManager.prototype._sendUpdate = function(banner) {
  if(this.server !== null) {
    this.server.sendMessage(Banner.MESSAGE_UPDATE_BANNER, banner);
  }
};

BannerManager.prototype._addToGrid = function(banner) {
  $("#bannerGrid").append(
    '
' +
    '  ' + banner.headline + '	' +
    '  ' + banner.subHeadline + '	' +
    '  ' +
    '
' +
    '      <button id="bannerBtnDelete' + banner.id + '" type="button" class="grid-row-buttons btn btn-default ">' +
    '        ' +
    '      </button>' +
    '
' +
    '  	' +
    '
'
  );

  $('#banner' + banner.id).hide().fadeIn('fast');

  $('#bannerPane .rowlink').rowlink();
  $('#bannerRow' + banner.id).on('click', function() {
    this.bannerDialog.display(BannerDialog.UPDATE, banner);
    return false;
  }.bind(this));

  $('#bannerBtnDelete' + banner.id).on('click', function() {
    this._sendDelete(banner.id);
    return false;
  }.bind(this));
};

BannerManager.prototype._removeFromGrid = function(id) {
  $('#bannerRow' + id)
    .animate({height: 0, padding: 0, margin: 0}, {easing: "linear", duration: "slow", queue: false})
    .fadeOut('slow', function() {
    this.remove();
  });
};

BannerManager.prototype._updateGrid = function(currentBanner, newBanner) {
  if(currentBanner.headline !== newBanner.headline) {
    $('#bannerRowHeadline' + newBanner.id).text(newBanner.headline);
  }
  if(currentBanner.subHeadline !== newBanner.subHeadline) {
    $('#bannerRowSubHeadLine'  + newBanner.id).text(newBanner.subHeadline);
  }
};

BannerManager.prototype._addToCarousel = function(banner) {
  $('#bannerCarouselContent').append(
    '
' +
    '
' +
    '    ' +
    '      ' + banner.headline +
    '    ' +
    '
' +
    '      ' +
    '        ' + banner.subHeadline +
    '      ' +
    '      ' +
    '        ' + banner.reference +
    '      ' +
    '
' +
    '
' +
    '
'
  );
};

BannerManager.prototype._removeFromCarousel = function(id) {
  $('#bannerCarouselItem' + id).remove();
};

BannerManager.prototype._updateCarousel = function(currentBanner, newBanner) {
  if(currentBanner.headline !== newBanner.headline) {
    $('#bannerCarouselItem' + newBanner.id + 'Headline').text(newBanner.headline);
  }
  if(currentBanner.headlineColor !== newBanner.headlineColor) {
    $('#bannerCarouselItem' + newBanner.id + 'Headline').css("color", newBanner.headlineColor);
  }

  if(currentBanner.subHeadline !== newBanner.subHeadline) {
    $('#bannerCarouselItem'  + newBanner.id + 'SubHeadline').text(newBanner.subHeadline);
  }
  if(currentBanner.subHeadlineColor !== newBanner.subHeadlineColor) {
    $('#bannerCarouselItem' + newBanner.id + 'SubHeadline').css("color", newBanner.subHeadlineColor);
  }

  if(currentBanner.reference !== newBanner.reference) {
    $('#bannerCarouselItem'  + newBanner.id + 'Reference').text(newBanner.reference);
  }
  if(currentBanner.referenceColor !== newBanner.referenceColor) {
    $('#bannerCarouselItem' + newBanner.id + 'Reference').css("color", newBanner.referenceColor);
  }
};

BannerManager.prototype._initialiseCarousel = function() {
  // TODO: figure out why there is a delay before first is selected, is it a html rendering issue?
  $('#bannerCarouselContent').carousel();
};

BannerManager.prototype.setup = function() {
  $(".bannerRow").on('click', function() {
    return false;
  });

  $("#addBanner").on('click', function() {
    this.bannerDialog.display(BannerDialog.ADD);
    return false;
  }.bind(this));
};

BannerManager.prototype._removeBannersFromMain = function() {
};

BannerManager.prototype._removeBannersToMain = function() {
};
<|endoftext|>


<commit_before>module.exports = function(correlationId, resStatus, response) {
  var messageCorrelation;

  correlationId = correlationId.toString();
  resStatus = resStatus.toString();

  try {
    response = JSON.parse(response.toString());
  } catch(err) {
    console.log("Error parsing JSON", err);
    // reject promise?
    return;
  }

  messageCorrelation = this.correlations[correlationId];

  if(!messageCorrelation) {
    console.log("Couldn't find correlationID", correlationId);
    return;
  }

  if(resStatus === 'SUCCESS') {
    messageCorrelation.promise.resolve(response);
  } else {
    messageCorrelation.promise.reject(resStatus, response);
  }

  // message has been handled, remove rejection timeout
  clearTimeout(messageCorrelation.rejectTimeout);

  delete this.correlations[correlationId];
};
<commit_msg>Only attempt to parse JSON if response exists<commit_after>module.exports = function(correlationId, resStatus, response) {
  var messageCorrelation;

  correlationId = correlationId.toString();
  resStatus = resStatus.toString();

  try {
    if(response) {
      response = JSON.parse(response.toString());
    } else {
      response = {};
    }
  } catch(err) {
    console.log("Error parsing JSON", err);
  }

  messageCorrelation = this.correlations[correlationId];

  if(!messageCorrelation) {
    console.log("Couldn't find correlationID", correlationId);
    return;
  }

  if(resStatus === 'SUCCESS') {
    messageCorrelation.promise.resolve(response);
  } else {
    messageCorrelation.promise.reject(resStatus, response);
  }

  // message has been handled, remove rejection timeout
  clearTimeout(messageCorrelation.rejectTimeout);

  delete this.correlations[correlationId];
};
<|endoftext|>


<commit_before>/** @babel */

import SelectListView from 'atom-select-list'
import {humanizeKeystroke} from 'underscore-plus'
import fuzzaldrin from 'fuzzaldrin'
import fuzzaldrinPlus from 'fuzzaldrin-plus'

export default class CommandPaletteView {
  constructor (initiallyVisibleItemCount = 10) {
    this.keyBindingsForActiveElement = []
    this.elementCache = new WeakMap()
    this.selectListView = new SelectListView({
      initiallyVisibleItemCount: initiallyVisibleItemCount, // just for being able to disable visible-on-render in spec
      items: [],
      filter: this.filter,
      emptyMessage: 'No matches found',
      elementForItem: (item, {index, selected, visible}) => {
        if (!visible) {
          return document.createElement("li")
        }
        const query = this.selectListView.getQuery()
        const queryKey = `${query}:${selected}`
        if(this.elementCache.has(item)) {
          if(this.elementCache.get(item).has(queryKey)) {
            return this.elementCache.get(item).get(queryKey)
          }
        } else {
          this.elementCache.set(item, new Map())
        }

        const li = document.createElement('li')
        li.classList.add('event', 'two-lines')
        li.dataset.eventName = item.name

        const rightBlock = document.createElement('div')
        rightBlock.classList.add('pull-right')
        let i = 0
        for (const keyBinding of this.keyBindingsForActiveElement) {
          if (keyBinding.command === item.name) {
            const kbd = document.createElement('kbd')
            kbd.classList.add('key-binding')
            kbd.textContent = humanizeKeystroke(keyBinding.keystrokes)
            rightBlock.appendChild(kbd)
          }
        }
        li.appendChild(rightBlock)

        const leftBlock = document.createElement('div')
        const titleEl = document.createElement('div')
        titleEl.classList.add('primary-line')
        titleEl.title = item.name
        leftBlock.appendChild(titleEl)

        this.highlightMatchesInElement(item.displayName, query, titleEl)

        if (selected) {
          let secondaryEl = document.createElement('div')
          secondaryEl.classList.add('secondary-line')
          secondaryEl.style.display = 'flex'

          if (typeof item.description === 'string') {
            secondaryEl.appendChild(this.createDescription(item.description, query))
          }

          if (Array.isArray(item.tags)) {
            const matchingTags = item.tags
              .map(t => [t, this.fuzz.score(t, query)])
              .filter(([t, s]) => s > 0)
              .sort((a, b) => a.s - b.s)
              .map(([t, s]) => t)

            if (matchingTags.length > 0) {
              secondaryEl.appendChild(this.createTags(matchingTags, query))
            }
          }

          leftBlock.appendChild(secondaryEl)
        }

        li.appendChild(leftBlock)
        this.elementCache.get(item).set(queryKey, li)
        return li
      },
      didConfirmSelection: (keyBinding) => {
        this.hide()
        const event = new CustomEvent(keyBinding.name, {bubbles: true, cancelable: true})
        this.activeElement.dispatchEvent(event)
      },
      didCancelSelection: () => {
        this.hide()
      }
    })
    this.selectListView.element.classList.add('command-palette')
  }

  async destroy () {
    await this.selectListView.destroy()
  }

  toggle () {
    if (this.panel && this.panel.isVisible()) {
      this.hide()
      return Promise.resolve()
    } else {
      return this.show()
    }
  }

  async show (showHiddenCommands = false) {
    if (!this.panel) {
      this.panel = atom.workspace.addModalPanel({item: this.selectListView})
    }

    if (!this.preserveLastSearch) {
      this.selectListView.reset()
    } else {
      this.selectListView.refs.queryEditor.selectAll()
    }

    this.activeElement = (document.activeElement === document.body) ? atom.views.getView(atom.workspace) : document.activeElement
    this.keyBindingsForActiveElement = atom.keymaps.findKeyBindings({target: this.activeElement})
    const commandsForActiveElement = atom.commands
        .findCommands({target: this.activeElement})
        .filter(command => showHiddenCommands === !!command.hiddenInCommandPalette)
    commandsForActiveElement.sort((a, b) => a.displayName.localeCompare(b.displayName))
    await this.selectListView.update({items: commandsForActiveElement})

    this.previouslyFocusedElement = document.activeElement
    this.panel.show()
    this.selectListView.focus()
  }

  hide () {
    this.panel.hide()
    if (this.previouslyFocusedElement) {
      this.previouslyFocusedElement.focus()
      this.previouslyFocusedElement = null
    }
  }

  async update (props) {
    if (props.hasOwnProperty('preserveLastSearch')) {
      this.preserveLastSearch = props.preserveLastSearch
    }

    if (props.hasOwnProperty('useAlternateScoring')) {
      this.useAlternateScoring = props.useAlternateScoring
    }
  }

  get fuzz () {
    return this.useAlternateScoring ? fuzzaldrinPlus : fuzzaldrin
  }

  highlightMatchesInElement (text, query, el) {
    const matches = this.fuzz.match(text, query)
    let matchedChars = []
    let lastIndex = 0
    for (const matchIndex of matches) {
      const unmatched = text.substring(lastIndex, matchIndex)
      if (unmatched) {
        if (matchedChars.length > 0) {
          const matchSpan = document.createElement('span')
          matchSpan.classList.add('character-match')
          matchSpan.textContent = matchedChars.join('')
          el.appendChild(matchSpan)
          matchedChars = []
        }

        el.appendChild(document.createTextNode(unmatched))
      }

      matchedChars.push(text[matchIndex])
      lastIndex = matchIndex + 1
    }

    if (matchedChars.length > 0) {
      const matchSpan = document.createElement('span')
      matchSpan.classList.add('character-match')
      matchSpan.textContent = matchedChars.join('')
      el.appendChild(matchSpan)
    }

    const unmatched = text.substring(lastIndex)
    if (unmatched) {
      el.appendChild(document.createTextNode(unmatched))
    }
  }

  filter = (items, query) => {
    if (query.length === 0) {
      return items
    }

    const scoredItems = []
    for (const item of items) {
      let score = this.fuzz.score(item.displayName, query)
      if (item.tags) {
        score += item.tags.reduce(
          (currentScore, tag) => currentScore + this.fuzz.score(tag, query),
          0
        )
      }
      if (item.description) {
        score += this.fuzz.score(item.description, query)
      }

      if (score > 0) {
        scoredItems.push({item, score})
      }
    }
    scoredItems.sort((a, b) => b.score - a.score)
    return scoredItems.map((i) => i.item)
  }

  createDescription (description, query) {
    const descriptionEl = document.createElement('div')

    // in case of overflow, give full contents on long hover
    descriptionEl.title = description

    Object.assign(descriptionEl.style, {
      flexGrow: 1,
      flexShrink: 1,
      textOverflow: 'ellipsis',
      whiteSpace: 'nowrap',
      overflow: 'hidden'
    })
    this.highlightMatchesInElement(description, query, descriptionEl)
    return descriptionEl
  }

  createTag (tagText, query) {
    const tagEl = document.createElement('li')
    Object.assign(tagEl.style, {
      borderBottom: 0,
      display: 'inline',
      padding: 0
    })
    this.highlightMatchesInElement(tagText, query, tagEl)
    return tagEl
  }

  createTags (matchingTags, query) {
    const tagsEl = document.createElement('ol')
    Object.assign(tagsEl.style, {
      display: 'inline',
      marginLeft: '4px',
      flexShrink: 0,
      padding: 0
    })

    const introEl = document.createElement('strong')
    introEl.textContent = 'matching tags: '

    tagsEl.appendChild(introEl)
    matchingTags.map(t => this.createTag(t, query)).forEach((tagEl, i) => {
      tagsEl.appendChild(tagEl)
      if (i < matchingTags.length - 1) {
        const commaSpace = document.createElement('span')
        commaSpace.textContent = ', '
        tagsEl.appendChild(commaSpace)
      }
    })
    return tagsEl
  }
}
<commit_msg>cache humanized keystrokes<commit_after>/** @babel */

import SelectListView from 'atom-select-list'
import {humanizeKeystroke} from 'underscore-plus'
import fuzzaldrin from 'fuzzaldrin'
import fuzzaldrinPlus from 'fuzzaldrin-plus'

export default class CommandPaletteView {
  constructor (initiallyVisibleItemCount = 10) {
    this.keyBindingsForActiveElement = []
    this.elementCache = new WeakMap()
    this.selectListView = new SelectListView({
      initiallyVisibleItemCount: initiallyVisibleItemCount, // just for being able to disable visible-on-render in spec
      items: [],
      filter: this.filter,
      emptyMessage: 'No matches found',
      elementForItem: (item, {index, selected, visible}) => {
        if (!visible) {
          return document.createElement("li")
        }
        const query = this.selectListView.getQuery()
        const queryKey = `${query}:${selected}`
        if(this.elementCache.has(item)) {
          if(this.elementCache.get(item).has(queryKey)) {
            return this.elementCache.get(item).get(queryKey)
          }
        } else {
          this.elementCache.set(item, new Map())
        }

        const li = document.createElement('li')
        li.classList.add('event', 'two-lines')
        li.dataset.eventName = item.name

        const rightBlock = document.createElement('div')
        rightBlock.classList.add('pull-right')

        this.keyBindingsForActiveElement
        .filter(({command}) => command === item.name)
        .forEach(keyBinding => {
          const kbd = document.createElement('kbd')
          kbd.classList.add('key-binding')
          if(!this.elementCache.get(item).has(keyBinding.keystrokes)) {
            this.elementCache.get(item).set(keyBinding.keystrokes, humanizeKeystroke(keyBinding.keystrokes))
          }
          kbd.textContent = this.elementCache.get(item).get(keyBinding.keystrokes)
          rightBlock.appendChild(kbd)
        })
        li.appendChild(rightBlock)

        const leftBlock = document.createElement('div')
        const titleEl = document.createElement('div')
        titleEl.classList.add('primary-line')
        titleEl.title = item.name
        leftBlock.appendChild(titleEl)

        this.highlightMatchesInElement(item.displayName, query, titleEl)

        if (selected) {
          let secondaryEl = document.createElement('div')
          secondaryEl.classList.add('secondary-line')
          secondaryEl.style.display = 'flex'

          if (typeof item.description === 'string') {
            secondaryEl.appendChild(this.createDescription(item.description, query))
          }

          if (Array.isArray(item.tags)) {
            const matchingTags = item.tags
              .map(t => [t, this.fuzz.score(t, query)])
              .filter(([t, s]) => s > 0)
              .sort((a, b) => a.s - b.s)
              .map(([t, s]) => t)

            if (matchingTags.length > 0) {
              secondaryEl.appendChild(this.createTags(matchingTags, query))
            }
          }

          leftBlock.appendChild(secondaryEl)
        }

        li.appendChild(leftBlock)
        this.elementCache.get(item).set(queryKey, li)
        return li
      },
      didConfirmSelection: (keyBinding) => {
        this.hide()
        const event = new CustomEvent(keyBinding.name, {bubbles: true, cancelable: true})
        this.activeElement.dispatchEvent(event)
      },
      didCancelSelection: () => {
        this.hide()
      }
    })
    this.selectListView.element.classList.add('command-palette')
  }

  async destroy () {
    await this.selectListView.destroy()
  }

  toggle () {
    if (this.panel && this.panel.isVisible()) {
      this.hide()
      return Promise.resolve()
    } else {
      return this.show()
    }
  }

  async show (showHiddenCommands = false) {
    if (!this.panel) {
      this.panel = atom.workspace.addModalPanel({item: this.selectListView})
    }

    if (!this.preserveLastSearch) {
      this.selectListView.reset()
    } else {
      this.selectListView.refs.queryEditor.selectAll()
    }

    this.activeElement = (document.activeElement === document.body) ? atom.views.getView(atom.workspace) : document.activeElement
    this.keyBindingsForActiveElement = atom.keymaps.findKeyBindings({target: this.activeElement})
    const commandsForActiveElement = atom.commands
        .findCommands({target: this.activeElement})
        .filter(command => showHiddenCommands === !!command.hiddenInCommandPalette)
    commandsForActiveElement.sort((a, b) => a.displayName.localeCompare(b.displayName))
    await this.selectListView.update({items: commandsForActiveElement})

    this.previouslyFocusedElement = document.activeElement
    this.panel.show()
    this.selectListView.focus()
  }

  hide () {
    this.panel.hide()
    if (this.previouslyFocusedElement) {
      this.previouslyFocusedElement.focus()
      this.previouslyFocusedElement = null
    }
  }

  async update (props) {
    if (props.hasOwnProperty('preserveLastSearch')) {
      this.preserveLastSearch = props.preserveLastSearch
    }

    if (props.hasOwnProperty('useAlternateScoring')) {
      this.useAlternateScoring = props.useAlternateScoring
    }
  }

  get fuzz () {
    return this.useAlternateScoring ? fuzzaldrinPlus : fuzzaldrin
  }

  highlightMatchesInElement (text, query, el) {
    const matches = this.fuzz.match(text, query)
    let matchedChars = []
    let lastIndex = 0
    for (const matchIndex of matches) {
      const unmatched = text.substring(lastIndex, matchIndex)
      if (unmatched) {
        if (matchedChars.length > 0) {
          const matchSpan = document.createElement('span')
          matchSpan.classList.add('character-match')
          matchSpan.textContent = matchedChars.join('')
          el.appendChild(matchSpan)
          matchedChars = []
        }

        el.appendChild(document.createTextNode(unmatched))
      }

      matchedChars.push(text[matchIndex])
      lastIndex = matchIndex + 1
    }

    if (matchedChars.length > 0) {
      const matchSpan = document.createElement('span')
      matchSpan.classList.add('character-match')
      matchSpan.textContent = matchedChars.join('')
      el.appendChild(matchSpan)
    }

    const unmatched = text.substring(lastIndex)
    if (unmatched) {
      el.appendChild(document.createTextNode(unmatched))
    }
  }

  filter = (items, query) => {
    if (query.length === 0) {
      return items
    }

    const scoredItems = []
    for (const item of items) {
      let score = this.fuzz.score(item.displayName, query)
      if (item.tags) {
        score += item.tags.reduce(
          (currentScore, tag) => currentScore + this.fuzz.score(tag, query),
          0
        )
      }
      if (item.description) {
        score += this.fuzz.score(item.description, query)
      }

      if (score > 0) {
        scoredItems.push({item, score})
      }
    }
    scoredItems.sort((a, b) => b.score - a.score)
    return scoredItems.map((i) => i.item)
  }

  createDescription (description, query) {
    const descriptionEl = document.createElement('div')

    // in case of overflow, give full contents on long hover
    descriptionEl.title = description

    Object.assign(descriptionEl.style, {
      flexGrow: 1,
      flexShrink: 1,
      textOverflow: 'ellipsis',
      whiteSpace: 'nowrap',
      overflow: 'hidden'
    })
    this.highlightMatchesInElement(description, query, descriptionEl)
    return descriptionEl
  }

  createTag (tagText, query) {
    const tagEl = document.createElement('li')
    Object.assign(tagEl.style, {
      borderBottom: 0,
      display: 'inline',
      padding: 0
    })
    this.highlightMatchesInElement(tagText, query, tagEl)
    return tagEl
  }

  createTags (matchingTags, query) {
    const tagsEl = document.createElement('ol')
    Object.assign(tagsEl.style, {
      display: 'inline',
      marginLeft: '4px',
      flexShrink: 0,
      padding: 0
    })

    const introEl = document.createElement('strong')
    introEl.textContent = 'matching tags: '

    tagsEl.appendChild(introEl)
    matchingTags.map(t => this.createTag(t, query)).forEach((tagEl, i) => {
      tagsEl.appendChild(tagEl)
      if (i < matchingTags.length - 1) {
        const commaSpace = document.createElement('span')
        commaSpace.textContent = ', '
        tagsEl.appendChild(commaSpace)
      }
    })
    return tagsEl
  }
}
<|endoftext|>