mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-22 00:19:24 +03:00
301 lines
12 KiB
CoffeeScript
301 lines
12 KiB
CoffeeScript
{Range} = require 'telepath'
|
|
_ = require './underscore-extensions'
|
|
{OnigRegExp} = require 'oniguruma'
|
|
EventEmitter = require './event-emitter'
|
|
Subscriber = require './subscriber'
|
|
|
|
### Internal ###
|
|
|
|
module.exports =
|
|
class LanguageMode
|
|
_.extend @prototype, EventEmitter
|
|
_.extend @prototype, Subscriber
|
|
|
|
buffer: null
|
|
grammar: null
|
|
editSession: null
|
|
currentGrammarScore: null
|
|
|
|
### Internal ###
|
|
|
|
destroy: ->
|
|
@unsubscribe()
|
|
|
|
### Public ###
|
|
|
|
# Sets up a `LanguageMode` for the given {EditSession}.
|
|
#
|
|
# editSession - The {EditSession} to associate with
|
|
constructor: (@editSession) ->
|
|
@buffer = @editSession.buffer
|
|
|
|
# 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 = @editSession.scopesForBufferPosition([start, 0])
|
|
properties = syntax.propertiesForScope(scopes, "editor.commentStart")[0]
|
|
return unless properties
|
|
|
|
commentStartString = _.valueForKeyPath(properties, "editor.commentStart")
|
|
commentEndString = _.valueForKeyPath(properties, "editor.commentEnd")
|
|
|
|
return unless commentStartString
|
|
|
|
buffer = @editSession.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.change([[start, columnStart], [start, columnEnd]], "")
|
|
|
|
endLength = buffer.lineLengthForRow(end) - endMatch[2].length
|
|
endColumn = endLength - endMatch[1].length
|
|
buffer.change([[end, endColumn], [end, endLength]], "")
|
|
else
|
|
buffer.transact ->
|
|
buffer.insert([start, 0], 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.change([[row, columnStart], [row, columnEnd]], "")
|
|
else
|
|
indent = @minIndentLevelForRowRange(start, end)
|
|
indentString = @editSession.buildIndentString(indent)
|
|
for row in [start..end]
|
|
buffer.change([[row, 0], [row, indentString.length]], indentString + commentStartString)
|
|
|
|
# Folds all the foldable lines in the buffer.
|
|
foldAll: ->
|
|
for currentRow in [0..@buffer.getLastRow()]
|
|
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
|
|
continue unless startRow?
|
|
@editSession.createFold(startRow, endRow)
|
|
|
|
# Unfolds all the foldable lines in the buffer.
|
|
unfoldAll: ->
|
|
for row in [@buffer.getLastRow()..0]
|
|
fold.destroy() for fold in @editSession.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 [0..@buffer.getLastRow()]
|
|
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
|
|
continue unless startRow?
|
|
|
|
# assumption: startRow will always be the min indent level for the entire range
|
|
if @editSession.indentationForBufferRow(startRow) == indentLevel
|
|
@editSession.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 = @editSession.displayBuffer.largestFoldStartingAtBufferRow(startRow)
|
|
return @editSession.createFold(startRow, endRow) unless fold
|
|
|
|
# Given a buffer row, this unfolds it.
|
|
#
|
|
# bufferRow - A {Number} indicating the buffer row
|
|
unfoldBufferRow: (bufferRow) ->
|
|
@editSession.displayBuffer.largestFoldContainingBufferRow(bufferRow)?.destroy()
|
|
|
|
# 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 @editSession.displayBuffer.tokenizedBuffer.lineForScreenRow(bufferRow).isComment()
|
|
|
|
startRow = bufferRow
|
|
for currentRow in [bufferRow-1..0]
|
|
break if @buffer.isRowBlank(currentRow)
|
|
break unless @editSession.displayBuffer.tokenizedBuffer.lineForScreenRow(currentRow).isComment()
|
|
startRow = currentRow
|
|
endRow = bufferRow
|
|
for currentRow in [bufferRow+1..@buffer.getLastRow()]
|
|
break if @buffer.isRowBlank(currentRow)
|
|
break unless @editSession.displayBuffer.tokenizedBuffer.lineForScreenRow(currentRow).isComment()
|
|
endRow = currentRow
|
|
return [startRow, endRow] if startRow isnt endRow
|
|
|
|
rowRangeForCodeFoldAtBufferRow: (bufferRow) ->
|
|
return null unless @doesBufferRowStartFold(bufferRow)
|
|
|
|
startIndentLevel = @editSession.indentationForBufferRow(bufferRow)
|
|
scopes = @editSession.scopesForBufferPosition([bufferRow, 0])
|
|
for row in [(bufferRow + 1)..@editSession.getLastBufferRow()]
|
|
continue if @editSession.isBufferRowBlank(row)
|
|
indentation = @editSession.indentationForBufferRow(row)
|
|
if indentation <= startIndentLevel
|
|
includeRowInFold = indentation == startIndentLevel and @foldEndRegexForScopes(scopes)?.search(@editSession.lineForBufferRow(row))
|
|
foldEndRow = row if includeRowInFold
|
|
break
|
|
|
|
foldEndRow = row
|
|
|
|
[bufferRow, foldEndRow]
|
|
|
|
doesBufferRowStartFold: (bufferRow) ->
|
|
return false if @editSession.isBufferRowBlank(bufferRow)
|
|
nextNonEmptyRow = @editSession.nextNonBlankBufferRow(bufferRow)
|
|
return false unless nextNonEmptyRow?
|
|
@editSession.indentationForBufferRow(nextNonEmptyRow) > @editSession.indentationForBufferRow(bufferRow)
|
|
|
|
# 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(@editSession.lineForBufferRow(bufferRow))
|
|
|
|
isRowComment = (row) =>
|
|
@editSession.displayBuffer.tokenizedBuffer.lineForScreenRow(row).isComment()
|
|
|
|
if isRowComment(bufferRow)
|
|
isOriginalRowComment = true
|
|
range = @rowRangeForCommentAtBufferRow(bufferRow)
|
|
[firstRow, lastRow] = range or [bufferRow, bufferRow]
|
|
else
|
|
isOriginalRowComment = false
|
|
[firstRow, lastRow] = [0, @editSession.getLastBufferRow()-1]
|
|
|
|
startRow = bufferRow
|
|
while startRow > firstRow
|
|
break if isRowComment(startRow - 1) != isOriginalRowComment
|
|
break unless /\w/.test(@editSession.lineForBufferRow(startRow - 1))
|
|
startRow--
|
|
|
|
endRow = bufferRow
|
|
lastRow = @editSession.getLastBufferRow()
|
|
while endRow < lastRow
|
|
break if isRowComment(endRow + 1) != isOriginalRowComment
|
|
break unless /\w/.test(@editSession.lineForBufferRow(endRow + 1))
|
|
endRow++
|
|
|
|
new Range([startRow, 0], [endRow, @editSession.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 = @editSession.indentationForBufferRow(bufferRow)
|
|
scopes = @editSession.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 = @editSession.indentationForBufferRow(precedingRow)
|
|
desiredIndentLevel += 1 if increaseIndentRegex.test(precedingLine) and not @editSession.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 = (@editSession.indentationForBufferRow(row) for row in [startRow..endRow] when not @editSession.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}
|
|
autoIndentBufferRow: (bufferRow) ->
|
|
indentLevel = @suggestedIndentForBufferRow(bufferRow)
|
|
@editSession.setIndentationForBufferRow(bufferRow, indentLevel)
|
|
|
|
# Given a buffer row, this decreases the indentation.
|
|
#
|
|
# bufferRow - The row {Number}
|
|
autoDecreaseIndentForBufferRow: (bufferRow) ->
|
|
scopes = @editSession.scopesForBufferPosition([bufferRow, 0])
|
|
increaseIndentRegex = @increaseIndentRegexForScopes(scopes)
|
|
decreaseIndentRegex = @decreaseIndentRegexForScopes(scopes)
|
|
return unless increaseIndentRegex and decreaseIndentRegex
|
|
|
|
line = @buffer.lineForRow(bufferRow)
|
|
return unless decreaseIndentRegex.test(line)
|
|
|
|
currentIndentLevel = @editSession.indentationForBufferRow(bufferRow)
|
|
return if currentIndentLevel is 0
|
|
precedingRow = @buffer.previousNonBlankRow(bufferRow)
|
|
return unless precedingRow?
|
|
precedingLine = @buffer.lineForRow(precedingRow)
|
|
|
|
desiredIndentLevel = @editSession.indentationForBufferRow(precedingRow)
|
|
desiredIndentLevel -= 1 unless increaseIndentRegex.test(precedingLine)
|
|
if desiredIndentLevel >= 0 and desiredIndentLevel < currentIndentLevel
|
|
@editSession.setIndentationForBufferRow(bufferRow, desiredIndentLevel)
|
|
|
|
tokenizeLine: (line, stack, firstLine) ->
|
|
{tokens, stack} = @grammar.tokenizeLine(line, stack, firstLine)
|
|
|
|
increaseIndentRegexForScopes: (scopes) ->
|
|
if increaseIndentPattern = syntax.getProperty(scopes, 'editor.increaseIndentPattern')
|
|
new OnigRegExp(increaseIndentPattern)
|
|
|
|
decreaseIndentRegexForScopes: (scopes) ->
|
|
if decreaseIndentPattern = syntax.getProperty(scopes, 'editor.decreaseIndentPattern')
|
|
new OnigRegExp(decreaseIndentPattern)
|
|
|
|
foldEndRegexForScopes: (scopes) ->
|
|
if foldEndPattern = syntax.getProperty(scopes, 'editor.foldEndPattern')
|
|
new OnigRegExp(foldEndPattern)
|