-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathproject.coffee
325 lines (264 loc) · 10.4 KB
/
project.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
path = require 'path'
url = require 'url'
_ = require 'underscore-plus'
fs = require 'fs-plus'
Q = require 'q'
{deprecate} = require 'grim'
{Model} = require 'theorist'
{Emitter, Subscriber} = require 'emissary'
Serializable = require 'serializable'
TextBuffer = require 'text-buffer'
{Directory} = require 'pathwatcher'
Editor = require './editor'
Task = require './task'
Git = require './git'
# Public: 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
atom.deserializers.add(this)
Serializable.includeInto(this)
# Public: Find the local path for the given repository URL.
@pathForRepositoryUrl: (repoUrl) ->
[repoName] = url.parse(repoUrl).path.split('/')[-1..]
repoName = repoName.replace(/\.git$/, '')
path.join(atom.config.get('core.projectHome'), repoName)
constructor: ({path, @buffers}={}) ->
@buffers ?= []
for buffer in @buffers
do (buffer) =>
buffer.once 'destroyed', => @removeBuffer(buffer)
@setPath(path)
serializeParams: ->
path: @path
buffers: _.compact(@buffers.map (buffer) -> buffer.serialize() if buffer.isRetained())
deserializeParams: (params) ->
params.buffers = params.buffers.map (bufferState) -> atom.deserializers.deserialize(bufferState)
params
destroyed: ->
buffer.destroy() for buffer in @getBuffers()
@destroyRepo()
destroyRepo: ->
if @repo?
@repo.destroy()
@repo = null
destroyUnretainedBuffers: ->
buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained()
# Public: Returns the {Git} repository if available.
getRepo: -> @repo
# Public: Returns the project's fullpath.
getPath: ->
@rootDirectory?.path
# Public: Sets the project's fullpath.
setPath: (projectPath) ->
@path = projectPath
@rootDirectory?.off()
@destroyRepo()
if projectPath?
directory = if fs.isDirectorySync(projectPath) then projectPath else path.dirname(projectPath)
@rootDirectory = new Directory(directory)
if @repo = Git.open(projectPath, project: this)
@repo.refreshIndex()
@repo.refreshStatus()
else
@rootDirectory = null
@emit "path-changed"
# Public: Returns the root {Directory} object for this project.
getRootDirectory: ->
@rootDirectory
# Public: 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} or undefined if the uri is not missing or empty.
resolve: (uri) ->
return unless uri
if uri?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme
uri
else
uri = path.join(@getPath(), uri) unless fs.isAbsolute(uri)
fs.absolute(uri)
# Public: Make the given path relative to the project directory.
relativize: (fullPath) ->
return fullPath if fullPath?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme
@rootDirectory?.relativize(fullPath) ? fullPath
# Public: Returns whether the given path is inside this project.
contains: (pathToCheck) ->
@rootDirectory?.contains(pathToCheck) ? false
# Given a path to a file, this constructs and associates a new
# {Editor}, showing the file.
#
# filePath - The {String} path of the file to associate with.
# options - Options that you can pass to the {Editor} constructor.
#
# Returns a promise that resolves to an {Editor}.
open: (filePath, options={}) ->
filePath = @resolve(filePath)
@bufferForPath(filePath).then (buffer) =>
@buildEditorForBuffer(buffer, options)
# Deprecated
openSync: (filePath, options={}) ->
deprecate("Use Project::open instead")
filePath = @resolve(filePath)
@buildEditorForBuffer(@bufferForPathSync(filePath), options)
# 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(@resolve(filePath))?.isModified()
findBufferForPath: (filePath) ->
_.find @buffers, (buffer) -> buffer.getPath() == filePath
# Only to be used in specs
bufferForPathSync: (filePath) ->
absoluteFilePath = @resolve(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: (filePath) ->
absoluteFilePath = @resolve(filePath)
existingBuffer = @findBufferForPath(absoluteFilePath) if absoluteFilePath
Q(existingBuffer ? @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) ->
if fs.getSizeSync(absoluteFilePath) >= 2 * 1048576 # 2MB
throw new Error("Atom can only handle files < 2MB, for now.")
buffer = new TextBuffer({filePath: absoluteFilePath})
@addBuffer(buffer)
buffer.load()
.then((buffer) -> buffer)
.catch(=> @removeBuffer(buffer))
addBuffer: (buffer, options={}) ->
@addBufferAtIndex(buffer, @buffers.length, options)
buffer.once 'destroyed', => @removeBuffer(buffer)
addBufferAtIndex: (buffer, index, options={}) ->
@buffers.splice(index, 0, buffer)
buffer.once 'destroyed', => @removeBuffer(buffer)
@emit 'buffer-created', 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()
# Public: Performs a search across all the files in the project.
#
# regex - A {RegExp} to search with.
# options - An optional options {Object} (default: {}):
# :paths - An {Array} of glob patterns to search within
# iterator - A {Function} callback on each file found
scan: (regex, options={}, iterator) ->
if _.isFunction(options)
iterator = options
options = {}
deferred = Q.defer()
searchOptions =
ignoreCase: regex.ignoreCase
inclusions: options.paths
includeHidden: true
excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths')
exclusions: atom.config.get('core.ignoredNames')
task = Task.once require.resolve('./scan-handler'), @getPath(), regex.source, searchOptions, ->
deferred.resolve()
task.on 'scan:result-found', (result) =>
iterator(result) unless @isPathModified(result.filePath)
if _.isFunction(options.onPathsSearched)
task.on 'scan:paths-searched', (numberOfPathsSearched) ->
options.onPathsSearched(numberOfPathsSearched)
for buffer in @getBuffers() when buffer.isModified()
filePath = buffer.getPath()
continue unless @contains(filePath)
matches = []
buffer.scan regex, (match) -> matches.push match
iterator {filePath, matches} if matches.length > 0
promise = deferred.promise
promise.cancel = ->
task.terminate()
deferred.resolve('cancelled')
promise
# Public: Performs a replace across all the specified files in the project.
#
# regex - A {RegExp} to search with.
# replacementText - Text to replace all matches of regex with
# filePaths - List of file path strings to run the replace on.
# iterator - A {Function} callback on each file with replacements:
# `({filePath, replacements}) ->`.
replace: (regex, replacementText, filePaths, iterator) ->
deferred = Q.defer()
openPaths = (buffer.getPath() for buffer in @getBuffers())
outOfProcessPaths = _.difference(filePaths, openPaths)
inProcessFinished = !openPaths.length
outOfProcessFinished = !outOfProcessPaths.length
checkFinished = ->
deferred.resolve() if outOfProcessFinished and inProcessFinished
unless outOfProcessFinished.length
flags = 'g'
flags += 'i' if regex.ignoreCase
task = Task.once require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, ->
outOfProcessFinished = true
checkFinished()
task.on 'replace:path-replaced', iterator
for buffer in @getBuffers()
continue unless buffer.getPath() in filePaths
replacements = buffer.replace(regex, replacementText, iterator)
iterator({filePath: buffer.getPath(), replacements}) if replacements
inProcessFinished = true
checkFinished()
deferred.promise
buildEditorForBuffer: (buffer, editorOptions) ->
editor = new Editor(_.extend({buffer, registerEditor: true}, editorOptions))
editor
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)
# Deprecated: delegate
registerOpener: (opener) ->
deprecate("Use Workspace::registerOpener instead")
atom.workspace.registerOpener(opener)
# Deprecated: delegate
unregisterOpener: (opener) ->
deprecate("Use Workspace::unregisterOpener instead")
atom.workspace.unregisterOpener(opener)
# Deprecated: delegate
eachEditor: (callback) ->
deprecate("Use Workspace::eachEditor instead")
atom.workspace.eachEditor(callback)
# Deprecated: delegate
getEditors: ->
deprecate("Use Workspace::getEditors instead")
atom.workspace.getEditors()