Merge remote-tracking branch 'origin/master' into weekly-2018-03-12

This commit is contained in:
Jason Rudolph
2018-03-12 09:49:12 -04:00
10 changed files with 343 additions and 43 deletions

View File

@@ -1,6 +1,8 @@
# Near-term plans
In this directory, you'll find weekly progress and plans from the core Atom team at GitHub. In addition, this document summarizes the work we're intending to prioritize within the next several months.
Want to know what the Atom team is working on and what has our focus over the next few months? You've come to the right place. 🎯
In this directory, you'll find **weekly progress and planning updates** from the core Atom team at GitHub (e.g., [`2018-02-12.md`](2018-02-12.md)), and the sections below represent our **near-term roadmap**:
* [Atom IDE](#atom-ide)
* [GitHub package](#github-package)
@@ -8,19 +10,25 @@ In this directory, you'll find weekly progress and plans from the core Atom team
* [Tree-sitter](#tree-sitter)
* [Xray](#xray)
This roadmap is a [living document](https://en.wikipedia.org/wiki/Living_document): it represents our current plans, but we expect these plans to change from time to time.
---
# Atom IDE
## Roadmap
## Looking ahead
TODO
## Looking farther ahead
TODO
---
# GitHub package
- [atom/github](http://github.com/atom/github) (Atom package)
Main repository: [atom/github](http://github.com/atom/github) (Atom package)
## Roadmap
@@ -66,7 +74,7 @@ _Longer-term goals:_ Finish the credential handler refactor begun in [#846](http
* Improve our handling of 2FA credentials. Ideally we could detect when a user has 2FA enabled and prompt for a one-time code. [#844](https://github.com/atom/github/issues/844)
## Looking ahead
## Looking farther ahead
In no particular order:
@@ -82,9 +90,45 @@ In no particular order:
# Teletype
Main repository: [atom/teletype](http://github.com/atom/teletype) (Atom package)
## Roadmap
## Looking ahead
##### 1. Deliver a multi-file collaboration experience that meets 80% of the needs with 20% of the effort
- Ship RFC-001 (https://github.com/atom/teletype/issues/268)
##### 2. Streamline collaboration set-up
Near-term goal: Encourage more collaboration by reducing barriers to entry.
Longer-term goal: Provide the world's fastest transition from "I want to collaborate" to "I am collaborating." 🚀
- Publish RFC (including a request for review from GitHub's Community and Safety team)
- Host can share a URL for the portal, and guests can follow the URL to instantly join the portal (https://github.com/atom/teletype/issues/109)
- Quickly collaborate with coworkers and friends (https://github.com/atom/teletype/issues/213, https://github.com/atom/teletype/issues/284)
- You can view a list of past collaborators (i.e., a ["buddy list"](https://github.com/atom/teletype/issues/22) of sorts).
- You can choose any online person in the buddy list and invite them to join your portal. They get a notification (or similar) informing them of the invitation, and they can choose to join the portal or not.
- To prevent abuse/harassment, each time you join a portal via a URL or portal ID, Teletype adds the collaborators to your buddy list. You can directly invite anyone in your buddy list to join your portal, and anyone in your buddy list can invite you to a portal. You can remove anyone from your buddy list, at which point they can no longer _directly_ invite you to a portal.
##### 3. Nice bang-for-the-buck refinements
- Add a colored border around avatars that matches the cursor when that participant's tether is not retracted (https://github.com/atom/teletype/issues/338)
##### 4. Prioritized bugs
- Uncaught TypeError: Cannot match against 'undefined' or 'null' (https://github.com/atom/teletype/issues/233)
## Looking farther ahead
In no particular order:
- 🐛 Resolve or reduce impact of package initialization errors (https://github.com/atom/teletype/issues/266)
- 🐛 Surface uncaught errors in promises (https://github.com/atom/teletype/issues/298#issuecomment-355369327)
- ✨ Ensure remote buffers are updated when host renames files (https://github.com/atom/teletype/issues/147)
- 💖 In the buddy list, you can see which people are currently online (i.e., presence)
- 💖 Screen-sharing -- (We should prioritize screen-sharing above audio. We can keep using Slack/Skype/Zoom/Whatever for audio and use Atom for screen-sharing, whereas the opposite is not true; disabling audio on a Slack call would feel unintuitive.)
- 💖 Audio
---
@@ -92,7 +136,11 @@ In no particular order:
## Roadmap
## Looking ahead
TODO
## Looking farther ahead
TODO
---
@@ -100,4 +148,8 @@ In no particular order:
## Roadmap
## Looking ahead
TODO
## Looking farther ahead
TODO

View File

@@ -148,15 +148,15 @@
"language-go": "0.45.2",
"language-html": "0.49.0",
"language-hyperlink": "0.16.3",
"language-java": "0.28.0",
"language-javascript": "0.128.3",
"language-java": "0.29.0",
"language-javascript": "0.128.4",
"language-json": "0.19.1",
"language-less": "0.34.2",
"language-make": "0.22.3",
"language-mustache": "0.14.5",
"language-objective-c": "0.15.1",
"language-perl": "0.38.1",
"language-php": "0.43.1",
"language-php": "0.43.2",
"language-property-list": "0.9.1",
"language-python": "0.49.2",
"language-ruby": "0.71.4",
@@ -165,12 +165,12 @@
"language-shellscript": "0.26.2",
"language-source": "0.9.0",
"language-sql": "0.25.10",
"language-text": "0.7.3",
"language-text": "0.7.4",
"language-todo": "0.29.4",
"language-toml": "0.18.2",
"language-typescript": "0.3.2",
"language-xml": "0.35.2",
"language-yaml": "0.31.2"
"language-yaml": "0.32.0"
},
"private": true,
"scripts": {

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}]
@@ -57,9 +58,11 @@ class AtomWindow extends EventEmitter {
Object.defineProperty(this.browserWindow, 'loadSettingsJSON', {
get: () => JSON.stringify(Object.assign({
userSettings: this.atomApplication.configFile.get()
}, this.loadSettings)),
configurable: true
userSettings: !this.isSpec
? this.atomApplication.configFile.get()
: 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)