mirror of
https://github.com/atom/atom.git
synced 2026-02-17 01:51:54 -05:00
552 lines
18 KiB
CoffeeScript
552 lines
18 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'
|
|
Subscriber = require 'subscriber'
|
|
|
|
module.exports =
|
|
class DisplayBuffer
|
|
@idCounter: 1
|
|
lineMap: null
|
|
tokenizedBuffer: null
|
|
markers: null
|
|
foldsByMarkerId: null
|
|
|
|
###
|
|
# Internal #
|
|
###
|
|
|
|
constructor: (@buffer, options={}) ->
|
|
@id = @constructor.idCounter++
|
|
@tokenizedBuffer = new TokenizedBuffer(@buffer, options)
|
|
@softWrapColumn = options.softWrapColumn ? Infinity
|
|
@markers = {}
|
|
@foldsByMarkerId = {}
|
|
@buildLineMap()
|
|
@tokenizedBuffer.on 'grammar-changed', (grammar) => @trigger 'grammar-changed', grammar
|
|
@tokenizedBuffer.on 'changed', @handleTokenizedBufferChange
|
|
@buffer.on 'markers-updated', @handleMarkersUpdated
|
|
@buffer.on 'marker-created', @handleMarkerCreated
|
|
|
|
buildLineMap: ->
|
|
@lineMap = new LineMap
|
|
@lineMap.insertAtScreenRow 0, @buildLinesForBufferRows(0, @buffer.getLastRow())
|
|
|
|
triggerChanged: (eventProperties, refreshMarkers=true) ->
|
|
if refreshMarkers
|
|
@pauseMarkerObservers()
|
|
@refreshMarkerScreenPositions()
|
|
@trigger 'changed', eventProperties
|
|
@resumeMarkerObservers()
|
|
|
|
###
|
|
# Public #
|
|
###
|
|
|
|
setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
|
|
|
|
# Public: Defines the limit at which the buffer begins to soft wrap text.
|
|
#
|
|
# softWrapColumn - A {Number} defining the soft wrap limit.
|
|
setSoftWrapColumn: (@softWrapColumn) ->
|
|
start = 0
|
|
end = @getLastRow()
|
|
@buildLineMap()
|
|
screenDelta = @getLastRow() - end
|
|
bufferDelta = 0
|
|
@triggerChanged({ start, end, screenDelta, bufferDelta })
|
|
|
|
# Public: Gets the screen line for the given screen row.
|
|
#
|
|
# screenRow - A {Number} indicating the screen row.
|
|
#
|
|
# Returns a {ScreenLine}.
|
|
lineForRow: (row) ->
|
|
@lineMap.lineForScreenRow(row)
|
|
|
|
# Public: Gets the screen lines for the given screen row range.
|
|
#
|
|
# startRow - A {Number} indicating the beginning screen row.
|
|
# endRow - A {Number} indicating the ending screen row.
|
|
#
|
|
# Returns an {Array} of {ScreenLine}s.
|
|
linesForRows: (startRow, endRow) ->
|
|
@lineMap.linesForScreenRows(startRow, endRow)
|
|
|
|
# Public: Gets all the screen lines.
|
|
#
|
|
# Returns an {Array} of {ScreenLines}s.
|
|
getLines: ->
|
|
@lineMap.linesForScreenRows(0, @lineMap.lastScreenRow())
|
|
|
|
|
|
# Public: Given starting and ending screen rows, this returns an array of the
|
|
# buffer rows corresponding to every screen row in the range
|
|
#
|
|
# startRow - The screen row {Number} to start at
|
|
# endRow - The screen row {Number} to end at (default: the last screen row)
|
|
#
|
|
# Returns an {Array} of buffer rows as {Numbers}s.
|
|
bufferRowsForScreenRows: (startRow, endRow) ->
|
|
@lineMap.bufferRowsForScreenRows(startRow, endRow)
|
|
|
|
# Public: Creates a new fold between two row numbers.
|
|
#
|
|
# startRow - The row {Number} to start folding at
|
|
# endRow - The row {Number} to end the fold
|
|
#
|
|
# Returns the new {Fold}.
|
|
createFold: (startRow, endRow) ->
|
|
foldMarker =
|
|
@findMarker({class: 'fold', startRow, endRow}) ?
|
|
@markBufferRange([[startRow, 0], [endRow, Infinity]], class: 'fold')
|
|
@foldForMarker(foldMarker)
|
|
|
|
# Public: Removes any folds found that contain the given buffer row.
|
|
#
|
|
# bufferRow - The buffer row {Number} to check against
|
|
destroyFoldsContainingBufferRow: (bufferRow) ->
|
|
for marker in @findMarkers(class: 'fold', containsBufferRow: bufferRow)
|
|
marker.destroy()
|
|
|
|
# Largest is defined as the fold whose difference between its start and end rows
|
|
# is the greatest.
|
|
#
|
|
# bufferRow - A {Number} indicating the buffer row
|
|
#
|
|
# Returns a {Fold} or null if none exists.
|
|
largestFoldStartingAtBufferRow: (bufferRow) ->
|
|
@foldsStartingAtBufferRow(bufferRow)[0]
|
|
|
|
# Public: Given a buffer row, this returns all folds that start there.
|
|
#
|
|
# bufferRow - A {Number} indicating the buffer row
|
|
#
|
|
# Returns an {Array} of {Fold}s.
|
|
foldsStartingAtBufferRow: (bufferRow) ->
|
|
for marker in @findMarkers(class: 'fold', startBufferRow: bufferRow)
|
|
@foldForMarker(marker)
|
|
|
|
# Public: Given a screen row, this returns the largest fold that starts there.
|
|
#
|
|
# Largest is defined as the fold whose difference between its start and end points
|
|
# are the greatest.
|
|
#
|
|
# screenRow - A {Number} indicating the screen row
|
|
#
|
|
# Returns a {Fold}.
|
|
largestFoldStartingAtScreenRow: (screenRow) ->
|
|
@largestFoldStartingAtBufferRow(@bufferRowForScreenRow(screenRow))
|
|
|
|
# Public: Given a buffer row, this returns the largest fold that includes it.
|
|
#
|
|
# Largest is defined as the fold whose difference between its start and end points
|
|
# are the greatest.
|
|
#
|
|
# bufferRow - A {Number} indicating the buffer row
|
|
#
|
|
# Returns a {Fold}.
|
|
largestFoldContainingBufferRow: (bufferRow) ->
|
|
largestFold = null
|
|
for currentBufferRow in [bufferRow..0]
|
|
if fold = @largestFoldStartingAtBufferRow(currentBufferRow)
|
|
largestFold = fold if fold.getEndRow() >= bufferRow
|
|
largestFold
|
|
|
|
largestFoldStartingAtBufferRange: (bufferRange) ->
|
|
if marker = @findMarker(class: 'fold', containingBufferRange: bufferRange)
|
|
@foldForMarker(marker)
|
|
|
|
# Public: Given a buffer range, this converts it into a screen range.
|
|
#
|
|
# bufferRange - A {Range} consisting of buffer positions
|
|
#
|
|
# Returns a {Range}.
|
|
screenLineRangeForBufferRange: (bufferRange) ->
|
|
@expandScreenRangeToLineEnds(
|
|
@lineMap.screenRangeForBufferRange(
|
|
@expandBufferRangeToLineEnds(bufferRange)))
|
|
|
|
# Public: Given a buffer row, this converts it into a screen row.
|
|
#
|
|
# bufferRow - A {Number} representing a buffer row
|
|
#
|
|
# Returns a {Number}.
|
|
screenRowForBufferRow: (bufferRow) ->
|
|
@lineMap.screenPositionForBufferPosition([bufferRow, 0]).row
|
|
|
|
lastScreenRowForBufferRow: (bufferRow) ->
|
|
@lineMap.screenPositionForBufferPosition([bufferRow, Infinity]).row
|
|
|
|
# Public: Given a screen row, this converts it into a buffer row.
|
|
#
|
|
# screenRow - A {Number} representing a screen row
|
|
#
|
|
# Returns a {Number}.
|
|
bufferRowForScreenRow: (screenRow) ->
|
|
@lineMap.bufferPositionForScreenPosition([screenRow, 0]).row
|
|
|
|
# Public: Given a buffer range, this converts it into a screen position.
|
|
#
|
|
# bufferRange - The {Range} to convert
|
|
#
|
|
# Returns a {Range}.
|
|
screenRangeForBufferRange: (bufferRange) ->
|
|
@lineMap.screenRangeForBufferRange(bufferRange)
|
|
|
|
# Public: Given a screen range, this converts it into a buffer position.
|
|
#
|
|
# screenRange - The {Range} to convert
|
|
#
|
|
# Returns a {Range}.
|
|
bufferRangeForScreenRange: (screenRange) ->
|
|
@lineMap.bufferRangeForScreenRange(screenRange)
|
|
|
|
# Public: Gets the number of lines in the buffer.
|
|
#
|
|
# Returns a {Number}.
|
|
getLineCount: ->
|
|
@lineMap.getScreenLineCount()
|
|
|
|
# Public: Gets the number of the last row in the buffer.
|
|
#
|
|
# Returns a {Number}.
|
|
getLastRow: ->
|
|
@getLineCount() - 1
|
|
|
|
# Public: Gets the length of the longest screen line.
|
|
#
|
|
# Returns a {Number}.
|
|
maxLineLength: ->
|
|
@lineMap.maxScreenLineLength
|
|
|
|
# Public: Given a buffer position, this converts it into a screen position.
|
|
#
|
|
# bufferPosition - An object that represents a buffer position. It can be either
|
|
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
|
|
# options - A hash of options with the following keys:
|
|
# :wrapBeyondNewlines -
|
|
# :wrapAtSoftNewlines -
|
|
#
|
|
# Returns a {Point}.
|
|
screenPositionForBufferPosition: (position, options) ->
|
|
@lineMap.screenPositionForBufferPosition(position, options)
|
|
|
|
# Public: Given a buffer range, this converts it into a screen position.
|
|
#
|
|
# screenPosition - An object that represents a buffer position. It can be either
|
|
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
|
|
# options - A hash of options with the following keys:
|
|
# :wrapBeyondNewlines -
|
|
# :wrapAtSoftNewlines -
|
|
#
|
|
# Returns a {Point}.
|
|
bufferPositionForScreenPosition: (position, options) ->
|
|
@lineMap.bufferPositionForScreenPosition(position, options)
|
|
|
|
# Public: Retrieves the grammar's token scopes for a buffer position.
|
|
#
|
|
# bufferPosition - A {Point} in the {Buffer}
|
|
#
|
|
# Returns an {Array} of {String}s.
|
|
scopesForBufferPosition: (bufferPosition) ->
|
|
@tokenizedBuffer.scopesForPosition(bufferPosition)
|
|
|
|
# Public: Retrieves the grammar's token for a buffer position.
|
|
#
|
|
# bufferPosition - A {Point} in the {Buffer}.
|
|
#
|
|
# Returns a {Token}.
|
|
tokenForBufferPosition: (bufferPosition) ->
|
|
@tokenizedBuffer.tokenForPosition(bufferPosition)
|
|
|
|
# Public: Retrieves the current tab length.
|
|
#
|
|
# Returns a {Number}.
|
|
getTabLength: ->
|
|
@tokenizedBuffer.getTabLength()
|
|
|
|
# Public: Specifies the tab length.
|
|
#
|
|
# tabLength - A {Number} that defines the new tab length.
|
|
setTabLength: (tabLength) ->
|
|
@tokenizedBuffer.setTabLength(tabLength)
|
|
|
|
getGrammar: ->
|
|
@tokenizedBuffer.grammar
|
|
|
|
setGrammar: (grammar) ->
|
|
@tokenizedBuffer.setGrammar(grammar)
|
|
|
|
reloadGrammar: ->
|
|
@tokenizedBuffer.reloadGrammar()
|
|
|
|
# Public: Given a position, this clips it to a real position.
|
|
#
|
|
# For example, if `position`'s row exceeds the row count of the buffer,
|
|
# or if its column goes beyond a line's length, this "sanitizes" the value
|
|
# to a real position.
|
|
#
|
|
# position - The {Point} to clip
|
|
# options - A hash with the following values:
|
|
# :wrapBeyondNewlines - if `true`, continues wrapping past newlines
|
|
# :wrapAtSoftNewlines - if `true`, continues wrapping past soft newlines
|
|
# :screenLine - if `true`, indicates that you're using a line number, not a row number
|
|
#
|
|
# Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed.
|
|
clipScreenPosition: (position, options) ->
|
|
@lineMap.clipScreenPosition(position, options)
|
|
|
|
###
|
|
# Internal #
|
|
###
|
|
|
|
handleTokenizedBufferChange: (tokenizedBufferChange) =>
|
|
{start, end, delta, bufferChange} = tokenizedBufferChange
|
|
@updateScreenLines(start, end, delta, delayChangeEvent: bufferChange?)
|
|
|
|
updateScreenLines: (startBufferRow, endBufferRow, bufferDelta, options={}) ->
|
|
startBufferRow = @bufferRowForScreenRow(@screenRowForBufferRow(startBufferRow))
|
|
newScreenLines = @buildLinesForBufferRows(startBufferRow, endBufferRow + bufferDelta)
|
|
|
|
startScreenRow = @screenRowForBufferRow(startBufferRow)
|
|
endScreenRow = @lastScreenRowForBufferRow(endBufferRow)
|
|
|
|
@lineMap.replaceScreenRows(startScreenRow, endScreenRow, newScreenLines)
|
|
|
|
changeEvent =
|
|
start: startScreenRow
|
|
end: endScreenRow
|
|
screenDelta: @lastScreenRowForBufferRow(endBufferRow + bufferDelta) - endScreenRow
|
|
bufferDelta: bufferDelta
|
|
|
|
if options.delayChangeEvent
|
|
@pauseMarkerObservers()
|
|
@pendingChangeEvent = changeEvent
|
|
else
|
|
@triggerChanged(changeEvent, options.refreshMarkers)
|
|
|
|
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.getEndRow() + 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
|
|
|
|
handleMarkersUpdated: =>
|
|
event = @pendingChangeEvent
|
|
@pendingChangeEvent = null
|
|
@triggerChanged(event, false)
|
|
|
|
handleMarkerCreated: (marker) =>
|
|
marker = @getMarker(marker.id)
|
|
new Fold(this, marker) if marker.matchesAttributes(class: 'fold')
|
|
@trigger 'marker-created', marker
|
|
|
|
buildFoldForMarker: (marker) ->
|
|
|
|
|
|
foldForMarker: (marker) ->
|
|
@foldsByMarkerId[marker.id]
|
|
|
|
###
|
|
# Public #
|
|
###
|
|
|
|
# Public: Given a line, finds the point where it would wrap.
|
|
#
|
|
# line - The {String} to check
|
|
# softWrapColumn - The {Number} where you want soft wrapping to occur
|
|
#
|
|
# Returns a {Number} representing the `line` position where the wrap would take place.
|
|
# Returns `null` if a wrap wouldn't occur.
|
|
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
|
|
|
|
# Public: Given a range in screen coordinates, this expands it to the start and end of a line
|
|
#
|
|
# screenRange - The {Range} to expand
|
|
#
|
|
# Returns a new {Range}.
|
|
expandScreenRangeToLineEnds: (screenRange) ->
|
|
screenRange = Range.fromObject(screenRange)
|
|
{ start, end } = screenRange
|
|
new Range([start.row, 0], [end.row, @lineMap.lineForScreenRow(end.row).text.length])
|
|
|
|
# Public: Given a range in buffer coordinates, this expands it to the start and end of a line
|
|
#
|
|
# screenRange - The {Range} to expand
|
|
#
|
|
# Returns a new {Range}.
|
|
expandBufferRangeToLineEnds: (bufferRange) ->
|
|
bufferRange = Range.fromObject(bufferRange)
|
|
{ start, end } = bufferRange
|
|
new Range([start.row, 0], [end.row, Infinity])
|
|
|
|
# Public: Calculates a {Range} representing the start of the {Buffer} until the end.
|
|
#
|
|
# Returns a {Range}.
|
|
rangeForAllLines: ->
|
|
new Range([0, 0], @clipScreenPosition([Infinity, Infinity]))
|
|
|
|
# Public: Retrieves a {DisplayBufferMarker} based on its id.
|
|
#
|
|
# id - A {Number} representing a marker id
|
|
#
|
|
# Returns the {DisplayBufferMarker} (if it exists).
|
|
getMarker: (id) ->
|
|
@markers[id] ?= do =>
|
|
if bufferMarker = @buffer.getMarker(id)
|
|
new DisplayBufferMarker({bufferMarker, displayBuffer: this})
|
|
|
|
# Public: Retrieves the active markers in the buffer.
|
|
#
|
|
# Returns an {Array} of existing {DisplayBufferMarker}s.
|
|
getMarkers: ->
|
|
_.values(@markers)
|
|
|
|
# Public: Constructs a new marker at the given screen range.
|
|
#
|
|
# range - The marker {Range} (representing the distance between the head and tail)
|
|
# options - Options to pass to the {BufferMarker} constructor
|
|
#
|
|
# Returns a {Number} representing the new marker's ID.
|
|
markScreenRange: (args...) ->
|
|
bufferRange = @bufferRangeForScreenRange(args.shift())
|
|
@markBufferRange(bufferRange, args...)
|
|
|
|
# Public: Constructs a new marker at the given buffer range.
|
|
#
|
|
# range - The marker {Range} (representing the distance between the head and tail)
|
|
# options - Options to pass to the {BufferMarker} constructor
|
|
#
|
|
# Returns a {Number} representing the new marker's ID.
|
|
markBufferRange: (args...) ->
|
|
@getMarker(@buffer.markRange(args...).id)
|
|
|
|
# Public: Constructs a new marker at the given screen position.
|
|
#
|
|
# range - The marker {Range} (representing the distance between the head and tail)
|
|
# options - Options to pass to the {BufferMarker} constructor
|
|
#
|
|
# Returns a {Number} representing the new marker's ID.
|
|
markScreenPosition: (screenPosition, options) ->
|
|
@markBufferPosition(@bufferPositionForScreenPosition(screenPosition), options)
|
|
|
|
# Public: Constructs a new marker at the given buffer position.
|
|
#
|
|
# range - The marker {Range} (representing the distance between the head and tail)
|
|
# options - Options to pass to the {BufferMarker} constructor
|
|
#
|
|
# Returns a {Number} representing the new marker's ID.
|
|
markBufferPosition: (bufferPosition, options) ->
|
|
@getMarker(@buffer.markPosition(bufferPosition, options).id)
|
|
|
|
# Public: Removes the marker with the given id.
|
|
#
|
|
# id - The {Number} of the ID to remove
|
|
destroyMarker: (id) ->
|
|
@buffer.destroyMarker(id)
|
|
delete @markers[id]
|
|
|
|
# Finds the first marker satisfying the given attributes
|
|
#
|
|
# Refer to {DisplayBuffer.findMarkers} for details.
|
|
#
|
|
# Returns a {DisplayBufferMarker} or null
|
|
findMarker: (attributes) ->
|
|
@findMarkers(attributes)[0]
|
|
|
|
# Finds all valid markers satisfying the given attributes
|
|
#
|
|
# attributes - The attributes against which to compare the markers' attributes
|
|
# There are some reserved keys that match against derived marker properties:
|
|
# startBufferRow - The buffer row at which the marker starts
|
|
# endBufferRow - The buffer row at which the marker ends
|
|
#
|
|
# Returns an {Array} of {DisplayBufferMarker}s
|
|
findMarkers: (attributes) ->
|
|
{ startBufferRow, endBufferRow, containsBufferRange, containsBufferRow } = attributes
|
|
attributes.startRow = startBufferRow if startBufferRow?
|
|
attributes.endRow = endBufferRow if endBufferRow?
|
|
attributes.containsRange = containsBufferRange if containsBufferRange?
|
|
attributes.containsRow = containsBufferRow if containsBufferRow?
|
|
attributes = _.omit(attributes, ['startBufferRow', 'endBufferRow', 'containsBufferRange', 'containsBufferRow'])
|
|
@buffer.findMarkers(attributes).map ({id}) => @getMarker(id)
|
|
|
|
###
|
|
# Internal #
|
|
###
|
|
|
|
pauseMarkerObservers: ->
|
|
marker.pauseEvents() for marker in @getMarkers()
|
|
|
|
resumeMarkerObservers: ->
|
|
marker.resumeEvents() for marker in @getMarkers()
|
|
|
|
refreshMarkerScreenPositions: ->
|
|
for marker in @getMarkers()
|
|
marker.notifyObservers(bufferChanged: false)
|
|
|
|
destroy: ->
|
|
@tokenizedBuffer.destroy()
|
|
@buffer.off 'markers-updated', @handleMarkersUpdated
|
|
|
|
logLines: (start, end) ->
|
|
@lineMap.logLines(start, end)
|
|
|
|
getDebugSnapshot: ->
|
|
lines = ["Display Buffer:"]
|
|
for screenLine, row in @lineMap.linesForScreenRows(0, @getLastRow())
|
|
lines.push "#{row}: #{screenLine.text}"
|
|
lines.join('\n')
|
|
|
|
_.extend DisplayBuffer.prototype, EventEmitter
|
|
_.extend DisplayBuffer.prototype, Subscriber
|