<commit_before>
'use strict';

const os = require('os');
const https = require('https');
const proc = require('child_process');
const StateController = require('../../lib/state-controller');
const OSXSupport = require('../../lib/support/osx');

const {
  fakeProcesses, fakeKiteInstallPaths, fakeResponse,
  withKiteInstalled, withKiteRunning, withKiteNotRunning,
  withFakeServer, withKiteEnterpriseInstalled, withBothKiteInstalled,
  withKiteEnterpriseRunning, withKiteEnterpriseNotRunning,
} = require('../spec-helpers.js');

describe('StateController - OSX Support', () => {
  beforeEach(() => {
    spyOn(os, 'platform').andReturn('darwin');
    spyOn(os, 'release').andReturn('14.0.0');
  });

  fakeKiteInstallPaths();

  describe('.isKiteSupported()', () => {
    it('returns a resolved promise', () => {
      waitsForPromise(() => StateController.isKiteSupported());
    });

    describe('when the os release is below 10.10', () => {
      beforeEach(() => {
        os.release.andReturn('13.0.0');
      });

      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.isKiteSupported());
      });
    });
  });

  describe('.isKiteInstalled()', () => {
    withKiteInstalled(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.isKiteInstalled());
      });
    });

    describe('when kite is not installed', () => {
      it('returns a rejected promise', () => {
        waitsForPromise({
          shouldReject: true,
        }, () => StateController.isKiteInstalled());
      });
    });

    withKiteEnterpriseInstalled(() => {
      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () =>
          StateController.isKiteInstalled());
      });
    });
  });

  describe('.isKiteEnterpriseInstalled()', () => {
    describe('when kite enterprise is not installed', () => {
      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () =>
          StateController.isKiteEnterpriseInstalled());
      });
    });

    withKiteInstalled(() => {
      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () =>
          StateController.isKiteEnterpriseInstalled());
      });
    });

    withKiteEnterpriseInstalled(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.isKiteEnterpriseInstalled());
      });
    });
  });

  describe('.hasBothKiteInstalled()', () => {
    describe('when no kite is installed', () => {
      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () =>
        StateController.hasBothKiteInstalled());
      });
    });

    describe('when kite enterprise is not installed', () => {
      withKiteInstalled(() => {
        it('returns a rejected promise', () => {
          waitsForPromise({shouldReject: true}, () =>
          StateController.hasBothKiteInstalled());
        });
      });
    });

    describe('when kite is not installed', () => {
      withKiteEnterpriseInstalled(() => {
        it('returns a rejected promise', () => {
          waitsForPromise({shouldReject: true}, () =>
          StateController.hasBothKiteInstalled());
        });
      });
    });

    withBothKiteInstalled(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.hasBothKiteInstalled());
      });
    });
  });

  describe('.installKite()', () => {
    describe('when every command succeeds', () => {
      beforeEach(() => {
        fakeProcesses({
          hdiutil: () => 0,
          cp: () => 0,
          rm: () => 0,
          mdfind: (ps, args) => {
            const [, key] = args[0].split(/\s=\s/);
            key === '"com.kite.Kite"'
              ? ps.stdout('/Applications/Kite.app')
              : ps.stdout('');
            return 0;
          },
        });
      });

      it('returns a resolved promise', () => {
        const options = {
          onInstallStart: jasmine.createSpy(),
          onMount: jasmine.createSpy(),
          onCopy: jasmine.createSpy(),
          onUnmount: jasmine.createSpy(),
          onRemove: jasmine.createSpy(),
        };

        waitsForPromise(() => StateController.installKite(options));
        runs(() => {
          expect(proc.spawn).toHaveBeenCalledWith('hdiutil', [
            'attach', '-nobrowse',
            OSXSupport.KITE_DMG_PATH,
          ]);
          expect(proc.spawn).toHaveBeenCalledWith('cp', [
            '-r',
            OSXSupport.KITE_APP_PATH.mounted,
            OSXSupport.APPS_PATH,
          ]);
          expect(proc.spawn).toHaveBeenCalledWith('hdiutil', [
            'detach',
            OSXSupport.KITE_VOLUME_PATH,
          ]);
          expect(proc.spawn).toHaveBeenCalledWith('rm', [
            OSXSupport.KITE_DMG_PATH,
          ]);
          expect(proc.spawn).toHaveBeenCalledWith('mdfind', [
          ]);

          expect(options.onInstallStart).toHaveBeenCalled();
          expect(options.onMount).toHaveBeenCalled();
          expect(options.onCopy).toHaveBeenCalled();
          expect(options.onUnmount).toHaveBeenCalled();
          expect(options.onRemove).toHaveBeenCalled();
        });
      });
    });

    describe('when mounting the archive fails', () => {
      beforeEach(() => {
        fakeProcesses({
          hdiutil: () => 1,
          cp: () => 0,
          rm: () => 0,
        });
      });

      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.installKite());
      });
    });

    describe('when copying the archive content fails', () => {
      beforeEach(() => {
        fakeProcesses({
          hdiutil: () => 0,
          cp: () => 1,
          rm: () => 0,
        });
      });

      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.installKite());
      });
    });

    describe('when unmounting the archive fails', () => {
      beforeEach(() => {
        fakeProcesses({
          hdiutil: (ps, [command]) => command === 'attach' ? 0 : 1,
          cp: () => 0,
          rm: () => 0,
        });
      });

      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.installKite());
      });
    });

    describe('when removing the downloaded archive fails', () => {
      beforeEach(() => {
        fakeProcesses({
          hdiutil: () => 0,
          cp: () => 0,
          rm: () => 1,
        });
      });

      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.installKite());
      });
    });
  });

  describe('.downloadKite()', () => {
    withFakeServer([
      [
        o => /^http:\/\/kite\.com/.test(o),
        o => fakeResponse(303, '', {headers: {location: 'https://download.kite.com'}}),
      ], [
        o => /^https:\/\/download\.kite\.com/.test(o),
        o => fakeResponse(200, 'foo'),
      ],
    ], () => {
      describe('when the download succeeds', () => {
        beforeEach(() => {
          fakeProcesses({
            hdiutil: () => 0,
            cp: () => 0,
            rm: () => 0,
          });
        });

        describe('with the install option', () => {
          it('returns a promise resolved after the install', () => {
            const options = {
              install: true,
              onDownload: jasmine.createSpy(),
              onInstallStart: jasmine.createSpy(),
              onMount: jasmine.createSpy(),
              onCopy: jasmine.createSpy(),
              onUnmount: jasmine.createSpy(),
              onRemove: jasmine.createSpy(),
            };
            const url = 'http://kite.com/download';

            waitsForPromise(() => StateController.downloadKite(url, options));
            runs(() => {
              expect(https.request).toHaveBeenCalledWith('http://kite.com/download');
              expect(proc.spawn).toHaveBeenCalledWith('hdiutil', [
                'attach', '-nobrowse',
                OSXSupport.KITE_DMG_PATH,
              ]);
              expect(proc.spawn).toHaveBeenCalledWith('cp', [
                '-r',
                OSXSupport.KITE_APP_PATH.mounted,
                OSXSupport.APPS_PATH,
              ]);
              expect(proc.spawn).toHaveBeenCalledWith('hdiutil', [
                'detach',
                OSXSupport.KITE_VOLUME_PATH,
              ]);
              expect(proc.spawn).toHaveBeenCalledWith('rm', [
                OSXSupport.KITE_DMG_PATH,
              ]);

              expect(options.onDownload).toHaveBeenCalled();
              expect(options.onInstallStart).toHaveBeenCalled();
              expect(options.onMount).toHaveBeenCalled();
              expect(options.onCopy).toHaveBeenCalled();
              expect(options.onUnmount).toHaveBeenCalled();
              expect(options.onRemove).toHaveBeenCalled();
            });
          });
        });
      });
    });
  });

  describe('.isKiteRunning()', () => {
    describe('when kite is not installed', () => {
      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.isKiteRunning());
      });
    });

    withKiteInstalled(() => {
      describe('but not running', () => {
        beforeEach(() => {
          fakeProcesses({
            '/bin/ps': (ps) => {
              ps.stdout('');
              return 0;
            },
          });
        });

        it('returns a rejected promise', () => {
          waitsForPromise({shouldReject: true}, () => StateController.isKiteRunning());
        });
      });

      withKiteRunning(() => {
        it('returns a resolved promise', () => {
          waitsForPromise(() => StateController.isKiteRunning());
        });
      });
    });
  });

  describe('.canRunKite()', () => {
    describe('when kite is not installed', () => {
      it('returns a rejected function', () => {
        waitsForPromise({shouldReject: true}, () => StateController.canRunKite());
      });
    });

    withKiteNotRunning(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.canRunKite());
      });
    });

    withKiteRunning(() => {
      it('returns a rejected function', () => {
        waitsForPromise({shouldReject: true}, () => StateController.canRunKite());
      });
    });
  });

  describe('.runKite()', () => {
    describe('when kite is not installed', () => {
      it('returns a rejected function', () => {
        waitsForPromise({shouldReject: true}, () => StateController.runKite());
      });
    });

    withKiteRunning(() => {
      it('returns a resolved function', () => {
        waitsForPromise(() => StateController.runKite());
      });
    });

    withKiteNotRunning(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.runKite());
        runs(() => {
          expect(proc.spawn).toHaveBeenCalledWith('defaults', [
            'write', 'com.kite.Kite', 'shouldReopenSidebar', '0',
          ]);

          expect(proc.spawn).toHaveBeenCalledWith('open', [
            '-a', OSXSupport.installPath,
          ]);
        });
      });
    });
  });

  describe('.isKiteEnterpriseRunning()', () => {
    describe('when kite is not installed', () => {
      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.isKiteEnterpriseRunning());
      });
    });

    withKiteEnterpriseInstalled(() => {
      describe('but not running', () => {
        beforeEach(() => {
          fakeProcesses({
            '/bin/ps': (ps) => {
              ps.stdout('');
              return 0;
            },
          });
        });

        it('returns a rejected promise', () => {
          waitsForPromise({shouldReject: true}, () => StateController.isKiteEnterpriseRunning());
        });
      });

      withKiteEnterpriseRunning(() => {
        it('returns a resolved promise', () => {
          waitsForPromise(() => StateController.isKiteEnterpriseRunning());
        });
      });
    });
  });

  describe('.canRunKiteEnterprise()', () => {
    describe('when kite is not installed', () => {
      it('returns a rejected function', () => {
        waitsForPromise({shouldReject: true}, () => StateController.canRunKiteEnterprise());
      });
    });

    withKiteEnterpriseNotRunning(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.canRunKiteEnterprise());
      });
    });

    withKiteEnterpriseRunning(() => {
      it('returns a rejected function', () => {
        waitsForPromise({shouldReject: true}, () => StateController.canRunKiteEnterprise());
      });
    });
  });

  describe('.runKiteEnterprise()', () => {
    describe('when kite is not installed', () => {
      it('returns a rejected function', () => {
        waitsForPromise({shouldReject: true}, () => StateController.runKiteEnterprise());
      });
    });

    withKiteEnterpriseRunning(() => {
      it('returns a resolved function', () => {
        waitsForPromise(() => StateController.runKiteEnterprise());
      });
    });

    withKiteEnterpriseNotRunning(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.runKiteEnterprise());
        runs(() => {
          expect(proc.spawn).toHaveBeenCalledWith('defaults', [
            'write', 'enterprise.kite.Kite', 'shouldReopenSidebar', '0',
          ]);

          expect(proc.spawn).toHaveBeenCalledWith('open', [
            '-a', OSXSupport.enterpriseInstallPath,
          ]);
        });
      });
    });
  });
});
<commit_msg>fix update test<commit_after>
'use strict';

const os = require('os');
const https = require('https');
const proc = require('child_process');
const StateController = require('../../lib/state-controller');
const OSXSupport = require('../../lib/support/osx');

const {
  fakeProcesses, fakeKiteInstallPaths, fakeResponse,
  withKiteInstalled, withKiteRunning, withKiteNotRunning,
  withFakeServer, withKiteEnterpriseInstalled, withBothKiteInstalled,
  withKiteEnterpriseRunning, withKiteEnterpriseNotRunning,
} = require('../spec-helpers.js');

describe('StateController - OSX Support', () => {
  beforeEach(() => {
    spyOn(os, 'platform').andReturn('darwin');
    spyOn(os, 'release').andReturn('14.0.0');
  });

  fakeKiteInstallPaths();

  describe('.isKiteSupported()', () => {
    it('returns a resolved promise', () => {
      waitsForPromise(() => StateController.isKiteSupported());
    });

    describe('when the os release is below 10.10', () => {
      beforeEach(() => {
        os.release.andReturn('13.0.0');
      });

      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.isKiteSupported());
      });
    });
  });

  describe('.isKiteInstalled()', () => {
    withKiteInstalled(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.isKiteInstalled());
      });
    });

    describe('when kite is not installed', () => {
      it('returns a rejected promise', () => {
        waitsForPromise({
          shouldReject: true,
        }, () => StateController.isKiteInstalled());
      });
    });

    withKiteEnterpriseInstalled(() => {
      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () =>
          StateController.isKiteInstalled());
      });
    });
  });

  describe('.isKiteEnterpriseInstalled()', () => {
    describe('when kite enterprise is not installed', () => {
      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () =>
          StateController.isKiteEnterpriseInstalled());
      });
    });

    withKiteInstalled(() => {
      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () =>
          StateController.isKiteEnterpriseInstalled());
      });
    });

    withKiteEnterpriseInstalled(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.isKiteEnterpriseInstalled());
      });
    });
  });

  describe('.hasBothKiteInstalled()', () => {
    describe('when no kite is installed', () => {
      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () =>
        StateController.hasBothKiteInstalled());
      });
    });

    describe('when kite enterprise is not installed', () => {
      withKiteInstalled(() => {
        it('returns a rejected promise', () => {
          waitsForPromise({shouldReject: true}, () =>
          StateController.hasBothKiteInstalled());
        });
      });
    });

    describe('when kite is not installed', () => {
      withKiteEnterpriseInstalled(() => {
        it('returns a rejected promise', () => {
          waitsForPromise({shouldReject: true}, () =>
          StateController.hasBothKiteInstalled());
        });
      });
    });

    withBothKiteInstalled(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.hasBothKiteInstalled());
      });
    });
  });

  describe('.installKite()', () => {
    describe('when every command succeeds', () => {
      beforeEach(() => {
        fakeProcesses({
          hdiutil: () => 0,
          cp: () => 0,
          rm: () => 0,
          mdfind: (ps, args) => {
            const [, key] = args[0].split(/\s=\s/);
            key === '"com.kite.Kite"'
              ? ps.stdout('/Applications/Kite.app')
              : ps.stdout('');
            return 0;
          },
        });
      });

      it('returns a resolved promise', () => {
        const options = {
          onInstallStart: jasmine.createSpy(),
          onMount: jasmine.createSpy(),
          onCopy: jasmine.createSpy(),
          onUnmount: jasmine.createSpy(),
          onRemove: jasmine.createSpy(),
        };

        waitsForPromise(() => StateController.installKite(options));
        runs(() => {
          expect(proc.spawn).toHaveBeenCalledWith('hdiutil', [
            'attach', '-nobrowse',
            OSXSupport.KITE_DMG_PATH,
          ]);
          expect(proc.spawn).toHaveBeenCalledWith('cp', [
            '-r',
            OSXSupport.KITE_APP_PATH.mounted,
            OSXSupport.APPS_PATH,
          ]);
          expect(proc.spawn).toHaveBeenCalledWith('hdiutil', [
            'detach',
            OSXSupport.KITE_VOLUME_PATH,
          ]);
          expect(proc.spawn).toHaveBeenCalledWith('rm', [
            OSXSupport.KITE_DMG_PATH,
          ]);
          expect(proc.spawn).toHaveBeenCalledWith('mdfind', [
            'kMDItemCFBundleIdentifier = "com.kite.Kite"',
          ]);

          expect(options.onInstallStart).toHaveBeenCalled();
          expect(options.onMount).toHaveBeenCalled();
          expect(options.onCopy).toHaveBeenCalled();
          expect(options.onUnmount).toHaveBeenCalled();
          expect(options.onRemove).toHaveBeenCalled();
        });
      });
    });

    describe('when mounting the archive fails', () => {
      beforeEach(() => {
        fakeProcesses({
          hdiutil: () => 1,
          cp: () => 0,
          rm: () => 0,
        });
      });

      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.installKite());
      });
    });

    describe('when copying the archive content fails', () => {
      beforeEach(() => {
        fakeProcesses({
          hdiutil: () => 0,
          cp: () => 1,
          rm: () => 0,
        });
      });

      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.installKite());
      });
    });

    describe('when unmounting the archive fails', () => {
      beforeEach(() => {
        fakeProcesses({
          hdiutil: (ps, [command]) => command === 'attach' ? 0 : 1,
          cp: () => 0,
          rm: () => 0,
        });
      });

      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.installKite());
      });
    });

    describe('when removing the downloaded archive fails', () => {
      beforeEach(() => {
        fakeProcesses({
          hdiutil: () => 0,
          cp: () => 0,
          rm: () => 1,
        });
      });

      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.installKite());
      });
    });
  });

  describe('.downloadKite()', () => {
    withFakeServer([
      [
        o => /^http:\/\/kite\.com/.test(o),
        o => fakeResponse(303, '', {headers: {location: 'https://download.kite.com'}}),
      ], [
        o => /^https:\/\/download\.kite\.com/.test(o),
        o => fakeResponse(200, 'foo'),
      ],
    ], () => {
      describe('when the download succeeds', () => {
        beforeEach(() => {
          fakeProcesses({
            hdiutil: () => 0,
            cp: () => 0,
            rm: () => 0,
          });
        });

        describe('with the install option', () => {
          it('returns a promise resolved after the install', () => {
            const options = {
              install: true,
              onDownload: jasmine.createSpy(),
              onInstallStart: jasmine.createSpy(),
              onMount: jasmine.createSpy(),
              onCopy: jasmine.createSpy(),
              onUnmount: jasmine.createSpy(),
              onRemove: jasmine.createSpy(),
            };
            const url = 'http://kite.com/download';

            waitsForPromise(() => StateController.downloadKite(url, options));
            runs(() => {
              expect(https.request).toHaveBeenCalledWith('http://kite.com/download');
              expect(proc.spawn).toHaveBeenCalledWith('hdiutil', [
                'attach', '-nobrowse',
                OSXSupport.KITE_DMG_PATH,
              ]);
              expect(proc.spawn).toHaveBeenCalledWith('cp', [
                '-r',
                OSXSupport.KITE_APP_PATH.mounted,
                OSXSupport.APPS_PATH,
              ]);
              expect(proc.spawn).toHaveBeenCalledWith('hdiutil', [
                'detach',
                OSXSupport.KITE_VOLUME_PATH,
              ]);
              expect(proc.spawn).toHaveBeenCalledWith('rm', [
                OSXSupport.KITE_DMG_PATH,
              ]);

              expect(options.onDownload).toHaveBeenCalled();
              expect(options.onInstallStart).toHaveBeenCalled();
              expect(options.onMount).toHaveBeenCalled();
              expect(options.onCopy).toHaveBeenCalled();
              expect(options.onUnmount).toHaveBeenCalled();
              expect(options.onRemove).toHaveBeenCalled();
            });
          });
        });
      });
    });
  });

  describe('.isKiteRunning()', () => {
    describe('when kite is not installed', () => {
      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.isKiteRunning());
      });
    });

    withKiteInstalled(() => {
      describe('but not running', () => {
        beforeEach(() => {
          fakeProcesses({
            '/bin/ps': (ps) => {
              ps.stdout('');
              return 0;
            },
          });
        });

        it('returns a rejected promise', () => {
          waitsForPromise({shouldReject: true}, () => StateController.isKiteRunning());
        });
      });

      withKiteRunning(() => {
        it('returns a resolved promise', () => {
          waitsForPromise(() => StateController.isKiteRunning());
        });
      });
    });
  });

  describe('.canRunKite()', () => {
    describe('when kite is not installed', () => {
      it('returns a rejected function', () => {
        waitsForPromise({shouldReject: true}, () => StateController.canRunKite());
      });
    });

    withKiteNotRunning(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.canRunKite());
      });
    });

    withKiteRunning(() => {
      it('returns a rejected function', () => {
        waitsForPromise({shouldReject: true}, () => StateController.canRunKite());
      });
    });
  });

  describe('.runKite()', () => {
    describe('when kite is not installed', () => {
      it('returns a rejected function', () => {
        waitsForPromise({shouldReject: true}, () => StateController.runKite());
      });
    });

    withKiteRunning(() => {
      it('returns a resolved function', () => {
        waitsForPromise(() => StateController.runKite());
      });
    });

    withKiteNotRunning(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.runKite());
        runs(() => {
          expect(proc.spawn).toHaveBeenCalledWith('defaults', [
            'write', 'com.kite.Kite', 'shouldReopenSidebar', '0',
          ]);

          expect(proc.spawn).toHaveBeenCalledWith('open', [
            '-a', OSXSupport.installPath,
          ]);
        });
      });
    });
  });

  describe('.isKiteEnterpriseRunning()', () => {
    describe('when kite is not installed', () => {
      it('returns a rejected promise', () => {
        waitsForPromise({shouldReject: true}, () => StateController.isKiteEnterpriseRunning());
      });
    });

    withKiteEnterpriseInstalled(() => {
      describe('but not running', () => {
        beforeEach(() => {
          fakeProcesses({
            '/bin/ps': (ps) => {
              ps.stdout('');
              return 0;
            },
          });
        });

        it('returns a rejected promise', () => {
          waitsForPromise({shouldReject: true}, () => StateController.isKiteEnterpriseRunning());
        });
      });

      withKiteEnterpriseRunning(() => {
        it('returns a resolved promise', () => {
          waitsForPromise(() => StateController.isKiteEnterpriseRunning());
        });
      });
    });
  });

  describe('.canRunKiteEnterprise()', () => {
    describe('when kite is not installed', () => {
      it('returns a rejected function', () => {
        waitsForPromise({shouldReject: true}, () => StateController.canRunKiteEnterprise());
      });
    });

    withKiteEnterpriseNotRunning(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.canRunKiteEnterprise());
      });
    });

    withKiteEnterpriseRunning(() => {
      it('returns a rejected function', () => {
        waitsForPromise({shouldReject: true}, () => StateController.canRunKiteEnterprise());
      });
    });
  });

  describe('.runKiteEnterprise()', () => {
    describe('when kite is not installed', () => {
      it('returns a rejected function', () => {
        waitsForPromise({shouldReject: true}, () => StateController.runKiteEnterprise());
      });
    });

    withKiteEnterpriseRunning(() => {
      it('returns a resolved function', () => {
        waitsForPromise(() => StateController.runKiteEnterprise());
      });
    });

    withKiteEnterpriseNotRunning(() => {
      it('returns a resolved promise', () => {
        waitsForPromise(() => StateController.runKiteEnterprise());
        runs(() => {
          expect(proc.spawn).toHaveBeenCalledWith('defaults', [
            'write', 'enterprise.kite.Kite', 'shouldReopenSidebar', '0',
          ]);

          expect(proc.spawn).toHaveBeenCalledWith('open', [
            '-a', OSXSupport.enterpriseInstallPath,
          ]);
        });
      });
    });
  });
});
<|endoftext|>


<commit_before>import _ from 'underscore';
import $ from 'jquery';
import is from 'is_js';
import baseVw from '../baseVw';
import loadTemplate from '../../utils/loadTemplate';
import app from '../../app';
import { openSimpleMessage } from '../modals/SimpleMessage';
import Dialog from '../modals/Dialog';
import Results from './Results';
import ResultsCol from '../../collections/Results';
import Providers from './SearchProviders';
import ProviderMd from '../../models/search/SearchProvider';
import defaultSearchProviders from '../../data/defaultSearchProviders';
import { selectEmojis } from '../../utils';
import { getCurrentConnection } from '../../utils/serverConnect';

export default class extends baseVw {
  constructor(options = {}) {
    const opts = {
      initialState: {
        fetching: false,
        ...options.initialState,
      },
      ...options,
    };

    super(opts);
    this.options = opts;
    // in the future the may be more possible types
    this.urlType = this.usingTor ? 'torlistings' : 'listings';

    this.sProvider = app.searchProviders[`default${this.torString}Provider`];

    // if the  provider returns a bad URL, the user must select a provider
    if (is.not.url(this.providerUrl)) {
      // use the first default temporarily to construct the tempUrl below
      this.sProvider = app.searchProviders.get(defaultSearchProviders[0].id);
      this.mustSelectDefault = true;
    }

    const tempUrl = new URL(`${this.providerUrl}?${options.query || ''}`);
    let queryParams = tempUrl.searchParams;

    // if a url with parameters was in the query in, use the parameters in it instead.
    if (queryParams.get('providerQ')) {
      const subURL = new URL(queryParams.get('providerQ'));
      queryParams = subURL.searchParams;
      const base = `${subURL.origin}${subURL.pathname}`;
      const matchedProvider =
        app.searchProviders.filter(p =>
          base === p.get('listings') || base === p.get('torlistings'));

      /* if the query provider doesn't exist, create a temporary provider model for it.
         One quirk to note: if a tor url is passed in while the user is in clear mode, and an
         existing provider has that tor url, that provider will be activated but will use its
         clear url if it has one. The opposite is also true.
       */
      if (!matchedProvider.length) {
        const queryOpts = {};
        queryOpts[`${this.usingTor ? 'tor' : ''}listings`] = `${subURL.origin}${subURL.pathname}`;
        this.queryProvider = new ProviderMd(queryOpts);
      } else {
        this.sProvider = matchedProvider[0];
        this.queryProvider = null;
      }
    }

    const params = {};

    for (const param of queryParams.entries()) {
      params[param[0]] = param[1];
    }

    // use the parameters from the query unless they were overridden in the options
    this.serverPage = options.serverPage || params.p || 0;
    this.pageSize = options.pageSize || params.ps || 24;
    this.term = options.term || params.q || '';
    this.sortBySelected = options.sortBySelected || params.sortBy || '';
    // all parameters not specified above are assumed to be filters
    this.filters = _.omit(params, ['q', 'p', 'ps', 'sortBy', 'providerQ']);

    this.processTerm(this.term);
  }

  className() {
    return 'search';
  }

  events() {
    return {
      'click .js-searchBtn': 'clickSearchBtn',
      'change .js-sortBy': 'changeSortBy',
      'change .js-filterWrapper select': 'changeFilter',
      'change .js-filterWrapper input': 'changeFilter',
      'keyup .js-searchInput': 'onKeyupSearchInput',
      'click .js-deleteProvider': 'clickDeleteProvider',
      'click .js-makeDefaultProvider': 'clickMakeDefaultProvider',
      'click .js-addQueryProvider': 'clickAddQueryProvider',
    };
  }

  get usingOriginal() {
    return this.sProvider.id === defaultSearchProviders[0].id;
  }

  get usingTor() {
    return app.serverConfig.tor && getCurrentConnection().server.get('useTor');
  }

  get torString() {
    return this.usingTor ? 'Tor' : '';
  }

  get providerUrl() {
    // if a provider was created by the address bar query, use it instead.
    // use the first default provider if no other provider is available
    const currentProvider = this.queryProvider || this.sProvider;
    return currentProvider && currentProvider.get(this.urlType);
  }

  getCurrentProviderID() {
    return this.queryProvider || this.mustSelectDefault ? '' : this.sProvider.id;
  }

  /**
   * This will create a url with the term and other query parameters
   * @param {string} term
   */
  processTerm(term) {
    this.term = term || '';
    // if term is false, search for *
    const query = `q=${encodeURIComponent(term || '*')}`;
    const page = `&p=${this.serverPage}&ps=${this.pageSize}`;
    const sortBy = this.sortBySelected ? `&sortBy=${encodeURIComponent(this.sortBySelected)}` : '';
    const network = `&network=${!!app.serverConfig.testnet ? 'testnet' : 'mainnet'}`;
    let filters = $.param(this.filters);
    filters = filters ? `&${filters}` : '';
    const newURL = `${this.providerUrl}?${query}${network}${sortBy}${page}${filters}`;
    this.callSearchProvider(newURL);
  }

  /**
   * This will set either the current default provider. If the user is currently in
   * Tor mode, the default Tor provider will be set.
   * @param md the search provider model
   */
  activateProvider(md) {
    if (!md || !(md instanceof ProviderMd)) {
      throw new Error('Please provide a search provider model.');
    }
    if (app.searchProviders.indexOf(md) === -1) {
      throw new Error('The provider must be in the collection.');
    }
    this.sProvider = md;
    this.queryProvider = null;
    if (this.mustSelectDefault) {
      this.mustSelectDefault = false;
      this.makeDefaultProvider();
    }
    this.processTerm(this.term);
  }

  deleteProvider(md = this.sProvider) {
    if (md.get('locked')) {
      openSimpleMessage(app.polyglot.t('search.errors.locked'));
    } else {
      md.destroy();
      if (app.searchProviders.length) this.activateProvider(app.searchProviders.at(0));
    }
  }

  clickDeleteProvider() {
    this.deleteProvider();
  }

  makeDefaultProvider() {
    app.searchProviders[`default${this.torString}Provider`] = this.sProvider;
    this.getCachedEl('.js-makeDefaultProvider').addClass('hide');
  }

  clickMakeDefaultProvider() {
    this.makeDefaultProvider();
  }

  addQueryProvider() {
    if (this.queryProvider) app.searchProviders.add(this.queryProvider);
    this.activateProvider(this.queryProvider);
  }

  clickAddQueryProvider() {
    this.addQueryProvider();
  }

  callSearchProvider(searchUrl) {
    // remove a pending search if it exists
    if (this.callSearch) this.callSearch.abort();

    this.setState({
      fetching: true,
      selecting: this.mustSelectDefault,
      data: '',
      searchUrl,
      xhr: '',
    });

    if (!this.mustSelectDefault) {
      // query the search provider
      this.callSearch = $.get({
        url: searchUrl,
        dataType: 'json',
      })
        .done((data, status, xhr) => {
          // make sure minimal data is present
          if (data.name && data.links) {
            // if data about the provider is recieved, update the model
            const update = { name: data.name };
            const urlTypes = [];
            if (data.logo && is.url(data.logo)) update.logo = data.logo;
            if (data.links) {
              if (is.url(data.links.search)) {
                update.search = data.links.search;
                urlTypes.push('search');
              }
              if (is.url(data.links.listings)) {
                update.listings = data.links.listings;
                urlTypes.push('listings');
              }
              if (data.links.tor) {
                if (is.url(data.links.tor.search)) {
                  update.torsearch = data.links.tor.search;
                  urlTypes.push('torsearch');
                }
                if (is.url(data.links.tor.listings)) {
                  update.torlistings = data.links.tor.listings;
                  urlTypes.push('torlistings');
                }
              }
            }
            // update the defaults but do not save them
            if (!_.findWhere(defaultSearchProviders, { id: this.sProvider.id })) {
              this.sProvider.save(update, { urlTypes });
            } else {
              this.sProvider.set(update, { urlTypes });
            }
            this.setState({
              fetching: false,
              selecting: false,
              data,
              searchUrl,
              xhr: '',
            });
          } else {
            this.setState({
              fetching: false,
              selecting: false,
              data: '',
              searchUrl,
              xhr,
            });
          }
        })
        .fail((xhr) => {
          if (xhr.statusText !== 'abort') {
            this.setState({
              fetching: false,
              selecting: false,
              data: '',
              searchUrl,
              xhr,
            });
          }
        });
    }
  }

  showSearchError(xhr = {}) {
    const title = app.polyglot.t('search.errors.searchFailTitle', { provider: this.sProvider });
    const failReason = xhr.responseJSON ? xhr.responseJSON.reason : '';
    const msg = failReason ?
                app.polyglot.t('search.errors.searchFailReason', { error: failReason }) : '';
    const buttons = [];
    if (this.usingOriginal) {
      buttons.push({
        text: app.polyglot.t('search.changeProvider'),
        fragment: 'changeProvider',
      });
    } else {
      buttons.push({
        text: app.polyglot.t('search.useDefault',
          {
            term: this.term,
            defaultProvider: app.searchProviders[`default${this.torString}Provider`],
          }),
        fragment: 'useDefault',
      });
    }

    const errorDialog = new Dialog({
      title,
      msg,
      buttons,
      showCloseButton: false,
      removeOnClose: true,
    }).render().open();
    this.listenTo(errorDialog, 'click-changeProvider', () => {
      errorDialog.close();
    });
    this.listenTo(errorDialog, 'click-useDefault', () => {
      this.activateProvider(app.searchProviders.at(0));
      errorDialog.close();
    });
  }

  createResults(data, searchUrl) {
    this.resultsCol = new ResultsCol();
    this.resultsCol.add(this.resultsCol.parse(data));

    const resultsView = this.createChild(Results, {
      searchUrl,
      total: data.results ? data.results.total : 0,
      morePages: data.results ? data.results.morePages : false,
      serverPage: this.serverPage,
      pageSize: this.pageSize,
      initCol: this.resultsCol,
    });

    this.$resultsWrapper.html(resultsView.render().el);

    this.listenTo(resultsView, 'searchError', (xhr) => this.showSearchError(xhr));
    this.listenTo(resultsView, 'loadingPage', () => this.scrollToTop());
  }

  clickSearchBtn() {
    this.processTerm(this.$searchInput.val());
  }

  onKeyupSearchInput(e) {
    if (e.which === 13) {
      this.processTerm(this.$searchInput.val());
    }
  }

  changeSortBy(e) {
    this.sortBySelected = $(e.target).val();
    this.processTerm(this.term);
  }

  changeFilter(e) {
    const targ = $(e.target);
    this.filters[targ.prop('name')] = targ.val();
    this.processTerm(this.term);
  }

  scrollToTop() {
    this.$el[0].scrollIntoView();
  }

  remove() {
    if (this.callSearch) this.callSearch.abort();
    super.remove();
  }

  render() {
    super.render();
    const state = this.getState();
    const data = state.data;

    if (data && !state.searchUrl) {
      throw new Error('Please provide the search URL along with the data.');
    }

    let errTitle;
    let errMsg;

    // check to see if the call to the provider failed, or returned an empty result
    const emptyData = $.isEmptyObject(data);

    if (state.xhr) {
      errTitle = app.polyglot.t('search.errors.searchFailTitle', { provider: state.searchUrl });
      const failReason = state.xhr.responseJSON ? state.xhr.responseJSON.reason : '';
      errMsg = failReason ?
        app.polyglot.t('search.errors.searchFailReason', { error: failReason }) : '';
    }

    loadTemplate('search/Search.html', (t) => {
      this.$el.html(t({
        term: this.term === '*' ? '' : this.term,
        sortBySelected: this.sortBySelected,
        filterVals: this.filters,
        errTitle,
        errMsg,
        providerLocked: this.sProvider.get('locked'),
        isQueryProvider: !!this.queryProvider,
        isDefaultProvider: this.sProvider === app.searchProviders.defaultProvider,
        emptyData,
        ...state,
        ...this.sProvider,
        ...data,
      }));
    });
    this.$sortBy = this.$('#sortBy');
    this.$sortBy.select2({
      // disables the search box
      minimumResultsForSearch: Infinity,
      templateResult: selectEmojis,
      templateSelection: selectEmojis,
    });
    const filterWrapper = this.$('.js-filterWrapper');
    filterWrapper.find('select').select2({
      // disables the search box
      minimumResultsForSearch: Infinity,
      templateResult: selectEmojis,
      templateSelection: selectEmojis,
    });
    this.$filters = filterWrapper.find('select, input');
    this.$resultsWrapper = this.$('.js-resultsWrapper');
    this.$searchInput = this.$('.js-searchInput');
    this.$searchLogo = this.$('.js-searchLogo');

    this.$searchLogo.find('img').on('error', () => {
      this.$searchLogo.addClass('loadError');
    });

    if (this.searchProviders) this.searchProviders.remove();
    this.searchProviders = this.createChild(Providers, {
      urlType: this.urlType,
      currentID: this.getCurrentProviderID(),
      selecting: this.mustSelectDefault,
    });
    this.listenTo(this.searchProviders, 'activateProvider', pOpts => this.activateProvider(pOpts));
    this.$('.js-searchProviders').append(this.searchProviders.render().el);

    // use the initial set of results data to create the results view
    if (data) this.createResults(data, state.searchUrl);

    return this;
  }
}
<commit_msg>Update comment<commit_after>import _ from 'underscore';
import $ from 'jquery';
import is from 'is_js';
import baseVw from '../baseVw';
import loadTemplate from '../../utils/loadTemplate';
import app from '../../app';
import { openSimpleMessage } from '../modals/SimpleMessage';
import Dialog from '../modals/Dialog';
import Results from './Results';
import ResultsCol from '../../collections/Results';
import Providers from './SearchProviders';
import ProviderMd from '../../models/search/SearchProvider';
import defaultSearchProviders from '../../data/defaultSearchProviders';
import { selectEmojis } from '../../utils';
import { getCurrentConnection } from '../../utils/serverConnect';

export default class extends baseVw {
  constructor(options = {}) {
    const opts = {
      initialState: {
        fetching: false,
        ...options.initialState,
      },
      ...options,
    };

    super(opts);
    this.options = opts;
    // in the future the may be more possible types
    this.urlType = this.usingTor ? 'torlistings' : 'listings';

    this.sProvider = app.searchProviders[`default${this.torString}Provider`];

    // if the  provider returns a bad URL, the user must select a provider
    if (is.not.url(this.providerUrl)) {
      // use the first default temporarily to construct the tempUrl below
      this.sProvider = app.searchProviders.get(defaultSearchProviders[0].id);
      this.mustSelectDefault = true;
    }

    const tempUrl = new URL(`${this.providerUrl}?${options.query || ''}`);
    let queryParams = tempUrl.searchParams;

    // if a url with parameters was in the query in, use the parameters in it instead.
    if (queryParams.get('providerQ')) {
      const subURL = new URL(queryParams.get('providerQ'));
      queryParams = subURL.searchParams;
      const base = `${subURL.origin}${subURL.pathname}`;
      const matchedProvider =
        app.searchProviders.filter(p =>
          base === p.get('listings') || base === p.get('torlistings'));

      /* if the query provider doesn't exist, create a temporary provider model for it.
         One quirk to note: if a tor url is passed in while the user is in clear mode, and an
         existing provider has that tor url, that provider will be activated but will use its
         clear url if it has one. The opposite is also true.
       */
      if (!matchedProvider.length) {
        const queryOpts = {};
        queryOpts[`${this.usingTor ? 'tor' : ''}listings`] = `${subURL.origin}${subURL.pathname}`;
        this.queryProvider = new ProviderMd(queryOpts);
      } else {
        this.sProvider = matchedProvider[0];
        this.queryProvider = null;
      }
    }

    const params = {};

    for (const param of queryParams.entries()) {
      params[param[0]] = param[1];
    }

    // use the parameters from the query unless they were overridden in the options
    this.serverPage = options.serverPage || params.p || 0;
    this.pageSize = options.pageSize || params.ps || 24;
    this.term = options.term || params.q || '';
    this.sortBySelected = options.sortBySelected || params.sortBy || '';
    // all parameters not specified above are assumed to be filters
    this.filters = _.omit(params, ['q', 'p', 'ps', 'sortBy', 'providerQ']);

    this.processTerm(this.term);
  }

  className() {
    return 'search';
  }

  events() {
    return {
      'click .js-searchBtn': 'clickSearchBtn',
      'change .js-sortBy': 'changeSortBy',
      'change .js-filterWrapper select': 'changeFilter',
      'change .js-filterWrapper input': 'changeFilter',
      'keyup .js-searchInput': 'onKeyupSearchInput',
      'click .js-deleteProvider': 'clickDeleteProvider',
      'click .js-makeDefaultProvider': 'clickMakeDefaultProvider',
      'click .js-addQueryProvider': 'clickAddQueryProvider',
    };
  }

  get usingOriginal() {
    return this.sProvider.id === defaultSearchProviders[0].id;
  }

  get usingTor() {
    return app.serverConfig.tor && getCurrentConnection().server.get('useTor');
  }

  get torString() {
    return this.usingTor ? 'Tor' : '';
  }

  get providerUrl() {
    // if a provider was created by the address bar query, use it instead.
    // return false if no provider is available
    const currentProvider = this.queryProvider || this.sProvider;
    return currentProvider && currentProvider.get(this.urlType);
  }

  getCurrentProviderID() {
    return this.queryProvider || this.mustSelectDefault ? '' : this.sProvider.id;
  }

  /**
   * This will create a url with the term and other query parameters
   * @param {string} term
   */
  processTerm(term) {
    this.term = term || '';
    // if term is false, search for *
    const query = `q=${encodeURIComponent(term || '*')}`;
    const page = `&p=${this.serverPage}&ps=${this.pageSize}`;
    const sortBy = this.sortBySelected ? `&sortBy=${encodeURIComponent(this.sortBySelected)}` : '';
    const network = `&network=${!!app.serverConfig.testnet ? 'testnet' : 'mainnet'}`;
    let filters = $.param(this.filters);
    filters = filters ? `&${filters}` : '';
    const newURL = `${this.providerUrl}?${query}${network}${sortBy}${page}${filters}`;
    this.callSearchProvider(newURL);
  }

  /**
   * This will set either the current default provider. If the user is currently in
   * Tor mode, the default Tor provider will be set.
   * @param md the search provider model
   */
  activateProvider(md) {
    if (!md || !(md instanceof ProviderMd)) {
      throw new Error('Please provide a search provider model.');
    }
    if (app.searchProviders.indexOf(md) === -1) {
      throw new Error('The provider must be in the collection.');
    }
    this.sProvider = md;
    this.queryProvider = null;
    if (this.mustSelectDefault) {
      this.mustSelectDefault = false;
      this.makeDefaultProvider();
    }
    this.processTerm(this.term);
  }

  deleteProvider(md = this.sProvider) {
    if (md.get('locked')) {
      openSimpleMessage(app.polyglot.t('search.errors.locked'));
    } else {
      md.destroy();
      if (app.searchProviders.length) this.activateProvider(app.searchProviders.at(0));
    }
  }

  clickDeleteProvider() {
    this.deleteProvider();
  }

  makeDefaultProvider() {
    app.searchProviders[`default${this.torString}Provider`] = this.sProvider;
    this.getCachedEl('.js-makeDefaultProvider').addClass('hide');
  }

  clickMakeDefaultProvider() {
    this.makeDefaultProvider();
  }

  addQueryProvider() {
    if (this.queryProvider) app.searchProviders.add(this.queryProvider);
    this.activateProvider(this.queryProvider);
  }

  clickAddQueryProvider() {
    this.addQueryProvider();
  }

  callSearchProvider(searchUrl) {
    // remove a pending search if it exists
    if (this.callSearch) this.callSearch.abort();

    this.setState({
      fetching: true,
      selecting: this.mustSelectDefault,
      data: '',
      searchUrl,
      xhr: '',
    });

    if (!this.mustSelectDefault) {
      // query the search provider
      this.callSearch = $.get({
        url: searchUrl,
        dataType: 'json',
      })
        .done((data, status, xhr) => {
          // make sure minimal data is present
          if (data.name && data.links) {
            // if data about the provider is recieved, update the model
            const update = { name: data.name };
            const urlTypes = [];
            if (data.logo && is.url(data.logo)) update.logo = data.logo;
            if (data.links) {
              if (is.url(data.links.search)) {
                update.search = data.links.search;
                urlTypes.push('search');
              }
              if (is.url(data.links.listings)) {
                update.listings = data.links.listings;
                urlTypes.push('listings');
              }
              if (data.links.tor) {
                if (is.url(data.links.tor.search)) {
                  update.torsearch = data.links.tor.search;
                  urlTypes.push('torsearch');
                }
                if (is.url(data.links.tor.listings)) {
                  update.torlistings = data.links.tor.listings;
                  urlTypes.push('torlistings');
                }
              }
            }
            // update the defaults but do not save them
            if (!_.findWhere(defaultSearchProviders, { id: this.sProvider.id })) {
              this.sProvider.save(update, { urlTypes });
            } else {
              this.sProvider.set(update, { urlTypes });
            }
            this.setState({
              fetching: false,
              selecting: false,
              data,
              searchUrl,
              xhr: '',
            });
          } else {
            this.setState({
              fetching: false,
              selecting: false,
              data: '',
              searchUrl,
              xhr,
            });
          }
        })
        .fail((xhr) => {
          if (xhr.statusText !== 'abort') {
            this.setState({
              fetching: false,
              selecting: false,
              data: '',
              searchUrl,
              xhr,
            });
          }
        });
    }
  }

  showSearchError(xhr = {}) {
    const title = app.polyglot.t('search.errors.searchFailTitle', { provider: this.sProvider });
    const failReason = xhr.responseJSON ? xhr.responseJSON.reason : '';
    const msg = failReason ?
                app.polyglot.t('search.errors.searchFailReason', { error: failReason }) : '';
    const buttons = [];
    if (this.usingOriginal) {
      buttons.push({
        text: app.polyglot.t('search.changeProvider'),
        fragment: 'changeProvider',
      });
    } else {
      buttons.push({
        text: app.polyglot.t('search.useDefault',
          {
            term: this.term,
            defaultProvider: app.searchProviders[`default${this.torString}Provider`],
          }),
        fragment: 'useDefault',
      });
    }

    const errorDialog = new Dialog({
      title,
      msg,
      buttons,
      showCloseButton: false,
      removeOnClose: true,
    }).render().open();
    this.listenTo(errorDialog, 'click-changeProvider', () => {
      errorDialog.close();
    });
    this.listenTo(errorDialog, 'click-useDefault', () => {
      this.activateProvider(app.searchProviders.at(0));
      errorDialog.close();
    });
  }

  createResults(data, searchUrl) {
    this.resultsCol = new ResultsCol();
    this.resultsCol.add(this.resultsCol.parse(data));

    const resultsView = this.createChild(Results, {
      searchUrl,
      total: data.results ? data.results.total : 0,
      morePages: data.results ? data.results.morePages : false,
      serverPage: this.serverPage,
      pageSize: this.pageSize,
      initCol: this.resultsCol,
    });

    this.$resultsWrapper.html(resultsView.render().el);

    this.listenTo(resultsView, 'searchError', (xhr) => this.showSearchError(xhr));
    this.listenTo(resultsView, 'loadingPage', () => this.scrollToTop());
  }

  clickSearchBtn() {
    this.processTerm(this.$searchInput.val());
  }

  onKeyupSearchInput(e) {
    if (e.which === 13) {
      this.processTerm(this.$searchInput.val());
    }
  }

  changeSortBy(e) {
    this.sortBySelected = $(e.target).val();
    this.processTerm(this.term);
  }

  changeFilter(e) {
    const targ = $(e.target);
    this.filters[targ.prop('name')] = targ.val();
    this.processTerm(this.term);
  }

  scrollToTop() {
    this.$el[0].scrollIntoView();
  }

  remove() {
    if (this.callSearch) this.callSearch.abort();
    super.remove();
  }

  render() {
    super.render();
    const state = this.getState();
    const data = state.data;

    if (data && !state.searchUrl) {
      throw new Error('Please provide the search URL along with the data.');
    }

    let errTitle;
    let errMsg;

    // check to see if the call to the provider failed, or returned an empty result
    const emptyData = $.isEmptyObject(data);

    if (state.xhr) {
      errTitle = app.polyglot.t('search.errors.searchFailTitle', { provider: state.searchUrl });
      const failReason = state.xhr.responseJSON ? state.xhr.responseJSON.reason : '';
      errMsg = failReason ?
        app.polyglot.t('search.errors.searchFailReason', { error: failReason }) : '';
    }

    loadTemplate('search/Search.html', (t) => {
      this.$el.html(t({
        term: this.term === '*' ? '' : this.term,
        sortBySelected: this.sortBySelected,
        filterVals: this.filters,
        errTitle,
        errMsg,
        providerLocked: this.sProvider.get('locked'),
        isQueryProvider: !!this.queryProvider,
        isDefaultProvider: this.sProvider === app.searchProviders.defaultProvider,
        emptyData,
        ...state,
        ...this.sProvider,
        ...data,
      }));
    });
    this.$sortBy = this.$('#sortBy');
    this.$sortBy.select2({
      // disables the search box
      minimumResultsForSearch: Infinity,
      templateResult: selectEmojis,
      templateSelection: selectEmojis,
    });
    const filterWrapper = this.$('.js-filterWrapper');
    filterWrapper.find('select').select2({
      // disables the search box
      minimumResultsForSearch: Infinity,
      templateResult: selectEmojis,
      templateSelection: selectEmojis,
    });
    this.$filters = filterWrapper.find('select, input');
    this.$resultsWrapper = this.$('.js-resultsWrapper');
    this.$searchInput = this.$('.js-searchInput');
    this.$searchLogo = this.$('.js-searchLogo');

    this.$searchLogo.find('img').on('error', () => {
      this.$searchLogo.addClass('loadError');
    });

    if (this.searchProviders) this.searchProviders.remove();
    this.searchProviders = this.createChild(Providers, {
      urlType: this.urlType,
      currentID: this.getCurrentProviderID(),
      selecting: this.mustSelectDefault,
    });
    this.listenTo(this.searchProviders, 'activateProvider', pOpts => this.activateProvider(pOpts));
    this.$('.js-searchProviders').append(this.searchProviders.render().el);

    // use the initial set of results data to create the results view
    if (data) this.createResults(data, state.searchUrl);

    return this;
  }
}
<|endoftext|>


<commit_before>import React from 'react';

import {CompositeDisposable, Disposable} from 'atom';
import cx from 'classnames';
import {autobind} from 'core-decorators';

import HunkView from './hunk-view';
import Commands, {Command} from './commands';
import FilePatchSelection from './file-patch-selection';
import EventWatcher from '../event-watcher';
import {setStatePromise} from '../helpers';

export default class FilePatchView extends React.Component {
  static propTypes = {
    commandRegistry: React.PropTypes.object.isRequired,
    filePath: React.PropTypes.string.isRequired,
    hunks: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
    stagingStatus: React.PropTypes.oneOf(['unstaged', 'staged']).isRequired,
    isPartiallyStaged: React.PropTypes.bool.isRequired,
    hasUndoHistory: React.PropTypes.bool.isRequired,
    attemptLineStageOperation: React.PropTypes.func.isRequired,
    attemptHunkStageOperation: React.PropTypes.func.isRequired,
    discardLines: React.PropTypes.func.isRequired,
    undoLastDiscard: React.PropTypes.func.isRequired,
    openCurrentFile: React.PropTypes.func.isRequired,
    didSurfaceFile: React.PropTypes.func.isRequired,
    didDiveIntoCorrespondingFilePatch: React.PropTypes.func.isRequired,
    eventWatcher: React.PropTypes.instanceOf(EventWatcher),
  }

  static defaultProps = {
    eventWatcher: new EventWatcher(),
  }

  constructor(props, context) {
    super(props, context);

    this.mouseSelectionInProgress = false;

    window.addEventListener('mouseup', this.mouseup);
    this.disposables = new CompositeDisposable();
    this.disposables.add(new Disposable(() => window.removeEventListener('mouseup', this.mouseup)));

    this.state = {
      selection: new FilePatchSelection(this.props.hunks),
    };
  }

  componentWillReceiveProps(nextProps) {
    const hunksChanged = this.props.hunks.length !== nextProps.hunks.length ||
      this.props.hunks.some((hunk, index) => hunk !== nextProps.hunks[index]);

    if (hunksChanged) {
      this.setState(prevState => {
        return {
          selection: prevState.selection.updateHunks(nextProps.hunks),
        };
      }, () => {
        nextProps.eventWatcher.resolvePatchChangedPromise();
      });
    }
  }

  render() {
    const selectedHunks = this.state.selection.getSelectedHunks();
    const selectedLines = this.state.selection.getSelectedLines();
    const headHunk = this.state.selection.getHeadHunk();
    const headLine = this.state.selection.getHeadLine();
    const hunkSelectionMode = this.state.selection.getMode() === 'hunk';

    const unstaged = this.props.stagingStatus === 'unstaged';
    const stageButtonLabelPrefix = unstaged ? 'Stage' : 'Unstage';

    return (

 { this.element = e; }}>

        <Commands registry={this.props.commandRegistry} target=".github-FilePatchView">
          <Command command="github:toggle-patch-selection-mode" callback={this.togglePatchSelectionMode} />
          <Command command="core:confirm" callback={this.didConfirm} />
          <Command command="core:move-up" callback={this.selectPrevious} />
          <Command command="core:move-down" callback={this.selectNext} />
          <Command command="core:move-right" callback={this.didMoveRight} />
          <Command command="core:move-to-top" callback={this.selectFirst} />
          <Command command="core:move-to-bottom" callback={this.selectLast} />
          <Command command="core:select-up" callback={this.selectToPrevious} />
          <Command command="core:select-down" callback={this.selectToNext} />
          <Command command="core:select-to-top" callback={this.selectToFirst} />
          <Command command="core:select-to-bottom" callback={this.selectToLast} />
          <Command command="core:select-all" callback={this.selectAll} />
          <Command command="github:select-next-hunk" callback={this.selectNextHunk} />
          <Command command="github:select-previous-hunk" callback={this.selectPreviousHunk} />
          <Command command="github:open-file" callback={this.openFile} />
          <Command
            command="github:view-corresponding-diff"
            callback={() => this.props.isPartiallyStaged && this.props.didDiveIntoCorrespondingFilePatch()}
          />
          <Command command="github:discard-selected-lines" callback={this.discardSelection} />
          <Command
            command="core:undo"
            callback={() => this.props.hasUndoHistory && this.props.undoLastDiscard()}
          />
        </Commands>

        <Commands registry={this.props.commandRegistry} target="atom-workspace">
          <Command
            command="github:undo-last-discard-in-diff-view"
            callback={() => this.props.hasUndoHistory && this.props.undoLastDiscard()}
          />
        </Commands>

            {unstaged ? 'Unstaged Changes for ' : 'Staged Changes for '}
            {this.props.filePath}

          {this.renderButtonGroup()}

          {this.props.hunks.map(hunk => {
            const isSelected = selectedHunks.has(hunk);
            let stageButtonSuffix = (hunkSelectionMode || !isSelected) ? ' Hunk' : ' Selection';
            if (selectedHunks.size > 1 && selectedHunks.has(hunk)) {
              stageButtonSuffix += 's';
            }
            const stageButtonLabel = stageButtonLabelPrefix + stageButtonSuffix;

            return (
              <HunkView
                key={hunk.getHeader()}
                hunk={hunk}
                isSelected={selectedHunks.has(hunk)}
                hunkSelectionMode={hunkSelectionMode}
                stageButtonLabel={stageButtonLabel}
                selectedLines={selectedLines}
                headLine={headLine}
                headHunk={headHunk}
                mousedownOnHeader={e => this.mousedownOnHeader(e, hunk)}
                mousedownOnLine={this.mousedownOnLine}
                mousemoveOnLine={this.mousemoveOnLine}
                contextMenuOnItem={this.contextMenuOnItem}
                didClickStageButton={() => this.didClickStageButtonForHunk(hunk)}
              />
            );
          })}

    );
  }

  @autobind
  renderButtonGroup() {
    const unstaged = this.props.stagingStatus === 'unstaged';

    return (

        {this.props.hasUndoHistory && unstaged ? (
          <button
            className="btn icon icon-history"
            onClick={this.props.undoLastDiscard}>
            Undo Discard
          </button>
        ) : null}
        {this.props.isPartiallyStaged ? (
          <button
            className={cx('btn', 'icon', {'icon-tasklist': unstaged, 'icon-list-unordered': !unstaged})}
            onClick={this.props.didDiveIntoCorrespondingFilePatch}
          />
        ) : null}
        <button
          className="btn icon icon-code"
          onClick={this.openFile}
        />
        <button
          className={cx('btn', 'icon', {'icon-move-down': unstaged, 'icon-move-up': !unstaged})}
          onClick={this.stageOrUnstageAll}>
          {unstaged ? 'Stage All' : 'Unstage All'}
        </button>

    );
  }

  componentWillUnmount() {
    this.disposables.dispose();
  }

  @autobind
  async contextMenuOnItem(event, hunk, line) {
    const mode = this.state.selection.getMode();
    if (mode === 'hunk' && !this.state.selection.getSelectedHunks().has(hunk)) {
      await setStatePromise(prevState => {
        return {selection: prevState.selection.selectHunk(hunk, event.shiftKey)};
      });
    } else if (mode === 'line' && !this.state.selection.getSelectedLines().has(line)) {
      await setStatePromise(prevState => {
        return {selection: prevState.selection.selectLine(line, event.shiftKey)};
      });
    } else {
      return;
    }
    event.stopPropagation();

    const newEvent = new MouseEvent(event.type, event);
    requestAnimationFrame(() => {
      event.target.parentNode.dispatchEvent(newEvent);
    });
  }

  mousedownOnHeader(event, hunk) {
    if (event.button !== 0) { return; }
    const windows = process.platform === 'win32';
    if (event.ctrlKey && !windows) { return; } // simply open context menu

    this.mouseSelectionInProgress = true;

    this.setState(prevState => {
      let selection = prevState.selection;
      if (event.metaKey || (event.ctrlKey && windows)) {
        if (selection.getMode() === 'hunk') {
          selection = selection.addOrSubtractHunkSelection(hunk);
        } else {
          // TODO: optimize
          selection = hunk.getLines().reduce(
            (current, line) => current.addOrSubtractLineSelection(line).coalesce(),
            selection,
          );
        }
      } else if (event.shiftKey) {
        if (selection.getMode() === 'hunk') {
          selection = selection.selectHunk(hunk, true);
        } else {
          const hunkLines = hunk.getLines();
          const tailIndex = selection.getLineSelectionTailIndex();
          const selectedHunkAfterTail = tailIndex < hunkLines[0].diffLineNumber;
          if (selectedHunkAfterTail) {
            selection = selection.selectLine(hunkLines[hunkLines.length - 1], true);
          } else {
            selection = selection.selectLine(hunkLines[0], true);
          }
        }
      } else {
        selection = selection.selectHunk(hunk, false);
      }

      return {selection};
    });
  }

  @autobind
  mousedownOnLine(event, hunk, line) {
    if (event.button !== 0) { return; }
    const windows = process.platform === 'win32';
    if (event.ctrlKey && !windows) { return; } // simply open context menu

    this.mouseSelectionInProgress = true;

    this.setState(prevState => {
      let selection = prevState.selection;

      if (event.metaKey || (event.ctrlKey && windows)) {
        if (selection.getMode() === 'hunk') {
          selection = selection.addOrSubtractHunkSelection(hunk);
        } else {
          selection = selection.addOrSubtractLineSelection(line);
        }
      } else if (event.shiftKey) {
        if (selection.getMode() === 'hunk') {
          selection = selection.selectHunk(hunk, true);
        } else {
          selection = selection.selectLine(line, true);
        }
      } else if (event.detail === 1) {
        selection = selection.selectLine(line, false);
      } else if (event.detail === 2) {
        selection = selection.selectHunk(hunk, false);
      }

      return {selection};
    });
  }

  @autobind
  mousemoveOnLine(event, hunk, line) {
    if (!this.mouseSelectionInProgress) { return; }

    this.setState(prevState => {
      let selection = null;
      if (prevState.selection.getMode() === 'hunk') {
        selection = prevState.selection.selectHunk(hunk, true);
      } else {
        selection = prevState.selection.selectLine(line, true);
      }
      return {selection};
    });
  }

  @autobind
  mouseup() {
    this.mouseSelectionInProgress = false;
    this.setState(prevState => {
      return {selection: prevState.selection.coalesce()};
    });
  }

  @autobind
  togglePatchSelectionMode() {
    this.setState(prevState => ({selection: prevState.selection.toggleMode()}));
  }

  getPatchSelectionMode() {
    return this.state.selection.getMode();
  }

  getSelectedHunks() {
    return this.state.selection.getSelectedHunks();
  }

  getSelectedLines() {
    return this.state.selection.getSelectedLines();
  }

  @autobind
  selectNext() {
    this.setState(prevState => ({selection: prevState.selection.selectNext()}));
  }

  @autobind
  selectNextHunk() {
    this.setState(prevState => ({selection: prevState.selection.jumpToNextHunk()}));
  }

  @autobind
  selectToNext() {
    this.setState(prevState => {
      return {selection: prevState.selection.selectNext(true).coalesce()};
    });
  }

  @autobind
  selectPrevious() {
    this.setState(prevState => ({selection: prevState.selection.selectPrevious()}));
  }

  @autobind
  selectPreviousHunk() {
    this.setState(prevState => ({selection: prevState.selection.jumpToPreviousHunk()}));
  }

  @autobind
  selectToPrevious() {
    this.setState(prevState => {
      return {selection: prevState.selection.selectPrevious(true).coalesce()};
    });
  }

  @autobind
  selectFirst() {
    this.setState(prevState => ({selection: prevState.selection.selectFirst()}));
  }

  @autobind
  selectToFirst() {
    this.setState(prevState => ({selection: prevState.selection.selectFirst(true)}));
  }

  @autobind
  selectLast() {
    this.setState(prevState => ({selection: prevState.selection.selectLast()}));
  }

  @autobind
  selectToLast() {
    this.setState(prevState => ({selection: prevState.selection.selectLast(true)}));
  }

  @autobind
  selectAll() {
    this.setState(prevState => ({selection: prevState.selection.selectAll()}));
  }

  getNextHunkUpdatePromise() {
    return this.state.selection.getNextUpdatePromise();
  }

  didClickStageButtonForHunk(hunk) {
    if (this.state.selection.getSelectedHunks().has(hunk)) {
      this.props.attemptLineStageOperation(this.state.selection.getSelectedLines());
    } else {
      this.setState(prevState => ({selection: prevState.selection.selectHunk(hunk)}), () => {
        this.props.attemptHunkStageOperation(hunk);
      });
    }
  }

  @autobind
  didConfirm() {
    return this.didClickStageButtonForHunk([...this.state.selection.getSelectedHunks()][0]);
  }

  @autobind
  didMoveRight() {
    if (this.props.didSurfaceFile) {
      this.props.didSurfaceFile();
    }
  }

  focus() {
    this.element.focus();
  }

  @autobind
  openFile() {
    let lineNumber = 0;
    const firstSelectedLine = Array.from(this.state.selection.getSelectedLines())[0];
    if (firstSelectedLine && firstSelectedLine.newLineNumber > -1) {
      lineNumber = firstSelectedLine.newLineNumber;
    } else {
      const firstSelectedHunk = Array.from(this.state.selection.getSelectedHunks())[0];
      lineNumber = firstSelectedHunk ? firstSelectedHunk.getNewStartRow() : 0;
    }
    return this.props.openCurrentFile({lineNumber});
  }

  @autobind
  async stageOrUnstageAll() {
    await this.selectAll();
    this.didConfirm();
  }

  @autobind
  discardSelection() {
    const selectedLines = this.state.selection.getSelectedLines();
    return selectedLines.size ? this.props.discardLines(selectedLines) : null;
  }
}
<commit_msg>Judicious use of `event.persist`<commit_after>import React from 'react';

import {CompositeDisposable, Disposable} from 'atom';
import cx from 'classnames';
import {autobind} from 'core-decorators';

import HunkView from './hunk-view';
import Commands, {Command} from './commands';
import FilePatchSelection from './file-patch-selection';
import EventWatcher from '../event-watcher';
import {setStatePromise} from '../helpers';

export default class FilePatchView extends React.Component {
  static propTypes = {
    commandRegistry: React.PropTypes.object.isRequired,
    filePath: React.PropTypes.string.isRequired,
    hunks: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
    stagingStatus: React.PropTypes.oneOf(['unstaged', 'staged']).isRequired,
    isPartiallyStaged: React.PropTypes.bool.isRequired,
    hasUndoHistory: React.PropTypes.bool.isRequired,
    attemptLineStageOperation: React.PropTypes.func.isRequired,
    attemptHunkStageOperation: React.PropTypes.func.isRequired,
    discardLines: React.PropTypes.func.isRequired,
    undoLastDiscard: React.PropTypes.func.isRequired,
    openCurrentFile: React.PropTypes.func.isRequired,
    didSurfaceFile: React.PropTypes.func.isRequired,
    didDiveIntoCorrespondingFilePatch: React.PropTypes.func.isRequired,
    eventWatcher: React.PropTypes.instanceOf(EventWatcher),
  }

  static defaultProps = {
    eventWatcher: new EventWatcher(),
  }

  constructor(props, context) {
    super(props, context);

    this.mouseSelectionInProgress = false;

    window.addEventListener('mouseup', this.mouseup);
    this.disposables = new CompositeDisposable();
    this.disposables.add(new Disposable(() => window.removeEventListener('mouseup', this.mouseup)));

    this.state = {
      selection: new FilePatchSelection(this.props.hunks),
    };
  }

  componentWillReceiveProps(nextProps) {
    const hunksChanged = this.props.hunks.length !== nextProps.hunks.length ||
      this.props.hunks.some((hunk, index) => hunk !== nextProps.hunks[index]);

    if (hunksChanged) {
      this.setState(prevState => {
        return {
          selection: prevState.selection.updateHunks(nextProps.hunks),
        };
      }, () => {
        nextProps.eventWatcher.resolvePatchChangedPromise();
      });
    }
  }

  render() {
    const selectedHunks = this.state.selection.getSelectedHunks();
    const selectedLines = this.state.selection.getSelectedLines();
    const headHunk = this.state.selection.getHeadHunk();
    const headLine = this.state.selection.getHeadLine();
    const hunkSelectionMode = this.state.selection.getMode() === 'hunk';

    const unstaged = this.props.stagingStatus === 'unstaged';
    const stageButtonLabelPrefix = unstaged ? 'Stage' : 'Unstage';

    return (

 { this.element = e; }}>

        <Commands registry={this.props.commandRegistry} target=".github-FilePatchView">
          <Command command="github:toggle-patch-selection-mode" callback={this.togglePatchSelectionMode} />
          <Command command="core:confirm" callback={this.didConfirm} />
          <Command command="core:move-up" callback={this.selectPrevious} />
          <Command command="core:move-down" callback={this.selectNext} />
          <Command command="core:move-right" callback={this.didMoveRight} />
          <Command command="core:move-to-top" callback={this.selectFirst} />
          <Command command="core:move-to-bottom" callback={this.selectLast} />
          <Command command="core:select-up" callback={this.selectToPrevious} />
          <Command command="core:select-down" callback={this.selectToNext} />
          <Command command="core:select-to-top" callback={this.selectToFirst} />
          <Command command="core:select-to-bottom" callback={this.selectToLast} />
          <Command command="core:select-all" callback={this.selectAll} />
          <Command command="github:select-next-hunk" callback={this.selectNextHunk} />
          <Command command="github:select-previous-hunk" callback={this.selectPreviousHunk} />
          <Command command="github:open-file" callback={this.openFile} />
          <Command
            command="github:view-corresponding-diff"
            callback={() => this.props.isPartiallyStaged && this.props.didDiveIntoCorrespondingFilePatch()}
          />
          <Command command="github:discard-selected-lines" callback={this.discardSelection} />
          <Command
            command="core:undo"
            callback={() => this.props.hasUndoHistory && this.props.undoLastDiscard()}
          />
        </Commands>

        <Commands registry={this.props.commandRegistry} target="atom-workspace">
          <Command
            command="github:undo-last-discard-in-diff-view"
            callback={() => this.props.hasUndoHistory && this.props.undoLastDiscard()}
          />
        </Commands>

            {unstaged ? 'Unstaged Changes for ' : 'Staged Changes for '}
            {this.props.filePath}

          {this.renderButtonGroup()}

          {this.props.hunks.map(hunk => {
            const isSelected = selectedHunks.has(hunk);
            let stageButtonSuffix = (hunkSelectionMode || !isSelected) ? ' Hunk' : ' Selection';
            if (selectedHunks.size > 1 && selectedHunks.has(hunk)) {
              stageButtonSuffix += 's';
            }
            const stageButtonLabel = stageButtonLabelPrefix + stageButtonSuffix;

            return (
              <HunkView
                key={hunk.getHeader()}
                hunk={hunk}
                isSelected={selectedHunks.has(hunk)}
                hunkSelectionMode={hunkSelectionMode}
                stageButtonLabel={stageButtonLabel}
                selectedLines={selectedLines}
                headLine={headLine}
                headHunk={headHunk}
                mousedownOnHeader={e => this.mousedownOnHeader(e, hunk)}
                mousedownOnLine={this.mousedownOnLine}
                mousemoveOnLine={this.mousemoveOnLine}
                contextMenuOnItem={this.contextMenuOnItem}
                didClickStageButton={() => this.didClickStageButtonForHunk(hunk)}
              />
            );
          })}

    );
  }

  @autobind
  renderButtonGroup() {
    const unstaged = this.props.stagingStatus === 'unstaged';

    return (

        {this.props.hasUndoHistory && unstaged ? (
          <button
            className="btn icon icon-history"
            onClick={this.props.undoLastDiscard}>
            Undo Discard
          </button>
        ) : null}
        {this.props.isPartiallyStaged ? (
          <button
            className={cx('btn', 'icon', {'icon-tasklist': unstaged, 'icon-list-unordered': !unstaged})}
            onClick={this.props.didDiveIntoCorrespondingFilePatch}
          />
        ) : null}
        <button
          className="btn icon icon-code"
          onClick={this.openFile}
        />
        <button
          className={cx('btn', 'icon', {'icon-move-down': unstaged, 'icon-move-up': !unstaged})}
          onClick={this.stageOrUnstageAll}>
          {unstaged ? 'Stage All' : 'Unstage All'}
        </button>

    );
  }

  componentWillUnmount() {
    this.disposables.dispose();
  }

  @autobind
  async contextMenuOnItem(event, hunk, line) {
    const mode = this.state.selection.getMode();
    if (mode === 'hunk' && !this.state.selection.getSelectedHunks().has(hunk)) {
      await setStatePromise(prevState => {
        return {selection: prevState.selection.selectHunk(hunk, event.shiftKey)};
      });
    } else if (mode === 'line' && !this.state.selection.getSelectedLines().has(line)) {
      await setStatePromise(prevState => {
        return {selection: prevState.selection.selectLine(line, event.shiftKey)};
      });
    } else {
      return;
    }
    event.stopPropagation();

    const newEvent = new MouseEvent(event.type, event);
    requestAnimationFrame(() => {
      event.target.parentNode.dispatchEvent(newEvent);
    });
  }

  mousedownOnHeader(event, hunk) {
    if (event.button !== 0) { return; }
    const windows = process.platform === 'win32';
    if (event.ctrlKey && !windows) { return; } // simply open context menu

    this.mouseSelectionInProgress = true;
    event.persist();

    this.setState(prevState => {
      let selection = prevState.selection;
      if (event.metaKey || (event.ctrlKey && windows)) {
        if (selection.getMode() === 'hunk') {
          selection = selection.addOrSubtractHunkSelection(hunk);
        } else {
          // TODO: optimize
          selection = hunk.getLines().reduce(
            (current, line) => current.addOrSubtractLineSelection(line).coalesce(),
            selection,
          );
        }
      } else if (event.shiftKey) {
        if (selection.getMode() === 'hunk') {
          selection = selection.selectHunk(hunk, true);
        } else {
          const hunkLines = hunk.getLines();
          const tailIndex = selection.getLineSelectionTailIndex();
          const selectedHunkAfterTail = tailIndex < hunkLines[0].diffLineNumber;
          if (selectedHunkAfterTail) {
            selection = selection.selectLine(hunkLines[hunkLines.length - 1], true);
          } else {
            selection = selection.selectLine(hunkLines[0], true);
          }
        }
      } else {
        selection = selection.selectHunk(hunk, false);
      }

      return {selection};
    });
  }

  @autobind
  mousedownOnLine(event, hunk, line) {
    if (event.button !== 0) { return; }
    const windows = process.platform === 'win32';
    if (event.ctrlKey && !windows) { return; } // simply open context menu

    this.mouseSelectionInProgress = true;
    event.persist();

    this.setState(prevState => {
      let selection = prevState.selection;

      if (event.metaKey || (event.ctrlKey && windows)) {
        if (selection.getMode() === 'hunk') {
          selection = selection.addOrSubtractHunkSelection(hunk);
        } else {
          selection = selection.addOrSubtractLineSelection(line);
        }
      } else if (event.shiftKey) {
        if (selection.getMode() === 'hunk') {
          selection = selection.selectHunk(hunk, true);
        } else {
          selection = selection.selectLine(line, true);
        }
      } else if (event.detail === 1) {
        selection = selection.selectLine(line, false);
      } else if (event.detail === 2) {
        selection = selection.selectHunk(hunk, false);
      }

      return {selection};
    });
  }

  @autobind
  mousemoveOnLine(event, hunk, line) {
    if (!this.mouseSelectionInProgress) { return; }

    this.setState(prevState => {
      let selection = null;
      if (prevState.selection.getMode() === 'hunk') {
        selection = prevState.selection.selectHunk(hunk, true);
      } else {
        selection = prevState.selection.selectLine(line, true);
      }
      return {selection};
    });
  }

  @autobind
  mouseup() {
    this.mouseSelectionInProgress = false;
    this.setState(prevState => {
      return {selection: prevState.selection.coalesce()};
    });
  }

  @autobind
  togglePatchSelectionMode() {
    this.setState(prevState => ({selection: prevState.selection.toggleMode()}));
  }

  getPatchSelectionMode() {
    return this.state.selection.getMode();
  }

  getSelectedHunks() {
    return this.state.selection.getSelectedHunks();
  }

  getSelectedLines() {
    return this.state.selection.getSelectedLines();
  }

  @autobind
  selectNext() {
    this.setState(prevState => ({selection: prevState.selection.selectNext()}));
  }

  @autobind
  selectNextHunk() {
    this.setState(prevState => ({selection: prevState.selection.jumpToNextHunk()}));
  }

  @autobind
  selectToNext() {
    this.setState(prevState => {
      return {selection: prevState.selection.selectNext(true).coalesce()};
    });
  }

  @autobind
  selectPrevious() {
    this.setState(prevState => ({selection: prevState.selection.selectPrevious()}));
  }

  @autobind
  selectPreviousHunk() {
    this.setState(prevState => ({selection: prevState.selection.jumpToPreviousHunk()}));
  }

  @autobind
  selectToPrevious() {
    this.setState(prevState => {
      return {selection: prevState.selection.selectPrevious(true).coalesce()};
    });
  }

  @autobind
  selectFirst() {
    this.setState(prevState => ({selection: prevState.selection.selectFirst()}));
  }

  @autobind
  selectToFirst() {
    this.setState(prevState => ({selection: prevState.selection.selectFirst(true)}));
  }

  @autobind
  selectLast() {
    this.setState(prevState => ({selection: prevState.selection.selectLast()}));
  }

  @autobind
  selectToLast() {
    this.setState(prevState => ({selection: prevState.selection.selectLast(true)}));
  }

  @autobind
  selectAll() {
    this.setState(prevState => ({selection: prevState.selection.selectAll()}));
  }

  getNextHunkUpdatePromise() {
    return this.state.selection.getNextUpdatePromise();
  }

  didClickStageButtonForHunk(hunk) {
    if (this.state.selection.getSelectedHunks().has(hunk)) {
      this.props.attemptLineStageOperation(this.state.selection.getSelectedLines());
    } else {
      this.setState(prevState => ({selection: prevState.selection.selectHunk(hunk)}), () => {
        this.props.attemptHunkStageOperation(hunk);
      });
    }
  }

  @autobind
  didConfirm() {
    return this.didClickStageButtonForHunk([...this.state.selection.getSelectedHunks()][0]);
  }

  @autobind
  didMoveRight() {
    if (this.props.didSurfaceFile) {
      this.props.didSurfaceFile();
    }
  }

  focus() {
    this.element.focus();
  }

  @autobind
  openFile() {
    let lineNumber = 0;
    const firstSelectedLine = Array.from(this.state.selection.getSelectedLines())[0];
    if (firstSelectedLine && firstSelectedLine.newLineNumber > -1) {
      lineNumber = firstSelectedLine.newLineNumber;
    } else {
      const firstSelectedHunk = Array.from(this.state.selection.getSelectedHunks())[0];
      lineNumber = firstSelectedHunk ? firstSelectedHunk.getNewStartRow() : 0;
    }
    return this.props.openCurrentFile({lineNumber});
  }

  @autobind
  async stageOrUnstageAll() {
    await this.selectAll();
    this.didConfirm();
  }

  @autobind
  discardSelection() {
    const selectedLines = this.state.selection.getSelectedLines();
    return selectedLines.size ? this.props.discardLines(selectedLines) : null;
  }
}
<|endoftext|>