mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2025-01-07 23:59:22 +03:00
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:
commit
f004f8c45d
@ -6,6 +6,6 @@
|
||||
"url": "https://github.com/atom/atom.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"atom-package-manager": "0.97.0"
|
||||
"atom-package-manager": "0.98.0"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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']}, ->
|
||||
|
@ -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')
|
||||
|
@ -10,5 +10,8 @@
|
||||
},
|
||||
"no_interpolation_in_single_quotes": {
|
||||
"level": "error"
|
||||
},
|
||||
"no_debugger": {
|
||||
"level": "error"
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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/
|
||||
|
@ -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'}
|
||||
]
|
||||
|
@ -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'}
|
||||
]
|
||||
|
@ -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'}
|
||||
]
|
||||
|
22
package.json
22
package.json
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
|
@ -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 []
|
||||
|
@ -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()
|
||||
|
13
spec/fixtures/packages/package-with-config-schema/index.coffee
vendored
Normal file
13
spec/fixtures/packages/package-with-config-schema/index.coffee
vendored
Normal 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
|
@ -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'}
|
||||
]
|
||||
|
@ -3,5 +3,6 @@
|
||||
]
|
||||
|
||||
'context-menu':
|
||||
'.test-1':
|
||||
'Menu item 2': 'command-2'
|
||||
'.test-1': [
|
||||
{label: 'Menu item 2', command: 'command-2'}
|
||||
]
|
||||
|
@ -3,5 +3,6 @@
|
||||
]
|
||||
|
||||
'context-menu':
|
||||
'.test-1':
|
||||
'Menu item 3': 'command-3'
|
||||
'.test-1': [
|
||||
{label: 'Menu item 3', command: 'command-3'}
|
||||
]
|
||||
|
@ -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}
|
||||
|
48
spec/menu-manager-spec.coffee
Normal file
48
spec/menu-manager-spec.coffee
Normal 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"}]}
|
@ -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", ->
|
||||
|
@ -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", ->
|
||||
|
@ -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", ->
|
||||
|
@ -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", ->
|
||||
|
@ -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 ->
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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 ->
|
||||
|
@ -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", ->
|
||||
|
@ -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", ->
|
||||
|
@ -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", ->
|
||||
|
@ -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) ->
|
||||
|
@ -28,6 +28,7 @@ class AtomWindow
|
||||
title: 'Atom'
|
||||
icon: @constructor.iconPath
|
||||
'web-preferences':
|
||||
'direct-write': false
|
||||
'subpixel-font-scaling': false
|
||||
global.atomApplication.addWindow(this)
|
||||
|
||||
|
@ -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
115
src/config-schema.coffee
Normal 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'
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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.
|
||||
|
@ -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
52
src/menu-helpers.coffee
Normal 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}
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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: ->
|
||||
|
Loading…
Reference in New Issue
Block a user