Merge pull request #16845 from atom/fb-pw-simple-project-config

Add the concept of an `atomproject` file to atom.
This commit is contained in:
Philip Weiss
2018-03-11 20:41:35 -07:00
committed by GitHub
8 changed files with 277 additions and 31 deletions

View File

@@ -1,7 +1,3 @@
const path = require('path')
const temp = require('temp').track()
const fs = require('fs-plus')
describe('Config', () => {
let savedSettings
@@ -490,7 +486,6 @@ describe('Config', () => {
atom.config.set('foo.bar.baz', 'value 2')
expect(observeHandler).toHaveBeenCalledWith({newValue: 'value 2', oldValue: 'value 1'})
observeHandler.reset()
observeHandler.andCallFake(() => { throw new Error('oops') })
expect(() => atom.config.set('foo.bar.baz', 'value 1')).toThrow('oops')
expect(observeHandler).toHaveBeenCalledWith({newValue: 'value 1', oldValue: 'value 2'})
@@ -1840,4 +1835,86 @@ describe('Config', () => {
expect(atom.config.get('do.ray')).toBe('me')
})
})
describe('project specific settings', () => {
describe('config.resetProjectSettings', () => {
it('gracefully handles invalid config objects', () => {
atom.config.resetProjectSettings({})
expect(atom.config.get('foo.bar')).toBeUndefined()
})
})
describe('config.get', () => {
const dummyPath = '/Users/dummy/path.json'
describe('project settings', () => {
it('returns a deep clone of the property value', () => {
atom.config.resetProjectSettings({'*': {'value': {array: [1, {b: 2}, 3]}}}, dummyPath)
const retrievedValue = atom.config.get('value')
retrievedValue.array[0] = 4
retrievedValue.array[1].b = 2.1
expect(atom.config.get('value')).toEqual({array: [1, {b: 2}, 3]})
})
it('properly gets project settings', () => {
atom.config.resetProjectSettings({'*': {'foo': 'wei'}}, dummyPath)
expect(atom.config.get('foo')).toBe('wei')
atom.config.resetProjectSettings({'*': {'foo': {'bar': 'baz'}}}, dummyPath)
expect(atom.config.get('foo.bar')).toBe('baz')
})
it('gets project settings with higher priority than regular settings', () => {
atom.config.set('foo', 'bar')
atom.config.resetProjectSettings({'*': {'foo': 'baz'}}, dummyPath)
expect(atom.config.get('foo')).toBe('baz')
})
it('correctly gets nested and scoped properties for project settings', () => {
expect(atom.config.set('foo.bar.str', 'global')).toBe(true)
expect(atom.config.set('foo.bar.str', 'scoped', {scopeSelector: '.source.js'})).toBe(true)
expect(atom.config.get('foo.bar.str')).toBe('global')
expect(atom.config.get('foo.bar.str', {scope: ['.source.js']})).toBe('scoped')
})
it('returns a deep clone of the property value', () => {
atom.config.set('value', {array: [1, {b: 2}, 3]})
const retrievedValue = atom.config.get('value')
retrievedValue.array[0] = 4
retrievedValue.array[1].b = 2.1
expect(atom.config.get('value')).toEqual({array: [1, {b: 2}, 3]})
})
it('gets scoped values correctly', () => {
atom.config.set('foo', 'bam', {scope: ['second']})
expect(atom.config.get('foo', {'scopeSelector': 'second'})).toBe('bam')
atom.config.resetProjectSettings({'*': {'foo': 'baz'}, 'second': {'foo': 'bar'}}, dummyPath)
expect(atom.config.get('foo', {'scopeSelector': 'second'})).toBe('baz')
atom.config.clearProjectSettings()
expect(atom.config.get('foo', {'scopeSelector': 'second'})).toBe('bam')
})
it('clears project settings correctly', () => {
atom.config.set('foo', 'bar')
expect(atom.config.get('foo')).toBe('bar')
atom.config.resetProjectSettings({'*': {'foo': 'baz'}, 'second': {'foo': 'bar'}}, dummyPath)
expect(atom.config.get('foo')).toBe('baz')
expect(atom.config.getSources().length).toBe(1)
atom.config.clearProjectSettings()
expect(atom.config.get('foo')).toBe('bar')
expect(atom.config.getSources().length).toBe(0)
})
})
})
describe('config.getAll', () => {
const dummyPath = '/Users/dummy/path.json'
it('gets settings in the same way .get would return them', () => {
atom.config.resetProjectSettings({'*': {'a': 'b'}}, dummyPath)
atom.config.set('a', 'f')
expect(atom.config.getAll('a')).toEqual([{
scopeSelector: '*',
value: 'b'
}])
})
})
})
})

View File

@@ -274,6 +274,51 @@ describe('Project', () => {
})
})
describe('.replace', () => {
let projectSpecification, projectPath1, projectPath2
beforeEach(() => {
atom.project.replace(null)
projectPath1 = temp.mkdirSync('project-path1')
projectPath2 = temp.mkdirSync('project-path2')
projectSpecification = {
paths: [projectPath1, projectPath2],
originPath: 'originPath',
config: {
'baz': 'buzz'
}
}
})
it('sets a project specification', () => {
expect(atom.config.get('baz')).toBeUndefined()
atom.project.replace(projectSpecification)
expect(atom.project.getPaths()).toEqual([projectPath1, projectPath2])
expect(atom.config.get('baz')).toBe('buzz')
})
it('clears a project through replace with no params', () => {
expect(atom.config.get('baz')).toBeUndefined()
atom.project.replace(projectSpecification)
expect(atom.config.get('baz')).toBe('buzz')
expect(atom.project.getPaths()).toEqual([projectPath1, projectPath2])
atom.project.replace()
expect(atom.config.get('baz')).toBeUndefined()
expect(atom.project.getPaths()).toEqual([])
})
it('responds to change of project specification', () => {
let wasCalled = false
const callback = () => {
wasCalled = true
}
atom.project.onDidReplace(callback)
atom.project.replace(projectSpecification)
expect(wasCalled).toBe(true)
wasCalled = false
atom.project.replace()
expect(wasCalled).toBe(true)
})
})
describe('before and after saving a buffer', () => {
let buffer
beforeEach(() =>

View File

@@ -50,7 +50,6 @@ let nextId = 0
//
// An instance of this class is always available as the `atom` global.
class AtomEnvironment {
/*
Section: Properties
*/
@@ -210,7 +209,7 @@ class AtomEnvironment {
this.blobStore = params.blobStore
this.configDirPath = params.configDirPath
const {devMode, safeMode, resourcePath, userSettings} = this.getLoadSettings()
const {devMode, safeMode, resourcePath, userSettings, projectSpecification} = this.getLoadSettings()
ConfigSchema.projectHome = {
type: 'string',
@@ -224,6 +223,10 @@ class AtomEnvironment {
})
this.config.resetUserSettings(userSettings)
if (projectSpecification != null && projectSpecification.config != null) {
this.project.replace(projectSpecification)
}
this.menu.initialize({resourcePath})
this.contextMenu.initialize({resourcePath, devMode})
@@ -788,6 +791,7 @@ class AtomEnvironment {
this.disposables.add(this.applicationDelegate.onDidFailToReadUserSettings(message =>
this.notifications.addError(message)
))
this.disposables.add(this.applicationDelegate.onDidOpenLocations(this.openLocations.bind(this)))
this.disposables.add(this.applicationDelegate.onApplicationMenuCommand(this.dispatchApplicationMenuCommand.bind(this)))
this.disposables.add(this.applicationDelegate.onContextMenuCommand(this.dispatchContextMenuCommand.bind(this)))

View File

@@ -422,8 +422,12 @@ class Config {
type: 'object',
properties: {}
}
this.defaultSettings = {}
this.settings = {}
this.projectSettings = {}
this.projectFile = null
this.scopedSettingsStore = new ScopedPropertyStore()
this.settingsLoaded = false
@@ -621,10 +625,10 @@ class Config {
legacyScopeDescriptor = this.getLegacyScopeDescriptorForNewScopeDescriptor(scopeDescriptor)
if (legacyScopeDescriptor) {
result.push(...Array.from(this.scopedSettingsStore.getAll(
legacyScopeDescriptor.getScopeChain(),
keyPath,
options
) || []))
legacyScopeDescriptor.getScopeChain(),
keyPath,
options
) || []))
}
} else {
result = []
@@ -691,7 +695,7 @@ class Config {
let source = options.source
const shouldSave = options.save != null ? options.save : true
if (source && !scopeSelector) {
if (source && !scopeSelector && source !== this.projectFile) {
throw new Error("::set with a 'source' and no 'sourceSelector' is not yet implemented!")
}
@@ -708,7 +712,7 @@ class Config {
if (scopeSelector != null) {
this.setRawScopedValue(keyPath, value, source, scopeSelector)
} else {
this.setRawValue(keyPath, value)
this.setRawValue(keyPath, value, {source})
}
if (source === this.mainSource && shouldSave && this.settingsLoaded) {
@@ -943,7 +947,12 @@ class Config {
Section: Private methods managing global settings
*/
resetUserSettings (newSettings) {
resetUserSettings (newSettings, options = {}) {
this._resetSettings(newSettings, options)
}
_resetSettings (newSettings, options = {}) {
const source = options.source
newSettings = Object.assign({}, newSettings)
if (newSettings.global != null) {
newSettings['*'] = newSettings.global
@@ -954,13 +963,16 @@ class Config {
const scopedSettings = newSettings
newSettings = newSettings['*']
delete scopedSettings['*']
this.resetUserScopedSettings(scopedSettings)
this.resetScopedSettings(scopedSettings, {source})
}
return this.transact(() => {
this.settings = {}
this._clearUnscopedSettingsForSource(source)
this.settingsLoaded = true
for (let key in newSettings) { const value = newSettings[key]; this.set(key, value, {save: false}) }
for (let key in newSettings) {
const value = newSettings[key]
this.set(key, value, {save: false, source})
}
if (this.pendingOperations.length) {
for (let op of this.pendingOperations) { op() }
this.pendingOperations = []
@@ -968,10 +980,39 @@ class Config {
})
}
_clearUnscopedSettingsForSource (source) {
if (source === this.projectFile) {
this.projectSettings = {}
} else {
this.settings = {}
}
}
resetProjectSettings (newSettings, projectFile) {
// Sets the scope and source of all project settings to `path`.
newSettings = Object.assign({}, newSettings)
const oldProjectFile = this.projectFile
this.projectFile = projectFile
if (this.projectFile != null) {
this._resetSettings(newSettings, {source: this.projectFile})
} else {
this.scopedSettingsStore.removePropertiesForSource(oldProjectFile)
this.projectSettings = {}
}
}
clearProjectSettings () {
this.resetProjectSettings({}, null)
}
getRawValue (keyPath, options = {}) {
let value
if (!options.excludeSources || !options.excludeSources.includes(this.mainSource)) {
value = getValueAtKeyPath(this.settings, keyPath)
if (this.projectFile != null) {
const projectValue = getValueAtKeyPath(this.projectSettings, keyPath)
value = (projectValue === undefined) ? value : projectValue
}
}
let defaultValue
@@ -990,19 +1031,22 @@ class Config {
}
}
setRawValue (keyPath, value) {
setRawValue (keyPath, value, options = {}) {
const source = options.source ? options.source : undefined
const settingsToChange = source === this.projectFile ? 'projectSettings' : 'settings'
const defaultValue = getValueAtKeyPath(this.defaultSettings, keyPath)
if (_.isEqual(defaultValue, value)) {
if (keyPath != null) {
deleteValueAtKeyPath(this.settings, keyPath)
deleteValueAtKeyPath(this[settingsToChange], keyPath)
} else {
this.settings = null
this[settingsToChange] = null
}
} else {
if (keyPath != null) {
setValueAtKeyPath(this.settings, keyPath, value)
setValueAtKeyPath(this[settingsToChange], keyPath, value)
} else {
this.settings = value
this[settingsToChange] = value
}
}
return this.emitChangeEvent()
@@ -1168,15 +1212,22 @@ class Config {
*/
priorityForSource (source) {
return (source === this.mainSource) ? 1000 : 0
switch (source) {
case this.mainSource:
return 1000
case this.projectFile:
return 2000
default:
return 0
}
}
emitChangeEvent () {
if (this.transactDepth <= 0) { return this.emitter.emit('did-change') }
}
resetUserScopedSettings (newScopedSettings) {
const source = this.mainSource
resetScopedSettings (newScopedSettings, options = {}) {
const source = options.source == null ? this.mainSource : options.source
const priority = this.priorityForSource(source)
this.scopedSettingsStore.removePropertiesForSource(source)

View File

@@ -93,7 +93,6 @@ class AtomApplication extends EventEmitter {
this.quitting = false
this.getAllWindows = this.getAllWindows.bind(this)
this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this)
this.resourcePath = options.resourcePath
this.devResourcePath = options.devResourcePath
this.version = options.version
@@ -203,6 +202,7 @@ class AtomApplication extends EventEmitter {
openWithOptions (options) {
const {
projectSpecification,
initialPaths,
pathsToOpen,
executedFrom,
@@ -257,6 +257,7 @@ class AtomApplication extends EventEmitter {
profileStartup,
clearWindowState,
addToLastWindow,
projectSpecification,
env
})
} else if (urlsToOpen.length > 0) {
@@ -820,6 +821,7 @@ class AtomApplication extends EventEmitter {
window,
clearWindowState,
addToLastWindow,
projectSpecification,
env
} = {}) {
if (!pathsToOpen || pathsToOpen.length === 0) return
@@ -853,7 +855,7 @@ class AtomApplication extends EventEmitter {
}
let openedWindow
if (existingWindow) {
if (existingWindow && (projectSpecification == null || projectSpecification.config == null)) {
openedWindow = existingWindow
openedWindow.openLocations(locationsToOpen)
if (openedWindow.isMinimized()) {
@@ -878,6 +880,7 @@ class AtomApplication extends EventEmitter {
}
if (!resourcePath) resourcePath = this.resourcePath
if (!windowDimensions) windowDimensions = this.getDimensionsForNewWindow()
openedWindow = new AtomWindow(this, this.fileRecoveryService, {
initialPaths,
locationsToOpen,
@@ -888,6 +891,7 @@ class AtomApplication extends EventEmitter {
windowDimensions,
profileStartup,
clearWindowState,
projectSpecification,
env
})
this.addWindow(openedWindow)

View File

@@ -22,6 +22,7 @@ class AtomWindow extends EventEmitter {
this.safeMode = settings.safeMode
this.devMode = settings.devMode
this.resourcePath = settings.resourcePath
this.projectSpecification = settings.projectSpecification
let {pathToOpen, locationsToOpen} = settings
if (!locationsToOpen && pathToOpen) locationsToOpen = [{pathToOpen}]
@@ -59,9 +60,9 @@ class AtomWindow extends EventEmitter {
get: () => JSON.stringify(Object.assign({
userSettings: !this.isSpec
? this.atomApplication.configFile.get()
: null
}, this.loadSettings)),
configurable: true
: null,
projectSpecification: this.projectSpecification
}, this.loadSettings))
})
this.handleEvents()

View File

@@ -5,6 +5,7 @@ const yargs = require('yargs')
const {app} = require('electron')
const path = require('path')
const fs = require('fs-plus')
const CSON = require('season')
module.exports = function parseCommandLine (processArgs) {
const options = yargs(processArgs).wrap(yargs.terminalWidth())
@@ -52,6 +53,7 @@ module.exports = function parseCommandLine (processArgs) {
'When in test mode, waits until the specified time (in minutes) and kills the process (exit code: 130).'
)
options.alias('v', 'version').boolean('v').describe('v', 'Print the version information.')
options.alias('p', 'project').describe('p', 'Start Atom with a project specification file.')
options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')
options.alias('a', 'add').boolean('a').describe('add', 'Open path as a new project in last used window.')
options.string('socket-path')
@@ -91,6 +93,7 @@ module.exports = function parseCommandLine (processArgs) {
const benchmark = args['benchmark']
const benchmarkTest = args['benchmark-test']
const test = args['test']
const projectSpecificationFile = args['project']
const mainProcess = args['main-process']
const timeout = args['timeout']
const newWindow = args['new-window']
@@ -125,6 +128,7 @@ module.exports = function parseCommandLine (processArgs) {
}
}
// Check to see if project flag is set, then add all paths from the .atomproject.
if (args['resource-path']) {
devMode = true
devResourcePath = args['resource-path']
@@ -134,6 +138,28 @@ module.exports = function parseCommandLine (processArgs) {
devMode = true
}
let projectSpecification = {}
if (projectSpecificationFile) {
const readPath = path.isAbsolute(projectSpecificationFile)
? projectSpecificationFile
: path.join(executedFrom, projectSpecificationFile)
const contents = Object.assign({}, readProjectSpecificationSync(readPath, executedFrom))
const pathToProjectFile = path.join(executedFrom, projectSpecificationFile)
const base = path.dirname(pathToProjectFile)
pathsToOpen.push(path.dirname(projectSpecificationFile))
const paths = (contents.paths == null)
? undefined
: contents.paths.map(curPath => path.resolve(base, curPath))
projectSpecification = {
originPath: pathToProjectFile,
paths,
config: contents.config
}
}
if (devMode) {
resourcePath = devResourcePath
}
@@ -152,6 +178,7 @@ module.exports = function parseCommandLine (processArgs) {
devResourcePath = normalizeDriveLetterName(devResourcePath)
return {
projectSpecification,
resourcePath,
devResourcePath,
pathsToOpen,
@@ -177,6 +204,18 @@ module.exports = function parseCommandLine (processArgs) {
}
}
function readProjectSpecificationSync (filepath, executedFrom) {
let contents
try {
contents = CSON.readFileSync(filepath)
} catch (e) {
throw new Error('Unable to read supplied project specification file.')
}
contents.config = (contents.config == null) ? {} : contents.config
return contents
}
function normalizeDriveLetterName (filePath) {
if (process.platform === 'win32') {
return filePath.replace(/^([a-z]):/, ([driveLetter]) => driveLetter.toUpperCase() + ':')

View File

@@ -77,6 +77,31 @@ class Project extends Model {
}
}
// Layers the contents of a project's file's config
// on top of the current global config.
replace (projectSpecification) {
if (projectSpecification == null) {
atom.config.clearProjectSettings()
this.setPaths([])
} else {
if (projectSpecification.originPath == null) {
return
}
// If no path is specified, set to directory of originPath.
if (!Array.isArray(projectSpecification.paths)) {
projectSpecification.paths = [path.dirname(projectSpecification.originPath)]
}
atom.config.resetProjectSettings(projectSpecification.config, projectSpecification.originPath)
this.setPaths(projectSpecification.paths)
}
this.emitter.emit('did-replace', projectSpecification)
}
onDidReplace (callback) {
return this.emitter.on('did-replace', callback)
}
/*
Section: Serialization
*/
@@ -323,7 +348,6 @@ class Project extends Model {
// a file or does not exist, its parent directory will be added instead.
addPath (projectPath, options = {}) {
const directory = this.getDirectoryForProjectPath(projectPath)
let ok = true
if (options.exact === true) {
ok = (directory.getPath() === projectPath)
@@ -353,6 +377,7 @@ class Project extends Model {
this.emitter.emit('did-change-files', events)
}
}
// We'll use the directory's custom onDidChangeFiles callback, if available.
// CustomDirectory::onDidChangeFiles should match the signature of
// Project::onDidChangeFiles below (although it may resolve asynchronously)