Merge branch 'master' into ns-workspace-custom-elements

Conflicts:
	spec/pane-view-spec.coffee
	spec/workspace-view-spec.coffee
	src/workspace-view.coffee
	src/workspace.coffee
This commit is contained in:
Nathan Sobo 2014-10-01 11:49:23 -06:00
commit f004f8c45d
53 changed files with 2057 additions and 726 deletions

View File

@ -6,6 +6,6 @@
"url": "https://github.com/atom/atom.git"
},
"dependencies": {
"atom-package-manager": "0.97.0"
"atom-package-manager": "0.98.0"
}
}

View File

@ -8,7 +8,6 @@
"dependencies": {
"async": "~0.2.9",
"donna": "1.0.1",
"tello": "1.0.3",
"formidable": "~1.0.14",
"fs-plus": "2.x",
"github-releases": "~0.2.0",
@ -35,6 +34,8 @@
"request": "~2.27.0",
"rimraf": "~2.2.2",
"runas": "~1.0.1",
"tello": "1.0.3",
"temp": "~0.8.1",
"underscore-plus": "1.x",
"unzip": "~0.1.9",
"vm-compatibility-layer": "~0.1.0"

View File

@ -26,7 +26,7 @@ module.exports = (grunt) ->
switch process.platform
when 'darwin'
cmd = 'codesign'
args = ['-f', '-v', '-s', 'Developer ID Application: GitHub', grunt.config.get('atom.shellAppDir')]
args = ['--deep', '--force', '--verbose', '--sign', 'Developer ID Application: GitHub', grunt.config.get('atom.shellAppDir')]
spawn {cmd, args}, (error) -> callback(error)
when 'win32'
spawn {cmd: 'taskkill', args: ['/F', '/IM', 'atom.exe']}, ->

View File

@ -3,6 +3,7 @@ path = require 'path'
_ = require 'underscore-plus'
fs = require 'fs-plus'
runas = null
temp = require 'temp'
module.exports = (grunt) ->
{cp, mkdir, rm} = require('./task-helpers')(grunt)
@ -22,7 +23,11 @@ module.exports = (grunt) ->
else if process.platform is 'darwin'
rm installDir
mkdir path.dirname(installDir)
cp shellAppDir, installDir
tempFolder = temp.path()
mkdir tempFolder
cp shellAppDir, tempFolder
fs.renameSync(tempFolder, installDir)
else
binDir = path.join(installDir, 'bin')
shareDir = path.join(installDir, 'share', 'atom')

View File

@ -10,5 +10,8 @@
},
"no_interpolation_in_single_quotes": {
"level": "error"
},
"no_debugger": {
"level": "error"
}
}

View File

@ -48,15 +48,6 @@ but you can programmatically write to it with `atom.config.set`:
atom.config.set("core.showInvisibles", true)
```
You should never mutate the value of a config key, because that would circumvent
the notification of observers. You can however use methods like `pushAtKeyPath`,
`unshiftAtKeyPath`, and `removeAtKeyPath` to manipulate mutable config values.
```coffeescript
atom.config.pushAtKeyPath("core.disabledPackages", "wrap-guide")
atom.config.removeAtKeyPath("core.disabledPackages", "terminal")
```
You can also use `setDefaults`, which will assign default values for keys that
are always overridden by values assigned with `set`. Defaults are not written
out to the the `config.json` file to prevent it from becoming cluttered.

View File

@ -321,6 +321,29 @@ extensions your grammar supports:
]
```
## Adding Configuration Settings
You can support config settings in your package that are editable in the
settings view. Specify a `config` key in your package main:
```coffeescript
module.exports =
# Your config schema!
config:
someInt:
type: 'integer'
default: 23
minimum: 1
activate: (state) -> # ...
# ...
```
To define the configuration, we use [json schema][json-schema] which allows you
to indicate the type your value should be, its default, etc.
See the [Config API Docs](https://atom.io/docs/api/latest/Config) for more
details specifying your configuration.
## Bundle External Resources
It's common to ship external resources like images and fonts in the package, to
@ -392,3 +415,4 @@ all the other available commands.
[first-package]: your-first-package.html
[convert-bundle]: converting-a-text-mate-bundle.html
[convert-theme]: converting-a-text-mate-theme.html
[json-schema]: http://json-schema.org/

View File

@ -196,11 +196,27 @@
]
'context-menu':
'.overlayer':
'Undo': 'core:undo'
'Redo': 'core:redo'
'Cut': 'core:cut'
'Copy': 'core:copy'
'Paste': 'core:paste'
'Delete': 'core:delete'
'Select All': 'core:select-all'
'.overlayer': [
{label: 'Undo', command: 'core:undo'}
{label: 'Redo', command: 'core:redo'}
{type: 'separator'}
{label: 'Cut', command: 'core:cut'}
{label: 'Copy', command: 'core:copy'}
{label: 'Paste', command: 'core:paste'}
{label: 'Delete', command: 'core:delete'}
{label: 'Select All', command: 'core:select-all'}
{type: 'separator'}
{label: 'Split Up', command: 'pane:split-up'}
{label: 'Split Down', command: 'pane:split-down'}
{label: 'Split Left', command: 'pane:split-left'}
{label: 'Split Right', command: 'pane:split-right'}
{type: 'separator'}
]
'.pane': [
{type: 'separator'}
{label: 'Split Up', command: 'pane:split-up'}
{label: 'Split Down', command: 'pane:split-down'}
{label: 'Split Left', command: 'pane:split-left'}
{label: 'Split Right', command: 'pane:split-right'}
{type: 'separator'}
]

View File

@ -153,11 +153,27 @@
]
'context-menu':
'.overlayer':
'Undo': 'core:undo'
'Redo': 'core:redo'
'Cut': 'core:cut'
'Copy': 'core:copy'
'Paste': 'core:paste'
'Delete': 'core:delete'
'Select All': 'core:select-all'
'.overlayer': [
{label: 'Undo', command: 'core:undo'}
{label: 'Redo', command: 'core:redo'}
{type: 'separator'}
{label: 'Cut', command: 'core:cut'}
{label: 'Copy', command: 'core:copy'}
{label: 'Paste', command: 'core:paste'}
{label: 'Delete', command: 'core:delete'}
{label: 'Select All', command: 'core:select-all'}
{type: 'separator'}
{label: 'Split Up', command: 'pane:split-up'}
{label: 'Split Down', command: 'pane:split-down'}
{label: 'Split Left', command: 'pane:split-left'}
{label: 'Split Right', command: 'pane:split-right'}
{type: 'separator'}
]
'.pane': [
{type: 'separator'}
{label: 'Split Up', command: 'pane:split-up'}
{label: 'Split Down', command: 'pane:split-down'}
{label: 'Split Left', command: 'pane:split-left'}
{label: 'Split Right', command: 'pane:split-right'}
{type: 'separator'}
]

View File

@ -171,11 +171,27 @@
]
'context-menu':
'.overlayer':
'Undo': 'core:undo'
'Redo': 'core:redo'
'Cut': 'core:cut'
'Copy': 'core:copy'
'Paste': 'core:paste'
'Delete': 'core:delete'
'Select All': 'core:select-all'
'.overlayer': [
{label: 'Undo', command: 'core:undo'}
{label: 'Redo', command: 'core:redo'}
{type: 'separator'}
{label: 'Cut', command: 'core:cut'}
{label: 'Copy', command: 'core:copy'}
{label: 'Paste', command: 'core:paste'}
{label: 'Delete', command: 'core:delete'}
{label: 'Select All', command: 'core:select-all'}
{type: 'separator'}
{label: 'Split Up', command: 'pane:split-up'}
{label: 'Split Down', command: 'pane:split-down'}
{label: 'Split Left', command: 'pane:split-left'}
{label: 'Split Right', command: 'pane:split-right'}
{type: 'separator'}
]
'.pane': [
{type: 'separator'}
{label: 'Split Up', command: 'pane:split-up'}
{label: 'Split Down', command: 'pane:split-down'}
{label: 'Split Left', command: 'pane:split-left'}
{label: 'Split Right', command: 'pane:split-right'}
{type: 'separator'}
]

View File

@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
"version": "0.133.0",
"version": "0.135.0",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/browser/main.js",
"repository": {
@ -17,10 +17,10 @@
"url": "http://github.com/atom/atom/raw/master/LICENSE.md"
}
],
"atomShellVersion": "0.16.2",
"atomShellVersion": "0.17.1",
"dependencies": {
"async": "0.2.6",
"atom-keymap": "^2.1.3",
"atom-keymap": "^2.2.0",
"bootstrap": "git+https://github.com/atom/bootstrap.git#6af81906189f1747fd6c93479e3d998ebe041372",
"clear-cut": "0.4.0",
"coffee-script": "1.7.0",
@ -28,8 +28,8 @@
"delegato": "^1",
"emissary": "^1.3.1",
"event-kit": "0.7.2",
"first-mate": "^2.1.2",
"fs-plus": "^2.2.6",
"first-mate": "^2.2.0",
"fs-plus": "^2.3.1",
"fstream": "0.1.24",
"fuzzaldrin": "^2.1",
"git-utils": "^2.1.4",
@ -83,12 +83,12 @@
"dev-live-reload": "0.34.0",
"exception-reporting": "0.20.0",
"feedback": "0.33.0",
"find-and-replace": "0.138.0",
"find-and-replace": "0.139.0",
"fuzzy-finder": "0.58.0",
"git-diff": "0.39.0",
"go-to-line": "0.25.0",
"grammar-selector": "0.34.0",
"image-view": "0.36.0",
"image-view": "0.37.0",
"incompatible-packages": "0.9.0",
"keybinding-resolver": "0.20.0",
"link": "0.25.0",
@ -97,13 +97,13 @@
"open-on-github": "0.30.0",
"package-generator": "0.31.0",
"release-notes": "0.36.0",
"settings-view": "0.147.0",
"settings-view": "0.149.0",
"snippets": "0.53.0",
"spell-check": "0.42.0",
"status-bar": "0.46.0",
"styleguide": "0.30.0",
"symbols-view": "0.66.0",
"tabs": "0.53.0",
"tabs": "0.54.0",
"timecop": "0.22.0",
"tree-view": "0.127.0",
"update-package-dependencies": "0.6.0",
@ -111,7 +111,7 @@
"whitespace": "0.25.0",
"wrap-guide": "0.22.0",
"language-c": "0.28.0",
"language-coffee-script": "0.34.0",
"language-coffee-script": "0.35.0",
"language-css": "0.17.0",
"language-gfm": "0.50.0",
"language-git": "0.9.0",
@ -138,7 +138,7 @@
"language-text": "0.6.0",
"language-todo": "0.12.0",
"language-toml": "0.12.0",
"language-xml": "0.21.0",
"language-xml": "0.22.0",
"language-yaml": "0.17.0"
},
"private": true,

View File

@ -47,3 +47,8 @@ describe "the `atom` global", ->
[event, version, notes] = updateAvailableHandler.mostRecentCall.args
expect(notes).toBe 'notes'
expect(version).toBe 'version'
describe "loading default config", ->
it 'loads the default core config', ->
expect(atom.config.get('core.excludeVcsIgnoredPaths')).toBe true
expect(atom.config.get('editor.showInvisibles')).toBe false

View File

@ -12,7 +12,7 @@ describe "Config", ->
describe ".get(keyPath)", ->
it "allows a key path's value to be read", ->
expect(atom.config.set("foo.bar.baz", 42)).toBe 42
expect(atom.config.set("foo.bar.baz", 42)).toBe true
expect(atom.config.get("foo.bar.baz")).toBe 42
expect(atom.config.get("bogus.key.path")).toBeUndefined()
@ -37,7 +37,7 @@ describe "Config", ->
describe ".set(keyPath, value)", ->
it "allows a key path's value to be written", ->
expect(atom.config.set("foo.bar.baz", 42)).toBe 42
expect(atom.config.set("foo.bar.baz", 42)).toBe true
expect(atom.config.get("foo.bar.baz")).toBe 42
it "updates observers and saves when a key path is set", ->
@ -48,7 +48,7 @@ describe "Config", ->
atom.config.set("foo.bar.baz", 42)
expect(atom.config.save).toHaveBeenCalled()
expect(observeHandler).toHaveBeenCalledWith 42, {previous: undefined}
expect(observeHandler).toHaveBeenCalledWith 42
describe "when the value equals the default value", ->
it "does not store the value", ->
@ -139,7 +139,7 @@ describe "Config", ->
expect(atom.config.pushAtKeyPath("foo.bar.baz", "b")).toBe 2
expect(atom.config.get("foo.bar.baz")).toEqual ["a", "b"]
expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz"), {previous: ['a']}
expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz")
describe ".unshiftAtKeyPath(keyPath, value)", ->
it "unshifts the given value to the array at the key path and updates observers", ->
@ -150,7 +150,7 @@ describe "Config", ->
expect(atom.config.unshiftAtKeyPath("foo.bar.baz", "a")).toBe 2
expect(atom.config.get("foo.bar.baz")).toEqual ["a", "b"]
expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz"), {previous: ['b']}
expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz")
describe ".removeAtKeyPath(keyPath, value)", ->
it "removes the given value from the array at the key path and updates observers", ->
@ -161,16 +161,22 @@ describe "Config", ->
expect(atom.config.removeAtKeyPath("foo.bar.baz", "b")).toEqual ["a", "c"]
expect(atom.config.get("foo.bar.baz")).toEqual ["a", "c"]
expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz"), {previous: ['a', 'b', 'c']}
expect(observeHandler).toHaveBeenCalledWith atom.config.get("foo.bar.baz")
describe ".getPositiveInt(keyPath, defaultValue)", ->
it "returns the proper current or default value", ->
it "returns the proper coerced value", ->
atom.config.set('editor.preferredLineLength', 0)
expect(atom.config.getPositiveInt('editor.preferredLineLength', 80)).toBe 80
expect(atom.config.getPositiveInt('editor.preferredLineLength', 80)).toBe 1
it "returns the proper coerced value", ->
atom.config.set('editor.preferredLineLength', -1234)
expect(atom.config.getPositiveInt('editor.preferredLineLength', 80)).toBe 80
expect(atom.config.getPositiveInt('editor.preferredLineLength', 80)).toBe 1
it "returns the default value when a string is passed in", ->
atom.config.set('editor.preferredLineLength', 'abcd')
expect(atom.config.getPositiveInt('editor.preferredLineLength', 80)).toBe 80
it "returns the default value when null is passed in", ->
atom.config.set('editor.preferredLineLength', null)
expect(atom.config.getPositiveInt('editor.preferredLineLength', 80)).toBe 80
@ -226,11 +232,54 @@ describe "Config", ->
it "emits an updated event", ->
updatedCallback = jasmine.createSpy('updated')
atom.config.observe('foo.bar.baz.a', callNow: false, updatedCallback)
atom.config.onDidChange('foo.bar.baz.a', updatedCallback)
expect(updatedCallback.callCount).toBe 0
atom.config.setDefaults("foo.bar.baz", a: 2)
expect(updatedCallback.callCount).toBe 1
describe ".onDidChange(keyPath)", ->
[observeHandler, observeSubscription] = []
describe 'when a keyPath is specified', ->
beforeEach ->
observeHandler = jasmine.createSpy("observeHandler")
atom.config.set("foo.bar.baz", "value 1")
observeSubscription = atom.config.onDidChange "foo.bar.baz", observeHandler
it "does not fire the given callback with the current value at the keypath", ->
expect(observeHandler).not.toHaveBeenCalled()
it "fires the callback every time the observed value changes", ->
observeHandler.reset() # clear the initial call
atom.config.set('foo.bar.baz', "value 2")
expect(observeHandler).toHaveBeenCalledWith({newValue: 'value 2', oldValue: 'value 1', keyPath: 'foo.bar.baz'})
observeHandler.reset()
atom.config.set('foo.bar.baz', "value 1")
expect(observeHandler).toHaveBeenCalledWith({newValue: 'value 1', oldValue: 'value 2', keyPath: 'foo.bar.baz'})
describe 'when a keyPath is not specified', ->
beforeEach ->
observeHandler = jasmine.createSpy("observeHandler")
atom.config.set("foo.bar.baz", "value 1")
observeSubscription = atom.config.onDidChange observeHandler
it "does not fire the given callback initially", ->
expect(observeHandler).not.toHaveBeenCalled()
it "fires the callback every time any value changes", ->
observeHandler.reset() # clear the initial call
atom.config.set('foo.bar.baz', "value 2")
expect(observeHandler).toHaveBeenCalledWith({newValue: 'value 2', oldValue: 'value 1', keyPath: 'foo.bar.baz'})
observeHandler.reset()
atom.config.set('foo.bar.baz', "value 1")
expect(observeHandler).toHaveBeenCalledWith({newValue: 'value 1', oldValue: 'value 2', keyPath: 'foo.bar.baz'})
observeHandler.reset()
atom.config.set('foo.bar.int', 1)
expect(observeHandler).toHaveBeenCalledWith({newValue: 1, oldValue: undefined, keyPath: 'foo.bar.int'})
describe ".observe(keyPath)", ->
[observeHandler, observeSubscription] = []
@ -245,26 +294,25 @@ describe "Config", ->
it "fires the callback every time the observed value changes", ->
observeHandler.reset() # clear the initial call
atom.config.set('foo.bar.baz', "value 2")
expect(observeHandler).toHaveBeenCalledWith("value 2", {previous: 'value 1'})
expect(observeHandler).toHaveBeenCalledWith("value 2")
observeHandler.reset()
atom.config.set('foo.bar.baz', "value 1")
expect(observeHandler).toHaveBeenCalledWith("value 1", {previous: 'value 2'})
expect(observeHandler).toHaveBeenCalledWith("value 1")
it "fires the callback when the observed value is deleted", ->
observeHandler.reset() # clear the initial call
atom.config.set('foo.bar.baz', undefined)
expect(observeHandler).toHaveBeenCalledWith(undefined, {previous: 'value 1'})
expect(observeHandler).toHaveBeenCalledWith(undefined)
it "fires the callback when the full key path goes into and out of existence", ->
observeHandler.reset() # clear the initial call
atom.config.set("foo.bar", undefined)
expect(observeHandler).toHaveBeenCalledWith(undefined)
expect(observeHandler).toHaveBeenCalledWith(undefined, {previous: 'value 1'})
observeHandler.reset()
atom.config.set("foo.bar.baz", "i'm back")
expect(observeHandler).toHaveBeenCalledWith("i'm back", {previous: undefined})
expect(observeHandler).toHaveBeenCalledWith("i'm back")
it "does not fire the callback once the observe subscription is off'ed", ->
observeHandler.reset() # clear the initial call
@ -311,11 +359,19 @@ describe "Config", ->
describe "when the config file contains valid cson", ->
beforeEach ->
fs.writeFileSync(atom.config.configFilePath, "foo: bar: 'baz'")
atom.config.loadUserConfig()
it "updates the config data based on the file contents", ->
atom.config.loadUserConfig()
expect(atom.config.get("foo.bar")).toBe 'baz'
it "notifies observers for updated keypaths on load", ->
observeHandler = jasmine.createSpy("observeHandler")
observeSubscription = atom.config.observe "foo.bar", observeHandler
atom.config.loadUserConfig()
expect(observeHandler).toHaveBeenCalledWith 'baz'
describe "when the config file contains invalid cson", ->
beforeEach ->
spyOn(console, 'error')
@ -334,10 +390,43 @@ describe "Config", ->
expect(fs.existsSync(atom.config.configFilePath)).toBe true
expect(CSON.readFileSync(atom.config.configFilePath)).toEqual {}
describe "when a schema is specified", ->
beforeEach ->
schema =
type: 'object'
properties:
bar:
type: 'string'
default: 'def'
int:
type: 'integer'
default: 12
atom.config.setSchema('foo', schema)
describe "when the config file contains values that do not adhere to the schema", ->
warnSpy = null
beforeEach ->
warnSpy = spyOn console, 'warn'
fs.writeFileSync atom.config.configFilePath, """
foo:
bar: 'baz'
int: 'bad value'
"""
atom.config.loadUserConfig()
it "updates the only the settings that have values matching the schema", ->
expect(atom.config.get("foo.bar")).toBe 'baz'
expect(atom.config.get("foo.int")).toBe 12
expect(warnSpy).toHaveBeenCalled()
expect(warnSpy.mostRecentCall.args[0]).toContain "'foo.int' could not be set"
describe ".observeUserConfig()", ->
updatedHandler = null
beforeEach ->
atom.config.setDefaults('foo', bar: 'def')
atom.config.configDirPath = dotAtomPath
atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson")
expect(fs.existsSync(atom.config.configDirPath)).toBeFalsy()
@ -345,7 +434,7 @@ describe "Config", ->
atom.config.loadUserConfig()
atom.config.observeUserConfig()
updatedHandler = jasmine.createSpy("updatedHandler")
atom.config.on 'updated', updatedHandler
atom.config.onDidChange updatedHandler
afterEach ->
atom.config.unobserveUserConfig()
@ -359,6 +448,60 @@ describe "Config", ->
expect(atom.config.get('foo.bar')).toBe 'quux'
expect(atom.config.get('foo.baz')).toBe 'bar'
it "does not fire a change event for paths that did not change", ->
atom.config.onDidChange 'foo.bar', noChangeSpy = jasmine.createSpy()
fs.writeFileSync(atom.config.configFilePath, "foo: { bar: 'baz', omg: 'ok'}")
waitsFor 'update event', -> updatedHandler.callCount > 0
runs ->
expect(noChangeSpy).not.toHaveBeenCalled()
expect(atom.config.get('foo.bar')).toBe 'baz'
expect(atom.config.get('foo.omg')).toBe 'ok'
describe 'when the default value is a complex value', ->
beforeEach ->
fs.writeFileSync(atom.config.configFilePath, "foo: { bar: ['baz', 'ok']}")
waitsFor 'update event', -> updatedHandler.callCount > 0
runs -> updatedHandler.reset()
it "does not fire a change event for paths that did not change", ->
atom.config.onDidChange 'foo.bar', noChangeSpy = jasmine.createSpy()
fs.writeFileSync(atom.config.configFilePath, "foo: { bar: ['baz', 'ok'], omg: 'another'}")
waitsFor 'update event', -> updatedHandler.callCount > 0
runs ->
expect(noChangeSpy).not.toHaveBeenCalled()
expect(atom.config.get('foo.bar')).toEqual ['baz', 'ok']
expect(atom.config.get('foo.omg')).toBe 'another'
describe "when the config file changes to omit a setting with a default", ->
it "resets the setting back to the default", ->
fs.writeFileSync(atom.config.configFilePath, "foo: { baz: 'new'}")
waitsFor 'update event', -> updatedHandler.callCount > 0
runs ->
expect(atom.config.get('foo.bar')).toBe 'def'
expect(atom.config.get('foo.baz')).toBe 'new'
describe "when the config file changes to be empty", ->
beforeEach ->
fs.writeFileSync(atom.config.configFilePath, "")
waitsFor 'update event', -> updatedHandler.callCount > 0
it "resets all settings back to the defaults", ->
expect(updatedHandler.callCount).toBe 1
expect(atom.config.get('foo.bar')).toBe 'def'
atom.config.set("hair", "blonde") # trigger a save
expect(atom.config.save).toHaveBeenCalled()
describe "when the config file subsequently changes again to contain configuration", ->
beforeEach ->
updatedHandler.reset()
fs.writeFileSync(atom.config.configFilePath, "foo: bar: 'newVal'")
waitsFor 'update event', -> updatedHandler.callCount > 0
it "sets the setting to the value specified in the config file", ->
expect(atom.config.get('foo.bar')).toBe 'newVal'
describe "when the config file changes to contain invalid cson", ->
beforeEach ->
spyOn(console, 'error')
@ -373,9 +516,349 @@ describe "Config", ->
describe "when the config file subsequently changes again to contain valid cson", ->
beforeEach ->
fs.writeFileSync(atom.config.configFilePath, "foo: bar: 'baz'")
fs.writeFileSync(atom.config.configFilePath, "foo: bar: 'newVal'")
waitsFor 'update event', -> updatedHandler.callCount > 0
it "updates the config data and resumes saving", ->
atom.config.set("hair", "blonde")
expect(atom.config.save).toHaveBeenCalled()
describe "when a schema is specified", ->
schema = null
describe '.setSchema(keyPath, schema)', ->
it 'creates a properly nested schema', ->
schema =
type: 'object'
properties:
anInt:
type: 'integer'
default: 12
atom.config.setSchema('foo.bar', schema)
expect(atom.config.getSchema('foo')).toEqual
type: 'object'
properties:
bar:
type: 'object'
properties:
anInt:
type: 'integer'
default: 12
it 'sets defaults specified by the schema', ->
schema =
type: 'object'
properties:
anInt:
type: 'integer'
default: 12
anObject:
type: 'object'
properties:
nestedInt:
type: 'integer'
default: 24
nestedObject:
type: 'object'
properties:
superNestedInt:
type: 'integer'
default: 36
atom.config.setSchema('foo.bar', schema)
expect(atom.config.get("foo.bar.anInt")).toBe 12
expect(atom.config.get("foo.bar.anObject")).toEqual
nestedInt: 24
nestedObject:
superNestedInt: 36
it 'can set a non-object schema', ->
schema =
type: 'integer'
default: 12
atom.config.setSchema('foo.bar.anInt', schema)
expect(atom.config.get("foo.bar.anInt")).toBe 12
expect(atom.config.getSchema('foo.bar.anInt')).toEqual
type: 'integer'
default: 12
describe '.getSchema(keyPath)', ->
schema =
type: 'object'
properties:
anInt:
type: 'integer'
default: 12
atom.config.setSchema('foo.bar', schema)
expect(atom.config.getSchema('foo.bar')).toEqual
type: 'object'
properties:
anInt:
type: 'integer'
default: 12
expect(atom.config.getSchema('foo.bar.anInt')).toEqual
type: 'integer'
default: 12
describe 'when the value has an "integer" type', ->
beforeEach ->
schema =
type: 'integer'
default: 12
atom.config.setSchema('foo.bar.anInt', schema)
it 'coerces a string to an int', ->
atom.config.set('foo.bar.anInt', '123')
expect(atom.config.get('foo.bar.anInt')).toBe 123
it 'does not allow infinity', ->
atom.config.set('foo.bar.anInt', Infinity)
expect(atom.config.get('foo.bar.anInt')).toBe 12
it 'coerces a float to an int', ->
atom.config.set('foo.bar.anInt', 12.3)
expect(atom.config.get('foo.bar.anInt')).toBe 12
it 'will not set non-integers', ->
atom.config.set('foo.bar.anInt', null)
expect(atom.config.get('foo.bar.anInt')).toBe 12
atom.config.set('foo.bar.anInt', 'nope')
expect(atom.config.get('foo.bar.anInt')).toBe 12
describe 'when the minimum and maximum keys are used', ->
beforeEach ->
schema =
type: 'integer'
minimum: 10
maximum: 20
default: 12
atom.config.setSchema('foo.bar.anInt', schema)
it 'keeps the specified value within the specified range', ->
atom.config.set('foo.bar.anInt', '123')
expect(atom.config.get('foo.bar.anInt')).toBe 20
atom.config.set('foo.bar.anInt', '1')
expect(atom.config.get('foo.bar.anInt')).toBe 10
describe 'when the value has an "integer" and "string" type', ->
beforeEach ->
schema =
type: ['integer', 'string']
default: 12
atom.config.setSchema('foo.bar.anInt', schema)
it 'can coerce an int, and fallback to a string', ->
atom.config.set('foo.bar.anInt', '123')
expect(atom.config.get('foo.bar.anInt')).toBe 123
atom.config.set('foo.bar.anInt', 'cats')
expect(atom.config.get('foo.bar.anInt')).toBe 'cats'
describe 'when the value has an "string" and "boolean" type', ->
beforeEach ->
schema =
type: ['string', 'boolean']
default: 'def'
atom.config.setSchema('foo.bar', schema)
it 'can set a string, a boolean, and revert back to the default', ->
atom.config.set('foo.bar', 'ok')
expect(atom.config.get('foo.bar')).toBe 'ok'
atom.config.set('foo.bar', false)
expect(atom.config.get('foo.bar')).toBe false
atom.config.set('foo.bar', undefined)
expect(atom.config.get('foo.bar')).toBe 'def'
describe 'when the value has a "number" type', ->
beforeEach ->
schema =
type: 'number'
default: 12.1
atom.config.setSchema('foo.bar.aFloat', schema)
it 'coerces a string to a float', ->
atom.config.set('foo.bar.aFloat', '12.23')
expect(atom.config.get('foo.bar.aFloat')).toBe 12.23
it 'will not set non-numbers', ->
atom.config.set('foo.bar.aFloat', null)
expect(atom.config.get('foo.bar.aFloat')).toBe 12.1
atom.config.set('foo.bar.aFloat', 'nope')
expect(atom.config.get('foo.bar.aFloat')).toBe 12.1
describe 'when the minimum and maximum keys are used', ->
beforeEach ->
schema =
type: 'number'
minimum: 11.2
maximum: 25.4
default: 12.1
atom.config.setSchema('foo.bar.aFloat', schema)
it 'keeps the specified value within the specified range', ->
atom.config.set('foo.bar.aFloat', '123.2')
expect(atom.config.get('foo.bar.aFloat')).toBe 25.4
atom.config.set('foo.bar.aFloat', '1.0')
expect(atom.config.get('foo.bar.aFloat')).toBe 11.2
describe 'when the value has a "boolean" type', ->
beforeEach ->
schema =
type: 'boolean'
default: true
atom.config.setSchema('foo.bar.aBool', schema)
it 'coerces various types to a boolean', ->
atom.config.set('foo.bar.aBool', 'true')
expect(atom.config.get('foo.bar.aBool')).toBe true
atom.config.set('foo.bar.aBool', 'false')
expect(atom.config.get('foo.bar.aBool')).toBe false
atom.config.set('foo.bar.aBool', 'TRUE')
expect(atom.config.get('foo.bar.aBool')).toBe true
atom.config.set('foo.bar.aBool', 'FALSE')
expect(atom.config.get('foo.bar.aBool')).toBe false
atom.config.set('foo.bar.aBool', 1)
expect(atom.config.get('foo.bar.aBool')).toBe false
atom.config.set('foo.bar.aBool', 0)
expect(atom.config.get('foo.bar.aBool')).toBe false
atom.config.set('foo.bar.aBool', {})
expect(atom.config.get('foo.bar.aBool')).toBe false
atom.config.set('foo.bar.aBool', null)
expect(atom.config.get('foo.bar.aBool')).toBe false
it 'reverts back to the default value when undefined is passed to set', ->
atom.config.set('foo.bar.aBool', 'false')
expect(atom.config.get('foo.bar.aBool')).toBe false
atom.config.set('foo.bar.aBool', undefined)
expect(atom.config.get('foo.bar.aBool')).toBe true
describe 'when the value has an "string" type', ->
beforeEach ->
schema =
type: 'string'
default: 'ok'
atom.config.setSchema('foo.bar.aString', schema)
it 'allows strings', ->
atom.config.set('foo.bar.aString', 'yep')
expect(atom.config.get('foo.bar.aString')).toBe 'yep'
it 'will only set strings', ->
expect(atom.config.set('foo.bar.aString', 123)).toBe false
expect(atom.config.get('foo.bar.aString')).toBe 'ok'
expect(atom.config.set('foo.bar.aString', true)).toBe false
expect(atom.config.get('foo.bar.aString')).toBe 'ok'
expect(atom.config.set('foo.bar.aString', null)).toBe false
expect(atom.config.get('foo.bar.aString')).toBe 'ok'
expect(atom.config.set('foo.bar.aString', [])).toBe false
expect(atom.config.get('foo.bar.aString')).toBe 'ok'
expect(atom.config.set('foo.bar.aString', nope: 'nope')).toBe false
expect(atom.config.get('foo.bar.aString')).toBe 'ok'
describe 'when the value has an "object" type', ->
beforeEach ->
schema =
type: 'object'
properties:
anInt:
type: 'integer'
default: 12
nestedObject:
type: 'object'
properties:
nestedBool:
type: 'boolean'
default: false
atom.config.setSchema('foo.bar', schema)
it 'converts and validates all the children', ->
atom.config.set 'foo.bar',
anInt: '23'
nestedObject:
nestedBool: 'true'
expect(atom.config.get('foo.bar')).toEqual
anInt: 23
nestedObject:
nestedBool: true
it 'will set only the values that adhere to the schema', ->
expect(atom.config.set 'foo.bar',
anInt: 'nope'
nestedObject:
nestedBool: true
).toBe true
expect(atom.config.get('foo.bar.anInt')).toEqual 12
expect(atom.config.get('foo.bar.nestedObject.nestedBool')).toEqual true
describe 'when the value has an "array" type', ->
beforeEach ->
schema =
type: 'array'
default: [1, 2, 3]
items:
type: 'integer'
atom.config.setSchema('foo.bar', schema)
it 'converts an array of strings to an array of ints', ->
atom.config.set 'foo.bar', ['2', '3', '4']
expect(atom.config.get('foo.bar')).toEqual [2, 3, 4]
describe 'when the `enum` key is used', ->
beforeEach ->
schema =
type: 'object'
properties:
str:
type: 'string'
default: 'ok'
enum: ['ok', 'one', 'two']
int:
type: 'integer'
default: 2
enum: [2, 3, 5]
arr:
type: 'array'
default: ['one', 'two']
items:
type: 'string'
enum: ['one', 'two', 'three']
atom.config.setSchema('foo.bar', schema)
it 'will only set a string when the string is in the enum values', ->
expect(atom.config.set('foo.bar.str', 'nope')).toBe false
expect(atom.config.get('foo.bar.str')).toBe 'ok'
expect(atom.config.set('foo.bar.str', 'one')).toBe true
expect(atom.config.get('foo.bar.str')).toBe 'one'
it 'will only set an integer when the integer is in the enum values', ->
expect(atom.config.set('foo.bar.int', '400')).toBe false
expect(atom.config.get('foo.bar.int')).toBe 2
expect(atom.config.set('foo.bar.int', '3')).toBe true
expect(atom.config.get('foo.bar.int')).toBe 3
it 'will only set an array when the array values are in the enum values', ->
expect(atom.config.set('foo.bar.arr', ['one', 'five'])).toBe true
expect(atom.config.get('foo.bar.arr')).toEqual ['one']
expect(atom.config.set('foo.bar.arr', ['two', 'three'])).toBe true
expect(atom.config.get('foo.bar.arr')).toEqual ['two', 'three']

View File

@ -3,173 +3,150 @@
ContextMenuManager = require '../src/context-menu-manager'
describe "ContextMenuManager", ->
[contextMenu] = []
[contextMenu, parent, child, grandchild] = []
beforeEach ->
{resourcePath} = atom.getLoadSettings()
contextMenu = new ContextMenuManager({resourcePath})
describe "adding definitions", ->
it 'loads', ->
contextMenu.add 'file-path',
'.selector':
'label': 'command'
parent = document.createElement("div")
child = document.createElement("div")
grandchild = document.createElement("div")
parent.classList.add('parent')
child.classList.add('child')
grandchild.classList.add('grandchild')
child.appendChild(grandchild)
parent.appendChild(child)
expect(contextMenu.definitions['.selector'][0].label).toEqual 'label'
expect(contextMenu.definitions['.selector'][0].command).toEqual 'command'
describe "::add(itemsBySelector)", ->
it "can add top-level menu items that can be removed with the returned disposable", ->
disposable = contextMenu.add
'.parent': [{label: 'A', command: 'a'}]
'.child': [{label: 'B', command: 'b'}]
'.grandchild': [{label: 'C', command: 'c'}]
it 'does not add duplicate menu items', ->
contextMenu.add 'file-path',
'.selector':
'label': 'command'
expect(contextMenu.templateForElement(grandchild)).toEqual [
{label: 'C', command: 'c'}
{label: 'B', command: 'b'}
{label: 'A', command: 'a'}
]
contextMenu.add 'file-path',
'.selector':
'label': 'command'
disposable.dispose()
expect(contextMenu.templateForElement(grandchild)).toEqual []
expect(contextMenu.definitions['.selector'][0].label).toEqual 'label'
expect(contextMenu.definitions['.selector'][0].command).toEqual 'command'
expect(contextMenu.definitions['.selector'].length).toBe 1
it "can add submenu items to existing menus that can be removed with the returned disposable", ->
disposable1 = contextMenu.add
'.grandchild': [{label: 'A', submenu: [{label: 'B', command: 'b'}]}]
disposable2 = contextMenu.add
'.grandchild': [{label: 'A', submenu: [{label: 'C', command: 'c'}]}]
it 'allows multiple separators', ->
contextMenu.add 'file-path',
'.selector':
'separator1': '-'
'separator2': '-'
expect(contextMenu.definitions['.selector'].length).toBe 2
expect(contextMenu.definitions['.selector'][0].type).toEqual 'separator'
expect(contextMenu.definitions['.selector'][1].type).toEqual 'separator'
it 'allows duplicate commands with different labels', ->
contextMenu.add 'file-path',
'.selector':
'label': 'command'
contextMenu.add 'file-path',
'.selector':
'another label': 'command'
expect(contextMenu.definitions['.selector'][0].label).toEqual 'label'
expect(contextMenu.definitions['.selector'][0].command).toEqual 'command'
expect(contextMenu.definitions['.selector'][1].label).toEqual 'another label'
expect(contextMenu.definitions['.selector'][1].command).toEqual 'command'
it "loads submenus", ->
contextMenu.add 'file-path',
'.selector':
'parent':
'child-1': 'child-1:trigger'
'child-2': 'child-2:trigger'
'parent-2': 'parent-2:trigger'
expect(contextMenu.definitions['.selector'].length).toBe 2
expect(contextMenu.definitions['.selector'][0].label).toEqual 'parent'
expect(contextMenu.definitions['.selector'][0].submenu.length).toBe 2
expect(contextMenu.definitions['.selector'][0].submenu[0].label).toBe 'child-1'
expect(contextMenu.definitions['.selector'][0].submenu[0].command).toBe 'child-1:trigger'
expect(contextMenu.definitions['.selector'][0].submenu[1].label).toBe 'child-2'
expect(contextMenu.definitions['.selector'][0].submenu[1].command).toBe 'child-2:trigger'
describe 'dev mode', ->
it 'loads', ->
contextMenu.add 'file-path',
'.selector':
'label': 'command'
, devMode: true
expect(contextMenu.devModeDefinitions['.selector'][0].label).toEqual 'label'
expect(contextMenu.devModeDefinitions['.selector'][0].command).toEqual 'command'
describe "building a menu template", ->
beforeEach ->
contextMenu.definitions = {
'.parent':[
label: 'parent'
command: 'command-p'
]
'.child': [
label: 'child'
command: 'command-c'
expect(contextMenu.templateForElement(grandchild)).toEqual [{
label: 'A',
submenu: [
{label: 'B', command: 'b'}
{label: 'C', command: 'c'}
]
}]
disposable2.dispose()
expect(contextMenu.templateForElement(grandchild)).toEqual [{
label: 'A',
submenu: [
{label: 'B', command: 'b'}
]
}]
disposable1.dispose()
expect(contextMenu.templateForElement(grandchild)).toEqual []
it "favors the most specific / recently added item in the case of a duplicate label", ->
grandchild.classList.add('foo')
disposable1 = contextMenu.add
'.grandchild': [{label: 'A', command: 'a'}]
disposable2 = contextMenu.add
'.grandchild.foo': [{label: 'A', command: 'b'}]
disposable3 = contextMenu.add
'.grandchild': [{label: 'A', command: 'c'}]
disposable4 = contextMenu.add
'.child': [{label: 'A', command: 'd'}]
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'b'}]
disposable2.dispose()
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'c'}]
disposable3.dispose()
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'a'}]
disposable1.dispose()
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'd'}]
it "allows multiple separators, but not adjacent to each other", ->
contextMenu.add
'.grandchild': [
{label: 'A', command: 'a'},
{type: 'separator'},
{type: 'separator'},
{label: 'B', command: 'b'},
{type: 'separator'},
{type: 'separator'},
{label: 'C', command: 'c'}
]
expect(contextMenu.templateForElement(grandchild)).toEqual [
{label: 'A', command: 'a'},
{type: 'separator'},
{label: 'B', command: 'b'},
{type: 'separator'},
{label: 'C', command: 'c'}
]
it "excludes items marked for display in devMode unless in dev mode", ->
disposable1 = contextMenu.add
'.grandchild': [{label: 'A', command: 'a', devMode: true}, {label: 'B', command: 'b', devMode: false}]
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'B', command: 'b'}]
contextMenu.devMode = true
expect(contextMenu.templateForElement(grandchild)).toEqual [{label: 'A', command: 'a'}, {label: 'B', command: 'b'}]
it "allows items to be associated with `created` hooks which are invoked on template construction with the item and event", ->
createdEvent = null
item = {
label: 'A',
command: 'a',
created: (event) ->
@command = 'b'
createdEvent = event
}
contextMenu.devModeDefinitions =
'.parent': [
label: 'dev-label'
command: 'dev-command'
]
contextMenu.add('.grandchild': [item])
describe "on a single element", ->
[element] = []
dispatchedEvent = {target: grandchild}
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual [{label: 'A', command: 'b'}]
expect(item.command).toBe 'a' # doesn't modify original item template
expect(createdEvent).toBe dispatchedEvent
beforeEach ->
element = ($$ -> @div class: 'parent')[0]
it "allows items to be associated with `shouldDisplay` hooks which are invoked on construction to determine whether the item should be included", ->
shouldDisplayEvent = null
shouldDisplay = true
it "creates a menu with a single item", ->
menu = contextMenu.combinedMenuTemplateForElement(element)
item = {
label: 'A',
command: 'a',
shouldDisplay: (event) ->
@foo = 'bar'
shouldDisplayEvent = event
shouldDisplay
}
contextMenu.add('.grandchild': [item])
expect(menu[0].label).toEqual 'parent'
expect(menu[0].command).toEqual 'command-p'
expect(menu[1]).toBeUndefined()
dispatchedEvent = {target: grandchild}
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual [{label: 'A', command: 'a'}]
expect(item.foo).toBeUndefined() # doesn't modify original item template
expect(shouldDisplayEvent).toBe dispatchedEvent
describe "in devMode", ->
beforeEach -> contextMenu.devMode = true
it "creates a menu with development items", ->
menu = contextMenu.combinedMenuTemplateForElement(element)
expect(menu[0].label).toEqual 'parent'
expect(menu[0].command).toEqual 'command-p'
expect(menu[1].type).toEqual 'separator'
expect(menu[2].label).toEqual 'dev-label'
expect(menu[2].command).toEqual 'dev-command'
describe "on multiple elements", ->
[element] = []
beforeEach ->
element = $$ ->
@div class: 'parent', =>
@div class: 'child'
element = element.find('.child')[0]
it "creates a menu with a two items", ->
menu = contextMenu.combinedMenuTemplateForElement(element)
expect(menu[0].label).toEqual 'child'
expect(menu[0].command).toEqual 'command-c'
expect(menu[1].label).toEqual 'parent'
expect(menu[1].command).toEqual 'command-p'
expect(menu[2]).toBeUndefined()
describe "in devMode", ->
beforeEach -> contextMenu.devMode = true
xit "creates a menu with development items", ->
menu = contextMenu.combinedMenuTemplateForElement(element)
expect(menu[0].label).toEqual 'child'
expect(menu[0].command).toEqual 'command-c'
expect(menu[1].label).toEqual 'parent'
expect(menu[1].command).toEqual 'command-p'
expect(menu[2].label).toEqual 'dev-label'
expect(menu[2].command).toEqual 'dev-command'
expect(menu[3]).toBeUndefined()
describe "executeBuildHandlers", ->
menuTemplate = [
label: 'label'
executeAtBuild: ->
]
event =
target: null
it 'should invoke the executeAtBuild fn', ->
buildFn = spyOn(menuTemplate[0], 'executeAtBuild')
contextMenu.executeBuildHandlers(event, menuTemplate)
expect(buildFn).toHaveBeenCalled()
expect(buildFn.mostRecentCall.args[0]).toBe event
shouldDisplay = false
expect(contextMenu.templateForEvent(dispatchedEvent)).toEqual []

View File

@ -1,33 +1,44 @@
DeserializerManager = require '../src/deserializer-manager'
describe ".deserialize(state)", ->
deserializer = null
describe "DeserializerManager", ->
manager = null
class Foo
@deserialize: ({name}) -> new Foo(name)
constructor: (@name) ->
beforeEach ->
deserializer = new DeserializerManager()
deserializer.add(Foo)
manager = new DeserializerManager
it "calls deserialize on the deserializer for the given state object, or returns undefined if one can't be found", ->
spyOn(console, 'warn')
object = deserializer.deserialize({ deserializer: 'Foo', name: 'Bar' })
expect(object.name).toBe 'Bar'
expect(deserializer.deserialize({ deserializer: 'Bogus' })).toBeUndefined()
describe "::add(deserializer)", ->
it "returns a disposable that can be used to remove the manager", ->
disposable = manager.add(Foo)
expect(manager.deserialize({deserializer: 'Foo', name: 'Bar'})).toBeDefined()
disposable.dispose()
spyOn(console, 'warn')
expect(manager.deserialize({deserializer: 'Foo', name: 'Bar'})).toBeUndefined()
describe "when the deserializer has a version", ->
describe "::deserialize(state)", ->
beforeEach ->
Foo.version = 2
manager.add(Foo)
describe "when the deserialized state has a matching version", ->
it "attempts to deserialize the state", ->
object = deserializer.deserialize({ deserializer: 'Foo', version: 2, name: 'Bar' })
expect(object.name).toBe 'Bar'
it "calls deserialize on the manager for the given state object, or returns undefined if one can't be found", ->
spyOn(console, 'warn')
object = manager.deserialize({deserializer: 'Foo', name: 'Bar'})
expect(object.name).toBe 'Bar'
expect(manager.deserialize({deserializer: 'Bogus'})).toBeUndefined()
describe "when the deserialized state has a non-matching version", ->
it "returns undefined", ->
expect(deserializer.deserialize({ deserializer: 'Foo', version: 3, name: 'Bar' })).toBeUndefined()
expect(deserializer.deserialize({ deserializer: 'Foo', version: 1, name: 'Bar' })).toBeUndefined()
expect(deserializer.deserialize({ deserializer: 'Foo', name: 'Bar' })).toBeUndefined()
describe "when the manager has a version", ->
beforeEach ->
Foo.version = 2
describe "when the deserialized state has a matching version", ->
it "attempts to deserialize the state", ->
object = manager.deserialize({deserializer: 'Foo', version: 2, name: 'Bar'})
expect(object.name).toBe 'Bar'
describe "when the deserialized state has a non-matching version", ->
it "returns undefined", ->
expect(manager.deserialize({deserializer: 'Foo', version: 3, name: 'Bar'})).toBeUndefined()
expect(manager.deserialize({deserializer: 'Foo', version: 1, name: 'Bar'})).toBeUndefined()
expect(manager.deserialize({deserializer: 'Foo', name: 'Bar'})).toBeUndefined()

View File

@ -0,0 +1,13 @@
module.exports =
config:
numbers:
type: 'object'
properties:
one:
type: 'integer'
default: 1
two:
type: 'integer'
default: 2
activate: -> # no-op

View File

@ -1,7 +1,8 @@
'menu': [
{ 'label': 'Second to Last' }
{'label': 'Second to Last'}
]
'context-menu':
'.test-1':
'Menu item 1': 'command-1'
'.test-1': [
{label: 'Menu item 1', command: 'command-1'}
]

View File

@ -3,5 +3,6 @@
]
'context-menu':
'.test-1':
'Menu item 2': 'command-2'
'.test-1': [
{label: 'Menu item 2', command: 'command-2'}
]

View File

@ -3,5 +3,6 @@
]
'context-menu':
'.test-1':
'Menu item 3': 'command-3'
'.test-1': [
{label: 'Menu item 3', command: 'command-3'}
]

View File

@ -223,7 +223,7 @@ describe "GitRepository", ->
[editor] = []
beforeEach ->
atom.project.setPath(copyRepository())
atom.project.setPaths([copyRepository()])
waitsForPromise ->
atom.workspace.open('other.txt').then (o) -> editor = o
@ -232,7 +232,7 @@ describe "GitRepository", ->
editor.insertNewline()
statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepo().onDidChangeStatus statusHandler
atom.project.getRepositories()[0].onDidChangeStatus statusHandler
editor.save()
expect(statusHandler.callCount).toBe 1
expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256}
@ -241,7 +241,7 @@ describe "GitRepository", ->
fs.writeFileSync(editor.getPath(), 'changed')
statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepo().onDidChangeStatus statusHandler
atom.project.getRepositories()[0].onDidChangeStatus statusHandler
editor.getBuffer().reload()
expect(statusHandler.callCount).toBe 1
expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256}
@ -252,7 +252,7 @@ describe "GitRepository", ->
fs.writeFileSync(editor.getPath(), 'changed')
statusHandler = jasmine.createSpy('statusHandler')
atom.project.getRepo().onDidChangeStatus statusHandler
atom.project.getRepositories()[0].onDidChangeStatus statusHandler
editor.getBuffer().emitter.emit 'did-change-path'
expect(statusHandler.callCount).toBe 1
expect(statusHandler).toHaveBeenCalledWith {path: editor.getPath(), pathStatus: 256}
@ -266,7 +266,7 @@ describe "GitRepository", ->
project2?.destroy()
it "subscribes to all the serialized buffers in the project", ->
atom.project.setPath(copyRepository())
atom.project.setPaths([copyRepository()])
waitsForPromise ->
atom.workspace.open('file.txt')
@ -283,7 +283,7 @@ describe "GitRepository", ->
buffer.append('changes')
statusHandler = jasmine.createSpy('statusHandler')
project2.getRepo().onDidChangeStatus statusHandler
project2.getRepositories()[0].onDidChangeStatus statusHandler
buffer.save()
expect(statusHandler.callCount).toBe 1
expect(statusHandler).toHaveBeenCalledWith {path: buffer.getPath(), pathStatus: 256}

View File

@ -0,0 +1,48 @@
MenuManager = require '../src/menu-manager'
describe "MenuManager", ->
menu = null
beforeEach ->
menu = new MenuManager(resourcePath: atom.getLoadSettings().resourcePath)
describe "::add(items)", ->
it "can add new menus that can be removed with the returned disposable", ->
disposable = menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}]
expect(menu.template).toEqual [{label: "A", submenu: [{label: "B", command: "b"}]}]
disposable.dispose()
expect(menu.template).toEqual []
it "can add submenu items to existing menus that can be removed with the returned disposable", ->
disposable1 = menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}]
disposable2 = menu.add [{label: "A", submenu: [{label: "C", submenu: [{label: "D", command: 'd'}]}]}]
disposable3 = menu.add [{label: "A", submenu: [{label: "C", submenu: [{label: "E", command: 'e'}]}]}]
expect(menu.template).toEqual [{
label: "A",
submenu: [
{label: "B", command: "b"},
{label: "C", submenu: [{label: 'D', command: 'd'}, {label: 'E', command: 'e'}]}
]
}]
disposable3.dispose()
expect(menu.template).toEqual [{
label: "A",
submenu: [
{label: "B", command: "b"},
{label: "C", submenu: [{label: 'D', command: 'd'}]}
]
}]
disposable2.dispose()
expect(menu.template).toEqual [{label: "A", submenu: [{label: "B", command: "b"}]}]
disposable1.dispose()
expect(menu.template).toEqual []
it "does not add duplicate labels to the same menu", ->
originalItemCount = menu.template.length
menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}]
menu.add [{label: "A", submenu: [{label: "B", command: "b"}]}]
expect(menu.template[originalItemCount]).toEqual {label: "A", submenu: [{label: "B", command: "b"}]}

View File

@ -82,7 +82,21 @@ describe "PackageManager", ->
expect(indexModule.activate).toHaveBeenCalled()
expect(pack.mainModule).toBe indexModule
it "assigns config defaults from the module", ->
it "assigns config schema, including defaults when package contains a schema", ->
expect(atom.config.get('package-with-config-schema.numbers.one')).toBeUndefined()
waitsForPromise ->
atom.packages.activatePackage('package-with-config-schema')
runs ->
expect(atom.config.get('package-with-config-schema.numbers.one')).toBe 1
expect(atom.config.get('package-with-config-schema.numbers.two')).toBe 2
expect(atom.config.set('package-with-config-schema.numbers.one', 'nope')).toBe false
expect(atom.config.set('package-with-config-schema.numbers.one', '10')).toBe true
expect(atom.config.get('package-with-config-schema.numbers.one')).toBe 10
it "still assigns configDefaults from the module though deprecated", ->
expect(atom.config.get('package-with-config-defaults.numbers.one')).toBeUndefined()
waitsForPromise ->
@ -216,30 +230,30 @@ describe "PackageManager", ->
it "loads all the .cson/.json files in the menus directory", ->
element = ($$ -> @div class: 'test-1')[0]
expect(atom.contextMenu.definitionsForElement(element)).toEqual []
expect(atom.contextMenu.templateForElement(element)).toEqual []
atom.packages.activatePackage("package-with-menus")
expect(atom.menu.template.length).toBe 2
expect(atom.menu.template[0].label).toBe "Second to Last"
expect(atom.menu.template[1].label).toBe "Last"
expect(atom.contextMenu.definitionsForElement(element)[0].label).toBe "Menu item 1"
expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 2"
expect(atom.contextMenu.definitionsForElement(element)[2].label).toBe "Menu item 3"
expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 1"
expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 2"
expect(atom.contextMenu.templateForElement(element)[2].label).toBe "Menu item 3"
describe "when the metadata contains a 'menus' manifest", ->
it "loads only the menus specified by the manifest, in the specified order", ->
element = ($$ -> @div class: 'test-1')[0]
expect(atom.contextMenu.definitionsForElement(element)).toEqual []
expect(atom.contextMenu.templateForElement(element)).toEqual []
atom.packages.activatePackage("package-with-menus-manifest")
expect(atom.menu.template[0].label).toBe "Second to Last"
expect(atom.menu.template[1].label).toBe "Last"
expect(atom.contextMenu.definitionsForElement(element)[0].label).toBe "Menu item 2"
expect(atom.contextMenu.definitionsForElement(element)[1].label).toBe "Menu item 1"
expect(atom.contextMenu.definitionsForElement(element)[2]).toBeUndefined()
expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 2"
expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 1"
expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined()
describe "stylesheet loading", ->
describe "when the metadata contains a 'stylesheets' manifest", ->

View File

@ -6,11 +6,11 @@ PaneView = require '../src/pane-view'
{$, View, $$} = require 'atom'
describe "PaneContainerView", ->
[TestView, container, pane1, pane2, pane3] = []
[TestView, container, pane1, pane2, pane3, deserializerDisposable] = []
beforeEach ->
class TestView extends View
atom.deserializers.add(this)
deserializerDisposable = atom.deserializers.add(this)
@deserialize: ({name}) -> new TestView(name)
@content: -> @div tabindex: -1
initialize: (@name) -> @text(@name)
@ -26,7 +26,7 @@ describe "PaneContainerView", ->
pane3 = pane2.splitDown(new TestView('3'))
afterEach ->
atom.deserializers.remove(TestView)
deserializerDisposable.dispose()
describe ".getActivePaneView()", ->
it "returns the most-recently focused pane", ->

View File

@ -4,6 +4,8 @@ PaneAxis = require '../src/pane-axis'
PaneContainer = require '../src/pane-container'
describe "Pane", ->
deserializerDisposable = null
class Item extends Model
@deserialize: ({name, uri}) -> new this(name, uri)
constructor: (@name, @uri) ->
@ -13,10 +15,10 @@ describe "Pane", ->
isEqual: (other) -> @name is other?.name
beforeEach ->
atom.deserializers.add(Item)
deserializerDisposable = atom.deserializers.add(Item)
afterEach ->
atom.deserializers.remove(Item)
deserializerDisposable.dispose()
describe "construction", ->
it "sets the active item to the first item", ->

View File

@ -7,7 +7,7 @@ path = require 'path'
temp = require 'temp'
describe "PaneView", ->
[container, containerModel, view1, view2, editor1, editor2, pane, paneModel] = []
[container, containerModel, view1, view2, editor1, editor2, pane, paneModel, deserializerDisposable] = []
class TestView extends View
@deserialize: ({id, text}) -> new TestView({id, text})
@ -23,7 +23,7 @@ describe "PaneView", ->
@emitter.on 'did-change-title', callback
beforeEach ->
atom.deserializers.add(TestView)
deserializerDisposable = atom.deserializers.add(TestView)
container = atom.workspace.getView(new PaneContainer).__spacePenView
containerModel = container.model
view1 = new TestView(id: 'view-1', text: 'View 1')
@ -40,7 +40,7 @@ describe "PaneView", ->
paneModel.addItems([view1, editor1, view2, editor2])
afterEach ->
atom.deserializers.remove(TestView)
deserializerDisposable.dispose()
describe "when the active pane item changes", ->
it "hides all item views except the active one", ->

View File

@ -9,7 +9,7 @@ BufferedProcess = require '../src/buffered-process'
describe "Project", ->
beforeEach ->
atom.project.setPath(atom.project.resolve('dir'))
atom.project.setPaths([atom.project.resolve('dir')])
describe "serialization", ->
deserializedProject = null
@ -41,8 +41,8 @@ describe "Project", ->
describe "when an editor is saved and the project has no path", ->
it "sets the project's path to the saved file's parent directory", ->
tempFile = temp.openSync().path
atom.project.setPath(undefined)
expect(atom.project.getPath()).toBeUndefined()
atom.project.setPaths([])
expect(atom.project.getPaths()[0]).toBeUndefined()
editor = null
waitsForPromise ->
@ -50,7 +50,7 @@ describe "Project", ->
runs ->
editor.saveAs(tempFile)
expect(atom.project.getPath()).toBe path.dirname(tempFile)
expect(atom.project.getPaths()[0]).toBe path.dirname(tempFile)
describe ".open(path)", ->
[absolutePath, newBufferHandler] = []
@ -164,7 +164,7 @@ describe "Project", ->
describe "when the project has no path", ->
it "returns undefined for relative URIs", ->
atom.project.setPath()
atom.project.setPaths([])
expect(atom.project.resolve('test.txt')).toBeUndefined()
expect(atom.project.resolve('http://github.com')).toBe 'http://github.com'
absolutePath = fs.absolute(__dirname)
@ -173,33 +173,33 @@ describe "Project", ->
describe ".setPath(path)", ->
describe "when path is a file", ->
it "sets its path to the files parent directory and updates the root directory", ->
atom.project.setPath(require.resolve('./fixtures/dir/a'))
expect(atom.project.getPath()).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
atom.project.setPaths([require.resolve('./fixtures/dir/a')])
expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
expect(atom.project.getRootDirectory().path).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
describe "when path is a directory", ->
it "sets its path to the directory and updates the root directory", ->
directory = fs.absolute(path.join(__dirname, 'fixtures', 'dir', 'a-dir'))
atom.project.setPath(directory)
expect(atom.project.getPath()).toEqual directory
atom.project.setPaths([directory])
expect(atom.project.getPaths()[0]).toEqual directory
expect(atom.project.getRootDirectory().path).toEqual directory
describe "when path is null", ->
it "sets its path and root directory to null", ->
atom.project.setPath(null)
expect(atom.project.getPath()?).toBeFalsy()
atom.project.setPaths([])
expect(atom.project.getPaths()[0]?).toBeFalsy()
expect(atom.project.getRootDirectory()?).toBeFalsy()
it "normalizes the path to remove consecutive slashes, ., and .. segments", ->
atom.project.setPath("#{require.resolve('./fixtures/dir/a')}#{path.sep}b#{path.sep}#{path.sep}..")
expect(atom.project.getPath()).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
atom.project.setPaths(["#{require.resolve('./fixtures/dir/a')}#{path.sep}b#{path.sep}#{path.sep}.."])
expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
expect(atom.project.getRootDirectory().path).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
describe ".replace()", ->
[filePath, commentFilePath, sampleContent, sampleCommentContent] = []
beforeEach ->
atom.project.setPath(atom.project.resolve('../'))
atom.project.setPaths([atom.project.resolve('../')])
filePath = atom.project.resolve('sample.js')
commentFilePath = atom.project.resolve('sample-with-comments.js')
@ -332,7 +332,7 @@ describe "Project", ->
it "works on evil filenames", ->
platform.generateEvilFiles()
atom.project.setPath(path.join(__dirname, 'fixtures', 'evil-files'))
atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')])
paths = []
matches = []
waitsForPromise ->
@ -387,7 +387,7 @@ describe "Project", ->
fs.removeSync(projectPath) if fs.existsSync(projectPath)
it "excludes ignored files", ->
atom.project.setPath(projectPath)
atom.project.setPaths([projectPath])
atom.config.set('core.excludeVcsIgnoredPaths', true)
resultHandler = jasmine.createSpy("result found")
waitsForPromise ->
@ -399,7 +399,7 @@ describe "Project", ->
it "includes only files when a directory filter is specified", ->
projectPath = path.join(path.join(__dirname, 'fixtures', 'dir'))
atom.project.setPath(projectPath)
atom.project.setPaths([projectPath])
filePath = path.join(projectPath, 'a-dir', 'oh-git')
@ -419,7 +419,7 @@ describe "Project", ->
projectPath = temp.mkdirSync()
filePath = path.join(projectPath, '.text')
fs.writeFileSync(filePath, 'match this')
atom.project.setPath(projectPath)
atom.project.setPaths([projectPath])
paths = []
matches = []
waitsForPromise ->

View File

@ -69,7 +69,7 @@ beforeEach ->
$.fx.off = true
documentTitle = null
projectPath = specProjectPath ? path.join(@specDirectory, 'fixtures')
atom.project = new Project(path: projectPath)
atom.project = new Project(paths: [projectPath])
atom.workspace = new Workspace()
atom.keymaps.keyBindings = _.clone(keyBindingsToRestore)
atom.commands.setRootNode(document.body)
@ -98,16 +98,16 @@ beforeEach ->
config = new Config({resourcePath, configDirPath: atom.getConfigDirPath()})
spyOn(config, 'load')
spyOn(config, 'save')
config.setDefaults('core', WorkspaceView.configDefaults)
config.setDefaults('editor', TextEditorView.configDefaults)
atom.config = config
atom.loadConfig()
config.set "core.destroyEmptyPanes", false
config.set "editor.fontFamily", "Courier"
config.set "editor.fontSize", 16
config.set "editor.autoIndent", false
config.set "core.disabledPackages", ["package-that-throws-an-exception",
"package-with-broken-package-json", "package-with-broken-keymap"]
config.load.reset()
config.save.reset()
atom.config = config
# make editor display updates synchronous
spyOn(TextEditorView.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay()
@ -132,6 +132,7 @@ beforeEach ->
afterEach ->
atom.packages.deactivatePackages()
atom.menu.template = []
atom.contextMenu.clear()
atom.workspaceView?.remove?()
atom.workspaceView = null

View File

@ -1734,11 +1734,11 @@ describe "TextEditorComponent", ->
nextAnimationFrame()
expect(verticalScrollbarNode.scrollTop).toBe 10
it "parses negative scrollSensitivity values as positive", ->
it "parses negative scrollSensitivity values at the minimum", ->
atom.config.set('editor.scrollSensitivity', -50)
componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -10))
nextAnimationFrame()
expect(verticalScrollbarNode.scrollTop).toBe 5
expect(verticalScrollbarNode.scrollTop).toBe 1
describe "when the mousewheel event's target is a line", ->
it "keeps the line on the DOM if it is scrolled off-screen", ->
@ -2264,6 +2264,10 @@ describe "TextEditorComponent", ->
wrapperView.detach()
wrapperView.attachToDom()
wrapperView.trigger('core:move-right')
expect(editor.getCursorBufferPosition()).toEqual [0, 1]
buildMouseEvent = (type, properties...) ->
properties = extend({bubbles: true, cancelable: true}, properties...)
properties.detail ?= 1

View File

@ -52,7 +52,7 @@ describe "ThemeManager", ->
expect(themeManager.getEnabledThemeNames()).toEqual ['atom-dark-ui', 'atom-light-ui']
describe "getImportPaths()", ->
describe "::getImportPaths()", ->
it "returns the theme directories before the themes are loaded", ->
atom.config.set('core.themes', ['theme-with-index-less', 'atom-dark-ui', 'atom-light-ui'])
@ -129,7 +129,7 @@ describe "ThemeManager", ->
spyOn(console, 'warn')
expect(-> atom.packages.activatePackage('a-theme-that-will-not-be-found')).toThrow()
describe "requireStylesheet(path)", ->
describe "::requireStylesheet(path)", ->
it "synchronously loads css at the given path and installs a style tag for it in the head", ->
themeManager.onDidChangeStylesheets stylesheetsChangedHandler = jasmine.createSpy("stylesheetsChangedHandler")
themeManager.onDidAddStylesheet stylesheetAddedHandler = jasmine.createSpy("stylesheetAddedHandler")
@ -185,18 +185,17 @@ describe "ThemeManager", ->
$('head style[id*="css.css"]').remove()
$('head style[id*="sample.less"]').remove()
describe ".removeStylesheet(path)", ->
it "removes styling applied by given stylesheet path", ->
it "returns a disposable allowing styles applied by the given path to be removed", ->
cssPath = require.resolve('./fixtures/css.css')
expect($(document.body).css('font-weight')).not.toBe("bold")
themeManager.requireStylesheet(cssPath)
disposable = themeManager.requireStylesheet(cssPath)
expect($(document.body).css('font-weight')).toBe("bold")
themeManager.onDidRemoveStylesheet stylesheetRemovedHandler = jasmine.createSpy("stylesheetRemovedHandler")
themeManager.onDidChangeStylesheets stylesheetsChangedHandler = jasmine.createSpy("stylesheetsChangedHandler")
themeManager.removeStylesheet(cssPath)
disposable.dispose()
expect($(document.body).css('font-weight')).not.toBe("bold")

View File

@ -586,7 +586,7 @@ describe "TokenizedBuffer", ->
atom.config.set('editor.tabLength', 1)
expect(tokenizedBuffer.tokenForPosition([0,0]).value).toBe ' '
atom.config.set('editor.tabLength', 0)
expect(tokenizedBuffer.tokenForPosition([0,0]).value).toBe ' '
expect(tokenizedBuffer.tokenForPosition([0,0]).value).toBe ' '
describe "when the invisibles value changes", ->
beforeEach ->

View File

@ -8,7 +8,7 @@ describe "Window", ->
beforeEach ->
spyOn(atom, 'hide')
initialPath = atom.project.getPath()
initialPath = atom.project.getPaths()[0]
spyOn(atom, 'getLoadSettings').andCallFake ->
loadSettings = atom.getLoadSettings.originalValue.call(atom)
loadSettings.initialPath = initialPath
@ -16,7 +16,7 @@ describe "Window", ->
atom.project.destroy()
windowEventHandler = new WindowEventHandler()
atom.deserializeEditorWindow()
projectPath = atom.project.getPath()
projectPath = atom.project.getPaths()[0]
afterEach ->
windowEventHandler.unsubscribe()
@ -263,19 +263,19 @@ describe "Window", ->
describe "when the project does not have a path", ->
beforeEach ->
atom.project.setPath()
atom.project.setPaths([])
describe "when the opened path exists", ->
it "sets the project path to the opened path", ->
$(window).trigger('window:open-path', [{pathToOpen: __filename}])
expect(atom.project.getPath()).toBe __dirname
expect(atom.project.getPaths()[0]).toBe __dirname
describe "when the opened path does not exist but its parent directory does", ->
it "sets the project path to the opened path's parent directory", ->
$(window).trigger('window:open-path', [{pathToOpen: path.join(__dirname, 'this-path-does-not-exist.txt')}])
expect(atom.project.getPath()).toBe __dirname
expect(atom.project.getPaths()[0]).toBe __dirname
describe "when the opened path is a file", ->
it "opens it in the workspace", ->

View File

@ -7,7 +7,7 @@ describe "Workspace", ->
workspace = null
beforeEach ->
atom.project.setPath(atom.project.resolve('dir'))
atom.project.setPaths([atom.project.resolve('dir')])
atom.workspace = workspace = new Workspace
describe "::open(uri, options)", ->
@ -222,8 +222,8 @@ describe "Workspace", ->
it "returns the resource returned by the custom opener", ->
fooOpener = (pathToOpen, options) -> { foo: pathToOpen, options } if pathToOpen?.match(/\.foo/)
barOpener = (pathToOpen) -> { bar: pathToOpen } if pathToOpen?.match(/^bar:\/\//)
workspace.registerOpener(fooOpener)
workspace.registerOpener(barOpener)
workspace.addOpener(fooOpener)
workspace.addOpener(barOpener)
waitsForPromise ->
pathToOpen = atom.project.resolve('a.foo')
@ -387,25 +387,25 @@ describe "Workspace", ->
it "sets the title to the pane item's title plus the project path", ->
item = atom.workspace.getActivePaneItem()
console.log item.getTitle()
expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPath()}"
expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]}"
describe "when the title of the active pane item changes", ->
it "updates the window title based on the item's new title", ->
editor = atom.workspace.getActivePaneItem()
editor.buffer.setPath(path.join(temp.dir, 'hi'))
expect(document.title).toBe "#{editor.getTitle()} - #{atom.project.getPath()}"
expect(document.title).toBe "#{editor.getTitle()} - #{atom.project.getPaths()[0]}"
describe "when the active pane's item changes", ->
it "updates the title to the new item's title plus the project path", ->
atom.workspace.getActivePane().activateNextItem()
item = atom.workspace.getActivePaneItem()
expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPath()}"
expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]}"
describe "when the last pane item is removed", ->
it "updates the title to contain the project's path", ->
atom.workspace.getActivePane().destroy()
expect(atom.workspace.getActivePaneItem()).toBeUndefined()
expect(document.title).toBe atom.project.getPath()
expect(document.title).toBe atom.project.getPaths()[0]
describe "when an inactive pane's item changes", ->
it "does not update the title", ->
@ -424,7 +424,7 @@ describe "Workspace", ->
console.log atom.workspace.getActivePaneItem()
workspace2 = atom.workspace.testSerialization()
item = atom.workspace.getActivePaneItem()
expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPath()}"
expect(document.title).toBe "#{item.getTitle()} - #{atom.project.getPaths()[0]}"
workspace2.destroy()
describe "document edited status", ->

View File

@ -10,7 +10,7 @@ describe "WorkspaceView", ->
pathToOpen = null
beforeEach ->
atom.project.setPath(atom.project.resolve('dir'))
atom.project.setPaths([atom.project.resolve('dir')])
pathToOpen = atom.project.resolve('a')
atom.workspace = new Workspace
atom.workspaceView = atom.workspace.getView(atom.workspace).__spacePenView
@ -49,7 +49,7 @@ describe "WorkspaceView", ->
expect(atom.workspaceView.getEditorViews().length).toBe 2
expect(atom.workspaceView.getActivePaneView()).toBe atom.workspaceView.getPaneViews()[1]
expect(document.title).toBe "untitled - #{atom.project.getPath()}"
expect(document.title).toBe "untitled - #{atom.project.getPaths()[0]}"
describe "when there are open editors", ->
it "constructs the view with the same panes", ->
@ -106,7 +106,7 @@ describe "WorkspaceView", ->
expect(editorView3).not.toHaveFocus()
expect(editorView4).not.toHaveFocus()
expect(document.title).toBe "#{path.basename(editorView2.getEditor().getPath())} - #{atom.project.getPath()}"
expect(document.title).toBe "#{path.basename(editorView2.getEditor().getPath())} - #{atom.project.getPaths()[0]}"
describe "where there are no open editors", ->
it "constructs the view with no open editors", ->

View File

@ -466,9 +466,7 @@ class Atom extends Model
console.warn error.message if error?
dimensions = @restoreWindowDimensions()
@config.load()
@config.setDefaults('core', require('./workspace-view').configDefaults)
@config.setDefaults('editor', require('./text-editor-view').configDefaults)
@loadConfig()
@keymaps.loadBundledKeymaps()
@themes.loadBaseStylesheets()
@packages.loadPackages()
@ -579,7 +577,7 @@ class Atom extends Model
Project = require './project'
startTime = Date.now()
@project ?= @deserializers.deserialize(@state.project) ? new Project(path: @getLoadSettings().initialPath)
@project ?= @deserializers.deserialize(@state.project) ? new Project(paths: [@getLoadSettings().initialPath])
@deserializeTimings.project = Date.now() - startTime
deserializeWorkspaceView: ->
@ -604,6 +602,10 @@ class Atom extends Model
@deserializeProject()
@deserializeWorkspaceView()
loadConfig: ->
@config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))}
@config.load()
loadThemes: ->
@themes.load()
@ -617,8 +619,8 @@ class Atom extends Model
# Notify the browser project of the window's current project path
watchProjectPath: ->
onProjectPathChanged = =>
ipc.send('window-command', 'project-path-changed', @project.getPath())
@subscribe @project, 'path-changed', onProjectPathChanged
ipc.send('window-command', 'project-path-changed', @project.getPaths()[0])
@subscribe @project.onDidChangePaths(onProjectPathChanged)
onProjectPathChanged()
exit: (status) ->

View File

@ -28,6 +28,7 @@ class AtomWindow
title: 'Atom'
icon: @constructor.iconPath
'web-preferences':
'direct-write': false
'subpixel-font-scaling': false
global.atomApplication.addWindow(this)

View File

@ -13,12 +13,12 @@ class ContextMenu
createClickHandlers: (template) ->
for item in template
if item.command
item.commandOptions ?= {}
item.commandOptions.contextCommand = true
item.commandOptions.atomWindow = @atomWindow
item.commandDetail ?= {}
item.commandDetail.contextCommand = true
item.commandDetail.atomWindow = @atomWindow
do (item) =>
item.click = =>
global.atomApplication.sendCommandToWindow(item.command, @atomWindow, item.commandOptions)
global.atomApplication.sendCommandToWindow(item.command, @atomWindow, item.commandDetail)
else if item.submenu
@createClickHandlers(item.submenu)
item

115
src/config-schema.coffee Normal file
View File

@ -0,0 +1,115 @@
path = require 'path'
fs = require 'fs-plus'
# This is loaded by atom.coffee
module.exports =
core:
type: 'object'
properties:
ignoredNames:
type: 'array'
default: [".git", ".hg", ".svn", ".DS_Store", "Thumbs.db"]
items:
type: 'string'
excludeVcsIgnoredPaths:
type: 'boolean'
default: true
title: 'Exclude VCS Ignored Paths'
disabledPackages:
type: 'array'
default: []
items:
type: 'string'
themes:
type: 'array'
default: ['atom-dark-ui', 'atom-dark-syntax']
items:
type: 'string'
projectHome:
type: 'string'
default: path.join(fs.getHomeDirectory(), 'github')
audioBeep:
type: 'boolean'
default: true
destroyEmptyPanes:
type: 'boolean'
default: true
editor:
type: 'object'
properties:
fontFamily:
type: 'string'
default: ''
fontSize:
type: 'integer'
default: 16
minimum: 1
lineHeight:
type: ['string', 'number']
default: 1.3
showInvisibles:
type: 'boolean'
default: false
showIndentGuide:
type: 'boolean'
default: false
showLineNumbers:
type: 'boolean'
default: true
autoIndent:
type: 'boolean'
default: true
normalizeIndentOnPaste:
type: 'boolean'
default: true
nonWordCharacters:
type: 'string'
default: "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
preferredLineLength:
type: 'integer'
default: 80
minimum: 1
tabLength:
type: 'integer'
default: 2
minimum: 1
softWrap:
type: 'boolean'
default: false
softTabs:
type: 'boolean'
default: true
softWrapAtPreferredLineLength:
type: 'boolean'
default: false
scrollSensitivity:
type: 'integer'
default: 40
minimum: 10
maximum: 200
scrollPastEnd:
type: 'boolean'
default: false
useHardwareAcceleration:
type: 'boolean'
default: true
confirmCheckoutHeadRevision:
type: 'boolean'
default: true
title: 'Confirm Checkout HEAD Revision'
invisibles:
type: 'object'
properties:
eol:
type: ['boolean', 'string']
default: '\u00ac'
space:
type: ['boolean', 'string']
default: '\u00b7'
tab:
type: ['boolean', 'string']
default: '\u00bb'
cr:
type: ['boolean', 'string']
default: '\u00a4'

View File

@ -1,39 +1,510 @@
_ = require 'underscore-plus'
fs = require 'fs-plus'
{Emitter} = require 'emissary'
EmitterMixin = require('emissary').Emitter
{Emitter} = require 'event-kit'
CSON = require 'season'
path = require 'path'
async = require 'async'
pathWatcher = require 'pathwatcher'
{deprecate} = require 'grim'
# Essential: Used to access all of Atom's configuration details.
#
# An instance of this class is always available as the `atom.config` global.
#
# ## Best practices
#
# * Create your own root keypath using your package's name.
# * Don't depend on (or write to) configuration keys outside of your keypath.
#
# ## Examples
# ## Getting and setting config settings. Note that with no value set, {::get}
# returns the setting's default value.
#
# ```coffee
# atom.config.set('my-package.key', 'value')
# atom.config.observe 'my-package.key', ->
# console.log 'My configuration changed:', atom.config.get('my-package.key')
# atom.config.get('my-package.myKey') # -> 'defaultValue'
#
# atom.config.set('my-package.myKey', 'value')
# atom.config.get('my-package.myKey') # -> 'value'
# ```
#
# You may want to watch for changes. Use {::observe} to catch changes to the setting.
#
# ```coffee
# atom.config.set('my-package.myKey', 'value')
# atom.config.observe 'my-package.myKey', (newValue) ->
# # `observe` calls immediately and every time the value is changed
# console.log 'My configuration changed:', newValue
# ```
#
# If you want a notification only when the value changes, use {::onDidChange}.
#
# ```coffee
# atom.config.onDidChange 'my-package.myKey', ({newValue, oldValue}) ->
# console.log 'My configuration changed:', newValue, oldValue
# ```
#
# ### Value Coercion
#
# Config settings each have a type specified by way of a
# [schema](json-schema.org). For example we might an integer setting that only
# allows integers greater than `0`:
#
# ```coffee
# # When no value has been set, `::get` returns the setting's default value
# atom.config.get('my-package.anInt') # -> 12
#
# # The string will be coerced to the integer 123
# atom.config.set('my-package.anInt', '123')
# atom.config.get('my-package.anInt') # -> 123
#
# # The string will be coerced to an integer, but it must be greater than 0, so is set to 1
# atom.config.set('my-package.anInt', '-20')
# atom.config.get('my-package.anInt') # -> 1
# ```
#
# ## Defining settings for your package
#
# Define a schema under a `config` key in your package main.
#
# ```coffee
# module.exports =
# # Your config schema
# config:
# someInt:
# type: 'integer'
# default: 23
# minimum: 1
#
# activate: (state) -> # ...
# # ...
# ```
#
# See [Creating a Package](https://atom.io/docs/latest/creating-a-package) for
# more info.
#
# ## Config Schemas
#
# We use [json schema](json-schema.org) which allows you to define your value's
# default, the type it should be, etc. A simple example:
#
# ```coffee
# # We want to provide an `enableThing`, and a `thingVolume`
# config:
# enableThing:
# type: 'boolean'
# default: false
# thingVolume:
# type: 'integer'
# default: 5
# minimum: 1
# maximum: 11
# ```
#
# The type keyword allows for type coercion and validation. If a `thingVolume` is
# set to a string `'10'`, it will be coerced into an integer.
#
# ```coffee
# atom.config.set('my-package.thingVolume', '10')
# atom.config.get('my-package.thingVolume') # -> 10
#
# # It respects the min / max
# atom.config.set('my-package.thingVolume', '400')
# atom.config.get('my-package.thingVolume') # -> 11
#
# # If it cannot be coerced, the value will not be set
# atom.config.set('my-package.thingVolume', 'cats')
# atom.config.get('my-package.thingVolume') # -> 11
# ```
#
# ### Supported Types
#
# The `type` keyword can be a string with any one of the following. You can also
# chain them by specifying multiple in an an array. For example
#
# ```coffee
# config:
# someSetting:
# type: ['boolean', 'integer']
# default: 5
#
# # Then
# atom.config.set('my-package.someSetting', 'true')
# atom.config.get('my-package.someSetting') # -> true
#
# atom.config.set('my-package.someSetting', '12')
# atom.config.get('my-package.someSetting') # -> 12
# ```
#
# #### string
#
# Values must be a string.
#
# ```coffee
# config:
# someSetting:
# type: 'string'
# default: 'hello'
# ```
#
# #### integer
#
# Values will be coerced into integer. Supports the (optional) `minimum` and
# `maximum` keys.
#
# ```coffee
# config:
# someSetting:
# type: 'integer'
# default: 5
# minimum: 1
# maximum: 11
# ```
#
# #### number
#
# Values will be coerced into a number, including real numbers. Supports the
# (optional) `minimum` and `maximum` keys.
#
# ```coffee
# config:
# someSetting:
# type: 'number'
# default: 5.3
# minimum: 1.5
# maximum: 11.5
# ```
#
# #### boolean
#
# Values will be coerced into a Boolean. `'true'` and `'false'` will be coerced into
# a boolean. Numbers, arrays, objects, and anything else will not be coerced.
#
# ```coffee
# config:
# someSetting:
# type: 'boolean'
# default: false
# ```
#
# #### array
#
# Value must be an Array. The types of the values can be specified by a
# subschema in the `items` key.
#
# ```coffee
# config:
# someSetting:
# type: 'array'
# default: [1, 2, 3]
# items:
# type: 'integer'
# minimum: 1.5
# maximum: 11.5
# ```
#
# #### object
#
# Value must be an object. This allows you to nest config options. Sub options
# must be under a `properties key`
#
# ```coffee
# config:
# someSetting:
# type: 'object'
# properties:
# myChildIntOption:
# type: 'integer'
# minimum: 1.5
# maximum: 11.5
# ```
#
# ### Other Supported Keys
#
# #### enum
#
# All types support an `enum` key. The enum key lets you specify all values
# that the config setting can possibly be. `enum` _must_ be an array of values
# of your specified type.
#
# ```coffee
# config:
# someSetting:
# type: 'integer'
# default: 4
# enum: [2, 4, 6, 8]
# ```
#
# ```coffee
# atom.config.set('my-package.someSetting', '2')
# atom.config.get('my-package.someSetting') # -> 2
#
# # will not set values outside of the enum values
# atom.config.set('my-package.someSetting', '3')
# atom.config.get('my-package.someSetting') # -> 2
#
# # If it cannot be coerced, the value will not be set
# atom.config.set('my-package.someSetting', '4')
# atom.config.get('my-package.someSetting') # -> 4
# ```
#
# #### title and description
#
# The settings view will use the `title` and `description` keys to display your
# config setting in a readable way. By default the settings view humanizes your
# config key, so `someSetting` becomes `Some Setting`. In some cases, this is
# confusing for users, and a more descriptive title is useful.
#
# Descriptions will be displayed below the title in the settings view.
#
# ```coffee
# config:
# someSetting:
# title: 'Setting Magnitude'
# description: 'This will affect the blah and the other blah'
# type: 'integer'
# default: 4
# ```
#
# __Note__: You should strive to be so clear in your naming of the setting that
# you do not need to specify a title or description!
#
# ## Best practices
#
# * Don't depend on (or write to) configuration keys outside of your keypath.
#
module.exports =
class Config
Emitter.includeInto(this)
EmitterMixin.includeInto(this)
@schemaEnforcers = {}
@addSchemaEnforcer: (typeName, enforcerFunction) ->
@schemaEnforcers[typeName] ?= []
@schemaEnforcers[typeName].push(enforcerFunction)
@addSchemaEnforcers: (filters) ->
for typeName, functions of filters
for name, enforcerFunction of functions
@addSchemaEnforcer(typeName, enforcerFunction)
@executeSchemaEnforcers: (keyPath, value, schema) ->
error = null
types = schema.type
types = [types] unless Array.isArray(types)
for type in types
try
enforcerFunctions = @schemaEnforcers[type].concat(@schemaEnforcers['*'])
for enforcer in enforcerFunctions
# At some point in one's life, one must call upon an enforcer.
value = enforcer.call(this, keyPath, value, schema)
error = null
break
catch e
error = e
throw error if error?
value
# Created during initialization, available as `atom.config`
constructor: ({@configDirPath, @resourcePath}={}) ->
@emitter = new Emitter
@schema =
type: 'object'
properties: {}
@defaultSettings = {}
@settings = {}
@configFileHasErrors = false
@configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson'])
@configFilePath ?= path.join(@configDirPath, 'config.cson')
###
Section: Config Subscription
###
# Essential: Add a listener for changes to a given key path. This is different
# than {::onDidChange} in that it will immediately call your callback with the
# current value of the config entry.
#
# * `keyPath` {String} name of the key to observe
# * `callback` {Function} to call when the value of the key changes.
# * `value` the new value of the key
#
# Returns a {Disposable} with the following keys on which you can call
# `.dispose()` to unsubscribe.
observe: (keyPath, options={}, callback) ->
if _.isFunction(options)
callback = options
options = {}
else
message = ""
message = "`callNow` was set to false. Use ::onDidChange instead. Note that ::onDidChange calls back with different arguments." if options.callNow == false
deprecate "Config::observe no longer supports options. #{message}"
callback(_.clone(@get(keyPath))) unless options.callNow == false
@emitter.on 'did-change', (event) ->
callback(event.newValue) if keyPath? and keyPath.indexOf(event?.keyPath) is 0
# Essential: Add a listener for changes to a given key path. If `keyPath` is
# not specified, your callback will be called on changes to any key.
#
# * `keyPath` (optional) {String} name of the key to observe
# * `callback` {Function} to call when the value of the key changes.
# * `event` {Object}
# * `newValue` the new value of the key
# * `oldValue` the prior value of the key.
# * `keyPath` the keyPath of the changed key
#
# Returns a {Disposable} with the following keys on which you can call
# `.dispose()` to unsubscribe.
onDidChange: (keyPath, callback) ->
if arguments.length is 1
callback = keyPath
keyPath = undefined
@emitter.on 'did-change', (event) ->
callback(event) if not keyPath? or (keyPath? and keyPath.indexOf(event?.keyPath) is 0)
###
Section: Managing Settings
###
# Essential: Retrieves the setting for the given key.
#
# * `keyPath` The {String} name of the key to retrieve.
#
# Returns the value from Atom's default settings, the user's configuration
# file in the type specified by the configuration schema.
get: (keyPath) ->
value = _.valueForKeyPath(@settings, keyPath)
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
if value?
value = _.deepClone(value)
valueIsObject = _.isObject(value) and not _.isArray(value)
defaultValueIsObject = _.isObject(defaultValue) and not _.isArray(defaultValue)
if valueIsObject and defaultValueIsObject
_.defaults(value, defaultValue)
else
value = _.deepClone(defaultValue)
value
# Essential: Sets the value for a configuration setting.
#
# This value is stored in Atom's internal configuration file.
#
# * `keyPath` The {String} name of the key.
# * `value` The value of the setting. Passing `undefined` will revert the
# setting to the default value.
#
# Returns a {Boolean}
# * `true` if the value was set.
# * `false` if the value was not able to be coerced to the type specified in the setting's schema.
set: (keyPath, value) ->
unless value == undefined
try
value = @makeValueConformToSchema(keyPath, value)
catch e
return false
@setRawValue(keyPath, value)
@save() unless @configFileHasErrors
true
# Extended: Restore the key path to its default value.
#
# * `keyPath` The {String} name of the key.
#
# Returns the new value.
restoreDefault: (keyPath) ->
@set(keyPath, _.valueForKeyPath(@defaultSettings, keyPath))
@get(keyPath)
# Extended: Get the default value of the key path. _Please note_ that in most
# cases calling this is not necessary! {::get} returns the default value when
# a custom value is not specified.
#
# * `keyPath` The {String} name of the key.
#
# Returns the default value.
getDefault: (keyPath) ->
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
_.deepClone(defaultValue)
# Extended: Is the key path value its default value?
#
# * `keyPath` The {String} name of the key.
#
# Returns a {Boolean}, `true` if the current value is the default, `false`
# otherwise.
isDefault: (keyPath) ->
not _.valueForKeyPath(@settings, keyPath)?
# Extended: Retrieve the schema for a specific key path. The shema will tell
# you what type the keyPath expects, and other metadata about the config
# option.
#
# * `keyPath` The {String} name of the key.
#
# Returns an {Object} eg. `{type: 'integer', default: 23, minimum: 1}`.
# Returns `null` when the keyPath has no schema specified.
getSchema: (keyPath) ->
keys = keyPath.split('.')
schema = @schema
for key in keys
break unless schema?
schema = schema.properties[key]
schema
# Extended: Returns a new {Object} containing all of settings and defaults.
getSettings: ->
_.deepExtend(@settings, @defaultSettings)
# Extended: Get the {String} path to the config file being used.
getUserConfigPath: ->
@configFilePath
###
Section: Deprecated
###
getInt: (keyPath) ->
deprecate '''Config::getInt is no longer necessary. Use ::get instead.
Make sure the config option you are accessing has specified an `integer`
schema. See the schema section of
https://atom.io/docs/api/latest/Config for more info.'''
parseInt(@get(keyPath))
getPositiveInt: (keyPath, defaultValue=0) ->
deprecate '''Config::getPositiveInt is no longer necessary. Use ::get instead.
Make sure the config option you are accessing has specified an `integer`
schema with `minimum: 1`. See the schema section of
https://atom.io/docs/api/latest/Config for more info.'''
Math.max(@getInt(keyPath), 0) or defaultValue
toggle: (keyPath) ->
deprecate 'Config::toggle is no longer supported. Please remove from your code.'
@set(keyPath, !@get(keyPath))
unobserve: (keyPath) ->
deprecate 'Config::unobserve no longer does anything. Call `.dispose()` on the object returned by Config::observe instead.'
###
Section: Private
###
pushAtKeyPath: (keyPath, value) ->
arrayValue = @get(keyPath) ? []
result = arrayValue.push(value)
@set(keyPath, arrayValue)
result
unshiftAtKeyPath: (keyPath, value) ->
arrayValue = @get(keyPath) ? []
result = arrayValue.unshift(value)
@set(keyPath, arrayValue)
result
removeAtKeyPath: (keyPath, value) ->
arrayValue = @get(keyPath) ? []
result = _.remove(arrayValue, value)
@set(keyPath, arrayValue)
result
initializeConfigDirectory: (done) ->
return if fs.existsSync(@configDirPath)
@ -62,9 +533,8 @@ class Config
try
userConfig = CSON.readFileSync(@configFilePath)
_.extend(@settings, userConfig)
@setAll(userConfig)
@configFileHasErrors = false
@emit 'updated'
catch error
@configFileHasErrors = true
console.error "Failed to load user config '#{@configFilePath}'", error.message
@ -82,194 +552,196 @@ class Config
@watchSubscription?.close()
@watchSubscription = null
setDefaults: (keyPath, defaults) ->
keys = keyPath.split('.')
hash = @defaultSettings
for key in keys
hash[key] ?= {}
hash = hash[key]
_.extend hash, defaults
@emit 'updated'
# Extended: Get the {String} path to the config file being used.
getUserConfigPath: ->
@configFilePath
# Extended: Returns a new {Object} containing all of settings and defaults.
getSettings: ->
_.deepExtend(@settings, @defaultSettings)
# Essential: Retrieves the setting for the given key.
#
# * `keyPath` The {String} name of the key to retrieve.
#
# Returns the value from Atom's default settings, the user's configuration
# file, or `null` if the key doesn't exist in either.
get: (keyPath) ->
value = _.valueForKeyPath(@settings, keyPath)
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
if value?
value = _.deepClone(value)
valueIsObject = _.isObject(value) and not _.isArray(value)
defaultValueIsObject = _.isObject(defaultValue) and not _.isArray(defaultValue)
if valueIsObject and defaultValueIsObject
_.defaults(value, defaultValue)
else
value = _.deepClone(defaultValue)
value
# Extended: Retrieves the setting for the given key as an integer.
#
# * `keyPath` The {String} name of the key to retrieve
#
# Returns the value from Atom's default settings, the user's configuration
# file, or `NaN` if the key doesn't exist in either.
getInt: (keyPath) ->
parseInt(@get(keyPath))
# Extended: Retrieves the setting for the given key as a positive integer.
#
# * `keyPath` The {String} name of the key to retrieve
# * `defaultValue` The integer {Number} to fall back to if the value isn't
# positive, defaults to 0.
#
# Returns the value from Atom's default settings, the user's configuration
# file, or `defaultValue` if the key value isn't greater than zero.
getPositiveInt: (keyPath, defaultValue=0) ->
Math.max(@getInt(keyPath), 0) or defaultValue
# Essential: Sets the value for a configuration setting.
#
# This value is stored in Atom's internal configuration file.
#
# * `keyPath` The {String} name of the key.
# * `value` The value of the setting.
#
# Returns the `value`.
set: (keyPath, value) ->
if @get(keyPath) isnt value
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
value = undefined if _.isEqual(defaultValue, value)
_.setValueForKeyPath(@settings, keyPath, value)
@update()
value
# Extended: Toggle the value at the key path.
#
# The new value will be `true` if the value is currently falsy and will be
# `false` if the value is currently truthy.
#
# * `keyPath` The {String} name of the key.
#
# Returns the new value.
toggle: (keyPath) ->
@set(keyPath, !@get(keyPath))
# Extended: Restore the key path to its default value.
#
# * `keyPath` The {String} name of the key.
#
# Returns the new value.
restoreDefault: (keyPath) ->
@set(keyPath, _.valueForKeyPath(@defaultSettings, keyPath))
# Extended: Get the default value of the key path.
#
# * `keyPath` The {String} name of the key.
#
# Returns the default value.
getDefault: (keyPath) ->
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
_.deepClone(defaultValue)
# Extended: Is the key path value its default value?
#
# * `keyPath` The {String} name of the key.
#
# Returns a {Boolean}, `true` if the current value is the default, `false`
# otherwise.
isDefault: (keyPath) ->
not _.valueForKeyPath(@settings, keyPath)?
# Extended: Push the value to the array at the key path.
#
# * `keyPath` The {String} key path.
# * `value` The value to push to the array.
#
# Returns the new array length {Number} of the setting.
pushAtKeyPath: (keyPath, value) ->
arrayValue = @get(keyPath) ? []
result = arrayValue.push(value)
@set(keyPath, arrayValue)
result
# Extended: Add the value to the beginning of the array at the key path.
#
# * `keyPath` The {String} key path.
# * `value` The value to shift onto the array.
#
# Returns the new array length {Number} of the setting.
unshiftAtKeyPath: (keyPath, value) ->
arrayValue = @get(keyPath) ? []
result = arrayValue.unshift(value)
@set(keyPath, arrayValue)
result
# Public: Remove the value from the array at the key path.
#
# * `keyPath` The {String} key path.
# * `value` The value to remove from the array.
#
# Returns the new array value of the setting.
removeAtKeyPath: (keyPath, value) ->
arrayValue = @get(keyPath) ? []
result = _.remove(arrayValue, value)
@set(keyPath, arrayValue)
result
# Essential: Add a listener for changes to a given key path.
#
# * `keyPath` The {String} name of the key to observe
# * `options` An optional {Object} containing the `callNow` key.
# * `callback` The {Function} to call when the value of the key changes.
# The first argument will be the new value of the key and the
#   second argument will be an {Object} with a `previous` property
# that is the prior value of the key.
#
# Returns an {Object} with the following keys:
# * `off` A {Function} that unobserves the `keyPath` when called.
observe: (keyPath, options={}, callback) ->
if _.isFunction(options)
callback = options
options = {}
value = @get(keyPath)
previousValue = _.clone(value)
updateCallback = =>
value = @get(keyPath)
unless _.isEqual(value, previousValue)
previous = previousValue
previousValue = _.clone(value)
callback(value, {previous})
eventName = "updated.#{keyPath.replace(/\./, '-')}"
subscription = @on eventName, updateCallback
callback(value) if options.callNow ? true
subscription
# Unobserve all callbacks on a given key.
#
# * `keyPath` The {String} name of the key to unobserve.
unobserve: (keyPath) ->
@off("updated.#{keyPath.replace(/\./, '-')}")
update: ->
return if @configFileHasErrors
@save()
@emit 'updated'
save: ->
CSON.writeFileSync(@configFilePath, @settings)
setRawValue: (keyPath, value) ->
defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
value = undefined if _.isEqual(defaultValue, value)
oldValue = _.clone(@get(keyPath))
_.setValueForKeyPath(@settings, keyPath, value)
newValue = @get(keyPath)
@emitter.emit 'did-change', {oldValue, newValue, keyPath} unless _.isEqual(newValue, oldValue)
setRawDefault: (keyPath, value) ->
oldValue = _.clone(@get(keyPath))
_.setValueForKeyPath(@defaultSettings, keyPath, value)
newValue = @get(keyPath)
@emitter.emit 'did-change', {oldValue, newValue, keyPath} unless _.isEqual(newValue, oldValue)
setRecursive: (keyPath, value) ->
if isPlainObject(value)
keys = if keyPath? then keyPath.split('.') else []
for key, childValue of value
continue unless value.hasOwnProperty(key)
@setRecursive(keys.concat([key]).join('.'), childValue)
else
try
value = @makeValueConformToSchema(keyPath, value)
@setRawValue(keyPath, value)
catch e
console.warn("'#{keyPath}' could not be set. Attempted value: #{JSON.stringify(value)}; Schema: #{JSON.stringify(@getSchema(keyPath))}")
setAll: (newSettings) ->
unless isPlainObject(newSettings)
@settings = {}
@emitter.emit 'did-change'
return
unsetUnspecifiedValues = (keyPath, value) =>
if isPlainObject(value)
keys = if keyPath? then keyPath.split('.') else []
for key, childValue of value
continue unless value.hasOwnProperty(key)
unsetUnspecifiedValues(keys.concat([key]).join('.'), childValue)
else
@setRawValue(keyPath, undefined) unless _.valueForKeyPath(newSettings, keyPath)?
return
@setRecursive(null, newSettings)
unsetUnspecifiedValues(null, @settings)
setDefaults: (keyPath, defaults) ->
if defaults? and isPlainObject(defaults)
keys = if keyPath? then keyPath.split('.') else []
for key, childValue of defaults
continue unless defaults.hasOwnProperty(key)
@setDefaults(keys.concat([key]).join('.'), childValue)
else
try
defaults = @makeValueConformToSchema(keyPath, defaults)
@setRawDefault(keyPath, defaults)
catch e
console.warn("'#{keyPath}' could not set the default. Attempted default: #{JSON.stringify(defaults)}; Schema: #{JSON.stringify(@getSchema(keyPath))}")
setSchema: (keyPath, schema) ->
unless isPlainObject(schema)
throw new Error("Error loading schema for #{keyPath}: schemas can only be objects!")
unless typeof schema.type?
throw new Error("Error loading schema for #{keyPath}: schema objects must have a type attribute")
rootSchema = @schema
if keyPath
for key in keyPath.split('.')
rootSchema.type = 'object'
rootSchema.properties ?= {}
properties = rootSchema.properties
properties[key] ?= {}
rootSchema = properties[key]
_.extend rootSchema, schema
@setDefaults(keyPath, @extractDefaultsFromSchema(schema))
extractDefaultsFromSchema: (schema) ->
if schema.default?
schema.default
else if schema.type is 'object' and schema.properties? and isPlainObject(schema.properties)
defaults = {}
properties = schema.properties or {}
defaults[key] = @extractDefaultsFromSchema(value) for key, value of properties
defaults
makeValueConformToSchema: (keyPath, value) ->
value = @constructor.executeSchemaEnforcers(keyPath, value, schema) if schema = @getSchema(keyPath)
value
# Base schema enforcers. These will coerce raw input into the specified type,
# and will throw an error when the value cannot be coerced. Throwing the error
# will indicate that the value should not be set.
#
# Enforcers are run from most specific to least. For a schema with type
# `integer`, all the enforcers for the `integer` type will be run first, in
# order of specification. Then the `*` enforcers will be run, in order of
# specification.
Config.addSchemaEnforcers
'integer':
coerce: (keyPath, value, schema) ->
value = parseInt(value)
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into an int") if isNaN(value) or not isFinite(value)
value
'number':
coerce: (keyPath, value, schema) ->
value = parseFloat(value)
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into a number") if isNaN(value) or not isFinite(value)
value
'boolean':
coerce: (keyPath, value, schema) ->
switch typeof value
when 'string'
if value.toLowerCase() is 'true'
true
else if value.toLowerCase() is 'false'
false
else
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'")
when 'boolean'
value
else
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'")
'string':
validate: (keyPath, value, schema) ->
unless typeof value is 'string'
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a string")
value
'null':
# null sort of isnt supported. It will just unset in this case
coerce: (keyPath, value, schema) ->
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be null") unless value == null
value
'object':
coerce: (keyPath, value, schema) ->
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an object") unless isPlainObject(value)
return value unless schema.properties?
newValue = {}
for prop, childSchema of schema.properties
continue unless value.hasOwnProperty(prop)
try
newValue[prop] = @executeSchemaEnforcers("#{keyPath}.#{prop}", value[prop], childSchema)
catch error
console.warn "Error setting item in object: #{error.message}"
newValue
'array':
coerce: (keyPath, value, schema) ->
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an array") unless Array.isArray(value)
itemSchema = schema.items
if itemSchema?
newValue = []
for item in value
try
newValue.push @executeSchemaEnforcers(keyPath, item, itemSchema)
catch error
console.warn "Error setting item in array: #{error.message}"
newValue
else
value
'*':
coerceMinimumAndMaximum: (keyPath, value, schema) ->
return value unless typeof value is 'number'
if schema.minimum? and typeof schema.minimum is 'number'
value = Math.max(value, schema.minimum)
if schema.maximum? and typeof schema.maximum is 'number'
value = Math.min(value, schema.maximum)
value
validateEnum: (keyPath, value, schema) ->
possibleValues = schema.enum
return value unless possibleValues? and Array.isArray(possibleValues) and possibleValues.length
for possibleValue in possibleValues
# Using `isEqual` for possibility of placing enums on array and object schemas
return value if _.isEqual(possibleValue, value)
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} is not one of #{JSON.stringify(possibleValues)}")
isPlainObject = (value) ->
_.isObject(value) and not _.isArray(value) and not _.isFunction(value) and not _.isString(value)

View File

@ -4,6 +4,12 @@ remote = require 'remote'
path = require 'path'
CSON = require 'season'
fs = require 'fs-plus'
{specificity} = require 'clear-cut'
{Disposable} = require 'event-kit'
Grim = require 'grim'
MenuHelpers = require './menu-helpers'
SpecificityCache = {}
# Extended: Provides a registry for commands that you'd like to appear in the
# context menu.
@ -13,16 +19,8 @@ fs = require 'fs-plus'
module.exports =
class ContextMenuManager
constructor: ({@resourcePath, @devMode}) ->
@definitions = {}
@devModeDefinitions = {}
@activeElement = null
@devModeDefinitions['.workspace'] = [
label: 'Inspect Element'
command: 'application:inspect'
executeAtBuild: (e) ->
@commandOptions = x: e.pageX, y: e.pageY
]
@definitions = {'.overlayer': []} # TODO: Remove once color picker package stops touching private data
@clear()
atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems()
@ -30,102 +28,143 @@ class ContextMenuManager
menusDirPath = path.join(@resourcePath, 'menus')
platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json'])
map = CSON.readFileSync(platformMenuPath)
atom.contextMenu.add(platformMenuPath, map['context-menu'])
atom.contextMenu.add(map['context-menu'])
# Public: Creates menu definitions from the object specified by the menu
# JSON API.
# Public: Add context menu items scoped by CSS selectors.
#
# * `name` The path of the file that contains the menu definitions.
# * `object` The 'context-menu' object specified in the menu JSON API.
# * `options` An optional {Object} with the following keys:
# * `devMode` Determines whether the entries should only be shown when
# the window is in dev mode.
add: (name, object, {devMode}={}) ->
for selector, items of object
for label, commandOrSubmenu of items
# ## Examples
#
# To add a context menu, pass a selector matching the elements to which you
# want the menu to apply as the top level key, followed by a menu descriptor.
# The invocation below adds a global 'Help' context menu item and a 'History'
# submenu on the editor supporting undo/redo. This is just for example
# purposes and not the way the menu is actually configured in Atom by default.
#
# ```coffee
# atom.contextMenu.add {
# '.workspace': [{label: 'Help', command: 'application:open-documentation'}]
# '.editor': [{
# label: 'History',
# submenu: [
# {label: 'Undo': command:'core:undo'}
# {label: 'Redo': command:'core:redo'}
# ]
# }]
# }
# ```
#
# ## Arguments
#
# * `itemsBySelector` An {Object} whose keys are CSS selectors and whose
# values are {Array}s of item {Object}s containing the following keys:
# * `label` (Optional) A {String} containing the menu item's label.
# * `command` (Optional) A {String} containing the command to invoke on the
# target of the right click that invoked the context menu.
# * `submenu` (Optional) An {Array} of additional items.
# * `type` (Optional) If you want to create a separator, provide an item
# with `type: 'separator'` and no other keys.
# * `created` (Optional) A {Function} that is called on the item each time a
# context menu is created via a right click. You can assign properties to
# `this` to dynamically compute the command, label, etc. This method is
# actually called on a clone of the original item template to prevent state
# from leaking across context menu deployments. Called with the following
# argument:
# * `event` The click event that deployed the context menu.
# * `shouldDisplay` (Optional) A {Function} that is called to determine
# whether to display this item on a given context menu deployment. Called
# with the following argument:
# * `event` The click event that deployed the context menu.
add: (itemsBySelector) ->
# Detect deprecated file path as first argument
unless typeof itemsBySelector is 'object'
Grim.deprecate("ContextMenuManage::add has changed to take a single object as its argument. Please consult the documentation.")
itemsBySelector = arguments[1]
devMode = arguments[2]?.devMode
# Detect deprecated format for items object
for key, value of itemsBySelector
unless _.isArray(value)
Grim.deprecate("The format for declaring context menu items has changed. Please consult the documentation.")
itemsBySelector = @convertLegacyItems(itemsBySelector, devMode)
addedItemSets = []
for selector, items of itemsBySelector
itemSet = new ContextMenuItemSet(selector, items)
addedItemSets.push(itemSet)
@itemSets.push(itemSet)
new Disposable =>
for itemSet in addedItemSets
@itemSets.splice(@itemSets.indexOf(itemSet), 1)
templateForElement: (target) ->
@templateForEvent({target})
templateForEvent: (event) ->
template = []
currentTarget = event.target
while currentTarget?
currentTargetItems = []
matchingItemSets =
@itemSets.filter (itemSet) -> currentTarget.webkitMatchesSelector(itemSet.selector)
for itemSet in matchingItemSets
for item in itemSet.items
continue if item.devMode and not @devMode
item = Object.create(item)
if typeof item.shouldDisplay is 'function'
continue unless item.shouldDisplay(event)
item.created?(event)
MenuHelpers.merge(currentTargetItems, MenuHelpers.cloneMenuItem(item), itemSet.specificity)
for item in currentTargetItems
MenuHelpers.merge(template, item, false)
currentTarget = currentTarget.parentElement
template
convertLegacyItems: (legacyItems, devMode) ->
itemsBySelector = {}
for selector, commandsByLabel of legacyItems
itemsBySelector[selector] = items = []
for label, commandOrSubmenu of commandsByLabel
if typeof commandOrSubmenu is 'object'
submenu = []
for submenuLabel, command of commandOrSubmenu
submenu.push(@buildMenuItem(submenuLabel, command))
@addBySelector(selector, {label: label, submenu: submenu}, {devMode})
items.push({label, submenu: @convertLegacyItems(commandOrSubmenu, devMode), devMode})
else if commandOrSubmenu is '-'
items.push({type: 'separator'})
else
menuItem = @buildMenuItem(label, commandOrSubmenu)
@addBySelector(selector, menuItem, {devMode})
items.push({label, command: commandOrSubmenu, devMode})
undefined
buildMenuItem: (label, command) ->
if command is '-'
{type: 'separator'}
else
{label, command}
# Registers a command to be displayed when the relevant item is right
# clicked.
#
# * `selector` The css selector for the active element which should include
# the given command in its context menu.
# * `definition` The object containing keys which match the menu template API.
# * `options` An optional {Object} with the following keys:
# * `devMode` Indicates whether this command should only appear while the
# editor is in dev mode.
addBySelector: (selector, definition, {devMode}={}) ->
definitions = if devMode then @devModeDefinitions else @definitions
if not _.findWhere(definitions[selector], definition) or _.isEqual(definition, {type: 'separator'})
(definitions[selector] ?= []).push(definition)
# Returns definitions which match the element and devMode.
definitionsForElement: (element, {devMode}={}) ->
definitions = if devMode then @devModeDefinitions else @definitions
matchedDefinitions = []
for selector, items of definitions when element.webkitMatchesSelector(selector)
matchedDefinitions.push(_.clone(item)) for item in items
matchedDefinitions
# Used to generate the context menu for a specific element and it's
# parents.
#
# The menu items are sorted such that menu items that match closest to the
# active element are listed first. The further down the list you go, the higher
# up the ancestor hierarchy they match.
#
# * `element` The DOM element to generate the menu template for.
menuTemplateForMostSpecificElement: (element, {devMode}={}) ->
menuTemplate = @definitionsForElement(element, {devMode})
if element.parentElement
menuTemplate.concat(@menuTemplateForMostSpecificElement(element.parentElement, {devMode}))
else
menuTemplate
# Returns a menu template for both normal entries as well as
# development mode entries.
combinedMenuTemplateForElement: (element) ->
normalItems = @menuTemplateForMostSpecificElement(element)
devItems = if @devMode then @menuTemplateForMostSpecificElement(element, devMode: true) else []
menuTemplate = normalItems
menuTemplate.push({ type: 'separator' }) if normalItems.length > 0 and devItems.length > 0
menuTemplate.concat(devItems)
# Executes `executeAtBuild` if defined for each menu item with
# the provided event and then removes the `executeAtBuild` property from
# the menu item.
#
# This is useful for commands that need to provide data about the event
# to the command.
executeBuildHandlers: (event, menuTemplate) ->
for template in menuTemplate
template?.executeAtBuild?.call(template, event)
delete template.executeAtBuild
itemsBySelector
# Public: Request a context menu to be displayed.
#
# * `event` A DOM event.
showForEvent: (event) ->
@activeElement = event.target
menuTemplate = @combinedMenuTemplateForElement(event.target)
menuTemplate = @templateForEvent(event)
return unless menuTemplate?.length > 0
@executeBuildHandlers(event, menuTemplate)
remote.getCurrentWindow().emit('context-menu', menuTemplate)
undefined
return
clear: ->
@activeElement = null
@itemSets = []
@add '.workspace': [{
label: 'Inspect Element'
command: 'application:inspect'
devMode: true
created: (event) ->
{pageX, pageY} = event
@commandDetail = {x: pageX, y: pageY}
}]
class ContextMenuItemSet
constructor: (@selector, @items) ->
@specificity = (SpecificityCache[@selector] ?= specificity(@selector))

View File

@ -1,3 +1,6 @@
{Disposable} = require 'event-kit'
Grim = require 'grim'
# Extended: Manages the deserializers used for serialized state
#
# An instance of this class is always available as the `atom.deserializers`
@ -24,14 +27,17 @@ class DeserializerManager
# Public: Register the given class(es) as deserializers.
#
# * `classes` One or more classes to register.
add: (classes...) ->
@deserializers[klass.name] = klass for klass in classes
# * `deserializers` One or more deserializers to register. A deserializer can
# be any object with a `.name` property and a `.deserialize()` method. A
# common approach is to register a *constructor* as the deserializer for its
# instances by adding a `.deserialize()` class method.
add: (deserializers...) ->
@deserializers[deserializer.name] = deserializer for deserializer in deserializers
new Disposable =>
delete @deserializers[deserializer.name] for deserializer in deserializers
# Public: Remove the given class(es) as deserializers.
#
# * `classes` One or more classes to remove.
remove: (classes...) ->
Grim.deprecate("Call .dispose() on the Disposable return from ::add instead")
delete @deserializers[name] for {name} in classes
# Public: Deserialize the state and params.

View File

@ -59,10 +59,10 @@ class DisplayBuffer extends Model
@subscribe @buffer.onDidUpdateMarkers @handleBufferMarkersUpdated
@subscribe @buffer.onDidCreateMarker @handleBufferMarkerCreated
@subscribe atom.config.observe 'editor.preferredLineLength', callNow: false, =>
@subscribe atom.config.onDidChange 'editor.preferredLineLength', =>
@updateWrappedScreenLines() if @isSoftWrapped() and atom.config.get('editor.softWrapAtPreferredLineLength')
@subscribe atom.config.observe 'editor.softWrapAtPreferredLineLength', callNow: false, =>
@subscribe atom.config.onDidChange 'editor.softWrapAtPreferredLineLength', =>
@updateWrappedScreenLines() if @isSoftWrapped()
@updateAllScreenLines()
@ -439,7 +439,7 @@ class DisplayBuffer extends Model
getSoftWrapColumn: ->
if atom.config.get('editor.softWrapAtPreferredLineLength')
Math.min(@getEditorWidthInChars(), atom.config.getPositiveInt('editor.preferredLineLength', @getEditorWidthInChars()))
Math.min(@getEditorWidthInChars(), atom.config.get('editor.preferredLineLength'))
else
@getEditorWidthInChars()

52
src/menu-helpers.coffee Normal file
View File

@ -0,0 +1,52 @@
_ = require 'underscore-plus'
ItemSpecificities = new WeakMap
merge = (menu, item, itemSpecificity=Infinity) ->
ItemSpecificities.set(item, itemSpecificity) if itemSpecificity
matchingItemIndex = findMatchingItemIndex(menu, item)
matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1
if matchingItem?
if item.submenu?
merge(matchingItem.submenu, submenuItem, itemSpecificity) for submenuItem in item.submenu
else if itemSpecificity
unless itemSpecificity < ItemSpecificities.get(matchingItem)
menu[matchingItemIndex] = item
else unless item.type is 'separator' and _.last(menu)?.type is 'separator'
menu.push(item)
unmerge = (menu, item) ->
matchingItemIndex = findMatchingItemIndex(menu, item)
matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1
if matchingItem?
if item.submenu?
unmerge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu
unless matchingItem.submenu?.length > 0
menu.splice(matchingItemIndex, 1)
findMatchingItemIndex = (menu, {type, label, submenu}) ->
return -1 if type is 'separator'
for item, index in menu
if normalizeLabel(item.label) is normalizeLabel(label) and item.submenu? is submenu?
return index
-1
normalizeLabel = (label) ->
return undefined unless label?
if process.platform is 'darwin'
label
else
label.replace(/\&/g, '')
cloneMenuItem = (item) ->
item = _.pick(item, 'type', 'label', 'command', 'submenu', 'commandDetail')
if item.submenu?
item.submenu = item.submenu.map (submenuItem) -> cloneMenuItem(submenuItem)
item
module.exports = {merge, unmerge, normalizeLabel, cloneMenuItem}

View File

@ -4,6 +4,9 @@ _ = require 'underscore-plus'
ipc = require 'ipc'
CSON = require 'season'
fs = require 'fs-plus'
{Disposable} = require 'event-kit'
MenuHelpers = require './menu-helpers'
# Extended: Provides a registry for menu items that you'd like to appear in the
# application menu.
@ -34,10 +37,18 @@ class MenuManager
# * `submenu` An optional {Array} of sub menu items.
# * `command` An optional {String} command to trigger when the item is
# clicked.
#
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# added menu items.
add: (items) ->
items = _.deepClone(items)
@merge(@template, item) for item in items
@update()
undefined
new Disposable => @remove(items)
remove: (items) ->
@unmerge(@template, item) for item in items
@update()
# Should the binding for the given selector be included in the menu
# commands.
@ -95,12 +106,10 @@ class MenuManager
# Merges an item in a submenu aware way such that new items are always
# appended to the bottom of existing menus where possible.
merge: (menu, item) ->
item = _.deepClone(item)
MenuHelpers.merge(menu, item)
if item.submenu? and match = _.find(menu, ({label, submenu}) => submenu? and label and @normalizeLabel(label) is @normalizeLabel(item.label))
@merge(match.submenu, i) for i in item.submenu
else
menu.push(item) unless _.find(menu, ({label}) => label and @normalizeLabel(label) is @normalizeLabel(item.label))
unmerge: (menu, item) ->
MenuHelpers.unmerge(menu, item)
# OSX can't handle displaying accelerators for multiple keystrokes.
# If they are sent across, it will stop processing accelerators for the rest
@ -119,25 +128,17 @@ class MenuManager
keystrokesByCommand = @filterMultipleKeystroke(keystrokesByCommand)
ipc.send 'update-application-menu', template, keystrokesByCommand
normalizeLabel: (label) ->
return undefined unless label?
if process.platform is 'darwin'
label
else
label.replace(/\&/g, '')
# Get an {Array} of {String} classes for the given element.
classesForElement: (element) ->
element?.classList.toString().split(' ') ? []
sortPackagesMenu: ->
packagesMenu = @template.find ({label}) => @normalizeLabel(label) is 'Packages'
packagesMenu = @template.find ({label}) => MenuHelpers.normalizeLabel(label) is 'Packages'
return unless packagesMenu?.submenu?
packagesMenu.submenu.sort (item1, item2) =>
if item1.label and item2.label
@normalizeLabel(item1.label).localeCompare(@normalizeLabel(item2.label))
MenuHelpers.normalizeLabel(item1.label).localeCompare(MenuHelpers.normalizeLabel(item2.label))
else
0
@update()

View File

@ -260,9 +260,9 @@ class PackageManager
@disabledPackagesSubscription = null
observeDisabledPackages: ->
@disabledPackagesSubscription ?= atom.config.observe 'core.disabledPackages', callNow: false, (disabledPackages, {previous}) =>
packagesToEnable = _.difference(previous, disabledPackages)
packagesToDisable = _.difference(disabledPackages, previous)
@disabledPackagesSubscription ?= atom.config.onDidChange 'core.disabledPackages', ({newValue, oldValue}) =>
packagesToEnable = _.difference(oldValue, newValue)
packagesToDisable = _.difference(newValue, oldValue)
@deactivatePackage(packageName) for packageName in packagesToDisable when @getActivePackage(packageName)
@activatePackage(packageName) for packageName in packagesToEnable

View File

@ -143,7 +143,13 @@ class Package
@requireMainModule()
if @mainModule?
atom.config.setDefaults(@name, @mainModule.configDefaults)
if @mainModule.config? and typeof @mainModule.config is 'object'
atom.config.setSchema @name, {type: 'object', properties: @mainModule.config}
else if @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object'
deprecate """Use a config schema instead. See the configuration section
of https://atom.io/docs/latest/creating-a-package and
https://atom.io/docs/api/latest/Config for more details"""
atom.config.setDefaults(@name, @mainModule.configDefaults)
@mainModule.activateConfig?()
@configActivated = true
@ -157,7 +163,7 @@ class Package
activateResources: ->
atom.keymaps.add(keymapPath, map) for [keymapPath, map] in @keymaps
atom.contextMenu.add(menuPath, map['context-menu']) for [menuPath, map] in @menus
atom.contextMenu.add(map['context-menu']) for [menuPath, map] in @menus
atom.menu.add(map.menu) for [menuPath, map] in @menus when map.menu
unless @grammarsActivated

View File

@ -6,10 +6,12 @@ fs = require 'fs-plus'
Q = require 'q'
{deprecate} = require 'grim'
{Model} = require 'theorist'
{Emitter, Subscriber} = require 'emissary'
{Subscriber} = require 'emissary'
{Emitter} = require 'event-kit'
Serializable = require 'serializable'
TextBuffer = require 'text-buffer'
{Directory} = require 'pathwatcher'
Grim = require 'grim'
TextEditor = require './text-editor'
Task = require './task'
@ -33,14 +35,17 @@ class Project extends Model
Section: Construction and Destruction
###
constructor: ({path, @buffers}={}) ->
constructor: ({path, paths, @buffers}={}) ->
@emitter = new Emitter
@buffers ?= []
for buffer in @buffers
do (buffer) =>
buffer.onDidDestroy => @removeBuffer(buffer)
@setPath(path)
Grim.deprecate("Pass 'paths' array instead of 'path' to project constructor") if path?
paths ?= _.compact([path])
@setPaths(paths)
destroyed: ->
buffer.destroy() for buffer in @getBuffers()
@ -66,25 +71,47 @@ class Project extends Model
params.buffers = params.buffers.map (bufferState) -> atom.deserializers.deserialize(bufferState)
params
###
Section: Event Subscription
###
onDidChangePaths: (callback) ->
@emitter.on 'did-change-paths', callback
on: (eventName) ->
if eventName is 'path-changed'
Grim.deprecate("Use Project::onDidChangePaths instead")
super
###
Section: Accessing the git repository
###
# Public: Returns the {GitRepository} if available.
getRepo: -> @repo
# Public: Get an {Array} of {GitRepository}s associated with the project's
# directories.
getRepositories: -> _.compact([@repo])
getRepo: ->
Grim.deprecate("Use ::getRepositories instead")
@repo
###
Section: Managing Paths
###
# Public: Returns the project's {String} fullpath.
# Public: Get an {Array} of {String}s containing the paths of the project's
# directories.
getPaths: -> _.compact([@rootDirectory?.path])
getPath: ->
Grim.deprecate("Use ::getPaths instead")
@rootDirectory?.path
# Public: Sets the project's fullpath.
# Public: Set the paths of the project's directories.
#
# * `projectPath` {String} path
setPath: (projectPath) ->
# * `projectPaths` {Array} of {String} paths.
setPaths: (projectPaths) ->
[projectPath] = projectPaths
projectPath = path.normalize(projectPath) if projectPath
@path = projectPath
@rootDirectory?.off()
@ -100,9 +127,16 @@ class Project extends Model
@rootDirectory = null
@emit "path-changed"
@emitter.emit 'did-change-paths', projectPaths
setPath: (path) ->
Grim.deprecate("Use ::setPaths instead")
@setPaths([path])
# Public: Returns the root {Directory} object for this project.
# Public: Get an {Array} of {Directory}s associated with this project.
getDirectories: ->
[@rootDirectory]
getRootDirectory: ->
Grim.deprecate("Use ::getDirectories instead")
@rootDirectory
# Public: Given a uri, this resolves it relative to the project directory. If
@ -120,7 +154,7 @@ class Project extends Model
else
if fs.isAbsolute(uri)
path.normalize(fs.absolute(uri))
else if projectPath = @getPath()
else if projectPath = @getPaths()[0]
path.normalize(fs.absolute(path.join(projectPath, uri)))
else
undefined
@ -345,12 +379,12 @@ class Project extends Model
# Deprecated: delegate
registerOpener: (opener) ->
deprecate("Use Workspace::registerOpener instead")
deprecate("Use Workspace::addOpener instead")
atom.workspace.registerOpener(opener)
# Deprecated: delegate
unregisterOpener: (opener) ->
deprecate("Use Workspace::unregisterOpener instead")
deprecate("Call .dispose() on the Disposable returned from ::addOpener instead")
atom.workspace.unregisterOpener(opener)
# Deprecated: delegate

View File

@ -500,22 +500,22 @@ TextEditorComponent = React.createClass
'editor:fold-at-indent-level-9': -> editor.foldAllAtIndentLevel(8)
'editor:toggle-line-comments': -> editor.toggleLineCommentsInSelection()
'editor:log-cursor-scope': -> editor.logCursorScope()
'editor:checkout-head-revision': -> atom.project.getRepo()?.checkoutHeadForEditor(editor)
'editor:checkout-head-revision': -> atom.project.getRepositories()[0]()?.checkoutHeadForEditor(editor)
'editor:copy-path': -> editor.copyPathToClipboard()
'editor:move-line-up': -> editor.moveLineUp()
'editor:move-line-down': -> editor.moveLineDown()
'editor:duplicate-lines': -> editor.duplicateLines()
'editor:join-lines': -> editor.joinLines()
'editor:toggle-indent-guide': -> atom.config.toggle('editor.showIndentGuide')
'editor:toggle-line-numbers': -> atom.config.toggle('editor.showLineNumbers')
'editor:toggle-indent-guide': -> atom.config.set('editor.showIndentGuide', not atom.config.get('editor.showIndentGuide'))
'editor:toggle-line-numbers': -> atom.config.set('editor.showLineNumbers', not atom.config.get('editor.showLineNumbers'))
'editor:scroll-to-cursor': -> editor.scrollToCursorPosition()
'benchmark:scroll': @runScrollBenchmark
addCommandListeners: (listenersByCommandName) ->
{parentView} = @props
addListener = (command, listener) ->
parentView.command command, (event) ->
addListener = (command, listener) =>
@subscribe parentView.command command, (event) ->
event.stopPropagation()
listener(event)

View File

@ -37,31 +37,6 @@ TextEditorComponent = require './text-editor-component'
# ```
module.exports =
class TextEditorView extends View
@configDefaults:
fontFamily: ''
fontSize: 16
lineHeight: 1.3
showInvisibles: false
showIndentGuide: false
showLineNumbers: true
autoIndent: true
normalizeIndentOnPaste: true
nonWordCharacters: "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
preferredLineLength: 80
tabLength: 2
softWrap: false
softTabs: true
softWrapAtPreferredLineLength: false
scrollSensitivity: 40
useHardwareAcceleration: true
confirmCheckoutHeadRevision: true
invisibles:
eol: '\u00ac'
space: '\u00b7'
tab: '\u00bb'
cr: '\u00a4'
scrollPastEnd: false
@content: (params) ->
attributes = params.attributes ? {}
attributes.class = 'editor react editor-colors'

View File

@ -113,8 +113,8 @@ class TextEditor extends Model
@emit 'scroll-left-changed', scrollLeft
@emitter.emit 'did-change-scroll-left', scrollLeft
@subscribe atom.config.observe 'editor.showInvisibles', callNow: false, (show) => @updateInvisibles()
@subscribe atom.config.observe 'editor.invisibles', callNow: false, => @updateInvisibles()
@subscribe atom.config.onDidChange 'editor.showInvisibles', => @updateInvisibles()
@subscribe atom.config.onDidChange 'editor.invisibles', => @updateInvisibles()
atom.workspace?.editorAdded(this) if registerEditor
@ -133,8 +133,8 @@ class TextEditor extends Model
subscribeToBuffer: ->
@buffer.retain()
@subscribe @buffer.onDidChangePath =>
unless atom.project.getPath()?
atom.project.setPath(path.dirname(@getPath()))
unless atom.project.getPaths()[0]?
atom.project.setPaths([path.dirname(@getPath())])
@emit "title-changed"
@emitter.emit 'did-change-title', @getTitle()
@emit "path-changed"

View File

@ -2,7 +2,7 @@ path = require 'path'
_ = require 'underscore-plus'
EmitterMixin = require('emissary').Emitter
{Emitter} = require 'event-kit'
{Emitter, Disposable} = require 'event-kit'
{File} = require 'pathwatcher'
fs = require 'fs-plus'
Q = require 'q'
@ -182,16 +182,16 @@ class ThemeManager
# * `stylesheetPath` A {String} path to the stylesheet that can be an absolute
# path or a relative path that will be resolved against the load path.
#
# Returns the absolute path to the required stylesheet.
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# required stylesheet.
requireStylesheet: (stylesheetPath, type='bundled') ->
if fullPath = @resolveStylesheet(stylesheetPath)
content = @loadStylesheet(fullPath)
@applyStylesheet(fullPath, content, type)
new Disposable => @removeStylesheet(fullPath)
else
throw new Error("Could not find a file at path '#{stylesheetPath}'")
fullPath
unwatchUserStylesheet: ->
@userStylesheetFile?.off()
@userStylesheetFile = null

View File

@ -25,7 +25,7 @@ class TokenizedBuffer extends Model
constructor: ({@buffer, @tabLength, @invisibles}) ->
@emitter = new Emitter
@tabLength ?= atom.config.getPositiveInt('editor.tabLength', 2)
@tabLength ?= atom.config.get('editor.tabLength')
@subscribe atom.syntax.onDidAddGrammar(@grammarAddedOrUpdated)
@subscribe atom.syntax.onDidUpdateGrammar(@grammarAddedOrUpdated)
@ -35,8 +35,7 @@ class TokenizedBuffer extends Model
@subscribe @$tabLength.changes, (tabLength) => @retokenizeLines()
@subscribe atom.config.observe 'editor.tabLength', callNow: false, =>
@setTabLength(atom.config.getPositiveInt('editor.tabLength', 2))
@subscribe atom.config.onDidChange 'editor.tabLength', ({newValue}) => @setTabLength(newValue)
@reloadGrammar()

View File

@ -7,6 +7,7 @@ Delegator = require 'delegato'
scrollbarStyle = require 'scrollbar-style'
{$, $$, View} = require './space-pen-extensions'
fs = require 'fs-plus'
Workspace = require './workspace'
PaneView = require './pane-view'
PaneContainerView = require './pane-container-view'
TextEditor = require './text-editor'
@ -56,15 +57,6 @@ class WorkspaceView extends View
'saveActivePaneItem', 'saveActivePaneItemAs', 'saveAll', 'destroyActivePaneItem',
'destroyActivePane', 'increaseFontSize', 'decreaseFontSize', toProperty: 'model'
@configDefaults:
ignoredNames: [".git", ".hg", ".svn", ".DS_Store", "Thumbs.db"]
excludeVcsIgnoredPaths: true
disabledPackages: []
themes: ['atom-dark-ui', 'atom-dark-syntax']
projectHome: path.join(fs.getHomeDirectory(), 'github')
audioBeep: true
destroyEmptyPanes: true
constructor: (@element) ->
unless @element?
return atom.workspace.getView(atom.workspace).__spacePenView

View File

@ -6,7 +6,7 @@ Q = require 'q'
Serializable = require 'serializable'
Delegator = require 'delegato'
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
CommandInstaller = require './command-installer'
Grim = require 'grim'
TextEditor = require './text-editor'
PaneContainer = require './pane-container'
Pane = require './pane'
@ -47,7 +47,7 @@ class Workspace extends Model
@subscribeToActiveItem()
@registerOpener (filePath) =>
@addOpener (filePath) =>
switch filePath
when 'atom://.atom/stylesheet'
@open(atom.themes.getUserStylesheetPath())
@ -107,7 +107,7 @@ class Workspace extends Model
subscribeToActiveItem: ->
@updateWindowTitle()
@updateDocumentEdited()
atom.project.on 'path-changed', @updateWindowTitle
atom.project.onDidChangePaths @updateWindowTitle
@observeActivePaneItem (item) =>
@updateWindowTitle()
@ -136,7 +136,7 @@ class Workspace extends Model
# Updates the application's title and proxy icon based on whichever file is
# open.
updateWindowTitle: =>
if projectPath = atom.project?.getPath()
if projectPath = atom.project?.getPaths()[0]
if item = @getActivePaneItem()
document.title = "#{item.getTitle?() ? 'untitled'} - #{projectPath}"
atom.setRepresentedFilename(item.getPath?() ? projectPath)
@ -414,8 +414,6 @@ class Workspace extends Model
if uri = @destroyedItemUris.pop()
@openSync(uri)
# TODO: make ::registerOpener() return a disposable
# Public: Register an opener for a uri.
#
# An {TextEditor} will be used if no openers return a value.
@ -423,17 +421,24 @@ class Workspace extends Model
# ## Examples
#
# ```coffee
# atom.project.registerOpener (uri) ->
# atom.project.addOpener (uri) ->
# if path.extname(uri) is '.toml'
# return new TomlEditor(uri)
# ```
#
# * `opener` A {Function} to be called when a path is being opened.
registerOpener: (opener) ->
#
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# opener.
addOpener: (opener) ->
@openers.push(opener)
new Disposable => _.remove(@openers, opener)
registerOpener: (opener) ->
Grim.deprecate("Call Workspace::addOpener instead")
@addOpener(opener)
# Unregister an opener registered with {::registerOpener}.
unregisterOpener: (opener) ->
Grim.deprecate("Call .dispose() on the Disposable returned from ::addOpener instead")
_.remove(@openers, opener)
getOpeners: ->