-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlanguage-mode.coffee
312 lines (265 loc) · 13 KB
/
language-mode.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
{Range} = require 'text-buffer'
_ = require 'underscore-plus'
{OnigRegExp} = require 'oniguruma'
{Emitter, Subscriber} = require 'emissary'
module.exports =
class LanguageMode
Emitter.includeInto(this)
Subscriber.includeInto(this)
# Sets up a `LanguageMode` for the given {Editor}.
#
# editor - The {Editor} to associate with
constructor: (@editor) ->
{@buffer} = @editor
destroy: ->
@unsubscribe()
toggleLineCommentForBufferRow: (row) ->
@toggleLineCommentsForBufferRows(row, row)
# Wraps the lines between two rows in comments.
#
# If the language doesn't have comment, nothing happens.
#
# startRow - The row {Number} to start at
# endRow - The row {Number} to end at
#
# Returns an {Array} of the commented {Ranges}.
toggleLineCommentsForBufferRows: (start, end) ->
scopes = @editor.scopesForBufferPosition([start, 0])
properties = atom.syntax.propertiesForScope(scopes, "editor.commentStart")[0]
return unless properties
commentStartString = _.valueForKeyPath(properties, "editor.commentStart")
commentEndString = _.valueForKeyPath(properties, "editor.commentEnd")
return unless commentStartString
buffer = @editor.buffer
commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?')
commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})")
shouldUncomment = commentStartRegex.test(buffer.lineForRow(start))
if commentEndString
if shouldUncomment
commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?')
commentEndRegex = new OnigRegExp("(#{commentEndRegexString})(\\s*)$")
startMatch = commentStartRegex.search(buffer.lineForRow(start))
endMatch = commentEndRegex.search(buffer.lineForRow(end))
if startMatch and endMatch
buffer.transact ->
columnStart = startMatch[1].length
columnEnd = columnStart + startMatch[2].length
buffer.setTextInRange([[start, columnStart], [start, columnEnd]], "")
endLength = buffer.lineLengthForRow(end) - endMatch[2].length
endColumn = endLength - endMatch[1].length
buffer.setTextInRange([[end, endColumn], [end, endLength]], "")
else
buffer.transact ->
indentLength = buffer.lineForRow(start).match(/^\s*/)?[0].length ? 0
buffer.insert([start, indentLength], commentStartString)
buffer.insert([end, buffer.lineLengthForRow(end)], commentEndString)
else
if shouldUncomment and start isnt end
shouldUncomment = [start+1..end].every (row) ->
line = buffer.lineForRow(row)
not line or commentStartRegex.test(line)
if shouldUncomment
for row in [start..end]
if match = commentStartRegex.search(buffer.lineForRow(row))
columnStart = match[1].length
columnEnd = columnStart + match[2].length
buffer.setTextInRange([[row, columnStart], [row, columnEnd]], "")
else
if start is end
indent = @editor.indentationForBufferRow(start)
else
indent = @minIndentLevelForRowRange(start, end)
indentString = @editor.buildIndentString(indent)
tabLength = @editor.getTabLength()
indentRegex = new RegExp("(\t|[ ]{#{tabLength}}){#{Math.floor(indent)}}")
for row in [start..end]
line = buffer.lineForRow(row)
if indentLength = line.match(indentRegex)?[0].length
buffer.insert([row, indentLength], commentStartString)
else
buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString)
# Folds all the foldable lines in the buffer.
foldAll: ->
for currentRow in [[email protected]()]
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
@editor.createFold(startRow, endRow)
# Unfolds all the foldable lines in the buffer.
unfoldAll: ->
for row in [@buffer.getLastRow()..0]
fold.destroy() for fold in @editor.displayBuffer.foldsStartingAtBufferRow(row)
# Fold all comment and code blocks at a given indentLevel
#
# indentLevel - A {Number} indicating indentLevel; 0 based.
foldAllAtIndentLevel: (indentLevel) ->
for currentRow in [[email protected]()]
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
# assumption: startRow will always be the min indent level for the entire range
if @editor.indentationForBufferRow(startRow) == indentLevel
@editor.createFold(startRow, endRow)
# Given a buffer row, creates a fold at it.
#
# bufferRow - A {Number} indicating the buffer row
#
# Returns the new {Fold}.
foldBufferRow: (bufferRow) ->
for currentRow in [bufferRow..0]
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow? and startRow <= bufferRow <= endRow
fold = @editor.displayBuffer.largestFoldStartingAtBufferRow(startRow)
return @editor.createFold(startRow, endRow) unless fold
# Find the row range for a fold at a given bufferRow. Will handle comments
# and code.
#
# bufferRow - A {Number} indicating the buffer row
#
# Returns an {Array} of the [startRow, endRow]. Returns null if no range.
rowRangeForFoldAtBufferRow: (bufferRow) ->
rowRange = @rowRangeForCommentAtBufferRow(bufferRow)
rowRange ?= @rowRangeForCodeFoldAtBufferRow(bufferRow)
rowRange
rowRangeForCommentAtBufferRow: (bufferRow) ->
return unless @editor.displayBuffer.tokenizedBuffer.lineForScreenRow(bufferRow).isComment()
startRow = bufferRow
for currentRow in [bufferRow-1..0]
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.lineForScreenRow(currentRow).isComment()
startRow = currentRow
endRow = bufferRow
for currentRow in [[email protected]()]
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.lineForScreenRow(currentRow).isComment()
endRow = currentRow
return [startRow, endRow] if startRow isnt endRow
rowRangeForCodeFoldAtBufferRow: (bufferRow) ->
return null unless @isFoldableAtBufferRow(bufferRow)
startIndentLevel = @editor.indentationForBufferRow(bufferRow)
scopes = @editor.scopesForBufferPosition([bufferRow, 0])
for row in [(bufferRow + 1)[email protected]()]
continue if @editor.isBufferRowBlank(row)
indentation = @editor.indentationForBufferRow(row)
if indentation <= startIndentLevel
includeRowInFold = indentation == startIndentLevel and @foldEndRegexForScopes(scopes)?.search(@editor.lineForBufferRow(row))
foldEndRow = row if includeRowInFold
break
foldEndRow = row
[bufferRow, foldEndRow]
# Public: Returns a {Boolean} indicating whether the given buffer row starts a
# foldable row range. Rows that are "foldable" have a fold icon next to their
# icon in the gutter in the default configuration.
isFoldableAtBufferRow: (bufferRow) ->
@isFoldableCodeAtBufferRow(bufferRow) or @isFoldableCommentAtBufferRow(bufferRow)
# Returns a {Boolean} indicating whether the given buffer row starts
# a a foldable row range due to the code's indentation patterns.
isFoldableCodeAtBufferRow: (bufferRow) ->
return false if @editor.isBufferRowBlank(bufferRow) or @isLineCommentedAtBufferRow(bufferRow)
nextNonEmptyRow = @editor.nextNonBlankBufferRow(bufferRow)
return false unless nextNonEmptyRow?
@editor.indentationForBufferRow(nextNonEmptyRow) > @editor.indentationForBufferRow(bufferRow)
# Returns a {Boolean} indicating whether the given buffer row starts
# a foldable row range due to being the start of a multi-line comment.
isFoldableCommentAtBufferRow: (bufferRow) ->
@isLineCommentedAtBufferRow(bufferRow) and
@isLineCommentedAtBufferRow(bufferRow + 1) and
not @isLineCommentedAtBufferRow(bufferRow - 1)
# Returns a {Boolean} indicating whether the line at the given buffer
# row is a comment.
isLineCommentedAtBufferRow: (bufferRow) ->
return false unless 0 <= bufferRow <= @editor.getLastBufferRow()
@editor.displayBuffer.tokenizedBuffer.lineForScreenRow(bufferRow).isComment()
# Find a row range for a 'paragraph' around specified bufferRow.
# Right now, a paragraph is a block of text bounded by and empty line or a
# block of text that is not the same type (comments next to source code).
rowRangeForParagraphAtBufferRow: (bufferRow) ->
return unless /\w/.test(@editor.lineForBufferRow(bufferRow))
if @isLineCommentedAtBufferRow(bufferRow)
isOriginalRowComment = true
range = @rowRangeForCommentAtBufferRow(bufferRow)
[firstRow, lastRow] = range or [bufferRow, bufferRow]
else
isOriginalRowComment = false
[firstRow, lastRow] = [0, @editor.getLastBufferRow()-1]
startRow = bufferRow
while startRow > firstRow
break if @isLineCommentedAtBufferRow(startRow - 1) != isOriginalRowComment
break unless /\w/.test(@editor.lineForBufferRow(startRow - 1))
startRow--
endRow = bufferRow
lastRow = @editor.getLastBufferRow()
while endRow < lastRow
break if @isLineCommentedAtBufferRow(endRow + 1) != isOriginalRowComment
break unless /\w/.test(@editor.lineForBufferRow(endRow + 1))
endRow++
new Range([startRow, 0], [endRow, @editor.lineLengthForBufferRow(endRow)])
# Given a buffer row, this returns a suggested indentation level.
#
# The indentation level provided is based on the current {LanguageMode}.
#
# bufferRow - A {Number} indicating the buffer row
#
# Returns a {Number}.
suggestedIndentForBufferRow: (bufferRow) ->
currentIndentLevel = @editor.indentationForBufferRow(bufferRow)
scopes = @editor.scopesForBufferPosition([bufferRow, 0])
return currentIndentLevel unless increaseIndentRegex = @increaseIndentRegexForScopes(scopes)
currentLine = @buffer.lineForRow(bufferRow)
precedingRow = if bufferRow > 0 then bufferRow - 1 else null
return currentIndentLevel unless precedingRow?
precedingLine = @buffer.lineForRow(precedingRow)
desiredIndentLevel = @editor.indentationForBufferRow(precedingRow)
desiredIndentLevel += 1 if increaseIndentRegex.test(precedingLine) and not @editor.isBufferRowCommented(precedingRow)
return desiredIndentLevel unless decreaseIndentRegex = @decreaseIndentRegexForScopes(scopes)
desiredIndentLevel -= 1 if decreaseIndentRegex.test(currentLine)
Math.max(desiredIndentLevel, 0)
# Calculate a minimum indent level for a range of lines excluding empty lines.
#
# startRow - The row {Number} to start at
# endRow - The row {Number} to end at
#
# Returns a {Number} of the indent level of the block of lines.
minIndentLevelForRowRange: (startRow, endRow) ->
indents = (@editor.indentationForBufferRow(row) for row in [startRow..endRow] when not @editor.isBufferRowBlank(row))
indents = [0] unless indents.length
Math.min(indents...)
# Indents all the rows between two buffer row numbers.
#
# startRow - The row {Number} to start at
# endRow - The row {Number} to end at
autoIndentBufferRows: (startRow, endRow) ->
@autoIndentBufferRow(row) for row in [startRow..endRow]
# Given a buffer row, this indents it.
#
# bufferRow - The row {Number}.
# options - An options {Object} to pass through to {Editor::setIndentationForBufferRow}.
autoIndentBufferRow: (bufferRow, options) ->
indentLevel = @suggestedIndentForBufferRow(bufferRow)
@editor.setIndentationForBufferRow(bufferRow, indentLevel, options)
# Given a buffer row, this decreases the indentation.
#
# bufferRow - The row {Number}
autoDecreaseIndentForBufferRow: (bufferRow) ->
scopes = @editor.scopesForBufferPosition([bufferRow, 0])
increaseIndentRegex = @increaseIndentRegexForScopes(scopes)
decreaseIndentRegex = @decreaseIndentRegexForScopes(scopes)
return unless increaseIndentRegex and decreaseIndentRegex
line = @buffer.lineForRow(bufferRow)
return unless decreaseIndentRegex.test(line)
currentIndentLevel = @editor.indentationForBufferRow(bufferRow)
return if currentIndentLevel is 0
precedingRow = @buffer.previousNonBlankRow(bufferRow)
return unless precedingRow?
precedingLine = @buffer.lineForRow(precedingRow)
desiredIndentLevel = @editor.indentationForBufferRow(precedingRow)
desiredIndentLevel -= 1 unless increaseIndentRegex.test(precedingLine)
if desiredIndentLevel >= 0 and desiredIndentLevel < currentIndentLevel
@editor.setIndentationForBufferRow(bufferRow, desiredIndentLevel)
getRegexForProperty: (scopes, property) ->
if pattern = atom.syntax.getProperty(scopes, property)
new OnigRegExp(pattern)
increaseIndentRegexForScopes: (scopes) ->
@getRegexForProperty(scopes, 'editor.increaseIndentPattern')
decreaseIndentRegexForScopes: (scopes) ->
@getRegexForProperty(scopes, 'editor.decreaseIndentPattern')
foldEndRegexForScopes: (scopes) ->
@getRegexForProperty(scopes, 'editor.foldEndPattern')