diff --git a/atom.gyp b/atom.gyp deleted file mode 100644 index 46eff41c9..000000000 --- a/atom.gyp +++ /dev/null @@ -1,14 +0,0 @@ -{ - 'targets': [ - { - 'target_name': 'Atom', - 'type': 'none', - 'postbuilds': [ - { - 'postbuild_name': 'Create Atom, basically do everything', - 'action': ['script/constructicon/build'], - }, - ], - }, - ], -} diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index 85edc9c49..d8de9ff55 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -147,8 +147,10 @@ module.exports = (grunt) -> 'dot-atom/**/*.coffee' 'exports/**/*.coffee' 'src/**/*.coffee' - 'tasks/**/*.coffee' - 'Gruntfile.coffee' + ] + build: [ + 'build/tasks/**/*.coffee' + 'build/Gruntfile.coffee' ] test: [ 'spec/*.coffee' @@ -221,7 +223,6 @@ module.exports = (grunt) -> grunt.registerTask('compile', ['coffee', 'prebuild-less', 'cson', 'peg']) grunt.registerTask('lint', ['coffeelint', 'csslint', 'lesslint']) grunt.registerTask('test', ['shell:kill-atom', 'run-specs']) - grunt.registerTask('ci', ['output-disk-space', 'download-atom-shell', 'build', 'set-development-version', 'lint', 'test', 'publish-build']) - grunt.registerTask('deploy', ['partial-clean', 'download-atom-shell', 'build', 'codesign']) + grunt.registerTask('ci', ['output-disk-space', 'download-atom-shell', 'build', 'set-version', 'lint', 'test', 'codesign', 'publish-build']) grunt.registerTask('docs', ['markdown:guides', 'build-docs']) - grunt.registerTask('default', ['download-atom-shell', 'build', 'set-development-version', 'install']) + grunt.registerTask('default', ['download-atom-shell', 'build', 'set-version', 'install']) diff --git a/build/tasks/codesign-task.coffee b/build/tasks/codesign-task.coffee index fd4592eba..93920d7a7 100644 --- a/build/tasks/codesign-task.coffee +++ b/build/tasks/codesign-task.coffee @@ -3,6 +3,23 @@ module.exports = (grunt) -> grunt.registerTask 'codesign', 'Codesign the app', -> done = @async() + + if process.env.XCODE_KEYCHAIN + unlockKeychain (error) -> + if error? + done(error) + else + signApp(done) + else + signApp(done) + + unlockKeychain = (callback) -> + cmd = 'security' + {XCODE_KEYCHAIN_PASSWORD, XCODE_KEYCHAIN} = process.env + args = ['unlock-keychain', '-p', XCODE_KEYCHAIN_PASSWORD, XCODE_KEYCHAIN] + spawn {cmd, args}, (error) -> callback(error) + + signApp = (callback) -> cmd = 'codesign' args = ['-f', '-v', '-s', 'Developer ID Application: GitHub', grunt.config.get('atom.shellAppDir')] - spawn {cmd, args}, (error) -> done(error) + spawn {cmd, args}, (error) -> callback(error) diff --git a/build/tasks/publish-build-task.coffee b/build/tasks/publish-build-task.coffee index 3f8dc909a..19f0aff90 100644 --- a/build/tasks/publish-build-task.coffee +++ b/build/tasks/publish-build-task.coffee @@ -24,13 +24,19 @@ module.exports = (gruntObject) -> done = @async() - createRelease (error, release) -> + createBuildRelease (error, release) -> return done(error) if error? zipApp (error) -> return done(error) if error? uploadAsset release, (error) -> return done(error) if error? - publishRelease(release, done) + publishRelease release, (error) -> + return done(error) if error? + getAtomDraftRelease (error, release) -> + return done(error) if error? + deleteExistingAsset release, (error) -> + return done(error) if error? + uploadAsset(release, done) logError = (message, error, details) -> grunt.log.error(message) @@ -65,6 +71,18 @@ getRelease = (callback) -> return callback() +getAtomDraftRelease = (callback) -> + atomRepo = new GitHub({repo: 'atom/atom', token}) + atomRepo.getReleases (error, releases=[]) -> + if error? + logError('Fetching atom/atom releases failed', error, releases) + callback(error) + else + for release in releases when release.draft + callback(null, release) + return + callback(new Error('No draft release in atom/atom repo')) + deleteRelease = (release) -> options = uri: release.url @@ -92,7 +110,7 @@ deleteExistingAsset = (release, callback) -> callback() -createRelease = (callback) -> +createBuildRelease = (callback) -> getRelease (error, release) -> if error? callback(error) @@ -123,7 +141,7 @@ createRelease = (callback) -> uploadAsset = (release, callback) -> options = - uri: "https://uploads.github.com/repos/atom/atom-master-builds/releases/#{release.id}/assets?name=#{assetName}" + uri: release.upload_url.replace(/\{.*$/, "?name=#{assetName}") method: 'POST' headers: _.extend({ 'Content-Type': 'application/zip' diff --git a/build/tasks/set-development-version-task.coffee b/build/tasks/set-version-task.coffee similarity index 64% rename from build/tasks/set-development-version-task.coffee rename to build/tasks/set-version-task.coffee index 66bdd1089..be41866a8 100644 --- a/build/tasks/set-development-version-task.coffee +++ b/build/tasks/set-version-task.coffee @@ -4,15 +4,24 @@ path = require 'path' module.exports = (grunt) -> {spawn} = require('./task-helpers')(grunt) - grunt.registerTask 'set-development-version', 'Sets version to current SHA-1', -> + getVersion = (callback) -> + if process.env.JANKY_SHA1 and process.env.JANKY_BRANCH is 'master' + {version} = require(path.join(grunt.config.get('atom.appDir'), 'package.json')) + callback(null, version) + else + cmd = 'git' + args = ['rev-parse', '--short', 'HEAD'] + spawn {cmd, args}, (error, {stdout}={}, code) -> + callback(error, stdout?.trim?()) + + grunt.registerTask 'set-version', 'Set the version in the plist and package.json', -> done = @async() - cmd = 'git' - args = ['rev-parse', '--short', 'HEAD'] - spawn {cmd, args}, (error, result, code) -> - return done(error) if error? + getVersion (error, version) -> + if error? + done(error) + return - version = result.stdout.trim() appDir = grunt.config.get('atom.appDir') # Replace version field of package.json. @@ -32,7 +41,7 @@ module.exports = (grunt) -> strings = CompanyName: 'GitHub, Inc.' - FileDescription: 'The hackable, collaborative editor of tomorrow!' + FileDescription: 'The hackable, collaborative editor' LegalCopyright: 'Copyright (C) 2013 GitHub, Inc. All rights reserved' ProductName: 'Atom' ProductVersion: version diff --git a/package.json b/package.json index 0d189c468..3da9d2244 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.45.0", + "version": "0.46.0", "main": "./src/browser/main.js", "repository": { "type": "git", @@ -48,11 +48,12 @@ "semver": "1.1.4", "space-pen": "3.1.0", "temp": "0.5.0", - "text-buffer": "0.12.0", + "text-buffer": "0.13.0", "underscore-plus": "0.6.1", "theorist": "~0.13.0", "delegato": "~0.4.0", - "mixto": "~0.4.0" + "mixto": "~0.4.0", + "property-accessors": "~0.1.0" }, "packageDependencies": { "atom-dark-syntax": "0.10.0", @@ -68,7 +69,7 @@ "autosave": "0.10.0", "background-tips": "0.4.0", "bookmarks": "0.16.0", - "bracket-matcher": "0.17.0", + "bracket-matcher": "0.18.0", "command-logger": "0.8.0", "command-palette": "0.14.0", "dev-live-reload": "0.22.0", diff --git a/script/cibuild b/script/cibuild index c5b1a0423..6c7cb8ef5 100755 --- a/script/cibuild +++ b/script/cibuild @@ -10,11 +10,9 @@ if (process.platform == 'linux') var homeDir = process.platform == 'win32' ? process.env.USERPROFILE : process.env.HOME; -function readEnvironmentVariables() { - var credentialsPath = '/var/lib/jenkins/config/atomcredentials'; +function loadEnvironmentVariables(filePath) { try { - var credentials = fs.readFileSync(credentialsPath, 'utf8'); - var lines = credentials.trim().split('\n'); + var lines = fs.readFileSync(filePath, 'utf8').trim().split('\n'); for (i in lines) { var parts = lines[i].split('='); var key = parts[0].trim(); @@ -24,6 +22,11 @@ function readEnvironmentVariables() { } catch(error) { } } +function readEnvironmentVariables() { + loadEnvironmentVariables('/var/lib/jenkins/config/atomcredentials') + loadEnvironmentVariables('/var/lib/jenkins/config/xcodekeychain') +} + readEnvironmentVariables(); cp.safeExec.bind(global, 'node script/bootstrap', function(error) { if (error) diff --git a/script/clean b/script/clean index d0c7c93ea..44c921a14 100755 --- a/script/clean +++ b/script/clean @@ -17,6 +17,8 @@ var commands = [ killatom, [__dirname, '..', 'node_modules'], [__dirname, '..', 'build', 'node_modules'], + [__dirname, '..', 'apm', 'node_modules'], + [__dirname, '..', 'vendor', 'apm', 'node_modules'], [__dirname, '..', 'atom-shell'], [home, '.atom', '.node-gyp'], [home, '.atom', 'storage'], diff --git a/script/constructicon/build b/script/constructicon/build deleted file mode 100755 index 647108d85..000000000 --- a/script/constructicon/build +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -set -ex - -# This entire file is a hack so that constructicon can build Atom via -# xcode - -cd "$(dirname "$0")/../.." -rm -fr node_modules -rm -fr vendor/apm/node_modules -./script/bootstrap --no-color -./build/node_modules/.bin/grunt --no-color --build-dir="$BUILT_PRODUCTS_DIR" deploy - -echo "TARGET_BUILD_DIR=$BUILT_PRODUCTS_DIR" -echo "FULL_PRODUCT_NAME=Atom.app" -echo "PRODUCT_NAME=Atom" diff --git a/script/constructicon/prebuild b/script/constructicon/prebuild deleted file mode 100755 index 600a56afa..000000000 --- a/script/constructicon/prebuild +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -set -ex -cd "$(dirname "$0")/../.." -export PATH="atom-shell/Atom.app/Contents/Resources/:${PATH}" - -rm -rf atom.xcodeproj -gyp --depth=. atom.gyp diff --git a/spec/pane-container-model-spec.coffee b/spec/pane-container-spec.coffee similarity index 100% rename from spec/pane-container-model-spec.coffee rename to spec/pane-container-spec.coffee diff --git a/spec/pane-container-view-spec.coffee b/spec/pane-container-view-spec.coffee index fa0bcb83d..c68180c1d 100644 --- a/spec/pane-container-view-spec.coffee +++ b/spec/pane-container-view-spec.coffee @@ -42,7 +42,7 @@ describe "PaneContainerView", -> describe ".focusPreviousPane()", -> it "focuses the pane preceding the focused pane or the last pane if no pane has focus", -> container.attachToDom() - $(document.body).focus() # clear focus + container.getPanes()[0].focus() # activate first pane container.focusPreviousPane() expect(pane3.activeItem).toMatchSelector ':focus' @@ -121,7 +121,7 @@ describe "PaneContainerView", -> describe "serialization", -> it "can be serialized and deserialized, and correctly adjusts dimensions of deserialized panes after attach", -> - newContainer = atom.deserializers.deserialize(container.serialize()) + newContainer = new PaneContainerView(container.model.testSerialization()) expect(newContainer.find('.pane-row > :contains(1)')).toExist() expect(newContainer.find('.pane-row > .pane-column > :contains(2)')).toExist() expect(newContainer.find('.pane-row > .pane-column > :contains(3)')).toExist() @@ -133,7 +133,7 @@ describe "PaneContainerView", -> it "removes empty panes on deserialization", -> # only deserialize pane 1's view successfully TestView.deserialize = ({name}) -> new TestView(name) if name is '1' - newContainer = atom.deserializers.deserialize(container.serialize()) + newContainer = new PaneContainerView(container.model.testSerialization()) expect(newContainer.find('.pane-row, .pane-column')).not.toExist() expect(newContainer.find('> :contains(1)')).toExist() diff --git a/spec/pane-model-spec.coffee b/spec/pane-spec.coffee similarity index 100% rename from spec/pane-model-spec.coffee rename to spec/pane-spec.coffee diff --git a/spec/pane-view-spec.coffee b/spec/pane-view-spec.coffee index 7440f7e73..acbc7323a 100644 --- a/spec/pane-view-spec.coffee +++ b/spec/pane-view-spec.coffee @@ -602,12 +602,12 @@ describe "PaneView", -> describe "serialization", -> it "can serialize and deserialize the pane and all its items", -> - newPane = pane.testSerialization() + newPane = new PaneView(pane.model.testSerialization()) expect(newPane.getItems()).toEqual [view1, editor1, view2, editor2] it "restores the active item on deserialization", -> pane.activateItem(editor2) - newPane = pane.testSerialization() + newPane = new PaneView(pane.model.testSerialization()) expect(newPane.activeItem).toEqual editor2 it "does not show items that cannot be deserialized", -> @@ -618,7 +618,7 @@ describe "PaneView", -> pane.activateItem(new Unserializable) - newPane = pane.testSerialization() + newPane = new PaneView(pane.model.testSerialization()) expect(newPane.activeItem).toEqual pane.items[0] expect(newPane.items.length).toBe pane.items.length - 1 @@ -626,13 +626,13 @@ describe "PaneView", -> container.attachToDom() pane.focus() - container2 = container.testSerialization() + container2 = new PaneContainerView(container.model.testSerialization()) pane2 = container2.getRoot() container2.attachToDom() expect(pane2).toMatchSelector(':has(:focus)') $(document.activeElement).blur() - container3 = container.testSerialization() + container3 = new PaneContainerView(container.model.testSerialization()) pane3 = container3.getRoot() container3.attachToDom() expect(pane3).not.toMatchSelector(':has(:focus)') diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 9c672e7a9..8e939d03d 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -107,7 +107,7 @@ afterEach -> atom.workspaceView?.remove?() atom.workspaceView = null - delete atom.state.workspaceView + delete atom.state.workspace atom.project?.destroy?() atom.project = null diff --git a/spec/window-spec.coffee b/spec/window-spec.coffee index 53f992555..78f90d3b7 100644 --- a/spec/window-spec.coffee +++ b/spec/window-spec.coffee @@ -88,12 +88,12 @@ describe "Window", -> describe ".unloadEditorWindow()", -> it "saves the serialized state of the window so it can be deserialized after reload", -> - workspaceViewState = atom.workspaceView.serialize() + workspaceState = atom.workspace.serialize() syntaxState = atom.syntax.serialize() atom.unloadEditorWindow() - expect(atom.state.workspaceView).toEqual workspaceViewState + expect(atom.state.workspace).toEqual workspaceState expect(atom.state.syntax).toEqual syntaxState expect(atom.saveSync).toHaveBeenCalled() diff --git a/spec/workspace-view-spec.coffee b/spec/workspace-view-spec.coffee index c58712c6d..5fcd7beb0 100644 --- a/spec/workspace-view-spec.coffee +++ b/spec/workspace-view-spec.coffee @@ -3,6 +3,7 @@ Q = require 'q' path = require 'path' temp = require 'temp' PaneView = require '../src/pane-view' +Workspace = require '../src/workspace' describe "WorkspaceView", -> pathToOpen = null @@ -10,7 +11,8 @@ describe "WorkspaceView", -> beforeEach -> atom.project.setPath(atom.project.resolve('dir')) pathToOpen = atom.project.resolve('a') - atom.workspaceView = new WorkspaceView + atom.workspace = new Workspace + atom.workspaceView = new WorkspaceView(atom.workspace) atom.workspaceView.enableKeymap() atom.workspaceView.openSync(pathToOpen) atom.workspaceView.focus() @@ -19,11 +21,12 @@ describe "WorkspaceView", -> viewState = null simulateReload = -> - workspaceState = atom.workspaceView.serialize() + workspaceState = atom.workspace.serialize() projectState = atom.project.serialize() atom.workspaceView.remove() atom.project = atom.deserializers.deserialize(projectState) - atom.workspaceView = WorkspaceView.deserialize(workspaceState) + atom.workspace = Workspace.deserialize(workspaceState) + atom.workspaceView = new WorkspaceView(atom.workspace) atom.workspaceView.attachToDom() describe "when the serialized WorkspaceView has an unsaved buffer", -> @@ -187,7 +190,7 @@ describe "WorkspaceView", -> describe "when the root view is deserialized", -> it "updates the title to contain the project's path", -> - workspaceView2 = atom.deserializers.deserialize(atom.workspaceView.serialize()) + workspaceView2 = new WorkspaceView(atom.workspace.testSerialization()) item = atom.workspaceView.getActivePaneItem() expect(workspaceView2.title).toBe "#{item.getTitle()} - #{atom.project.getPath()}" workspaceView2.remove() diff --git a/src/atom.coffee b/src/atom.coffee index bab3cd0e6..26c19dd30 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -230,8 +230,10 @@ class Atom extends Model # Private: deserializeWorkspaceView: -> + Workspace = require './workspace' WorkspaceView = require './workspace-view' - @workspaceView = @deserializers.deserialize(@state.workspaceView) ? new WorkspaceView + @workspace = Workspace.deserialize(@state.workspace) ? new Workspace + @workspaceView = new WorkspaceView(@workspace) $(@workspaceViewParentSelector).append(@workspaceView) # Private: @@ -277,7 +279,7 @@ class Atom extends Model return if not @project and not @workspaceView @state.syntax = @syntax.serialize() - @state.workspaceView = @workspaceView.serialize() + @state.workspace = @workspace.serialize() @packages.deactivatePackages() @state.packageStates = @packages.packageStates @saveSync() diff --git a/src/pane-container-view.coffee b/src/pane-container-view.coffee index a55a1d65b..7c1ee5533 100644 --- a/src/pane-container-view.coffee +++ b/src/pane-container-view.coffee @@ -1,4 +1,4 @@ -Serializable = require 'serializable' +Delegator = require 'delegato' {$, View} = require './space-pen-extensions' PaneView = require './pane-view' PaneContainer = require './pane-container' @@ -6,11 +6,9 @@ PaneContainer = require './pane-container' # Private: Manages the list of panes within a {WorkspaceView} module.exports = class PaneContainerView extends View - atom.deserializers.add(this) - Serializable.includeInto(this) + Delegator.includeInto(this) - @deserialize: (state) -> - new this(PaneContainer.deserialize(state.model)) + @delegatesMethod 'saveAll', toProperty: 'model' @content: -> @div class: 'panes' @@ -29,14 +27,8 @@ class PaneContainerView extends View viewClass = model.getViewClass() model._view ?= new viewClass(model) - serializeParams: -> - model: @model.serialize() - ### Public ### - itemDestroyed: (item) -> - @trigger 'item-destroyed', [item] - getRoot: -> @children().first().view() @@ -65,9 +57,6 @@ class PaneContainerView extends View @setRoot(null) @trigger 'pane:removed', [child] if child instanceof PaneView - saveAll: -> - pane.saveItems() for pane in @getPanes() - confirmClose: -> saved = true for pane in @getPanes() @@ -105,28 +94,10 @@ class PaneContainerView extends View @getActivePane()?.activeView paneForUri: (uri) -> - for pane in @getPanes() - view = pane.itemForUri(uri) - return pane if view? - null + @viewForModel(@model.paneForUri(uri)) focusNextPane: -> - panes = @getPanes() - if panes.length > 1 - currentIndex = panes.indexOf(@getFocusedPane()) - nextIndex = (currentIndex + 1) % panes.length - panes[nextIndex].focus() - true - else - false + @model.activateNextPane() focusPreviousPane: -> - panes = @getPanes() - if panes.length > 1 - currentIndex = panes.indexOf(@getFocusedPane()) - previousIndex = currentIndex - 1 - previousIndex = panes.length - 1 if previousIndex < 0 - panes[previousIndex].focus() - true - else - false + @model.activatePreviousPane() diff --git a/src/pane-container.coffee b/src/pane-container.coffee index 14803afc8..9f0dfc7ea 100644 --- a/src/pane-container.coffee +++ b/src/pane-container.coffee @@ -1,3 +1,4 @@ +{find} = require 'underscore-plus' {Model} = require 'theorist' Serializable = require 'serializable' Pane = require './pane' @@ -36,14 +37,32 @@ class PaneContainer extends Model getPanes: -> @root?.getPanes() ? [] + paneForUri: (uri) -> + find @getPanes(), (pane) -> pane.itemForUri(uri)? + + saveAll: -> + pane.saveItems() for pane in @getPanes() + activateNextPane: -> panes = @getPanes() if panes.length > 1 currentIndex = panes.indexOf(@activePane) nextIndex = (currentIndex + 1) % panes.length panes[nextIndex].activate() + true else - @activePane = null + false + + activatePreviousPane: -> + panes = @getPanes() + if panes.length > 1 + currentIndex = panes.indexOf(@activePane) + previousIndex = currentIndex - 1 + previousIndex = panes.length - 1 if previousIndex < 0 + panes[previousIndex].activate() + true + else + false onRootChanged: (root) => @unsubscribe(@previousRoot) if @previousRoot? @@ -64,3 +83,6 @@ class PaneContainer extends Model destroyEmptyPanes: -> pane.destroy() for pane in @getPanes() when pane.items.length is 0 + + itemDestroyed: (item) -> + @emit 'item-destroyed', item diff --git a/src/pane-model.coffee b/src/pane-model.coffee deleted file mode 100644 index a6902968a..000000000 --- a/src/pane-model.coffee +++ /dev/null @@ -1,307 +0,0 @@ -{find, compact, extend} = require 'underscore-plus' -{dirname} = require 'path' -{Model, Sequence} = require 'theorist' -Serializable = require 'serializable' -PaneAxis = require './pane-axis' -PaneView = null - -# Public: A container for multiple items, one of which is *active* at a given -# time. With the default packages, a tab is displayed for each item and the -# active item's view is displayed. -module.exports = -class PaneModel extends Model - atom.deserializers.add(this) - Serializable.includeInto(this) - - @properties - container: null - activeItem: null - focused: false - - # Public: Only one pane is considered *active* at a time. A pane is activated - # when it is focused, and when focus returns to the pane container after - # moving to another element such as a panel, it returns to the active pane. - @behavior 'active', -> - @$container - .switch((container) -> container?.$activePane) - .map((activePane) => activePane is this) - .distinctUntilChanged() - - # Private: - constructor: (params) -> - super - - @items = Sequence.fromArray(params?.items ? []) - @activeItem ?= @items[0] - - @subscribe @items.onEach (item) => - if typeof item.on is 'function' - @subscribe item, 'destroyed', => @removeItem(item) - - @subscribe @items.onRemoval (item, index) => - @unsubscribe item if typeof item.on is 'function' - - @activate() if params?.active - - # Private: Called by the Serializable mixin during serialization. - serializeParams: -> - items: compact(@items.map((item) -> item.serialize?())) - activeItemUri: @activeItem?.getUri?() - focused: @focused - active: @active - - # Private: Called by the Serializable mixin during deserialization. - deserializeParams: (params) -> - {items, activeItemUri} = params - params.items = compact(items.map (itemState) -> atom.deserializers.deserialize(itemState)) - params.activeItem = find params.items, (item) -> item.getUri?() is activeItemUri - params - - # Private: Called by the view layer to construct a view for this model. - getViewClass: -> PaneView ?= require './pane-view' - - isActive: -> @active - - # Private: Called by the view layer to indicate that the pane has gained focus. - focus: -> - @focused = true - @activate() unless @isActive() - - # Private: Called by the view layer to indicate that the pane has lost focus. - blur: -> - @focused = false - true # if this is called from an event handler, don't cancel it - - # Public: Makes this pane the *active* pane, causing it to gain focus - # immediately. - activate: -> - @container?.activePane = this - @emit 'activated' - - # Private: - getPanes: -> [this] - - # Public: - getItems: -> - @items.slice() - - # Public: Returns the item at the specified index. - itemAtIndex: (index) -> - @items[index] - - # Public: Makes the next item active. - activateNextItem: -> - index = @getActiveItemIndex() - if index < @items.length - 1 - @activateItemAtIndex(index + 1) - else - @activateItemAtIndex(0) - - # Public: Makes the previous item active. - activatePreviousItem: -> - index = @getActiveItemIndex() - if index > 0 - @activateItemAtIndex(index - 1) - else - @activateItemAtIndex(@items.length - 1) - - # Public: Returns the index of the current active item. - getActiveItemIndex: -> - @items.indexOf(@activeItem) - - # Public: Makes the item at the given index active. - activateItemAtIndex: (index) -> - @activateItem(@itemAtIndex(index)) - - # Public: Makes the given item active, adding the item if necessary. - activateItem: (item) -> - if item? - @addItem(item) - @activeItem = item - - # Public: Adds the item to the pane. - # - # * item: - # The item to add. It can be a model with an associated view or a view. - # * index: - # An optional index at which to add the item. If omitted, the item is - # added to the end. - # - # Returns the added item - addItem: (item, index=@getActiveItemIndex() + 1) -> - return if item in @items - - @items.splice(index, 0, item) - @emit 'item-added', item, index - item - - # Private: - removeItem: (item, destroying) -> - index = @items.indexOf(item) - return if index is -1 - @activateNextItem() if item is @activeItem and @items.length > 1 - @items.splice(index, 1) - @emit 'item-removed', item, index, destroying - @destroy() if @items.length is 0 - - # Public: Moves the given item to the specified index. - moveItem: (item, newIndex) -> - oldIndex = @items.indexOf(item) - @items.splice(oldIndex, 1) - @items.splice(newIndex, 0, item) - @emit 'item-moved', item, newIndex - - # Public: Moves the given item to the given index at another pane. - moveItemToPane: (item, pane, index) -> - pane.addItem(item, index) - @removeItem(item) - - # Public: Destroys the currently active item and make the next item active. - destroyActiveItem: -> - @destroyItem(@activeItem) - false - - # Public: Destroys the given item. If it is the active item, activate the next - # one. If this is the last item, also destroys the pane. - destroyItem: (item) -> - @emit 'before-item-destroyed', item - if @promptToSaveItem(item) - @emit 'item-destroyed', item - @removeItem(item, true) - item.destroy?() - true - else - false - - # Public: Destroys all items and destroys the pane. - destroyItems: -> - @destroyItem(item) for item in @getItems() - - # Public: Destroys all items but the active one. - destroyInactiveItems: -> - @destroyItem(item) for item in @getItems() when item isnt @activeItem - - # Private: Called by model superclass. - destroyed: -> - @container.activateNextPane() if @isActive() - item.destroy?() for item in @items.slice() - - # Public: Prompts the user to save the given item if it can be saved and is - # currently unsaved. - promptToSaveItem: (item) -> - return true unless item.shouldPromptToSave?() - - uri = item.getUri() - chosen = atom.confirm - message: "'#{item.getTitle?() ? item.getUri()}' has changes, do you want to save them?" - detailedMessage: "Your changes will be lost if you close this item without saving." - buttons: ["Save", "Cancel", "Don't Save"] - - switch chosen - when 0 then @saveItem(item, -> true) - when 1 then false - when 2 then true - - # Public: Saves the active item. - saveActiveItem: -> - @saveItem(@activeItem) - - # Public: Saves the active item at a prompted-for location. - saveActiveItemAs: -> - @saveItemAs(@activeItem) - - # Public: Saves the specified item. - # - # * item: The item to save. - # * nextAction: An optional function which will be called after the item is saved. - saveItem: (item, nextAction) -> - if item.getUri?() - item.save?() - nextAction?() - else - @saveItemAs(item, nextAction) - - # Public: Saves the given item at a prompted-for location. - # - # * item: The item to save. - # * nextAction: An optional function which will be called after the item is saved. - saveItemAs: (item, nextAction) -> - return unless item.saveAs? - - itemPath = item.getPath?() - itemPath = dirname(itemPath) if itemPath - path = atom.showSaveDialogSync(itemPath) - if path - item.saveAs(path) - nextAction?() - - # Public: Saves all items. - saveItems: -> - @saveItem(item) for item in @getItems() - - # Public: Returns the first item that matches the given URI or undefined if - # none exists. - itemForUri: (uri) -> - find @items, (item) -> item.getUri?() is uri - - # Public: Activates the first item that matches the given URI. Returns a - # boolean indicating whether a matching item was found. - activateItemForUri: (uri) -> - if item = @itemForUri(uri) - @activateItem(item) - true - else - false - - # Private: - copyActiveItem: -> - @activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize()) - - # Public: Creates a new pane to the left of the receiver. - # - # * params: - # + items: An optional array of items with which to construct the new pane. - # - # Returns the new {PaneModel}. - splitLeft: (params) -> - @split('horizontal', 'before', params) - - # Public: Creates a new pane to the right of the receiver. - # - # * params: - # + items: An optional array of items with which to construct the new pane. - # - # Returns the new {PaneModel}. - splitRight: (params) -> - @split('horizontal', 'after', params) - - # Public: Creates a new pane above the receiver. - # - # * params: - # + items: An optional array of items with which to construct the new pane. - # - # Returns the new {PaneModel}. - splitUp: (params) -> - @split('vertical', 'before', params) - - # Public: Creates a new pane below the receiver. - # - # * params: - # + items: An optional array of items with which to construct the new pane. - # - # Returns the new {PaneModel}. - splitDown: (params) -> - @split('vertical', 'after', params) - - # Private: - split: (orientation, side, params) -> - if @parent.orientation isnt orientation - @parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this]})) - - newPane = new @constructor(extend({focused: true}, params)) - switch side - when 'before' then @parent.insertChildBefore(this, newPane) - when 'after' then @parent.insertChildAfter(this, newPane) - - newPane.activate() - newPane diff --git a/src/pane-view.coffee b/src/pane-view.coffee index fe9da3f31..f021ee2e7 100644 --- a/src/pane-view.coffee +++ b/src/pane-view.coffee @@ -1,6 +1,6 @@ {$, View} = require './space-pen-extensions' -Serializable = require 'serializable' Delegator = require 'delegato' +PropertyAccessors = require 'property-accessors' Pane = require './pane' @@ -12,14 +12,11 @@ Pane = require './pane' # building a package that deals with switching between panes or tiems. module.exports = class PaneView extends View - Serializable.includeInto(this) Delegator.includeInto(this) + PropertyAccessors.includeInto(this) @version: 1 - @deserialize: (state) -> - new this(Pane.deserialize(state.model)) - @content: (wrappedView) -> @div class: 'pane', tabindex: -1, => @div class: 'item-views', outlet: 'itemViews' @@ -54,7 +51,6 @@ class PaneView extends View @subscribe @model, 'item-removed', @onItemRemoved @subscribe @model, 'item-moved', @onItemMoved @subscribe @model, 'before-item-destroyed', @onBeforeItemDestroyed - @subscribe @model, 'item-destroyed', @onItemDestroyed @subscribe @model, 'activated', @onActivated @subscribe @model.$active, @onActiveStatusChanged @@ -85,13 +81,6 @@ class PaneView extends View @command 'pane:close', => @destroyItems() @command 'pane:close-other-items', => @destroyInactiveItems() - deserializeParams: (params) -> - params.model = Pane.deserialize(params.model) - params - - serializeParams: -> - model: @model.serialize() - # Deprecated: Use ::destroyItem removeItem: (item) -> @destroyItem(item) @@ -153,7 +142,6 @@ class PaneView extends View view.show() if @attached view.focus() if hasFocus - @activeView = view @trigger 'pane:active-item-changed', [item] onItemAdded: (item, index) => @@ -166,7 +154,6 @@ class PaneView extends View @viewsByItem.delete(item) if viewToRemove? - viewToRemove.setModel?(null) if destroyed viewToRemove.remove() else @@ -181,15 +168,13 @@ class PaneView extends View @unsubscribe(item) if typeof item.off is 'function' @trigger 'pane:before-item-destroyed', [item] - onItemDestroyed: (item) => - @getContainer()?.itemDestroyed(item) - # Private: activeItemTitleChanged: => @trigger 'pane:active-item-title-changed' # Private: viewForItem: (item) -> + return unless item? if item instanceof $ item else if view = @viewsByItem.get(item) @@ -201,8 +186,7 @@ class PaneView extends View view # Private: - viewForActiveItem: -> - @viewForItem(@activeItem) + @::accessor 'activeView', -> @viewForItem(@activeItem) splitLeft: (items...) -> @model.splitLeft({items})._view diff --git a/src/pane.coffee b/src/pane.coffee index 746cfb989..346f46bce 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -36,7 +36,7 @@ class Pane extends Model @subscribe @items.onEach (item) => if typeof item.on is 'function' - @subscribe item, 'destroyed', => @removeItem(item) + @subscribe item, 'destroyed', => @removeItem(item, true) @subscribe @items.onRemoval (item, index) => @unsubscribe item if typeof item.on is 'function' @@ -142,6 +142,7 @@ class Pane extends Model @activateNextItem() if item is @activeItem and @items.length > 1 @items.splice(index, 1) @emit 'item-removed', item, index, destroying + @container?.itemDestroyed(item) if destroying @destroy() if @items.length is 0 # Public: Moves the given item to the specified index. @@ -166,7 +167,6 @@ class Pane extends Model destroyItem: (item) -> @emit 'before-item-destroyed', item if @promptToSaveItem(item) - @emit 'item-destroyed', item @removeItem(item, true) item.destroy?() true diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index b5bdff230..8c7f8b4ac 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -1,10 +1,11 @@ ipc = require 'ipc' path = require 'path' Q = require 'q' -{$, $$, View} = require './space-pen-extensions' _ = require 'underscore-plus' +Delegator = require 'delegato' +{$, $$, View} = require './space-pen-extensions' fs = require 'fs-plus' -Serializable = require 'serializable' +Workspace = require './workspace' EditorView = require './editor-view' PaneView = require './pane-view' PaneColumnView = require './pane-column-view' @@ -38,10 +39,14 @@ Editor = require './editor' # module.exports = class WorkspaceView extends View - Serializable.includeInto(this) - atom.deserializers.add(this, PaneView, PaneRowView, PaneColumnView, EditorView) + Delegator.includeInto(this) - @version: 2 + @delegatesProperty 'fullScreen', 'destroyedItemUris', toProperty: 'model' + @delegatesMethods 'open', 'openSync', 'openSingletonSync', 'reopenItemSync', + 'saveActivePaneItem', 'saveActivePaneItemAs', 'saveAll', 'destroyActivePaneItem', + toProperty: 'model' + + @version: 4 @configDefaults: ignoredNames: [".git", ".svn", ".DS_Store"] @@ -59,13 +64,14 @@ class WorkspaceView extends View @div class: 'panes', outlet: 'panes' # Private: - initialize: ({panes, @fullScreen}={}) -> - panes ?= new PaneContainerView + initialize: (@model) -> + @model ?= new Workspace + + panes = new PaneContainerView(@model.paneContainer) @panes.replaceWith(panes) @panes = panes - @destroyedItemUris = [] - @subscribe @panes, 'item-destroyed', @onPaneItemDestroyed + @subscribe @model, 'uri-opened', => @trigger 'uri-opened' @updateTitle() @@ -116,16 +122,6 @@ class WorkspaceView extends View @command 'core:save', => @saveActivePaneItem() @command 'core:save-as', => @saveActivePaneItemAs() - # Private: - deserializeParams: (params) -> - params.panes = atom.deserializers.deserialize(params.panes) - params - - # Private: - serializeParams: -> - panes: @panes.serialize() - fullScreen: atom.isFullScreen() - # Private: handleFocus: (e) -> if @getActivePane() @@ -149,81 +145,6 @@ class WorkspaceView extends View confirmClose: -> @panes.confirmClose() - # Public: Asynchronously opens a given a filepath in Atom. - # - # * filePath: A file path - # * options - # + initialLine: The buffer line number to open to. - # - # Returns a promise that resolves to the {Editor} for the file URI. - open: (filePath, options={}) -> - changeFocus = options.changeFocus ? true - filePath = atom.project.resolve(filePath) - initialLine = options.initialLine - activePane = @getActivePane() - - editor = activePane.itemForUri(atom.project.relativize(filePath)) if activePane and filePath - promise = atom.project.open(filePath, {initialLine}) if not editor - - Q(editor ? promise) - .then (editor) => - if not activePane - activePane = new PaneView(editor) - @panes.setRoot(activePane) - - @itemOpened(editor) - activePane.activateItem(editor) - activePane.activate() if changeFocus - @trigger "uri-opened" - editor - .catch (error) -> - console.error(error.stack ? error) - - # Private: Only used in specs - openSync: (uri, {changeFocus, initialLine, pane, split}={}) -> - changeFocus ?= true - pane ?= @getActivePane() - uri = atom.project.relativize(uri) - - if pane - if uri - paneItem = pane.itemForUri(uri) ? atom.project.openSync(uri, {initialLine}) - else - paneItem = atom.project.openSync() - - if split == 'right' - panes = @getPanes() - if panes.length == 1 - pane = panes[0].splitRight() - else - pane = _.last(panes) - else if split == 'left' - pane = @getPanes()[0] - - pane.activateItem(paneItem) - else - paneItem = atom.project.openSync(uri, {initialLine}) - pane = new PaneView(paneItem) - @panes.setRoot(pane) - - @itemOpened(paneItem) - - pane.activate() if changeFocus - paneItem - - openSingletonSync: (uri, {changeFocus, initialLine, split}={}) -> - changeFocus ?= true - uri = atom.project.relativize(uri) - pane = @panes.paneForUri(uri) - - if pane - paneItem = pane.itemForUri(uri) - pane.activateItem(paneItem) - pane.activate() if changeFocus - paneItem - else - @openSync(uri, {changeFocus, initialLine, split}) - # Public: Updates the application's title, based on whichever file is open. updateTitle: -> if projectPath = atom.project.getPath() @@ -242,22 +163,6 @@ class WorkspaceView extends View getEditorViews: -> @panes.find('.pane > .item-views > .editor').map(-> $(this).view()).toArray() - # Private: Retrieves all of the modified buffers that are open and unsaved. - # - # Returns an {Array} of {TextBuffer}s. - getModifiedBuffers: -> - modifiedBuffers = [] - for pane in @getPanes() - for item in pane.getItems() when item instanceof Editor - modifiedBuffers.push item.buffer if item.buffer.isModified() - modifiedBuffers - - # Private: Retrieves all of the paths to open files. - # - # Returns an {Array} of {String}s. - getOpenBufferPaths: -> - _.uniq(_.flatten(@getEditorViews().map (editorView) -> editorView.getOpenBufferPaths())) - # Public: Prepends the element to the top of the window. prependToTop: (element) -> @vertical.prepend(element) @@ -296,39 +201,23 @@ class WorkspaceView extends View # Public: Returns the currently focused item from within the focused {PaneView} getActivePaneItem: -> - @panes.getActivePaneItem() + @model.activePaneItem # Public: Returns the view of the currently focused item. getActiveView: -> @panes.getActiveView() - # Public: destroy/close the active item. - destroyActivePaneItem: -> - @getActivePane()?.destroyActiveItem() - - # Public: save the active item. - saveActivePaneItem: -> - @getActivePane()?.saveActiveItem() - - # Public: save the active item as. - saveActivePaneItemAs: -> - @getActivePane()?.saveActiveItemAs() - # Public: Focuses the previous pane by id. - focusPreviousPane: -> @panes.focusPreviousPane() + focusPreviousPane: -> @model.activatePreviousPane() # Public: Focuses the next pane by id. - focusNextPane: -> @panes.focusNextPane() + focusNextPane: -> @model.activateNextPane() # Public: # # FIXME: Difference between active and focused pane? getFocusedPane: -> @panes.getFocusedPane() - # Public: Saves all of the open items within panes. - saveAll: -> - @panes.saveAll() - # Public: Fires a callback on each open {PaneView}. eachPane: (callback) -> @panes.eachPane(callback) @@ -350,20 +239,6 @@ class WorkspaceView extends View # Private: Destroys everything. remove: -> + @model.destroy() editorView.remove() for editorView in @getEditorViews() super - - # Private: Adds the destroyed item's uri to the list of items to reopen. - onPaneItemDestroyed: (e, item) => - if uri = item.getUri?() - @destroyedItemUris.push(uri) - - # Public: Reopens the last-closed item uri if it hasn't already been reopened. - reopenItemSync: -> - if uri = @destroyedItemUris.pop() - @openSync(uri) - - # Private: Removes the item's uri from the list of potential items to reopen. - itemOpened: (item) -> - if uri = item.getUri?() - _.remove(@destroyedItemUris, uri) diff --git a/src/workspace.coffee b/src/workspace.coffee new file mode 100644 index 000000000..39f730e0a --- /dev/null +++ b/src/workspace.coffee @@ -0,0 +1,143 @@ +{remove, last} = require 'underscore-plus' +{Model} = require 'theorist' +Q = require 'q' +Serializable = require 'serializable' +Delegator = require 'delegato' +PaneContainer = require './pane-container' +Pane = require './pane' + +# Public: Represents the view state of the entire window, including the panes at +# the center and panels around the periphery. You can access the singleton +# instance via `atom.workspace`. +module.exports = +class Workspace extends Model + atom.deserializers.add(this) + Serializable.includeInto(this) + + @delegatesProperty 'activePane', 'activePaneItem', toProperty: 'paneContainer' + @delegatesMethod 'getPanes', 'saveAll', 'activateNextPane', 'activatePreviousPane', + toProperty: 'paneContainer' + + @properties + paneContainer: -> new PaneContainer + fullScreen: false + destroyedItemUris: -> [] + + # Private: + constructor: -> + super + @subscribe @paneContainer, 'item-destroyed', @onPaneItemDestroyed + + # Private: Called by the Serializable mixin during deserialization + deserializeParams: (params) -> + params.paneContainer = PaneContainer.deserialize(params.paneContainer) + params + + # Private: Called by the Serializable mixin during serialization. + serializeParams: -> + paneContainer: @paneContainer.serialize() + fullScreen: atom.isFullScreen() + + # Public: Asynchronously opens a given a filepath in Atom. + # + # * filePath: A file path + # * options + # + initialLine: The buffer line number to open to. + # + # Returns a promise that resolves to the {Editor} for the file URI. + open: (filePath, options={}) -> + changeFocus = options.changeFocus ? true + filePath = atom.project.resolve(filePath) + initialLine = options.initialLine + activePane = @activePane + + editor = activePane.itemForUri(atom.project.relativize(filePath)) if activePane and filePath + promise = atom.project.open(filePath, {initialLine}) if not editor + + Q(editor ? promise) + .then (editor) => + if not activePane + activePane = new Pane(items: [editor]) + @paneContainer.root = activePane + + @itemOpened(editor) + activePane.activateItem(editor) + activePane.activate() if changeFocus + @emit "uri-opened" + editor + .catch (error) -> + console.error(error.stack ? error) + + # Private: Only used in specs + openSync: (uri, {changeFocus, initialLine, pane, split}={}) -> + changeFocus ?= true + pane ?= @activePane + uri = atom.project.relativize(uri) + + if pane + if uri + paneItem = pane.itemForUri(uri) ? atom.project.openSync(uri, {initialLine}) + else + paneItem = atom.project.openSync() + + if split == 'right' + panes = @getPanes() + if panes.length == 1 + pane = panes[0].splitRight() + else + pane = last(panes) + else if split == 'left' + pane = @getPanes()[0] + + pane.activateItem(paneItem) + else + paneItem = atom.project.openSync(uri, {initialLine}) + pane = new Pane(items: [paneItem]) + @paneContainer.root = pane + + @itemOpened(paneItem) + + pane.activate() if changeFocus + paneItem + + # Public: Synchronously open an editor for the given URI or activate an existing + # editor in any pane if one already exists. + openSingletonSync: (uri, {changeFocus, initialLine, split}={}) -> + changeFocus ?= true + uri = atom.project.relativize(uri) + pane = @paneContainer.paneForUri(uri) + + if pane + paneItem = pane.itemForUri(uri) + pane.activateItem(paneItem) + pane.activate() if changeFocus + paneItem + else + @openSync(uri, {changeFocus, initialLine, split}) + + # Public: Reopens the last-closed item uri if it hasn't already been reopened. + reopenItemSync: -> + if uri = @destroyedItemUris.pop() + @openSync(uri) + + # Public: save the active item. + saveActivePaneItem: -> + @activePane?.saveActiveItem() + + # Public: save the active item as. + saveActivePaneItemAs: -> + @activePane?.saveActiveItemAs() + + # Public: destroy/close the active item. + destroyActivePaneItem: -> + @activePane?.destroyActiveItem() + + # Private: Removes the item's uri from the list of potential items to reopen. + itemOpened: (item) -> + if uri = item.getUri?() + remove(@destroyedItemUris, uri) + + # Private: Adds the destroyed item's uri to the list of items to reopen. + onPaneItemDestroyed: (item) => + if uri = item.getUri?() + @destroyedItemUris.push(uri)