mirror of
https://github.com/atom/atom.git
synced 2026-02-05 04:05:05 -05:00
318 lines
9.3 KiB
CoffeeScript
318 lines
9.3 KiB
CoffeeScript
fsUtils = require 'fs-utils'
|
|
_ = require 'underscore'
|
|
$ = require 'jquery'
|
|
Range = require 'range'
|
|
Buffer = require 'text-buffer'
|
|
EditSession = require 'edit-session'
|
|
EventEmitter = require 'event-emitter'
|
|
Directory = require 'directory'
|
|
BufferedProcess = require 'buffered-process'
|
|
|
|
# Public: Represents a project that's opened in Atom.
|
|
#
|
|
# Ultimately, a project is a git directory that's been opened. It's a collection
|
|
# of directories and files that you can operate on.
|
|
module.exports =
|
|
class Project
|
|
registerDeserializer(this)
|
|
|
|
@openers: []
|
|
|
|
@registerOpener: (opener) ->
|
|
@openers.push(opener)
|
|
|
|
@unregisterOpener: (opener) ->
|
|
_.remove(@openers, opener)
|
|
|
|
tabLength: 2
|
|
softTabs: true
|
|
softWrap: false
|
|
rootDirectory: null
|
|
editSessions: null
|
|
ignoredPathRegexes: null
|
|
|
|
### Internal ###
|
|
|
|
serialize: ->
|
|
deserializer: 'Project'
|
|
path: @getPath()
|
|
|
|
@deserialize: (state) ->
|
|
new Project(state.path)
|
|
|
|
destroy: ->
|
|
editSession.destroy() for editSession in @getEditSessions()
|
|
|
|
### Public ###
|
|
|
|
# Establishes a new project at a given path.
|
|
#
|
|
# path - The {String} name of the path
|
|
constructor: (path) ->
|
|
@setPath(path)
|
|
@editSessions = []
|
|
@buffers = []
|
|
|
|
# Retrieves the project path.
|
|
#
|
|
# Returns a {String}.
|
|
getPath: ->
|
|
@rootDirectory?.path
|
|
|
|
# Sets the project path.
|
|
#
|
|
# path - A {String} representing the new path
|
|
setPath: (path) ->
|
|
@rootDirectory?.off()
|
|
|
|
if path?
|
|
directory = if fsUtils.isDirectory(path) then path else fsUtils.directory(path)
|
|
@rootDirectory = new Directory(directory)
|
|
else
|
|
@rootDirectory = null
|
|
|
|
@trigger "path-changed"
|
|
|
|
# Retrieves the name of the root directory.
|
|
#
|
|
# Returns a {String}.
|
|
getRootDirectory: ->
|
|
@rootDirectory
|
|
|
|
# Retrieves the names of every file (that's not `git ignore`d) in the project.
|
|
#
|
|
# Returns an {Array} of {String}s.
|
|
getFilePaths: ->
|
|
deferred = $.Deferred()
|
|
paths = []
|
|
onFile = (path) => paths.push(path) unless @isPathIgnored(path)
|
|
onDirectory = -> true
|
|
fsUtils.traverseTreeSync(@getPath(), onFile, onDirectory)
|
|
deferred.resolve(paths)
|
|
deferred.promise()
|
|
|
|
# Identifies if a path is ignored.
|
|
#
|
|
# path - The {String} name of the path to check
|
|
#
|
|
# Returns a {Boolean}.
|
|
isPathIgnored: (path) ->
|
|
for segment in path.split("/")
|
|
ignoredNames = config.get("core.ignoredNames") or []
|
|
return true if _.contains(ignoredNames, segment)
|
|
|
|
@ignoreRepositoryPath(path)
|
|
|
|
# Identifies if a path is ignored.
|
|
#
|
|
# path - The {String} name of the path to check
|
|
#
|
|
# Returns a {Boolean}.
|
|
ignoreRepositoryPath: (path) ->
|
|
config.get("core.hideGitIgnoredFiles") and git?.isPathIgnored(fsUtils.join(@getPath(), path))
|
|
|
|
# Given a uri, this resolves it relative to the project directory. If the path
|
|
# is already absolute or if it is prefixed with a scheme, it is returned unchanged.
|
|
#
|
|
# uri - The {String} name of the path to convert
|
|
#
|
|
# Returns a {String}.
|
|
resolve: (uri) ->
|
|
if uri?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme
|
|
uri
|
|
else
|
|
uri = fsUtils.join(@getPath(), uri) unless uri[0] == '/'
|
|
fsUtils.absolute uri
|
|
|
|
# Given a path, this makes it relative to the project directory.
|
|
#
|
|
# filePath - The {String} name of the path to convert
|
|
#
|
|
# Returns a {String}.
|
|
relativize: (fullPath) ->
|
|
return fullPath unless fullPath.lastIndexOf(@getPath()) is 0
|
|
fullPath.replace(@getPath(), "").replace(/^\//, '')
|
|
|
|
# Identifies if the project is using soft tabs.
|
|
#
|
|
# Returns a {Boolean}.
|
|
getSoftTabs: -> @softTabs
|
|
|
|
# Sets the project to use soft tabs.
|
|
#
|
|
# softTabs - A {Boolean} which, if `true`, sets soft tabs
|
|
setSoftTabs: (@softTabs) ->
|
|
|
|
# Identifies if the project is using soft wrapping.
|
|
#
|
|
# Returns a {Boolean}.
|
|
getSoftWrap: -> @softWrap
|
|
|
|
# Sets the project to use soft wrapping.
|
|
#
|
|
# softTabs - A {Boolean} which, if `true`, sets soft wrapping
|
|
setSoftWrap: (@softWrap) ->
|
|
|
|
# Given a path to a file, this constructs and associates a new `EditSession`, showing the file.
|
|
#
|
|
# filePath - The {String} path of the file to associate with
|
|
# editSessionOptions - Options that you can pass to the `EditSession` constructor
|
|
#
|
|
# Returns either an {EditSession} (for text) or {ImageEditSession} (for images).
|
|
open: (filePath, options={}) ->
|
|
for opener in @constructor.openers
|
|
return resource if resource = opener(filePath, options)
|
|
|
|
@buildEditSessionForBuffer(@bufferForPath(filePath), options)
|
|
|
|
# Retrieves all the {EditSession}s in the project; that is, the `EditSession`s for all open files.
|
|
#
|
|
# Returns an {Array} of {EditSession}s.
|
|
getEditSessions: ->
|
|
new Array(@editSessions...)
|
|
|
|
### Public ###
|
|
|
|
# Removes an {EditSession} association from the project.
|
|
#
|
|
# Returns the removed {EditSession}.
|
|
removeEditSession: (editSession) ->
|
|
_.remove(@editSessions, editSession)
|
|
|
|
# Retrieves all the {Buffer}s in the project; that is, the buffers for all open files.
|
|
#
|
|
# Returns an {Array} of {Buffer}s.
|
|
getBuffers: ->
|
|
buffers = []
|
|
for editSession in @editSessions when not _.include(buffers, editSession.buffer)
|
|
buffers.push editSession.buffer
|
|
buffers
|
|
|
|
# Given a file path, this retrieves or creates a new {Buffer}.
|
|
#
|
|
# 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.
|
|
# text - The {String} text to use as a buffer, if the file doesn't have any contents
|
|
#
|
|
# Returns the {Buffer}.
|
|
bufferForPath: (filePath, text) ->
|
|
if filePath?
|
|
filePath = @resolve(filePath)
|
|
if filePath
|
|
buffer = _.find @buffers, (buffer) -> buffer.getPath() == filePath
|
|
buffer or @buildBuffer(filePath, text)
|
|
else
|
|
@buildBuffer(null, text)
|
|
|
|
# Given a file path, this sets its {Buffer}.
|
|
#
|
|
# filePath - A {String} representing a path
|
|
# text - The {String} text to use as a buffer
|
|
#
|
|
# Returns the {Buffer}.
|
|
buildBuffer: (filePath, text) ->
|
|
buffer = new Buffer(filePath, text)
|
|
@buffers.push buffer
|
|
@trigger 'buffer-created', buffer
|
|
buffer
|
|
|
|
# Removes a {Buffer} association from the project.
|
|
#
|
|
# Returns the removed {Buffer}.
|
|
removeBuffer: (buffer) ->
|
|
_.remove(@buffers, buffer)
|
|
|
|
# Performs a search across all the files in the project.
|
|
#
|
|
# regex - A {RegExp} to search with
|
|
# iterator - A {Function} callback on each file found
|
|
scan: (regex, iterator) ->
|
|
bufferedData = ""
|
|
state = 'readingPath'
|
|
path = null
|
|
|
|
readPath = (line) ->
|
|
if /^[0-9,; ]+:/.test(line)
|
|
state = 'readingLines'
|
|
else if /^:/.test line
|
|
path = line.substr(1)
|
|
else
|
|
path += ('\n' + line)
|
|
|
|
readLine = (line) ->
|
|
if line.length == 0
|
|
state = 'readingPath'
|
|
path = null
|
|
else
|
|
colonIndex = line.indexOf(':')
|
|
matchInfo = line.substring(0, colonIndex)
|
|
lineText = line.substring(colonIndex + 1)
|
|
readMatches(matchInfo, lineText)
|
|
|
|
readMatches = (matchInfo, lineText) ->
|
|
[lineNumber, matchPositionsText] = matchInfo.match(/(\d+);(.+)/)[1..]
|
|
row = parseInt(lineNumber) - 1
|
|
matchPositions = matchPositionsText.split(',').map (positionText) -> positionText.split(' ').map (pos) -> parseInt(pos)
|
|
|
|
for [column, length] in matchPositions
|
|
range = new Range([row, column], [row, column + length])
|
|
match = lineText.substr(column, length)
|
|
iterator({path, range, match})
|
|
|
|
deferred = $.Deferred()
|
|
exit = (code) ->
|
|
if code is -1
|
|
deferred.reject({command, code})
|
|
else
|
|
deferred.resolve()
|
|
stdout = (data) ->
|
|
lines = data.split('\n')
|
|
lines.pop() # the last segment is a spurious '' because data always ends in \n due to bufferLines: true
|
|
for line in lines
|
|
readPath(line) if state is 'readingPath'
|
|
readLine(line) if state is 'readingLines'
|
|
|
|
command = require.resolve('.bin/nak')
|
|
args = ['--hidden', '--ackmate', regex.source, @getPath()]
|
|
ignoredNames = config.get('core.ignoredNames') ? []
|
|
args.unshift('--ignore', ignoredNames.join(',')) if ignoredNames.length > 0
|
|
args.unshift('--addVCSIgnores') if config.get('core.excludeVcsIgnoredPaths')
|
|
new BufferedProcess({command, args, stdout, exit})
|
|
deferred
|
|
|
|
### Internal ###
|
|
|
|
buildEditSessionForBuffer: (buffer, editSessionOptions) ->
|
|
options = _.extend(@defaultEditSessionOptions(), editSessionOptions)
|
|
options.project = this
|
|
options.buffer = buffer
|
|
editSession = new EditSession(options)
|
|
@editSessions.push editSession
|
|
@trigger 'edit-session-created', editSession
|
|
editSession
|
|
|
|
defaultEditSessionOptions: ->
|
|
tabLength: @tabLength
|
|
softTabs: @getSoftTabs()
|
|
softWrap: @getSoftWrap()
|
|
|
|
eachEditSession: (callback) ->
|
|
callback(editSession) for editSession in @getEditSessions()
|
|
@on 'edit-session-created', (editSession) -> callback(editSession)
|
|
|
|
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)
|
|
|
|
_.extend Project.prototype, EventEmitter
|
|
|
|
require 'image-edit-session'
|