diff --git a/CHANGELOG.md b/CHANGELOG.md
index 16da93e55..61765affb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+* Fixed: Search never completing in the command panel
+
+* Fixed: cmd-n now works when no windows are open
+
+* Fixed: Error selecting a grammar for an untitled editor
+
+* Added: j/k now can be used to navigate the tree view and archive editor
+
* Fixed: Atom can now be launched when ~/.atom/config.cson doesn't exist
* Added: Initial collaboration sessions
* Fixed: Empty lines being deleted via uppercase/downcase command
diff --git a/Gruntfile.coffee b/Gruntfile.coffee
index 2d0e1da1a..10e3fae1a 100644
--- a/Gruntfile.coffee
+++ b/Gruntfile.coffee
@@ -113,5 +113,5 @@ module.exports = (grunt) ->
grunt.registerTask('compile', ['coffee', 'less', 'cson'])
grunt.registerTask('lint', ['coffeelint', 'csslint', 'lesslint'])
grunt.registerTask('ci', ['lint', 'partial-clean', 'update-atom-shell', 'build', 'test'])
- grunt.registerTask('deploy', ['update-atom-shell', 'build', 'codesign'])
+ grunt.registerTask('deploy', ['partial-clean', 'update-atom-shell', 'build', 'codesign'])
grunt.registerTask('default', ['update-atom-shell', 'build', 'set-development-version', 'install'])
diff --git a/docs/packages/authoring-packages.md b/docs/packages/authoring-packages.md
index 6e6c1af7a..677df5fa8 100644
--- a/docs/packages/authoring-packages.md
+++ b/docs/packages/authoring-packages.md
@@ -29,7 +29,7 @@ on creating your first package.
## package.json
-Similar to [npm packages](http://en.wikipedia.org/wiki/Npm_(software)), Atom packages
+Similar to [npm packages](http://en.wikipedia.org/wiki/Npm_(software\)), Atom packages
can contain a _package.json_ file in their top-level directory. This file contains metadata
about the package, such as the path to its "main" module, library dependencies,
and manifests specifying the order in which its resources should be loaded.
diff --git a/package.json b/package.json
index 2779ef254..f3c1534d3 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"ctags": "0.5.0",
"oniguruma": "0.16.0",
"mkdirp": "0.3.5",
- "git-utils": "0.19.0",
+ "git-utils": "0.21.0",
"underscore": "1.4.4",
"d3": "3.0.8",
"coffee-cache": "0.1.0",
@@ -34,6 +34,9 @@
"humanize-plus": "1.1.0",
"semver": "1.1.4",
"guid": "0.0.10",
+ "tantamount": "0.3.0",
+ "coffeestack": "0.4.0",
+ "patrick": "0.2.0",
"c-tmbundle": "1.0.0",
"coffee-script-tmbundle": "2.0.0",
"css-tmbundle": "1.0.0",
diff --git a/resources/mac/atom-Info.plist b/resources/mac/atom-Info.plist
index 9fa80ea8e..d3865ab50 100644
--- a/resources/mac/atom-Info.plist
+++ b/resources/mac/atom-Info.plist
@@ -36,6 +36,17 @@
speakeasy.pem
SUScheduledCheckInterval
3600
+ CFBundleURLTypes
+
+
+ CFBundleURLSchemes
+
+ atom
+
+ CFBundleURLName
+ Atom Shared Session Protocol
+
+
CFBundleDocumentTypes
diff --git a/script/constructicon/build b/script/constructicon/build
index 8abf2180f..e3d40044b 100755
--- a/script/constructicon/build
+++ b/script/constructicon/build
@@ -6,6 +6,7 @@ set -ex
# xcode
cd "$(dirname "$0")/../.."
+rm -fr node_modules
./script/bootstrap
./node_modules/.bin/grunt --build-dir="$BUILT_PRODUCTS_DIR" deploy
diff --git a/script/update-atom-shell b/script/update-atom-shell
index 256e18698..1d42c3ffc 100755
--- a/script/update-atom-shell
+++ b/script/update-atom-shell
@@ -5,7 +5,7 @@ cd "$(dirname "${BASH_SOURCE[0]}" )/.."
TARGET=${1:-atom-shell}
DISTURL="https://gh-contractor-zcbenz.s3.amazonaws.com/atom-shell"
CURRENT_VERSION=$(cat "${TARGET}/version" 2>&1)
-LATEST_VERSION=$(curl -fsSkL $DISTURL/version)
+LATEST_VERSION=23dd5b4da8019d37eb0d4992d933f1351ece5a59
if [ -z "${LATEST_VERSION}" ] ; then
echo "Could determine lastest version of atom-shell" >&2
diff --git a/spec/app/project-spec.coffee b/spec/app/project-spec.coffee
index 7ae1e62cc..e73041c37 100644
--- a/spec/app/project-spec.coffee
+++ b/spec/app/project-spec.coffee
@@ -82,7 +82,8 @@ describe "Project", ->
describe "when passed a path that matches a custom opener", ->
it "returns the resource returned by the custom opener", ->
- expect(project.open("a.foo", hey: "there")).toEqual { foo: "a.foo", options: {hey: "there"} }
+ pathToOpen = project.resolve('a.foo')
+ expect(project.open(pathToOpen, hey: "there")).toEqual { foo: pathToOpen, options: {hey: "there"} }
expect(project.open("bar://baz")).toEqual { bar: "bar://baz" }
describe ".bufferForPath(path)", ->
diff --git a/spec/app/syntax-spec.coffee b/spec/app/syntax-spec.coffee
index 497bb88ff..0cca30ed1 100644
--- a/spec/app/syntax-spec.coffee
+++ b/spec/app/syntax-spec.coffee
@@ -75,6 +75,12 @@ describe "the `syntax` global", ->
expect(syntax.selectGrammar('more.test', '')).toBe grammar1
+ describe "when there is no file path", ->
+ it "does not throw an exception (regression)", ->
+ expect(-> syntax.selectGrammar(null, '#!/usr/bin/ruby')).not.toThrow()
+ expect(-> syntax.selectGrammar(null, '')).not.toThrow()
+ expect(-> syntax.selectGrammar(null, null)).not.toThrow()
+
describe ".removeGrammar(grammar)", ->
it "removes the grammar, so it won't be returned by selectGrammar", ->
grammar = syntax.selectGrammar('foo.js')
diff --git a/spec/atom-reporter.coffee b/spec/atom-reporter.coffee
index 5636647ca..106f71519 100644
--- a/spec/atom-reporter.coffee
+++ b/spec/atom-reporter.coffee
@@ -1,6 +1,19 @@
$ = require 'jquery'
{View, $$} = require 'space-pen'
_ = require 'underscore'
+{convertStackTrace} = require 'coffeestack'
+
+sourceMaps = {}
+formatStackTrace = (stackTrace) ->
+ return stackTrace unless stackTrace
+
+ jasminePath = require.resolve('jasmine')
+ jasminePattern = new RegExp("\\(#{_.escapeRegExp(jasminePath)}:\\d+:\\d+\\)\\s*$")
+ convertedLines = []
+ for line in stackTrace.split('\n')
+ convertedLines.push(line) unless jasminePattern.test(line)
+
+ convertStackTrace(convertedLines.join('\n'), sourceMaps)
module.exports =
class AtomReporter extends View
@@ -42,6 +55,7 @@ class AtomReporter extends View
reportSpecResults: (spec) ->
@completeSpecCount++
+ spec.endedAt = new Date().getTime()
@specComplete(spec)
@updateStatusView(spec)
@@ -99,7 +113,7 @@ class AtomReporter extends View
rootSuite = rootSuite.parentSuite while rootSuite.parentSuite
@message.text rootSuite.description
- time = "#{Math.round((new Date().getTime() - @startedAt.getTime()) / 10)}"
+ time = "#{Math.round((spec.endedAt - @startedAt.getTime()) / 10)}"
time = "0#{time}" if time.length < 3
@time.text "#{time[0...-2]}.#{time[-2..]}s"
@@ -166,9 +180,10 @@ class SpecResultView extends View
@description.html @spec.description
for result in @spec.results().getItems() when not result.passed()
+ stackTrace = formatStackTrace(result.trace.stack)
@specFailures.append $$ ->
@div result.message, class: 'resultMessage fail'
- @div result.trace.stack, class: 'stackTrace' if result.trace.stack
+ @div stackTrace, class: 'stackTrace' if stackTrace
attach: ->
@parentSuiteView().append this
diff --git a/src/app/directory.coffee b/src/app/directory.coffee
index 802c814fd..e4bc243a6 100644
--- a/src/app/directory.coffee
+++ b/src/app/directory.coffee
@@ -49,7 +49,9 @@ class Directory
# pathToCheck - the {String} path to check.
#
# Returns a {Boolean}.
- contains: (pathToCheck='') ->
+ contains: (pathToCheck) ->
+ return false unless pathToCheck
+
if pathToCheck.indexOf(path.join(@getPath(), path.sep)) is 0
true
else if pathToCheck.indexOf(path.join(@getRealPath(), path.sep)) is 0
@@ -62,7 +64,9 @@ class Directory
# fullPath - The {String} path to convert.
#
# Returns a {String}.
- relativize: (fullPath='') ->
+ relativize: (fullPath) ->
+ return fullPath unless fullPath
+
if fullPath is @getPath()
''
else if fullPath.indexOf(path.join(@getPath(), path.sep)) is 0
diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee
index 1fb935b83..df229b420 100644
--- a/src/app/edit-session.coffee
+++ b/src/app/edit-session.coffee
@@ -319,7 +319,7 @@ class EditSession
# Retrieves the current buffer's URI.
#
# Returns a {String}.
- getUri: -> @getPath()
+ getUri: -> @buffer.getUri()
# {Delegates to: Buffer.isRowBlank}
isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow)
diff --git a/src/app/git.coffee b/src/app/git.coffee
index 19cee55cc..7efc6dfc5 100644
--- a/src/app/git.coffee
+++ b/src/app/git.coffee
@@ -10,6 +10,28 @@ GitUtils = require 'git-utils'
# Ultimately, this is an overlay to the native [git-utils](https://github.com/atom/node-git) module.
module.exports =
class Git
+ ### Public ###
+ # Creates a new `Git` instance.
+ #
+ # path - The git repository to open
+ # options - A hash with one key:
+ # refreshOnWindowFocus: A {Boolean} that identifies if the windows should refresh
+ #
+ # Returns a new {Git} object.
+ @open: (path, options) ->
+ return null unless path
+ try
+ new Git(path, options)
+ catch e
+ null
+
+ @exists: (path) ->
+ if git = @open(path)
+ git.destroy()
+ true
+ else
+ false
+
path: null
statuses: null
upstream: null
@@ -57,20 +79,6 @@ class Git
### Public ###
- # Creates a new `Git` instance.
- #
- # path - The git repository to open
- # options - A hash with one key:
- # refreshOnWindowFocus: A {Boolean} that identifies if the windows should refresh
- #
- # Returns a new {Git} object.
- @open: (path, options) ->
- return null unless path
- try
- new Git(path, options)
- catch e
- null
-
# Retrieves the git repository.
#
# Returns a new `Repository`.
@@ -213,6 +221,14 @@ class Git
# Returns an object with two keys, `ahead` and `behind`. These will always be greater than zero.
getLineDiffs: (path, text) -> @getRepo().getLineDiffs(@relativize(path), text)
+ getConfigValue: (key) -> @getRepo().getConfigValue(key)
+
+ getReferenceTarget: (reference) -> @getRepo().getReferenceTarget(reference)
+
+ getAheadBehindCount: (reference) -> @getRepo().getAheadBehindCount(reference)
+
+ hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")?
+
### Internal ###
refreshStatus: ->
diff --git a/src/app/project.coffee b/src/app/project.coffee
index d070c90d2..7d5e6c9d9 100644
--- a/src/app/project.coffee
+++ b/src/app/project.coffee
@@ -193,6 +193,7 @@ class Project
#
# Returns either an {EditSession} (for text) or {ImageEditSession} (for images).
open: (filePath, options={}) ->
+ filePath = @resolve(filePath) if filePath?
for opener in @constructor.openers
return resource if resource = opener(filePath, options)
diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee
index 4c452756c..1df0655c4 100644
--- a/src/app/root-view.coffee
+++ b/src/app/root-view.coffee
@@ -125,7 +125,7 @@ class RootView extends View
# Returns the `EditSession` for the file URI.
open: (path, options = {}) ->
changeFocus = options.changeFocus ? true
- path = project.resolve(path) if path?
+ path = project.relativize(path)
if activePane = @getActivePane()
editSession = activePane.itemForUri(path) ? project.open(path)
activePane.showItem(editSession)
diff --git a/src/app/text-buffer.coffee b/src/app/text-buffer.coffee
index 78cc49b96..1de3265a8 100644
--- a/src/app/text-buffer.coffee
+++ b/src/app/text-buffer.coffee
@@ -147,6 +147,9 @@ class TextBuffer
getPath: ->
@file?.getPath()
+ getUri: ->
+ project?.relativize(@getPath()) ? @getPath()
+
# Sets the path for the file.
#
# path - A {String} representing the new file path
diff --git a/src/app/text-mate-grammar.coffee b/src/app/text-mate-grammar.coffee
index 5796631b0..ce5e0c60b 100644
--- a/src/app/text-mate-grammar.coffee
+++ b/src/app/text-mate-grammar.coffee
@@ -74,11 +74,10 @@ class TextMateGrammar
getScore: (filePath, contents) ->
contents = fsUtils.read(filePath) if not contents? and fsUtils.isFileSync(filePath)
-
if syntax.grammarOverrideForPath(filePath) is @scopeName
- 2 + filePath.length
+ 2 + (filePath?.length ? 0)
else if @matchesContents(contents)
- 1 + filePath.length
+ 1 + (filePath?.length ? 0)
else
@getPathScore(filePath)
diff --git a/src/atom-application.coffee b/src/atom-application.coffee
index 993807ed4..7bb956ab6 100644
--- a/src/atom-application.coffee
+++ b/src/atom-application.coffee
@@ -8,6 +8,7 @@ dialog = require 'dialog'
fs = require 'fs'
path = require 'path'
net = require 'net'
+url = require 'url'
socketPath = '/tmp/atom.sock'
@@ -38,7 +39,7 @@ class AtomApplication
installUpdate: null
version: null
- constructor: ({@resourcePath, pathsToOpen, @version, test, pidToKillWhenClosed, @dev, newWindow}) ->
+ constructor: ({@resourcePath, pathsToOpen, urlsToOpen, @version, test, pidToKillWhenClosed, @dev, newWindow}) ->
global.atomApplication = this
@pidsToOpenWindows = {}
@@ -56,6 +57,8 @@ class AtomApplication
@runSpecs({exitWhenDone: true, @resourcePath})
else if pathsToOpen.length > 0
@openPaths({pathsToOpen, pidToKillWhenClosed, newWindow})
+ else if urlsToOpen.length > 0
+ @openUrl(urlToOpen) for urlToOpen in urlsToOpen
else
# Always open a editor window if this is the first instance of Atom.
@openPath({pidToKillWhenClosed, newWindow})
@@ -124,6 +127,7 @@ class AtomApplication
menus.push
label: 'File'
submenu: [
+ { label: 'New Window', accelerator: 'Command+N', click: => @openPath() }
{ label: 'Open...', accelerator: 'Command+O', click: => @promptForPath() }
{ label: 'Open In Dev Mode...', accelerator: 'Command+Shift+O', click: => @promptForPath(devMode: true) }
]
@@ -170,6 +174,10 @@ class AtomApplication
event.preventDefault()
@openPath({pathToOpen})
+ app.on 'open-url', (event, urlToOpen) =>
+ event.preventDefault()
+ @openUrl(urlToOpen)
+
autoUpdater.on 'ready-for-update-on-quit', (event, version, quitAndUpdate) =>
event.preventDefault()
@installUpdate = quitAndUpdate
@@ -239,6 +247,14 @@ class AtomApplication
console.log("Killing process #{pid} failed: #{error.code}")
delete @pidsToOpenWindows[pid]
+ openUrl: (urlToOpen) ->
+ parsedUrl = url.parse(urlToOpen)
+ if parsedUrl.host is 'session'
+ sessionId = parsedUrl.path.split('/')[1]
+ if sessionId
+ bootstrapScript = 'collaboration/lib/bootstrap'
+ new AtomWindow({bootstrapScript, @resourcePath, sessionId})
+
openConfig: ->
if @configWindow
@configWindow.focus()
diff --git a/src/main.coffee b/src/main.coffee
index d2f2bd05b..5eaa3422f 100644
--- a/src/main.coffee
+++ b/src/main.coffee
@@ -7,6 +7,7 @@ fs = require 'fs'
path = require 'path'
optimist = require 'optimist'
nslog = require 'nslog'
+dialog = require 'dialog'
_ = require 'underscore'
console.log = (args...) ->
@@ -17,11 +18,21 @@ require 'coffee-script'
delegate.browserMainParts.preMainMessageLoopRun = ->
args = parseCommandLine()
- addPathToOpen = (event, filePath) ->
+ addPathToOpen = (event, pathToOpen) ->
event.preventDefault()
- args.pathsToOpen.push(filePath)
+ args.pathsToOpen.push(pathToOpen)
+
+ args.urlsToOpen = []
+ addUrlToOpen = (event, urlToOpen) ->
+ event.preventDefault()
+ args.urlsToOpen.push(urlToOpen)
+
+ app.on 'open-url', (event, urlToOpen) ->
+ event.preventDefault()
+ args.urlsToOpen.push(urlToOpen)
app.on 'open-file', addPathToOpen
+ app.on 'open-url', addUrlToOpen
app.on 'will-finish-launching', ->
setupCrashReporter()
@@ -29,6 +40,7 @@ delegate.browserMainParts.preMainMessageLoopRun = ->
app.on 'finish-launching', ->
app.removeListener 'open-file', addPathToOpen
+ app.removeListener 'open-url', addUrlToOpen
args.pathsToOpen = args.pathsToOpen.map (pathToOpen) ->
path.resolve(args.executedFrom ? process.cwd(), pathToOpen)
diff --git a/src/packages/archive-view/keymaps/archive-view.cson b/src/packages/archive-view/keymaps/archive-view.cson
new file mode 100644
index 000000000..6369a1bae
--- /dev/null
+++ b/src/packages/archive-view/keymaps/archive-view.cson
@@ -0,0 +1,3 @@
+'.archive-view':
+ 'k': 'core:move-up'
+ 'j': 'core:move-down'
diff --git a/src/packages/archive-view/lib/archive-edit-session.coffee b/src/packages/archive-view/lib/archive-edit-session.coffee
index 94d6e7be9..1c8e4c789 100644
--- a/src/packages/archive-view/lib/archive-edit-session.coffee
+++ b/src/packages/archive-view/lib/archive-edit-session.coffee
@@ -7,6 +7,7 @@ File = require 'file'
module.exports=
class ArchiveEditSession
registerDeserializer(this)
+ @version: 1
@activate: ->
Project = require 'project'
@@ -14,10 +15,11 @@ class ArchiveEditSession
new ArchiveEditSession(filePath) if archive.isPathSupported(filePath)
@deserialize: ({path}={}) ->
+ path = project.resolve(path)
if fsUtils.isFileSync(path)
new ArchiveEditSession(path)
else
- console.warn "Could not build edit session for path '#{path}' because that file no longer exists"
+ console.warn "Could not build archive edit session for path '#{path}' because that file no longer exists"
constructor: (@path) ->
@file = new File(@path)
@@ -27,7 +29,7 @@ class ArchiveEditSession
serialize: ->
deserializer: 'ArchiveEditSession'
- path: @path
+ path: @getUri()
getViewClass: ->
require './archive-view'
@@ -38,7 +40,7 @@ class ArchiveEditSession
else
'untitled'
- getUri: -> @path
+ getUri: -> project?.relativize(@getPath()) ? @getPath()
getPath: -> @path
diff --git a/src/packages/collaboration/lib/bootstrap.coffee b/src/packages/collaboration/lib/bootstrap.coffee
index 3c6bf3a65..02d9f7cf8 100644
--- a/src/packages/collaboration/lib/bootstrap.coffee
+++ b/src/packages/collaboration/lib/bootstrap.coffee
@@ -1,27 +1,35 @@
require 'atom'
require 'window'
+
$ = require 'jquery'
{$$} = require 'space-pen'
-{createPeer, connectDocument} = require './session-utils'
-{createSite, Document} = require 'telepath'
+GuestSession = require './guest-session'
-window.setDimensions(width: 350, height: 100)
+window.setDimensions(width: 350, height: 125)
window.setUpEnvironment('editor')
{sessionId} = atom.getLoadSettings()
loadingView = $$ ->
- @div style: 'margin: 10px; text-align: center', =>
- @div "Joining session #{sessionId}"
+ @div style: 'margin: 10px', =>
+ @h4 style: 'text-align: center', 'Joining Session'
+ @div class: 'progress progress-striped active', style: 'margin-bottom: 10px', =>
+ @div class: 'progress-bar', style: 'width: 0%'
+ @div class: 'progress-bar-message', 'Establishing connection\u2026'
$(window.rootViewParentSelector).append(loadingView)
atom.show()
-peer = createPeer()
-connection = peer.connect(sessionId, reliable: true)
-connection.on 'open', ->
- console.log 'connection opened'
- connection.once 'data', (data) ->
- loadingView.remove()
- console.log 'received document'
- atom.windowState = Document.deserialize(data, site: createSite(peer.id))
- connectDocument(atom.windowState, connection)
- window.startEditorWindow()
+updateProgressBar = (message, percentDone) ->
+ loadingView.find('.progress-bar-message').text("#{message}\u2026")
+ loadingView.find('.progress-bar').css('width', "#{percentDone}%")
+
+guestSession = new GuestSession(sessionId)
+guestSession.on 'started', -> loadingView.remove()
+guestSession.on 'connection-opened', -> updateProgressBar('Downloading session data', 25)
+guestSession.on 'connection-document-received', -> updateProgressBar('Synchronizing repository', 50)
+operationsDone = -1
+guestSession.on 'mirror-progress', (message, command, operationCount) ->
+ operationsDone++
+ percentDone = Math.round((operationsDone / operationCount) * 50) + 50
+ updateProgressBar(message, percentDone)
+
+atom.guestSession = guestSession
diff --git a/src/packages/collaboration/lib/collaboration.coffee b/src/packages/collaboration/lib/collaboration.coffee
index e6a57d520..dd1a5f7e6 100644
--- a/src/packages/collaboration/lib/collaboration.coffee
+++ b/src/packages/collaboration/lib/collaboration.coffee
@@ -1,32 +1,33 @@
+GuestView = require './guest-view'
+HostView = require './host-view'
+HostSession = require './host-session'
JoinPromptView = require './join-prompt-view'
-{createSite, Document} = require 'telepath'
-{createPeer, connectDocument} = require './session-utils'
-
-startSession = ->
- peer = createPeer()
- peer.on 'connection', (connection) ->
- connection.on 'open', ->
- console.log 'sending document'
- windowState = atom.getWindowState()
- connection.send(windowState.serialize())
- connectDocument(windowState, connection)
- peer.id
+{getSessionUrl} = require './session-utils'
module.exports =
activate: ->
- sessionId = null
+ hostView = null
- rootView.command 'collaboration:copy-session-id', ->
- pasteboard.write(sessionId) if sessionId
+ if atom.getLoadSettings().sessionId
+ new GuestView(atom.guestSession)
+ else
+ hostSession = new HostSession()
- rootView.command 'collaboration:start-session', ->
- if sessionId = startSession()
- pasteboard.write(sessionId)
+ copySession = ->
+ sessionId = hostSession.getId()
+ pasteboard.write(getSessionUrl(sessionId)) if sessionId
- rootView.command 'collaboration:join-session', ->
- new JoinPromptView (id) ->
- windowSettings =
- bootstrapScript: require.resolve('collaboration/lib/bootstrap')
- resourcePath: window.resourcePath
- sessionId: id
- atom.openWindow(windowSettings)
+ rootView.command 'collaboration:copy-session-id', copySession
+
+ rootView.command 'collaboration:start-session', ->
+ hostView ?= new HostView(hostSession)
+ copySession() if hostSession.start()
+
+ rootView.command 'collaboration:join-session', ->
+ new JoinPromptView (id) ->
+ return unless id
+ windowSettings =
+ bootstrapScript: require.resolve('collaboration/lib/bootstrap')
+ resourcePath: window.resourcePath
+ sessionId: id
+ atom.openWindow(windowSettings)
diff --git a/src/packages/collaboration/lib/guest-session.coffee b/src/packages/collaboration/lib/guest-session.coffee
new file mode 100644
index 000000000..3ed270125
--- /dev/null
+++ b/src/packages/collaboration/lib/guest-session.coffee
@@ -0,0 +1,57 @@
+path = require 'path'
+remote = require 'remote'
+url = require 'url'
+
+_ = require 'underscore'
+patrick = require 'patrick'
+telepath = require 'telepath'
+
+{connectDocument, createPeer} = require './session-utils'
+
+module.exports =
+class GuestSession
+ _.extend @prototype, require('event-emitter')
+
+ participants: null
+ repository: null
+ peer: null
+
+ constructor: (sessionId) ->
+ @peer = createPeer()
+ connection = @peer.connect(sessionId, {reliable: true, connectionId: @getId()})
+ connection.on 'open', =>
+ console.log 'connection opened'
+ @trigger 'connection-opened'
+ connection.once 'data', (data) =>
+ @trigger 'connection-document-received'
+ console.log 'received document', data
+ doc = telepath.Document.deserialize(data.doc, site: telepath.createSite(@getId()))
+ atom.windowState = doc.get('windowState')
+ @repository = doc.get('collaborationState.repositoryState')
+ @participants = doc.get('collaborationState.participants')
+ @participants.on 'changed', =>
+ @trigger 'participants-changed', @participants.toObject()
+ connectDocument(doc, connection)
+ @mirrorRepository(data.repoSnapshot)
+
+ mirrorRepository: (repoSnapshot)->
+ repoUrl = @repository.get('url')
+ [repoName] = url.parse(repoUrl).path.split('/')[-1..]
+ repoName = repoName.replace(/\.git$/, '')
+ repoPath = path.join(remote.require('app').getHomeDir(), 'github', repoName)
+
+ progressCallback = (args...) => @trigger 'mirror-progress', args...
+
+ patrick.mirror repoPath, repoSnapshot, {progressCallback}, (error) =>
+ if error?
+ console.error(error)
+ else
+ @trigger 'started'
+
+ atom.getLoadSettings().initialPath = repoPath
+ window.startEditorWindow()
+ @participants.push
+ id: @getId()
+ email: git.getConfigValue('user.email')
+
+ getId: -> @peer.id
diff --git a/src/packages/collaboration/lib/guest-view.coffee b/src/packages/collaboration/lib/guest-view.coffee
new file mode 100644
index 000000000..ab10b3c77
--- /dev/null
+++ b/src/packages/collaboration/lib/guest-view.coffee
@@ -0,0 +1,35 @@
+{$$, View} = require 'space-pen'
+ParticipantView = require './participant-view'
+
+module.exports =
+class GuestView extends View
+ @content: ->
+ @div class: 'collaboration', tabindex: -1, =>
+ @div class: 'guest'
+ @div outlet: 'participants'
+
+ guestSession: null
+
+ initialize: (@guestSession) ->
+ @guestSession.on 'participants-changed', (participants) =>
+ @updateParticipants(participants)
+
+ @updateParticipants(@guestSession.participants.toObject())
+
+ @attach()
+
+ updateParticipants: (participants) ->
+ @participants.empty()
+ guestId = @guestSession.getId()
+ for participant in participants when participant.id isnt guestId
+ @participants.append(new ParticipantView(participant))
+
+ toggle: ->
+ if @hasParent()
+ @detach()
+ else
+ @attach()
+
+ attach: ->
+ rootView.horizontal.append(this)
+ @focus()
diff --git a/src/packages/collaboration/lib/host-session.coffee b/src/packages/collaboration/lib/host-session.coffee
new file mode 100644
index 000000000..466ac004b
--- /dev/null
+++ b/src/packages/collaboration/lib/host-session.coffee
@@ -0,0 +1,76 @@
+fs = require 'fs'
+
+_ = require 'underscore'
+patrick = require 'patrick'
+telepath = require 'telepath'
+
+{createPeer, connectDocument} = require './session-utils'
+
+module.exports =
+class HostSession
+ _.extend @prototype, require('event-emitter')
+
+ doc: null
+ participants: null
+ peer: null
+ sharing: false
+
+ start: ->
+ return if @peer?
+
+ @peer = createPeer()
+ @doc = telepath.Document.create({}, site: telepath.createSite(@getId()))
+ @doc.set('windowState', atom.windowState)
+ patrick.snapshot project.getPath(), (error, repoSnapshot) =>
+ if error?
+ console.error(error)
+ return
+
+ @doc.set 'collaborationState',
+ participants: []
+ repositoryState:
+ url: git.getConfigValue('remote.origin.url')
+ branch: git.getShortHead()
+
+ @participants = @doc.get('collaborationState.participants')
+ @participants.push
+ id: @getId()
+ email: git.getConfigValue('user.email')
+ @participants.on 'changed', =>
+ @trigger 'participants-changed', @participants.toObject()
+
+ @peer.on 'connection', (connection) =>
+ connection.on 'open', =>
+ console.log 'sending document'
+ connection.send({repoSnapshot, doc: @doc.serialize()})
+ connectDocument(@doc, connection)
+
+ connection.on 'close', =>
+ console.log 'conection closed'
+ @participants.each (participant, index) =>
+ if connection.peer is participant.get('id')
+ @participants.remove(index)
+
+ @peer.on 'open', =>
+ console.log 'sharing session started'
+ @sharing = true
+ @trigger 'started'
+
+ @peer.on 'close', =>
+ console.log 'sharing session stopped'
+ @sharing = false
+ @trigger 'stopped'
+
+ @getId()
+
+ stop: ->
+ return unless @peer?
+
+ @peer.destroy()
+ @peer = null
+
+ getId: ->
+ @peer.id
+
+ isSharing: ->
+ @sharing
diff --git a/src/packages/collaboration/lib/host-view.coffee b/src/packages/collaboration/lib/host-view.coffee
new file mode 100644
index 000000000..31dae50a2
--- /dev/null
+++ b/src/packages/collaboration/lib/host-view.coffee
@@ -0,0 +1,47 @@
+{$$, View} = require 'space-pen'
+ParticipantView = require './participant-view'
+
+module.exports =
+class HostView extends View
+ @content: ->
+ @div class: 'collaboration', tabindex: -1, =>
+ @div outlet: 'share', type: 'button', class: 'share'
+ @div outlet: 'participants'
+
+ hostSession: null
+
+ initialize: (@hostSession) ->
+ if @hostSession.isSharing()
+ @share.addClass('running')
+
+ @share.on 'click', =>
+ @share.disable()
+
+ if @hostSession.isSharing()
+ @hostSession.stop()
+ else
+ @hostSession.start()
+
+ @hostSession.on 'started stopped', =>
+ @share.toggleClass('running').enable()
+
+ @hostSession.on 'participants-changed', (participants) =>
+ @updateParticipants(participants)
+
+ @attach()
+
+ updateParticipants: (participants) ->
+ @participants.empty()
+ hostId = @hostSession.getId()
+ for participant in participants when participant.id isnt hostId
+ @participants.append(new ParticipantView(participant))
+
+ toggle: ->
+ if @hasParent()
+ @detach()
+ else
+ @attach()
+
+ attach: ->
+ rootView.horizontal.append(this)
+ @focus()
diff --git a/src/packages/collaboration/lib/join-prompt-view.coffee b/src/packages/collaboration/lib/join-prompt-view.coffee
index 310456ce9..3e0914f4d 100644
--- a/src/packages/collaboration/lib/join-prompt-view.coffee
+++ b/src/packages/collaboration/lib/join-prompt-view.coffee
@@ -2,7 +2,8 @@
Editor = require 'editor'
$ = require 'jquery'
_ = require 'underscore'
-Guid = require 'guid'
+
+{getSessionId} = require './session-utils'
module.exports =
class JoinPromptView extends View
@@ -19,8 +20,8 @@ class JoinPromptView extends View
@on 'core:cancel', => @remove()
clipboard = pasteboard.read()[0]
- if Guid.isGuid(clipboard)
- @miniEditor.setText(clipboard)
+ if sessionId = getSessionId(clipboard)
+ @miniEditor.setText(sessionId)
@attach()
@@ -29,7 +30,7 @@ class JoinPromptView extends View
@miniEditor.setText('')
confirm: ->
- @confirmed(@miniEditor.getText())
+ @confirmed(@miniEditor.getText().trim())
@remove()
attach: ->
diff --git a/src/packages/collaboration/lib/participant-view.coffee b/src/packages/collaboration/lib/participant-view.coffee
new file mode 100644
index 000000000..e5eec1caf
--- /dev/null
+++ b/src/packages/collaboration/lib/participant-view.coffee
@@ -0,0 +1,12 @@
+crypto = require 'crypto'
+{View} = require 'space-pen'
+
+module.exports =
+class ParticipantView extends View
+ @content: ->
+ @div class: 'participant', =>
+ @img class: 'avatar', outlet: 'avatar'
+
+ initialize: ({id, email}) ->
+ emailMd5 = crypto.createHash('md5').update(email).digest('hex')
+ @avatar.attr('src', "http://www.gravatar.com/avatar/#{emailMd5}?s=32")
diff --git a/src/packages/collaboration/lib/session-utils.coffee b/src/packages/collaboration/lib/session-utils.coffee
index 36de3da08..cf26a6c03 100644
--- a/src/packages/collaboration/lib/session-utils.coffee
+++ b/src/packages/collaboration/lib/session-utils.coffee
@@ -1,7 +1,26 @@
-Peer = require './peer'
+Peer = require '../vendor/peer.js'
Guid = require 'guid'
+url = require 'url'
+
module.exports =
+ getSessionId: (text) ->
+ return null unless text
+
+ text = text.trim()
+ sessionUrl = url.parse(text)
+ if sessionUrl.host is 'session'
+ sessionId = sessionUrl.path.split('/')[1]
+ else
+ sessionId = text
+
+ if Guid.isGuid(sessionId)
+ sessionId
+ else
+ null
+
+ getSessionUrl: (sessionId) -> "atom://session/#{sessionId}"
+
createPeer: ->
id = Guid.create().toString()
new Peer(id, key: '0njqmaln320dlsor')
@@ -9,6 +28,7 @@ module.exports =
connectDocument: (doc, connection) ->
nextOutputEventId = 1
outputListener = (event) ->
+ return unless connection.open
event.id = nextOutputEventId++
console.log 'sending event', event.id, event
connection.send(event)
@@ -39,7 +59,7 @@ module.exports =
queuedEvents.push(event)
connection.on 'close', ->
- doc.off('output', outputListener)
+ doc.off('replicate-change', outputListener)
connection.on 'error', (error) ->
console.error 'connection error', error.stack ? error
diff --git a/src/packages/collaboration/stylesheets/collaboration.less b/src/packages/collaboration/stylesheets/collaboration.less
new file mode 100644
index 000000000..9e2af05e0
--- /dev/null
+++ b/src/packages/collaboration/stylesheets/collaboration.less
@@ -0,0 +1,30 @@
+@import "bootstrap/less/variables.less";
+@import "octicon-mixins.less";
+
+.collaboration {
+ @item-line-height: @line-height-base * 1.25;
+ padding: 10px;
+ -webkit-user-select: none;
+
+ .share {
+ cursor: pointer;
+ .mini-icon(notifications);
+ position: relative;
+ right: -1px;
+ margin-bottom: 5px;
+ }
+
+ .guest {
+ .mini-icon(watchers);
+ position: relative;
+ right: -2px;
+ margin-bottom: 5px;
+ }
+
+ .avatar {
+ border-radius: 3px;
+ height: 16px;
+ width: 16px;
+ margin: 2px;
+ }
+}
diff --git a/src/packages/collaboration/lib/peer.js b/src/packages/collaboration/vendor/peer.js
similarity index 99%
rename from src/packages/collaboration/lib/peer.js
rename to src/packages/collaboration/vendor/peer.js
index fc63d2717..8e6bd6954 100644
--- a/src/packages/collaboration/lib/peer.js
+++ b/src/packages/collaboration/vendor/peer.js
@@ -782,7 +782,7 @@ var util = {
global.attachEvent('onmessage', handleMessage);
}
return setZeroTimeoutPostMessage;
- }(this)),
+ }(window)),
blobToArrayBuffer: function(blob, cb){
var fr = new FileReader();
diff --git a/src/packages/image-view/lib/image-edit-session.coffee b/src/packages/image-view/lib/image-edit-session.coffee
index ac0410350..c4f066b6a 100644
--- a/src/packages/image-view/lib/image-edit-session.coffee
+++ b/src/packages/image-view/lib/image-edit-session.coffee
@@ -8,6 +8,7 @@ _ = require 'underscore'
module.exports=
class ImageEditSession
registerDeserializer(this)
+ @version: 1
@activate: ->
# Files with these extensions will be opened as images
@@ -18,7 +19,8 @@ class ImageEditSession
new ImageEditSession(filePath)
@deserialize: ({path}={}) ->
- if fsUtils.exists(path)
+ path = project.resolve(path)
+ if fsUtils.isFileSync(path)
new ImageEditSession(path)
else
console.warn "Could not build image edit session for path '#{path}' because that file no longer exists"
@@ -27,7 +29,7 @@ class ImageEditSession
serialize: ->
deserializer: 'ImageEditSession'
- path: @path
+ path: @getUri()
getViewClass: ->
require './image-view'
@@ -48,7 +50,7 @@ class ImageEditSession
# Retrieves the URI of the current image.
#
# Returns a {String}.
- getUri: -> @path
+ getUri: -> project?.relativize(@getPath()) ? @getPath()
# Retrieves the path of the current image.
#
diff --git a/src/packages/tree-view/keymaps/tree-view.cson b/src/packages/tree-view/keymaps/tree-view.cson
index 5dd1a928c..1be64e05c 100644
--- a/src/packages/tree-view/keymaps/tree-view.cson
+++ b/src/packages/tree-view/keymaps/tree-view.cson
@@ -12,6 +12,8 @@
'a': 'tree-view:add'
'delete': 'tree-view:remove'
'backspace': 'tree-view:remove'
+ 'k': 'core:move-up'
+ 'j': 'core:move-down'
'.tree-view-dialog .mini.editor':
'enter': 'core:confirm'
diff --git a/src/stdlib/buffered-process.coffee b/src/stdlib/buffered-process.coffee
index 94c75137c..a24381999 100644
--- a/src/stdlib/buffered-process.coffee
+++ b/src/stdlib/buffered-process.coffee
@@ -43,7 +43,7 @@ class BufferedProcess
addNodeDirectoryToPath: (options) ->
options.env ?= process.env
pathSegments = []
- nodeDirectoryPath = path.resolve(process.execPath, '..', '..', '..', '..', 'Resources')
+ nodeDirectoryPath = path.resolve(process.execPath, '..', '..', '..', '..', '..', 'Resources')
pathSegments.push(nodeDirectoryPath)
pathSegments.push(options.env.PATH) if options.env.PATH
options.env = _.extend({}, options.env, PATH: pathSegments.join(path.delimiter))
diff --git a/src/stdlib/underscore-extensions.coffee b/src/stdlib/underscore-extensions.coffee
index 55a9d1435..4eac66b8d 100644
--- a/src/stdlib/underscore-extensions.coffee
+++ b/src/stdlib/underscore-extensions.coffee
@@ -180,52 +180,4 @@ _.mixin
newObject[key] = value if value?
newObject
-originalIsEqual = _.isEqual
-extendedIsEqual = (a, b, aStack=[], bStack=[]) ->
- return originalIsEqual(a, b) if a is b
- return originalIsEqual(a, b) if _.isFunction(a) or _.isFunction(b)
- return a.isEqual(b) if _.isFunction(a?.isEqual)
- return b.isEqual(a) if _.isFunction(b?.isEqual)
-
- stackIndex = aStack.length
- while stackIndex--
- return bStack[stackIndex] is b if aStack[stackIndex] is a
- aStack.push(a)
- bStack.push(b)
-
- equal = false
- if _.isArray(a) and _.isArray(b) and a.length is b.length
- equal = true
- for aElement, i in a
- unless extendedIsEqual(aElement, b[i], aStack, bStack)
- equal = false
- break
- else if _.isObject(a) and _.isObject(b)
- aCtor = a.constructor
- bCtor = b.constructor
- aCtorValid = _.isFunction(aCtor) and aCtor instanceof aCtor
- bCtorValid = _.isFunction(bCtor) and bCtor instanceof bCtor
- if aCtor isnt bCtor and not (aCtorValid and bCtorValid)
- equal = false
- else
- aKeyCount = 0
- equal = true
- for key, aValue of a
- continue unless _.has(a, key)
- aKeyCount++
- unless _.has(b, key) and extendedIsEqual(aValue, b[key], aStack, bStack)
- equal = false
- break
- if equal
- bKeyCount = 0
- for key, bValue of b
- bKeyCount++ if _.has(b, key)
- equal = aKeyCount is bKeyCount
- else
- equal = originalIsEqual(a, b)
-
- aStack.pop()
- bStack.pop()
- equal
-
-_.isEqual = (a, b) -> extendedIsEqual(a, b)
+_.isEqual = require 'tantamount'
diff --git a/static/jasmine.less b/static/jasmine.less
index 33e886f45..310d15669 100644
--- a/static/jasmine.less
+++ b/static/jasmine.less
@@ -158,4 +158,5 @@ body {
border: 1px solid #ddd;
background: white;
white-space: pre;
+ overflow: auto;
}
diff --git a/tasks/update-atom-shell-task.coffee b/tasks/update-atom-shell-task.coffee
index 4e95df45e..2c7091bf1 100644
--- a/tasks/update-atom-shell-task.coffee
+++ b/tasks/update-atom-shell-task.coffee
@@ -1,6 +1,26 @@
+path = require 'path'
+
module.exports = (grunt) ->
{spawn} = require('./task-helpers')(grunt)
+ getAtomShellVersion = ->
+ versionPath = path.join('atom-shell', 'version')
+ if grunt.file.isFile(versionPath)
+ grunt.file.read(versionPath).trim()
+ else
+ null
+
grunt.registerTask 'update-atom-shell', 'Update atom-shell', ->
done = @async()
- spawn cmd: 'script/update-atom-shell', (error) -> done(error)
+ currentVersion = getAtomShellVersion()
+ spawn cmd: 'script/update-atom-shell', (error) ->
+ if error?
+ done(error)
+ else
+ newVersion = getAtomShellVersion()
+ if newVersion and currentVersion isnt newVersion
+ grunt.log.writeln("Rebuilding native modules for new atom-shell version #{newVersion.cyan}.")
+ cmd = path.join('node_modules', '.bin', 'apm')
+ spawn {cmd, args: ['rebuild']}, (error) -> done(error)
+ else
+ done()
diff --git a/themes/atom-dark-ui/collaboration.less b/themes/atom-dark-ui/collaboration.less
new file mode 100644
index 000000000..927c66643
--- /dev/null
+++ b/themes/atom-dark-ui/collaboration.less
@@ -0,0 +1,21 @@
+@runningColor: #99CC99;
+
+.collaboration {
+ background: #1b1c1e;
+ box-shadow:
+ 1px 0 0 #131516,
+ inset -1px 0 0 rgba(255, 255, 255, 0.02),
+ 1px 0 3px rgba(0, 0, 0, 0.2);
+
+ .guest {
+ color: #96CBFE;
+ }
+
+ .running {
+ color: @runningColor;
+
+ &:hover {
+ color: lighten(@runningColor, 15%);
+ }
+ }
+}
diff --git a/themes/atom-dark-ui/package.cson b/themes/atom-dark-ui/package.cson
index bf65ab7d7..6a1ef2223 100644
--- a/themes/atom-dark-ui/package.cson
+++ b/themes/atom-dark-ui/package.cson
@@ -10,4 +10,5 @@
'blurred'
'image-view'
'archive-view'
+ 'collaboration'
]
diff --git a/themes/atom-light-ui/collaboration.less b/themes/atom-light-ui/collaboration.less
new file mode 100644
index 000000000..5079c3763
--- /dev/null
+++ b/themes/atom-light-ui/collaboration.less
@@ -0,0 +1,18 @@
+@runningColor: #f78a46;
+
+.collaboration {
+ background: #dde3e8;
+ border-left: 1px solid #989898;
+
+ .guest {
+ color: #5293d8;
+ }
+
+ .running {
+ color: @runningColor;
+
+ &:hover {
+ color: darken(@runningColor, 10%);
+ }
+ }
+}
diff --git a/themes/atom-light-ui/package.cson b/themes/atom-light-ui/package.cson
index ace6d0f78..9e3f407e3 100644
--- a/themes/atom-light-ui/package.cson
+++ b/themes/atom-light-ui/package.cson
@@ -11,4 +11,5 @@
'blurred'
'image-view'
'archive-view'
+ 'collaboration'
]
diff --git a/themes/atom-light-ui/tree-view.less b/themes/atom-light-ui/tree-view.less
index 5ab39df36..452cabb28 100644
--- a/themes/atom-light-ui/tree-view.less
+++ b/themes/atom-light-ui/tree-view.less
@@ -1,5 +1,8 @@
.tree-view {
background: #dde3e8;
+}
+
+.tree-view-resizer {
border-right: 1px solid #989898;
}
diff --git a/vendor/jasmine-console-reporter.js b/vendor/jasmine-console-reporter.js
index 3d1faad26..28dbcfded 100644
--- a/vendor/jasmine-console-reporter.js
+++ b/vendor/jasmine-console-reporter.js
@@ -1,3 +1,31 @@
+var _ = require('underscore');
+var convertStackTrace = require('coffeestack').convertStackTrace;
+
+var sourceMaps = {};
+var formatStackTrace = function(stackTrace) {
+ if (!stackTrace)
+ return stackTrace;
+
+ // Remove all lines containing jasmine.js path
+ var jasminePath = require.resolve('jasmine');
+ var jasminePattern = new RegExp("\\(" + _.escapeRegExp(jasminePath) + ":\\d+:\\d+\\)\\s*$");
+ var convertedLines = [];
+ var lines = stackTrace.split('\n');
+ for (var i = 0; i < lines.length; i++)
+ if (!jasminePattern.test(lines[i]))
+ convertedLines.push(lines[i]);
+
+ //Remove last util.spawn.callDone line and all lines after it
+ var gruntSpawnPattern = /^\s*at util\.spawn\.callDone\s*\(.*\/grunt\/util\.js:\d+:\d+\)\s*$/
+ for (var i = convertedLines.length - 1; i > 0; i--)
+ if (gruntSpawnPattern.test(convertedLines[i])) {
+ convertedLines = convertedLines.slice(0, i);
+ break;
+ }
+
+ return convertStackTrace(convertedLines.join('\n'), sourceMaps);
+}
+
jasmine.ConsoleReporter = function(doc, logErrors) {
this.logErrors = logErrors == false ? false : true
};
@@ -35,7 +63,7 @@ jasmine.ConsoleReporter.prototype.reportSpecResults = function(spec) {
console.log("\n\n" + message)
console.log((new Array(message.length + 1)).join('-'))
if (result.trace.stack) {
- console.log(result.trace.stack)
+ console.log(formatStackTrace(result.trace.stack));
}
else {
console.log(result.message)