Files
atom/src/app/display-buffer.coffee
Nathan Sobo afe0a6570a Reduce observation to single observeMarker method on display buffer
It will call the observer callbacks with the buffer and screen positions
of the head and tell each time any of these values is changed.
2013-02-04 21:24:06 -07:00

386 lines
12 KiB
CoffeeScript

_ = require 'underscore'
TokenizedBuffer = require 'tokenized-buffer'
LineMap = require 'line-map'
Point = require 'point'
EventEmitter = require 'event-emitter'
Range = require 'range'
Fold = require 'fold'
ScreenLine = require 'screen-line'
Token = require 'token'
DisplayBufferMarker = require 'display-buffer-marker'
module.exports =
class DisplayBuffer
@idCounter: 1
lineMap: null
languageMode: null
tokenizedBuffer: null
activeFolds: null
foldsById: null
markers: null
constructor: (@buffer, options={}) ->
@id = @constructor.idCounter++
@languageMode = options.languageMode
@tokenizedBuffer = new TokenizedBuffer(@buffer, options)
@softWrapColumn = options.softWrapColumn ? Infinity
@activeFolds = {}
@foldsById = {}
@markers = {}
@buildLineMap()
@tokenizedBuffer.on 'changed', (e) => @handleTokenizedBufferChange(e)
setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
buildLineMap: ->
@lineMap = new LineMap
@lineMap.insertAtScreenRow 0, @buildLinesForBufferRows(0, @buffer.getLastRow())
triggerChanged: (eventProperties, refreshMarkers=true) ->
@refreshMarkerScreenPositions() if refreshMarkers
@trigger 'changed', eventProperties
setSoftWrapColumn: (@softWrapColumn) ->
start = 0
end = @getLastRow()
@buildLineMap()
screenDelta = @getLastRow() - end
bufferDelta = 0
@triggerChanged({ start, end, screenDelta, bufferDelta })
lineForRow: (row) ->
@lineMap.lineForScreenRow(row)
linesForRows: (startRow, endRow) ->
@lineMap.linesForScreenRows(startRow, endRow)
getLines: ->
@lineMap.linesForScreenRows(0, @lineMap.lastScreenRow())
bufferRowsForScreenRows: (startRow, endRow) ->
@lineMap.bufferRowsForScreenRows(startRow, endRow)
foldAll: ->
for currentRow in [0..@buffer.getLastRow()]
[startRow, endRow] = @languageMode.rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
@createFold(startRow, endRow)
unfoldAll: ->
for row in [@buffer.getLastRow()..0]
@activeFolds[row]?.forEach (fold) => @destroyFold(fold)
foldBufferRow: (bufferRow) ->
for currentRow in [bufferRow..0]
[startRow, endRow] = @languageMode.rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow? and startRow <= bufferRow <= endRow
fold = @largestFoldStartingAtBufferRow(startRow)
continue if fold
@createFold(startRow, endRow)
return
unfoldBufferRow: (bufferRow) ->
@largestFoldContainingBufferRow(bufferRow)?.destroy()
createFold: (startRow, endRow) ->
return fold if fold = @foldFor(startRow, endRow)
fold = new Fold(this, startRow, endRow)
@registerFold(fold)
unless @isFoldContainedByActiveFold(fold)
bufferRange = new Range([startRow, 0], [endRow, @buffer.lineLengthForRow(endRow)])
oldScreenRange = @screenLineRangeForBufferRange(bufferRange)
lines = @buildLineForBufferRow(startRow)
@lineMap.replaceScreenRows(oldScreenRange.start.row, oldScreenRange.end.row, lines)
newScreenRange = @screenLineRangeForBufferRange(bufferRange)
start = oldScreenRange.start.row
end = oldScreenRange.end.row
screenDelta = newScreenRange.end.row - oldScreenRange.end.row
bufferDelta = 0
@triggerChanged({ start, end, screenDelta, bufferDelta })
fold
isFoldContainedByActiveFold: (fold) ->
for row, folds of @activeFolds
for otherFold in folds
return otherFold if fold != otherFold and fold.isContainedByFold(otherFold)
foldFor: (startRow, endRow) ->
_.find @activeFolds[startRow] ? [], (fold) ->
fold.startRow == startRow and fold.endRow == endRow
destroyFold: (fold) ->
@unregisterFold(fold.startRow, fold)
unless @isFoldContainedByActiveFold(fold)
{ startRow, endRow } = fold
bufferRange = new Range([startRow, 0], [endRow, @buffer.lineLengthForRow(endRow)])
oldScreenRange = @screenLineRangeForBufferRange(bufferRange)
lines = @buildLinesForBufferRows(startRow, endRow)
@lineMap.replaceScreenRows(oldScreenRange.start.row, oldScreenRange.end.row, lines)
newScreenRange = @screenLineRangeForBufferRange(bufferRange)
start = oldScreenRange.start.row
end = oldScreenRange.end.row
screenDelta = newScreenRange.end.row - oldScreenRange.end.row
bufferDelta = 0
@refreshMarkerScreenPositions()
@triggerChanged({ start, end, screenDelta, bufferDelta })
destroyFoldsContainingBufferRow: (bufferRow) ->
for row, folds of @activeFolds
for fold in new Array(folds...)
fold.destroy() if fold.getBufferRange().containsRow(bufferRow)
registerFold: (fold) ->
@activeFolds[fold.startRow] ?= []
@activeFolds[fold.startRow].push(fold)
@foldsById[fold.id] = fold
unregisterFold: (bufferRow, fold) ->
folds = @activeFolds[bufferRow]
_.remove(folds, fold)
delete @foldsById[fold.id]
delete @activeFolds[bufferRow] if folds.length == 0
largestFoldStartingAtBufferRow: (bufferRow) ->
return unless folds = @activeFolds[bufferRow]
(folds.sort (a, b) -> b.endRow - a.endRow)[0]
largestFoldStartingAtScreenRow: (screenRow) ->
@largestFoldStartingAtBufferRow(@bufferRowForScreenRow(screenRow))
largestFoldContainingBufferRow: (bufferRow) ->
largestFold = null
for currentBufferRow in [bufferRow..0]
if fold = @largestFoldStartingAtBufferRow(currentBufferRow)
largestFold = fold if fold.endRow >= bufferRow
largestFold
screenLineRangeForBufferRange: (bufferRange) ->
@expandScreenRangeToLineEnds(
@lineMap.screenRangeForBufferRange(
@expandBufferRangeToLineEnds(bufferRange)))
screenRowForBufferRow: (bufferRow) ->
@lineMap.screenPositionForBufferPosition([bufferRow, 0]).row
lastScreenRowForBufferRow: (bufferRow) ->
@lineMap.screenPositionForBufferPosition([bufferRow, Infinity]).row
bufferRowForScreenRow: (screenRow) ->
@lineMap.bufferPositionForScreenPosition([screenRow, 0]).row
screenRangeForBufferRange: (bufferRange) ->
@lineMap.screenRangeForBufferRange(bufferRange)
bufferRangeForScreenRange: (screenRange) ->
@lineMap.bufferRangeForScreenRange(screenRange)
lineCount: ->
@lineMap.screenLineCount()
getLastRow: ->
@lineCount() - 1
maxLineLength: ->
@lineMap.maxScreenLineLength
screenPositionForBufferPosition: (position, options) ->
@lineMap.screenPositionForBufferPosition(position, options)
bufferPositionForScreenPosition: (position, options) ->
@lineMap.bufferPositionForScreenPosition(position, options)
scopesForBufferPosition: (bufferPosition) ->
@tokenizedBuffer.scopesForPosition(bufferPosition)
getTabLength: ->
@tokenizedBuffer.getTabLength()
setTabLength: (tabLength) ->
@tokenizedBuffer.setTabLength(tabLength)
clipScreenPosition: (position, options) ->
@lineMap.clipScreenPosition(position, options)
handleBufferChange: (e) ->
allFolds = [] # Folds can modify @activeFolds, so first make sure we have a stable array of folds
allFolds.push(folds...) for row, folds of @activeFolds
fold.handleBufferChange(e) for fold in allFolds
handleTokenizedBufferChange: (tokenizedBufferChange) ->
if bufferChange = tokenizedBufferChange.bufferChange
@handleBufferChange(bufferChange)
bufferDelta = bufferChange.newRange.end.row - bufferChange.oldRange.end.row
tokenizedBufferStart = @bufferRowForScreenRow(@screenRowForBufferRow(tokenizedBufferChange.start))
tokenizedBufferEnd = tokenizedBufferChange.end
tokenizedBufferDelta = tokenizedBufferChange.delta
start = @screenRowForBufferRow(tokenizedBufferStart)
end = @lastScreenRowForBufferRow(tokenizedBufferEnd)
newScreenLines = @buildLinesForBufferRows(tokenizedBufferStart, tokenizedBufferEnd + tokenizedBufferDelta)
@lineMap.replaceScreenRows(start, end, newScreenLines)
screenDelta = @lastScreenRowForBufferRow(tokenizedBufferEnd + tokenizedBufferDelta) - end
@triggerChanged({ start, end, screenDelta, bufferDelta }, false)
buildLineForBufferRow: (bufferRow) ->
@buildLinesForBufferRows(bufferRow, bufferRow)
buildLinesForBufferRows: (startBufferRow, endBufferRow) ->
lineFragments = []
startBufferColumn = null
currentBufferRow = startBufferRow
currentScreenLineLength = 0
startBufferColumn = 0
while currentBufferRow <= endBufferRow
screenLine = @tokenizedBuffer.lineForScreenRow(currentBufferRow)
if fold = @largestFoldStartingAtBufferRow(currentBufferRow)
screenLine = screenLine.copy()
screenLine.fold = fold
screenLine.bufferRows = fold.getBufferRowCount()
lineFragments.push(screenLine)
currentBufferRow = fold.endRow + 1
continue
startBufferColumn ?= 0
screenLine = screenLine.softWrapAt(startBufferColumn)[1] if startBufferColumn > 0
wrapScreenColumn = @findWrapColumn(screenLine.text, @softWrapColumn)
if wrapScreenColumn?
screenLine = screenLine.softWrapAt(wrapScreenColumn)[0]
screenLine.screenDelta = new Point(1, 0)
startBufferColumn += wrapScreenColumn
else
currentBufferRow++
startBufferColumn = 0
lineFragments.push(screenLine)
lineFragments
findWrapColumn: (line, softWrapColumn) ->
return unless line.length > softWrapColumn
if /\s/.test(line[softWrapColumn])
# search forward for the start of a word past the boundary
for column in [softWrapColumn..line.length]
return column if /\S/.test(line[column])
return line.length
else
# search backward for the start of the word on the boundary
for column in [softWrapColumn..0]
return column + 1 if /\s/.test(line[column])
return softWrapColumn
expandScreenRangeToLineEnds: (screenRange) ->
screenRange = Range.fromObject(screenRange)
{ start, end } = screenRange
new Range([start.row, 0], [end.row, @lineMap.lineForScreenRow(end.row).text.length])
expandBufferRangeToLineEnds: (bufferRange) ->
bufferRange = Range.fromObject(bufferRange)
{ start, end } = bufferRange
new Range([start.row, 0], [end.row, Infinity])
rangeForAllLines: ->
new Range([0, 0], @clipScreenPosition([Infinity, Infinity]))
getMarker: (id) ->
@markers[id] ? new DisplayBufferMarker({id, displayBuffer: this})
getMarkers: ->
_.values(@markers)
markScreenRange: (screenRange) ->
@markBufferRange(@bufferRangeForScreenRange(screenRange))
markBufferRange: (args...) ->
@buffer.markRange(args...)
markScreenPosition: (screenPosition, options) ->
@markBufferPosition(@bufferPositionForScreenPosition(screenPosition), options)
markBufferPosition: (bufferPosition, options) ->
@buffer.markPosition(bufferPosition, options)
destroyMarker: (id) ->
@buffer.destroyMarker(id)
delete @markers[id]
getMarkerScreenRange: (id) ->
@getMarker(id).getScreenRange()
setMarkerScreenRange: (id, screenRange, options) ->
@getMarker(id).setScreenRange(screenRange, options)
getMarkerBufferRange: (id) ->
@getMarker(id).getBufferRange()
setMarkerBufferRange: (id, bufferRange, options) ->
@getMarker(id).setBufferRange(bufferRange, options)
getMarkerScreenPosition: (id) ->
@getMarkerHeadScreenPosition(id)
getMarkerBufferPosition: (id) ->
@getMarkerHeadBufferPosition(id)
getMarkerHeadScreenPosition: (id) ->
@getMarker(id).getHeadScreenPosition()
setMarkerHeadScreenPosition: (id, screenPosition, options) ->
@getMarker(id).setHeadScreenPosition(screenPosition, options)
getMarkerHeadBufferPosition: (id) ->
@getMarker(id).getHeadBufferPosition()
setMarkerHeadBufferPosition: (id, bufferPosition) ->
@getMarker(id).setHeadBufferPosition(bufferPosition)
getMarkerTailScreenPosition: (id) ->
@getMarker(id).getTailScreenPosition()
setMarkerTailScreenPosition: (id, screenPosition, options) ->
@getMarker(id).setTailScreenPosition(screenPosition, options)
getMarkerTailBufferPosition: (id) ->
@getMarker(id).getTailBufferPosition()
setMarkerTailBufferPosition: (id, bufferPosition) ->
@getMarker(id).setTailBufferPosition(bufferPosition)
placeMarkerTail: (id) ->
@getMarker(id).placeTail()
clearMarkerTail: (id) ->
@getMarker(id).clearTail()
isMarkerReversed: (id) ->
@buffer.isMarkerReversed(id)
observeMarker: (id, callback) ->
@getMarker(id).observe(callback)
refreshMarkerScreenPositions: ->
for marker in @getMarkers()
marker.notifyObservers(bufferChanged: false)
destroy: ->
@tokenizedBuffer.destroy()
logLines: (start, end) ->
@lineMap.logLines(start, end)
_.extend DisplayBuffer.prototype, EventEmitter