From 25b7ddb328ab396e7ed53cad0775fdd0eba5ac74 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 11:58:42 -0400 Subject: [PATCH 01/14] =?UTF-8?q?=E2=98=A0=EF=B8=8F=E2=98=95=20Decaffeinat?= =?UTF-8?q?e=20src/project.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply results of running: ``` $ decaffeinate --keep-commonjs --prefer-const --loose-default-params --loose-for-expressions --loose-for-of --loose-includes' $ standard --fix src/project.js ``` --- src/project.coffee | 565 ----------------------------------- src/project.js | 714 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 714 insertions(+), 565 deletions(-) delete mode 100644 src/project.coffee create mode 100644 src/project.js diff --git a/src/project.coffee b/src/project.coffee deleted file mode 100644 index ab41f9eb3..000000000 --- a/src/project.coffee +++ /dev/null @@ -1,565 +0,0 @@ -path = require 'path' - -_ = require 'underscore-plus' -fs = require 'fs-plus' -{Emitter, Disposable} = require 'event-kit' -TextBuffer = require 'text-buffer' -{watchPath} = require('./path-watcher') - -DefaultDirectoryProvider = require './default-directory-provider' -Model = require './model' -GitRepositoryProvider = require './git-repository-provider' - -# Extended: Represents a project that's opened in Atom. -# -# An instance of this class is always available as the `atom.project` global. -module.exports = -class Project extends Model - ### - Section: Construction and Destruction - ### - - constructor: ({@notificationManager, packageManager, config, @applicationDelegate}) -> - @emitter = new Emitter - @buffers = [] - @rootDirectories = [] - @repositories = [] - @directoryProviders = [] - @defaultDirectoryProvider = new DefaultDirectoryProvider() - @repositoryPromisesByPath = new Map() - @repositoryProviders = [new GitRepositoryProvider(this, config)] - @loadPromisesByPath = {} - @watcherPromisesByPath = {} - @retiredBufferIDs = new Set() - @retiredBufferPaths = new Set() - @consumeServices(packageManager) - - destroyed: -> - buffer.destroy() for buffer in @buffers.slice() - repository?.destroy() for repository in @repositories.slice() - watcher.dispose() for _, watcher in @watcherPromisesByPath - @rootDirectories = [] - @repositories = [] - - reset: (packageManager) -> - @emitter.dispose() - @emitter = new Emitter - - buffer?.destroy() for buffer in @buffers - @buffers = [] - @setPaths([]) - @loadPromisesByPath = {} - @retiredBufferIDs = new Set() - @retiredBufferPaths = new Set() - @consumeServices(packageManager) - - destroyUnretainedBuffers: -> - buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained() - return - - ### - Section: Serialization - ### - - deserialize: (state) -> - @retiredBufferIDs = new Set() - @retiredBufferPaths = new Set() - - handleBufferState = (bufferState) => - bufferState.shouldDestroyOnFileDelete ?= -> atom.config.get('core.closeDeletedFileTabs') - - # Use a little guilty knowledge of the way TextBuffers are serialized. - # This allows TextBuffers that have never been saved (but have filePaths) to be deserialized, but prevents - # TextBuffers backed by files that have been deleted from being saved. - bufferState.mustExist = bufferState.digestWhenLastPersisted isnt false - - TextBuffer.deserialize(bufferState).catch (err) => - @retiredBufferIDs.add(bufferState.id) - @retiredBufferPaths.add(bufferState.filePath) - null - - bufferPromises = (handleBufferState(bufferState) for bufferState in state.buffers) - - Promise.all(bufferPromises).then (buffers) => - @buffers = buffers.filter(Boolean) - @subscribeToBuffer(buffer) for buffer in @buffers - @setPaths(state.paths or [], mustExist: true, exact: true) - - serialize: (options={}) -> - deserializer: 'Project' - paths: @getPaths() - buffers: _.compact(@buffers.map (buffer) -> - if buffer.isRetained() - isUnloading = options.isUnloading is true - buffer.serialize({markerLayers: isUnloading, history: isUnloading}) - ) - - ### - Section: Event Subscription - ### - - # Public: Invoke the given callback when the project paths change. - # - # * `callback` {Function} to be called after the project paths change. - # * `projectPaths` An {Array} of {String} project paths. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangePaths: (callback) -> - @emitter.on 'did-change-paths', callback - - # Public: Invoke the given callback when a text buffer is added to the - # project. - # - # * `callback` {Function} to be called when a text buffer is added. - # * `buffer` A {TextBuffer} item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddBuffer: (callback) -> - @emitter.on 'did-add-buffer', callback - - # Public: Invoke the given callback with all current and future text - # buffers in the project. - # - # * `callback` {Function} to be called with current and future text buffers. - # * `buffer` A {TextBuffer} item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeBuffers: (callback) -> - callback(buffer) for buffer in @getBuffers() - @onDidAddBuffer callback - - # Extended: Invoke a callback when a filesystem change occurs within any open - # project path. - # - # ```js - # const disposable = atom.project.onDidChangeFiles(events => { - # for (const event of events) { - # // "created", "modified", "deleted", or "renamed" - # console.log(`Event action: ${event.type}`) - # - # // absolute path to the filesystem entry that was touched - # console.log(`Event path: ${event.path}`) - # - # if (event.type === 'renamed') { - # console.log(`.. renamed from: ${event.oldPath}`) - # } - # } - # } - # - # disposable.dispose() - # ``` - # - # To watch paths outside of open projects, use the `watchPaths` function instead; see {PathWatcher}. - # - # When writing tests against functionality that uses this method, be sure to wait for the - # {Promise} returned by {getWatcherPromise()} before manipulating the filesystem to ensure that - # the watcher is receiving events. - # - # * `callback` {Function} to be called with batches of filesystem events reported by - # the operating system. - # * `events` An {Array} of objects that describe a batch of filesystem events. - # * `action` {String} describing the filesystem action that occurred. One of `"created"`, - # `"modified"`, `"deleted"`, or `"renamed"`. - # * `path` {String} containing the absolute path to the filesystem entry - # that was acted upon. - # * `oldPath` For rename events, {String} containing the filesystem entry's - # former absolute path. - # - # Returns a {Disposable} to manage this event subscription. - onDidChangeFiles: (callback) -> - @emitter.on 'did-change-files', callback - - ### - Section: Accessing the git repository - ### - - # Public: Get an {Array} of {GitRepository}s associated with the project's - # directories. - # - # This method will be removed in 2.0 because it does synchronous I/O. - # Prefer the following, which evaluates to a {Promise} that resolves to an - # {Array} of {Repository} objects: - # ``` - # Promise.all(atom.project.getDirectories().map( - # atom.project.repositoryForDirectory.bind(atom.project))) - # ``` - getRepositories: -> @repositories - - # Public: Get the repository for a given directory asynchronously. - # - # * `directory` {Directory} for which to get a {Repository}. - # - # Returns a {Promise} that resolves with either: - # * {Repository} if a repository can be created for the given directory - # * `null` if no repository can be created for the given directory. - repositoryForDirectory: (directory) -> - pathForDirectory = directory.getRealPathSync() - promise = @repositoryPromisesByPath.get(pathForDirectory) - unless promise - promises = @repositoryProviders.map (provider) -> - provider.repositoryForDirectory(directory) - promise = Promise.all(promises).then (repositories) => - repo = _.find(repositories, (repo) -> repo?) ? null - - # If no repository is found, remove the entry in for the directory in - # @repositoryPromisesByPath in case some other RepositoryProvider is - # registered in the future that could supply a Repository for the - # directory. - @repositoryPromisesByPath.delete(pathForDirectory) unless repo? - repo?.onDidDestroy?(=> @repositoryPromisesByPath.delete(pathForDirectory)) - repo - @repositoryPromisesByPath.set(pathForDirectory, promise) - promise - - ### - Section: Managing Paths - ### - - # Public: Get an {Array} of {String}s containing the paths of the project's - # directories. - getPaths: -> rootDirectory.getPath() for rootDirectory in @rootDirectories - - # Public: Set the paths of the project's directories. - # - # * `projectPaths` {Array} of {String} paths. - # * `options` An optional {Object} that may contain the following keys: - # * `mustExist` If `true`, throw an Error if any `projectPaths` do not exist. Any remaining `projectPaths` that - # do exist will still be added to the project. Default: `false`. - # * `exact` If `true`, only add a `projectPath` if it names an existing directory. If `false` and any `projectPath` - # is a file or does not exist, its parent directory will be added instead. Default: `false`. - setPaths: (projectPaths, options = {}) -> - repository?.destroy() for repository in @repositories - @rootDirectories = [] - @repositories = [] - - watcher.then((w) -> w.dispose()) for _, watcher in @watcherPromisesByPath - @watcherPromisesByPath = {} - - missingProjectPaths = [] - for projectPath in projectPaths - try - @addPath projectPath, emitEvent: false, mustExist: true, exact: options.exact is true - catch e - if e.missingProjectPaths? - missingProjectPaths.push e.missingProjectPaths... - else - throw e - - @emitter.emit 'did-change-paths', projectPaths - - if options.mustExist is true and missingProjectPaths.length > 0 - err = new Error "One or more project directories do not exist" - err.missingProjectPaths = missingProjectPaths - throw err - - # Public: Add a path to the project's list of root paths - # - # * `projectPath` {String} The path to the directory to add. - # * `options` An optional {Object} that may contain the following keys: - # * `mustExist` If `true`, throw an Error if the `projectPath` does not exist. If `false`, a `projectPath` that does - # not exist is ignored. Default: `false`. - # * `exact` If `true`, only add `projectPath` if it names an existing directory. If `false`, if `projectPath` is a - # a file or does not exist, its parent directory will be added instead. - addPath: (projectPath, options = {}) -> - directory = @getDirectoryForProjectPath(projectPath) - - ok = true - ok = ok and directory.getPath() is projectPath if options.exact is true - ok = ok and directory.existsSync() - - unless ok - if options.mustExist is true - err = new Error "Project directory #{directory} does not exist" - err.missingProjectPaths = [projectPath] - throw err - else - return - - for existingDirectory in @getDirectories() - return if existingDirectory.getPath() is directory.getPath() - - @rootDirectories.push(directory) - @watcherPromisesByPath[directory.getPath()] = watchPath directory.getPath(), {}, (events) => - # Stop event delivery immediately on removal of a rootDirectory, even if its watcher - # promise has yet to resolve at the time of removal - if @rootDirectories.includes directory - @emitter.emit 'did-change-files', events - - for root, watcherPromise in @watcherPromisesByPath - unless @rootDirectories.includes root - watcherPromise.then (watcher) -> watcher.dispose() - - repo = null - for provider in @repositoryProviders - break if repo = provider.repositoryForDirectorySync?(directory) - @repositories.push(repo ? null) - - unless options.emitEvent is false - @emitter.emit 'did-change-paths', @getPaths() - - getDirectoryForProjectPath: (projectPath) -> - directory = null - for provider in @directoryProviders - break if directory = provider.directoryForURISync?(projectPath) - directory ?= @defaultDirectoryProvider.directoryForURISync(projectPath) - directory - - # Extended: Access a {Promise} that resolves when the filesystem watcher associated with a project - # root directory is ready to begin receiving events. - # - # This is especially useful in test cases, where it's important to know that the watcher is - # ready before manipulating the filesystem to produce events. - # - # * `projectPath` {String} One of the project's root directories. - # - # Returns a {Promise} that resolves with the {PathWatcher} associated with this project root - # once it has initialized and is ready to start sending events. The Promise will reject with - # an error instead if `projectPath` is not currently a root directory. - getWatcherPromise: (projectPath) -> - @watcherPromisesByPath[projectPath] or - Promise.reject(new Error("#{projectPath} is not a project root")) - - # Public: remove a path from the project's list of root paths. - # - # * `projectPath` {String} The path to remove. - removePath: (projectPath) -> - # The projectPath may be a URI, in which case it should not be normalized. - unless projectPath in @getPaths() - projectPath = @defaultDirectoryProvider.normalizePath(projectPath) - - indexToRemove = null - for directory, i in @rootDirectories - if directory.getPath() is projectPath - indexToRemove = i - break - - if indexToRemove? - [removedDirectory] = @rootDirectories.splice(indexToRemove, 1) - [removedRepository] = @repositories.splice(indexToRemove, 1) - removedRepository?.destroy() unless removedRepository in @repositories - @watcherPromisesByPath[projectPath]?.then (w) -> w.dispose() - delete @watcherPromisesByPath[projectPath] - @emitter.emit "did-change-paths", @getPaths() - true - else - false - - # Public: Get an {Array} of {Directory}s associated with this project. - getDirectories: -> - @rootDirectories - - resolvePath: (uri) -> - return unless uri - - if uri?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme - uri - else - if fs.isAbsolute(uri) - @defaultDirectoryProvider.normalizePath(fs.resolveHome(uri)) - # TODO: what should we do here when there are multiple directories? - else if projectPath = @getPaths()[0] - @defaultDirectoryProvider.normalizePath(fs.resolveHome(path.join(projectPath, uri))) - else - undefined - - relativize: (fullPath) -> - @relativizePath(fullPath)[1] - - # Public: Get the path to the project directory that contains the given path, - # and the relative path from that project directory to the given path. - # - # * `fullPath` {String} An absolute path. - # - # Returns an {Array} with two elements: - # * `projectPath` The {String} path to the project directory that contains the - # given path, or `null` if none is found. - # * `relativePath` {String} The relative path from the project directory to - # the given path. - relativizePath: (fullPath) -> - result = [null, fullPath] - if fullPath? - for rootDirectory in @rootDirectories - relativePath = rootDirectory.relativize(fullPath) - if relativePath?.length < result[1].length - result = [rootDirectory.getPath(), relativePath] - result - - # Public: Determines whether the given path (real or symbolic) is inside the - # project's directory. - # - # This method does not actually check if the path exists, it just checks their - # locations relative to each other. - # - # ## Examples - # - # Basic operation - # - # ```coffee - # # Project's root directory is /foo/bar - # project.contains('/foo/bar/baz') # => true - # project.contains('/usr/lib/baz') # => false - # ``` - # - # Existence of the path is not required - # - # ```coffee - # # Project's root directory is /foo/bar - # fs.existsSync('/foo/bar/baz') # => false - # project.contains('/foo/bar/baz') # => true - # ``` - # - # * `pathToCheck` {String} path - # - # Returns whether the path is inside the project's root directory. - contains: (pathToCheck) -> - @rootDirectories.some (dir) -> dir.contains(pathToCheck) - - ### - Section: Private - ### - - consumeServices: ({serviceHub}) -> - serviceHub.consume( - 'atom.directory-provider', - '^0.1.0', - (provider) => - @directoryProviders.unshift(provider) - new Disposable => - @directoryProviders.splice(@directoryProviders.indexOf(provider), 1) - ) - - serviceHub.consume( - 'atom.repository-provider', - '^0.1.0', - (provider) => - @repositoryProviders.unshift(provider) - @setPaths(@getPaths()) if null in @repositories - new Disposable => - @repositoryProviders.splice(@repositoryProviders.indexOf(provider), 1) - ) - - # Retrieves all the {TextBuffer}s in the project; that is, the - # buffers for all open files. - # - # Returns an {Array} of {TextBuffer}s. - getBuffers: -> - @buffers.slice() - - # Is the buffer for the given path modified? - isPathModified: (filePath) -> - @findBufferForPath(@resolvePath(filePath))?.isModified() - - findBufferForPath: (filePath) -> - _.find @buffers, (buffer) -> buffer.getPath() is filePath - - findBufferForId: (id) -> - _.find @buffers, (buffer) -> buffer.getId() is id - - # Only to be used in specs - bufferForPathSync: (filePath) -> - absoluteFilePath = @resolvePath(filePath) - return null if @retiredBufferPaths.has absoluteFilePath - existingBuffer = @findBufferForPath(absoluteFilePath) if filePath - existingBuffer ? @buildBufferSync(absoluteFilePath) - - # Only to be used when deserializing - bufferForIdSync: (id) -> - return null if @retiredBufferIDs.has id - existingBuffer = @findBufferForId(id) if id - existingBuffer ? @buildBufferSync() - - # Given a file path, this retrieves or creates a new {TextBuffer}. - # - # If the `filePath` already has a `buffer`, that value is used instead. Otherwise, - # `text` is used as the contents of the new buffer. - # - # * `filePath` A {String} representing a path. If `null`, an "Untitled" buffer is created. - # - # Returns a {Promise} that resolves to the {TextBuffer}. - bufferForPath: (absoluteFilePath) -> - existingBuffer = @findBufferForPath(absoluteFilePath) if absoluteFilePath? - if existingBuffer - Promise.resolve(existingBuffer) - else - @buildBuffer(absoluteFilePath) - - shouldDestroyBufferOnFileDelete: -> - atom.config.get('core.closeDeletedFileTabs') - - # Still needed when deserializing a tokenized buffer - buildBufferSync: (absoluteFilePath) -> - params = {shouldDestroyOnFileDelete: @shouldDestroyBufferOnFileDelete} - if absoluteFilePath? - buffer = TextBuffer.loadSync(absoluteFilePath, params) - else - buffer = new TextBuffer(params) - @addBuffer(buffer) - buffer - - # Given a file path, this sets its {TextBuffer}. - # - # * `absoluteFilePath` A {String} representing a path. - # * `text` The {String} text to use as a buffer. - # - # Returns a {Promise} that resolves to the {TextBuffer}. - buildBuffer: (absoluteFilePath) -> - params = {shouldDestroyOnFileDelete: @shouldDestroyBufferOnFileDelete} - if absoluteFilePath? - promise = - @loadPromisesByPath[absoluteFilePath] ?= - TextBuffer.load(absoluteFilePath, params).catch (error) => - delete @loadPromisesByPath[absoluteFilePath] - throw error - else - promise = Promise.resolve(new TextBuffer(params)) - promise.then (buffer) => - delete @loadPromisesByPath[absoluteFilePath] - @addBuffer(buffer) - buffer - - - addBuffer: (buffer, options={}) -> - @addBufferAtIndex(buffer, @buffers.length, options) - - addBufferAtIndex: (buffer, index, options={}) -> - @buffers.splice(index, 0, buffer) - @subscribeToBuffer(buffer) - @emitter.emit 'did-add-buffer', buffer - buffer - - # Removes a {TextBuffer} association from the project. - # - # Returns the removed {TextBuffer}. - removeBuffer: (buffer) -> - index = @buffers.indexOf(buffer) - @removeBufferAtIndex(index) unless index is -1 - - removeBufferAtIndex: (index, options={}) -> - [buffer] = @buffers.splice(index, 1) - buffer?.destroy() - - eachBuffer: (args...) -> - subscriber = args.shift() if args.length > 1 - callback = args.shift() - - callback(buffer) for buffer in @getBuffers() - if subscriber - subscriber.subscribe this, 'buffer-created', (buffer) -> callback(buffer) - else - @on 'buffer-created', (buffer) -> callback(buffer) - - subscribeToBuffer: (buffer) -> - buffer.onWillSave ({path}) => @applicationDelegate.emitWillSavePath(path) - buffer.onDidSave ({path}) => @applicationDelegate.emitDidSavePath(path) - buffer.onDidDestroy => @removeBuffer(buffer) - buffer.onDidChangePath => - unless @getPaths().length > 0 - @setPaths([path.dirname(buffer.getPath())]) - buffer.onWillThrowWatchError ({error, handle}) => - handle() - @notificationManager.addWarning """ - Unable to read file after file `#{error.eventType}` event. - Make sure you have permission to access `#{buffer.getPath()}`. - """, - detail: error.message - dismissable: true diff --git a/src/project.js b/src/project.js new file mode 100644 index 000000000..448f2b87c --- /dev/null +++ b/src/project.js @@ -0,0 +1,714 @@ +/* + * decaffeinate suggestions: + * DS001: Remove Babel/TypeScript constructor workaround + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS104: Avoid inline assignments + * DS204: Change includes calls to have a more natural evaluation order + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Project +const path = require('path') + +let _ = require('underscore-plus') +const fs = require('fs-plus') +const {Emitter, Disposable} = require('event-kit') +const TextBuffer = require('text-buffer') +const {watchPath} = require('./path-watcher') + +const DefaultDirectoryProvider = require('./default-directory-provider') +const Model = require('./model') +const GitRepositoryProvider = require('./git-repository-provider') + +// Extended: Represents a project that's opened in Atom. +// +// An instance of this class is always available as the `atom.project` global. +module.exports = +(Project = class Project extends Model { + /* + Section: Construction and Destruction + */ + + constructor ({notificationManager, packageManager, config, applicationDelegate}) { + { + // Hack: trick Babel/TypeScript into allowing this before super. + if (false) { super() } + let thisFn = (() => { this }).toString() + let thisName = thisFn.slice(thisFn.indexOf('{') + 1, thisFn.indexOf(';')).trim() + eval(`${thisName} = this;`) + } + this.notificationManager = notificationManager + this.applicationDelegate = applicationDelegate + this.emitter = new Emitter() + this.buffers = [] + this.rootDirectories = [] + this.repositories = [] + this.directoryProviders = [] + this.defaultDirectoryProvider = new DefaultDirectoryProvider() + this.repositoryPromisesByPath = new Map() + this.repositoryProviders = [new GitRepositoryProvider(this, config)] + this.loadPromisesByPath = {} + this.watcherPromisesByPath = {} + this.retiredBufferIDs = new Set() + this.retiredBufferPaths = new Set() + this.consumeServices(packageManager) + } + + destroyed () { + for (let buffer of this.buffers.slice()) { buffer.destroy() } + for (let repository of this.repositories.slice()) { + if (repository != null) { + repository.destroy() + } + } + for (let watcher = 0; watcher < this.watcherPromisesByPath.length; watcher++) { _ = this.watcherPromisesByPath[watcher]; watcher.dispose() } + this.rootDirectories = [] + return this.repositories = [] + } + + reset (packageManager) { + this.emitter.dispose() + this.emitter = new Emitter() + + for (let buffer of this.buffers) { + if (buffer != null) { + buffer.destroy() + } + } + this.buffers = [] + this.setPaths([]) + this.loadPromisesByPath = {} + this.retiredBufferIDs = new Set() + this.retiredBufferPaths = new Set() + return this.consumeServices(packageManager) + } + + destroyUnretainedBuffers () { + for (let buffer of this.getBuffers()) { if (!buffer.isRetained()) { buffer.destroy() } } + } + + /* + Section: Serialization + */ + + deserialize (state) { + let bufferState + this.retiredBufferIDs = new Set() + this.retiredBufferPaths = new Set() + + const handleBufferState = bufferState => { + if (bufferState.shouldDestroyOnFileDelete == null) { bufferState.shouldDestroyOnFileDelete = () => atom.config.get('core.closeDeletedFileTabs') } + + // Use a little guilty knowledge of the way TextBuffers are serialized. + // This allows TextBuffers that have never been saved (but have filePaths) to be deserialized, but prevents + // TextBuffers backed by files that have been deleted from being saved. + bufferState.mustExist = bufferState.digestWhenLastPersisted !== false + + return TextBuffer.deserialize(bufferState).catch(err => { + this.retiredBufferIDs.add(bufferState.id) + this.retiredBufferPaths.add(bufferState.filePath) + return null + }) + } + + const bufferPromises = ((() => { + const result = [] + for (bufferState of state.buffers) { + result.push(handleBufferState(bufferState)) + } + return result + })()) + + return Promise.all(bufferPromises).then(buffers => { + this.buffers = buffers.filter(Boolean) + for (let buffer of this.buffers) { this.subscribeToBuffer(buffer) } + return this.setPaths(state.paths || [], {mustExist: true, exact: true}) + }) + } + + serialize (options = {}) { + return { + deserializer: 'Project', + paths: this.getPaths(), + buffers: _.compact(this.buffers.map(function (buffer) { + if (buffer.isRetained()) { + const isUnloading = options.isUnloading === true + return buffer.serialize({markerLayers: isUnloading, history: isUnloading}) + } + })) + } + } + + /* + Section: Event Subscription + */ + + // Public: Invoke the given callback when the project paths change. + // + // * `callback` {Function} to be called after the project paths change. + // * `projectPaths` An {Array} of {String} project paths. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangePaths (callback) { + return this.emitter.on('did-change-paths', callback) + } + + // Public: Invoke the given callback when a text buffer is added to the + // project. + // + // * `callback` {Function} to be called when a text buffer is added. + // * `buffer` A {TextBuffer} item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddBuffer (callback) { + return this.emitter.on('did-add-buffer', callback) + } + + // Public: Invoke the given callback with all current and future text + // buffers in the project. + // + // * `callback` {Function} to be called with current and future text buffers. + // * `buffer` A {TextBuffer} item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeBuffers (callback) { + for (let buffer of this.getBuffers()) { callback(buffer) } + return this.onDidAddBuffer(callback) + } + + // Extended: Invoke a callback when a filesystem change occurs within any open + // project path. + // + // ```js + // const disposable = atom.project.onDidChangeFiles(events => { + // for (const event of events) { + // // "created", "modified", "deleted", or "renamed" + // console.log(`Event action: ${event.type}`) + // + // // absolute path to the filesystem entry that was touched + // console.log(`Event path: ${event.path}`) + // + // if (event.type === 'renamed') { + // console.log(`.. renamed from: ${event.oldPath}`) + // } + // } + // } + // + // disposable.dispose() + // ``` + // + // To watch paths outside of open projects, use the `watchPaths` function instead; see {PathWatcher}. + // + // When writing tests against functionality that uses this method, be sure to wait for the + // {Promise} returned by {getWatcherPromise()} before manipulating the filesystem to ensure that + // the watcher is receiving events. + // + // * `callback` {Function} to be called with batches of filesystem events reported by + // the operating system. + // * `events` An {Array} of objects that describe a batch of filesystem events. + // * `action` {String} describing the filesystem action that occurred. One of `"created"`, + // `"modified"`, `"deleted"`, or `"renamed"`. + // * `path` {String} containing the absolute path to the filesystem entry + // that was acted upon. + // * `oldPath` For rename events, {String} containing the filesystem entry's + // former absolute path. + // + // Returns a {Disposable} to manage this event subscription. + onDidChangeFiles (callback) { + return this.emitter.on('did-change-files', callback) + } + + /* + Section: Accessing the git repository + */ + + // Public: Get an {Array} of {GitRepository}s associated with the project's + // directories. + // + // This method will be removed in 2.0 because it does synchronous I/O. + // Prefer the following, which evaluates to a {Promise} that resolves to an + // {Array} of {Repository} objects: + // ``` + // Promise.all(atom.project.getDirectories().map( + // atom.project.repositoryForDirectory.bind(atom.project))) + // ``` + getRepositories () { return this.repositories } + + // Public: Get the repository for a given directory asynchronously. + // + // * `directory` {Directory} for which to get a {Repository}. + // + // Returns a {Promise} that resolves with either: + // * {Repository} if a repository can be created for the given directory + // * `null` if no repository can be created for the given directory. + repositoryForDirectory (directory) { + const pathForDirectory = directory.getRealPathSync() + let promise = this.repositoryPromisesByPath.get(pathForDirectory) + if (!promise) { + const promises = this.repositoryProviders.map(provider => provider.repositoryForDirectory(directory)) + promise = Promise.all(promises).then(repositories => { + let left + const repo = (left = _.find(repositories, repo => repo != null)) != null ? left : null + + // If no repository is found, remove the entry in for the directory in + // @repositoryPromisesByPath in case some other RepositoryProvider is + // registered in the future that could supply a Repository for the + // directory. + if (repo == null) { this.repositoryPromisesByPath.delete(pathForDirectory) } + __guardMethod__(repo, 'onDidDestroy', o => o.onDidDestroy(() => this.repositoryPromisesByPath.delete(pathForDirectory))) + return repo + }) + this.repositoryPromisesByPath.set(pathForDirectory, promise) + } + return promise + } + + /* + Section: Managing Paths + */ + + // Public: Get an {Array} of {String}s containing the paths of the project's + // directories. + getPaths () { return this.rootDirectories.map((rootDirectory) => rootDirectory.getPath()) } + + // Public: Set the paths of the project's directories. + // + // * `projectPaths` {Array} of {String} paths. + // * `options` An optional {Object} that may contain the following keys: + // * `mustExist` If `true`, throw an Error if any `projectPaths` do not exist. Any remaining `projectPaths` that + // do exist will still be added to the project. Default: `false`. + // * `exact` If `true`, only add a `projectPath` if it names an existing directory. If `false` and any `projectPath` + // is a file or does not exist, its parent directory will be added instead. Default: `false`. + setPaths (projectPaths, options = {}) { + for (let repository of this.repositories) { + if (repository != null) { + repository.destroy() + } + } + this.rootDirectories = [] + this.repositories = [] + + for (let watcher = 0; watcher < this.watcherPromisesByPath.length; watcher++) { _ = this.watcherPromisesByPath[watcher]; watcher.then(w => w.dispose()) } + this.watcherPromisesByPath = {} + + const missingProjectPaths = [] + for (let projectPath of projectPaths) { + try { + this.addPath(projectPath, {emitEvent: false, mustExist: true, exact: options.exact === true}) + } catch (e) { + if (e.missingProjectPaths != null) { + missingProjectPaths.push(...Array.from(e.missingProjectPaths || [])) + } else { + throw e + } + } + } + + this.emitter.emit('did-change-paths', projectPaths) + + if ((options.mustExist === true) && (missingProjectPaths.length > 0)) { + const err = new Error('One or more project directories do not exist') + err.missingProjectPaths = missingProjectPaths + throw err + } + } + + // Public: Add a path to the project's list of root paths + // + // * `projectPath` {String} The path to the directory to add. + // * `options` An optional {Object} that may contain the following keys: + // * `mustExist` If `true`, throw an Error if the `projectPath` does not exist. If `false`, a `projectPath` that does + // not exist is ignored. Default: `false`. + // * `exact` If `true`, only add `projectPath` if it names an existing directory. If `false`, if `projectPath` is a + // 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 = ok && (directory.getPath() === projectPath) } + ok = ok && directory.existsSync() + + if (!ok) { + if (options.mustExist === true) { + const err = new Error(`Project directory ${directory} does not exist`) + err.missingProjectPaths = [projectPath] + throw err + } else { + return + } + } + + for (let existingDirectory of this.getDirectories()) { + if (existingDirectory.getPath() === directory.getPath()) { return } + } + + this.rootDirectories.push(directory) + this.watcherPromisesByPath[directory.getPath()] = watchPath(directory.getPath(), {}, events => { + // Stop event delivery immediately on removal of a rootDirectory, even if its watcher + // promise has yet to resolve at the time of removal + if (this.rootDirectories.includes(directory)) { + return this.emitter.emit('did-change-files', events) + } + }) + + for (let watcherPromise = 0; watcherPromise < this.watcherPromisesByPath.length; watcherPromise++) { + const root = this.watcherPromisesByPath[watcherPromise] + if (!this.rootDirectories.includes(root)) { + watcherPromise.then(watcher => watcher.dispose()) + } + } + + let repo = null + for (let provider of this.repositoryProviders) { + if (repo = typeof provider.repositoryForDirectorySync === 'function' ? provider.repositoryForDirectorySync(directory) : undefined) { break } + } + this.repositories.push(repo != null ? repo : null) + + if (options.emitEvent !== false) { + return this.emitter.emit('did-change-paths', this.getPaths()) + } + } + + getDirectoryForProjectPath (projectPath) { + let directory = null + for (let provider of this.directoryProviders) { + if (directory = typeof provider.directoryForURISync === 'function' ? provider.directoryForURISync(projectPath) : undefined) { break } + } + if (directory == null) { directory = this.defaultDirectoryProvider.directoryForURISync(projectPath) } + return directory + } + + // Extended: Access a {Promise} that resolves when the filesystem watcher associated with a project + // root directory is ready to begin receiving events. + // + // This is especially useful in test cases, where it's important to know that the watcher is + // ready before manipulating the filesystem to produce events. + // + // * `projectPath` {String} One of the project's root directories. + // + // Returns a {Promise} that resolves with the {PathWatcher} associated with this project root + // once it has initialized and is ready to start sending events. The Promise will reject with + // an error instead if `projectPath` is not currently a root directory. + getWatcherPromise (projectPath) { + return this.watcherPromisesByPath[projectPath] || + Promise.reject(new Error(`${projectPath} is not a project root`)) + } + + // Public: remove a path from the project's list of root paths. + // + // * `projectPath` {String} The path to remove. + removePath (projectPath) { + // The projectPath may be a URI, in which case it should not be normalized. + let needle + if ((needle = projectPath, !this.getPaths().includes(needle))) { + projectPath = this.defaultDirectoryProvider.normalizePath(projectPath) + } + + let indexToRemove = null + for (let i = 0; i < this.rootDirectories.length; i++) { + const directory = this.rootDirectories[i] + if (directory.getPath() === projectPath) { + indexToRemove = i + break + } + } + + if (indexToRemove != null) { + const [removedDirectory] = Array.from(this.rootDirectories.splice(indexToRemove, 1)) + const [removedRepository] = Array.from(this.repositories.splice(indexToRemove, 1)) + if (!this.repositories.includes(removedRepository)) { + if (removedRepository != null) { + removedRepository.destroy() + } + } + if (this.watcherPromisesByPath[projectPath] != null) { + this.watcherPromisesByPath[projectPath].then(w => w.dispose()) + } + delete this.watcherPromisesByPath[projectPath] + this.emitter.emit('did-change-paths', this.getPaths()) + return true + } else { + return false + } + } + + // Public: Get an {Array} of {Directory}s associated with this project. + getDirectories () { + return this.rootDirectories + } + + resolvePath (uri) { + if (!uri) { return } + + if ((uri != null ? uri.match(/[A-Za-z0-9+-.]+:\/\//) : undefined)) { // leave path alone if it has a scheme + return uri + } else { + let projectPath + if (fs.isAbsolute(uri)) { + return this.defaultDirectoryProvider.normalizePath(fs.resolveHome(uri)) + // TODO: what should we do here when there are multiple directories? + } else if ((projectPath = this.getPaths()[0])) { + return this.defaultDirectoryProvider.normalizePath(fs.resolveHome(path.join(projectPath, uri))) + } else { + return undefined + } + } + } + + relativize (fullPath) { + return this.relativizePath(fullPath)[1] + } + + // Public: Get the path to the project directory that contains the given path, + // and the relative path from that project directory to the given path. + // + // * `fullPath` {String} An absolute path. + // + // Returns an {Array} with two elements: + // * `projectPath` The {String} path to the project directory that contains the + // given path, or `null` if none is found. + // * `relativePath` {String} The relative path from the project directory to + // the given path. + relativizePath (fullPath) { + let result = [null, fullPath] + if (fullPath != null) { + for (let rootDirectory of this.rootDirectories) { + const relativePath = rootDirectory.relativize(fullPath) + if ((relativePath != null ? relativePath.length : undefined) < result[1].length) { + result = [rootDirectory.getPath(), relativePath] + } + } + } + return result + } + + // Public: Determines whether the given path (real or symbolic) is inside the + // project's directory. + // + // This method does not actually check if the path exists, it just checks their + // locations relative to each other. + // + // ## Examples + // + // Basic operation + // + // ```coffee + // # Project's root directory is /foo/bar + // project.contains('/foo/bar/baz') # => true + // project.contains('/usr/lib/baz') # => false + // ``` + // + // Existence of the path is not required + // + // ```coffee + // # Project's root directory is /foo/bar + // fs.existsSync('/foo/bar/baz') # => false + // project.contains('/foo/bar/baz') # => true + // ``` + // + // * `pathToCheck` {String} path + // + // Returns whether the path is inside the project's root directory. + contains (pathToCheck) { + return this.rootDirectories.some(dir => dir.contains(pathToCheck)) + } + + /* + Section: Private + */ + + consumeServices ({serviceHub}) { + serviceHub.consume( + 'atom.directory-provider', + '^0.1.0', + provider => { + this.directoryProviders.unshift(provider) + return new Disposable(() => { + return this.directoryProviders.splice(this.directoryProviders.indexOf(provider), 1) + }) + }) + + return serviceHub.consume( + 'atom.repository-provider', + '^0.1.0', + provider => { + this.repositoryProviders.unshift(provider) + if (this.repositories.includes(null)) { this.setPaths(this.getPaths()) } + return new Disposable(() => { + return this.repositoryProviders.splice(this.repositoryProviders.indexOf(provider), 1) + }) + }) + } + + // Retrieves all the {TextBuffer}s in the project; that is, the + // buffers for all open files. + // + // Returns an {Array} of {TextBuffer}s. + getBuffers () { + return this.buffers.slice() + } + + // Is the buffer for the given path modified? + isPathModified (filePath) { + return __guard__(this.findBufferForPath(this.resolvePath(filePath)), x => x.isModified()) + } + + findBufferForPath (filePath) { + return _.find(this.buffers, buffer => buffer.getPath() === filePath) + } + + findBufferForId (id) { + return _.find(this.buffers, buffer => buffer.getId() === id) + } + + // Only to be used in specs + bufferForPathSync (filePath) { + let existingBuffer + const absoluteFilePath = this.resolvePath(filePath) + if (this.retiredBufferPaths.has(absoluteFilePath)) { return null } + if (filePath) { existingBuffer = this.findBufferForPath(absoluteFilePath) } + return existingBuffer != null ? existingBuffer : this.buildBufferSync(absoluteFilePath) + } + + // Only to be used when deserializing + bufferForIdSync (id) { + let existingBuffer + if (this.retiredBufferIDs.has(id)) { return null } + if (id) { existingBuffer = this.findBufferForId(id) } + return existingBuffer != null ? existingBuffer : this.buildBufferSync() + } + + // Given a file path, this retrieves or creates a new {TextBuffer}. + // + // If the `filePath` already has a `buffer`, that value is used instead. Otherwise, + // `text` is used as the contents of the new buffer. + // + // * `filePath` A {String} representing a path. If `null`, an "Untitled" buffer is created. + // + // Returns a {Promise} that resolves to the {TextBuffer}. + bufferForPath (absoluteFilePath) { + let existingBuffer + if (absoluteFilePath != null) { existingBuffer = this.findBufferForPath(absoluteFilePath) } + if (existingBuffer) { + return Promise.resolve(existingBuffer) + } else { + return this.buildBuffer(absoluteFilePath) + } + } + + shouldDestroyBufferOnFileDelete () { + return atom.config.get('core.closeDeletedFileTabs') + } + + // Still needed when deserializing a tokenized buffer + buildBufferSync (absoluteFilePath) { + let buffer + const params = {shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete} + if (absoluteFilePath != null) { + buffer = TextBuffer.loadSync(absoluteFilePath, params) + } else { + buffer = new TextBuffer(params) + } + this.addBuffer(buffer) + return buffer + } + + // Given a file path, this sets its {TextBuffer}. + // + // * `absoluteFilePath` A {String} representing a path. + // * `text` The {String} text to use as a buffer. + // + // Returns a {Promise} that resolves to the {TextBuffer}. + buildBuffer (absoluteFilePath) { + let promise + const params = {shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete} + if (absoluteFilePath != null) { + promise = + this.loadPromisesByPath[absoluteFilePath] != null ? this.loadPromisesByPath[absoluteFilePath] : (this.loadPromisesByPath[absoluteFilePath] = + TextBuffer.load(absoluteFilePath, params).catch(error => { + delete this.loadPromisesByPath[absoluteFilePath] + throw error + })) + } else { + promise = Promise.resolve(new TextBuffer(params)) + } + return promise.then(buffer => { + delete this.loadPromisesByPath[absoluteFilePath] + this.addBuffer(buffer) + return buffer + }) + } + + addBuffer (buffer, options = {}) { + return this.addBufferAtIndex(buffer, this.buffers.length, options) + } + + addBufferAtIndex (buffer, index, options = {}) { + this.buffers.splice(index, 0, buffer) + this.subscribeToBuffer(buffer) + this.emitter.emit('did-add-buffer', buffer) + return buffer + } + + // Removes a {TextBuffer} association from the project. + // + // Returns the removed {TextBuffer}. + removeBuffer (buffer) { + const index = this.buffers.indexOf(buffer) + if (index !== -1) { return this.removeBufferAtIndex(index) } + } + + removeBufferAtIndex (index, options = {}) { + const [buffer] = Array.from(this.buffers.splice(index, 1)) + return (buffer != null ? buffer.destroy() : undefined) + } + + eachBuffer (...args) { + let subscriber + if (args.length > 1) { subscriber = args.shift() } + const callback = args.shift() + + for (let buffer of this.getBuffers()) { callback(buffer) } + if (subscriber) { + return subscriber.subscribe(this, 'buffer-created', buffer => callback(buffer)) + } else { + return this.on('buffer-created', buffer => callback(buffer)) + } + } + + subscribeToBuffer (buffer) { + buffer.onWillSave(({path}) => this.applicationDelegate.emitWillSavePath(path)) + buffer.onDidSave(({path}) => this.applicationDelegate.emitDidSavePath(path)) + buffer.onDidDestroy(() => this.removeBuffer(buffer)) + buffer.onDidChangePath(() => { + if (!(this.getPaths().length > 0)) { + return this.setPaths([path.dirname(buffer.getPath())]) + } + }) + return buffer.onWillThrowWatchError(({error, handle}) => { + handle() + return this.notificationManager.addWarning(`\ +Unable to read file after file \`${error.eventType}\` event. +Make sure you have permission to access \`${buffer.getPath()}\`.\ +`, { + detail: error.message, + dismissable: true +} + ) + }) + } +}) + +function __guardMethod__ (obj, methodName, transform) { + if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { + return transform(obj, methodName) + } else { + return undefined + } +} +function __guard__ (value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined +} From cab8824aaedee5ab54333633b05b389402490d32 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:47:00 -0400 Subject: [PATCH 02/14] :necktie: Fix linter violations --- src/project.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/project.js b/src/project.js index 448f2b87c..4a481f5e6 100644 --- a/src/project.js +++ b/src/project.js @@ -10,7 +10,6 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -let Project const path = require('path') let _ = require('underscore-plus') @@ -27,7 +26,7 @@ const GitRepositoryProvider = require('./git-repository-provider') // // An instance of this class is always available as the `atom.project` global. module.exports = -(Project = class Project extends Model { +class Project extends Model { /* Section: Construction and Destruction */ @@ -66,7 +65,7 @@ module.exports = } for (let watcher = 0; watcher < this.watcherPromisesByPath.length; watcher++) { _ = this.watcherPromisesByPath[watcher]; watcher.dispose() } this.rootDirectories = [] - return this.repositories = [] + this.repositories = [] } reset (packageManager) { @@ -107,7 +106,7 @@ module.exports = // TextBuffers backed by files that have been deleted from being saved. bufferState.mustExist = bufferState.digestWhenLastPersisted !== false - return TextBuffer.deserialize(bufferState).catch(err => { + return TextBuffer.deserialize(bufferState).catch((_) => { this.retiredBufferIDs.add(bufferState.id) this.retiredBufferPaths.add(bufferState.filePath) return null @@ -363,7 +362,10 @@ module.exports = let repo = null for (let provider of this.repositoryProviders) { - if (repo = typeof provider.repositoryForDirectorySync === 'function' ? provider.repositoryForDirectorySync(directory) : undefined) { break } + if (provider.repositoryForDirectorySync) { + repo = provider.repositoryForDirectorySync(directory) + } + if (repo) { break } } this.repositories.push(repo != null ? repo : null) @@ -375,9 +377,14 @@ module.exports = getDirectoryForProjectPath (projectPath) { let directory = null for (let provider of this.directoryProviders) { - if (directory = typeof provider.directoryForURISync === 'function' ? provider.directoryForURISync(projectPath) : undefined) { break } + if (typeof provider.directoryForURISync === 'function') { + directory = provider.directoryForURISync(projectPath) + if (directory) break + } + } + if (directory == null) { + directory = this.defaultDirectoryProvider.directoryForURISync(projectPath) } - if (directory == null) { directory = this.defaultDirectoryProvider.directoryForURISync(projectPath) } return directory } @@ -417,7 +424,7 @@ module.exports = } if (indexToRemove != null) { - const [removedDirectory] = Array.from(this.rootDirectories.splice(indexToRemove, 1)) + this.rootDirectories.splice(indexToRemove, 1) const [removedRepository] = Array.from(this.repositories.splice(indexToRemove, 1)) if (!this.repositories.includes(removedRepository)) { if (removedRepository != null) { @@ -700,7 +707,7 @@ Make sure you have permission to access \`${buffer.getPath()}\`.\ ) }) } -}) +} function __guardMethod__ (obj, methodName, transform) { if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { From dd6359b507583c6bc4e5c021a2f4dd0a61365f1c Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:00:09 -0400 Subject: [PATCH 03/14] Remove Babel/TypeScript constructor workaround --- src/project.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/project.js b/src/project.js index 4a481f5e6..1909b0b94 100644 --- a/src/project.js +++ b/src/project.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS001: Remove Babel/TypeScript constructor workaround * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ @@ -32,13 +31,7 @@ class Project extends Model { */ constructor ({notificationManager, packageManager, config, applicationDelegate}) { - { - // Hack: trick Babel/TypeScript into allowing this before super. - if (false) { super() } - let thisFn = (() => { this }).toString() - let thisName = thisFn.slice(thisFn.indexOf('{') + 1, thisFn.indexOf(';')).trim() - eval(`${thisName} = this;`) - } + super() this.notificationManager = notificationManager this.applicationDelegate = applicationDelegate this.emitter = new Emitter() From 8f40af16a96e314d722f925cd362cbfc2533e60b Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:10:32 -0400 Subject: [PATCH 04/14] :art: --- src/project.js | 85 ++++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/src/project.js b/src/project.js index 1909b0b94..32538fc02 100644 --- a/src/project.js +++ b/src/project.js @@ -52,9 +52,7 @@ class Project extends Model { destroyed () { for (let buffer of this.buffers.slice()) { buffer.destroy() } for (let repository of this.repositories.slice()) { - if (repository != null) { - repository.destroy() - } + if (repository != null) repository.destroy() } for (let watcher = 0; watcher < this.watcherPromisesByPath.length; watcher++) { _ = this.watcherPromisesByPath[watcher]; watcher.dispose() } this.rootDirectories = [] @@ -66,9 +64,7 @@ class Project extends Model { this.emitter = new Emitter() for (let buffer of this.buffers) { - if (buffer != null) { - buffer.destroy() - } + if (buffer != null) buffer.destroy() } this.buffers = [] this.setPaths([]) @@ -79,7 +75,9 @@ class Project extends Model { } destroyUnretainedBuffers () { - for (let buffer of this.getBuffers()) { if (!buffer.isRetained()) { buffer.destroy() } } + for (let buffer of this.getBuffers()) { + if (!buffer.isRetained()) buffer.destroy() + } } /* @@ -91,8 +89,10 @@ class Project extends Model { this.retiredBufferIDs = new Set() this.retiredBufferPaths = new Set() - const handleBufferState = bufferState => { - if (bufferState.shouldDestroyOnFileDelete == null) { bufferState.shouldDestroyOnFileDelete = () => atom.config.get('core.closeDeletedFileTabs') } + const handleBufferState = (bufferState) => { + if (bufferState.shouldDestroyOnFileDelete == null) { + bufferState.shouldDestroyOnFileDelete = () => atom.config.get('core.closeDeletedFileTabs') + } // Use a little guilty knowledge of the way TextBuffers are serialized. // This allows TextBuffers that have never been saved (but have filePaths) to be deserialized, but prevents @@ -116,7 +116,9 @@ class Project extends Model { return Promise.all(bufferPromises).then(buffers => { this.buffers = buffers.filter(Boolean) - for (let buffer of this.buffers) { this.subscribeToBuffer(buffer) } + for (let buffer of this.buffers) { + this.subscribeToBuffer(buffer) + } return this.setPaths(state.paths || [], {mustExist: true, exact: true}) }) } @@ -227,7 +229,9 @@ class Project extends Model { // Promise.all(atom.project.getDirectories().map( // atom.project.repositoryForDirectory.bind(atom.project))) // ``` - getRepositories () { return this.repositories } + getRepositories () { + return this.repositories + } // Public: Get the repository for a given directory asynchronously. // @@ -245,7 +249,7 @@ class Project extends Model { let left const repo = (left = _.find(repositories, repo => repo != null)) != null ? left : null - // If no repository is found, remove the entry in for the directory in + // If no repository is found, remove the entry for the directory in // @repositoryPromisesByPath in case some other RepositoryProvider is // registered in the future that could supply a Repository for the // directory. @@ -264,7 +268,9 @@ class Project extends Model { // Public: Get an {Array} of {String}s containing the paths of the project's // directories. - getPaths () { return this.rootDirectories.map((rootDirectory) => rootDirectory.getPath()) } + getPaths () { + return this.rootDirectories.map((rootDirectory) => rootDirectory.getPath()) + } // Public: Set the paths of the project's directories. // @@ -276,9 +282,7 @@ class Project extends Model { // is a file or does not exist, its parent directory will be added instead. Default: `false`. setPaths (projectPaths, options = {}) { for (let repository of this.repositories) { - if (repository != null) { - repository.destroy() - } + if (repository != null) repository.destroy() } this.rootDirectories = [] this.repositories = [] @@ -320,7 +324,9 @@ class Project extends Model { const directory = this.getDirectoryForProjectPath(projectPath) let ok = true - if (options.exact === true) { ok = ok && (directory.getPath() === projectPath) } + if (options.exact === true) { + ok = (directory.getPath() === projectPath) + } ok = ok && directory.existsSync() if (!ok) { @@ -420,9 +426,7 @@ class Project extends Model { this.rootDirectories.splice(indexToRemove, 1) const [removedRepository] = Array.from(this.repositories.splice(indexToRemove, 1)) if (!this.repositories.includes(removedRepository)) { - if (removedRepository != null) { - removedRepository.destroy() - } + if (removedRepository) removedRepository.destroy() } if (this.watcherPromisesByPath[projectPath] != null) { this.watcherPromisesByPath[projectPath].then(w => w.dispose()) @@ -566,17 +570,19 @@ class Project extends Model { // Only to be used in specs bufferForPathSync (filePath) { - let existingBuffer const absoluteFilePath = this.resolvePath(filePath) if (this.retiredBufferPaths.has(absoluteFilePath)) { return null } + + let existingBuffer if (filePath) { existingBuffer = this.findBufferForPath(absoluteFilePath) } return existingBuffer != null ? existingBuffer : this.buildBufferSync(absoluteFilePath) } // Only to be used when deserializing bufferForIdSync (id) { - let existingBuffer if (this.retiredBufferIDs.has(id)) { return null } + + let existingBuffer if (id) { existingBuffer = this.findBufferForId(id) } return existingBuffer != null ? existingBuffer : this.buildBufferSync() } @@ -605,8 +611,9 @@ class Project extends Model { // Still needed when deserializing a tokenized buffer buildBufferSync (absoluteFilePath) { - let buffer const params = {shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete} + + let buffer if (absoluteFilePath != null) { buffer = TextBuffer.loadSync(absoluteFilePath, params) } else { @@ -623,15 +630,18 @@ class Project extends Model { // // Returns a {Promise} that resolves to the {TextBuffer}. buildBuffer (absoluteFilePath) { - let promise const params = {shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete} + + let promise if (absoluteFilePath != null) { - promise = - this.loadPromisesByPath[absoluteFilePath] != null ? this.loadPromisesByPath[absoluteFilePath] : (this.loadPromisesByPath[absoluteFilePath] = - TextBuffer.load(absoluteFilePath, params).catch(error => { - delete this.loadPromisesByPath[absoluteFilePath] - throw error - })) + if (this.loadPromisesByPath[absoluteFilePath] == null) { + this.loadPromisesByPath[absoluteFilePath] = + TextBuffer.load(absoluteFilePath, params).catch(error => { + delete this.loadPromisesByPath[absoluteFilePath] + throw error + }) + } + promise = this.loadPromisesByPath[absoluteFilePath] } else { promise = Promise.resolve(new TextBuffer(params)) } @@ -690,14 +700,13 @@ class Project extends Model { }) return buffer.onWillThrowWatchError(({error, handle}) => { handle() - return this.notificationManager.addWarning(`\ -Unable to read file after file \`${error.eventType}\` event. -Make sure you have permission to access \`${buffer.getPath()}\`.\ -`, { - detail: error.message, - dismissable: true -} - ) + const message = + `Unable to read file after file \`${error.eventType}\` event.` + + `Make sure you have permission to access \`${buffer.getPath()}\`.` + this.notificationManager.addWarning(message, { + detail: error.message, + dismissable: true + }) }) } } From c1b0afe96981b7232ee8fcc605d19119b76cc9f6 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:34:25 -0400 Subject: [PATCH 05/14] Remove unnecessary code created because of implicit returns --- src/project.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/project.js b/src/project.js index 32538fc02..b176679c0 100644 --- a/src/project.js +++ b/src/project.js @@ -1,7 +1,6 @@ /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns * DS103: Rewrite code to no longer use __guard__ * DS104: Avoid inline assignments * DS204: Change includes calls to have a more natural evaluation order @@ -71,7 +70,7 @@ class Project extends Model { this.loadPromisesByPath = {} this.retiredBufferIDs = new Set() this.retiredBufferPaths = new Set() - return this.consumeServices(packageManager) + this.consumeServices(packageManager) } destroyUnretainedBuffers () { @@ -119,7 +118,7 @@ class Project extends Model { for (let buffer of this.buffers) { this.subscribeToBuffer(buffer) } - return this.setPaths(state.paths || [], {mustExist: true, exact: true}) + this.setPaths(state.paths || [], {mustExist: true, exact: true}) }) } @@ -348,7 +347,7 @@ class Project extends Model { // Stop event delivery immediately on removal of a rootDirectory, even if its watcher // promise has yet to resolve at the time of removal if (this.rootDirectories.includes(directory)) { - return this.emitter.emit('did-change-files', events) + this.emitter.emit('did-change-files', events) } }) @@ -369,7 +368,7 @@ class Project extends Model { this.repositories.push(repo != null ? repo : null) if (options.emitEvent !== false) { - return this.emitter.emit('did-change-paths', this.getPaths()) + this.emitter.emit('did-change-paths', this.getPaths()) } } @@ -695,10 +694,10 @@ class Project extends Model { buffer.onDidDestroy(() => this.removeBuffer(buffer)) buffer.onDidChangePath(() => { if (!(this.getPaths().length > 0)) { - return this.setPaths([path.dirname(buffer.getPath())]) + this.setPaths([path.dirname(buffer.getPath())]) } }) - return buffer.onWillThrowWatchError(({error, handle}) => { + buffer.onWillThrowWatchError(({error, handle}) => { handle() const message = `Unable to read file after file \`${error.eventType}\` event.` + From 94a552149d09b442be47679ecd50d14abcc7364f Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:36:47 -0400 Subject: [PATCH 06/14] Remove unnecessary use of Array.from --- src/project.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/project.js b/src/project.js index b176679c0..8d5d5e957 100644 --- a/src/project.js +++ b/src/project.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from * DS103: Rewrite code to no longer use __guard__ * DS104: Avoid inline assignments * DS204: Change includes calls to have a more natural evaluation order @@ -295,7 +294,7 @@ class Project extends Model { this.addPath(projectPath, {emitEvent: false, mustExist: true, exact: options.exact === true}) } catch (e) { if (e.missingProjectPaths != null) { - missingProjectPaths.push(...Array.from(e.missingProjectPaths || [])) + missingProjectPaths.push(...e.missingProjectPaths) } else { throw e } @@ -423,7 +422,7 @@ class Project extends Model { if (indexToRemove != null) { this.rootDirectories.splice(indexToRemove, 1) - const [removedRepository] = Array.from(this.repositories.splice(indexToRemove, 1)) + const [removedRepository] = this.repositories.splice(indexToRemove, 1) if (!this.repositories.includes(removedRepository)) { if (removedRepository) removedRepository.destroy() } @@ -671,7 +670,7 @@ class Project extends Model { } removeBufferAtIndex (index, options = {}) { - const [buffer] = Array.from(this.buffers.splice(index, 1)) + const [buffer] = this.buffers.splice(index, 1) return (buffer != null ? buffer.destroy() : undefined) } From 99aaafed1b8d39713b7e033d68b248f710778fa2 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:39:40 -0400 Subject: [PATCH 07/14] DS103: Rewrite code to no longer use __guard__ --- src/project.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/project.js b/src/project.js index 8d5d5e957..144e3d7c4 100644 --- a/src/project.js +++ b/src/project.js @@ -1,6 +1,5 @@ /* * decaffeinate suggestions: - * DS103: Rewrite code to no longer use __guard__ * DS104: Avoid inline assignments * DS204: Change includes calls to have a more natural evaluation order * DS205: Consider reworking code to avoid use of IIFEs @@ -251,8 +250,12 @@ class Project extends Model { // @repositoryPromisesByPath in case some other RepositoryProvider is // registered in the future that could supply a Repository for the // directory. - if (repo == null) { this.repositoryPromisesByPath.delete(pathForDirectory) } - __guardMethod__(repo, 'onDidDestroy', o => o.onDidDestroy(() => this.repositoryPromisesByPath.delete(pathForDirectory))) + if (repo == null) this.repositoryPromisesByPath.delete(pathForDirectory) + + if (repo && repo.onDidDestroy) { + repo.onDidDestroy(() => this.repositoryPromisesByPath.delete(pathForDirectory)) + } + return repo }) this.repositoryPromisesByPath.set(pathForDirectory, promise) @@ -555,7 +558,8 @@ class Project extends Model { // Is the buffer for the given path modified? isPathModified (filePath) { - return __guard__(this.findBufferForPath(this.resolvePath(filePath)), x => x.isModified()) + const bufferForPath = this.findBufferForPath(this.resolvePath(filePath)) + return bufferForPath && bufferForPath.isModified() } findBufferForPath (filePath) { @@ -708,14 +712,3 @@ class Project extends Model { }) } } - -function __guardMethod__ (obj, methodName, transform) { - if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { - return transform(obj, methodName) - } else { - return undefined - } -} -function __guard__ (value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined -} From b2571e8976b084e466f336c192fd0a2e4b5491e3 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:53:11 -0400 Subject: [PATCH 08/14] Avoid inline assignments --- src/project.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/project.js b/src/project.js index 144e3d7c4..f960cf2bc 100644 --- a/src/project.js +++ b/src/project.js @@ -409,8 +409,7 @@ class Project extends Model { // * `projectPath` {String} The path to remove. removePath (projectPath) { // The projectPath may be a URI, in which case it should not be normalized. - let needle - if ((needle = projectPath, !this.getPaths().includes(needle))) { + if (!this.getPaths().includes(projectPath)) { projectPath = this.defaultDirectoryProvider.normalizePath(projectPath) } From 48625584e4fb766e5d169887e671fa9657ac0c17 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 12:57:03 -0400 Subject: [PATCH 09/14] :art: Use shorter variations of null checks --- src/project.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/project.js b/src/project.js index f960cf2bc..123401b56 100644 --- a/src/project.js +++ b/src/project.js @@ -241,10 +241,11 @@ class Project extends Model { const pathForDirectory = directory.getRealPathSync() let promise = this.repositoryPromisesByPath.get(pathForDirectory) if (!promise) { - const promises = this.repositoryProviders.map(provider => provider.repositoryForDirectory(directory)) - promise = Promise.all(promises).then(repositories => { - let left - const repo = (left = _.find(repositories, repo => repo != null)) != null ? left : null + const promises = this.repositoryProviders.map((provider) => + provider.repositoryForDirectory(directory) + ) + promise = Promise.all(promises).then((repositories) => { + const repo = repositories.find((repo) => repo != null) || null // If no repository is found, remove the entry for the directory in // @repositoryPromisesByPath in case some other RepositoryProvider is @@ -447,7 +448,7 @@ class Project extends Model { resolvePath (uri) { if (!uri) { return } - if ((uri != null ? uri.match(/[A-Za-z0-9+-.]+:\/\//) : undefined)) { // leave path alone if it has a scheme + if (uri.match(/[A-Za-z0-9+-.]+:\/\//)) { // leave path alone if it has a scheme return uri } else { let projectPath @@ -481,7 +482,7 @@ class Project extends Model { if (fullPath != null) { for (let rootDirectory of this.rootDirectories) { const relativePath = rootDirectory.relativize(fullPath) - if ((relativePath != null ? relativePath.length : undefined) < result[1].length) { + if ((relativePath != null) && (relativePath.length < result[1].length)) { result = [rootDirectory.getPath(), relativePath] } } From 8df4cfbe58ee009ec532b27198a64e54f97d5f5e Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 17:27:06 -0400 Subject: [PATCH 10/14] :art: --- src/project.js | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/project.js b/src/project.js index 123401b56..5de586c1d 100644 --- a/src/project.js +++ b/src/project.js @@ -1,11 +1,3 @@ -/* - * decaffeinate suggestions: - * DS104: Avoid inline assignments - * DS204: Change includes calls to have a more natural evaluation order - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ const path = require('path') let _ = require('underscore-plus') @@ -82,7 +74,6 @@ class Project extends Model { */ deserialize (state) { - let bufferState this.retiredBufferIDs = new Set() this.retiredBufferPaths = new Set() @@ -103,13 +94,10 @@ class Project extends Model { }) } - const bufferPromises = ((() => { - const result = [] - for (bufferState of state.buffers) { - result.push(handleBufferState(bufferState)) - } - return result - })()) + const bufferPromises = [] + for (let bufferState of state.buffers) { + bufferPromises.push(handleBufferState(bufferState)) + } return Promise.all(bufferPromises).then(buffers => { this.buffers = buffers.filter(Boolean) From 790e2025490026fb3eb4128cf9c9c683e6f481bb Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 17:46:46 -0400 Subject: [PATCH 11/14] :art: --- src/project.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/project.js b/src/project.js index 5de586c1d..5f401dbd2 100644 --- a/src/project.js +++ b/src/project.js @@ -1,6 +1,6 @@ const path = require('path') -let _ = require('underscore-plus') +const _ = require('underscore-plus') const fs = require('fs-plus') const {Emitter, Disposable} = require('event-kit') const TextBuffer = require('text-buffer') From 5937b95b4969c89689fbbfdec3f77e40a01b9eb9 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sat, 14 Oct 2017 17:43:58 -0400 Subject: [PATCH 12/14] Fix loop contructs that got borked by `decaffeinate` --- src/project.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/project.js b/src/project.js index 5f401dbd2..4f95cf851 100644 --- a/src/project.js +++ b/src/project.js @@ -43,7 +43,9 @@ class Project extends Model { for (let repository of this.repositories.slice()) { if (repository != null) repository.destroy() } - for (let watcher = 0; watcher < this.watcherPromisesByPath.length; watcher++) { _ = this.watcherPromisesByPath[watcher]; watcher.dispose() } + for (let path in this.watcherPromisesByPath) { + this.watcherPromisesByPath[path].then(watcher => { watcher.dispose() }) + } this.rootDirectories = [] this.repositories = [] } @@ -277,7 +279,9 @@ class Project extends Model { this.rootDirectories = [] this.repositories = [] - for (let watcher = 0; watcher < this.watcherPromisesByPath.length; watcher++) { _ = this.watcherPromisesByPath[watcher]; watcher.then(w => w.dispose()) } + for (let path in this.watcherPromisesByPath) { + this.watcherPromisesByPath[path].then(watcher => { watcher.dispose() }) + } this.watcherPromisesByPath = {} const missingProjectPaths = [] @@ -342,10 +346,9 @@ class Project extends Model { } }) - for (let watcherPromise = 0; watcherPromise < this.watcherPromisesByPath.length; watcherPromise++) { - const root = this.watcherPromisesByPath[watcherPromise] - if (!this.rootDirectories.includes(root)) { - watcherPromise.then(watcher => watcher.dispose()) + for (let path in this.watcherPromisesByPath) { + if (!this.rootDirectories.includes(path)) { + this.watcherPromisesByPath[path].then(watcher => { watcher.dispose() }) } } From 5ec9d0f1347eda1aecea72ab00348a56eb3d0ec6 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Sun, 15 Oct 2017 09:59:41 -0400 Subject: [PATCH 13/14] =?UTF-8?q?=F0=9F=90=9B=20Fix=20bug=20disposing=20wa?= =?UTF-8?q?tchers=20in=20Project::addPath?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `this.rootDirectories` is an Array of Directory objects. `path` is a String. Therefore, `this.rootDirectories.includes(path)` will always evaluate to `false`. We instead need to look for an entry in `this.rootDirectories` where the Directory object's path is equal to the given path. --- src/project.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/project.js b/src/project.js index 4f95cf851..b99a60e85 100644 --- a/src/project.js +++ b/src/project.js @@ -347,7 +347,7 @@ class Project extends Model { }) for (let path in this.watcherPromisesByPath) { - if (!this.rootDirectories.includes(path)) { + if (!this.rootDirectories.find(dir => dir.getPath() === path)) { this.watcherPromisesByPath[path].then(watcher => { watcher.dispose() }) } } From a4ea46c57e078ae691d9aa9781948c39253ed4c9 Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Mon, 16 Oct 2017 11:04:56 -0400 Subject: [PATCH 14/14] Rename local variable /xref: https://github.com/atom/atom/pull/15898#discussion_r144850427 --- src/project.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/project.js b/src/project.js index b99a60e85..48541c395 100644 --- a/src/project.js +++ b/src/project.js @@ -346,9 +346,9 @@ class Project extends Model { } }) - for (let path in this.watcherPromisesByPath) { - if (!this.rootDirectories.find(dir => dir.getPath() === path)) { - this.watcherPromisesByPath[path].then(watcher => { watcher.dispose() }) + for (let watchedPath in this.watcherPromisesByPath) { + if (!this.rootDirectories.find(dir => dir.getPath() === watchedPath)) { + this.watcherPromisesByPath[watchedPath].then(watcher => { watcher.dispose() }) } }