Merge pull request #4802 from atom/mb-reduce-config-saves

Don't write the config to disk so darn often
This commit is contained in:
Max Brunsfeld
2014-12-30 10:41:01 -08:00
4 changed files with 80 additions and 46 deletions

View File

@@ -9,6 +9,8 @@ describe "Config", ->
beforeEach ->
dotAtomPath = temp.path('dot-atom-dir')
atom.config.configDirPath = dotAtomPath
atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson")
describe ".get(keyPath, {scope, sources, excludeSources})", ->
it "allows a key path's value to be read", ->
@@ -124,15 +126,21 @@ describe "Config", ->
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", ->
observeHandler = jasmine.createSpy "observeHandler"
atom.config.observe "foo.bar.baz", observeHandler
observeHandler.reset()
it "saves the user's config to disk after it stops changing", ->
atom.config.set("foo.bar.baz", 42)
advanceClock(50)
expect(atom.config.save).not.toHaveBeenCalled()
atom.config.set("foo.bar.baz", 43)
advanceClock(50)
expect(atom.config.save).not.toHaveBeenCalled()
atom.config.set("foo.bar.baz", 44)
advanceClock(150)
expect(atom.config.save).toHaveBeenCalled()
expect(observeHandler).toHaveBeenCalledWith 42
it "does not save when a non-default 'source' is given", ->
atom.config.set("foo.bar.baz", 42, source: 'some-other-source', scopeSelector: '.a')
advanceClock(500)
expect(atom.config.save).not.toHaveBeenCalled()
it "does not allow a 'source' option without a 'scopeSelector'", ->
expect(-> atom.config.set("foo", 1, source: [".source.ruby"])).toThrow()
@@ -215,6 +223,7 @@ describe "Config", ->
atom.config.save.reset()
atom.config.unset('a.c')
advanceClock(500)
expect(atom.config.save.callCount).toBe 1
describe "when a 'source' and no 'scopeSelector' is given", ->
@@ -252,6 +261,7 @@ describe "Config", ->
atom.config.save.reset()
atom.config.unset('foo.bar.baz', scopeSelector: '.source.coffee')
advanceClock(150)
expect(atom.config.save.callCount).toBe 1
it "allows removing settings for a specific source and scope selector", ->
@@ -285,26 +295,32 @@ describe "Config", ->
it "removes the scoped value when it was the only set value on the object", ->
spyOn(CSON, 'writeFileSync')
jasmine.unspy atom.config, 'save'
atom.config.save.andCallThrough()
atom.config.setDefaults("foo", bar: baz: 10)
atom.config.set('foo.bar.baz', 55, scopeSelector: '.source.coffee')
atom.config.set('foo.bar.ok', 20, scopeSelector: '.source.coffee')
CSON.writeFileSync.reset()
expect(atom.config.get('foo.bar.baz', scope: ['.source.coffee'])).toBe 55
advanceClock(150)
CSON.writeFileSync.reset()
atom.config.unset('foo.bar.baz', scopeSelector: '.source.coffee')
expect(atom.config.get('foo.bar.baz', scope: ['.source.coffee'])).toBe 10
expect(atom.config.get('foo.bar.ok', scope: ['.source.coffee'])).toBe 20
advanceClock(150)
expect(CSON.writeFileSync).toHaveBeenCalled()
properties = CSON.writeFileSync.mostRecentCall.args[1]
expect(properties['.coffee.source']).toEqual
foo:
bar:
ok: 20
CSON.writeFileSync.reset()
atom.config.unset('foo.bar.ok', scopeSelector: '.source.coffee')
advanceClock(150)
expect(CSON.writeFileSync).toHaveBeenCalled()
properties = CSON.writeFileSync.mostRecentCall.args[1]
expect(properties['.coffee.source']).toBeUndefined()
@@ -573,7 +589,6 @@ describe "Config", ->
describe "when ~/.atom/config.json exists", ->
it "writes any non-default properties to ~/.atom/config.json", ->
atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.json")
atom.config.set("a.b.c", 1)
atom.config.set("a.b.d", 2)
atom.config.set("x.y.z", 3)
@@ -582,13 +597,12 @@ describe "Config", ->
CSON.writeFileSync.reset()
atom.config.save()
expect(CSON.writeFileSync.argsForCall[0][0]).toBe path.join(atom.config.configDirPath, "atom.config.json")
expect(CSON.writeFileSync.argsForCall[0][0]).toBe atom.config.configFilePath
writtenConfig = CSON.writeFileSync.argsForCall[0][1]
expect(writtenConfig).toEqual global: atom.config.settings
describe "when ~/.atom/config.json doesn't exist", ->
it "writes any non-default properties to ~/.atom/config.cson", ->
atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson")
atom.config.set("a.b.c", 1)
atom.config.set("a.b.d", 2)
atom.config.set("x.y.z", 3)
@@ -624,8 +638,6 @@ describe "Config", ->
describe ".loadUserConfig()", ->
beforeEach ->
atom.config.configDirPath = dotAtomPath
atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson")
expect(fs.existsSync(atom.config.configDirPath)).toBeFalsy()
atom.config.setSchema 'foo',
type: 'object'
@@ -714,6 +726,15 @@ describe "Config", ->
describe ".observeUserConfig()", ->
updatedHandler = null
writeConfigFile = (data) ->
previousSetTimeoutCallCount = setTimeout.callCount
runs ->
fs.writeFileSync(atom.config.configFilePath, data)
waitsFor "debounced config file load", ->
setTimeout.callCount > previousSetTimeoutCallCount
runs ->
advanceClock(1000)
beforeEach ->
atom.config.setSchema 'foo',
type: 'object'
@@ -729,8 +750,6 @@ describe "Config", ->
type: 'integer'
default: 12
atom.config.configDirPath = dotAtomPath
atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson")
expect(fs.existsSync(atom.config.configDirPath)).toBeFalsy()
fs.writeFileSync atom.config.configFilePath, """
global:
@@ -752,7 +771,7 @@ describe "Config", ->
describe "when the config file changes to contain valid cson", ->
it "updates the config data", ->
fs.writeFileSync(atom.config.configFilePath, "foo: { bar: 'quux', baz: 'bar'}")
writeConfigFile("foo: { bar: 'quux', baz: 'bar'}")
waitsFor 'update event', -> updatedHandler.callCount > 0
runs ->
expect(atom.config.get('foo.bar')).toBe 'quux'
@@ -761,8 +780,9 @@ describe "Config", ->
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', baz: 'ok'}")
writeConfigFile("foo: { bar: 'baz', baz: 'ok'}")
waitsFor 'update event', -> updatedHandler.callCount > 0
runs ->
expect(noChangeSpy).not.toHaveBeenCalled()
expect(atom.config.get('foo.bar')).toBe 'baz'
@@ -774,7 +794,8 @@ describe "Config", ->
type: 'array'
items:
type: 'string'
fs.writeFileSync(atom.config.configFilePath, "foo: { bar: ['baz', 'ok']}")
writeConfigFile("foo: { bar: ['baz', 'ok']}")
waitsFor 'update event', -> updatedHandler.callCount > 0
runs -> updatedHandler.reset()
@@ -782,8 +803,9 @@ describe "Config", ->
noChangeSpy = jasmine.createSpy()
atom.config.onDidChange('foo.bar', noChangeSpy)
fs.writeFileSync(atom.config.configFilePath, "foo: { bar: ['baz', 'ok'], baz: 'another'}")
writeConfigFile("foo: { bar: ['baz', 'ok'], baz: 'another'}")
waitsFor 'update event', -> updatedHandler.callCount > 0
runs ->
expect(noChangeSpy).not.toHaveBeenCalled()
expect(atom.config.get('foo.bar')).toEqual ['baz', 'ok']
@@ -794,12 +816,13 @@ describe "Config", ->
scopedSpy = jasmine.createSpy()
atom.config.onDidChange('foo.scoped', scope: ['.source.ruby'], scopedSpy)
fs.writeFileSync atom.config.configFilePath, """
writeConfigFile """
global:
foo:
scoped: false
"""
waitsFor 'update event', -> updatedHandler.callCount > 0
runs ->
expect(scopedSpy).toHaveBeenCalled()
expect(atom.config.get('foo.scoped', scope: ['.source.ruby'])).toBe false
@@ -808,7 +831,7 @@ describe "Config", ->
noChangeSpy = jasmine.createSpy()
atom.config.onDidChange('foo.scoped', scope: ['.source.ruby'], noChangeSpy)
fs.writeFileSync atom.config.configFilePath, """
writeConfigFile """
global:
foo:
bar: 'baz'
@@ -817,6 +840,7 @@ describe "Config", ->
scoped: true
"""
waitsFor 'update event', -> updatedHandler.callCount > 0
runs ->
expect(noChangeSpy).not.toHaveBeenCalled()
expect(atom.config.get('foo.bar', scope: ['.source.ruby'])).toBe 'baz'
@@ -824,7 +848,7 @@ describe "Config", ->
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'}")
writeConfigFile("foo: { baz: 'new'}")
waitsFor 'update event', -> updatedHandler.callCount > 0
runs ->
expect(atom.config.get('foo.bar')).toBe 'def'
@@ -832,19 +856,20 @@ describe "Config", ->
describe "when the config file changes to be empty", ->
beforeEach ->
fs.writeFileSync(atom.config.configFilePath, "")
writeConfigFile("")
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
advanceClock(500)
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'")
writeConfigFile("foo: bar: 'newVal'")
waitsFor 'update event', -> updatedHandler.callCount > 0
it "sets the setting to the value specified in the config file", ->
@@ -853,7 +878,7 @@ describe "Config", ->
describe "when the config file changes to contain invalid cson", ->
beforeEach ->
spyOn(console, 'error')
fs.writeFileSync(atom.config.configFilePath, "}}}")
writeConfigFile("}}}")
waitsFor "error to be logged", -> console.error.callCount > 0
it "logs a warning and does not update config data", ->
@@ -864,11 +889,12 @@ describe "Config", ->
describe "when the config file subsequently changes again to contain valid cson", ->
beforeEach ->
fs.writeFileSync(atom.config.configFilePath, "foo: bar: 'newVal'")
writeConfigFile("foo: bar: 'newVal'")
waitsFor 'update event', -> updatedHandler.callCount > 0
it "updates the config data and resumes saving", ->
atom.config.set("hair", "blonde")
advanceClock(500)
expect(atom.config.save).toHaveBeenCalled()
describe ".initializeConfigDirectory()", ->

View File

@@ -84,6 +84,10 @@ beforeEach ->
atom.workspaceViewParentSelector = '#jasmine-content'
window.resetTimeouts()
spyOn(_._, "now").andCallFake -> window.now
spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout
spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout
atom.packages.packageStates = {}
serializedWindowState = null
@@ -102,9 +106,9 @@ beforeEach ->
spyOn(atom.menu, 'sendToBrowserProcess')
# reset config before each spec; don't load or save from/to `config.json`
spyOn(Config::, 'load')
spyOn(Config::, 'save')
config = new Config({resourcePath, configDirPath: atom.getConfigDirPath()})
spyOn(config, 'load')
spyOn(config, 'save')
atom.config = config
atom.loadConfig()
config.set "core.destroyEmptyPanes", false
@@ -114,6 +118,7 @@ beforeEach ->
config.set "core.disabledPackages", ["package-that-throws-an-exception",
"package-with-broken-package-json", "package-with-broken-keymap"]
config.set "editor.useShadowDOM", true
advanceClock(1000)
config.load.reset()
config.save.reset()
@@ -121,8 +126,6 @@ beforeEach ->
TextEditorElement::setUpdatedSynchronously(true)
spyOn(atom, "setRepresentedFilename")
spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout
spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout
spyOn(pathwatcher.File.prototype, "detectResurrectionAfterDelay").andCallFake -> @detectResurrection()
spyOn(TextEditor.prototype, "shouldPromptToSave").andReturn false

View File

@@ -755,7 +755,6 @@ describe "TextEditorComponent", ->
expect(cursorNode.offsetWidth).toBe charWidth
it "blinks cursors when they aren't moving", ->
spyOn(_._, 'now').andCallFake -> window.now # Ensure _.debounce is based on our fake spec timeline
cursorsNode = componentNode.querySelector('.cursors')
expect(cursorsNode.classList.contains('blink-off')).toBe false
@@ -2048,8 +2047,6 @@ describe "TextEditorComponent", ->
expect(component.mouseWheelScreenRow).toBe null
it "clears the mouseWheelScreenRow after a delay even if the event does not cause scrolling", ->
spyOn(_._, 'now').andCallFake -> window.now # Ensure _.debounce is based on our fake spec timeline
expect(editor.getScrollTop()).toBe 0
lineNode = componentNode.querySelector('.line')

View File

@@ -317,8 +317,9 @@ class Config
@configFilePath = fs.resolve(@configDirPath, 'config', ['json', 'cson'])
@configFilePath ?= path.join(@configDirPath, 'config.cson')
@transactDepth = 0
@prioritiesBySource = {}
@prioritiesBySource[@getUserConfigPath()] = 1000
@debouncedSave = _.debounce(@save, 100)
@debouncedLoad = _.debounce(@loadUserConfig, 100)
###
Section: Config Subscription
@@ -570,6 +571,7 @@ class Config
[keyPath, value, options] = arguments
scopeSelector = options?.scopeSelector
source = options?.source
shouldSave = options?.save ? true
if source and not scopeSelector
throw new Error("::set with a 'source' and no 'sourceSelector' is not yet implemented!")
@@ -587,7 +589,7 @@ class Config
else
@setRawValue(keyPath, value)
@save() unless @configFileHasErrors or options?.save is false
@debouncedSave() if source is @getUserConfigPath() and shouldSave and not @configFileHasErrors
true
# Essential: Restore the setting at `keyPath` to its default value.
@@ -616,17 +618,16 @@ class Config
@scopedSettingsStore.removePropertiesForSourceAndSelector(source, scopeSelector)
_.setValueForKeyPath(settings, keyPath, undefined)
settings = withoutEmptyObjects(settings)
@set(null, settings, {scopeSelector, source, priority: @prioritiesBySource[source]}) if settings?
@save() unless @configFileHasErrors
@set(null, settings, {scopeSelector, source, priority: @priorityForSource(source)}) if settings?
@debouncedSave()
else
@scopedSettingsStore.removePropertiesForSource(source)
@scopedSettingsStore.removePropertiesForSourceAndSelector(source, scopeSelector)
@emitChangeEvent()
else
@scopedSettingsStore.removePropertiesForSource(source)
if keyPath?
@set(keyPath, _.valueForKeyPath(@defaultSettings, keyPath))
# Extended: Get an {Array} of all of the `source` {String}s with which
# settings have been added via {::set}.
getSources: ->
@@ -820,7 +821,7 @@ class Config
observeUserConfig: ->
try
@watchSubscription ?= pathWatcher.watch @configFilePath, (eventType) =>
@loadUserConfig() if eventType is 'change' and @watchSubscription?
@debouncedLoad() if eventType is 'change' and @watchSubscription?
catch error
@notifyFailure('Failed to watch user config', error)
@@ -963,12 +964,19 @@ class Config
Section: Private Scoped Settings
###
priorityForSource: (source) ->
if source is @getUserConfigPath()
1000
else
0
emitChangeEvent: ->
@emitter.emit 'did-change' unless @transactDepth > 0
resetUserScopedSettings: (newScopedSettings) ->
@scopedSettingsStore.removePropertiesForSource(@getUserConfigPath())
@scopedSettingsStore.addProperties(@getUserConfigPath(), newScopedSettings, priority: @prioritiesBySource[@getUserConfigPath()])
source = @getUserConfigPath()
@scopedSettingsStore.removePropertiesForSource(source)
@scopedSettingsStore.addProperties(source, newScopedSettings, priority: @priorityForSource(source))
@emitChangeEvent()
addScopedSettings: (source, selector, value, options) ->
@@ -989,7 +997,7 @@ class Config
settingsBySelector = {}
settingsBySelector[selector] = value
@scopedSettingsStore.addProperties(source, settingsBySelector, priority: @prioritiesBySource[source])
@scopedSettingsStore.addProperties(source, settingsBySelector, priority: @priorityForSource(source))
@emitChangeEvent()
getRawScopedValue: (scopeDescriptor, keyPath, options) ->