From bf6a4e3db4b28bfe068feb72199746dad98d6a83 Mon Sep 17 00:00:00 2001 From: Philip Weiss Date: Sun, 18 Feb 2018 15:03:00 -0800 Subject: [PATCH] add .atomProject files --- spec/config-spec.js | 76 ++++++++++++++++++++++-- src/atom-environment.js | 7 ++- src/config.js | 82 +++++++++++++++++++++++--- src/main-process/atom-application.js | 2 +- src/main-process/atom-window.js | 11 +++- src/main-process/parse-command-line.js | 36 ++++++++++- src/project.js | 8 +++ undefined/.gitignore | 7 +++ undefined/init.coffee | 11 ++++ undefined/keymap.cson | 32 ++++++++++ undefined/packages/README.md | 1 + undefined/snippets.cson | 21 +++++++ undefined/styles.less | 32 ++++++++++ 13 files changed, 305 insertions(+), 21 deletions(-) create mode 100644 undefined/.gitignore create mode 100644 undefined/init.coffee create mode 100644 undefined/keymap.cson create mode 100644 undefined/packages/README.md create mode 100644 undefined/snippets.cson create mode 100644 undefined/styles.less diff --git a/spec/config-spec.js b/spec/config-spec.js index 3754e517f..27c37de56 100644 --- a/spec/config-spec.js +++ b/spec/config-spec.js @@ -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,75 @@ describe('Config', () => { expect(atom.config.get('do.ray')).toBe('me') }) }) + + describe('project specific configs', () => { + describe('config.resetProjectSettings', () => { + it('gracefully handles invalid config objects', () => { + atom.config.resetProjectSettings({}) + expect(atom.config.get('foo.bar')).toBeUndefined() + }) + }) + + describe('config.get', () => { + describe('project configs', () => { + it('returns a deep clone of the property value', () => { + atom.config.resetProjectSettings({'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('should properly get project configs', () => { + atom.config.resetProjectSettings({'foo': 'wei'}) + expect(atom.config.get('foo')).toBe('wei') + atom.config.resetProjectSettings({'foo': {'bar': 'baz'}}) + expect(atom.config.get('foo.bar')).toBe('baz') + }) + + it('should get project settings with higher priority than regular settings', () => { + atom.config.set('foo', 'bar') + atom.config.resetProjectSettings({'foo': 'baz'}) + expect(atom.config.get('foo')).toBe('baz') + }) + + it('correctly gets nested and scoped properties for project configs', () => { + 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('clears project settings correctly', () => { + atom.config.set('foo', 'bar') + expect(atom.config.get('foo')).toBe('bar') + atom.config.resetProjectSettings({'foo': 'baz'}) + expect(atom.config.get('foo')).toBe('baz') + expect(atom.config.getSources().length).toBe(1) + atom.config.removeProjectSettings() + expect(atom.config.get('foo')).toBe('bar') + expect(atom.config.getSources().length).toBe(0) + }) + }) + }) + + describe('config.getAll', () => { + it('should get settings in the same way .get would return them', () => { + atom.config.resetProjectSettings({'a': 'b'}) + atom.config.set('a', 'f') + expect(atom.config.getAll('a')).toEqual([{ + scopeSelector: '*', + value: 'b' + }]) + }) + }) + }) }) diff --git a/src/atom-environment.js b/src/atom-environment.js index 841d93d95..dcf235e7d 100644 --- a/src/atom-environment.js +++ b/src/atom-environment.js @@ -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, projectSettings} = this.getLoadSettings() ConfigSchema.projectHome = { type: 'string', @@ -224,6 +223,10 @@ class AtomEnvironment { }) this.config.resetUserSettings(userSettings) + if (projectSettings != null) { + this.project.resetProjectSettings(projectSettings) + } + this.menu.initialize({resourcePath}) this.contextMenu.initialize({resourcePath, devMode}) diff --git a/src/config.js b/src/config.js index a589654c9..9c2760fc2 100644 --- a/src/config.js +++ b/src/config.js @@ -360,6 +360,7 @@ const ScopeDescriptor = require('./scope-descriptor') // * Don't depend on (or write to) configuration keys outside of your keypath. // const schemaEnforcers = {} +const PROJECT = '__project' class Config { static addSchemaEnforcer (typeName, enforcerFunction) { @@ -422,6 +423,8 @@ class Config { type: 'object', properties: {} } + + this.hasCurrentProject = false this.defaultSettings = {} this.settings = {} this.scopedSettingsStore = new ScopedPropertyStore() @@ -579,7 +582,9 @@ class Config { // Returns the value from Atom's default settings, the user's configuration // file in the type specified by the configuration schema. get (...args) { - let keyPath, options, scope + let keyPath, scope, value + let options = {} + if (args.length > 1) { if ((typeof args[0] === 'string') || (args[0] == null)) { [keyPath, options] = args; @@ -589,8 +594,18 @@ class Config { [keyPath] = args } + const noSources = !options.sources || (Array.isArray(options.sources) && options.sources.length === 0) + if (this.hasCurrentProject && noSources) { + const projectScope = Array.isArray(scope) ? scope.push(PROJECT) : [PROJECT] + const projectOptions = Object.assign({sources: [PROJECT]}, options) + value = this.getRawScopedValue(projectScope, keyPath, projectOptions) + if (value != null) { + return value + } + } + if (scope != null) { - const value = this.getRawScopedValue(scope, keyPath, options) + value = this.getRawScopedValue(scope, keyPath, options) return value != null ? value : this.getRawValue(keyPath, options) } else { return this.getRawValue(keyPath, options) @@ -606,31 +621,68 @@ class Config { // Returns an {Array} of {Object}s with the following keys: // * `scopeDescriptor` The {ScopeDescriptor} with which the value is associated // * `value` The value for the key-path - getAll (keyPath, options) { - let globalValue, result, scope + getAll (keyPath, options = {}) { + let result, scope if (options != null) { ({scope} = options) } + const priorities = [] + + // Priority level for atom project settings. + if (this.hasCurrentProject) { + options.scope = (scope == null) ? [PROJECT] : scope.push(PROJECT) + options.sources = (options.sources == null ? [PROJECT] : options.sources.push(PROJECT)) + priorities.push({ options, source: PROJECT }) + } + + // Priority level for atom global settings. + priorities.push({options}) + + for (let priority of priorities) { + result = this.getAllForPriority(keyPath, priority) + if (result.length > 0) { + return result + } + } + return result + } + + // Gets all values for a particular priority. IE: Project settings, + // then global settings. + getAllForPriority (keyPath, priority) { + let globalValue, result, scope + if (priority.options != null) { ({scope} = priority.options) } + if (scope != null) { let legacyScopeDescriptor const scopeDescriptor = ScopeDescriptor.fromObject(scope) result = this.scopedSettingsStore.getAll( scopeDescriptor.getScopeChain(), keyPath, - options + priority.options ) legacyScopeDescriptor = this.getLegacyScopeDescriptor(scopeDescriptor) if (legacyScopeDescriptor) { result.push(...Array.from(this.scopedSettingsStore.getAll( legacyScopeDescriptor.getScopeChain(), keyPath, - options + priority.options ) || [])) } } else { result = [] } - globalValue = this.getRawValue(keyPath, options) + if (priority.source == null) { + globalValue = this.getRawValue(keyPath, priority.options) + } else { + result = result.map((obj) => { + if (obj.scopeSelector === priority.source) { + obj.scopeSelector = '*' + } + return obj + }) + } + if (globalValue) { result.push({scopeSelector: '*', value: globalValue}) } @@ -958,6 +1010,18 @@ class Config { }) } + resetProjectSettings (newSettings, options = {}) { + // Sets the scope and source of all project settings to `path`. + this.hasCurrentProject = !options.removeProject + const pathScopedSettings = {} + pathScopedSettings[PROJECT] = newSettings + this.resetUserScopedSettings(pathScopedSettings, {source: PROJECT}) + } + + removeProjectSettings () { + this.resetProjectSettings({}, {removeProject: true}) + } + getRawValue (keyPath, options = {}) { let value if (!options.excludeSources || !options.excludeSources.includes(this.mainSource)) { @@ -1165,8 +1229,8 @@ class Config { if (this.transactDepth <= 0) { return this.emitter.emit('did-change') } } - resetUserScopedSettings (newScopedSettings) { - const source = this.mainSource + resetUserScopedSettings (newScopedSettings, options = {}) { + const source = options.source == null ? this.mainSource : options.source const priority = this.priorityForSource(source) this.scopedSettingsStore.removePropertiesForSource(source) diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index c57b43305..df541446e 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -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 @@ -101,6 +100,7 @@ class AtomApplication extends EventEmitter { this.safeMode = options.safeMode this.socketPath = options.socketPath this.logFile = options.logFile + this.projectSettings = options.projectSettings this.userDataDir = options.userDataDir this._killProcess = options.killProcess || process.kill.bind(process) if (options.test || options.benchmark || options.benchmarkTest) this.socketPath = null diff --git a/src/main-process/atom-window.js b/src/main-process/atom-window.js index 8d700e379..b6eeb804b 100644 --- a/src/main-process/atom-window.js +++ b/src/main-process/atom-window.js @@ -55,10 +55,17 @@ class AtomWindow extends EventEmitter { if (this.shouldHideTitleBar()) options.frame = false this.browserWindow = new BrowserWindow(options) + let projectSettings + if (this.atomApplication.projectSettings != null) { + projectSettings = this.atomApplication.projectSettings + } + Object.defineProperty(this.browserWindow, 'loadSettingsJSON', { get: () => JSON.stringify(Object.assign({ - userSettings: this.atomApplication.configFile.get() - }, this.loadSettings)) + userSettings: this.atomApplication.configFile.get(), + projectSettings + }, this.loadSettings)), + configurable: true }) this.handleEvents() diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js index 3b0654962..6b5964554 100644 --- a/src/main-process/parse-command-line.js +++ b/src/main-process/parse-command-line.js @@ -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', 'atom-project').describe('p', 'Start atom with an atom-project 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 atomProject = args['atom-project'] const mainProcess = args['main-process'] const timeout = args['timeout'] const newWindow = args['new-window'] @@ -125,6 +128,8 @@ module.exports = function parseCommandLine (processArgs) { } } + // Check to see if atom-project flag is set, then add all paths from the .atom-project. + if (args['resource-path']) { devMode = true devResourcePath = args['resource-path'] @@ -134,6 +139,16 @@ module.exports = function parseCommandLine (processArgs) { devMode = true } + let projectSettings + if (atomProject) { + const config = readProjectSettingsSync(atomProject, executedFrom) + const paths = config.paths + projectSettings = config.config + if (paths != null) { + pathsToOpen = pathsToOpen.concat(paths) + } + } + if (devMode) { resourcePath = devResourcePath } @@ -150,8 +165,8 @@ module.exports = function parseCommandLine (processArgs) { resourcePath = normalizeDriveLetterName(resourcePath) devResourcePath = normalizeDriveLetterName(devResourcePath) - return { + projectSettings, resourcePath, devResourcePath, pathsToOpen, @@ -177,7 +192,24 @@ module.exports = function parseCommandLine (processArgs) { } } -function normalizeDriveLetterName (filePath) { +const readProjectSettingsSync = (filepath, executedFrom) => { + if (!hasAtomProjectFormat(path.basename(filepath))) { + throw new Error('File must match format: *.atom-project.{json, cson}') + } + try { + const readPath = path.isAbsolute(filepath) ? filepath : path.join(executedFrom, filepath) + return CSON.readFileSync(readPath) + } catch (e) { + throw new Error('Unable to read supplied config file.') + } +} + +const hasAtomProjectFormat = (atomProject) => { + const projectFileFormat = /.*\.atomProject\.(json|cson)/ + return projectFileFormat.test(atomProject) +} + +const normalizeDriveLetterName = (filePath) => { if (process.platform === 'win32') { return filePath.replace(/^([a-z]):/, ([driveLetter]) => driveLetter.toUpperCase() + ':') } else { diff --git a/src/project.js b/src/project.js index 9cd2d1245..7a5680da0 100644 --- a/src/project.js +++ b/src/project.js @@ -77,6 +77,14 @@ class Project extends Model { } } + resetProjectSettings (newSettings) { + atom.config.resetProjectSettings(newSettings) + } + + clearProjectSettings () { + atom.config.clearProjectSettings() + } + /* Section: Serialization */ diff --git a/undefined/.gitignore b/undefined/.gitignore new file mode 100644 index 000000000..41cb47854 --- /dev/null +++ b/undefined/.gitignore @@ -0,0 +1,7 @@ +blob-store +compile-cache +dev +storage +.apm +.node-gyp +.npm diff --git a/undefined/init.coffee b/undefined/init.coffee new file mode 100644 index 000000000..09dab00dc --- /dev/null +++ b/undefined/init.coffee @@ -0,0 +1,11 @@ +# Your init script +# +# Atom will evaluate this file each time a new window is opened. It is run +# after packages are loaded/activated and after the previous editor state +# has been restored. +# +# An example hack to log to the console when each text editor is saved. +# +# atom.workspace.observeTextEditors (editor) -> +# editor.onDidSave -> +# console.log "Saved! #{editor.getPath()}" diff --git a/undefined/keymap.cson b/undefined/keymap.cson new file mode 100644 index 000000000..bfbadea95 --- /dev/null +++ b/undefined/keymap.cson @@ -0,0 +1,32 @@ +# Your keymap +# +# Atom keymaps work similarly to style sheets. Just as style sheets use +# selectors to apply styles to elements, Atom keymaps use selectors to associate +# keystrokes with events in specific contexts. Unlike style sheets however, +# each selector can only be declared once. +# +# You can create a new keybinding in this file by typing "key" and then hitting +# tab. +# +# Here's an example taken from Atom's built-in keymap: +# +# 'atom-text-editor': +# 'enter': 'editor:newline' +# +# 'atom-workspace': +# 'ctrl-shift-p': 'core:move-up' +# 'ctrl-p': 'core:move-down' +# +# You can find more information about keymaps in these guides: +# * http://flight-manual.atom.io/using-atom/sections/basic-customization/#customizing-keybindings +# * http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth/ +# +# If you're having trouble with your keybindings not working, try the +# Keybinding Resolver: `Cmd+.` on macOS and `Ctrl+.` on other platforms. See the +# Debugging Guide for more information: +# * http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-the-keybindings +# +# This file uses CoffeeScript Object Notation (CSON). +# If you are unfamiliar with CSON, you can read more about it in the +# Atom Flight Manual: +# http://flight-manual.atom.io/using-atom/sections/basic-customization/#configuring-with-cson diff --git a/undefined/packages/README.md b/undefined/packages/README.md new file mode 100644 index 000000000..540b6949c --- /dev/null +++ b/undefined/packages/README.md @@ -0,0 +1 @@ +All packages in this directory will be automatically loaded diff --git a/undefined/snippets.cson b/undefined/snippets.cson new file mode 100644 index 000000000..cd66bba04 --- /dev/null +++ b/undefined/snippets.cson @@ -0,0 +1,21 @@ +# Your snippets +# +# Atom snippets allow you to enter a simple prefix in the editor and hit tab to +# expand the prefix into a larger code block with templated values. +# +# You can create a new snippet in this file by typing "snip" and then hitting +# tab. +# +# An example CoffeeScript snippet to expand log to console.log: +# +# '.source.coffee': +# 'Console log': +# 'prefix': 'log' +# 'body': 'console.log $1' +# +# Each scope (e.g. '.source.coffee' above) can only be declared once. +# +# This file uses CoffeeScript Object Notation (CSON). +# If you are unfamiliar with CSON, you can read more about it in the +# Atom Flight Manual: +# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson diff --git a/undefined/styles.less b/undefined/styles.less new file mode 100644 index 000000000..e4fad4f31 --- /dev/null +++ b/undefined/styles.less @@ -0,0 +1,32 @@ +/* + * Your Stylesheet + * + * This stylesheet is loaded when Atom starts up and is reloaded automatically + * when it is changed and saved. + * + * Add your own CSS or Less to fully customize Atom. + * If you are unfamiliar with Less, you can read more about it here: + * http://lesscss.org + */ + + +/* + * Examples + * (To see them, uncomment and save) + */ + +// style the background color of the tree view +.tree-view { + // background-color: whitesmoke; +} + +// style the background and foreground colors on the atom-text-editor-element itself +atom-text-editor { + // color: white; + // background-color: hsl(180, 24%, 12%); +} + +// style UI elements inside atom-text-editor +atom-text-editor .cursor { + // border-color: red; +}