Files
atom/src/app/tokenized-buffer.coffee

194 lines
6.6 KiB
CoffeeScript

_ = require 'underscore'
ScreenLine = require 'screen-line'
EventEmitter = require 'event-emitter'
Token = require 'token'
Range = require 'range'
Point = require 'point'
AceAdaptor = require 'ace-adaptor'
module.exports =
class TokenizedBuffer
@idCounter: 1
buffer: null
aceMode: null
aceAdaptor: null
screenLines: null
constructor: (@buffer, @tabText) ->
@id = @constructor.idCounter++
@aceMode = @requireAceMode()
@screenLines = @buildScreenLinesForRows('start', 0, @buffer.getLastRow())
@buffer.on "change.tokenized-buffer#{@id}", (e) => @handleBufferChange(e)
@aceAdaptor = new AceAdaptor(this)
requireAceMode: ->
modeName = switch @buffer.getExtension()
when 'js' then 'javascript'
when 'coffee' then 'coffee'
when 'rb', 'ru' then 'ruby'
when 'c', 'h', 'cpp' then 'c_cpp'
when 'html', 'htm' then 'html'
when 'css' then 'css'
when 'java' then 'java'
when 'xml' then 'xml'
else 'text'
new (require("ace/mode/#{modeName}").Mode)
toggleLineCommentsInRange: (range) ->
range = Range.fromObject(range)
@aceMode.toggleCommentLines(@stateForRow(range.start.row), @aceAdaptor, range.start.row, range.end.row)
isBufferRowFoldable: (bufferRow) ->
@aceMode.foldingRules?.getFoldWidget(@aceAdaptor, null, bufferRow) == "start"
rowRangeForFoldAtBufferRow: (bufferRow) ->
if aceRange = @aceMode.foldingRules?.getFoldWidgetRange(@aceAdaptor, null, bufferRow)
[aceRange.start.row, aceRange.end.row]
else
null
indentationForRow: (row) ->
state = @stateForRow(row)
previousRowText = @buffer.lineForRow(row - 1)
@aceMode.getNextLineIndent(state, previousRowText, @tabText)
autoIndentTextAfterBufferPosition: (text, bufferPosition) ->
{ row, column} = bufferPosition
state = @stateForRow(row)
lineBeforeCursor = @buffer.lineForRow(row)[0...column]
if text[0] == "\n"
indent = @aceMode.getNextLineIndent(state, lineBeforeCursor, @tabText)
text = text[0] + indent + text[1..]
else if @aceMode.checkOutdent(state, lineBeforeCursor, text)
shouldOutdent = true
{text, shouldOutdent}
autoOutdentBufferRow: (bufferRow) ->
state = @stateForRow(bufferRow)
@aceMode.autoOutdent(state, @aceAdaptor, bufferRow)
handleBufferChange: (e) ->
oldRange = e.oldRange.copy()
newRange = e.newRange.copy()
previousState = @stateForRow(oldRange.end.row) # used in spill detection below
startState = @stateForRow(newRange.start.row - 1)
@screenLines[oldRange.start.row..oldRange.end.row] =
@buildScreenLinesForRows(startState, newRange.start.row, newRange.end.row)
# spill detection
# compare scanner state of last re-highlighted line with its previous state.
# if it differs, re-tokenize the next line with the new state and repeat for
# each line until the line's new state matches the previous state. this covers
# cases like inserting a /* needing to comment out lines below until we see a */
for row in [newRange.end.row...@buffer.getLastRow()]
break if @stateForRow(row) == previousState
nextRow = row + 1
previousState = @stateForRow(nextRow)
@screenLines[nextRow] = @buildScreenLineForRow(@stateForRow(row), nextRow)
# if highlighting spilled beyond the bounds of the textual change, update
# the pre and post range to reflect area of highlight changes
if nextRow > newRange.end.row
oldRange.end.row += (nextRow - newRange.end.row)
newRange.end.row = nextRow
endColumn = @buffer.lineForRow(nextRow).length
newRange.end.column = endColumn
oldRange.end.column = endColumn
@trigger("change", {oldRange, newRange})
buildScreenLinesForRows: (startState, startRow, endRow) ->
state = startState
for row in [startRow..endRow]
screenLine = @buildScreenLineForRow(state, row)
state = screenLine.state
screenLine
buildScreenLineForRow: (state, row) ->
tokenizer = @aceMode.getTokenizer()
line = @buffer.lineForRow(row)
{tokens, state} = tokenizer.getLineTokens(line, state)
tokenObjects = []
for tokenProperties in tokens
token = new Token(tokenProperties)
tokenObjects.push(token.breakOutTabCharacters(@tabText)...)
text = _.pluck(tokenObjects, 'value').join('')
new ScreenLine(tokenObjects, text, [1, 0], [1, 0], { state })
lineForScreenRow: (row) ->
@screenLines[row]
linesForScreenRows: (startRow, endRow) ->
@screenLines[startRow..endRow]
stateForRow: (row) ->
@screenLines[row]?.state ? 'start'
destroy: ->
@buffer.off ".tokenized-buffer#{@id}"
iterateTokensInBufferRange: (bufferRange, iterator) ->
bufferRange = Range.fromObject(bufferRange)
{ start, end } = bufferRange
keepLooping = true
stop = -> keepLooping = false
for bufferRow in [start.row..end.row]
bufferColumn = 0
for token in @screenLines[bufferRow].tokens
startOfToken = new Point(bufferRow, bufferColumn)
iterator(token, startOfToken, { stop }) if bufferRange.containsPoint(startOfToken)
return unless keepLooping
bufferColumn += token.bufferDelta
backwardsIterateTokensInBufferRange: (bufferRange, iterator) ->
bufferRange = Range.fromObject(bufferRange)
{ start, end } = bufferRange
keepLooping = true
stop = -> keepLooping = false
for bufferRow in [end.row..start.row]
bufferColumn = @buffer.lineLengthForRow(bufferRow)
for token in new Array(@screenLines[bufferRow].tokens...).reverse()
bufferColumn -= token.bufferDelta
startOfToken = new Point(bufferRow, bufferColumn)
iterator(token, startOfToken, { stop }) if bufferRange.containsPoint(startOfToken)
return unless keepLooping
findOpeningBracket: (startBufferPosition) ->
range = [[0,0], startBufferPosition]
position = null
depth = 0
@backwardsIterateTokensInBufferRange range, (token, startPosition, { stop }) ->
if token.type.match /lparen|rparen/
if token.value == '}'
depth++
else if token.value == '{'
depth--
if depth == 0
position = startPosition
stop()
position
findClosingBracket: (startBufferPosition) ->
range = [startBufferPosition, @buffer.getEofPosition()]
position = null
depth = 0
@iterateTokensInBufferRange range, (token, startPosition, { stop }) ->
if token.type.match /lparen|rparen/
if token.value == '{'
depth++
else if token.value == '}'
depth--
if depth == 0
position = startPosition
stop()
position
_.extend(TokenizedBuffer.prototype, EventEmitter)