├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── images ├── demo.gif ├── hipchat.jpg ├── invite.gif └── slack.jpg ├── keymaps └── atom_pair.cson ├── lib ├── atom_pair.coffee ├── helpers │ ├── chunk-string.coffee │ └── colour-list.coffee ├── modules │ ├── grammar_sync.coffee │ ├── invitations │ │ ├── hipchat_invitation.coffee │ │ ├── invitation.coffee │ │ └── slack_invitation.coffee │ ├── message_queue.coffee │ ├── presence_indicator.coffee │ ├── session.coffee │ ├── share_pane.coffee │ └── user.coffee ├── pusher │ ├── pusher-js-client-auth.js │ └── pusher.js └── views │ ├── atom-pair-view.coffee │ └── input-view.coffee ├── menus └── atom-pair.json ├── package.json ├── spec ├── fixtures │ ├── basic-buffer-write.json │ ├── david_copperfield.txt │ ├── insert-and-line-break.json │ ├── large-text-for-small.json │ ├── multiline-deletions.json │ └── small-deletions.json ├── helpers │ ├── buffer-triggers.coffee │ └── spec-setup.coffee ├── invitations │ └── invitation-spec.coffee ├── message-queue │ └── queue-spec.coffee ├── pusher-mock.coffee ├── session │ └── session-spec.coffee ├── sharepane │ ├── disconnect-spec.coffee │ ├── grammar-sync-spec.coffee │ ├── sharepane-binds-spec.coffee │ └── sharepane-triggers-spec.coffee └── user │ └── user-spec.coffee └── styles └── atom_pair.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### 2.0.6 2 | 3 | * Pencil icon shows the active pane the buddy is working on. 4 | * Syncs over the names of tab titles. 5 | * Throws error if editor or buffer is of an unexpected type. Helps diagnose errors 6 | 7 | #### 2.0.5 8 | 9 | * Fix errors with multiple tab syncing. 10 | * Fix custom paste errors 11 | 12 | #### 2.0.2 13 | 14 | * Resolve issue where `ensureActiveTextEditor` would return a promise object, and therefore raise an error. 15 | 16 | # 2.0.0 17 | 18 | * Support for multiple tab sharing. Any tab opened in the window of a sharing section will be synced across to the partner. 19 | * Killing some views in favour of Atom's Notifications API. 20 | * Support for autocomplete/snippets. Previously it would cause clients becoming out of sync. 21 | * Automatic copying of session ID to the clipboard 22 | 23 | ####1.1.6 24 | 25 | Replaces deprecated jQuery event listeners on views with Atom command registry events. 26 | 27 | ####1.1.5 28 | 29 | Removes css id attribute of editor only if there is an active editor. 30 | Added little x to close view panels. 31 | 32 | ####1.1.4 33 | 34 | Ensures there are no references to a destroyed editor within the package. 35 | 36 | ####1.1.3 37 | 38 | * Package now ensures an active editor. 39 | * Package only registers customPaste command if user is in a pairing session. 40 | 41 | ####1.1.1 42 | 43 | * Fixed issue with Slack invite 44 | 45 | ### 1.1.0 46 | 47 | * Removed deprecated calls ahead of Atom version 1.0.0. 48 | * Fixed issues with the package swallowing escape key. 49 | * Resolved issues where package mistakenly says you are in a pairing session. 50 | 51 | ### 1.0.1 52 | 53 | * Package load time has dropped by around 100ms to around 21ms. 54 | 55 | ## 1.0.0 56 | * Uses package settings page for app configuration instead of a new config menu 57 | * Has Slack invitations added 58 | * Handles large deletions and insertions 59 | 60 | # Pre 1.0.0 61 | 62 | * Text synchronization 63 | * File-sharing 64 | * HipChat invitations 65 | * Synchronized syntax highlighting 66 | * Collaborator visibility. 67 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Pusher 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AtomPair 2 | [![Build Status](https://travis-ci.org/pusher/atom-pair.svg?branch=tweaks)](https://travis-ci.org/pusher/atom-pair) 3 | 4 | Remote pairing within the [Atom](http://atom.io) text editor, powered by Pusher. 5 | 6 | ## Version 2.0.0 7 | 8 | * This major release allows users to share multiple tabs. Any new tabs open within a window where a pairing session is active will be synchronized across. 9 | * Hand-made notifications views have now been ditched in favour of using Atom's Notifications API. 10 | * Initiating a session automatically writes the session ID to your clipboard, allowing you to simply paste it to your partner. 11 | * Using autocomplete no longer leaves clients out of sync. 12 | 13 | ## How Do I Get Started? 14 | 15 | ### Install 16 | 17 | First off, install Atom if you haven't already. Now type into your terminal: 18 | 19 | $ apm install atom-pair 20 | 21 | Alternatively, go to the command palette via Command+Shift+P and go to `Install Packages and Themes`. Then search for and install `atom-pair`. 22 | 23 | ### Invite 24 | 25 | You can either decide to pair on a blank slate, or on existing code. If you invite somebody to collaborate on existing code, they will see everything you can, and their syntax highlighting will be synchronized with yours. 26 | 27 | As detailed below, there are two ways you can invite others. Given a [free](https://pusher.com/signup?utm_source=Reddit&utm_medium=Atom.io_Package_Page&utm_campaign=AtomPair) Sandbox plan, there will be a maximum of 20 collaborators per session. *Note that you must enable client-events in App Settings when you create a new Pusher app, otherwise this plugin will not work.* 28 | 29 | #### Basic Invitation 30 | 31 | Hit Command+Shift+P, and in the command palette, hit `AtomPair: Start A New Pairing Session`. 32 | 33 | A session ID will be automatically copied to your clipboard. 34 | 35 | ![Basic Invite](https://raw.githubusercontent.com/pusher/atom-pair/master/images/invite.gif) 36 | 37 | #### HipChat Invitation 38 | 39 | The other way - one that we use quite often - is to invite collaborators over [HipChat](http://hipchat.com), a service for intra-company chat. You can sign up for a free account [here](https://www.hipchat.com/sign_up). 40 | 41 | We wanted this partly as an easy way of giving collaborators a session ID, but also so that other members of the team could join in if they wanted to. 42 | 43 | If you have admin privileges in a HipChat organization, go to your Package Settings (`⌘+,` -> 'Packages' -> 'atom-pair'). Enter your HipChat API key and the room you wish the invitation to be sent through. 44 | 45 | Now, when you enter `AtomPair: Invite Over HipChat` and enter your collaborator's HipChat @mention_name in the command palette, they will receive an invitation with a session ID. 46 | 47 | ![HipChat Invite](https://raw.githubusercontent.com/pusher/atom-pair/master/images/hipchat.jpg) 48 | 49 | #### Slack Invitation 50 | 51 | If you use [Slack](https://slack.com/) instead of HipChat, we have you covered for that too. It works pretty much the same way as the HipChat integration. All you need to do is log into your Slack account and click "Configure Integrations" and configure an "Incoming Webhook". It will ask you to choose a channel you want to post messages to, but this doesn't really matter too much, you will manually specify the channel or recipient when you send the invite. Once you set up your integration, it will give you a "Webhook URL". You'll need to copy this URL, and put it in your atom-pair configuration where it asks for a "WebHook URL for Slack Incoming Webhook Integration". 52 | 53 | To send the invite, simply enter "AtomPair: Invite Over Slack" and enter either the channel you want to send the invite to _(#channel)_ or the person you want to send the invite to _(@person)_. Once you do, all they have to do is join the session with the session ID and you'll be pair programming! 54 | 55 | ![Slack Invite](https://raw.githubusercontent.com/pusher/atom-pair/master/images/slack.jpg) 56 | 57 | ### Collaborate! 58 | 59 | ![Demo](https://raw.githubusercontent.com/pusher/atom-pair/master/images/demo.gif) 60 | 61 | Once your partner has a session ID, they should go to the command pallette and hit `AtomPair: Join a pairing session`, and enter the ID. 62 | 63 | Once there are more than one of you in a session, your collaborators will be represented by a coloured marker in the gutter, which will changed position based on their selections and inputs. 64 | 65 | Any new files opened in that window will be automatically synced across, and you can work on different files at the same time. 66 | 67 | To end a pairing session, go to `AtomPair: Disconnect`, and you will be disconnected from Pusher, and the file will be free for you to save. 68 | 69 | ## Free And Open For Everyone 70 | 71 | Currently, you are given default Pusher credentials when you install the package, so that you can get started with as less friction as possible. Communication will take place over a randomly generated channel name. However, for improved security, we encourage you to [create a free account](https://pusher.com/signup?utm_source=Reddit&utm_medium=Atom.io_Package_Page&utm_campaign=AtomPair) and enter your own app key and app secret by going to your Package Settings. A free Sandbox plan should be more than enough for your pairing sessions. *Note that you must enable client-events in App Settings when you create a new Pusher app, otherwise this plugin will not work.* 72 | 73 | ### Contributing 74 | 75 | Here is a current list of features: 76 | 77 | * Text synchronization 78 | * Multiple tab syncing 79 | * File-sharing 80 | * HipChat invitations 81 | * Slack invitations 82 | * Synchronized syntax highlighting 83 | * Collaborator visibility. 84 | 85 | But if there are any features you find lacking, feel more than welcome to [get in touch](). 86 | 87 | ### Running Tests 88 | 89 | To run the tests, just type into your command line at the root of the project: 90 | 91 | $ apm test 92 | 93 | ### Adding New Methods of Invitation 94 | 95 | Currently there is support for inviting people over HipChat and Slack. If you would like to invite friends or colleagues through any other integration, there is a mini-API to make this more simple. All you have to do is inherit from our `Invitation` class and implement two methods: 96 | 97 | ```coffee 98 | class YourInvitation extends Invitation 99 | 100 | checkConfig: -> 101 | # Must be implemented 102 | # Returns false if they are missing your integration's API keys, otherwise true. 103 | 104 | send: (callback)-> 105 | # Must be implemented 106 | # Send your invitation and simple call the callback when you're done. 107 | 108 | ``` 109 | 110 | See [here](https://github.com/pusher/atom-pair/blob/master/lib/modules/invitations/slack_invitation.coffee) for an example. 111 | 112 | ## Credits 113 | 114 | This project is owned and maintained by [@jpatel531](http://github.com/jpatel531), a developer at [Pusher](http://pusher.com). 115 | 116 | Special thanks for contributing go to: 117 | 118 | * [@snollygolly](http://github.com/snollygolly) 119 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/atom-pair/3e4b12b074a1b4e981bd19e1558f6edf8b3a360b/images/demo.gif -------------------------------------------------------------------------------- /images/hipchat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/atom-pair/3e4b12b074a1b4e981bd19e1558f6edf8b3a360b/images/hipchat.jpg -------------------------------------------------------------------------------- /images/invite.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/atom-pair/3e4b12b074a1b4e981bd19e1558f6edf8b3a360b/images/invite.gif -------------------------------------------------------------------------------- /images/slack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pusher/atom-pair/3e4b12b074a1b4e981bd19e1558f6edf8b3a360b/images/slack.jpg -------------------------------------------------------------------------------- /keymaps/atom_pair.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/advanced/keymaps 10 | 'atom-panel-container.bottom': 11 | 'escape': 'core:cancel' 12 | -------------------------------------------------------------------------------- /lib/atom_pair.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | 3 | Invitation = null 4 | HipChatInvitation = null 5 | SlackInvitation = null 6 | Session = null 7 | _ = null 8 | 9 | module.exports = AtomPair = 10 | 11 | AtomPairView: null 12 | modalPanel: null 13 | subscriptions: null 14 | 15 | config: 16 | hipchat_token: 17 | type: 'string' 18 | description: 'HipChat admin token (optional)' 19 | default: '' 20 | hipchat_room_name: 21 | type: 'string' 22 | description: 'HipChat room name for sending invitations (optional)' 23 | default: '' 24 | pusher_app_key: 25 | type: 'string' 26 | description: 'Pusher App Key (sign up at http://pusher.com/signup and change for added security)' 27 | default: 'd41a439c438a100756f5' 28 | pusher_app_secret: 29 | type: 'string' 30 | description: 'Pusher App Secret' 31 | default: '4bf35003e819bb138249' 32 | slack_url: 33 | type: 'string' 34 | description: 'WebHook URL for Slack Incoming Webhook Integration' 35 | default: '' 36 | 37 | activate: (state) -> 38 | _ = require 'underscore' 39 | Invitation = require './modules/invitations/invitation' 40 | HipChatInvitation = require './modules/invitations/hipchat_invitation' 41 | SlackInvitation = require './modules/invitations/slack_invitation' 42 | Session = require './modules/session' 43 | 44 | # Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable 45 | @subscriptions = new CompositeDisposable 46 | 47 | # Register command that toggles this view 48 | @subscriptions.add atom.commands.add 'atom-workspace', 'AtomPair:start new pairing session': => 49 | Session.initiate(Invitation) 50 | @subscriptions.add atom.commands.add 'atom-workspace', 'AtomPair:invite over hipchat': => 51 | Session.initiate(HipChatInvitation) 52 | @subscriptions.add atom.commands.add 'atom-workspace', 'AtomPair:invite over slack': => 53 | Session.initiate(SlackInvitation) 54 | @subscriptions.add atom.commands.add 'atom-workspace', 'AtomPair:join pairing session': => 55 | Session.join() 56 | -------------------------------------------------------------------------------- /lib/helpers/chunk-string.coffee: -------------------------------------------------------------------------------- 1 | module.exports = chunkString = (str, len) -> 2 | _size = Math.ceil(str.length / len) 3 | _ret = new Array(_size) 4 | _offset = undefined 5 | _i = 0 6 | 7 | while _i < _size 8 | _offset = _i * len 9 | _ret[_i] = str.substring(_offset, _offset + len) 10 | _i++ 11 | _ret 12 | -------------------------------------------------------------------------------- /lib/helpers/colour-list.coffee: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | "red" 3 | "blue" 4 | "pink" 5 | "yellow" 6 | "orange" 7 | "purple" 8 | "green" 9 | "brown" 10 | "skyblue" 11 | "olive" 12 | "salmon" 13 | "white" 14 | "lime" 15 | "maroon" 16 | "beige" 17 | "darkgoldenrod" 18 | "blanchedalmond" 19 | "tan" 20 | "violet" 21 | "navy" 22 | "gold" 23 | "black" 24 | ] 25 | -------------------------------------------------------------------------------- /lib/modules/grammar_sync.coffee: -------------------------------------------------------------------------------- 1 | module.exports = GrammarSync = 2 | syncGrammars: -> 3 | @editor.onDidChangeGrammar => @sendGrammar() 4 | 5 | sendGrammar: -> 6 | grammar = @editor.getGrammar() 7 | @queue.add(@channel.name, 'client-grammar-sync', grammar.scopeName) 8 | -------------------------------------------------------------------------------- /lib/modules/invitations/hipchat_invitation.coffee: -------------------------------------------------------------------------------- 1 | Invitation = require './invitation' 2 | HipChat = require 'node-hipchat' 3 | _ = require 'underscore' 4 | 5 | module.exports = 6 | class HipChatInvitation extends Invitation 7 | 8 | needsInput: true 9 | askRecipientName: "Please enter the HipChat mention name of your pair partner:" 10 | 11 | checkConfig: -> 12 | if @session.missingHipChatKeys() 13 | atom.notifications.addError("Please set your HipChat keys.") 14 | false 15 | else 16 | true 17 | 18 | getHipChat: -> 19 | new HipChat(@session.hc_key) 20 | 21 | send: (done) -> 22 | collaboratorsArray = @recipient.match(/\w+/g) 23 | collaboratorsString = _.map(collaboratorsArray, (collaborator) -> 24 | "@" + collaborator unless collaborator[0] is "@" 25 | ).join(", ") 26 | 27 | hc_client = @getHipChat() 28 | 29 | hc_client.listRooms (data) => 30 | try 31 | room_id = _.findWhere(data.rooms, {name: @session.room_name}).room_id 32 | catch error 33 | atom.notifications.addError("Something went wrong. Please check your HipChat keys.") 34 | return 35 | 36 | params = 37 | room: room_id 38 | from: 'AtomPair' 39 | message: "Hello there #{collaboratorsString}. You have been invited to a pairing session. If you haven't installed the AtomPair plugin, type \`apm install atom-pair\` into your terminal. Go onto Atom, hit 'Join a pairing session', and enter this string: #{@session.id}" 40 | message_format: 'text' 41 | 42 | hc_client.postMessage params, (data) => 43 | if collaboratorsArray.length > 1 then verb = "have" else verb = "has" 44 | atom.notifications.addInfo("#{collaboratorsString} #{verb} been sent an invitation. Hold tight!") 45 | done() 46 | -------------------------------------------------------------------------------- /lib/modules/invitations/invitation.coffee: -------------------------------------------------------------------------------- 1 | InputView = require '../../views/input-view' 2 | User = require '../user' 3 | 4 | module.exports = 5 | class Invitation 6 | 7 | constructor: (@session) -> 8 | @invite() 9 | 10 | configPresent: -> 11 | @session.getKeysFromConfig() 12 | if @session.missingPusherKeys() 13 | atom.notifications.addError('Please set your Pusher keys.') 14 | return false 15 | if @checkConfig then @checkConfig() else true 16 | 17 | getRecipientName: (cta, callback)-> 18 | inviteView = new InputView(cta) 19 | inviteView.miniEditor.focus() 20 | atom.commands.add inviteView.element, 'core:confirm': => 21 | @recipient = inviteView.miniEditor.getText() 22 | inviteView.panel.hide() 23 | callback() 24 | 25 | afterSend: -> 26 | User.addMe() unless User.me 27 | @session.pairingSetup() 28 | 29 | invite: -> 30 | return unless @configPresent() 31 | if @needsInput 32 | @getRecipientName @askRecipientName, => @send => @afterSend() 33 | else 34 | atom.clipboard.write(@session.id) 35 | atom.notifications.addInfo "Your session ID has been copied to your clipboard." 36 | @afterSend() unless @session.active 37 | -------------------------------------------------------------------------------- /lib/modules/invitations/slack_invitation.coffee: -------------------------------------------------------------------------------- 1 | Invitation = require './invitation' 2 | Slack = require 'slack-node' 3 | 4 | module.exports = 5 | class SlackInvitation extends Invitation 6 | 7 | needsInput:true 8 | askRecipientName: "Please enter the Slack name of your pair partner (or channel name):" 9 | 10 | checkConfig: -> 11 | if @session.missingSlackWebHook() 12 | atom.notifications.addError("Please set your Slack Incoming WebHook") 13 | false 14 | else 15 | true 16 | 17 | getSlack: -> new Slack() 18 | 19 | send: (done)-> 20 | slack = @getSlack() 21 | slack.setWebhook @session.slack_url 22 | params = 23 | text: "Hello there #{@recipient}. You have been invited to a pairing session. If you haven't installed the AtomPair plugin, type \`apm install atom-pair\` into your terminal. Go onto Atom, hit 'Join a pairing session', and enter this string: #{@session.id}" 24 | channel: @recipient 25 | username: 'AtomPair' 26 | icon_emoji: ':couple_with_heart:' 27 | slack.webhook params, (err, response) => 28 | atom.notifications.addInfo("#{@recipient} has been sent an invitation. Hold tight!") 29 | done() 30 | -------------------------------------------------------------------------------- /lib/modules/message_queue.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | 3 | module.exports = 4 | class MessageQueue 5 | 6 | constructor: (@pusher) -> 7 | @items = [] 8 | @cycle() 9 | 10 | cycle: -> 11 | @interval = setInterval(=> 12 | if @items.length > 0 13 | item = @items.shift() 14 | @pusher.channel(item.channel).trigger(item.event, item.payload) 15 | , 120) 16 | 17 | dispose: -> 18 | clearInterval(@interval) 19 | @items = [] 20 | 21 | add: (channel, event, payload) -> 22 | lastItem = @items[@items.length - 1] 23 | if lastItem and lastItem.channel is channel and lastItem.event is event is 'client-change' 24 | item = { 25 | event: event, 26 | channel: channel, 27 | payload: _.flatten([lastItem.payload, payload]) 28 | } 29 | @items[@items.length - 1] = item 30 | else 31 | item = {channel: channel, event: event, payload: payload} 32 | @items.push(item) 33 | -------------------------------------------------------------------------------- /lib/modules/presence_indicator.coffee: -------------------------------------------------------------------------------- 1 | $ = require 'jquery' 2 | _ = require 'underscore' 3 | 4 | module.exports = PresenceIndicator = 5 | timeouts: [] 6 | 7 | markRows: (rows, colour) -> 8 | _.each rows, (row) => @addMarker(row, colour) 9 | 10 | clearMarkers: (colour) -> 11 | $("atom-text-editor#AtomPair::shadow .line-number").each (index, line) => 12 | $(line).removeClass(colour) 13 | 14 | addMarker: (line, colour) -> 15 | element = $("atom-text-editor#AtomPair::shadow .line-number-#{line}") 16 | if element.length is 0 17 | @timeouts.push(setTimeout((=> @addMarker(line,colour)), 50)) 18 | else 19 | _.each @timeouts, (timeout) -> clearTimeout(timeout) 20 | element.addClass(colour) 21 | 22 | updateCollaboratorMarker: (colour, rows) -> 23 | @clearMarkers(colour) 24 | @markRows(rows, colour) 25 | 26 | setActiveIcon: (tab, colour)-> 27 | $('.atom-pair-active-icon').remove() 28 | icon = $("") 29 | tab.itemTitle.appendChild(icon[0]) 30 | -------------------------------------------------------------------------------- /lib/modules/session.coffee: -------------------------------------------------------------------------------- 1 | require '../pusher/pusher' 2 | require '../pusher/pusher-js-client-auth' 3 | {CompositeDisposable, Emitter} = require 'atom' 4 | MessageQueue = require './message_queue' 5 | SharePane = require './share_pane' 6 | User = require './user' 7 | InputView = require '../views/input-view' 8 | randomstring = require 'randomstring' 9 | _ = require 'underscore' 10 | 11 | module.exports = 12 | class Session 13 | 14 | @initiate: (invitationMethod)-> 15 | session = @active ? new Session 16 | new invitationMethod(session) 17 | session 18 | 19 | @fromID: (id) -> 20 | keys = id.split("-") 21 | [app_key, app_secret] = [keys[0], keys[1]] 22 | new Session(id, app_key, app_secret) 23 | 24 | @join: -> 25 | if @active 26 | atom.notifications.addError "It looks like you are already in a pairing session. Please open a new window (cmd+shift+N) to start/join a new one." 27 | return 28 | joinView = new InputView("Enter the session ID here:") 29 | 30 | joinView.onInput (text) => 31 | session = Session.fromID(text) 32 | joinView.panel.hide() 33 | session.pairingSetup() 34 | 35 | constructor: (@id, @app_key, @app_secret)-> 36 | @getKeysFromConfig() 37 | @id ?= "#{@app_key}-#{@app_secret}-#{randomstring.generate(11)}" 38 | @triggerPush = @engageTabListener = true 39 | @subscriptions = new CompositeDisposable 40 | if SharePane.globalEmitter.disposed then SharePane.globalEmitter = new Emitter 41 | 42 | end: -> 43 | @pusher.disconnect() 44 | _.each @friendColours, (colour) => SharePane.each (pane) -> pane.clearMarkers(colour) 45 | User.clear() 46 | SharePane.clear() 47 | @subscriptions.dispose() 48 | @queue.dispose() 49 | @id = null 50 | @active = false 51 | @constructor.active = null 52 | atom.notifications.addWarning("You have been disconnected.") 53 | 54 | pairingSetup: -> 55 | @connectToPusher() 56 | @getExistingMembers() 57 | 58 | connectToPusher: -> 59 | colour = User.me?.colour 60 | arrivalTime = User.me?.arrivalTime 61 | 62 | @pusher = new Pusher @app_key, 63 | encrypted: true 64 | authTransport: 'client' 65 | clientAuth: 66 | key: @app_key 67 | secret: @app_secret 68 | user_id: colour || "blank" 69 | user_info: 70 | arrivalTime: arrivalTime || "blank" 71 | @queue = new MessageQueue(@pusher) 72 | @channel = @pusher.subscribe("presence-session-#{@id}") 73 | 74 | getExistingMembers: -> 75 | @channel.bind 'pusher:subscription_succeeded', (members) => 76 | members.each (member) -> 77 | return if User.withColour(member.id) or member.id is "blank" 78 | User.add(member.id, member.arrivalTime) 79 | _.each User.allButMe(), (user) -> 80 | SharePane.each (pane) -> user.updatePosition(pane.getTab(), [0]) 81 | return @resubscribe() unless User.me 82 | @startPairing() 83 | 84 | resubscribe: -> 85 | @channel.unsubscribe() 86 | @queue.dispose() 87 | User.addMe() 88 | @pairingSetup() 89 | 90 | createSharePane: (editor, id, title) -> 91 | new SharePane({ 92 | editor: editor, 93 | pusher: @pusher, 94 | sessionId: @id, 95 | queue: @queue, 96 | id: id, 97 | title: title 98 | }) 99 | 100 | ensureActiveTextEditor: (fn)-> 101 | editor = atom.workspace.getActiveTextEditor() 102 | if !editor 103 | @engageTabListener = false 104 | atom.workspace.open().then (editor)=> 105 | @engageTabListener = true 106 | fn(editor) 107 | else 108 | @engageTabListener = true 109 | fn(editor) 110 | 111 | shareOpenPanes: -> 112 | @ensureActiveTextEditor => 113 | _.each atom.workspace.getTextEditors(), (editor) => @createSharePane(editor) 114 | 115 | setActive: -> 116 | @active = true 117 | @constructor.active = @ 118 | 119 | startPairing: -> 120 | @setActive() 121 | 122 | @subscriptions.add atom.commands.add 'atom-workspace', 'AtomPair:disconnect': => @end() 123 | if User.me.isLeader() then @shareOpenPanes() 124 | @subscriptions.add @listenForNewTab() 125 | 126 | @channel.bind 'client-i-made-a-share-pane',(data) => 127 | return unless data.to is User.me.colour or data.to is 'all' 128 | sharePane = SharePane.id(data.paneId) 129 | sharePane.shareFile() 130 | sharePane.sendGrammar() 131 | 132 | @channel.bind 'client-please-make-a-share-pane', (data) => 133 | return unless data.to is User.me.colour or data.to is 'all' 134 | paneId = data.paneId 135 | title = data.title 136 | @engageTabListener = false 137 | atom.workspace.open().then (editor)=> 138 | pane = @createSharePane(editor, paneId, title) 139 | @queue.add(@channel.name, 'client-i-made-a-share-pane', {to: data.from, paneId: paneId}) 140 | @engageTabListener = true 141 | 142 | @channel.bind 'pusher:member_added', (member) => 143 | atom.notifications.addSuccess "Your pair buddy has joined the session." 144 | User.add(member.id, member.arrivalTime) 145 | return unless User.me.isLeader() 146 | SharePane.each (sharePane) => 147 | @queue.add(@channel.name, 'client-please-make-a-share-pane', { 148 | to: member.id, 149 | from: User.me.colour, 150 | paneId: sharePane.id, 151 | title: sharePane.editor.getTitle() 152 | }) 153 | User.withColour(member.id).updatePosition(sharePane.getTab(), [0]) 154 | 155 | @channel.bind 'pusher:member_removed', (member) => 156 | user = User.withColour(member.id) 157 | user.clearIndicators() 158 | user.remove() 159 | atom.notifications.addWarning('Your pair buddy has left the session.') 160 | 161 | @listenForDestruction() 162 | 163 | listenForNewTab: -> 164 | atom.workspace.onDidOpen (e) => 165 | return unless @engageTabListener 166 | editor = e.item 167 | return unless editor.constructor.name is "TextEditor" 168 | sharePane = @createSharePane(editor) 169 | @queue.add(@channel.name, 'client-please-make-a-share-pane', { 170 | to: 'all', 171 | from: User.me.colour, 172 | paneId: sharePane.id, 173 | title: editor.getTitle() 174 | }) 175 | 176 | listenForDestruction: -> 177 | SharePane.globalEmitter.on 'disconnected', => 178 | if (_.all SharePane.all, (pane) => !pane.connected) then @end() 179 | 180 | getKeysFromConfig: -> 181 | @app_key ?= atom.config.get 'atom-pair.pusher_app_key' 182 | @app_secret ?= atom.config.get 'atom-pair.pusher_app_secret' 183 | @hc_key ?= atom.config.get 'atom-pair.hipchat_token' 184 | @room_name ?= atom.config.get 'atom-pair.hipchat_room_name' 185 | @slack_url ?= atom.config.get 'atom-pair.slack_url' 186 | 187 | missingPusherKeys: -> _.any([@app_key, @app_secret], @missing) 188 | missingHipChatKeys: -> _.any([@hc_key, @room_name], @missing) 189 | missingSlackWebHook: -> _.any([@slack_url], @missing) 190 | missing: (key) -> key is '' || typeof(key) is "undefined" 191 | -------------------------------------------------------------------------------- /lib/modules/share_pane.coffee: -------------------------------------------------------------------------------- 1 | randomstring = require 'randomstring' 2 | GrammarSync = null 3 | chunkString = null 4 | User = require './user' 5 | 6 | {CompositeDisposable, Range, Emitter} = require 'atom' 7 | _ = require 'underscore' 8 | $ = require 'jquery' 9 | 10 | module.exports = 11 | class SharePane 12 | 13 | @all: [] 14 | 15 | @id: (id) -> _.findWhere(@all,{id: id}) 16 | @each: (fn) -> _.each(@all, fn) 17 | @any: (fn)-> _.any(@all, fn) 18 | 19 | @globalEmitter: new Emitter 20 | 21 | @clear: -> 22 | @all = [] 23 | @globalEmitter.dispose() 24 | 25 | constructor: (options) -> 26 | _.extend(@, options) 27 | if @editor.constructor.name isnt "TextEditor" then throw("editor is of type #{@editor.constructor.name}") 28 | @buffer = @editor.buffer 29 | if !@buffer then throw("buffer is nil. editor: #{@editor}") 30 | 31 | @id ?= randomstring.generate(6) 32 | @triggerPush = true 33 | 34 | @editorListeners = new CompositeDisposable 35 | 36 | if @title 37 | @setTabTitle() 38 | @persistTabTitle() 39 | 40 | atom.views.getView(@editor).setAttribute('id', 'AtomPair') 41 | 42 | GrammarSync = require './grammar_sync' 43 | chunkString = require '../helpers/chunk-string' 44 | 45 | _.extend(@, GrammarSync) 46 | @constructor.all.push(@) 47 | @subscribe() 48 | @activate() 49 | 50 | subscribe: -> 51 | channelName = "presence-session-#{@sessionId}-#{@id}" 52 | @channel = @pusher.subscribe(channelName) 53 | @connected = true 54 | 55 | activate: -> 56 | @channel.bind 'client-grammar-sync', (syntax) => 57 | grammar = atom.grammars.grammarForScopeName(syntax) 58 | @editor.setGrammar(grammar) 59 | 60 | @channel.bind 'client-share-whole-file', (file) => 61 | @withoutTrigger => @buffer.setText(file) 62 | 63 | @channel.bind 'client-share-partial-file', (chunk) => 64 | @withoutTrigger => @buffer.append(chunk) 65 | 66 | @channel.bind 'client-change', (events) => 67 | _.each events, (event) => 68 | @changeBuffer(event) 69 | 70 | @channel.bind 'client-buffer-selection', (event) => 71 | User.withColour(event.colour).updatePosition(@getTab(), event.rows) 72 | 73 | @editorListeners.add @listenToBufferChanges() 74 | @editorListeners.add @syncSelectionRange() 75 | @editorListeners.add @syncGrammars() 76 | 77 | @listenForDestruction() 78 | 79 | setTabTitle: -> 80 | tab = @getTab() 81 | tab.itemTitle.innerText = @title 82 | 83 | persistTabTitle: -> 84 | openListener = atom.workspace.onDidOpen => @setTabTitle() 85 | closeListener = @constructor.globalEmitter.on 'disconnected', => @setTabTitle() 86 | @editorListeners.add(openListener) 87 | @editorListeners.add(closeListener) 88 | 89 | disconnect: -> 90 | @channel.unsubscribe() 91 | @editorListeners.dispose() 92 | @connected = false 93 | atom.views.getView(@editor)?.removeAttribute('id') 94 | $('.atom-pair-active-icon').remove() 95 | @editor = @buffer = null 96 | @constructor.globalEmitter.emit('disconnected') 97 | 98 | listenForDestruction: -> 99 | @editorListeners.add @buffer.onDidDestroy => @disconnect() 100 | @editorListeners.add @editor.onDidDestroy => @disconnect() 101 | 102 | withoutTrigger: (callback) -> 103 | @triggerPush = false 104 | callback() 105 | @triggerPush = true 106 | 107 | listenToBufferChanges: -> 108 | @buffer.onDidChange (event) => 109 | return unless @triggerPush 110 | 111 | if event.newText is event.oldText and _.isEqual(event.oldRange, event.newRange) 112 | return 113 | 114 | if !(event.newText is "\n") and (event.newText.length is 0) 115 | changeType = 'deletion' 116 | event = {oldRange: event.oldRange} 117 | else if event.oldRange.containsRange(event.newRange) or event.newRange.containsRange(event.oldRange) 118 | changeType = 'substitution' 119 | event = {oldRange: event.oldRange, newRange: event.newRange, newText: event.newText} 120 | else 121 | changeType = 'insertion' 122 | event = {newRange: event.newRange, newText: event.newText} 123 | 124 | if event.newText and event.newText.length > 800 125 | @shareFile() 126 | else 127 | event = {changeType: changeType, event: event, colour: User.me.colour} 128 | @queue.add(@channel.name, 'client-change', [event]) 129 | 130 | changeBuffer: (data) -> 131 | if data.event.newRange then newRange = Range.fromObject(data.event.newRange) 132 | if data.event.oldRange then oldRange = Range.fromObject(data.event.oldRange) 133 | if data.event.newText then newText = data.event.newText 134 | 135 | @withoutTrigger => 136 | switch data.changeType 137 | when 'deletion' 138 | @buffer.delete oldRange 139 | actionArea = oldRange.start 140 | when 'substitution' 141 | @buffer.setTextInRange oldRange, newText 142 | actionArea = oldRange.start 143 | else 144 | @buffer.insert newRange.start, newText 145 | actionArea = newRange.start 146 | User.withColour(data.colour).updatePosition(@getTab(), [actionArea.toArray()[0]]) 147 | 148 | getTab: -> 149 | tabs = $('li[is="tabs-tab"]') 150 | tab = (t for t in tabs when t.item.id is @editor.id)[0] 151 | tab 152 | 153 | syncSelectionRange: -> 154 | @editor.onDidChangeSelectionRange (event) => 155 | rows = event.newBufferRange.getRows() 156 | return unless rows.length > 1 157 | @queue.add(@channel.name, 'client-buffer-selection', {colour: User.me.colour, rows: rows}) 158 | 159 | shareFile: -> 160 | currentFile = @buffer.getText() 161 | return if currentFile.length is 0 162 | 163 | if currentFile.length < 950 164 | @queue.add(@channel.name, 'client-share-whole-file', currentFile) 165 | else 166 | chunks = chunkString(currentFile, 950) 167 | _.each chunks, (chunk, index) => @queue.add @channel.name, 'client-share-partial-file', chunk 168 | -------------------------------------------------------------------------------- /lib/modules/user.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | PresenceIndicator = require './presence_indicator' 3 | 4 | module.exports = 5 | class User 6 | 7 | @colours: require('../helpers/colour-list') 8 | 9 | @clear: -> 10 | @me = null 11 | @all = [] 12 | 13 | @availableColours: -> 14 | _.reject @colours, (colour) => _.any @all, (user) -> user.colour is colour 15 | 16 | @nextAvailableColour: -> 17 | @availableColours()[0] 18 | 19 | @all: [] 20 | 21 | @withColour: (colour) -> 22 | _.findWhere @all, {colour: colour} 23 | 24 | @allButMe: -> 25 | _.reject @all, (user) -> user is User.me 26 | 27 | @addMe: -> 28 | @me = @add(@nextAvailableColour()) 29 | 30 | @add: (colour = @nextAvailableColour(), arrivalTime = new Date().getTime())-> 31 | user = new User(colour, arrivalTime) 32 | @all.push(user) 33 | user 34 | 35 | @remove: (colour)-> 36 | @all = _.reject @all, (user) -> user.colour is colour 37 | 38 | @me: null 39 | 40 | @clearIndicators: -> 41 | _.each User.all, (user) => user.clearIndicators() 42 | 43 | constructor: (@colour, @arrivalTime)-> 44 | 45 | isLeader: -> 46 | leader = _.sortBy(@constructor.all, 'arrivalTime')[0] 47 | @arrivalTime is leader.arrivalTime 48 | 49 | remove: -> 50 | User.remove(@colour) 51 | 52 | clearIndicators: -> 53 | PresenceIndicator.clearMarkers(@colour) 54 | 55 | updatePosition: (tab, rows)-> 56 | PresenceIndicator.updateCollaboratorMarker(@colour, rows) 57 | PresenceIndicator.setActiveIcon(tab, @colour) 58 | -------------------------------------------------------------------------------- /lib/pusher/pusher-js-client-auth.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 123 | * @license MIT 124 | */ 125 | 126 | var base64 = require('base64-js') 127 | var ieee754 = require('ieee754') 128 | 129 | exports.Buffer = Buffer 130 | exports.SlowBuffer = Buffer 131 | exports.INSPECT_MAX_BYTES = 50 132 | Buffer.poolSize = 8192 133 | 134 | /** 135 | * If `Buffer._useTypedArrays`: 136 | * === true Use Uint8Array implementation (fastest) 137 | * === false Use Object implementation (compatible down to IE6) 138 | */ 139 | Buffer._useTypedArrays = (function () { 140 | // Detect if browser supports Typed Arrays. Supported browsers are IE 10+, Firefox 4+, 141 | // Chrome 7+, Safari 5.1+, Opera 11.6+, iOS 4.2+. If the browser does not support adding 142 | // properties to `Uint8Array` instances, then that's the same as no `Uint8Array` support 143 | // because we need to be able to add all the node Buffer API methods. This is an issue 144 | // in Firefox 4-29. Now fixed: https://bugzilla.mozilla.org/show_bug.cgi?id=695438 145 | try { 146 | var buf = new ArrayBuffer(0) 147 | var arr = new Uint8Array(buf) 148 | arr.foo = function () { return 42 } 149 | return 42 === arr.foo() && 150 | typeof arr.subarray === 'function' // Chrome 9-10 lack `subarray` 151 | } catch (e) { 152 | return false 153 | } 154 | })() 155 | 156 | /** 157 | * Class: Buffer 158 | * ============= 159 | * 160 | * The Buffer constructor returns instances of `Uint8Array` that are augmented 161 | * with function properties for all the node `Buffer` API functions. We use 162 | * `Uint8Array` so that square bracket notation works as expected -- it returns 163 | * a single octet. 164 | * 165 | * By augmenting the instances, we can avoid modifying the `Uint8Array` 166 | * prototype. 167 | */ 168 | function Buffer (subject, encoding, noZero) { 169 | if (!(this instanceof Buffer)) 170 | return new Buffer(subject, encoding, noZero) 171 | 172 | var type = typeof subject 173 | 174 | // Workaround: node's base64 implementation allows for non-padded strings 175 | // while base64-js does not. 176 | if (encoding === 'base64' && type === 'string') { 177 | subject = stringtrim(subject) 178 | while (subject.length % 4 !== 0) { 179 | subject = subject + '=' 180 | } 181 | } 182 | 183 | // Find the length 184 | var length 185 | if (type === 'number') 186 | length = coerce(subject) 187 | else if (type === 'string') 188 | length = Buffer.byteLength(subject, encoding) 189 | else if (type === 'object') 190 | length = coerce(subject.length) // assume that object is array-like 191 | else 192 | throw new Error('First argument needs to be a number, array or string.') 193 | 194 | var buf 195 | if (Buffer._useTypedArrays) { 196 | // Preferred: Return an augmented `Uint8Array` instance for best performance 197 | buf = Buffer._augment(new Uint8Array(length)) 198 | } else { 199 | // Fallback: Return THIS instance of Buffer (created by `new`) 200 | buf = this 201 | buf.length = length 202 | buf._isBuffer = true 203 | } 204 | 205 | var i 206 | if (Buffer._useTypedArrays && typeof subject.byteLength === 'number') { 207 | // Speed optimization -- use set if we're copying from a typed array 208 | buf._set(subject) 209 | } else if (isArrayish(subject)) { 210 | // Treat array-ish objects as a byte array 211 | for (i = 0; i < length; i++) { 212 | if (Buffer.isBuffer(subject)) 213 | buf[i] = subject.readUInt8(i) 214 | else 215 | buf[i] = subject[i] 216 | } 217 | } else if (type === 'string') { 218 | buf.write(subject, 0, encoding) 219 | } else if (type === 'number' && !Buffer._useTypedArrays && !noZero) { 220 | for (i = 0; i < length; i++) { 221 | buf[i] = 0 222 | } 223 | } 224 | 225 | return buf 226 | } 227 | 228 | // STATIC METHODS 229 | // ============== 230 | 231 | Buffer.isEncoding = function (encoding) { 232 | switch (String(encoding).toLowerCase()) { 233 | case 'hex': 234 | case 'utf8': 235 | case 'utf-8': 236 | case 'ascii': 237 | case 'binary': 238 | case 'base64': 239 | case 'raw': 240 | case 'ucs2': 241 | case 'ucs-2': 242 | case 'utf16le': 243 | case 'utf-16le': 244 | return true 245 | default: 246 | return false 247 | } 248 | } 249 | 250 | Buffer.isBuffer = function (b) { 251 | return !!(b !== null && b !== undefined && b._isBuffer) 252 | } 253 | 254 | Buffer.byteLength = function (str, encoding) { 255 | var ret 256 | str = str + '' 257 | switch (encoding || 'utf8') { 258 | case 'hex': 259 | ret = str.length / 2 260 | break 261 | case 'utf8': 262 | case 'utf-8': 263 | ret = utf8ToBytes(str).length 264 | break 265 | case 'ascii': 266 | case 'binary': 267 | case 'raw': 268 | ret = str.length 269 | break 270 | case 'base64': 271 | ret = base64ToBytes(str).length 272 | break 273 | case 'ucs2': 274 | case 'ucs-2': 275 | case 'utf16le': 276 | case 'utf-16le': 277 | ret = str.length * 2 278 | break 279 | default: 280 | throw new Error('Unknown encoding') 281 | } 282 | return ret 283 | } 284 | 285 | Buffer.concat = function (list, totalLength) { 286 | assert(isArray(list), 'Usage: Buffer.concat(list, [totalLength])\n' + 287 | 'list should be an Array.') 288 | 289 | if (list.length === 0) { 290 | return new Buffer(0) 291 | } else if (list.length === 1) { 292 | return list[0] 293 | } 294 | 295 | var i 296 | if (typeof totalLength !== 'number') { 297 | totalLength = 0 298 | for (i = 0; i < list.length; i++) { 299 | totalLength += list[i].length 300 | } 301 | } 302 | 303 | var buf = new Buffer(totalLength) 304 | var pos = 0 305 | for (i = 0; i < list.length; i++) { 306 | var item = list[i] 307 | item.copy(buf, pos) 308 | pos += item.length 309 | } 310 | return buf 311 | } 312 | 313 | // BUFFER INSTANCE METHODS 314 | // ======================= 315 | 316 | function _hexWrite (buf, string, offset, length) { 317 | offset = Number(offset) || 0 318 | var remaining = buf.length - offset 319 | if (!length) { 320 | length = remaining 321 | } else { 322 | length = Number(length) 323 | if (length > remaining) { 324 | length = remaining 325 | } 326 | } 327 | 328 | // must be an even number of digits 329 | var strLen = string.length 330 | assert(strLen % 2 === 0, 'Invalid hex string') 331 | 332 | if (length > strLen / 2) { 333 | length = strLen / 2 334 | } 335 | for (var i = 0; i < length; i++) { 336 | var byte = parseInt(string.substr(i * 2, 2), 16) 337 | assert(!isNaN(byte), 'Invalid hex string') 338 | buf[offset + i] = byte 339 | } 340 | Buffer._charsWritten = i * 2 341 | return i 342 | } 343 | 344 | function _utf8Write (buf, string, offset, length) { 345 | var charsWritten = Buffer._charsWritten = 346 | blitBuffer(utf8ToBytes(string), buf, offset, length) 347 | return charsWritten 348 | } 349 | 350 | function _asciiWrite (buf, string, offset, length) { 351 | var charsWritten = Buffer._charsWritten = 352 | blitBuffer(asciiToBytes(string), buf, offset, length) 353 | return charsWritten 354 | } 355 | 356 | function _binaryWrite (buf, string, offset, length) { 357 | return _asciiWrite(buf, string, offset, length) 358 | } 359 | 360 | function _base64Write (buf, string, offset, length) { 361 | var charsWritten = Buffer._charsWritten = 362 | blitBuffer(base64ToBytes(string), buf, offset, length) 363 | return charsWritten 364 | } 365 | 366 | function _utf16leWrite (buf, string, offset, length) { 367 | var charsWritten = Buffer._charsWritten = 368 | blitBuffer(utf16leToBytes(string), buf, offset, length) 369 | return charsWritten 370 | } 371 | 372 | Buffer.prototype.write = function (string, offset, length, encoding) { 373 | // Support both (string, offset, length, encoding) 374 | // and the legacy (string, encoding, offset, length) 375 | if (isFinite(offset)) { 376 | if (!isFinite(length)) { 377 | encoding = length 378 | length = undefined 379 | } 380 | } else { // legacy 381 | var swap = encoding 382 | encoding = offset 383 | offset = length 384 | length = swap 385 | } 386 | 387 | offset = Number(offset) || 0 388 | var remaining = this.length - offset 389 | if (!length) { 390 | length = remaining 391 | } else { 392 | length = Number(length) 393 | if (length > remaining) { 394 | length = remaining 395 | } 396 | } 397 | encoding = String(encoding || 'utf8').toLowerCase() 398 | 399 | var ret 400 | switch (encoding) { 401 | case 'hex': 402 | ret = _hexWrite(this, string, offset, length) 403 | break 404 | case 'utf8': 405 | case 'utf-8': 406 | ret = _utf8Write(this, string, offset, length) 407 | break 408 | case 'ascii': 409 | ret = _asciiWrite(this, string, offset, length) 410 | break 411 | case 'binary': 412 | ret = _binaryWrite(this, string, offset, length) 413 | break 414 | case 'base64': 415 | ret = _base64Write(this, string, offset, length) 416 | break 417 | case 'ucs2': 418 | case 'ucs-2': 419 | case 'utf16le': 420 | case 'utf-16le': 421 | ret = _utf16leWrite(this, string, offset, length) 422 | break 423 | default: 424 | throw new Error('Unknown encoding') 425 | } 426 | return ret 427 | } 428 | 429 | Buffer.prototype.toString = function (encoding, start, end) { 430 | var self = this 431 | 432 | encoding = String(encoding || 'utf8').toLowerCase() 433 | start = Number(start) || 0 434 | end = (end !== undefined) 435 | ? Number(end) 436 | : end = self.length 437 | 438 | // Fastpath empty strings 439 | if (end === start) 440 | return '' 441 | 442 | var ret 443 | switch (encoding) { 444 | case 'hex': 445 | ret = _hexSlice(self, start, end) 446 | break 447 | case 'utf8': 448 | case 'utf-8': 449 | ret = _utf8Slice(self, start, end) 450 | break 451 | case 'ascii': 452 | ret = _asciiSlice(self, start, end) 453 | break 454 | case 'binary': 455 | ret = _binarySlice(self, start, end) 456 | break 457 | case 'base64': 458 | ret = _base64Slice(self, start, end) 459 | break 460 | case 'ucs2': 461 | case 'ucs-2': 462 | case 'utf16le': 463 | case 'utf-16le': 464 | ret = _utf16leSlice(self, start, end) 465 | break 466 | default: 467 | throw new Error('Unknown encoding') 468 | } 469 | return ret 470 | } 471 | 472 | Buffer.prototype.toJSON = function () { 473 | return { 474 | type: 'Buffer', 475 | data: Array.prototype.slice.call(this._arr || this, 0) 476 | } 477 | } 478 | 479 | // copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length) 480 | Buffer.prototype.copy = function (target, target_start, start, end) { 481 | var source = this 482 | 483 | if (!start) start = 0 484 | if (!end && end !== 0) end = this.length 485 | if (!target_start) target_start = 0 486 | 487 | // Copy 0 bytes; we're done 488 | if (end === start) return 489 | if (target.length === 0 || source.length === 0) return 490 | 491 | // Fatal error conditions 492 | assert(end >= start, 'sourceEnd < sourceStart') 493 | assert(target_start >= 0 && target_start < target.length, 494 | 'targetStart out of bounds') 495 | assert(start >= 0 && start < source.length, 'sourceStart out of bounds') 496 | assert(end >= 0 && end <= source.length, 'sourceEnd out of bounds') 497 | 498 | // Are we oob? 499 | if (end > this.length) 500 | end = this.length 501 | if (target.length - target_start < end - start) 502 | end = target.length - target_start + start 503 | 504 | var len = end - start 505 | 506 | if (len < 100 || !Buffer._useTypedArrays) { 507 | for (var i = 0; i < len; i++) 508 | target[i + target_start] = this[i + start] 509 | } else { 510 | target._set(this.subarray(start, start + len), target_start) 511 | } 512 | } 513 | 514 | function _base64Slice (buf, start, end) { 515 | if (start === 0 && end === buf.length) { 516 | return base64.fromByteArray(buf) 517 | } else { 518 | return base64.fromByteArray(buf.slice(start, end)) 519 | } 520 | } 521 | 522 | function _utf8Slice (buf, start, end) { 523 | var res = '' 524 | var tmp = '' 525 | end = Math.min(buf.length, end) 526 | 527 | for (var i = start; i < end; i++) { 528 | if (buf[i] <= 0x7F) { 529 | res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i]) 530 | tmp = '' 531 | } else { 532 | tmp += '%' + buf[i].toString(16) 533 | } 534 | } 535 | 536 | return res + decodeUtf8Char(tmp) 537 | } 538 | 539 | function _asciiSlice (buf, start, end) { 540 | var ret = '' 541 | end = Math.min(buf.length, end) 542 | 543 | for (var i = start; i < end; i++) 544 | ret += String.fromCharCode(buf[i]) 545 | return ret 546 | } 547 | 548 | function _binarySlice (buf, start, end) { 549 | return _asciiSlice(buf, start, end) 550 | } 551 | 552 | function _hexSlice (buf, start, end) { 553 | var len = buf.length 554 | 555 | if (!start || start < 0) start = 0 556 | if (!end || end < 0 || end > len) end = len 557 | 558 | var out = '' 559 | for (var i = start; i < end; i++) { 560 | out += toHex(buf[i]) 561 | } 562 | return out 563 | } 564 | 565 | function _utf16leSlice (buf, start, end) { 566 | var bytes = buf.slice(start, end) 567 | var res = '' 568 | for (var i = 0; i < bytes.length; i += 2) { 569 | res += String.fromCharCode(bytes[i] + bytes[i+1] * 256) 570 | } 571 | return res 572 | } 573 | 574 | Buffer.prototype.slice = function (start, end) { 575 | var len = this.length 576 | start = clamp(start, len, 0) 577 | end = clamp(end, len, len) 578 | 579 | if (Buffer._useTypedArrays) { 580 | return Buffer._augment(this.subarray(start, end)) 581 | } else { 582 | var sliceLen = end - start 583 | var newBuf = new Buffer(sliceLen, undefined, true) 584 | for (var i = 0; i < sliceLen; i++) { 585 | newBuf[i] = this[i + start] 586 | } 587 | return newBuf 588 | } 589 | } 590 | 591 | // `get` will be removed in Node 0.13+ 592 | Buffer.prototype.get = function (offset) { 593 | console.log('.get() is deprecated. Access using array indexes instead.') 594 | return this.readUInt8(offset) 595 | } 596 | 597 | // `set` will be removed in Node 0.13+ 598 | Buffer.prototype.set = function (v, offset) { 599 | console.log('.set() is deprecated. Access using array indexes instead.') 600 | return this.writeUInt8(v, offset) 601 | } 602 | 603 | Buffer.prototype.readUInt8 = function (offset, noAssert) { 604 | if (!noAssert) { 605 | assert(offset !== undefined && offset !== null, 'missing offset') 606 | assert(offset < this.length, 'Trying to read beyond buffer length') 607 | } 608 | 609 | if (offset >= this.length) 610 | return 611 | 612 | return this[offset] 613 | } 614 | 615 | function _readUInt16 (buf, offset, littleEndian, noAssert) { 616 | if (!noAssert) { 617 | assert(typeof littleEndian === 'boolean', 'missing or invalid endian') 618 | assert(offset !== undefined && offset !== null, 'missing offset') 619 | assert(offset + 1 < buf.length, 'Trying to read beyond buffer length') 620 | } 621 | 622 | var len = buf.length 623 | if (offset >= len) 624 | return 625 | 626 | var val 627 | if (littleEndian) { 628 | val = buf[offset] 629 | if (offset + 1 < len) 630 | val |= buf[offset + 1] << 8 631 | } else { 632 | val = buf[offset] << 8 633 | if (offset + 1 < len) 634 | val |= buf[offset + 1] 635 | } 636 | return val 637 | } 638 | 639 | Buffer.prototype.readUInt16LE = function (offset, noAssert) { 640 | return _readUInt16(this, offset, true, noAssert) 641 | } 642 | 643 | Buffer.prototype.readUInt16BE = function (offset, noAssert) { 644 | return _readUInt16(this, offset, false, noAssert) 645 | } 646 | 647 | function _readUInt32 (buf, offset, littleEndian, noAssert) { 648 | if (!noAssert) { 649 | assert(typeof littleEndian === 'boolean', 'missing or invalid endian') 650 | assert(offset !== undefined && offset !== null, 'missing offset') 651 | assert(offset + 3 < buf.length, 'Trying to read beyond buffer length') 652 | } 653 | 654 | var len = buf.length 655 | if (offset >= len) 656 | return 657 | 658 | var val 659 | if (littleEndian) { 660 | if (offset + 2 < len) 661 | val = buf[offset + 2] << 16 662 | if (offset + 1 < len) 663 | val |= buf[offset + 1] << 8 664 | val |= buf[offset] 665 | if (offset + 3 < len) 666 | val = val + (buf[offset + 3] << 24 >>> 0) 667 | } else { 668 | if (offset + 1 < len) 669 | val = buf[offset + 1] << 16 670 | if (offset + 2 < len) 671 | val |= buf[offset + 2] << 8 672 | if (offset + 3 < len) 673 | val |= buf[offset + 3] 674 | val = val + (buf[offset] << 24 >>> 0) 675 | } 676 | return val 677 | } 678 | 679 | Buffer.prototype.readUInt32LE = function (offset, noAssert) { 680 | return _readUInt32(this, offset, true, noAssert) 681 | } 682 | 683 | Buffer.prototype.readUInt32BE = function (offset, noAssert) { 684 | return _readUInt32(this, offset, false, noAssert) 685 | } 686 | 687 | Buffer.prototype.readInt8 = function (offset, noAssert) { 688 | if (!noAssert) { 689 | assert(offset !== undefined && offset !== null, 690 | 'missing offset') 691 | assert(offset < this.length, 'Trying to read beyond buffer length') 692 | } 693 | 694 | if (offset >= this.length) 695 | return 696 | 697 | var neg = this[offset] & 0x80 698 | if (neg) 699 | return (0xff - this[offset] + 1) * -1 700 | else 701 | return this[offset] 702 | } 703 | 704 | function _readInt16 (buf, offset, littleEndian, noAssert) { 705 | if (!noAssert) { 706 | assert(typeof littleEndian === 'boolean', 'missing or invalid endian') 707 | assert(offset !== undefined && offset !== null, 'missing offset') 708 | assert(offset + 1 < buf.length, 'Trying to read beyond buffer length') 709 | } 710 | 711 | var len = buf.length 712 | if (offset >= len) 713 | return 714 | 715 | var val = _readUInt16(buf, offset, littleEndian, true) 716 | var neg = val & 0x8000 717 | if (neg) 718 | return (0xffff - val + 1) * -1 719 | else 720 | return val 721 | } 722 | 723 | Buffer.prototype.readInt16LE = function (offset, noAssert) { 724 | return _readInt16(this, offset, true, noAssert) 725 | } 726 | 727 | Buffer.prototype.readInt16BE = function (offset, noAssert) { 728 | return _readInt16(this, offset, false, noAssert) 729 | } 730 | 731 | function _readInt32 (buf, offset, littleEndian, noAssert) { 732 | if (!noAssert) { 733 | assert(typeof littleEndian === 'boolean', 'missing or invalid endian') 734 | assert(offset !== undefined && offset !== null, 'missing offset') 735 | assert(offset + 3 < buf.length, 'Trying to read beyond buffer length') 736 | } 737 | 738 | var len = buf.length 739 | if (offset >= len) 740 | return 741 | 742 | var val = _readUInt32(buf, offset, littleEndian, true) 743 | var neg = val & 0x80000000 744 | if (neg) 745 | return (0xffffffff - val + 1) * -1 746 | else 747 | return val 748 | } 749 | 750 | Buffer.prototype.readInt32LE = function (offset, noAssert) { 751 | return _readInt32(this, offset, true, noAssert) 752 | } 753 | 754 | Buffer.prototype.readInt32BE = function (offset, noAssert) { 755 | return _readInt32(this, offset, false, noAssert) 756 | } 757 | 758 | function _readFloat (buf, offset, littleEndian, noAssert) { 759 | if (!noAssert) { 760 | assert(typeof littleEndian === 'boolean', 'missing or invalid endian') 761 | assert(offset + 3 < buf.length, 'Trying to read beyond buffer length') 762 | } 763 | 764 | return ieee754.read(buf, offset, littleEndian, 23, 4) 765 | } 766 | 767 | Buffer.prototype.readFloatLE = function (offset, noAssert) { 768 | return _readFloat(this, offset, true, noAssert) 769 | } 770 | 771 | Buffer.prototype.readFloatBE = function (offset, noAssert) { 772 | return _readFloat(this, offset, false, noAssert) 773 | } 774 | 775 | function _readDouble (buf, offset, littleEndian, noAssert) { 776 | if (!noAssert) { 777 | assert(typeof littleEndian === 'boolean', 'missing or invalid endian') 778 | assert(offset + 7 < buf.length, 'Trying to read beyond buffer length') 779 | } 780 | 781 | return ieee754.read(buf, offset, littleEndian, 52, 8) 782 | } 783 | 784 | Buffer.prototype.readDoubleLE = function (offset, noAssert) { 785 | return _readDouble(this, offset, true, noAssert) 786 | } 787 | 788 | Buffer.prototype.readDoubleBE = function (offset, noAssert) { 789 | return _readDouble(this, offset, false, noAssert) 790 | } 791 | 792 | Buffer.prototype.writeUInt8 = function (value, offset, noAssert) { 793 | if (!noAssert) { 794 | assert(value !== undefined && value !== null, 'missing value') 795 | assert(offset !== undefined && offset !== null, 'missing offset') 796 | assert(offset < this.length, 'trying to write beyond buffer length') 797 | verifuint(value, 0xff) 798 | } 799 | 800 | if (offset >= this.length) return 801 | 802 | this[offset] = value 803 | } 804 | 805 | function _writeUInt16 (buf, value, offset, littleEndian, noAssert) { 806 | if (!noAssert) { 807 | assert(value !== undefined && value !== null, 'missing value') 808 | assert(typeof littleEndian === 'boolean', 'missing or invalid endian') 809 | assert(offset !== undefined && offset !== null, 'missing offset') 810 | assert(offset + 1 < buf.length, 'trying to write beyond buffer length') 811 | verifuint(value, 0xffff) 812 | } 813 | 814 | var len = buf.length 815 | if (offset >= len) 816 | return 817 | 818 | for (var i = 0, j = Math.min(len - offset, 2); i < j; i++) { 819 | buf[offset + i] = 820 | (value & (0xff << (8 * (littleEndian ? i : 1 - i)))) >>> 821 | (littleEndian ? i : 1 - i) * 8 822 | } 823 | } 824 | 825 | Buffer.prototype.writeUInt16LE = function (value, offset, noAssert) { 826 | _writeUInt16(this, value, offset, true, noAssert) 827 | } 828 | 829 | Buffer.prototype.writeUInt16BE = function (value, offset, noAssert) { 830 | _writeUInt16(this, value, offset, false, noAssert) 831 | } 832 | 833 | function _writeUInt32 (buf, value, offset, littleEndian, noAssert) { 834 | if (!noAssert) { 835 | assert(value !== undefined && value !== null, 'missing value') 836 | assert(typeof littleEndian === 'boolean', 'missing or invalid endian') 837 | assert(offset !== undefined && offset !== null, 'missing offset') 838 | assert(offset + 3 < buf.length, 'trying to write beyond buffer length') 839 | verifuint(value, 0xffffffff) 840 | } 841 | 842 | var len = buf.length 843 | if (offset >= len) 844 | return 845 | 846 | for (var i = 0, j = Math.min(len - offset, 4); i < j; i++) { 847 | buf[offset + i] = 848 | (value >>> (littleEndian ? i : 3 - i) * 8) & 0xff 849 | } 850 | } 851 | 852 | Buffer.prototype.writeUInt32LE = function (value, offset, noAssert) { 853 | _writeUInt32(this, value, offset, true, noAssert) 854 | } 855 | 856 | Buffer.prototype.writeUInt32BE = function (value, offset, noAssert) { 857 | _writeUInt32(this, value, offset, false, noAssert) 858 | } 859 | 860 | Buffer.prototype.writeInt8 = function (value, offset, noAssert) { 861 | if (!noAssert) { 862 | assert(value !== undefined && value !== null, 'missing value') 863 | assert(offset !== undefined && offset !== null, 'missing offset') 864 | assert(offset < this.length, 'Trying to write beyond buffer length') 865 | verifsint(value, 0x7f, -0x80) 866 | } 867 | 868 | if (offset >= this.length) 869 | return 870 | 871 | if (value >= 0) 872 | this.writeUInt8(value, offset, noAssert) 873 | else 874 | this.writeUInt8(0xff + value + 1, offset, noAssert) 875 | } 876 | 877 | function _writeInt16 (buf, value, offset, littleEndian, noAssert) { 878 | if (!noAssert) { 879 | assert(value !== undefined && value !== null, 'missing value') 880 | assert(typeof littleEndian === 'boolean', 'missing or invalid endian') 881 | assert(offset !== undefined && offset !== null, 'missing offset') 882 | assert(offset + 1 < buf.length, 'Trying to write beyond buffer length') 883 | verifsint(value, 0x7fff, -0x8000) 884 | } 885 | 886 | var len = buf.length 887 | if (offset >= len) 888 | return 889 | 890 | if (value >= 0) 891 | _writeUInt16(buf, value, offset, littleEndian, noAssert) 892 | else 893 | _writeUInt16(buf, 0xffff + value + 1, offset, littleEndian, noAssert) 894 | } 895 | 896 | Buffer.prototype.writeInt16LE = function (value, offset, noAssert) { 897 | _writeInt16(this, value, offset, true, noAssert) 898 | } 899 | 900 | Buffer.prototype.writeInt16BE = function (value, offset, noAssert) { 901 | _writeInt16(this, value, offset, false, noAssert) 902 | } 903 | 904 | function _writeInt32 (buf, value, offset, littleEndian, noAssert) { 905 | if (!noAssert) { 906 | assert(value !== undefined && value !== null, 'missing value') 907 | assert(typeof littleEndian === 'boolean', 'missing or invalid endian') 908 | assert(offset !== undefined && offset !== null, 'missing offset') 909 | assert(offset + 3 < buf.length, 'Trying to write beyond buffer length') 910 | verifsint(value, 0x7fffffff, -0x80000000) 911 | } 912 | 913 | var len = buf.length 914 | if (offset >= len) 915 | return 916 | 917 | if (value >= 0) 918 | _writeUInt32(buf, value, offset, littleEndian, noAssert) 919 | else 920 | _writeUInt32(buf, 0xffffffff + value + 1, offset, littleEndian, noAssert) 921 | } 922 | 923 | Buffer.prototype.writeInt32LE = function (value, offset, noAssert) { 924 | _writeInt32(this, value, offset, true, noAssert) 925 | } 926 | 927 | Buffer.prototype.writeInt32BE = function (value, offset, noAssert) { 928 | _writeInt32(this, value, offset, false, noAssert) 929 | } 930 | 931 | function _writeFloat (buf, value, offset, littleEndian, noAssert) { 932 | if (!noAssert) { 933 | assert(value !== undefined && value !== null, 'missing value') 934 | assert(typeof littleEndian === 'boolean', 'missing or invalid endian') 935 | assert(offset !== undefined && offset !== null, 'missing offset') 936 | assert(offset + 3 < buf.length, 'Trying to write beyond buffer length') 937 | verifIEEE754(value, 3.4028234663852886e+38, -3.4028234663852886e+38) 938 | } 939 | 940 | var len = buf.length 941 | if (offset >= len) 942 | return 943 | 944 | ieee754.write(buf, value, offset, littleEndian, 23, 4) 945 | } 946 | 947 | Buffer.prototype.writeFloatLE = function (value, offset, noAssert) { 948 | _writeFloat(this, value, offset, true, noAssert) 949 | } 950 | 951 | Buffer.prototype.writeFloatBE = function (value, offset, noAssert) { 952 | _writeFloat(this, value, offset, false, noAssert) 953 | } 954 | 955 | function _writeDouble (buf, value, offset, littleEndian, noAssert) { 956 | if (!noAssert) { 957 | assert(value !== undefined && value !== null, 'missing value') 958 | assert(typeof littleEndian === 'boolean', 'missing or invalid endian') 959 | assert(offset !== undefined && offset !== null, 'missing offset') 960 | assert(offset + 7 < buf.length, 961 | 'Trying to write beyond buffer length') 962 | verifIEEE754(value, 1.7976931348623157E+308, -1.7976931348623157E+308) 963 | } 964 | 965 | var len = buf.length 966 | if (offset >= len) 967 | return 968 | 969 | ieee754.write(buf, value, offset, littleEndian, 52, 8) 970 | } 971 | 972 | Buffer.prototype.writeDoubleLE = function (value, offset, noAssert) { 973 | _writeDouble(this, value, offset, true, noAssert) 974 | } 975 | 976 | Buffer.prototype.writeDoubleBE = function (value, offset, noAssert) { 977 | _writeDouble(this, value, offset, false, noAssert) 978 | } 979 | 980 | // fill(value, start=0, end=buffer.length) 981 | Buffer.prototype.fill = function (value, start, end) { 982 | if (!value) value = 0 983 | if (!start) start = 0 984 | if (!end) end = this.length 985 | 986 | if (typeof value === 'string') { 987 | value = value.charCodeAt(0) 988 | } 989 | 990 | assert(typeof value === 'number' && !isNaN(value), 'value is not a number') 991 | assert(end >= start, 'end < start') 992 | 993 | // Fill 0 bytes; we're done 994 | if (end === start) return 995 | if (this.length === 0) return 996 | 997 | assert(start >= 0 && start < this.length, 'start out of bounds') 998 | assert(end >= 0 && end <= this.length, 'end out of bounds') 999 | 1000 | for (var i = start; i < end; i++) { 1001 | this[i] = value 1002 | } 1003 | } 1004 | 1005 | Buffer.prototype.inspect = function () { 1006 | var out = [] 1007 | var len = this.length 1008 | for (var i = 0; i < len; i++) { 1009 | out[i] = toHex(this[i]) 1010 | if (i === exports.INSPECT_MAX_BYTES) { 1011 | out[i + 1] = '...' 1012 | break 1013 | } 1014 | } 1015 | return '' 1016 | } 1017 | 1018 | /** 1019 | * Creates a new `ArrayBuffer` with the *copied* memory of the buffer instance. 1020 | * Added in Node 0.12. Only available in browsers that support ArrayBuffer. 1021 | */ 1022 | Buffer.prototype.toArrayBuffer = function () { 1023 | if (typeof Uint8Array !== 'undefined') { 1024 | if (Buffer._useTypedArrays) { 1025 | return (new Buffer(this)).buffer 1026 | } else { 1027 | var buf = new Uint8Array(this.length) 1028 | for (var i = 0, len = buf.length; i < len; i += 1) 1029 | buf[i] = this[i] 1030 | return buf.buffer 1031 | } 1032 | } else { 1033 | throw new Error('Buffer.toArrayBuffer not supported in this browser') 1034 | } 1035 | } 1036 | 1037 | // HELPER FUNCTIONS 1038 | // ================ 1039 | 1040 | function stringtrim (str) { 1041 | if (str.trim) return str.trim() 1042 | return str.replace(/^\s+|\s+$/g, '') 1043 | } 1044 | 1045 | var BP = Buffer.prototype 1046 | 1047 | /** 1048 | * Augment a Uint8Array *instance* (not the Uint8Array class!) with Buffer methods 1049 | */ 1050 | Buffer._augment = function (arr) { 1051 | arr._isBuffer = true 1052 | 1053 | // save reference to original Uint8Array get/set methods before overwriting 1054 | arr._get = arr.get 1055 | arr._set = arr.set 1056 | 1057 | // deprecated, will be removed in node 0.13+ 1058 | arr.get = BP.get 1059 | arr.set = BP.set 1060 | 1061 | arr.write = BP.write 1062 | arr.toString = BP.toString 1063 | arr.toLocaleString = BP.toString 1064 | arr.toJSON = BP.toJSON 1065 | arr.copy = BP.copy 1066 | arr.slice = BP.slice 1067 | arr.readUInt8 = BP.readUInt8 1068 | arr.readUInt16LE = BP.readUInt16LE 1069 | arr.readUInt16BE = BP.readUInt16BE 1070 | arr.readUInt32LE = BP.readUInt32LE 1071 | arr.readUInt32BE = BP.readUInt32BE 1072 | arr.readInt8 = BP.readInt8 1073 | arr.readInt16LE = BP.readInt16LE 1074 | arr.readInt16BE = BP.readInt16BE 1075 | arr.readInt32LE = BP.readInt32LE 1076 | arr.readInt32BE = BP.readInt32BE 1077 | arr.readFloatLE = BP.readFloatLE 1078 | arr.readFloatBE = BP.readFloatBE 1079 | arr.readDoubleLE = BP.readDoubleLE 1080 | arr.readDoubleBE = BP.readDoubleBE 1081 | arr.writeUInt8 = BP.writeUInt8 1082 | arr.writeUInt16LE = BP.writeUInt16LE 1083 | arr.writeUInt16BE = BP.writeUInt16BE 1084 | arr.writeUInt32LE = BP.writeUInt32LE 1085 | arr.writeUInt32BE = BP.writeUInt32BE 1086 | arr.writeInt8 = BP.writeInt8 1087 | arr.writeInt16LE = BP.writeInt16LE 1088 | arr.writeInt16BE = BP.writeInt16BE 1089 | arr.writeInt32LE = BP.writeInt32LE 1090 | arr.writeInt32BE = BP.writeInt32BE 1091 | arr.writeFloatLE = BP.writeFloatLE 1092 | arr.writeFloatBE = BP.writeFloatBE 1093 | arr.writeDoubleLE = BP.writeDoubleLE 1094 | arr.writeDoubleBE = BP.writeDoubleBE 1095 | arr.fill = BP.fill 1096 | arr.inspect = BP.inspect 1097 | arr.toArrayBuffer = BP.toArrayBuffer 1098 | 1099 | return arr 1100 | } 1101 | 1102 | // slice(start, end) 1103 | function clamp (index, len, defaultValue) { 1104 | if (typeof index !== 'number') return defaultValue 1105 | index = ~~index; // Coerce to integer. 1106 | if (index >= len) return len 1107 | if (index >= 0) return index 1108 | index += len 1109 | if (index >= 0) return index 1110 | return 0 1111 | } 1112 | 1113 | function coerce (length) { 1114 | // Coerce length to a number (possibly NaN), round up 1115 | // in case it's fractional (e.g. 123.456) then do a 1116 | // double negate to coerce a NaN to 0. Easy, right? 1117 | length = ~~Math.ceil(+length) 1118 | return length < 0 ? 0 : length 1119 | } 1120 | 1121 | function isArray (subject) { 1122 | return (Array.isArray || function (subject) { 1123 | return Object.prototype.toString.call(subject) === '[object Array]' 1124 | })(subject) 1125 | } 1126 | 1127 | function isArrayish (subject) { 1128 | return isArray(subject) || Buffer.isBuffer(subject) || 1129 | subject && typeof subject === 'object' && 1130 | typeof subject.length === 'number' 1131 | } 1132 | 1133 | function toHex (n) { 1134 | if (n < 16) return '0' + n.toString(16) 1135 | return n.toString(16) 1136 | } 1137 | 1138 | function utf8ToBytes (str) { 1139 | var byteArray = [] 1140 | for (var i = 0; i < str.length; i++) { 1141 | var b = str.charCodeAt(i) 1142 | if (b <= 0x7F) 1143 | byteArray.push(str.charCodeAt(i)) 1144 | else { 1145 | var start = i 1146 | if (b >= 0xD800 && b <= 0xDFFF) i++ 1147 | var h = encodeURIComponent(str.slice(start, i+1)).substr(1).split('%') 1148 | for (var j = 0; j < h.length; j++) 1149 | byteArray.push(parseInt(h[j], 16)) 1150 | } 1151 | } 1152 | return byteArray 1153 | } 1154 | 1155 | function asciiToBytes (str) { 1156 | var byteArray = [] 1157 | for (var i = 0; i < str.length; i++) { 1158 | // Node's code seems to be doing this and not & 0x7F.. 1159 | byteArray.push(str.charCodeAt(i) & 0xFF) 1160 | } 1161 | return byteArray 1162 | } 1163 | 1164 | function utf16leToBytes (str) { 1165 | var c, hi, lo 1166 | var byteArray = [] 1167 | for (var i = 0; i < str.length; i++) { 1168 | c = str.charCodeAt(i) 1169 | hi = c >> 8 1170 | lo = c % 256 1171 | byteArray.push(lo) 1172 | byteArray.push(hi) 1173 | } 1174 | 1175 | return byteArray 1176 | } 1177 | 1178 | function base64ToBytes (str) { 1179 | return base64.toByteArray(str) 1180 | } 1181 | 1182 | function blitBuffer (src, dst, offset, length) { 1183 | var pos 1184 | for (var i = 0; i < length; i++) { 1185 | if ((i + offset >= dst.length) || (i >= src.length)) 1186 | break 1187 | dst[i + offset] = src[i] 1188 | } 1189 | return i 1190 | } 1191 | 1192 | function decodeUtf8Char (str) { 1193 | try { 1194 | return decodeURIComponent(str) 1195 | } catch (err) { 1196 | return String.fromCharCode(0xFFFD) // UTF 8 invalid char 1197 | } 1198 | } 1199 | 1200 | /* 1201 | * We have to make sure that the value is a valid integer. This means that it 1202 | * is non-negative. It has no fractional component and that it does not 1203 | * exceed the maximum allowed value. 1204 | */ 1205 | function verifuint (value, max) { 1206 | assert(typeof value === 'number', 'cannot write a non-number as a number') 1207 | assert(value >= 0, 'specified a negative value for writing an unsigned value') 1208 | assert(value <= max, 'value is larger than maximum value for type') 1209 | assert(Math.floor(value) === value, 'value has a fractional component') 1210 | } 1211 | 1212 | function verifsint (value, max, min) { 1213 | assert(typeof value === 'number', 'cannot write a non-number as a number') 1214 | assert(value <= max, 'value larger than maximum allowed value') 1215 | assert(value >= min, 'value smaller than minimum allowed value') 1216 | assert(Math.floor(value) === value, 'value has a fractional component') 1217 | } 1218 | 1219 | function verifIEEE754 (value, max, min) { 1220 | assert(typeof value === 'number', 'cannot write a non-number as a number') 1221 | assert(value <= max, 'value larger than maximum allowed value') 1222 | assert(value >= min, 'value smaller than minimum allowed value') 1223 | } 1224 | 1225 | function assert (test, message) { 1226 | if (!test) throw new Error(message || 'Failed assertion') 1227 | } 1228 | 1229 | },{"base64-js":4,"ieee754":5}],4:[function(require,module,exports){ 1230 | var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 1231 | 1232 | ;(function (exports) { 1233 | 'use strict'; 1234 | 1235 | var Arr = (typeof Uint8Array !== 'undefined') 1236 | ? Uint8Array 1237 | : Array 1238 | 1239 | var PLUS = '+'.charCodeAt(0) 1240 | var SLASH = '/'.charCodeAt(0) 1241 | var NUMBER = '0'.charCodeAt(0) 1242 | var LOWER = 'a'.charCodeAt(0) 1243 | var UPPER = 'A'.charCodeAt(0) 1244 | 1245 | function decode (elt) { 1246 | var code = elt.charCodeAt(0) 1247 | if (code === PLUS) 1248 | return 62 // '+' 1249 | if (code === SLASH) 1250 | return 63 // '/' 1251 | if (code < NUMBER) 1252 | return -1 //no match 1253 | if (code < NUMBER + 10) 1254 | return code - NUMBER + 26 + 26 1255 | if (code < UPPER + 26) 1256 | return code - UPPER 1257 | if (code < LOWER + 26) 1258 | return code - LOWER + 26 1259 | } 1260 | 1261 | function b64ToByteArray (b64) { 1262 | var i, j, l, tmp, placeHolders, arr 1263 | 1264 | if (b64.length % 4 > 0) { 1265 | throw new Error('Invalid string. Length must be a multiple of 4') 1266 | } 1267 | 1268 | // the number of equal signs (place holders) 1269 | // if there are two placeholders, than the two characters before it 1270 | // represent one byte 1271 | // if there is only one, then the three characters before it represent 2 bytes 1272 | // this is just a cheap hack to not do indexOf twice 1273 | var len = b64.length 1274 | placeHolders = '=' === b64.charAt(len - 2) ? 2 : '=' === b64.charAt(len - 1) ? 1 : 0 1275 | 1276 | // base64 is 4/3 + up to two characters of the original data 1277 | arr = new Arr(b64.length * 3 / 4 - placeHolders) 1278 | 1279 | // if there are placeholders, only get up to the last complete 4 chars 1280 | l = placeHolders > 0 ? b64.length - 4 : b64.length 1281 | 1282 | var L = 0 1283 | 1284 | function push (v) { 1285 | arr[L++] = v 1286 | } 1287 | 1288 | for (i = 0, j = 0; i < l; i += 4, j += 3) { 1289 | tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3)) 1290 | push((tmp & 0xFF0000) >> 16) 1291 | push((tmp & 0xFF00) >> 8) 1292 | push(tmp & 0xFF) 1293 | } 1294 | 1295 | if (placeHolders === 2) { 1296 | tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4) 1297 | push(tmp & 0xFF) 1298 | } else if (placeHolders === 1) { 1299 | tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2) 1300 | push((tmp >> 8) & 0xFF) 1301 | push(tmp & 0xFF) 1302 | } 1303 | 1304 | return arr 1305 | } 1306 | 1307 | function uint8ToBase64 (uint8) { 1308 | var i, 1309 | extraBytes = uint8.length % 3, // if we have 1 byte left, pad 2 bytes 1310 | output = "", 1311 | temp, length 1312 | 1313 | function encode (num) { 1314 | return lookup.charAt(num) 1315 | } 1316 | 1317 | function tripletToBase64 (num) { 1318 | return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F) 1319 | } 1320 | 1321 | // go through the array every three bytes, we'll deal with trailing stuff later 1322 | for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) { 1323 | temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2]) 1324 | output += tripletToBase64(temp) 1325 | } 1326 | 1327 | // pad the end with zeros, but make sure to not forget the extra bytes 1328 | switch (extraBytes) { 1329 | case 1: 1330 | temp = uint8[uint8.length - 1] 1331 | output += encode(temp >> 2) 1332 | output += encode((temp << 4) & 0x3F) 1333 | output += '==' 1334 | break 1335 | case 2: 1336 | temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1]) 1337 | output += encode(temp >> 10) 1338 | output += encode((temp >> 4) & 0x3F) 1339 | output += encode((temp << 2) & 0x3F) 1340 | output += '=' 1341 | break 1342 | } 1343 | 1344 | return output 1345 | } 1346 | 1347 | exports.toByteArray = b64ToByteArray 1348 | exports.fromByteArray = uint8ToBase64 1349 | }(typeof exports === 'undefined' ? (this.base64js = {}) : exports)) 1350 | 1351 | },{}],5:[function(require,module,exports){ 1352 | exports.read = function(buffer, offset, isLE, mLen, nBytes) { 1353 | var e, m, 1354 | eLen = nBytes * 8 - mLen - 1, 1355 | eMax = (1 << eLen) - 1, 1356 | eBias = eMax >> 1, 1357 | nBits = -7, 1358 | i = isLE ? (nBytes - 1) : 0, 1359 | d = isLE ? -1 : 1, 1360 | s = buffer[offset + i]; 1361 | 1362 | i += d; 1363 | 1364 | e = s & ((1 << (-nBits)) - 1); 1365 | s >>= (-nBits); 1366 | nBits += eLen; 1367 | for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8); 1368 | 1369 | m = e & ((1 << (-nBits)) - 1); 1370 | e >>= (-nBits); 1371 | nBits += mLen; 1372 | for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8); 1373 | 1374 | if (e === 0) { 1375 | e = 1 - eBias; 1376 | } else if (e === eMax) { 1377 | return m ? NaN : ((s ? -1 : 1) * Infinity); 1378 | } else { 1379 | m = m + Math.pow(2, mLen); 1380 | e = e - eBias; 1381 | } 1382 | return (s ? -1 : 1) * m * Math.pow(2, e - mLen); 1383 | }; 1384 | 1385 | exports.write = function(buffer, value, offset, isLE, mLen, nBytes) { 1386 | var e, m, c, 1387 | eLen = nBytes * 8 - mLen - 1, 1388 | eMax = (1 << eLen) - 1, 1389 | eBias = eMax >> 1, 1390 | rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0), 1391 | i = isLE ? 0 : (nBytes - 1), 1392 | d = isLE ? 1 : -1, 1393 | s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0; 1394 | 1395 | value = Math.abs(value); 1396 | 1397 | if (isNaN(value) || value === Infinity) { 1398 | m = isNaN(value) ? 1 : 0; 1399 | e = eMax; 1400 | } else { 1401 | e = Math.floor(Math.log(value) / Math.LN2); 1402 | if (value * (c = Math.pow(2, -e)) < 1) { 1403 | e--; 1404 | c *= 2; 1405 | } 1406 | if (e + eBias >= 1) { 1407 | value += rt / c; 1408 | } else { 1409 | value += rt * Math.pow(2, 1 - eBias); 1410 | } 1411 | if (value * c >= 2) { 1412 | e++; 1413 | c /= 2; 1414 | } 1415 | 1416 | if (e + eBias >= eMax) { 1417 | m = 0; 1418 | e = eMax; 1419 | } else if (e + eBias >= 1) { 1420 | m = (value * c - 1) * Math.pow(2, mLen); 1421 | e = e + eBias; 1422 | } else { 1423 | m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen); 1424 | e = 0; 1425 | } 1426 | } 1427 | 1428 | for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8); 1429 | 1430 | e = (e << mLen) | m; 1431 | eLen += mLen; 1432 | for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8); 1433 | 1434 | buffer[offset + i - d] |= s * 128; 1435 | }; 1436 | 1437 | },{}],6:[function(require,module,exports){ 1438 | var Buffer = require('buffer').Buffer; 1439 | var intSize = 4; 1440 | var zeroBuffer = new Buffer(intSize); zeroBuffer.fill(0); 1441 | var chrsz = 8; 1442 | 1443 | function toArray(buf, bigEndian) { 1444 | if ((buf.length % intSize) !== 0) { 1445 | var len = buf.length + (intSize - (buf.length % intSize)); 1446 | buf = Buffer.concat([buf, zeroBuffer], len); 1447 | } 1448 | 1449 | var arr = []; 1450 | var fn = bigEndian ? buf.readInt32BE : buf.readInt32LE; 1451 | for (var i = 0; i < buf.length; i += intSize) { 1452 | arr.push(fn.call(buf, i)); 1453 | } 1454 | return arr; 1455 | } 1456 | 1457 | function toBuffer(arr, size, bigEndian) { 1458 | var buf = new Buffer(size); 1459 | var fn = bigEndian ? buf.writeInt32BE : buf.writeInt32LE; 1460 | for (var i = 0; i < arr.length; i++) { 1461 | fn.call(buf, arr[i], i * 4, true); 1462 | } 1463 | return buf; 1464 | } 1465 | 1466 | function hash(buf, fn, hashSize, bigEndian) { 1467 | if (!Buffer.isBuffer(buf)) buf = new Buffer(buf); 1468 | var arr = fn(toArray(buf, bigEndian), buf.length * chrsz); 1469 | return toBuffer(arr, hashSize, bigEndian); 1470 | } 1471 | 1472 | module.exports = { hash: hash }; 1473 | 1474 | },{"buffer":3}],7:[function(require,module,exports){ 1475 | var Buffer = require('buffer').Buffer 1476 | var sha = require('./sha') 1477 | var sha256 = require('./sha256') 1478 | var rng = require('./rng') 1479 | var md5 = require('./md5') 1480 | 1481 | var algorithms = { 1482 | sha1: sha, 1483 | sha256: sha256, 1484 | md5: md5 1485 | } 1486 | 1487 | var blocksize = 64 1488 | var zeroBuffer = new Buffer(blocksize); zeroBuffer.fill(0) 1489 | function hmac(fn, key, data) { 1490 | if(!Buffer.isBuffer(key)) key = new Buffer(key) 1491 | if(!Buffer.isBuffer(data)) data = new Buffer(data) 1492 | 1493 | if(key.length > blocksize) { 1494 | key = fn(key) 1495 | } else if(key.length < blocksize) { 1496 | key = Buffer.concat([key, zeroBuffer], blocksize) 1497 | } 1498 | 1499 | var ipad = new Buffer(blocksize), opad = new Buffer(blocksize) 1500 | for(var i = 0; i < blocksize; i++) { 1501 | ipad[i] = key[i] ^ 0x36 1502 | opad[i] = key[i] ^ 0x5C 1503 | } 1504 | 1505 | var hash = fn(Buffer.concat([ipad, data])) 1506 | return fn(Buffer.concat([opad, hash])) 1507 | } 1508 | 1509 | function hash(alg, key) { 1510 | alg = alg || 'sha1' 1511 | var fn = algorithms[alg] 1512 | var bufs = [] 1513 | var length = 0 1514 | if(!fn) error('algorithm:', alg, 'is not yet supported') 1515 | return { 1516 | update: function (data) { 1517 | if(!Buffer.isBuffer(data)) data = new Buffer(data) 1518 | 1519 | bufs.push(data) 1520 | length += data.length 1521 | return this 1522 | }, 1523 | digest: function (enc) { 1524 | var buf = Buffer.concat(bufs) 1525 | var r = key ? hmac(fn, key, buf) : fn(buf) 1526 | bufs = null 1527 | return enc ? r.toString(enc) : r 1528 | } 1529 | } 1530 | } 1531 | 1532 | function error () { 1533 | var m = [].slice.call(arguments).join(' ') 1534 | throw new Error([ 1535 | m, 1536 | 'we accept pull requests', 1537 | 'http://github.com/dominictarr/crypto-browserify' 1538 | ].join('\n')) 1539 | } 1540 | 1541 | exports.createHash = function (alg) { return hash(alg) } 1542 | exports.createHmac = function (alg, key) { return hash(alg, key) } 1543 | exports.randomBytes = function(size, callback) { 1544 | if (callback && callback.call) { 1545 | try { 1546 | callback.call(this, undefined, new Buffer(rng(size))) 1547 | } catch (err) { callback(err) } 1548 | } else { 1549 | return new Buffer(rng(size)) 1550 | } 1551 | } 1552 | 1553 | function each(a, f) { 1554 | for(var i in a) 1555 | f(a[i], i) 1556 | } 1557 | 1558 | // the least I can do is make error messages for the rest of the node.js/crypto api. 1559 | each(['createCredentials' 1560 | , 'createCipher' 1561 | , 'createCipheriv' 1562 | , 'createDecipher' 1563 | , 'createDecipheriv' 1564 | , 'createSign' 1565 | , 'createVerify' 1566 | , 'createDiffieHellman' 1567 | , 'pbkdf2'], function (name) { 1568 | exports[name] = function () { 1569 | error('sorry,', name, 'is not implemented yet') 1570 | } 1571 | }) 1572 | 1573 | },{"./md5":8,"./rng":9,"./sha":10,"./sha256":11,"buffer":3}],8:[function(require,module,exports){ 1574 | /* 1575 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message 1576 | * Digest Algorithm, as defined in RFC 1321. 1577 | * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. 1578 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 1579 | * Distributed under the BSD License 1580 | * See http://pajhome.org.uk/crypt/md5 for more info. 1581 | */ 1582 | 1583 | var helpers = require('./helpers'); 1584 | 1585 | /* 1586 | * Perform a simple self-test to see if the VM is working 1587 | */ 1588 | function md5_vm_test() 1589 | { 1590 | return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72"; 1591 | } 1592 | 1593 | /* 1594 | * Calculate the MD5 of an array of little-endian words, and a bit length 1595 | */ 1596 | function core_md5(x, len) 1597 | { 1598 | /* append padding */ 1599 | x[len >> 5] |= 0x80 << ((len) % 32); 1600 | x[(((len + 64) >>> 9) << 4) + 14] = len; 1601 | 1602 | var a = 1732584193; 1603 | var b = -271733879; 1604 | var c = -1732584194; 1605 | var d = 271733878; 1606 | 1607 | for(var i = 0; i < x.length; i += 16) 1608 | { 1609 | var olda = a; 1610 | var oldb = b; 1611 | var oldc = c; 1612 | var oldd = d; 1613 | 1614 | a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); 1615 | d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); 1616 | c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); 1617 | b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); 1618 | a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); 1619 | d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); 1620 | c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); 1621 | b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); 1622 | a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); 1623 | d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); 1624 | c = md5_ff(c, d, a, b, x[i+10], 17, -42063); 1625 | b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); 1626 | a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); 1627 | d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); 1628 | c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); 1629 | b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); 1630 | 1631 | a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); 1632 | d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); 1633 | c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); 1634 | b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); 1635 | a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); 1636 | d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); 1637 | c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); 1638 | b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); 1639 | a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); 1640 | d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); 1641 | c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); 1642 | b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); 1643 | a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); 1644 | d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); 1645 | c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); 1646 | b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); 1647 | 1648 | a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); 1649 | d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); 1650 | c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); 1651 | b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); 1652 | a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); 1653 | d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); 1654 | c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); 1655 | b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); 1656 | a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); 1657 | d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); 1658 | c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); 1659 | b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); 1660 | a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); 1661 | d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); 1662 | c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); 1663 | b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); 1664 | 1665 | a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); 1666 | d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); 1667 | c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); 1668 | b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); 1669 | a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); 1670 | d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); 1671 | c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); 1672 | b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); 1673 | a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); 1674 | d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); 1675 | c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); 1676 | b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); 1677 | a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); 1678 | d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); 1679 | c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); 1680 | b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); 1681 | 1682 | a = safe_add(a, olda); 1683 | b = safe_add(b, oldb); 1684 | c = safe_add(c, oldc); 1685 | d = safe_add(d, oldd); 1686 | } 1687 | return Array(a, b, c, d); 1688 | 1689 | } 1690 | 1691 | /* 1692 | * These functions implement the four basic operations the algorithm uses. 1693 | */ 1694 | function md5_cmn(q, a, b, x, s, t) 1695 | { 1696 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); 1697 | } 1698 | function md5_ff(a, b, c, d, x, s, t) 1699 | { 1700 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); 1701 | } 1702 | function md5_gg(a, b, c, d, x, s, t) 1703 | { 1704 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); 1705 | } 1706 | function md5_hh(a, b, c, d, x, s, t) 1707 | { 1708 | return md5_cmn(b ^ c ^ d, a, b, x, s, t); 1709 | } 1710 | function md5_ii(a, b, c, d, x, s, t) 1711 | { 1712 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); 1713 | } 1714 | 1715 | /* 1716 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 1717 | * to work around bugs in some JS interpreters. 1718 | */ 1719 | function safe_add(x, y) 1720 | { 1721 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 1722 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 1723 | return (msw << 16) | (lsw & 0xFFFF); 1724 | } 1725 | 1726 | /* 1727 | * Bitwise rotate a 32-bit number to the left. 1728 | */ 1729 | function bit_rol(num, cnt) 1730 | { 1731 | return (num << cnt) | (num >>> (32 - cnt)); 1732 | } 1733 | 1734 | module.exports = function md5(buf) { 1735 | return helpers.hash(buf, core_md5, 16); 1736 | }; 1737 | 1738 | },{"./helpers":6}],9:[function(require,module,exports){ 1739 | // Original code adapted from Robert Kieffer. 1740 | // details at https://github.com/broofa/node-uuid 1741 | (function() { 1742 | var _global = this; 1743 | 1744 | var mathRNG, whatwgRNG; 1745 | 1746 | // NOTE: Math.random() does not guarantee "cryptographic quality" 1747 | mathRNG = function(size) { 1748 | var bytes = new Array(size); 1749 | var r; 1750 | 1751 | for (var i = 0, r; i < size; i++) { 1752 | if ((i & 0x03) == 0) r = Math.random() * 0x100000000; 1753 | bytes[i] = r >>> ((i & 0x03) << 3) & 0xff; 1754 | } 1755 | 1756 | return bytes; 1757 | } 1758 | 1759 | if (_global.crypto && crypto.getRandomValues) { 1760 | whatwgRNG = function(size) { 1761 | var bytes = new Uint8Array(size); 1762 | crypto.getRandomValues(bytes); 1763 | return bytes; 1764 | } 1765 | } 1766 | 1767 | module.exports = whatwgRNG || mathRNG; 1768 | 1769 | }()) 1770 | 1771 | },{}],10:[function(require,module,exports){ 1772 | /* 1773 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined 1774 | * in FIPS PUB 180-1 1775 | * Version 2.1a Copyright Paul Johnston 2000 - 2002. 1776 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 1777 | * Distributed under the BSD License 1778 | * See http://pajhome.org.uk/crypt/md5 for details. 1779 | */ 1780 | 1781 | var helpers = require('./helpers'); 1782 | 1783 | /* 1784 | * Calculate the SHA-1 of an array of big-endian words, and a bit length 1785 | */ 1786 | function core_sha1(x, len) 1787 | { 1788 | /* append padding */ 1789 | x[len >> 5] |= 0x80 << (24 - len % 32); 1790 | x[((len + 64 >> 9) << 4) + 15] = len; 1791 | 1792 | var w = Array(80); 1793 | var a = 1732584193; 1794 | var b = -271733879; 1795 | var c = -1732584194; 1796 | var d = 271733878; 1797 | var e = -1009589776; 1798 | 1799 | for(var i = 0; i < x.length; i += 16) 1800 | { 1801 | var olda = a; 1802 | var oldb = b; 1803 | var oldc = c; 1804 | var oldd = d; 1805 | var olde = e; 1806 | 1807 | for(var j = 0; j < 80; j++) 1808 | { 1809 | if(j < 16) w[j] = x[i + j]; 1810 | else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); 1811 | var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), 1812 | safe_add(safe_add(e, w[j]), sha1_kt(j))); 1813 | e = d; 1814 | d = c; 1815 | c = rol(b, 30); 1816 | b = a; 1817 | a = t; 1818 | } 1819 | 1820 | a = safe_add(a, olda); 1821 | b = safe_add(b, oldb); 1822 | c = safe_add(c, oldc); 1823 | d = safe_add(d, oldd); 1824 | e = safe_add(e, olde); 1825 | } 1826 | return Array(a, b, c, d, e); 1827 | 1828 | } 1829 | 1830 | /* 1831 | * Perform the appropriate triplet combination function for the current 1832 | * iteration 1833 | */ 1834 | function sha1_ft(t, b, c, d) 1835 | { 1836 | if(t < 20) return (b & c) | ((~b) & d); 1837 | if(t < 40) return b ^ c ^ d; 1838 | if(t < 60) return (b & c) | (b & d) | (c & d); 1839 | return b ^ c ^ d; 1840 | } 1841 | 1842 | /* 1843 | * Determine the appropriate additive constant for the current iteration 1844 | */ 1845 | function sha1_kt(t) 1846 | { 1847 | return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : 1848 | (t < 60) ? -1894007588 : -899497514; 1849 | } 1850 | 1851 | /* 1852 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 1853 | * to work around bugs in some JS interpreters. 1854 | */ 1855 | function safe_add(x, y) 1856 | { 1857 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 1858 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 1859 | return (msw << 16) | (lsw & 0xFFFF); 1860 | } 1861 | 1862 | /* 1863 | * Bitwise rotate a 32-bit number to the left. 1864 | */ 1865 | function rol(num, cnt) 1866 | { 1867 | return (num << cnt) | (num >>> (32 - cnt)); 1868 | } 1869 | 1870 | module.exports = function sha1(buf) { 1871 | return helpers.hash(buf, core_sha1, 20, true); 1872 | }; 1873 | 1874 | },{"./helpers":6}],11:[function(require,module,exports){ 1875 | 1876 | /** 1877 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-256, as defined 1878 | * in FIPS 180-2 1879 | * Version 2.2-beta Copyright Angel Marin, Paul Johnston 2000 - 2009. 1880 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 1881 | * 1882 | */ 1883 | 1884 | var helpers = require('./helpers'); 1885 | 1886 | var safe_add = function(x, y) { 1887 | var lsw = (x & 0xFFFF) + (y & 0xFFFF); 1888 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 1889 | return (msw << 16) | (lsw & 0xFFFF); 1890 | }; 1891 | 1892 | var S = function(X, n) { 1893 | return (X >>> n) | (X << (32 - n)); 1894 | }; 1895 | 1896 | var R = function(X, n) { 1897 | return (X >>> n); 1898 | }; 1899 | 1900 | var Ch = function(x, y, z) { 1901 | return ((x & y) ^ ((~x) & z)); 1902 | }; 1903 | 1904 | var Maj = function(x, y, z) { 1905 | return ((x & y) ^ (x & z) ^ (y & z)); 1906 | }; 1907 | 1908 | var Sigma0256 = function(x) { 1909 | return (S(x, 2) ^ S(x, 13) ^ S(x, 22)); 1910 | }; 1911 | 1912 | var Sigma1256 = function(x) { 1913 | return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); 1914 | }; 1915 | 1916 | var Gamma0256 = function(x) { 1917 | return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); 1918 | }; 1919 | 1920 | var Gamma1256 = function(x) { 1921 | return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); 1922 | }; 1923 | 1924 | var core_sha256 = function(m, l) { 1925 | var K = new Array(0x428A2F98,0x71374491,0xB5C0FBCF,0xE9B5DBA5,0x3956C25B,0x59F111F1,0x923F82A4,0xAB1C5ED5,0xD807AA98,0x12835B01,0x243185BE,0x550C7DC3,0x72BE5D74,0x80DEB1FE,0x9BDC06A7,0xC19BF174,0xE49B69C1,0xEFBE4786,0xFC19DC6,0x240CA1CC,0x2DE92C6F,0x4A7484AA,0x5CB0A9DC,0x76F988DA,0x983E5152,0xA831C66D,0xB00327C8,0xBF597FC7,0xC6E00BF3,0xD5A79147,0x6CA6351,0x14292967,0x27B70A85,0x2E1B2138,0x4D2C6DFC,0x53380D13,0x650A7354,0x766A0ABB,0x81C2C92E,0x92722C85,0xA2BFE8A1,0xA81A664B,0xC24B8B70,0xC76C51A3,0xD192E819,0xD6990624,0xF40E3585,0x106AA070,0x19A4C116,0x1E376C08,0x2748774C,0x34B0BCB5,0x391C0CB3,0x4ED8AA4A,0x5B9CCA4F,0x682E6FF3,0x748F82EE,0x78A5636F,0x84C87814,0x8CC70208,0x90BEFFFA,0xA4506CEB,0xBEF9A3F7,0xC67178F2); 1926 | var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19); 1927 | var W = new Array(64); 1928 | var a, b, c, d, e, f, g, h, i, j; 1929 | var T1, T2; 1930 | /* append padding */ 1931 | m[l >> 5] |= 0x80 << (24 - l % 32); 1932 | m[((l + 64 >> 9) << 4) + 15] = l; 1933 | for (var i = 0; i < m.length; i += 16) { 1934 | a = HASH[0]; b = HASH[1]; c = HASH[2]; d = HASH[3]; e = HASH[4]; f = HASH[5]; g = HASH[6]; h = HASH[7]; 1935 | for (var j = 0; j < 64; j++) { 1936 | if (j < 16) { 1937 | W[j] = m[j + i]; 1938 | } else { 1939 | W[j] = safe_add(safe_add(safe_add(Gamma1256(W[j - 2]), W[j - 7]), Gamma0256(W[j - 15])), W[j - 16]); 1940 | } 1941 | T1 = safe_add(safe_add(safe_add(safe_add(h, Sigma1256(e)), Ch(e, f, g)), K[j]), W[j]); 1942 | T2 = safe_add(Sigma0256(a), Maj(a, b, c)); 1943 | h = g; g = f; f = e; e = safe_add(d, T1); d = c; c = b; b = a; a = safe_add(T1, T2); 1944 | } 1945 | HASH[0] = safe_add(a, HASH[0]); HASH[1] = safe_add(b, HASH[1]); HASH[2] = safe_add(c, HASH[2]); HASH[3] = safe_add(d, HASH[3]); 1946 | HASH[4] = safe_add(e, HASH[4]); HASH[5] = safe_add(f, HASH[5]); HASH[6] = safe_add(g, HASH[6]); HASH[7] = safe_add(h, HASH[7]); 1947 | } 1948 | return HASH; 1949 | }; 1950 | 1951 | module.exports = function sha256(buf) { 1952 | return helpers.hash(buf, core_sha256, 32, true); 1953 | }; 1954 | 1955 | },{"./helpers":6}]},{},[2]) 1956 | -------------------------------------------------------------------------------- /lib/views/atom-pair-view.coffee: -------------------------------------------------------------------------------- 1 | {View} = require 'space-pen' 2 | _ = require 'underscore' 3 | 4 | module.exports = 5 | class AtomPairView extends View 6 | 7 | initialize: -> 8 | @panel ?= atom.workspace.addModalPanel(item: @, visible: true) 9 | @.focus() 10 | atom.commands.add(@element, 'core:cancel', => @hideView()) 11 | 12 | hideView: -> 13 | @panel.hide() 14 | @.focusout() 15 | -------------------------------------------------------------------------------- /lib/views/input-view.coffee: -------------------------------------------------------------------------------- 1 | AtomPairView = require './atom-pair-view' 2 | {TextEditorView} = require 'atom-space-pen-views' 3 | 4 | module.exports = 5 | class InputView extends AtomPairView 6 | 7 | @content: (label)-> 8 | @div => 9 | @span click: 'hideView', class: 'atom-pair-exit-view', "X" 10 | @div label 11 | @subview 'miniEditor', new TextEditorView(mini: true) 12 | 13 | onInput: (fn)-> 14 | @miniEditor.focus() 15 | atom.commands.add @element, 'core:confirm': => 16 | @panel.hide() 17 | fn(@miniEditor.getText()) 18 | -------------------------------------------------------------------------------- /menus/atom-pair.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": [ 3 | { 4 | "label": "Packages", 5 | "submenu": [ 6 | { 7 | "label": "Atom Pair", 8 | "submenu": [ 9 | { 10 | "label": "Start New Pairing Session", 11 | "command": "AtomPair:start new pairing session" 12 | }, 13 | { 14 | "label": "Join Existing Pairing Session", 15 | "command": "AtomPair:join pairing session" 16 | }, 17 | { 18 | "label": "Invite", 19 | "submenu": [ 20 | { 21 | "label": "Invite Over HipChat", 22 | "command": "AtomPair:invite over hipchat" 23 | }, 24 | { 25 | "label": "Invite Over Slack", 26 | "command": "AtomPair:invite over slack" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-pair", 3 | "main": "./lib/atom_pair", 4 | "version": "2.0.13", 5 | "description": "An Atom Package that allows for epic pair programming, powered by Pusher", 6 | "repository": "https://github.com/pusher/atom-pair", 7 | "license": "MIT", 8 | "engines": { 9 | "atom": ">0.50.0" 10 | }, 11 | "dependencies": { 12 | "atom-space-pen-views": "^2.0.3", 13 | "jquery": "^2.1.3", 14 | "node-hipchat": "^0.4.5", 15 | "randomstring": "^1.0.3", 16 | "slack-node": "^0.1.0", 17 | "space-pen": "^5.0.1", 18 | "underscore": "^1.7.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/fixtures/basic-buffer-write.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "changeType": "substitution", 5 | "event": { 6 | "oldRange": { 7 | "start": { 8 | "row": 0, 9 | "column": 0 10 | }, 11 | "end": { 12 | "row": 0, 13 | "column": 0 14 | } 15 | }, 16 | "newRange": { 17 | "start": { 18 | "row": 0, 19 | "column": 0 20 | }, 21 | "end": { 22 | "row": 0, 23 | "column": 1 24 | } 25 | }, 26 | "newText": "h" 27 | }, 28 | "colour": "red" 29 | } 30 | ], 31 | [ 32 | { 33 | "changeType": "substitution", 34 | "event": { 35 | "oldRange": { 36 | "start": { 37 | "row": 0, 38 | "column": 1 39 | }, 40 | "end": { 41 | "row": 0, 42 | "column": 1 43 | } 44 | }, 45 | "newRange": { 46 | "start": { 47 | "row": 0, 48 | "column": 1 49 | }, 50 | "end": { 51 | "row": 0, 52 | "column": 2 53 | } 54 | }, 55 | "newText": "e" 56 | }, 57 | "colour": "red" 58 | } 59 | ], 60 | [ 61 | { 62 | "changeType": "substitution", 63 | "event": { 64 | "oldRange": { 65 | "start": { 66 | "row": 0, 67 | "column": 2 68 | }, 69 | "end": { 70 | "row": 0, 71 | "column": 2 72 | } 73 | }, 74 | "newRange": { 75 | "start": { 76 | "row": 0, 77 | "column": 2 78 | }, 79 | "end": { 80 | "row": 0, 81 | "column": 3 82 | } 83 | }, 84 | "newText": "l" 85 | }, 86 | "colour": "red" 87 | } 88 | ], 89 | [ 90 | { 91 | "changeType": "substitution", 92 | "event": { 93 | "oldRange": { 94 | "start": { 95 | "row": 0, 96 | "column": 3 97 | }, 98 | "end": { 99 | "row": 0, 100 | "column": 3 101 | } 102 | }, 103 | "newRange": { 104 | "start": { 105 | "row": 0, 106 | "column": 3 107 | }, 108 | "end": { 109 | "row": 0, 110 | "column": 4 111 | } 112 | }, 113 | "newText": "l" 114 | }, 115 | "colour": "red" 116 | } 117 | ], 118 | [ 119 | { 120 | "changeType": "substitution", 121 | "event": { 122 | "oldRange": { 123 | "start": { 124 | "row": 0, 125 | "column": 4 126 | }, 127 | "end": { 128 | "row": 0, 129 | "column": 4 130 | } 131 | }, 132 | "newRange": { 133 | "start": { 134 | "row": 0, 135 | "column": 4 136 | }, 137 | "end": { 138 | "row": 0, 139 | "column": 5 140 | } 141 | }, 142 | "newText": "o" 143 | }, 144 | "colour": "red" 145 | } 146 | ], 147 | [ 148 | { 149 | "changeType": "substitution", 150 | "event": { 151 | "oldRange": { 152 | "start": { 153 | "row": 0, 154 | "column": 5 155 | }, 156 | "end": { 157 | "row": 0, 158 | "column": 5 159 | } 160 | }, 161 | "newRange": { 162 | "start": { 163 | "row": 0, 164 | "column": 5 165 | }, 166 | "end": { 167 | "row": 0, 168 | "column": 6 169 | } 170 | }, 171 | "newText": " " 172 | }, 173 | "colour": "red" 174 | } 175 | ], 176 | [ 177 | { 178 | "changeType": "substitution", 179 | "event": { 180 | "oldRange": { 181 | "start": { 182 | "row": 0, 183 | "column": 6 184 | }, 185 | "end": { 186 | "row": 0, 187 | "column": 6 188 | } 189 | }, 190 | "newRange": { 191 | "start": { 192 | "row": 0, 193 | "column": 6 194 | }, 195 | "end": { 196 | "row": 0, 197 | "column": 7 198 | } 199 | }, 200 | "newText": "w" 201 | }, 202 | "colour": "red" 203 | } 204 | ], 205 | [ 206 | { 207 | "changeType": "substitution", 208 | "event": { 209 | "oldRange": { 210 | "start": { 211 | "row": 0, 212 | "column": 7 213 | }, 214 | "end": { 215 | "row": 0, 216 | "column": 7 217 | } 218 | }, 219 | "newRange": { 220 | "start": { 221 | "row": 0, 222 | "column": 7 223 | }, 224 | "end": { 225 | "row": 0, 226 | "column": 8 227 | } 228 | }, 229 | "newText": "o" 230 | }, 231 | "colour": "red" 232 | } 233 | ], 234 | [ 235 | { 236 | "changeType": "substitution", 237 | "event": { 238 | "oldRange": { 239 | "start": { 240 | "row": 0, 241 | "column": 8 242 | }, 243 | "end": { 244 | "row": 0, 245 | "column": 8 246 | } 247 | }, 248 | "newRange": { 249 | "start": { 250 | "row": 0, 251 | "column": 8 252 | }, 253 | "end": { 254 | "row": 0, 255 | "column": 9 256 | } 257 | }, 258 | "newText": "r" 259 | }, 260 | "colour": "red" 261 | } 262 | ], 263 | [ 264 | { 265 | "changeType": "substitution", 266 | "event": { 267 | "oldRange": { 268 | "start": { 269 | "row": 0, 270 | "column": 9 271 | }, 272 | "end": { 273 | "row": 0, 274 | "column": 9 275 | } 276 | }, 277 | "newRange": { 278 | "start": { 279 | "row": 0, 280 | "column": 9 281 | }, 282 | "end": { 283 | "row": 0, 284 | "column": 10 285 | } 286 | }, 287 | "newText": "l" 288 | }, 289 | "colour": "red" 290 | } 291 | ], 292 | [ 293 | { 294 | "changeType": "substitution", 295 | "event": { 296 | "oldRange": { 297 | "start": { 298 | "row": 0, 299 | "column": 10 300 | }, 301 | "end": { 302 | "row": 0, 303 | "column": 10 304 | } 305 | }, 306 | "newRange": { 307 | "start": { 308 | "row": 0, 309 | "column": 10 310 | }, 311 | "end": { 312 | "row": 0, 313 | "column": 11 314 | } 315 | }, 316 | "newText": "d" 317 | }, 318 | "colour": "red" 319 | } 320 | ] 321 | ] -------------------------------------------------------------------------------- /spec/fixtures/insert-and-line-break.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "changeType": "substitution", 5 | "event": { 6 | "oldRange": { 7 | "start": { 8 | "row": 0, 9 | "column": 0 10 | }, 11 | "end": { 12 | "row": 0, 13 | "column": 0 14 | } 15 | }, 16 | "newRange": { 17 | "start": { 18 | "row": 0, 19 | "column": 0 20 | }, 21 | "end": { 22 | "row": 0, 23 | "column": 1 24 | } 25 | }, 26 | "newText": "h" 27 | }, 28 | "colour": "red" 29 | } 30 | ], 31 | [ 32 | { 33 | "changeType": "substitution", 34 | "event": { 35 | "oldRange": { 36 | "start": { 37 | "row": 0, 38 | "column": 1 39 | }, 40 | "end": { 41 | "row": 0, 42 | "column": 1 43 | } 44 | }, 45 | "newRange": { 46 | "start": { 47 | "row": 0, 48 | "column": 1 49 | }, 50 | "end": { 51 | "row": 0, 52 | "column": 2 53 | } 54 | }, 55 | "newText": "e" 56 | }, 57 | "colour": "red" 58 | } 59 | ], 60 | [ 61 | { 62 | "changeType": "substitution", 63 | "event": { 64 | "oldRange": { 65 | "start": { 66 | "row": 0, 67 | "column": 2 68 | }, 69 | "end": { 70 | "row": 0, 71 | "column": 2 72 | } 73 | }, 74 | "newRange": { 75 | "start": { 76 | "row": 0, 77 | "column": 2 78 | }, 79 | "end": { 80 | "row": 0, 81 | "column": 3 82 | } 83 | }, 84 | "newText": "l" 85 | }, 86 | "colour": "red" 87 | } 88 | ], 89 | [ 90 | { 91 | "changeType": "substitution", 92 | "event": { 93 | "oldRange": { 94 | "start": { 95 | "row": 0, 96 | "column": 3 97 | }, 98 | "end": { 99 | "row": 0, 100 | "column": 3 101 | } 102 | }, 103 | "newRange": { 104 | "start": { 105 | "row": 0, 106 | "column": 3 107 | }, 108 | "end": { 109 | "row": 0, 110 | "column": 4 111 | } 112 | }, 113 | "newText": "l" 114 | }, 115 | "colour": "red" 116 | } 117 | ], 118 | [ 119 | { 120 | "changeType": "substitution", 121 | "event": { 122 | "oldRange": { 123 | "start": { 124 | "row": 0, 125 | "column": 4 126 | }, 127 | "end": { 128 | "row": 0, 129 | "column": 4 130 | } 131 | }, 132 | "newRange": { 133 | "start": { 134 | "row": 0, 135 | "column": 4 136 | }, 137 | "end": { 138 | "row": 0, 139 | "column": 5 140 | } 141 | }, 142 | "newText": "o" 143 | }, 144 | "colour": "red" 145 | } 146 | ], 147 | [ 148 | { 149 | "changeType": "substitution", 150 | "event": { 151 | "oldRange": { 152 | "start": { 153 | "row": 0, 154 | "column": 5 155 | }, 156 | "end": { 157 | "row": 0, 158 | "column": 5 159 | } 160 | }, 161 | "newRange": { 162 | "start": { 163 | "row": 0, 164 | "column": 5 165 | }, 166 | "end": { 167 | "row": 0, 168 | "column": 6 169 | } 170 | }, 171 | "newText": " " 172 | }, 173 | "colour": "red" 174 | } 175 | ], 176 | [ 177 | { 178 | "changeType": "substitution", 179 | "event": { 180 | "oldRange": { 181 | "start": { 182 | "row": 0, 183 | "column": 6 184 | }, 185 | "end": { 186 | "row": 0, 187 | "column": 6 188 | } 189 | }, 190 | "newRange": { 191 | "start": { 192 | "row": 0, 193 | "column": 6 194 | }, 195 | "end": { 196 | "row": 0, 197 | "column": 7 198 | } 199 | }, 200 | "newText": "w" 201 | }, 202 | "colour": "red" 203 | } 204 | ], 205 | [ 206 | { 207 | "changeType": "substitution", 208 | "event": { 209 | "oldRange": { 210 | "start": { 211 | "row": 0, 212 | "column": 7 213 | }, 214 | "end": { 215 | "row": 0, 216 | "column": 7 217 | } 218 | }, 219 | "newRange": { 220 | "start": { 221 | "row": 0, 222 | "column": 7 223 | }, 224 | "end": { 225 | "row": 0, 226 | "column": 8 227 | } 228 | }, 229 | "newText": "o" 230 | }, 231 | "colour": "red" 232 | } 233 | ], 234 | [ 235 | { 236 | "changeType": "substitution", 237 | "event": { 238 | "oldRange": { 239 | "start": { 240 | "row": 0, 241 | "column": 8 242 | }, 243 | "end": { 244 | "row": 0, 245 | "column": 8 246 | } 247 | }, 248 | "newRange": { 249 | "start": { 250 | "row": 0, 251 | "column": 8 252 | }, 253 | "end": { 254 | "row": 0, 255 | "column": 9 256 | } 257 | }, 258 | "newText": "r" 259 | }, 260 | "colour": "red" 261 | } 262 | ], 263 | [ 264 | { 265 | "changeType": "substitution", 266 | "event": { 267 | "oldRange": { 268 | "start": { 269 | "row": 0, 270 | "column": 9 271 | }, 272 | "end": { 273 | "row": 0, 274 | "column": 9 275 | } 276 | }, 277 | "newRange": { 278 | "start": { 279 | "row": 0, 280 | "column": 9 281 | }, 282 | "end": { 283 | "row": 0, 284 | "column": 10 285 | } 286 | }, 287 | "newText": "l" 288 | }, 289 | "colour": "red" 290 | } 291 | ], 292 | [ 293 | { 294 | "changeType": "substitution", 295 | "event": { 296 | "oldRange": { 297 | "start": { 298 | "row": 0, 299 | "column": 10 300 | }, 301 | "end": { 302 | "row": 0, 303 | "column": 10 304 | } 305 | }, 306 | "newRange": { 307 | "start": { 308 | "row": 0, 309 | "column": 10 310 | }, 311 | "end": { 312 | "row": 0, 313 | "column": 11 314 | } 315 | }, 316 | "newText": "d" 317 | }, 318 | "colour": "red" 319 | } 320 | ], 321 | [ 322 | { 323 | "changeType": "substitution", 324 | "event": { 325 | "oldRange": { 326 | "start": { 327 | "row": 0, 328 | "column": 6 329 | }, 330 | "end": { 331 | "row": 0, 332 | "column": 6 333 | } 334 | }, 335 | "newRange": { 336 | "start": { 337 | "row": 0, 338 | "column": 6 339 | }, 340 | "end": { 341 | "row": 1, 342 | "column": 0 343 | } 344 | }, 345 | "newText": "\n" 346 | }, 347 | "colour": "red" 348 | } 349 | ] 350 | ] -------------------------------------------------------------------------------- /spec/fixtures/multiline-deletions.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "changeType": "substitution", 5 | "event": { 6 | "oldRange": { 7 | "start": { 8 | "row": 0, 9 | "column": 0 10 | }, 11 | "end": { 12 | "row": 0, 13 | "column": 0 14 | } 15 | }, 16 | "newRange": { 17 | "start": { 18 | "row": 0, 19 | "column": 0 20 | }, 21 | "end": { 22 | "row": 6, 23 | "column": 23 24 | } 25 | }, 26 | "newText": "i have of late\nwherefore i know not\nlost all my mirth\nand indeed it goes so heavily with my disposition\nthat this goodly frame the earth\nseems to me\na sterile promontory :(" 27 | }, 28 | "colour": "red" 29 | } 30 | ], 31 | [ 32 | { 33 | "changeType": "deletion", 34 | "event": { 35 | "oldRange": { 36 | "start": { 37 | "row": 3, 38 | "column": 0 39 | }, 40 | "end": { 41 | "row": 4, 42 | "column": 32 43 | } 44 | } 45 | }, 46 | "colour": "red" 47 | } 48 | ] 49 | ] 50 | -------------------------------------------------------------------------------- /spec/fixtures/small-deletions.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "changeType": "substitution", 5 | "event": { 6 | "oldRange": { 7 | "start": { 8 | "row": 0, 9 | "column": 0 10 | }, 11 | "end": { 12 | "row": 0, 13 | "column": 0 14 | } 15 | }, 16 | "newRange": { 17 | "start": { 18 | "row": 0, 19 | "column": 0 20 | }, 21 | "end": { 22 | "row": 0, 23 | "column": 1 24 | } 25 | }, 26 | "newText": "h" 27 | }, 28 | "colour": "red" 29 | } 30 | ], 31 | [ 32 | { 33 | "changeType": "substitution", 34 | "event": { 35 | "oldRange": { 36 | "start": { 37 | "row": 0, 38 | "column": 1 39 | }, 40 | "end": { 41 | "row": 0, 42 | "column": 1 43 | } 44 | }, 45 | "newRange": { 46 | "start": { 47 | "row": 0, 48 | "column": 1 49 | }, 50 | "end": { 51 | "row": 0, 52 | "column": 2 53 | } 54 | }, 55 | "newText": "e" 56 | }, 57 | "colour": "red" 58 | } 59 | ], 60 | [ 61 | { 62 | "changeType": "substitution", 63 | "event": { 64 | "oldRange": { 65 | "start": { 66 | "row": 0, 67 | "column": 2 68 | }, 69 | "end": { 70 | "row": 0, 71 | "column": 2 72 | } 73 | }, 74 | "newRange": { 75 | "start": { 76 | "row": 0, 77 | "column": 2 78 | }, 79 | "end": { 80 | "row": 0, 81 | "column": 3 82 | } 83 | }, 84 | "newText": "l" 85 | }, 86 | "colour": "red" 87 | } 88 | ], 89 | [ 90 | { 91 | "changeType": "substitution", 92 | "event": { 93 | "oldRange": { 94 | "start": { 95 | "row": 0, 96 | "column": 3 97 | }, 98 | "end": { 99 | "row": 0, 100 | "column": 3 101 | } 102 | }, 103 | "newRange": { 104 | "start": { 105 | "row": 0, 106 | "column": 3 107 | }, 108 | "end": { 109 | "row": 0, 110 | "column": 4 111 | } 112 | }, 113 | "newText": "l" 114 | }, 115 | "colour": "red" 116 | } 117 | ], 118 | [ 119 | { 120 | "changeType": "substitution", 121 | "event": { 122 | "oldRange": { 123 | "start": { 124 | "row": 0, 125 | "column": 4 126 | }, 127 | "end": { 128 | "row": 0, 129 | "column": 4 130 | } 131 | }, 132 | "newRange": { 133 | "start": { 134 | "row": 0, 135 | "column": 4 136 | }, 137 | "end": { 138 | "row": 0, 139 | "column": 5 140 | } 141 | }, 142 | "newText": "o" 143 | }, 144 | "colour": "red" 145 | } 146 | ], 147 | [ 148 | { 149 | "changeType": "substitution", 150 | "event": { 151 | "oldRange": { 152 | "start": { 153 | "row": 0, 154 | "column": 5 155 | }, 156 | "end": { 157 | "row": 0, 158 | "column": 5 159 | } 160 | }, 161 | "newRange": { 162 | "start": { 163 | "row": 0, 164 | "column": 5 165 | }, 166 | "end": { 167 | "row": 0, 168 | "column": 6 169 | } 170 | }, 171 | "newText": " " 172 | }, 173 | "colour": "red" 174 | } 175 | ], 176 | [ 177 | { 178 | "changeType": "substitution", 179 | "event": { 180 | "oldRange": { 181 | "start": { 182 | "row": 0, 183 | "column": 6 184 | }, 185 | "end": { 186 | "row": 0, 187 | "column": 6 188 | } 189 | }, 190 | "newRange": { 191 | "start": { 192 | "row": 0, 193 | "column": 6 194 | }, 195 | "end": { 196 | "row": 0, 197 | "column": 7 198 | } 199 | }, 200 | "newText": "w" 201 | }, 202 | "colour": "red" 203 | } 204 | ], 205 | [ 206 | { 207 | "changeType": "substitution", 208 | "event": { 209 | "oldRange": { 210 | "start": { 211 | "row": 0, 212 | "column": 7 213 | }, 214 | "end": { 215 | "row": 0, 216 | "column": 7 217 | } 218 | }, 219 | "newRange": { 220 | "start": { 221 | "row": 0, 222 | "column": 7 223 | }, 224 | "end": { 225 | "row": 0, 226 | "column": 8 227 | } 228 | }, 229 | "newText": "o" 230 | }, 231 | "colour": "red" 232 | } 233 | ], 234 | [ 235 | { 236 | "changeType": "substitution", 237 | "event": { 238 | "oldRange": { 239 | "start": { 240 | "row": 0, 241 | "column": 8 242 | }, 243 | "end": { 244 | "row": 0, 245 | "column": 8 246 | } 247 | }, 248 | "newRange": { 249 | "start": { 250 | "row": 0, 251 | "column": 8 252 | }, 253 | "end": { 254 | "row": 0, 255 | "column": 9 256 | } 257 | }, 258 | "newText": "r" 259 | }, 260 | "colour": "red" 261 | } 262 | ], 263 | [ 264 | { 265 | "changeType": "substitution", 266 | "event": { 267 | "oldRange": { 268 | "start": { 269 | "row": 0, 270 | "column": 9 271 | }, 272 | "end": { 273 | "row": 0, 274 | "column": 9 275 | } 276 | }, 277 | "newRange": { 278 | "start": { 279 | "row": 0, 280 | "column": 9 281 | }, 282 | "end": { 283 | "row": 0, 284 | "column": 10 285 | } 286 | }, 287 | "newText": "l" 288 | }, 289 | "colour": "red" 290 | } 291 | ], 292 | [ 293 | { 294 | "changeType": "substitution", 295 | "event": { 296 | "oldRange": { 297 | "start": { 298 | "row": 0, 299 | "column": 10 300 | }, 301 | "end": { 302 | "row": 0, 303 | "column": 10 304 | } 305 | }, 306 | "newRange": { 307 | "start": { 308 | "row": 0, 309 | "column": 10 310 | }, 311 | "end": { 312 | "row": 0, 313 | "column": 11 314 | } 315 | }, 316 | "newText": "d" 317 | }, 318 | "colour": "red" 319 | } 320 | ], 321 | [ 322 | { 323 | "changeType": "deletion", 324 | "event": { 325 | "oldRange": { 326 | "start": { 327 | "row": 0, 328 | "column": 10 329 | }, 330 | "end": { 331 | "row": 0, 332 | "column": 11 333 | } 334 | } 335 | }, 336 | "colour": "red" 337 | } 338 | ], 339 | [ 340 | { 341 | "changeType": "deletion", 342 | "event": { 343 | "oldRange": { 344 | "start": { 345 | "row": 0, 346 | "column": 9 347 | }, 348 | "end": { 349 | "row": 0, 350 | "column": 10 351 | } 352 | } 353 | }, 354 | "colour": "red" 355 | } 356 | ], 357 | [ 358 | { 359 | "changeType": "deletion", 360 | "event": { 361 | "oldRange": { 362 | "start": { 363 | "row": 0, 364 | "column": 1 365 | }, 366 | "end": { 367 | "row": 0, 368 | "column": 2 369 | } 370 | } 371 | }, 372 | "colour": "red" 373 | } 374 | ] 375 | ] -------------------------------------------------------------------------------- /spec/helpers/buffer-triggers.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | 3 | module.exports = bufferTriggerTest = (buffer, fileName, queue, action)-> 4 | expectedEvents = require "../fixtures/#{fileName}" 5 | action(buffer) 6 | 7 | argsForCall = _.map expectedEvents, (expected) -> 8 | ['test-channel', 'client-change', expected] 9 | 10 | expect(queue.add.argsForCall).toEqual(argsForCall) 11 | -------------------------------------------------------------------------------- /spec/helpers/spec-setup.coffee: -------------------------------------------------------------------------------- 1 | SharePane = require '../../lib/modules/share_pane' 2 | PusherMock = require '../pusher-mock' 3 | MessageQueue = require '../../lib/modules/message_queue' 4 | User = require '../../lib/modules/user' 5 | 6 | module.exports = setup = (ctx, initialText)-> 7 | pusher = new PusherMock 'key', 'secret' 8 | queue = new MessageQueue pusher 9 | User.addMe().colour = 'red' 10 | ctx.activationPromise = atom.packages.activatePackage('atom-pair') 11 | ctx.openedEditor = atom.workspace.open().then (editor) => 12 | ctx.sharePane = new SharePane({ 13 | editor: editor, 14 | pusher: pusher, 15 | sessionId: 'yolo', 16 | markerColour: 'red', 17 | id: 'a', 18 | queue: queue 19 | }) 20 | ctx.buffer = ctx.sharePane.editor.buffer 21 | if initialText then ctx.buffer.setText(initialText) 22 | -------------------------------------------------------------------------------- /spec/invitations/invitation-spec.coffee: -------------------------------------------------------------------------------- 1 | Invitation = require '../../lib/modules/invitations/invitation' 2 | HipChatInvitation = require '../../lib/modules/invitations/hipchat_invitation' 3 | SlackInvitation = require '../../lib/modules/invitations/slack_invitation' 4 | Session = require '../../lib/modules/session' 5 | PusherMock = require '../pusher-mock' 6 | User = require '../../lib/modules/user' 7 | 8 | describe 'Invitation', -> 9 | 10 | activationPromise = null 11 | 12 | beforeEach -> 13 | activationPromise = atom.packages.activatePackage('atom-pair') 14 | pusher = new PusherMock 15 | spyOn(window, 'Pusher').andReturn(pusher) 16 | 17 | it 'complains if there are no Pusher keys', -> 18 | waitsForPromise -> activationPromise 19 | runs -> 20 | atom.config.set('atom-pair.pusher_app_key', '') 21 | atom.config.set('atom-pair.pusher_app_secret', '') 22 | session = new Session 23 | spyOn(atom.notifications, 'addError') 24 | invitation = new Invitation(session) 25 | expect(atom.notifications.addError).toHaveBeenCalled() 26 | 27 | it 'writes a session ID to clipboard and sets up session', -> 28 | waitsForPromise -> activationPromise 29 | runs -> 30 | session = new Session 31 | atom.config.set('atom-pair.pusher_app_key', 'key') 32 | atom.config.set('atom-pair.pusher_app_secret', 'secret') 33 | spyOn(atom.clipboard, 'write') 34 | spyOn(atom.notifications, 'addError') 35 | spyOn(atom.notifications, 'addInfo') 36 | spyOn(session, 'pairingSetup') 37 | session.id = 'lala' 38 | invitation = new Invitation(session) 39 | expect(atom.notifications.addError).not.toHaveBeenCalled() 40 | expect(atom.clipboard.write).toHaveBeenCalledWith('lala') 41 | expect(atom.notifications.addInfo).toHaveBeenCalled() 42 | expect(User.me).toBeDefined() 43 | expect(session.pairingSetup).toHaveBeenCalled() 44 | 45 | it 'allows you to invite to an already active session', -> 46 | waitsForPromise -> activationPromise 47 | 48 | runs -> 49 | atom.config.set('atom-pair.pusher_app_key', 'key') 50 | atom.config.set('atom-pair.pusher_app_secret', 'secret') 51 | session = new Session 52 | session.id = "my_session_id" 53 | Session.active = session 54 | spyOn(atom.clipboard, 'write') 55 | Session.initiate(Invitation) 56 | expect(atom.clipboard.write).toHaveBeenCalledWith('my_session_id') 57 | 58 | 59 | 60 | describe 'SlackInvitation', -> 61 | 62 | it 'complains if there is no slack webhook url', -> 63 | waitsForPromise -> activationPromise 64 | 65 | runs -> 66 | session = new Session 67 | atom.config.set('atom-pair.pusher_app_key', 'key') 68 | atom.config.set('atom-pair.pusher_app_secret', 'secret') 69 | spyOn(atom.notifications, 'addError') 70 | invitation = new SlackInvitation(session) 71 | expect(atom.notifications.addError).toHaveBeenCalledWith("Please set your Slack Incoming WebHook") 72 | 73 | it 'sends the slack webhook', -> 74 | waitsForPromise -> activationPromise 75 | 76 | runs -> 77 | mockSlack = { 78 | setWebhook: -> 79 | webhook: (params, done) -> 80 | done() 81 | } 82 | spyOn(SlackInvitation.prototype, 'getSlack').andReturn(mockSlack) 83 | spyOn(SlackInvitation.prototype, 'afterSend') 84 | SlackInvitation.prototype.getRecipientName = (cta, callback) -> 85 | callback() 86 | 87 | spyOn(mockSlack, 'setWebhook') 88 | spyOn(mockSlack, 'webhook').andCallThrough() 89 | spyOn(atom.notifications, 'addInfo') 90 | atom.config.set('atom-pair.pusher_app_key', 'key') 91 | atom.config.set('atom-pair.pusher_app_secret', 'secret') 92 | atom.config.set('atom-pair.slack_url', 'yolo.com') 93 | session = new Session 94 | invitation = new SlackInvitation(session) 95 | expect(mockSlack.setWebhook).toHaveBeenCalledWith('yolo.com') 96 | expect(mockSlack.webhook).toHaveBeenCalled() 97 | expect(atom.notifications.addInfo).toHaveBeenCalled() 98 | expect(SlackInvitation.prototype.afterSend).toHaveBeenCalled() 99 | 100 | describe 'HipChat invitation', -> 101 | 102 | it 'complains if there is no token', -> 103 | waitsForPromise -> activationPromise 104 | 105 | runs -> 106 | session = new Session 107 | atom.config.set('atom-pair.pusher_app_key', 'key') 108 | atom.config.set('atom-pair.pusher_app_secret', 'secret') 109 | spyOn(atom.notifications, 'addError') 110 | invitation = new HipChatInvitation(session) 111 | expect(atom.notifications.addError).toHaveBeenCalledWith("Please set your HipChat keys.") 112 | 113 | it 'sends the hipchat invitation', -> 114 | waitsForPromise -> activationPromise 115 | 116 | runs -> 117 | mockRoom = 'room' 118 | mockHipChat = { 119 | listRooms: (done) -> done(rooms: [{name:mockRoom, room_id: 1}]) 120 | postMessage: (params, done) -> 121 | done() 122 | } 123 | spyOn(HipChatInvitation.prototype, 'getHipChat').andReturn(mockHipChat) 124 | spyOn(HipChatInvitation.prototype, 'afterSend') 125 | HipChatInvitation.prototype.getRecipientName = (cta, callback) -> 126 | @recipient = "@jam" 127 | callback() 128 | 129 | spyOn(atom.notifications, 'addInfo') 130 | spyOn(atom.notifications, 'addError') 131 | spyOn(mockHipChat, 'postMessage').andCallThrough() 132 | atom.config.set('atom-pair.pusher_app_key', 'key') 133 | atom.config.set('atom-pair.pusher_app_secret', 'secret') 134 | atom.config.set('atom-pair.hipchat_room_name', mockRoom) 135 | atom.config.set('atom-pair.hipchat_token', 'token') 136 | session = new Session 137 | invitation = new HipChatInvitation(session) 138 | expect(atom.notifications.addError).not.toHaveBeenCalled() 139 | expect(mockHipChat.postMessage).toHaveBeenCalled() 140 | expect(atom.notifications.addInfo).toHaveBeenCalled() 141 | expect(HipChatInvitation.prototype.afterSend).toHaveBeenCalled() 142 | -------------------------------------------------------------------------------- /spec/message-queue/queue-spec.coffee: -------------------------------------------------------------------------------- 1 | PusherMock = require '../pusher-mock' 2 | MessageQueue = require '../../lib/modules/message_queue' 3 | specSetup = require '../helpers/spec-setup.coffee' 4 | _ = require 'underscore' 5 | 6 | describe 'messagequeue', -> 7 | 8 | activationPromise = null 9 | 10 | beforeEach -> 11 | activationPromise = atom.packages.activatePackage('atom-pair') 12 | pusher = new PusherMock 'key', 'secret' 13 | @queue_ = new MessageQueue pusher 14 | 15 | describe 'q', -> 16 | 17 | it 'should not send more than 10 messages a second', -> 18 | 19 | waitsForPromise -> activationPromise 20 | 21 | runs -> 22 | channel = @queue_.pusher.channel('test-channel') 23 | spyOn(channel, 'trigger') 24 | jasmine.Clock.useMock() 25 | jasmine.Clock.tick(1001) 26 | for num in [1..100] 27 | @queue_.add('test-channel', 'event', {hello: 'world'}) 28 | 29 | rateLimit = channel.trigger.argsForCall.length > 10 30 | expect(rateLimit).toBe(false) 31 | 32 | it 'batches same-pane typing events', -> 33 | 34 | waitsForPromise -> activationPromise 35 | 36 | runs -> 37 | @queue_.cycle = -> 38 | for event in ['test1', 'test2', 'test3'] 39 | @queue_.add('test-channel', 'client-change', event) 40 | for event in ['test4, test5', 'test6'] 41 | @queue_.add('test-channel2', 'client-change', event) 42 | @queue_.add('test-channel2', 'client-buffer-selection', { 43 | "colour": "blue", 44 | "rows": [ 45 | 0, 46 | 1 47 | ] 48 | }) 49 | expect(@queue_.items.length).toBe(3) 50 | 51 | it 'disposes correctly', -> 52 | waitsForPromise -> activationPromise 53 | runs -> 54 | channel = @queue_.pusher.channel('test-channel') 55 | spyOn(channel, 'trigger') 56 | @queue_.add('test-channel', 'event', {hello: 'world'}) 57 | @queue_.dispose() 58 | expect(@queue_.items.length).toBe(0) 59 | @queue_.add('test-channel', 'event', {hello: 'world'}) 60 | jasmine.Clock.useMock() 61 | jasmine.Clock.tick(1001) 62 | expect(channel.trigger).not.toHaveBeenCalled() 63 | -------------------------------------------------------------------------------- /spec/pusher-mock.coffee: -------------------------------------------------------------------------------- 1 | {Emitter} = require 'atom' 2 | _ = require 'underscore' 3 | 4 | module.exports = 5 | class PusherMock 6 | 7 | constructor: (@key, @secret) -> 8 | @connected = true 9 | 10 | subscribe: -> 11 | new ChannelMock 12 | 13 | disconnect: -> 14 | @connected = false 15 | 16 | channel: (arg)-> 17 | @chan ?= new ChannelMock 18 | 19 | mockMembers: (members) -> 20 | new Members members 21 | 22 | 23 | class ChannelMock 24 | 25 | name: 'test-channel' 26 | 27 | constructor: -> 28 | @emitter = new Emitter 29 | 30 | fakeSend: (event, data) -> 31 | @emitter.emit(event, data) 32 | 33 | bind: (event, callback)-> 34 | @emitter.on event, callback 35 | 36 | trigger: (evt, payload) -> 37 | 38 | unsubscribe: -> 39 | 40 | class Members 41 | 42 | constructor: (@members) -> 43 | @members ?= [] 44 | 45 | each: (fn)-> 46 | _.each @members, fn 47 | -------------------------------------------------------------------------------- /spec/session/session-spec.coffee: -------------------------------------------------------------------------------- 1 | Session = require '../../lib/modules/session' 2 | Invitation = require '../../lib/modules/invitations/invitation' 3 | PusherMock = require '../pusher-mock' 4 | User = require '../../lib/modules/user' 5 | SharePane = require '../../lib/modules/share_pane' 6 | PresenceIndicator = require '../../lib/modules/presence_indicator' 7 | _ = require 'underscore' 8 | 9 | describe 'Session', -> 10 | 11 | User.prototype.updatePosition =-> 12 | 13 | activationPromise = null 14 | pusher = null 15 | 16 | beforeEach -> 17 | activationPromise = atom.packages.activatePackage('atom-pair') 18 | pusher = new PusherMock 19 | spyOn(window, 'Pusher').andReturn(pusher) 20 | atom.config.set("atom_pair.pusher_app_key", "key") 21 | atom.config.set("atom_pair.pusher_app_secret","secret") 22 | 23 | it 'complains if joining when already in an active session', -> 24 | waitsForPromise -> activationPromise 25 | 26 | runs -> 27 | spyOn(atom.notifications, 'addError') 28 | session = new Session 29 | Session.active = session 30 | Session.join() 31 | expect(atom.notifications.addError).toHaveBeenCalledWith("It looks like you are already in a pairing session. Please open a new window (cmd+shift+N) to start/join a new one.") 32 | 33 | it 'can be created from an ID', -> 34 | waitsForPromise -> activationPromise 35 | 36 | runs -> 37 | session = Session.fromID("key-secret-randomstring") 38 | expect(session.id).toEqual("key-secret-randomstring") 39 | expect(session.app_key).toEqual("key") 40 | expect(session.app_secret).toEqual("secret") 41 | 42 | describe 'panesync', -> 43 | 44 | setUpSession = -> 45 | atom.config.set('atom-pair.pusher_app_key', 'key') 46 | atom.config.set('atom-pair.pusher_app_secret', 'secret') 47 | session = new Session 48 | new Invitation(session) 49 | session.channel.fakeSend('pusher:subscription_succeeded', pusher.mockMembers()) 50 | session 51 | 52 | it 'syncs over existing panes', -> 53 | waitsForPromise -> activationPromise 54 | openOneEditor = atom.workspace.open().then (editor1)-> 55 | editor1.buffer.setText("hello world") 56 | openSecondEditor = atom.workspace.open().then (editor2) -> 57 | editor2.buffer.setText("waddup") 58 | waitsForPromise -> openOneEditor 59 | waitsForPromise -> openSecondEditor 60 | 61 | runs -> 62 | session = setUpSession() 63 | spyOn(session.queue, 'add') 64 | expect(SharePane.all.length).toEqual(2) 65 | expect(session.active).toBe(true) 66 | expect(User.me.isLeader()).toBe(true) 67 | 68 | # when new person is added 69 | session.channel.fakeSend('pusher:member_added', { 70 | id: 'blue', 71 | arrivalTime: new Date().getTime() 72 | }) 73 | 74 | firstCall = session.queue.add.argsForCall[0] 75 | secondCall = session.queue.add.argsForCall[1] 76 | 77 | expect(firstCall[1]).toEqual('client-please-make-a-share-pane') 78 | expect(secondCall[1]).toEqual('client-please-make-a-share-pane') 79 | expect(firstCall[2].from).toEqual('red') 80 | expect(secondCall[2].from).toEqual('red') 81 | expect(firstCall[2].to).toEqual('blue') 82 | expect(secondCall[2].to).toEqual('blue') 83 | expect(firstCall[2].paneId).not.toEqual(secondCall.paneId) 84 | 85 | # sending over files 86 | SharePane.each (pane) -> 87 | spyOn(pane, 'shareFile') 88 | spyOn(pane, 'sendGrammar') 89 | session.channel.fakeSend('client-i-made-a-share-pane', { 90 | paneId: pane.id, 91 | to: 'red' 92 | }) 93 | expect(pane.shareFile).toHaveBeenCalled() 94 | expect(pane.sendGrammar).toHaveBeenCalled() 95 | _.each atom.workspace.getPaneItems(), (pane) -> pane.destroy() 96 | SharePane.clear() 97 | 98 | it 'creates new tabs on receipt of please-make-a-sharepane event', -> 99 | waitsForPromise -> activationPromise 100 | newEditor = null 101 | session = null 102 | runs -> 103 | Session.prototype.shareOpenPanes = -> 104 | SharePane.prototype.setTabTitle =-> 105 | session = setUpSession() 106 | expect(session.active).toBe(true) 107 | expect(atom.workspace.getTextEditors().length).toEqual(0) 108 | expect(atom.workspace.getActiveTextEditor()).toBe(undefined) 109 | spyOn(session, 'createSharePane').andCallThrough() 110 | spyOn(session.queue, 'add') 111 | session.channel.fakeSend('client-please-make-a-share-pane', { 112 | to: User.me.colour, 113 | from: 'blue' 114 | paneId: 'hello', 115 | title: 'untitled' 116 | }) 117 | 118 | atom.workspace.onDidOpen ({item})-> newEditor = item 119 | waitsFor -> !!newEditor 120 | runs -> 121 | expect(session.queue.add.argsForCall).toEqual([ [ 'test-channel', 'client-i-made-a-share-pane', { to : 'blue', paneId : 'hello' } ] ]) 122 | expect(session.createSharePane.argsForCall).toEqual([[newEditor, 'hello', 'untitled']]) 123 | expect(atom.workspace.getTextEditors().length).toEqual(1) 124 | expect(SharePane.all.length).toEqual(1) 125 | expect(SharePane.all[0].id).toEqual('hello') 126 | SharePane.clear() 127 | 128 | it 'syncs newly opened panes', -> 129 | waitsForPromise -> activationPromise 130 | waitsForPromise -> 131 | atom.packages.activatePackage('language-ruby') 132 | 133 | newEditor = null 134 | session = null 135 | runs -> 136 | session = setUpSession() 137 | 138 | spyOn(session.queue, 'add') 139 | newEditor = atom.workspace.open('../fixtures/basic-buffer-write.json').then (editor)-> 140 | 141 | waitsForPromise -> newEditor 142 | runs -> 143 | pane = SharePane.all[0] 144 | expect(session.queue.add.argsForCall).toEqual([ 145 | ['test-channel', 146 | 'client-please-make-a-share-pane', { 147 | to: 'all', 148 | from: 'red', 149 | paneId: pane.id, 150 | title: 'basic-buffer-write.json' 151 | } 152 | ] 153 | ]) 154 | spyOn(pane, 'shareFile') 155 | spyOn(pane, 'sendGrammar') 156 | session.channel.fakeSend('client-i-made-a-share-pane', { 157 | to: User.me.colour, 158 | paneId: pane.id 159 | }) 160 | expect(pane.shareFile).toHaveBeenCalled() 161 | expect(pane.sendGrammar).toHaveBeenCalled() 162 | SharePane.clear() 163 | 164 | describe 'disconnection', -> 165 | 166 | it 'resets state on disconnect',-> 167 | newEditor = null 168 | session = null 169 | waitsFor -> activationPromise 170 | 171 | runs -> 172 | atom.config.set('atom-pair.pusher_app_key', 'key') 173 | atom.config.set('atom-pair.pusher_app_secret', 'secret') 174 | session = new Session 175 | new Invitation(session) 176 | session.channel.fakeSend('pusher:subscription_succeeded', pusher.mockMembers([ 177 | {id: 'blue', arrivalTime: 1} 178 | ])) 179 | session.channel.fakeSend('client-please-make-a-share-pane', { 180 | to: User.me.colour, 181 | from: 'blue' 182 | paneId: 'hello', 183 | title: 'untitled' 184 | }) 185 | 186 | atom.workspace.onDidOpen ({item})-> newEditor = item 187 | 188 | waitsFor -> !!newEditor 189 | runs -> 190 | expect(User.all.length).toEqual(2) 191 | expect(SharePane.all.length).toEqual(1) 192 | spyOn(session.queue, 'dispose') 193 | session.end() 194 | expect(pusher.connected).toBe(false) 195 | expect(User.all.length).toBe(0) 196 | expect(User.me).toBe(null) 197 | expect(SharePane.all.length).toBe(0) 198 | expect(session.subscriptions.disposed).toBe(true) 199 | expect(session.queue.dispose).toHaveBeenCalled() 200 | expect(session.id).toBe(null) 201 | expect(Session.active).toBe(null) 202 | expect(session.active).toBe(false) 203 | -------------------------------------------------------------------------------- /spec/sharepane/disconnect-spec.coffee: -------------------------------------------------------------------------------- 1 | SharePane = require '../../lib/modules/share_pane' 2 | Session = require '../../lib/modules/session' 3 | Invitation = require '../../lib/modules/invitations/invitation' 4 | PusherMock = require '../pusher-mock' 5 | MessageQueue = require '../../lib/modules/message_queue' 6 | User = require '../../lib/modules/user' 7 | _ = require 'underscore' 8 | 9 | describe 'SharePane:disconnect',-> 10 | 11 | activationPromise = null 12 | pusher = null 13 | 14 | setUpSession = -> 15 | session = Session.initiate(Invitation) 16 | session.channel.fakeSend( 17 | 'pusher:subscription_succeeded', 18 | pusher.mockMembers() 19 | ) 20 | session 21 | 22 | beforeEach -> 23 | atom.config.set('atom-pair.pusher_app_key', 'key') 24 | atom.config.set('atom-pair.pusher_app_secret', 'secret') 25 | activationPromise = atom.packages.activatePackage('atom-pair') 26 | pusher = new PusherMock 'key', 'secret' 27 | spyOn(window, 'Pusher').andReturn(pusher) 28 | 29 | it 'cleans up state upon disconnect', -> 30 | waitsForPromise -> activationPromise 31 | openedEditor = null 32 | runs -> 33 | session = setUpSession() 34 | openedEditor = atom.workspace.open() 35 | 36 | waitsForPromise -> openedEditor 37 | runs -> 38 | expect(SharePane.all.length).toEqual(1) 39 | sharePane = SharePane.all[0] 40 | expect(sharePane.editorListeners.disposed).toBe(false) 41 | expect(sharePane.buffer).toBeDefined() 42 | expect(SharePane.globalEmitter.disposed).toBe(false) 43 | sharePane.disconnect() 44 | expect(SharePane.all.length).toEqual(0) 45 | expect(sharePane.editorListeners.disposed).toBe(true) 46 | expect(sharePane.buffer).toBe(null) 47 | expect(SharePane.globalEmitter.disposed).toBe(true) 48 | 49 | it 'closes the session if all sharepanes have been destroyed', -> 50 | waitsForPromise -> activationPromise 51 | openedEditor1 = null 52 | openedEditor2 = null 53 | session = null 54 | runs -> 55 | session = setUpSession() 56 | openedEditor1 = atom.workspace.open() 57 | openedEditor2 = atom.workspace.open() 58 | 59 | waitsForPromise -> openedEditor1 60 | waitsForPromise -> openedEditor2 61 | runs -> 62 | spyOn(session, 'end') 63 | expect(SharePane.all.length).toBe(2) 64 | SharePane.each (pane) -> 65 | pane.disconnect() 66 | expect(session.end).toHaveBeenCalled() 67 | -------------------------------------------------------------------------------- /spec/sharepane/grammar-sync-spec.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | bufferTriggerTest = require '../helpers/buffer-triggers' 3 | {Range} = require 'atom' 4 | specSetup = require '../helpers/spec-setup' 5 | 6 | describe "sharePane", -> 7 | 8 | awaitPromises = (ctx)-> 9 | waitsForPromise -> ctx.activationPromise 10 | waitsForPromise -> 11 | ctx.openedEditor 12 | 13 | beforeEach -> 14 | specSetup(@) 15 | 16 | describe 'grammarSync', -> 17 | 18 | it 'sends grammar upon grammar change', -> 19 | awaitPromises(@) 20 | 21 | waitsForPromise -> 22 | atom.packages.activatePackage('language-ruby') 23 | 24 | runs -> 25 | queue = @sharePane.queue 26 | spyOn(queue, 'add') unless queue.add.isSpy 27 | editor = @sharePane.editor 28 | grammar = atom.grammars.grammarForScopeName('source.ruby') 29 | editor.setGrammar(grammar) 30 | expect(queue.add.argsForCall).toEqual([ [ 'test-channel', 'client-grammar-sync', 'source.ruby' ] ]) 31 | 32 | it 'handles grammar sync message', -> 33 | awaitPromises(@) 34 | waitsForPromise -> 35 | atom.packages.activatePackage('language-ruby') 36 | 37 | runs -> 38 | @sharePane.channel.fakeSend('client-grammar-sync', 'source.ruby') 39 | editor = @sharePane.editor 40 | grammar = editor.getGrammar().scopeName 41 | expect(grammar).toBe('source.ruby') 42 | -------------------------------------------------------------------------------- /spec/sharepane/sharepane-binds-spec.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | bufferTriggerTest = require '../helpers/buffer-triggers' 3 | specSetup = require '../helpers/spec-setup.coffee' 4 | 5 | describe "sharePane:binds", -> 6 | 7 | awaitPromises = (ctx)-> 8 | waitsForPromise -> ctx.activationPromise 9 | waitsForPromise -> ctx.openedEditor 10 | 11 | beforeEach -> 12 | specSetup(@) 13 | 14 | describe 'sharePane:binds', -> 15 | 16 | it 'should create the right text from an event', -> 17 | awaitPromises(@) 18 | runs -> 19 | testEvents = require '../fixtures/basic-buffer-write' 20 | _.each testEvents, (event) => 21 | @sharePane.channel.fakeSend('client-change', event) 22 | result = @buffer.getText() 23 | expect(result).toBe('hello world') 24 | 25 | it 'should handle insert + linebreak', -> 26 | awaitPromises(@) 27 | runs -> 28 | testEvents = require '../fixtures/insert-and-line-break' 29 | _.each testEvents, (event) => 30 | @sharePane.channel.fakeSend('client-change', event) 31 | result = @buffer.getText() 32 | expect(result).toBe("hello \nworld") 33 | 34 | it 'should handle deletions', -> 35 | awaitPromises(@) 36 | runs -> 37 | testEvents = require '../fixtures/small-deletions' 38 | _.each testEvents, (event) => 39 | @sharePane.channel.fakeSend('client-change', event) 40 | result = @buffer.getText() 41 | expect(result).toBe("hllo wor") 42 | 43 | it 'should handle multiline deletions', -> 44 | awaitPromises(@) 45 | runs -> 46 | testEvents = require '../fixtures/multiline-deletions' 47 | _.each testEvents, (event) => 48 | @sharePane.channel.fakeSend('client-change', event) 49 | result = @buffer.getText() 50 | expected = "i have of late\nwherefore i know not\nlost all my mirth\n\nseems to me\na sterile promontory :(" 51 | expect(result).toBe(expected) 52 | 53 | it 'handles large insertations + subsituting it for small', -> 54 | awaitPromises(@) 55 | runs -> 56 | fs = require 'fs' 57 | davidCopperfield = fs.readFileSync 'spec/fixtures/david_copperfield.txt', {encoding: 'utf8'} 58 | testEvents = require '../fixtures/large-text-for-small' 59 | _.each testEvents, (event, index) => 60 | @sharePane.channel.fakeSend(event[1], event[2]) 61 | if index is 84 then expect(@buffer.getText()).toEqual(davidCopperfield) 62 | 63 | result = @buffer.getText() 64 | expect(result).toBe('lala') 65 | -------------------------------------------------------------------------------- /spec/sharepane/sharepane-triggers-spec.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | bufferTriggerTest = require '../helpers/buffer-triggers' 3 | {Range} = require 'atom' 4 | specSetup = require '../helpers/spec-setup' 5 | 6 | describe "sharePane", -> 7 | 8 | awaitPromises = (ctx)-> 9 | waitsForPromise -> ctx.activationPromise 10 | waitsForPromise -> ctx.openedEditor 11 | 12 | beforeEach -> 13 | specSetup(@) 14 | 15 | describe 'triggers', -> 16 | it 'sends the right event for a basic one-line typing', -> 17 | awaitPromises(@) 18 | runs -> 19 | queue = @sharePane.queue 20 | spyOn queue, 'add' 21 | bufferTriggerTest @buffer, 'basic-buffer-write', queue, => 22 | _.each 'hello world', (char, index) => 23 | @buffer.insert([0, index], char) 24 | 25 | it 'sends the right event for an insert + linebreak', -> 26 | awaitPromises(@) 27 | runs -> 28 | queue = @sharePane.queue 29 | spyOn queue, 'add' 30 | bufferTriggerTest @buffer, 'insert-and-line-break', queue, => 31 | _.each 'hello world', (char, index) => @buffer.insert([0, index], char) 32 | @buffer.insert([0, 6], "\n") 33 | 34 | it 'handles deletions', -> 35 | awaitPromises(@) 36 | runs -> 37 | queue = @sharePane.queue 38 | spyOn queue, 'add' 39 | bufferTriggerTest @buffer, 'small-deletions', queue, => 40 | _.each 'hello world', (char, index) => @buffer.insert([0, index], char) 41 | @buffer.delete(new Range([0, 10], [0,11])) 42 | @buffer.delete(new Range([0, 9], [0,10])) 43 | @buffer.delete(new Range [0,1], [0,2]) 44 | 45 | it 'handles multiline deletions', -> 46 | awaitPromises(@) 47 | runs -> 48 | queue = @sharePane.queue 49 | spyOn queue, 'add' 50 | bufferTriggerTest @buffer, 'multiline-deletions', queue, => 51 | @buffer.setText("i have of late\nwherefore i know not\nlost all my mirth\nand indeed it goes so heavily with my disposition\nthat this goodly frame the earth\nseems to me\na sterile promontory :(") 52 | range = new Range([3,0], [4,32]) 53 | @buffer.delete(range) 54 | 55 | it 'handles large insertions + substituting it for small', -> 56 | awaitPromises(@) 57 | runs -> 58 | queue = @sharePane.queue 59 | spyOn queue, 'add' 60 | fs = require 'fs' 61 | davidCopperfield = fs.readFileSync 'spec/fixtures/david_copperfield.txt', {encoding: 'utf8'} 62 | argsForCall = require '../fixtures/large-text-for-small' 63 | @buffer.setText(davidCopperfield) 64 | @buffer.setTextInRange(new Range([0,0], [313, 0]), 'l') 65 | _.each 'ala', (char, index) => @buffer.insert([0, index + 1], char) 66 | expect(queue.add.argsForCall).toEqual(argsForCall) 67 | 68 | describe 'sharefile', -> 69 | 70 | awaitPromises = (ctx)-> 71 | waitsForPromise -> ctx.activationPromise 72 | waitsForPromise -> ctx.openedEditor 73 | 74 | it 'sends the right event for a small file', -> 75 | specSetup(@, "I'm a little teapot short and stout") 76 | awaitPromises(@) 77 | 78 | runs -> 79 | spyOn(@sharePane.queue, 'add') 80 | @sharePane.shareFile() 81 | expect(@sharePane.queue.add.argsForCall).toEqual([ [ 'test-channel', 'client-share-whole-file', "I'm a little teapot short and stout" ] ]) 82 | 83 | it 'sends the right event for a large file', -> 84 | fs = require 'fs' 85 | davidCopperfield = fs.readFileSync 'spec/fixtures/david_copperfield.txt', {encoding: 'utf8'} 86 | specSetup(@, davidCopperfield) 87 | awaitPromises(@) 88 | 89 | runs -> 90 | spyOn(@sharePane.queue, 'add') 91 | @sharePane.shareFile() 92 | argsForCall = require('../fixtures/large-text-for-small')[0..84] 93 | expect(@sharePane.queue.add.argsForCall).toEqual(argsForCall) 94 | -------------------------------------------------------------------------------- /spec/user/user-spec.coffee: -------------------------------------------------------------------------------- 1 | User = require '../../lib/modules/user' 2 | Session = require '../../lib/modules/session' 3 | SharePane = require '../../lib/modules/share_pane' 4 | PusherMock = require '../pusher-mock' 5 | _ = require 'underscore' 6 | 7 | describe "User", -> 8 | 9 | activationPromise = null 10 | pusher = null 11 | session = null 12 | 13 | beforeEach -> 14 | activationPromise = atom.packages.activatePackage('atom-pair') 15 | pusher = new PusherMock 'key', 'secret' 16 | spyOn(window, 'Pusher').andReturn(pusher) 17 | User.clear() 18 | 19 | afterEach -> 20 | session.end() 21 | expect(User.all.length).toEqual(0) 22 | expect(User.me).toBe(null) 23 | 24 | it 'knows the correct leader in a two person session', -> 25 | 26 | waitsForPromise -> activationPromise 27 | 28 | runs -> 29 | expect(User.all.length).toBe(0) 30 | 31 | session = new Session 32 | SharePane.each = -> 33 | 34 | spyOn(session, 'startPairing').andCallThrough() 35 | spyOn(session, 'resubscribe') 36 | spyOn(User, 'add').andCallThrough() 37 | 38 | me = User.addMe() 39 | me.arrivalTime = 1 40 | 41 | session.pairingSetup() 42 | session.channel.fakeSend('pusher:subscription_succeeded', pusher.mockMembers()) 43 | expect(session.resubscribe).not.toHaveBeenCalled() 44 | 45 | session.channel.fakeSend('pusher:member_added', { 46 | id: 'blue', 47 | arrivalTime: 30 48 | }) 49 | expect(User.add.calls.length).toEqual(2) 50 | expect(User.all.length).toBe(2) 51 | expect(me.isLeader()).toBe(true) 52 | expect(User.withColour('blue').isLeader()).toBe(false) 53 | 54 | it 'handles resubscription logic when !1st person', -> 55 | 56 | waitsForPromise -> activationPromise 57 | 58 | runs -> 59 | spyOn(window, 'Date').andReturn({getTime: -> 30}) 60 | atom.config.set('atom-pair.pusher_app_key', 'key') 61 | atom.config.set('atom-pair.pusher_app_secret', 'secret') 62 | session = new Session 63 | session.pairingSetup() 64 | session.channel.fakeSend('pusher:subscription_succeeded', pusher.mockMembers( 65 | [{id: 'red', arrivalTime: 1}] 66 | )) 67 | 68 | expect(window.Pusher.argsForCall.length).toBe(2) 69 | expect(window.Pusher.argsForCall).toEqual([ [ 'key', { encrypted: true, authTransport : 'client', clientAuth : { key : 'key', secret : 'secret', user_id : 'blank', user_info : { arrivalTime : 'blank' } } } ], [ 'key', { encrypted: true, authTransport : 'client', clientAuth : { key : 'key', secret : 'secret', user_id : 'blue', user_info : { arrivalTime : 30 } } } ] ]) 70 | expect(User.all.length).toBe(2) 71 | expect(User.me.isLeader()).toBe(false) 72 | expect(User.me.colour).not.toBe('red') 73 | 74 | it 'handles the departure of a leader correctly', -> 75 | 76 | waitsForPromise -> activationPromise 77 | 78 | runs -> 79 | 80 | session = new Session 81 | me = User.addMe() 82 | me.colour = 'blue' 83 | me.arrivalTime = 30 84 | session.pairingSetup() 85 | 86 | 87 | session.channel.fakeSend('pusher:subscription_succeeded', pusher.mockMembers( 88 | [{id: 'red', arrivalTime: 1}] 89 | )) 90 | 91 | session.channel.fakeSend('pusher:member_added', { 92 | id: 'green', 93 | arrivalTime: 60 94 | }) 95 | spyOn(atom.notifications, 'addWarning') 96 | expect(User.me.isLeader()).toBe(false) 97 | expect(User.all.length).toEqual(3) 98 | expect(_.pluck(User.all, 'colour').sort()).toEqual(['blue', 'green', 'red']) 99 | 100 | session.channel.fakeSend('pusher:member_removed', { 101 | id: 'red', 102 | arrivalTime: 1 103 | }) 104 | 105 | expect(User.all.length).toEqual(2) 106 | expect(User.me.isLeader()).toBe(true) 107 | expect(atom.notifications.addWarning).toHaveBeenCalledWith('Your pair buddy has left the session.') 108 | -------------------------------------------------------------------------------- /styles/atom_pair.less: -------------------------------------------------------------------------------- 1 | atom-workspace span.atom-pair-exit-view { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | cursor: pointer; 6 | } 7 | 8 | atom-text-editor.editor .syntax--aqua{ 9 | &:extend(.cursor); 10 | background-color: aqua !important; 11 | opacity: 0.4; 12 | } 13 | atom-text-editor.editor .syntax--aquamarine{ 14 | &:extend(.cursor); 15 | background-color: aquamarine !important; 16 | opacity: 0.4; 17 | } 18 | atom-text-editor.editor .syntax--beige{ 19 | &:extend(.cursor); 20 | background-color: beige !important; 21 | opacity: 0.4; 22 | } 23 | atom-text-editor.editor .syntax--bisque{ 24 | &:extend(.cursor); 25 | background-color: bisque !important; 26 | opacity: 0.4; 27 | } 28 | atom-text-editor.editor .syntax--black{ 29 | &:extend(.cursor); 30 | background-color: black !important; 31 | opacity: 0.4; 32 | } 33 | atom-text-editor.editor .syntax--blanchedalmond{ 34 | &:extend(.cursor); 35 | background-color: blanchedalmond !important; 36 | opacity: 0.4; 37 | } 38 | atom-text-editor.editor .syntax--blue{ 39 | &:extend(.cursor); 40 | background-color: blue !important; 41 | opacity: 0.4; 42 | } 43 | atom-text-editor.editor .syntax--blueviolet{ 44 | &:extend(.cursor); 45 | background-color: blueviolet !important; 46 | opacity: 0.4; 47 | } 48 | atom-text-editor.editor .syntax--brown{ 49 | &:extend(.cursor); 50 | background-color: brown !important; 51 | opacity: 0.4; 52 | } 53 | atom-text-editor.editor .syntax--burlywood{ 54 | &:extend(.cursor); 55 | background-color: burlywood !important; 56 | opacity: 0.4; 57 | } 58 | atom-text-editor.editor .syntax--cadetblue{ 59 | &:extend(.cursor); 60 | background-color: cadetblue !important; 61 | opacity: 0.4; 62 | } 63 | atom-text-editor.editor .syntax--chartreuse{ 64 | &:extend(.cursor); 65 | background-color: chartreuse !important; 66 | opacity: 0.4; 67 | } 68 | atom-text-editor.editor .syntax--chocolate{ 69 | &:extend(.cursor); 70 | background-color: chocolate !important; 71 | opacity: 0.4; 72 | } 73 | atom-text-editor.editor .syntax--coral{ 74 | &:extend(.cursor); 75 | background-color: coral !important; 76 | opacity: 0.4; 77 | } 78 | atom-text-editor.editor .syntax--cornflowerblue{ 79 | &:extend(.cursor); 80 | background-color: cornflowerblue !important; 81 | opacity: 0.4; 82 | } 83 | atom-text-editor.editor .syntax--cornsilk{ 84 | &:extend(.cursor); 85 | background-color: cornsilk !important; 86 | opacity: 0.4; 87 | } 88 | atom-text-editor.editor .syntax--crimson{ 89 | &:extend(.cursor); 90 | background-color: crimson !important; 91 | opacity: 0.4; 92 | } 93 | atom-text-editor.editor .syntax--cyan{ 94 | &:extend(.cursor); 95 | background-color: cyan !important; 96 | opacity: 0.4; 97 | } 98 | atom-text-editor.editor .syntax--darkblue{ 99 | &:extend(.cursor); 100 | background-color: darkblue !important; 101 | opacity: 0.4; 102 | } 103 | atom-text-editor.editor .syntax--darkcyan{ 104 | &:extend(.cursor); 105 | background-color: darkcyan !important; 106 | opacity: 0.4; 107 | } 108 | atom-text-editor.editor .syntax--darkgoldenrod{ 109 | &:extend(.cursor); 110 | background-color: darkgoldenrod !important; 111 | opacity: 0.4; 112 | } 113 | atom-text-editor.editor .syntax--darkgray{ 114 | &:extend(.cursor); 115 | background-color: darkgray !important; 116 | opacity: 0.4; 117 | } 118 | atom-text-editor.editor .syntax--darkgreen{ 119 | &:extend(.cursor); 120 | background-color: darkgreen !important; 121 | opacity: 0.4; 122 | } 123 | atom-text-editor.editor .syntax--darkkhaki{ 124 | &:extend(.cursor); 125 | background-color: darkkhaki !important; 126 | opacity: 0.4; 127 | } 128 | atom-text-editor.editor .syntax--darkmagenta{ 129 | &:extend(.cursor); 130 | background-color: darkmagenta !important; 131 | opacity: 0.4; 132 | } 133 | atom-text-editor.editor .syntax--darkolivegreen{ 134 | &:extend(.cursor); 135 | background-color: darkolivegreen !important; 136 | opacity: 0.4; 137 | } 138 | atom-text-editor.editor .syntax--darkorange{ 139 | &:extend(.cursor); 140 | background-color: darkorange !important; 141 | opacity: 0.4; 142 | } 143 | atom-text-editor.editor .syntax--darkorchid{ 144 | &:extend(.cursor); 145 | background-color: darkorchid !important; 146 | opacity: 0.4; 147 | } 148 | atom-text-editor.editor .syntax--darkred{ 149 | &:extend(.cursor); 150 | background-color: darkred !important; 151 | opacity: 0.4; 152 | } 153 | atom-text-editor.editor .syntax--darksalmon{ 154 | &:extend(.cursor); 155 | background-color: darksalmon !important; 156 | opacity: 0.4; 157 | } 158 | atom-text-editor.editor .syntax--darkseagreen{ 159 | &:extend(.cursor); 160 | background-color: darkseagreen !important; 161 | opacity: 0.4; 162 | } 163 | atom-text-editor.editor .syntax--darkslateblue{ 164 | &:extend(.cursor); 165 | background-color: darkslateblue !important; 166 | opacity: 0.4; 167 | } 168 | atom-text-editor.editor .syntax--darkslategray{ 169 | &:extend(.cursor); 170 | background-color: darkslategray !important; 171 | opacity: 0.4; 172 | } 173 | atom-text-editor.editor .syntax--darkturquoise{ 174 | &:extend(.cursor); 175 | background-color: darkturquoise !important; 176 | opacity: 0.4; 177 | } 178 | atom-text-editor.editor .syntax--darkviolet{ 179 | &:extend(.cursor); 180 | background-color: darkviolet !important; 181 | opacity: 0.4; 182 | } 183 | atom-text-editor.editor .syntax--deeppink{ 184 | &:extend(.cursor); 185 | background-color: deeppink !important; 186 | opacity: 0.4; 187 | } 188 | atom-text-editor.editor .syntax--deepskyblue{ 189 | &:extend(.cursor); 190 | background-color: deepskyblue !important; 191 | opacity: 0.4; 192 | } 193 | atom-text-editor.editor .syntax--dimgray{ 194 | &:extend(.cursor); 195 | background-color: dimgray !important; 196 | opacity: 0.4; 197 | } 198 | atom-text-editor.editor .syntax--dodgerblue{ 199 | &:extend(.cursor); 200 | background-color: dodgerblue !important; 201 | opacity: 0.4; 202 | } 203 | atom-text-editor.editor .syntax--firebrick{ 204 | &:extend(.cursor); 205 | background-color: firebrick !important; 206 | opacity: 0.4; 207 | } 208 | atom-text-editor.editor .syntax--forestgreen{ 209 | &:extend(.cursor); 210 | background-color: forestgreen !important; 211 | opacity: 0.4; 212 | } 213 | atom-text-editor.editor .syntax--fuchsia{ 214 | &:extend(.cursor); 215 | background-color: fuchsia !important; 216 | opacity: 0.4; 217 | } 218 | atom-text-editor.editor .syntax--gainsboro{ 219 | &:extend(.cursor); 220 | background-color: gainsboro !important; 221 | opacity: 0.4; 222 | } 223 | atom-text-editor.editor .syntax--gold{ 224 | &:extend(.cursor); 225 | background-color: gold !important; 226 | opacity: 0.4; 227 | } 228 | atom-text-editor.editor .syntax--goldenrod{ 229 | &:extend(.cursor); 230 | background-color: goldenrod !important; 231 | opacity: 0.4; 232 | } 233 | atom-text-editor.editor .syntax--gray{ 234 | &:extend(.cursor); 235 | background-color: gray !important; 236 | opacity: 0.4; 237 | } 238 | atom-text-editor.editor .syntax--green{ 239 | &:extend(.cursor); 240 | background-color: green !important; 241 | opacity: 0.4; 242 | } 243 | atom-text-editor.editor .syntax--greenyellow{ 244 | &:extend(.cursor); 245 | background-color: greenyellow !important; 246 | opacity: 0.4; 247 | } 248 | atom-text-editor.editor .syntax--hotpink{ 249 | &:extend(.cursor); 250 | background-color: hotpink !important; 251 | opacity: 0.4; 252 | } 253 | atom-text-editor.editor .syntax--indianred{ 254 | &:extend(.cursor); 255 | background-color: indianred !important; 256 | opacity: 0.4; 257 | } 258 | atom-text-editor.editor .syntax--indigo{ 259 | &:extend(.cursor); 260 | background-color: indigo !important; 261 | opacity: 0.4; 262 | } 263 | atom-text-editor.editor .syntax--khaki{ 264 | &:extend(.cursor); 265 | background-color: khaki !important; 266 | opacity: 0.4; 267 | } 268 | atom-text-editor.editor .syntax--lavender{ 269 | &:extend(.cursor); 270 | background-color: lavender !important; 271 | opacity: 0.4; 272 | } 273 | atom-text-editor.editor .syntax--lavenderblush{ 274 | &:extend(.cursor); 275 | background-color: lavenderblush !important; 276 | opacity: 0.4; 277 | } 278 | atom-text-editor.editor .syntax--lawngreen{ 279 | &:extend(.cursor); 280 | background-color: lawngreen !important; 281 | opacity: 0.4; 282 | } 283 | atom-text-editor.editor .syntax--lightblue{ 284 | &:extend(.cursor); 285 | background-color: lightblue !important; 286 | opacity: 0.4; 287 | } 288 | atom-text-editor.editor .syntax--lightcoral{ 289 | &:extend(.cursor); 290 | background-color: lightcoral !important; 291 | opacity: 0.4; 292 | } 293 | atom-text-editor.editor .syntax--lightgray{ 294 | &:extend(.cursor); 295 | background-color: lightgray !important; 296 | opacity: 0.4; 297 | } 298 | atom-text-editor.editor .syntax--lightgreen{ 299 | &:extend(.cursor); 300 | background-color: lightgreen !important; 301 | opacity: 0.4; 302 | } 303 | atom-text-editor.editor .syntax--lightpink{ 304 | &:extend(.cursor); 305 | background-color: lightpink !important; 306 | opacity: 0.4; 307 | } 308 | atom-text-editor.editor .syntax--lightsalmon{ 309 | &:extend(.cursor); 310 | background-color: lightsalmon !important; 311 | opacity: 0.4; 312 | } 313 | atom-text-editor.editor .syntax--lightseagreen{ 314 | &:extend(.cursor); 315 | background-color: lightseagreen !important; 316 | opacity: 0.4; 317 | } 318 | atom-text-editor.editor .syntax--lightskyblue{ 319 | &:extend(.cursor); 320 | background-color: lightskyblue !important; 321 | opacity: 0.4; 322 | } 323 | atom-text-editor.editor .syntax--lightslategray{ 324 | &:extend(.cursor); 325 | background-color: lightslategray !important; 326 | opacity: 0.4; 327 | } 328 | atom-text-editor.editor .syntax--lightsteelblue{ 329 | &:extend(.cursor); 330 | background-color: lightsteelblue !important; 331 | opacity: 0.4; 332 | } 333 | atom-text-editor.editor .syntax--lime{ 334 | &:extend(.cursor); 335 | background-color: lime !important; 336 | opacity: 0.4; 337 | } 338 | atom-text-editor.editor .syntax--limegreen{ 339 | &:extend(.cursor); 340 | background-color: limegreen !important; 341 | opacity: 0.4; 342 | } 343 | atom-text-editor.editor .syntax--magenta{ 344 | &:extend(.cursor); 345 | background-color: magenta !important; 346 | opacity: 0.4; 347 | } 348 | atom-text-editor.editor .syntax--maroon{ 349 | &:extend(.cursor); 350 | background-color: maroon !important; 351 | opacity: 0.4; 352 | } 353 | atom-text-editor.editor .syntax--mediumaquamarine{ 354 | &:extend(.cursor); 355 | background-color: mediumaquamarine !important; 356 | opacity: 0.4; 357 | } 358 | atom-text-editor.editor .syntax--mediumblue{ 359 | &:extend(.cursor); 360 | background-color: mediumblue !important; 361 | opacity: 0.4; 362 | } 363 | atom-text-editor.editor .syntax--mediumorchid{ 364 | &:extend(.cursor); 365 | background-color: mediumorchid !important; 366 | opacity: 0.4; 367 | } 368 | atom-text-editor.editor .syntax--mediumpurple{ 369 | &:extend(.cursor); 370 | background-color: mediumpurple !important; 371 | opacity: 0.4; 372 | } 373 | atom-text-editor.editor .syntax--mediumseagreen{ 374 | &:extend(.cursor); 375 | background-color: mediumseagreen !important; 376 | opacity: 0.4; 377 | } 378 | atom-text-editor.editor .syntax--mediumslateblue{ 379 | &:extend(.cursor); 380 | background-color: mediumslateblue !important; 381 | opacity: 0.4; 382 | } 383 | atom-text-editor.editor .syntax--mediumspringgreen{ 384 | &:extend(.cursor); 385 | background-color: mediumspringgreen !important; 386 | opacity: 0.4; 387 | } 388 | atom-text-editor.editor .syntax--mediumturquoise{ 389 | &:extend(.cursor); 390 | background-color: mediumturquoise !important; 391 | opacity: 0.4; 392 | } 393 | atom-text-editor.editor .syntax--mediumvioletred{ 394 | &:extend(.cursor); 395 | background-color: mediumvioletred !important; 396 | opacity: 0.4; 397 | } 398 | atom-text-editor.editor .syntax--midnightblue{ 399 | &:extend(.cursor); 400 | background-color: midnightblue !important; 401 | opacity: 0.4; 402 | } 403 | atom-text-editor.editor .syntax--mistyrose{ 404 | &:extend(.cursor); 405 | background-color: mistyrose !important; 406 | opacity: 0.4; 407 | } 408 | atom-text-editor.editor .syntax--moccasin{ 409 | &:extend(.cursor); 410 | background-color: moccasin !important; 411 | opacity: 0.4; 412 | } 413 | atom-text-editor.editor .syntax--navy{ 414 | &:extend(.cursor); 415 | background-color: navy !important; 416 | opacity: 0.4; 417 | } 418 | atom-text-editor.editor .syntax--olive{ 419 | &:extend(.cursor); 420 | background-color: olive !important; 421 | opacity: 0.4; 422 | } 423 | atom-text-editor.editor .syntax--olivedrab{ 424 | &:extend(.cursor); 425 | background-color: olivedrab !important; 426 | opacity: 0.4; 427 | } 428 | atom-text-editor.editor .syntax--orange{ 429 | &:extend(.cursor); 430 | background-color: orange !important; 431 | opacity: 0.4; 432 | } 433 | atom-text-editor.editor .syntax--orangered{ 434 | &:extend(.cursor); 435 | background-color: orangered !important; 436 | opacity: 0.4; 437 | } 438 | atom-text-editor.editor .syntax--orchid{ 439 | &:extend(.cursor); 440 | background-color: orchid !important; 441 | opacity: 0.4; 442 | } 443 | atom-text-editor.editor .syntax--palegoldenrod{ 444 | &:extend(.cursor); 445 | background-color: palegoldenrod !important; 446 | opacity: 0.4; 447 | } 448 | atom-text-editor.editor .syntax--palegreen{ 449 | &:extend(.cursor); 450 | background-color: palegreen !important; 451 | opacity: 0.4; 452 | } 453 | atom-text-editor.editor .syntax--paleturquoise{ 454 | &:extend(.cursor); 455 | background-color: paleturquoise !important; 456 | opacity: 0.4; 457 | } 458 | atom-text-editor.editor .syntax--palevioletred{ 459 | &:extend(.cursor); 460 | background-color: palevioletred !important; 461 | opacity: 0.4; 462 | } 463 | atom-text-editor.editor .syntax--papayawhip{ 464 | &:extend(.cursor); 465 | background-color: papayawhip !important; 466 | opacity: 0.4; 467 | } 468 | atom-text-editor.editor .syntax--peachpuff{ 469 | &:extend(.cursor); 470 | background-color: peachpuff !important; 471 | opacity: 0.4; 472 | } 473 | atom-text-editor.editor .syntax--peru{ 474 | &:extend(.cursor); 475 | background-color: peru !important; 476 | opacity: 0.4; 477 | } 478 | atom-text-editor.editor .syntax--pink{ 479 | &:extend(.cursor); 480 | background-color: pink !important; 481 | opacity: 0.4; 482 | } 483 | atom-text-editor.editor .syntax--plum{ 484 | &:extend(.cursor); 485 | background-color: plum !important; 486 | opacity: 0.4; 487 | } 488 | atom-text-editor.editor .syntax--powderblue{ 489 | &:extend(.cursor); 490 | background-color: powderblue !important; 491 | opacity: 0.4; 492 | } 493 | atom-text-editor.editor .syntax--purple{ 494 | &:extend(.cursor); 495 | background-color: purple !important; 496 | opacity: 0.4; 497 | } 498 | atom-text-editor.editor .syntax--red{ 499 | &:extend(.cursor); 500 | background-color: red !important; 501 | opacity: 0.4; 502 | } 503 | atom-text-editor.editor .syntax--rosybrown{ 504 | &:extend(.cursor); 505 | background-color: rosybrown !important; 506 | opacity: 0.4; 507 | } 508 | atom-text-editor.editor .syntax--royalblue{ 509 | &:extend(.cursor); 510 | background-color: royalblue !important; 511 | opacity: 0.4; 512 | } 513 | atom-text-editor.editor .syntax--saddlebrown{ 514 | &:extend(.cursor); 515 | background-color: saddlebrown !important; 516 | opacity: 0.4; 517 | } 518 | atom-text-editor.editor .syntax--salmon{ 519 | &:extend(.cursor); 520 | background-color: salmon !important; 521 | opacity: 0.4; 522 | } 523 | atom-text-editor.editor .syntax--sandybrown{ 524 | &:extend(.cursor); 525 | background-color: sandybrown !important; 526 | opacity: 0.4; 527 | } 528 | atom-text-editor.editor .syntax--seagreen{ 529 | &:extend(.cursor); 530 | background-color: seagreen !important; 531 | opacity: 0.4; 532 | } 533 | atom-text-editor.editor .syntax--sienna{ 534 | &:extend(.cursor); 535 | background-color: sienna !important; 536 | opacity: 0.4; 537 | } 538 | atom-text-editor.editor .syntax--silver{ 539 | &:extend(.cursor); 540 | background-color: silver !important; 541 | opacity: 0.4; 542 | } 543 | atom-text-editor.editor .syntax--skyblue{ 544 | &:extend(.cursor); 545 | background-color: skyblue !important; 546 | opacity: 0.4; 547 | } 548 | atom-text-editor.editor .syntax--slateblue{ 549 | &:extend(.cursor); 550 | background-color: slateblue !important; 551 | opacity: 0.4; 552 | } 553 | atom-text-editor.editor .syntax--slategray{ 554 | &:extend(.cursor); 555 | background-color: slategray !important; 556 | opacity: 0.4; 557 | } 558 | atom-text-editor.editor .syntax--springgreen{ 559 | &:extend(.cursor); 560 | background-color: springgreen !important; 561 | opacity: 0.4; 562 | } 563 | atom-text-editor.editor .syntax--steelblue{ 564 | &:extend(.cursor); 565 | background-color: steelblue !important; 566 | opacity: 0.4; 567 | } 568 | atom-text-editor.editor .syntax--tan{ 569 | &:extend(.cursor); 570 | background-color: tan !important; 571 | opacity: 0.4; 572 | } 573 | atom-text-editor.editor .syntax--teal{ 574 | &:extend(.cursor); 575 | background-color: teal !important; 576 | opacity: 0.4; 577 | } 578 | atom-text-editor.editor .syntax--thistle{ 579 | &:extend(.cursor); 580 | background-color: thistle !important; 581 | opacity: 0.4; 582 | } 583 | atom-text-editor.editor .syntax--tomato{ 584 | &:extend(.cursor); 585 | background-color: tomato !important; 586 | opacity: 0.4; 587 | } 588 | atom-text-editor.editor .syntax--turquoise{ 589 | &:extend(.cursor); 590 | background-color: turquoise !important; 591 | opacity: 0.4; 592 | } 593 | atom-text-editor.editor .syntax--violet{ 594 | &:extend(.cursor); 595 | background-color: violet !important; 596 | opacity: 0.4; 597 | } 598 | atom-text-editor.editor .syntax--yellow{ 599 | &:extend(.cursor); 600 | background-color: yellow !important; 601 | opacity: 0.4; 602 | } 603 | atom-text-editor.editor .syntax--yellowgreen{ 604 | &:extend(.cursor); 605 | background-color: yellowgreen !important; 606 | opacity: 0.4; 607 | } 608 | --------------------------------------------------------------------------------