mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Emulate a "filesystem watcher" by subscribing to Atom events
This commit is contained in:
@@ -23,6 +23,144 @@ const WATCHER_STATE = {
|
||||
STOPPING: Symbol('stopping')
|
||||
}
|
||||
|
||||
// Private: Emulate a "filesystem watcher" by subscribing to Atom events like buffers being saved. This will miss
|
||||
// any changes made to files outside of Atom, but it also has no overhead.
|
||||
class AtomBackend {
|
||||
async start (rootPath, eventCallback, errorCallback) {
|
||||
const getRealPath = givenPath => {
|
||||
return new Promise(resolve => {
|
||||
fs.realpath(givenPath, (err, resolvedPath) => {
|
||||
err ? resolve(null) : resolve(resolvedPath)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.subs = new CompositeDisposable()
|
||||
|
||||
this.subs.add(atom.workspace.observeTextEditors(async editor => {
|
||||
let realPath = await getRealPath(editor.getPath())
|
||||
if (!realPath || !realPath.startsWith(rootPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const announce = (type, oldPath) => {
|
||||
const payload = {type, path: realPath}
|
||||
if (oldPath) payload.oldPath = oldPath
|
||||
eventCallback([payload])
|
||||
}
|
||||
|
||||
const buffer = editor.getBuffer()
|
||||
|
||||
this.subs.add(buffer.onDidConflict(() => announce('modified')))
|
||||
this.subs.add(buffer.onDidReload(() => announce('modified')))
|
||||
this.subs.add(buffer.onDidSave(event => {
|
||||
if (event.path === realPath) {
|
||||
announce('modified')
|
||||
} else {
|
||||
const oldPath = realPath
|
||||
realPath = event.path
|
||||
announce('renamed', oldPath)
|
||||
}
|
||||
}))
|
||||
|
||||
this.subs.add(buffer.onDidDelete(() => announce('deleted')))
|
||||
|
||||
this.subs.add(buffer.onDidChangePath(newPath => {
|
||||
if (newPath !== realPath) {
|
||||
const oldPath = realPath
|
||||
realPath = newPath
|
||||
announce('renamed', oldPath)
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
// Giant-ass brittle hack to hook files (and eventually directories) created from the TreeView.
|
||||
const treeViewPackage = await atom.packages.getLoadedPackage('tree-view')
|
||||
if (!treeViewPackage) return
|
||||
await treeViewPackage.activationPromise
|
||||
const treeViewModule = treeViewPackage.mainModule
|
||||
if (!treeViewModule) return
|
||||
const treeView = treeViewModule.getTreeViewInstance()
|
||||
|
||||
const isOpenInEditor = async eventPath => {
|
||||
const openPaths = await Promise.all(
|
||||
atom.workspace.getTextEditors().map(editor => getRealPath(editor.getPath()))
|
||||
)
|
||||
return openPaths.includes(eventPath)
|
||||
}
|
||||
|
||||
this.subs.add(treeView.onFileCreated(async event => {
|
||||
const realPath = await getRealPath(event.path)
|
||||
if (!realPath) return
|
||||
|
||||
eventCallback([{type: 'added', path: realPath}])
|
||||
}))
|
||||
|
||||
this.subs.add(treeView.onEntryDeleted(async event => {
|
||||
const realPath = await getRealPath(event.path)
|
||||
if (!realPath || isOpenInEditor(realPath)) return
|
||||
|
||||
eventCallback([{type: 'deleted', path: realPath}])
|
||||
}))
|
||||
|
||||
this.subs.add(treeView.onEntryMoved(async event => {
|
||||
const [realNewPath, realOldPath] = await Promise.all([
|
||||
getRealPath(event.newPath),
|
||||
getRealPath(event.initialPath)
|
||||
])
|
||||
if (!realNewPath || !realOldPath || isOpenInEditor(realNewPath) || isOpenInEditor(realOldPath)) return
|
||||
|
||||
eventCallback([{type: 'renamed', path: realNewPath, oldPath: realOldPath}])
|
||||
}))
|
||||
}
|
||||
|
||||
async stop () {
|
||||
this.subs && this.subs.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Implement a native watcher by translating events from an NSFW watcher.
|
||||
class NSFWBackend {
|
||||
async start (rootPath, eventCallback, errorCallback) {
|
||||
const handler = events => {
|
||||
eventCallback(events.map(event => {
|
||||
const type = ACTION_MAP.get(event.action) || `unexpected (${event.action})`
|
||||
const payload = {type}
|
||||
|
||||
if (event.file) {
|
||||
payload.path = path.join(event.directory, event.file)
|
||||
} else {
|
||||
payload.oldPath = path.join(event.directory, event.oldFile)
|
||||
payload.path = path.join(event.directory, event.newFile)
|
||||
}
|
||||
|
||||
return payload
|
||||
}))
|
||||
}
|
||||
|
||||
this.watcher = await nsfw(
|
||||
rootPath,
|
||||
handler,
|
||||
{debounceMS: 100, errorCallback}
|
||||
)
|
||||
|
||||
await this.watcher.start()
|
||||
}
|
||||
|
||||
stop () {
|
||||
return this.watcher.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Map configuration settings from the feature flag to backend implementations.
|
||||
const BACKENDS = {
|
||||
atom: AtomBackend,
|
||||
native: NSFWBackend
|
||||
}
|
||||
|
||||
// Private: the backend implementation to fall back to if the config setting is invalid.
|
||||
const DEFAULT_BACKEND = BACKENDS.nsfw
|
||||
|
||||
// Private: Interface with and normalize events from a native OS filesystem watcher.
|
||||
class NativeWatcher {
|
||||
|
||||
@@ -32,9 +170,39 @@ class NativeWatcher {
|
||||
constructor (normalizedPath) {
|
||||
this.normalizedPath = normalizedPath
|
||||
this.emitter = new Emitter()
|
||||
this.subs = new CompositeDisposable()
|
||||
|
||||
this.watcher = null
|
||||
this.backend = null
|
||||
this.state = WATCHER_STATE.STOPPED
|
||||
|
||||
this.onEvents = this.onEvents.bind(this)
|
||||
this.onError = this.onError.bind(this)
|
||||
|
||||
this.subs.add(atom.config.onDidChange('core.fileSystemWatcher', async () => {
|
||||
if (this.state === WATCHER_STATE.STARTING) {
|
||||
// Wait for this watcher to finish starting.
|
||||
await new Promise(resolve => {
|
||||
const sub = this.onDidStart(() => {
|
||||
sub.dispose()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Re-read the config setting in case it's changed again while we were waiting for the watcher
|
||||
// to start.
|
||||
const Backend = this.getCurrentBackend()
|
||||
if (this.state === WATCHER_STATE.RUNNING && !(this.backend instanceof Backend)) {
|
||||
await this.stop()
|
||||
await this.start()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// Private: Read the `core.fileSystemWatcher` setting to determine the filesystem backend to use.
|
||||
getCurrentBackend () {
|
||||
const setting = atom.config.get('core.fileSystemWatcher')
|
||||
return BACKENDS[setting] || DEFAULT_BACKEND
|
||||
}
|
||||
|
||||
// Private: Begin watching for filesystem events.
|
||||
@@ -46,16 +214,10 @@ class NativeWatcher {
|
||||
}
|
||||
this.state = WATCHER_STATE.STARTING
|
||||
|
||||
this.watcher = await nsfw(
|
||||
this.normalizedPath,
|
||||
this.onEvents.bind(this),
|
||||
{
|
||||
debounceMS: 100,
|
||||
errorCallback: this.onError.bind(this)
|
||||
}
|
||||
)
|
||||
const Backend = this.getCurrentBackend()
|
||||
|
||||
await this.watcher.start()
|
||||
this.backend = new Backend()
|
||||
await this.backend.start(this.normalizedPath, this.onEvents, this.onError)
|
||||
|
||||
this.state = WATCHER_STATE.RUNNING
|
||||
this.emitter.emit('did-start')
|
||||
@@ -137,7 +299,7 @@ class NativeWatcher {
|
||||
this.state = WATCHER_STATE.STOPPING
|
||||
this.emitter.emit('will-stop')
|
||||
|
||||
await this.watcher.stop()
|
||||
await this.backend.stop()
|
||||
this.state = WATCHER_STATE.STOPPED
|
||||
|
||||
this.emitter.emit('did-stop')
|
||||
@@ -153,19 +315,7 @@ class NativeWatcher {
|
||||
//
|
||||
// * `events` An Array of filesystem events.
|
||||
onEvents (events) {
|
||||
this.emitter.emit('did-change', events.map(event => {
|
||||
const type = ACTION_MAP.get(event.action) || `unexpected (${event.action})`
|
||||
const payload = {type}
|
||||
|
||||
if (event.file) {
|
||||
payload.path = path.join(event.directory, event.file)
|
||||
} else {
|
||||
payload.oldPath = path.join(event.directory, event.oldFile)
|
||||
payload.path = path.join(event.directory, event.newFile)
|
||||
}
|
||||
|
||||
return payload
|
||||
}))
|
||||
this.emitter.emit('did-change', events)
|
||||
}
|
||||
|
||||
// Private: Callback function invoked by the native watcher when an error occurs.
|
||||
|
||||
Reference in New Issue
Block a user