Files
atom/src/project.coffee
Nathan Sobo f9a269ed99 Prompt about checking out head revision in TextEditor, not GitRepository
This allows us not to inject confirm or ApplicationDelegate into
Project, GitRepositoryProvider, and GitRepository.
2015-10-13 19:11:55 -06:00

396 lines
12 KiB
CoffeeScript

path = require 'path'
url = require 'url'
_ = require 'underscore-plus'
fs = require 'fs-plus'
{Emitter, Disposable} = require 'event-kit'
TextBuffer = require 'text-buffer'
DefaultDirectoryProvider = require './default-directory-provider'
Model = require './model'
TextEditor = require './text-editor'
Task = require './task'
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}) ->
@emitter = new Emitter
@buffers = []
@paths = []
@rootDirectories = []
@repositories = []
@directoryProviders = []
@defaultDirectoryProvider = new DefaultDirectoryProvider()
@repositoryPromisesByPath = new Map()
@repositoryProviders = [new GitRepositoryProvider(this, config)]
@consumeServices(packageManager)
destroyed: ->
buffer.destroy() for buffer in @buffers
@setPaths([])
reset: (packageManager) ->
@emitter.dispose()
@emitter = new Emitter
buffer?.destroy() for buffer in @buffers
@buffers = []
@setPaths([])
@consumeServices(packageManager)
destroyUnretainedBuffers: ->
buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained()
return
###
Section: Serialization
###
deserialize: (state, deserializerManager) ->
states.paths = [state.path] if state.path? # backward compatibility
@buffers = _.compact state.buffers.map (bufferState) ->
# Check that buffer's file path is accessible
return if fs.isDirectorySync(bufferState.filePath)
if bufferState.filePath
try
fs.closeSync(fs.openSync(bufferState.filePath, 'r'))
catch error
return unless error.code is 'ENOENT'
deserializerManager.deserialize(bufferState)
@subscribeToBuffer(buffer) for buffer in @buffers
@setPaths(state.paths)
serialize: ->
deserializer: 'Project'
paths: @getPaths()
buffers: _.compact(@buffers.map (buffer) -> buffer.serialize() if buffer.isRetained())
###
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
onDidAddBuffer: (callback) ->
@emitter.on 'did-add-buffer', 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
@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.
setPaths: (projectPaths) ->
repository?.destroy() for repository in @repositories
@rootDirectories = []
@repositories = []
@addPath(projectPath, emitEvent: false) for projectPath in projectPaths
@emitter.emit 'did-change-paths', projectPaths
# Public: Add a path to the project's list of root paths
#
# * `projectPath` {String} The path to the directory to add.
addPath: (projectPath, options) ->
directory = null
for provider in @directoryProviders
break if directory = provider.directoryForURISync?(projectPath)
directory ?= @defaultDirectoryProvider.directoryForURISync(projectPath)
directoryExists = directory.existsSync()
for rootDirectory in @getDirectories()
return if rootDirectory.getPath() is directory.getPath()
return if not directoryExists and rootDirectory.contains(directory.getPath())
@rootDirectories.push(directory)
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()
# 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 = path.normalize(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
@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)
path.normalize(fs.absolute(uri))
# TODO: what should we do here when there are multiple directories?
else if projectPath = @getPaths()[0]
path.normalize(fs.absolute(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.push(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
# Only to be used in specs
bufferForPathSync: (filePath) ->
absoluteFilePath = @resolvePath(filePath)
existingBuffer = @findBufferForPath(absoluteFilePath) if filePath
existingBuffer ? @buildBufferSync(absoluteFilePath)
# 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)
bufferForId: (id) ->
_.find @buffers, (buffer) -> buffer.id is id
# Still needed when deserializing a tokenized buffer
buildBufferSync: (absoluteFilePath) ->
buffer = new TextBuffer({filePath: absoluteFilePath})
@addBuffer(buffer)
buffer.loadSync()
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) ->
buffer = new TextBuffer({filePath: absoluteFilePath})
@addBuffer(buffer)
buffer.load()
.then((buffer) -> buffer)
.catch(=> @removeBuffer(buffer))
addBuffer: (buffer, options={}) ->
@addBufferAtIndex(buffer, @buffers.length, options)
@subscribeToBuffer(buffer)
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.onDidDestroy => @removeBuffer(buffer)
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