├── .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 | [](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 | 
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 | 
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 | 
56 |
57 | ### Collaborate!
58 |
59 | 
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 |
--------------------------------------------------------------------------------