diff --git a/spec/app/config-spec.coffee b/spec/app/config-spec.coffee index 63dfac4ae..b153cc960 100644 --- a/spec/app/config-spec.coffee +++ b/spec/app/config-spec.coffee @@ -1,19 +1,32 @@ describe "Config", -> - describe ".get(keyPath) and .set(keyPath, value)", -> - it "allows a key path's value to be read and written", -> - expect(config.set("foo.bar.baz", 42)).toBe 42 - expect(config.get("foo.bar.baz")).toBe 42 - expect(config.get("bogus.key.path")).toBeUndefined() + describe ".get([scope], keyPath) and .set([scope], keyPath, value)", -> + describe "when called without a scope argument", -> + it "allows a key path's value to be read and written", -> + expect(config.set("foo.bar.baz", 42)).toBe 42 + expect(config.get("foo.bar.baz")).toBe 42 + expect(config.get("bogus.key.path")).toBeUndefined() - it "updates observers and saves when a key path is set", -> - observeHandler = jasmine.createSpy "observeHandler" - config.observe "foo.bar.baz", observeHandler - observeHandler.reset() + it "updates observers and saves when a key path is set", -> + observeHandler = jasmine.createSpy "observeHandler" + config.observe "foo.bar.baz", observeHandler + observeHandler.reset() - config.set("foo.bar.baz", 42) + config.set("foo.bar.baz", 42) - expect(config.save).toHaveBeenCalled() - expect(observeHandler).toHaveBeenCalledWith 42 + expect(config.save).toHaveBeenCalled() + expect(observeHandler).toHaveBeenCalledWith 42 + + describe "when called with a scope argument", -> + it "returns the config value for the most specific selector", -> + expect(config.set(".source.coffee .string.quoted.double.coffee", "foo.bar.baz", 42)).toBe 42 + config.set(".source .string.quoted.double", "foo.bar.baz", 22) + config.set(".source", "foo.bar.baz", 11) + config.set("foo.bar.baz", 1) + + expect(config.get([".source.coffee", ".string.quoted.double.coffee"], "foo.bar.baz")).toBe 42 + expect(config.get([".source.js", ".string.quoted.double.js"], "foo.bar.baz")).toBe 22 + expect(config.get([".source.js", ".variable.assignment.js"], "foo.bar.baz")).toBe 11 + expect(config.get([".text"], "foo.bar.baz")).toBe 1 describe ".setDefaults(keyPath, defaults)", -> it "assigns any previously-unassigned keys to the object at the key path", -> diff --git a/src/app/config.coffee b/src/app/config.coffee index 54033c4b2..ce2a22b5d 100644 --- a/src/app/config.coffee +++ b/src/app/config.coffee @@ -1,6 +1,9 @@ fs = require 'fs' _ = require 'underscore' EventEmitter = require 'event-emitter' +{$$} = require 'space-pen' +jQuery = require 'jquery' +Specificity = require 'specificity' configDirPath = fs.absolute("~/.atom") configJsonPath = fs.join(configDirPath, "config.json") @@ -31,15 +34,33 @@ class Config userConfig = JSON.parse(fs.read(configJsonPath)) _.extend(@settings, userConfig) - get: (keyPath) -> + get: (args...) -> + scopeStack = args.shift() if args.length > 1 + keyPath = args.shift() keys = @keysForKeyPath(keyPath) - value = @settings - for key in keys - break unless value = value[key] - value - set: (keyPath, value) -> + settingsToSearch = [] + settingsToSearch.push(@settingsForScopeChain(scopeStack)...) if scopeStack + settingsToSearch.push(@settings) + + for settings in settingsToSearch + value = settings + for key in keys + value = value[key] + break unless value? + return value if value? + undefined + + set: (args...) -> + scope = args.shift() if args.length > 2 + keyPath = args.shift() + value = args.shift() + keys = @keysForKeyPath(keyPath) + if scope + keys.unshift(scope) + keys.unshift('scopes') + hash = @settings while keys.length > 1 key = keys.shift() @@ -60,12 +81,6 @@ class Config _.defaults hash, defaults @update() - keysForKeyPath: (keyPath) -> - if typeof keyPath is 'string' - keyPath.split(".") - else - new Array(keyPath...) - observe: (keyPath, callback) -> value = @get(keyPath) previousValue = _.clone(value) @@ -87,6 +102,46 @@ class Config save: -> fs.write(configJsonPath, JSON.stringify(@settings, undefined, 2) + "\n") + keysForKeyPath: (keyPath) -> + if typeof keyPath is 'string' + keyPath.split(".") + else + new Array(keyPath...) + + settingsForScopeChain: (scopeStack) -> + return [] unless @settings.scopes? + + matchingScopeSelectors = [] + node = @buildDomNodeFromScopeChain(scopeStack) + while node + scopeSelectorsForNode = [] + for scopeSelector of @settings.scopes + if jQuery.find.matchesSelector(node, scopeSelector) + scopeSelectorsForNode.push(scopeSelector) + scopeSelectorsForNode.sort (a, b) -> Specificity(b) - Specificity(a) + matchingScopeSelectors.push(scopeSelectorsForNode...) + node = node.parentNode + + matchingScopeSelectors.map (scopeSelector) => @settings.scopes[scopeSelector] + + buildDomNodeFromScopeChain: (scopeStack) -> + scopeStack = new Array(scopeStack...) + element = $$ -> + elementsForRemainingScopes = => + classString = scopeStack.shift() + classes = classString.replace(/^\./, '').replace(/\./g, ' ') + if scopeStack.length + @div class: classes, elementsForRemainingScopes + else + @div class: classes + elementsForRemainingScopes() + + deepestChild = element.find(":not(:has(*))") + if deepestChild.length + deepestChild[0] + else + element[0] + requireUserInitScript: -> try require userInitScriptPath if fs.exists(userInitScriptPath)