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)