mirror of
https://github.com/atom/atom.git
synced 2026-02-12 07:35:14 -05:00
Indentation is used to determine foldable regions, if the last line matches TextMate's foldEndRegex it is included in the foldable region.
282 lines
9.5 KiB
CoffeeScript
282 lines
9.5 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'
|
|
|
|
module.exports =
|
|
class DisplayBuffer
|
|
@idCounter: 1
|
|
lineMap: null
|
|
languageMode: null
|
|
tokenizedBuffer: null
|
|
activeFolds: null
|
|
foldsById: null
|
|
lastTokenizedBufferChangeEvent: null
|
|
|
|
constructor: (@buffer, options={}) ->
|
|
@id = @constructor.idCounter++
|
|
options.tabText ?= ' '
|
|
@languageMode = options.languageMode
|
|
@tokenizedBuffer = new TokenizedBuffer(@buffer, options)
|
|
@softWrapColumn = options.softWrapColumn ? Infinity
|
|
@activeFolds = {}
|
|
@foldsById = {}
|
|
@buildLineMap()
|
|
@tokenizedBuffer.on 'change', (e) => @lastTokenizedBufferChangeEvent = e
|
|
@buffer.on "change.displayBuffer#{@id}", (e) => @handleBufferChange(e)
|
|
|
|
buildLineMap: ->
|
|
@lineMap = new LineMap
|
|
@lineMap.insertAtBufferRow 0, @buildLinesForBufferRows(0, @buffer.getLastRow())
|
|
|
|
setSoftWrapColumn: (@softWrapColumn) ->
|
|
oldRange = @rangeForAllLines()
|
|
@buildLineMap()
|
|
newRange = @rangeForAllLines()
|
|
@trigger 'change', { oldRange, newRange, lineNumbersChanged: true }
|
|
|
|
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)
|
|
|
|
@trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange, lineNumbersChanged: true
|
|
|
|
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)
|
|
|
|
@trigger 'change', oldRange: oldScreenRange, newRange: newScreenRange, lineNumbersChanged: true
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
stateForScreenRow: (screenRow) ->
|
|
@tokenizedBuffer.stackForRow(screenRow)
|
|
|
|
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(@lastTokenizedBufferChangeEvent)
|
|
|
|
handleTokenizedBufferChange: (e) ->
|
|
newRange = e.newRange.copy()
|
|
newRange.start.row = @bufferRowForScreenRow(@screenRowForBufferRow(newRange.start.row))
|
|
|
|
oldScreenRange = @screenLineRangeForBufferRange(e.oldRange)
|
|
|
|
newScreenLines = @buildLinesForBufferRows(newRange.start.row, newRange.end.row)
|
|
@lineMap.replaceScreenRows oldScreenRange.start.row, oldScreenRange.end.row, newScreenLines
|
|
newScreenRange = @screenLineRangeForBufferRange(newRange)
|
|
|
|
@trigger 'change',
|
|
oldRange: oldScreenRange
|
|
newRange: newScreenRange
|
|
bufferChanged: true
|
|
lineNumbersChanged: !e.oldRange.coversSameRows(newRange) or !oldScreenRange.coversSameRows(newScreenRange)
|
|
|
|
buildLineForBufferRow: (bufferRow) ->
|
|
@buildLinesForBufferRows(bufferRow, bufferRow)
|
|
|
|
buildLinesForBufferRows: (startBufferRow, endBufferRow) ->
|
|
lineFragments = []
|
|
startBufferColumn = null
|
|
currentBufferRow = startBufferRow
|
|
currentScreenLineLength = 0
|
|
|
|
startBufferColumn = 0
|
|
while currentBufferRow <= endBufferRow
|
|
screenLine = @tokenizedBuffer.lineForScreenRow(currentBufferRow)
|
|
screenLine.foldable = @languageMode.doesBufferRowStartFold(currentBufferRow)
|
|
|
|
if fold = @largestFoldStartingAtBufferRow(currentBufferRow)
|
|
screenLine = screenLine.copy()
|
|
screenLine.fold = fold
|
|
screenLine.bufferDelta = fold.getBufferDelta()
|
|
lineFragments.push(screenLine)
|
|
currentBufferRow = fold.endRow + 1
|
|
continue
|
|
|
|
startBufferColumn ?= 0
|
|
screenLine = screenLine.splitAt(startBufferColumn)[1] if startBufferColumn > 0
|
|
wrapScreenColumn = @findWrapColumn(screenLine.text, @softWrapColumn)
|
|
if wrapScreenColumn?
|
|
screenLine = screenLine.splitAt(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]))
|
|
|
|
destroy: ->
|
|
@tokenizedBuffer.destroy()
|
|
@buffer.off ".displayBuffer#{@id}"
|
|
|
|
logLines: (start, end) ->
|
|
@lineMap.logLines(start, end)
|
|
|
|
_.extend DisplayBuffer.prototype, EventEmitter
|