Files
atom/src/language-mode.coffee
2015-10-07 15:25:00 -05:00

352 lines
14 KiB
CoffeeScript

{Range} = require 'text-buffer'
_ = require 'underscore-plus'
{OnigRegExp} = require 'oniguruma'
ScopeDescriptor = require './scope-descriptor'
module.exports =
class LanguageMode
# Sets up a `LanguageMode` for the given {TextEditor}.
#
# editor - The {TextEditor} to associate with
constructor: (@editor, @config) ->
{@buffer} = @editor
destroy: ->
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
toggleLineCommentsForBufferRows: (start, end) ->
scope = @editor.scopeDescriptorForBufferPosition([start, 0])
{commentStartString, commentEndString} = @commentStartAndEndStringsForScope(scope)
return unless commentStartString?
buffer = @editor.buffer
commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?')
commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})")
if commentEndString
shouldUncomment = commentStartRegex.testSync(buffer.lineForRow(start))
if shouldUncomment
commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?')
commentEndRegex = new OnigRegExp("(#{commentEndRegexString})(\\s*)$")
startMatch = commentStartRegex.searchSync(buffer.lineForRow(start))
endMatch = commentEndRegex.searchSync(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
allBlank = true
allBlankOrCommented = true
for row in [start..end] by 1
line = buffer.lineForRow(row)
blank = line?.match(/^\s*$/)
allBlank = false unless blank
allBlankOrCommented = false unless blank or commentStartRegex.testSync(line)
shouldUncomment = allBlankOrCommented and not allBlank
if shouldUncomment
for row in [start..end] by 1
if match = commentStartRegex.searchSync(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] by 1
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)
return
# Folds all the foldable lines in the buffer.
foldAll: ->
for currentRow in [0..@buffer.getLastRow()] by 1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
@editor.createFold(startRow, endRow)
return
# Unfolds all the foldable lines in the buffer.
unfoldAll: ->
for row in [@buffer.getLastRow()..0] by -1
fold.destroy() for fold in @editor.displayBuffer.foldsStartingAtBufferRow(row)
return
# Fold all comment and code blocks at a given indentLevel
#
# indentLevel - A {Number} indicating indentLevel; 0 based.
foldAllAtIndentLevel: (indentLevel) ->
@unfoldAll()
for currentRow in [0..@buffer.getLastRow()] by 1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
# assumption: startRow will always be the min indent level for the entire range
if @editor.indentationForBufferRow(startRow) is indentLevel
@editor.createFold(startRow, endRow)
return
# 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] by -1
[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.tokenizedLineForRow(bufferRow).isComment()
startRow = bufferRow
endRow = bufferRow
if bufferRow > 0
for currentRow in [bufferRow-1..0] by -1
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
startRow = currentRow
if bufferRow < @buffer.getLastRow()
for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
break if @buffer.isRowBlank(currentRow)
break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
endRow = currentRow
return [startRow, endRow] if startRow isnt endRow
rowRangeForCodeFoldAtBufferRow: (bufferRow) ->
return null unless @isFoldableAtBufferRow(bufferRow)
startIndentLevel = @editor.indentationForBufferRow(bufferRow)
scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
for row in [(bufferRow + 1)..@editor.getLastBufferRow()] by 1
continue if @editor.isBufferRowBlank(row)
indentation = @editor.indentationForBufferRow(row)
if indentation <= startIndentLevel
includeRowInFold = indentation is startIndentLevel and @foldEndRegexForScopeDescriptor(scopeDescriptor)?.searchSync(@editor.lineTextForBufferRow(row))
foldEndRow = row if includeRowInFold
break
foldEndRow = row
[bufferRow, foldEndRow]
isFoldableAtBufferRow: (bufferRow) ->
@editor.displayBuffer.tokenizedBuffer.isFoldableAtRow(bufferRow)
# 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.tokenizedLineForRow(bufferRow).isComment()
# Find a row range for a 'paragraph' around specified bufferRow. 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) ->
scope = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
{commentStartString, commentEndString} = @commentStartAndEndStringsForScope(scope)
commentStartRegex = null
if commentStartString? and not commentEndString?
commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?')
commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})")
filterCommentStart = (line) ->
if commentStartRegex?
matches = commentStartRegex.searchSync(line)
line = line.substring(matches[0].end) if matches?.length
line
return unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(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) isnt isOriginalRowComment
break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(startRow - 1)))
startRow--
endRow = bufferRow
lastRow = @editor.getLastBufferRow()
while endRow < lastRow
break if @isLineCommentedAtBufferRow(endRow + 1) isnt isOriginalRowComment
break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(endRow + 1)))
endRow++
new Range([startRow, 0], [endRow, @editor.lineTextForBufferRow(endRow).length])
# 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, options) ->
line = @buffer.lineForRow(bufferRow)
tokenizedLine = @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow)
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
suggestedIndentForLineAtBufferRow: (bufferRow, line, options) ->
tokenizedLine = @editor.displayBuffer.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line)
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
suggestedIndentForTokenizedLineAtBufferRow: (bufferRow, line, tokenizedLine, options) ->
iterator = tokenizedLine.getTokenIterator()
iterator.next()
scopeDescriptor = new ScopeDescriptor(scopes: iterator.getScopes())
increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor)
decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
decreaseNextIndentRegex = @decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)
if options?.skipBlankLines ? true
precedingRow = @buffer.previousNonBlankRow(bufferRow)
return 0 unless precedingRow?
else
precedingRow = bufferRow - 1
return 0 if precedingRow < 0
desiredIndentLevel = @editor.indentationForBufferRow(precedingRow)
return desiredIndentLevel unless increaseIndentRegex
unless @editor.isBufferRowCommented(precedingRow)
precedingLine = @buffer.lineForRow(precedingRow)
desiredIndentLevel += 1 if increaseIndentRegex?.testSync(precedingLine)
desiredIndentLevel -= 1 if decreaseNextIndentRegex?.testSync(precedingLine)
unless @buffer.isRowBlank(precedingRow)
desiredIndentLevel -= 1 if decreaseIndentRegex?.testSync(line)
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] by 1 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] by 1
return
# Given a buffer row, this indents it.
#
# bufferRow - The row {Number}.
# options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}.
autoIndentBufferRow: (bufferRow, options) ->
indentLevel = @suggestedIndentForBufferRow(bufferRow, options)
@editor.setIndentationForBufferRow(bufferRow, indentLevel, options)
# Given a buffer row, this decreases the indentation.
#
# bufferRow - The row {Number}
autoDecreaseIndentForBufferRow: (bufferRow) ->
scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
return unless decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
line = @buffer.lineForRow(bufferRow)
return unless decreaseIndentRegex.testSync(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)
if increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor)
desiredIndentLevel -= 1 unless increaseIndentRegex.testSync(precedingLine)
if decreaseNextIndentRegex = @decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)
desiredIndentLevel -= 1 if decreaseNextIndentRegex.testSync(precedingLine)
if desiredIndentLevel >= 0 and desiredIndentLevel < currentIndentLevel
@editor.setIndentationForBufferRow(bufferRow, desiredIndentLevel)
getRegexForProperty: (scopeDescriptor, property) ->
if pattern = @config.get(property, scope: scopeDescriptor)
new OnigRegExp(pattern)
increaseIndentRegexForScopeDescriptor: (scopeDescriptor) ->
@getRegexForProperty(scopeDescriptor, 'editor.increaseIndentPattern')
decreaseIndentRegexForScopeDescriptor: (scopeDescriptor) ->
@getRegexForProperty(scopeDescriptor, 'editor.decreaseIndentPattern')
decreaseNextIndentRegexForScopeDescriptor: (scopeDescriptor) ->
@getRegexForProperty(scopeDescriptor, 'editor.decreaseNextIndentPattern')
foldEndRegexForScopeDescriptor: (scopeDescriptor) ->
@getRegexForProperty(scopeDescriptor, 'editor.foldEndPattern')
commentStartAndEndStringsForScope: (scope) ->
commentStartEntry = @config.getAll('editor.commentStart', {scope})[0]
commentEndEntry = _.find @config.getAll('editor.commentEnd', {scope}), (entry) ->
entry.scopeSelector is commentStartEntry.scopeSelector
commentStartString = commentStartEntry?.value
commentEndString = commentEndEntry?.value
{commentStartString, commentEndString}