diff --git a/lib/directory.js b/lib/directory.js index 0fc96d38..aa7c0704 100644 --- a/lib/directory.js +++ b/lib/directory.js @@ -2,7 +2,6 @@ const path = require('path') const _ = require('underscore-plus') const {CompositeDisposable, Emitter} = require('atom') const fs = require('fs-plus') -const PathWatcher = require('pathwatcher') const File = require('./file') const {repoForPath} = require('./helpers') @@ -79,7 +78,6 @@ class Directory { destroy () { this.destroyed = true - this.unwatch() this.subscriptions.dispose() this.emitter.emit('did-destroy') } @@ -239,36 +237,6 @@ class Directory { return false } - // Public: Stop watching this directory for changes. - unwatch () { - if (this.watchSubscription != null) { - this.watchSubscription.close() - this.watchSubscription = null - } - - for (let [key, entry] of this.entries) { - entry.destroy() - this.entries.delete(key) - } - } - - // Public: Watch this directory for changes. - watch () { - if (this.watchSubscription != null) return - try { - this.watchSubscription = PathWatcher.watch(this.path, eventType => { - switch (eventType) { - case 'change': - this.reload() - break - case 'delete': - this.destroy() - break - } - }) - } catch (error) {} - } - getEntries () { let names try { @@ -396,20 +364,17 @@ class Directory { } } - // Public: Collapse this directory and stop watching it. + // Public: Collapse this directory collapse () { this.expansionState.isExpanded = false this.expansionState = this.serializeExpansionState() - this.unwatch() this.emitter.emit('did-collapse') } - // Public: Expand this directory, load its children, and start watching it for - // changes. + // Public: Expand this directory expand () { this.expansionState.isExpanded = true - this.reload() - this.watch() + // this.reload() this.emitter.emit('did-expand') } diff --git a/lib/root-directory.js b/lib/root-directory.js new file mode 100644 index 00000000..886880ab --- /dev/null +++ b/lib/root-directory.js @@ -0,0 +1,202 @@ +const {CompositeDisposable, watchPath} = require('atom') + +const _ = require('underscore-plus') +const fs = require('fs-plus') +const path = require('path') + +const File = require('./file') +const Directory = require('./directory') + +module.exports = +class RootDirectory extends Directory { + constructor ({name, fullPath, symlink, expansionState, isRoot, ignoredNames, useSyncFS, stats}) { + super({name, fullPath, symlink, expansionState, isRoot, ignoredNames, useSyncFS, stats}) + + this.directories = new Map() + this.files = new Map() + + this.disposables = new CompositeDisposable() + this.disposables.add(this.onDidDeletePath(deletedPath => { + // todo + })) + this.disposables.add(this.onDidCreatePath(addedPath => { + // todo + })) + this.disposables.add(this.onDidRenamePath((newPath, oldPath) => { + // todo + })) + + this.loadEntries() + this.watch() + } + + async destroy () { + super.destroy() + await this.unwatch() + this.disposables.dispose() + } + + onDidDeletePath (callback) { + return this.emitter.on('did-delete-path', callback) + } + + onDidRenamePath (callback) { + return this.emitter.on('did-rename-path', callback) + } + + onDidCreatePath (callback) { + return this.emitter.on('did-create-path', callback) + } + + onDidModifyPath (callback) { + return this.emitter.on('did-modify-path', callback) + } + + loadEntries () { + fs.readdir(this.path, (err, names) => { + if (err) { + names = [] + atom.notifications.addWarning(`Could not read files in ${this.path}`, err.message) + } + + names.sort(new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'}).compare) + + for (let name of names) { + const fullPath = path.join(this.path, name) + if (this.isPathIgnored(fullPath)) continue + + fs.lstat(fullPath, (err, stats) => { + if (err) return + + const symlink = stats.isSymbolicLink() + if (symlink) { + // TODO + // stats = fs.statSyncNoException(fullPath) + } + + const statsFlat = _.pick(stats, _.keys(stats)) + for (let key of ['atime', 'birthtime', 'ctime', 'mtime']) { + statsFlat[key] = statsFlat[key] && statsFlat[key].getTime() + } + + if (stats.isDirectory()) { + const expansionState = this.expansionState.entries.get(name) + const directory = new Directory({ + name, + fullPath, + symlink, + expansionState, + ignoredNames: this.ignoredNames, + useSyncFS: this.useSyncFS, + stats: statsFlat + }) + this.directories.set(fullPath, directory) + } else if (stats.isFile()) { + const file = new File({name, fullPath, symlink, ignoredNames: this.ignoredNames, useSyncFS: this.useSyncFS, stats: statsFlat}) + this.files.set(fullPath, file) + } + }) + } + + // return this.sortEntries(directories.concat(files)) + }) + } + + // Public: Watch this project for changes. + async watch () { + if (this.watchSubscription != null) return + try { + this.watchSubscription = await watchPath(this.path, {}, events => { + // let reload = false + for (const event of events) { + console.log(event) + if (this.isPathIgnored(event.path)) continue + const relativePath = path.relative(this.path, event.path) + if (event.action === 'deleted') { + if (event.kind === 'file') { + this.files.get(relativePath).destroy() + } else if (event.kind === 'directory') { + this.directories.get(relativePath).destroy() + } + this.emitter.emit('did-delete-path', event.path) + } else if (event.action === 'renamed') { + // TODO: Will this be emitted if we move the file out of the root? + if (event.kind === 'file') { + this.files.set(relativePath, this.files.get(path.relative(this.path, event.oldPath))) + } else if (event.kind === 'directory') { + this.directories.set(relativePath, this.files.get(path.relative(this.path, event.oldPath))) + } + this.emitter.emit('did-rename-path', event.path, event.oldPath) + } else if (event.action === 'created') { + if (event.kind === 'file') { + fs.lstat(event.path, (err, stats) => { + if (err) return + + const symlink = stats.isSymbolicLink() + if (symlink) { + // TODO + // stats = fs.statSyncNoException(fullPath) + } + + const statsFlat = _.pick(stats, _.keys(stats)) + for (let key of ['atime', 'birthtime', 'ctime', 'mtime']) { + statsFlat[key] = statsFlat[key] && statsFlat[key].getTime() + } + + const file = new File({name, fullPath: event.path, symlink, ignoredNames: this.ignoredNames, useSyncFS: this.useSyncFS, stats: statsFlat}) + this.files.set(relativePath, file) + }) + } else if (event.kind === 'directory') { + fs.lstat(event.path, (err, stats) => { + if (err) return + + const symlink = stats.isSymbolicLink() + if (symlink) { + // TODO + // stats = fs.statSyncNoException(event.path) + } + + const statsFlat = _.pick(stats, _.keys(stats)) + for (let key of ['atime', 'birthtime', 'ctime', 'mtime']) { + statsFlat[key] = statsFlat[key] && statsFlat[key].getTime() + } + + const expansionState = this.expansionState.entries.get(name) + const directory = new Directory({ + name, + fullPath: event.path, + symlink, + expansionState, + ignoredNames: this.ignoredNames, + useSyncFS: this.useSyncFS, + stats: statsFlat + }) + this.directories.set(relativePath, directory) + }) + } + this.emitter.emit('did-create-path', event.path) + } else if (event.action === 'modified') { + this.emitter.emit('did-modify-path', event.path) + } + } + }) + } catch (error) {} // TODO + + this.reload() + } + + // Public: Stop watching this project for changes. + async unwatch () { + if (this.watchSubscription != null) { + await this.watchSubscription + console.log(this.watchSubscription) + this.watchSubscription.dispose() + this.watchSubscription = null + } + + for (let [key, entry] of this.entries) { + entry.destroy() + this.entries.delete(key) + } + } +} diff --git a/lib/tree-view.coffee b/lib/tree-view.coffee index 054578b2..95a92e3a 100644 --- a/lib/tree-view.coffee +++ b/lib/tree-view.coffee @@ -12,6 +12,7 @@ CopyDialog = require './copy-dialog' IgnoredNames = null # Defer requiring until actually needed Directory = require './directory' +RootDirectory = require './root-directory' DirectoryView = require './directory-view' RootDragAndDrop = require './root-drag-and-drop' @@ -332,7 +333,7 @@ class TreeView for key in ["atime", "birthtime", "ctime", "mtime"] stats[key] = stats[key].getTime() - directory = new Directory({ + directory = new RootDirectory({ name: path.basename(projectPath) fullPath: projectPath symlink: false diff --git a/package.json b/package.json index 1d674979..fbe73525 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "dependencies": { "fs-plus": "^3.0.0", "minimatch": "~0.3.0", - "pathwatcher": "^8.0.0", "@atom/temp": "~0.8.4", "underscore-plus": "^1.0.0" }, diff --git a/spec/tree-view-package-spec.coffee b/spec/tree-view-package-spec.coffee index 4105dcaa..752d76dd 100644 --- a/spec/tree-view-package-spec.coffee +++ b/spec/tree-view-package-spec.coffee @@ -61,7 +61,12 @@ describe "TreeView", -> root2 = treeView.roots[1] sampleJs = files[0] sampleTxt = files[1] - expect(root1.directory.watchSubscription).toBeTruthy() + + waitsFor -> + root1.directory.watchSubscription + + runs -> + expect(root1.directory.watchSubscription).toBeTruthy() afterEach -> if treeViewOpenPromise = atom.packages.getActivePackage('tree-view')?.mainModule.treeViewOpenPromise @@ -551,15 +556,25 @@ describe "TreeView", -> grandchild = child.querySelector('li') grandchild.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - expect(root1.directory.watchSubscription).toBeTruthy() - expect(child.directory.watchSubscription).toBeTruthy() - expect(grandchild.directory.watchSubscription).toBeTruthy() + waitsFor -> + root1.directory.watchSubscription - root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + waitsFor -> + child.directory.watchSubscription + + waitsFor -> + grandchild.directory.watchSubscription + + runs -> + expect(root1.directory.watchSubscription).toBeTruthy() + expect(child.directory.watchSubscription).toBeTruthy() + expect(grandchild.directory.watchSubscription).toBeTruthy() + + root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - expect(root1.directory.watchSubscription).toBeFalsy() - expect(child.directory.watchSubscription).toBeFalsy() - expect(grandchild.directory.watchSubscription).toBeFalsy() + expect(root1.directory.watchSubscription).toBeFalsy() + expect(child.directory.watchSubscription).toBeFalsy() + expect(grandchild.directory.watchSubscription).toBeFalsy() describe "when mouse down fires on a file or directory", -> it "selects the entry", ->