Files
atom/src/text-editor-presenter.coffee
2015-11-19 16:38:20 -08:00

1553 lines
52 KiB
CoffeeScript

{CompositeDisposable, Disposable, Emitter} = require 'event-kit'
{Point, Range} = require 'text-buffer'
_ = require 'underscore-plus'
Decoration = require './decoration'
module.exports =
class TextEditorPresenter
toggleCursorBlinkHandle: null
startBlinkingCursorsAfterDelay: null
stoppedScrollingTimeoutId: null
mouseWheelScreenRow: null
overlayDimensions: {}
minimumReflowInterval: 200
constructor: (params) ->
{@model, @config} = params
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params
{@contentFrameWidth} = params
@gutterWidth = 0
@tileSize ?= 6
@realScrollTop = @scrollTop
@realScrollLeft = @scrollLeft
@disposables = new CompositeDisposable
@emitter = new Emitter
@visibleHighlights = {}
@characterWidthsByScope = {}
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
@customGutterDecorationsByGutterName = {}
@screenRowsToMeasure = []
@transferMeasurementsToModel()
@transferMeasurementsFromModel()
@observeModel()
@observeConfig()
@buildState()
@invalidateState()
@startBlinkingCursors() if @focused
@startReflowing() if @continuousReflow
@updating = false
setLinesYardstick: (@linesYardstick) ->
getLinesYardstick: -> @linesYardstick
destroy: ->
@disposables.dispose()
clearTimeout(@stoppedScrollingTimeoutId) if @stoppedScrollingTimeoutId?
clearInterval(@reflowingInterval) if @reflowingInterval?
@stopBlinkingCursors()
# Calls your `callback` when some changes in the model occurred and the current state has been updated.
onDidUpdateState: (callback) ->
@emitter.on 'did-update-state', callback
emitDidUpdateState: ->
@emitter.emit "did-update-state" if @isBatching()
transferMeasurementsToModel: ->
@model.setLineHeightInPixels(@lineHeight) if @lineHeight?
@model.setDefaultCharWidth(@baseCharacterWidth) if @baseCharacterWidth?
transferMeasurementsFromModel: ->
@editorWidthInChars = @model.getEditorWidthInChars()
# Private: Determines whether {TextEditorPresenter} is currently batching changes.
# Returns a {Boolean}, `true` if is collecting changes, `false` if is applying them.
isBatching: ->
@updating is false
getPreMeasurementState: ->
@updating = true
@updateVerticalDimensions()
@updateScrollbarDimensions()
@commitPendingLogicalScrollTopPosition()
@commitPendingScrollTopPosition()
@updateStartRow()
@updateEndRow()
@updateCommonGutterState()
@updateReflowState()
if @shouldUpdateDecorations
@fetchDecorations()
@updateLineDecorations()
if @shouldUpdateLinesState or @shouldUpdateLineNumbersState
@updateTilesState()
@shouldUpdateLinesState = false
@shouldUpdateLineNumbersState = false
@shouldUpdateTilesState = true
@updating = false
@state
getPostMeasurementState: ->
@updating = true
@updateHorizontalDimensions()
@commitPendingLogicalScrollLeftPosition()
@commitPendingScrollLeftPosition()
@clearPendingScrollPosition()
@updateRowsPerPage()
@updateFocusedState() if @shouldUpdateFocusedState
@updateHeightState() if @shouldUpdateHeightState
@updateVerticalScrollState() if @shouldUpdateVerticalScrollState
@updateHorizontalScrollState() if @shouldUpdateHorizontalScrollState
@updateScrollbarsState() if @shouldUpdateScrollbarsState
@updateHiddenInputState() if @shouldUpdateHiddenInputState
@updateContentState() if @shouldUpdateContentState
@updateHighlightDecorations() if @shouldUpdateDecorations
@updateTilesState() if @shouldUpdateTilesState
@updateCursorsState() if @shouldUpdateCursorsState
@updateOverlaysState() if @shouldUpdateOverlaysState
@updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState
@updateGutterOrderState() if @shouldUpdateGutterOrderState
@updateCustomGutterDecorationState() if @shouldUpdateCustomGutterDecorationState
@updating = false
@resetTrackedUpdates()
# Public: Gets this presenter's state, updating it just in time before returning from this function.
# Returns a state {Object}, useful for rendering to screen.
getState: ->
@linesYardstick.prepareScreenRowsForMeasurement()
@getPostMeasurementState()
@state
resetTrackedUpdates: ->
@shouldUpdateFocusedState = false
@shouldUpdateHeightState = false
@shouldUpdateVerticalScrollState = false
@shouldUpdateHorizontalScrollState = false
@shouldUpdateScrollbarsState = false
@shouldUpdateHiddenInputState = false
@shouldUpdateContentState = false
@shouldUpdateDecorations = false
@shouldUpdateLinesState = false
@shouldUpdateTilesState = false
@shouldUpdateCursorsState = false
@shouldUpdateOverlaysState = false
@shouldUpdateLineNumberGutterState = false
@shouldUpdateLineNumbersState = false
@shouldUpdateGutterOrderState = false
@shouldUpdateCustomGutterDecorationState = false
invalidateState: ->
@shouldUpdateFocusedState = true
@shouldUpdateHeightState = true
@shouldUpdateVerticalScrollState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateTilesState = true
@shouldUpdateCursorsState = true
@shouldUpdateOverlaysState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateGutterOrderState = true
@shouldUpdateCustomGutterDecorationState = true
observeModel: ->
@disposables.add @model.onDidChange =>
@shouldUpdateHeightState = true
@shouldUpdateVerticalScrollState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateCursorsState = true
@shouldUpdateLinesState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateGutterOrderState = true
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
@disposables.add @model.onDidUpdateDecorations =>
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateDecorations = true
@shouldUpdateOverlaysState = true
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
@disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this))
@disposables.add @model.onDidChangePlaceholderText =>
@shouldUpdateContentState = true
@emitDidUpdateState()
@disposables.add @model.onDidChangeMini =>
@shouldUpdateScrollbarsState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateGutterOrderState = true
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
@disposables.add @model.onDidChangeLineNumberGutterVisible =>
@shouldUpdateLineNumberGutterState = true
@shouldUpdateGutterOrderState = true
@emitDidUpdateState()
@disposables.add @model.onDidAddCursor(@didAddCursor.bind(this))
@disposables.add @model.onDidRequestAutoscroll(@requestAutoscroll.bind(this))
@disposables.add @model.onDidChangeFirstVisibleScreenRow(@didChangeFirstVisibleScreenRow.bind(this))
@observeCursor(cursor) for cursor in @model.getCursors()
@disposables.add @model.onDidAddGutter(@didAddGutter.bind(this))
return
observeConfig: ->
configParams = {scope: @model.getRootScopeDescriptor()}
@scrollPastEnd = @config.get('editor.scrollPastEnd', configParams)
@showLineNumbers = @config.get('editor.showLineNumbers', configParams)
@showIndentGuide = @config.get('editor.showIndentGuide', configParams)
if @configDisposables?
@configDisposables?.dispose()
@disposables.remove(@configDisposables)
@configDisposables = new CompositeDisposable
@disposables.add(@configDisposables)
@configDisposables.add @config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) =>
@showIndentGuide = newValue
@shouldUpdateContentState = true
@emitDidUpdateState()
@configDisposables.add @config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) =>
@scrollPastEnd = newValue
@shouldUpdateVerticalScrollState = true
@shouldUpdateScrollbarsState = true
@updateScrollHeight()
@emitDidUpdateState()
@configDisposables.add @config.onDidChange 'editor.showLineNumbers', configParams, ({newValue}) =>
@showLineNumbers = newValue
@shouldUpdateLineNumberGutterState = true
@shouldUpdateGutterOrderState = true
@emitDidUpdateState()
didChangeGrammar: ->
@observeConfig()
@shouldUpdateContentState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateGutterOrderState = true
@emitDidUpdateState()
buildState: ->
@state =
horizontalScrollbar: {}
verticalScrollbar: {}
hiddenInput: {}
content:
scrollingVertically: false
cursorsVisible: false
tiles: {}
highlights: {}
overlays: {}
cursors: {}
gutters: []
# Shared state that is copied into ``@state.gutters`.
@sharedGutterStyles = {}
@customGutterDecorations = {}
@lineNumberGutter =
tiles: {}
setContinuousReflow: (@continuousReflow) ->
if @continuousReflow
@startReflowing()
else
@stopReflowing()
updateReflowState: ->
@state.content.continuousReflow = @continuousReflow
@lineNumberGutter.continuousReflow = @continuousReflow
startReflowing: ->
@reflowingInterval = setInterval(@emitDidUpdateState.bind(this), @minimumReflowInterval)
stopReflowing: ->
clearInterval(@reflowingInterval)
@reflowingInterval = null
updateFocusedState: ->
@state.focused = @focused
updateHeightState: ->
if @autoHeight
@state.height = @contentHeight
else
@state.height = null
updateVerticalScrollState: ->
@state.content.scrollHeight = @scrollHeight
@sharedGutterStyles.scrollHeight = @scrollHeight
@state.verticalScrollbar.scrollHeight = @scrollHeight
@state.content.scrollTop = @scrollTop
@sharedGutterStyles.scrollTop = @scrollTop
@state.verticalScrollbar.scrollTop = @scrollTop
updateHorizontalScrollState: ->
@state.content.scrollWidth = @scrollWidth
@state.horizontalScrollbar.scrollWidth = @scrollWidth
@state.content.scrollLeft = @scrollLeft
@state.horizontalScrollbar.scrollLeft = @scrollLeft
updateScrollbarsState: ->
@state.horizontalScrollbar.visible = @horizontalScrollbarHeight > 0
@state.horizontalScrollbar.height = @measuredHorizontalScrollbarHeight
@state.horizontalScrollbar.right = @verticalScrollbarWidth
@state.verticalScrollbar.visible = @verticalScrollbarWidth > 0
@state.verticalScrollbar.width = @measuredVerticalScrollbarWidth
@state.verticalScrollbar.bottom = @horizontalScrollbarHeight
updateHiddenInputState: ->
return unless lastCursor = @model.getLastCursor()
{top, left, height, width} = @pixelRectForScreenRange(lastCursor.getScreenRange())
if @focused
@state.hiddenInput.top = Math.max(Math.min(top, @clientHeight - height), 0)
@state.hiddenInput.left = Math.max(Math.min(left, @clientWidth - width), 0)
else
@state.hiddenInput.top = 0
@state.hiddenInput.left = 0
@state.hiddenInput.height = height
@state.hiddenInput.width = Math.max(width, 2)
updateContentState: ->
if @boundingClientRect?
@sharedGutterStyles.maxHeight = @boundingClientRect.height
@state.content.maxHeight = @boundingClientRect.height
@state.content.width = Math.max(@contentWidth + @verticalScrollbarWidth, @contentFrameWidth)
@state.content.scrollWidth = @scrollWidth
@state.content.scrollLeft = @scrollLeft
@state.content.indentGuidesVisible = not @model.isMini() and @showIndentGuide
@state.content.backgroundColor = if @model.isMini() then null else @backgroundColor
@state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null
tileForRow: (row) ->
row - (row % @tileSize)
constrainRow: (row) ->
Math.max(0, Math.min(row, @model.getScreenLineCount()))
getStartTileRow: ->
@constrainRow(@tileForRow(@startRow))
getEndTileRow: ->
@constrainRow(@tileForRow(@endRow))
isValidScreenRow: (screenRow) ->
screenRow >= 0 and screenRow < @model.getScreenLineCount()
getScreenRows: ->
startRow = @getStartTileRow()
endRow = @constrainRow(@getEndTileRow() + @tileSize)
screenRows = [startRow...endRow]
longestScreenRow = @model.getLongestScreenRow()
if longestScreenRow?
screenRows.push(longestScreenRow)
if @screenRowsToMeasure?
screenRows.push(@screenRowsToMeasure...)
screenRows = screenRows.filter @isValidScreenRow.bind(this)
screenRows.sort (a, b) -> a - b
_.uniq(screenRows, true)
setScreenRowsToMeasure: (screenRows) ->
return if not screenRows? or screenRows.length is 0
@screenRowsToMeasure = screenRows
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateDecorations = true
clearScreenRowsToMeasure: ->
@screenRowsToMeasure = []
updateTilesState: ->
return unless @startRow? and @endRow? and @lineHeight?
screenRows = @getScreenRows()
visibleTiles = {}
startRow = screenRows[0]
endRow = screenRows[screenRows.length - 1]
screenRowIndex = screenRows.length - 1
zIndex = 0
for tileStartRow in [@tileForRow(endRow)..@tileForRow(startRow)] by -@tileSize
rowsWithinTile = []
while screenRowIndex >= 0
currentScreenRow = screenRows[screenRowIndex]
break if currentScreenRow < tileStartRow
rowsWithinTile.push(currentScreenRow)
screenRowIndex--
continue if rowsWithinTile.length is 0
tile = @state.content.tiles[tileStartRow] ?= {}
tile.top = tileStartRow * @lineHeight - @scrollTop
tile.left = -@scrollLeft
tile.height = @tileSize * @lineHeight
tile.display = "block"
tile.zIndex = zIndex
tile.highlights ?= {}
gutterTile = @lineNumberGutter.tiles[tileStartRow] ?= {}
gutterTile.top = tileStartRow * @lineHeight - @scrollTop
gutterTile.height = @tileSize * @lineHeight
gutterTile.display = "block"
gutterTile.zIndex = zIndex
@updateLinesState(tile, rowsWithinTile) if @shouldUpdateLinesState
@updateLineNumbersState(gutterTile, rowsWithinTile) if @shouldUpdateLineNumbersState
visibleTiles[tileStartRow] = true
zIndex++
if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)?
mouseWheelTile = @tileForRow(@mouseWheelScreenRow)
unless visibleTiles[mouseWheelTile]?
@lineNumberGutter.tiles[mouseWheelTile].display = "none"
@state.content.tiles[mouseWheelTile].display = "none"
visibleTiles[mouseWheelTile] = true
for id, tile of @state.content.tiles
continue if visibleTiles.hasOwnProperty(id)
delete @state.content.tiles[id]
delete @lineNumberGutter.tiles[id]
updateLinesState: (tileState, screenRows) ->
tileState.lines ?= {}
visibleLineIds = {}
for screenRow in screenRows
line = @model.tokenizedLineForScreenRow(screenRow)
unless line?
throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}")
visibleLineIds[line.id] = true
if tileState.lines.hasOwnProperty(line.id)
lineState = tileState.lines[line.id]
lineState.screenRow = screenRow
lineState.decorationClasses = @lineDecorationClassesForRow(screenRow)
else
tileState.lines[line.id] =
screenRow: screenRow
text: line.text
openScopes: line.openScopes
tags: line.tags
specialTokens: line.specialTokens
firstNonWhitespaceIndex: line.firstNonWhitespaceIndex
firstTrailingWhitespaceIndex: line.firstTrailingWhitespaceIndex
invisibles: line.invisibles
endOfLineInvisibles: line.endOfLineInvisibles
isOnlyWhitespace: line.isOnlyWhitespace()
indentLevel: line.indentLevel
tabLength: line.tabLength
fold: line.fold
decorationClasses: @lineDecorationClassesForRow(screenRow)
for id, line of tileState.lines
delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id)
return
updateCursorsState: ->
@state.content.cursors = {}
@updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation
return
updateCursorState: (cursor) ->
return unless @startRow? and @endRow? and @hasPixelRectRequirements() and @baseCharacterWidth?
screenRange = cursor.getScreenRange()
return unless cursor.isVisible() and @startRow <= screenRange.start.row < @endRow
pixelRect = @pixelRectForScreenRange(screenRange)
pixelRect.width = Math.round(@baseCharacterWidth) if pixelRect.width is 0
@state.content.cursors[cursor.id] = pixelRect
updateOverlaysState: ->
return unless @hasOverlayPositionRequirements()
visibleDecorationIds = {}
for decoration in @model.getOverlayDecorations()
continue unless decoration.getMarker().isValid()
{item, position, class: klass} = decoration.getProperties()
if position is 'tail'
screenPosition = decoration.getMarker().getTailScreenPosition()
else
screenPosition = decoration.getMarker().getHeadScreenPosition()
pixelPosition = @pixelPositionForScreenPosition(screenPosition, true)
top = pixelPosition.top + @lineHeight
left = pixelPosition.left + @gutterWidth
if overlayDimensions = @overlayDimensions[decoration.id]
{itemWidth, itemHeight, contentMargin} = overlayDimensions
rightDiff = left + @boundingClientRect.left + itemWidth + contentMargin - @windowWidth
left -= rightDiff if rightDiff > 0
leftDiff = left + @boundingClientRect.left + contentMargin
left -= leftDiff if leftDiff < 0
if top + @boundingClientRect.top + itemHeight > @windowHeight and top - (itemHeight + @lineHeight) >= 0
top -= itemHeight + @lineHeight
pixelPosition.top = top
pixelPosition.left = left
overlayState = @state.content.overlays[decoration.id] ?= {item}
overlayState.pixelPosition = pixelPosition
overlayState.class = klass if klass?
visibleDecorationIds[decoration.id] = true
for id of @state.content.overlays
delete @state.content.overlays[id] unless visibleDecorationIds[id]
for id of @overlayDimensions
delete @overlayDimensions[id] unless visibleDecorationIds[id]
return
updateLineNumberGutterState: ->
@lineNumberGutter.maxLineNumberDigits = @model.getLineCount().toString().length
updateCommonGutterState: ->
@sharedGutterStyles.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)"
@gutterBackgroundColor
else
@backgroundColor
didAddGutter: (gutter) ->
gutterDisposables = new CompositeDisposable
gutterDisposables.add gutter.onDidChangeVisible =>
@shouldUpdateGutterOrderState = true
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
gutterDisposables.add gutter.onDidDestroy =>
@disposables.remove(gutterDisposables)
gutterDisposables.dispose()
@shouldUpdateGutterOrderState = true
@emitDidUpdateState()
# It is not necessary to @updateCustomGutterDecorationState here.
# The destroyed gutter will be removed from the list of gutters in @state,
# and thus will be removed from the DOM.
@disposables.add(gutterDisposables)
@shouldUpdateGutterOrderState = true
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
updateGutterOrderState: ->
@state.gutters = []
if @model.isMini()
return
for gutter in @model.getGutters()
isVisible = @gutterIsVisible(gutter)
if gutter.name is 'line-number'
content = @lineNumberGutter
else
@customGutterDecorations[gutter.name] ?= {}
content = @customGutterDecorations[gutter.name]
@state.gutters.push({
gutter,
visible: isVisible,
styles: @sharedGutterStyles,
content,
})
# Updates the decoration state for the gutter with the given gutterName.
# @customGutterDecorations is an {Object}, with the form:
# * gutterName : {
# decoration.id : {
# top: # of pixels from top
# height: # of pixels height of this decoration
# item (optional): HTMLElement
# class (optional): {String} class
# }
# }
updateCustomGutterDecorationState: ->
return unless @startRow? and @endRow? and @lineHeight?
if @model.isMini()
# Mini editors have no gutter decorations.
# We clear instead of reassigning to preserve the reference.
@clearAllCustomGutterDecorations()
for gutter in @model.getGutters()
gutterName = gutter.name
gutterDecorations = @customGutterDecorations[gutterName]
if gutterDecorations
# Clear the gutter decorations; they are rebuilt.
# We clear instead of reassigning to preserve the reference.
@clearDecorationsForCustomGutterName(gutterName)
else
@customGutterDecorations[gutterName] = {}
continue unless @gutterIsVisible(gutter)
for decorationId, {properties, screenRange} of @customGutterDecorationsByGutterName[gutterName]
@customGutterDecorations[gutterName][decorationId] =
top: @lineHeight * screenRange.start.row
height: @lineHeight * screenRange.getRowCount()
item: properties.item
class: properties.class
clearAllCustomGutterDecorations: ->
allGutterNames = Object.keys(@customGutterDecorations)
for gutterName in allGutterNames
@clearDecorationsForCustomGutterName(gutterName)
clearDecorationsForCustomGutterName: (gutterName) ->
gutterDecorations = @customGutterDecorations[gutterName]
if gutterDecorations
allDecorationIds = Object.keys(gutterDecorations)
for decorationId in allDecorationIds
delete gutterDecorations[decorationId]
gutterIsVisible: (gutterModel) ->
isVisible = gutterModel.isVisible()
if gutterModel.name is 'line-number'
isVisible = isVisible and @showLineNumbers
isVisible
updateLineNumbersState: (tileState, screenRows) ->
tileState.lineNumbers ?= {}
visibleLineNumberIds = {}
startRow = screenRows[screenRows.length - 1]
endRow = Math.min(screenRows[0] + 1, @model.getScreenLineCount())
if startRow > 0
rowBeforeStartRow = startRow - 1
lastBufferRow = @model.bufferRowForScreenRow(rowBeforeStartRow)
else
lastBufferRow = null
if endRow > startRow
bufferRows = @model.bufferRowsForScreenRows(startRow, endRow - 1)
for bufferRow, i in bufferRows
if bufferRow is lastBufferRow
softWrapped = true
else
lastBufferRow = bufferRow
softWrapped = false
screenRow = startRow + i
line = @model.tokenizedLineForScreenRow(screenRow)
decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
foldable = @model.isFoldableAtScreenRow(screenRow)
tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable}
visibleLineNumberIds[line.id] = true
for id of tileState.lineNumbers
delete tileState.lineNumbers[id] unless visibleLineNumberIds[id]
return
updateStartRow: ->
return unless @scrollTop? and @lineHeight?
startRow = Math.floor(@scrollTop / @lineHeight)
@startRow = Math.max(0, startRow)
updateEndRow: ->
return unless @scrollTop? and @lineHeight? and @height?
startRow = Math.max(0, Math.floor(@scrollTop / @lineHeight))
visibleLinesCount = Math.ceil(@height / @lineHeight) + 1
endRow = startRow + visibleLinesCount
@endRow = Math.min(@model.getScreenLineCount(), endRow)
updateRowsPerPage: ->
rowsPerPage = Math.floor(@getClientHeight() / @lineHeight)
if rowsPerPage isnt @rowsPerPage
@rowsPerPage = rowsPerPage
@model.setRowsPerPage(@rowsPerPage)
updateScrollWidth: ->
return unless @contentWidth? and @clientWidth?
scrollWidth = Math.max(@contentWidth, @clientWidth)
unless @scrollWidth is scrollWidth
@scrollWidth = scrollWidth
@updateScrollLeft(@scrollLeft)
updateScrollHeight: ->
return unless @contentHeight? and @clientHeight?
contentHeight = @contentHeight
if @scrollPastEnd
extraScrollHeight = @clientHeight - (@lineHeight * 3)
contentHeight += extraScrollHeight if extraScrollHeight > 0
scrollHeight = Math.max(contentHeight, @height)
unless @scrollHeight is scrollHeight
@scrollHeight = scrollHeight
@updateScrollTop(@scrollTop)
updateVerticalDimensions: ->
if @lineHeight?
oldContentHeight = @contentHeight
@contentHeight = @lineHeight * @model.getScreenLineCount()
if @contentHeight isnt oldContentHeight
@updateHeight()
@updateScrollbarDimensions()
@updateScrollHeight()
updateHorizontalDimensions: ->
if @baseCharacterWidth?
oldContentWidth = @contentWidth
clip = @model.tokenizedLineForScreenRow(@model.getLongestScreenRow())?.isSoftWrapped()
@contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), @model.getMaxScreenLineLength()], clip).left
@contentWidth += @scrollLeft
@contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width
if @contentWidth isnt oldContentWidth
@updateScrollbarDimensions()
@updateScrollWidth()
updateClientHeight: ->
return unless @height? and @horizontalScrollbarHeight?
clientHeight = @height - @horizontalScrollbarHeight
@model.setHeight(clientHeight, true)
unless @clientHeight is clientHeight
@clientHeight = clientHeight
@updateScrollHeight()
@updateScrollTop(@scrollTop)
updateClientWidth: ->
return unless @contentFrameWidth? and @verticalScrollbarWidth?
clientWidth = @contentFrameWidth - @verticalScrollbarWidth
@model.setWidth(clientWidth, true) unless @editorWidthInChars
unless @clientWidth is clientWidth
@clientWidth = clientWidth
@updateScrollWidth()
@updateScrollLeft(@scrollLeft)
updateScrollTop: (scrollTop) ->
scrollTop = @constrainScrollTop(scrollTop)
if scrollTop isnt @realScrollTop and not Number.isNaN(scrollTop)
@realScrollTop = scrollTop
@scrollTop = Math.round(scrollTop)
@model.setFirstVisibleScreenRow(Math.round(@scrollTop / @lineHeight), true)
@updateStartRow()
@updateEndRow()
@didStartScrolling()
@emitter.emit 'did-change-scroll-top', @scrollTop
constrainScrollTop: (scrollTop) ->
return scrollTop unless scrollTop? and @scrollHeight? and @clientHeight?
Math.max(0, Math.min(scrollTop, @scrollHeight - @clientHeight))
updateScrollLeft: (scrollLeft) ->
scrollLeft = @constrainScrollLeft(scrollLeft)
if scrollLeft isnt @realScrollLeft and not Number.isNaN(scrollLeft)
@realScrollLeft = scrollLeft
@scrollLeft = Math.round(scrollLeft)
@model.setFirstVisibleScreenColumn(Math.round(@scrollLeft / @baseCharacterWidth))
@emitter.emit 'did-change-scroll-left', @scrollLeft
constrainScrollLeft: (scrollLeft) ->
return scrollLeft unless scrollLeft? and @scrollWidth? and @clientWidth?
Math.max(0, Math.min(scrollLeft, @scrollWidth - @clientWidth))
updateScrollbarDimensions: ->
return unless @contentFrameWidth? and @height?
return unless @measuredVerticalScrollbarWidth? and @measuredHorizontalScrollbarHeight?
return unless @contentWidth? and @contentHeight?
clientWidthWithoutVerticalScrollbar = @contentFrameWidth
clientWidthWithVerticalScrollbar = clientWidthWithoutVerticalScrollbar - @measuredVerticalScrollbarWidth
clientHeightWithoutHorizontalScrollbar = @height
clientHeightWithHorizontalScrollbar = clientHeightWithoutHorizontalScrollbar - @measuredHorizontalScrollbarHeight
horizontalScrollbarVisible =
not @model.isMini() and
(@contentWidth > clientWidthWithoutVerticalScrollbar or
@contentWidth > clientWidthWithVerticalScrollbar and @contentHeight > clientHeightWithoutHorizontalScrollbar)
verticalScrollbarVisible =
not @model.isMini() and
(@contentHeight > clientHeightWithoutHorizontalScrollbar or
@contentHeight > clientHeightWithHorizontalScrollbar and @contentWidth > clientWidthWithoutVerticalScrollbar)
horizontalScrollbarHeight =
if horizontalScrollbarVisible
@measuredHorizontalScrollbarHeight
else
0
verticalScrollbarWidth =
if verticalScrollbarVisible
@measuredVerticalScrollbarWidth
else
0
unless @horizontalScrollbarHeight is horizontalScrollbarHeight
@horizontalScrollbarHeight = horizontalScrollbarHeight
@updateClientHeight()
unless @verticalScrollbarWidth is verticalScrollbarWidth
@verticalScrollbarWidth = verticalScrollbarWidth
@updateClientWidth()
lineDecorationClassesForRow: (row) ->
return null if @model.isMini()
decorationClasses = null
for id, properties of @lineDecorationsByScreenRow[row]
decorationClasses ?= []
decorationClasses.push(properties.class)
decorationClasses
lineNumberDecorationClassesForRow: (row) ->
return null if @model.isMini()
decorationClasses = null
for id, properties of @lineNumberDecorationsByScreenRow[row]
decorationClasses ?= []
decorationClasses.push(properties.class)
decorationClasses
getCursorBlinkPeriod: -> @cursorBlinkPeriod
getCursorBlinkResumeDelay: -> @cursorBlinkResumeDelay
setFocused: (focused) ->
unless @focused is focused
@focused = focused
if @focused
@startBlinkingCursors()
else
@stopBlinkingCursors(false)
@shouldUpdateFocusedState = true
@shouldUpdateHiddenInputState = true
@emitDidUpdateState()
setScrollTop: (scrollTop, overrideScroll=true) ->
return unless scrollTop?
@pendingScrollLogicalPosition = null if overrideScroll
@pendingScrollTop = scrollTop
@shouldUpdateVerticalScrollState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
getScrollTop: ->
@scrollTop
getRealScrollTop: ->
@realScrollTop ? @scrollTop
didStartScrolling: ->
if @stoppedScrollingTimeoutId?
clearTimeout(@stoppedScrollingTimeoutId)
@stoppedScrollingTimeoutId = null
@stoppedScrollingTimeoutId = setTimeout(@didStopScrolling.bind(this), @stoppedScrollingDelay)
@state.content.scrollingVertically = true
@emitDidUpdateState()
didStopScrolling: ->
@state.content.scrollingVertically = false
if @mouseWheelScreenRow?
@mouseWheelScreenRow = null
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
setScrollLeft: (scrollLeft, overrideScroll=true) ->
return unless scrollLeft?
@pendingScrollLogicalPosition = null if overrideScroll
@pendingScrollLeft = scrollLeft
@shouldUpdateHorizontalScrollState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateCursorsState = true
@shouldUpdateOverlaysState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@emitDidUpdateState()
getScrollLeft: ->
@scrollLeft
getRealScrollLeft: ->
@realScrollLeft ? @scrollLeft
getClientHeight: ->
if @clientHeight
@clientHeight
else
@explicitHeight - @horizontalScrollbarHeight
getClientWidth: ->
if @clientWidth
@clientWidth
else
@contentFrameWidth - @verticalScrollbarWidth
getScrollBottom: -> @getScrollTop() + @getClientHeight()
setScrollBottom: (scrollBottom, overrideScroll) ->
@setScrollTop(scrollBottom - @getClientHeight(), overrideScroll)
@getScrollBottom()
getScrollRight: -> @getScrollLeft() + @getClientWidth()
setScrollRight: (scrollRight, overrideScroll) ->
@setScrollLeft(scrollRight - @getClientWidth(), overrideScroll)
@getScrollRight()
getScrollHeight: ->
@scrollHeight
getScrollWidth: ->
@scrollWidth
getMaxScrollTop: ->
scrollHeight = @getScrollHeight()
clientHeight = @getClientHeight()
return 0 unless scrollHeight? and clientHeight?
scrollHeight - clientHeight
setHorizontalScrollbarHeight: (horizontalScrollbarHeight) ->
unless @measuredHorizontalScrollbarHeight is horizontalScrollbarHeight
oldHorizontalScrollbarHeight = @measuredHorizontalScrollbarHeight
@measuredHorizontalScrollbarHeight = horizontalScrollbarHeight
@shouldUpdateScrollbarsState = true
@shouldUpdateVerticalScrollState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateCursorsState = true unless oldHorizontalScrollbarHeight?
@emitDidUpdateState()
setVerticalScrollbarWidth: (verticalScrollbarWidth) ->
unless @measuredVerticalScrollbarWidth is verticalScrollbarWidth
oldVerticalScrollbarWidth = @measuredVerticalScrollbarWidth
@measuredVerticalScrollbarWidth = verticalScrollbarWidth
@shouldUpdateScrollbarsState = true
@shouldUpdateVerticalScrollState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateCursorsState = true unless oldVerticalScrollbarWidth?
@emitDidUpdateState()
setAutoHeight: (autoHeight) ->
unless @autoHeight is autoHeight
@autoHeight = autoHeight
@shouldUpdateHeightState = true
@emitDidUpdateState()
setExplicitHeight: (explicitHeight) ->
unless @explicitHeight is explicitHeight
@explicitHeight = explicitHeight
@updateHeight()
@shouldUpdateVerticalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@emitDidUpdateState()
updateHeight: ->
height = @explicitHeight ? @contentHeight
unless @height is height
@height = height
@updateScrollbarDimensions()
@updateClientHeight()
@updateScrollHeight()
@updateEndRow()
setContentFrameWidth: (contentFrameWidth) ->
if @contentFrameWidth isnt contentFrameWidth or @editorWidthInChars?
oldContentFrameWidth = @contentFrameWidth
@contentFrameWidth = contentFrameWidth
@editorWidthInChars = null
@updateScrollbarDimensions()
@updateClientWidth()
@shouldUpdateVerticalScrollState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true unless oldContentFrameWidth?
@emitDidUpdateState()
setBoundingClientRect: (boundingClientRect) ->
unless @clientRectsEqual(@boundingClientRect, boundingClientRect)
@boundingClientRect = boundingClientRect
@shouldUpdateOverlaysState = true
@shouldUpdateContentState = true
@emitDidUpdateState()
clientRectsEqual: (clientRectA, clientRectB) ->
clientRectA? and clientRectB? and
clientRectA.top is clientRectB.top and
clientRectA.left is clientRectB.left and
clientRectA.width is clientRectB.width and
clientRectA.height is clientRectB.height
setWindowSize: (width, height) ->
if @windowWidth isnt width or @windowHeight isnt height
@windowWidth = width
@windowHeight = height
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
setBackgroundColor: (backgroundColor) ->
unless @backgroundColor is backgroundColor
@backgroundColor = backgroundColor
@shouldUpdateContentState = true
@shouldUpdateLineNumberGutterState = true
@shouldUpdateGutterOrderState = true
@emitDidUpdateState()
setGutterBackgroundColor: (gutterBackgroundColor) ->
unless @gutterBackgroundColor is gutterBackgroundColor
@gutterBackgroundColor = gutterBackgroundColor
@shouldUpdateLineNumberGutterState = true
@shouldUpdateGutterOrderState = true
@emitDidUpdateState()
setGutterWidth: (gutterWidth) ->
if @gutterWidth isnt gutterWidth
@gutterWidth = gutterWidth
@updateOverlaysState()
getGutterWidth: ->
@gutterWidth
setLineHeight: (lineHeight) ->
unless @lineHeight is lineHeight
@lineHeight = lineHeight
@restoreScrollTopIfNeeded()
@model.setLineHeightInPixels(lineHeight)
@shouldUpdateHeightState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateVerticalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateCustomGutterDecorationState = true
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
setMouseWheelScreenRow: (screenRow) ->
if @mouseWheelScreenRow isnt screenRow
@mouseWheelScreenRow = screenRow
@didStartScrolling()
setBaseCharacterWidth: (baseCharacterWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) ->
unless @baseCharacterWidth is baseCharacterWidth and @doubleWidthCharWidth is doubleWidthCharWidth and @halfWidthCharWidth is halfWidthCharWidth and koreanCharWidth is @koreanCharWidth
@baseCharacterWidth = baseCharacterWidth
@doubleWidthCharWidth = doubleWidthCharWidth
@halfWidthCharWidth = halfWidthCharWidth
@koreanCharWidth = koreanCharWidth
@model.setDefaultCharWidth(baseCharacterWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
@restoreScrollLeftIfNeeded()
@characterWidthsChanged()
characterWidthsChanged: ->
@shouldUpdateHorizontalScrollState = true
@shouldUpdateVerticalScrollState = true
@shouldUpdateScrollbarsState = true
@shouldUpdateHiddenInputState = true
@shouldUpdateContentState = true
@shouldUpdateDecorations = true
@shouldUpdateLinesState = true
@shouldUpdateCursorsState = true
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
hasPixelPositionRequirements: ->
@lineHeight? and @baseCharacterWidth?
pixelPositionForScreenPosition: (screenPosition, clip=true) ->
position =
@linesYardstick.pixelPositionForScreenPosition(screenPosition, clip, true)
position.top -= @getScrollTop()
position.left -= @getScrollLeft()
position.top = Math.round(position.top)
position.left = Math.round(position.left)
position
hasPixelRectRequirements: ->
@hasPixelPositionRequirements() and @scrollWidth?
hasOverlayPositionRequirements: ->
@hasPixelRectRequirements() and @boundingClientRect? and @windowWidth and @windowHeight
pixelRectForScreenRange: (screenRange) ->
rect = @linesYardstick.pixelRectForScreenRange(screenRange, true)
rect.top -= @getScrollTop()
rect.left -= @getScrollLeft()
rect.top = Math.round(rect.top)
rect.left = Math.round(rect.left)
rect.width = Math.round(rect.width)
rect.height = Math.round(rect.height)
rect
fetchDecorations: ->
return unless 0 <= @startRow <= @endRow <= Infinity
@decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1)
updateLineDecorations: ->
@lineDecorationsByScreenRow = {}
@lineNumberDecorationsByScreenRow = {}
@customGutterDecorationsByGutterName = {}
for decorationId, decorationState of @decorations
{properties, screenRange, rangeIsReversed} = decorationState
if Decoration.isType(properties, 'line') or Decoration.isType(properties, 'line-number')
@addToLineDecorationCaches(decorationId, properties, screenRange, rangeIsReversed)
else if Decoration.isType(properties, 'gutter') and properties.gutterName?
@customGutterDecorationsByGutterName[properties.gutterName] ?= {}
@customGutterDecorationsByGutterName[properties.gutterName][decorationId] = decorationState
return
updateHighlightDecorations: ->
@visibleHighlights = {}
for decorationId, {properties, screenRange} of @decorations
if Decoration.isType(properties, 'highlight')
@updateHighlightState(decorationId, properties, screenRange)
for tileId, tileState of @state.content.tiles
for id, highlight of tileState.highlights
delete tileState.highlights[id] unless @visibleHighlights[tileId]?[id]?
return
addToLineDecorationCaches: (decorationId, properties, screenRange, rangeIsReversed) ->
if screenRange.isEmpty()
return if properties.onlyNonEmpty
else
return if properties.onlyEmpty
omitLastRow = screenRange.end.column is 0
if rangeIsReversed
headPosition = screenRange.start
else
headPosition = screenRange.end
for row in [screenRange.start.row..screenRange.end.row] by 1
continue if properties.onlyHead and row isnt headPosition.row
continue if omitLastRow and row is screenRange.end.row
if Decoration.isType(properties, 'line')
@lineDecorationsByScreenRow[row] ?= {}
@lineDecorationsByScreenRow[row][decorationId] = properties
if Decoration.isType(properties, 'line-number')
@lineNumberDecorationsByScreenRow[row] ?= {}
@lineNumberDecorationsByScreenRow[row][decorationId] = properties
return
intersectRangeWithTile: (range, tileStartRow) ->
intersectingStartRow = Math.max(tileStartRow, range.start.row)
intersectingEndRow = Math.min(tileStartRow + @tileSize - 1, range.end.row)
intersectingRange = new Range(
new Point(intersectingStartRow, 0),
new Point(intersectingEndRow, Infinity)
)
if intersectingStartRow is range.start.row
intersectingRange.start.column = range.start.column
if intersectingEndRow is range.end.row
intersectingRange.end.column = range.end.column
intersectingRange
updateHighlightState: (decorationId, properties, screenRange) ->
return unless @startRow? and @endRow? and @lineHeight? and @hasPixelPositionRequirements()
@constrainRangeToVisibleRowRange(screenRange)
return if screenRange.isEmpty()
startTile = @tileForRow(screenRange.start.row)
endTile = @tileForRow(screenRange.end.row)
for tileStartRow in [startTile..endTile] by @tileSize
rangeWithinTile = @intersectRangeWithTile(screenRange, tileStartRow)
continue if rangeWithinTile.isEmpty()
tileState = @state.content.tiles[tileStartRow] ?= {highlights: {}}
highlightState = tileState.highlights[decorationId] ?= {}
highlightState.flashCount = properties.flashCount
highlightState.flashClass = properties.flashClass
highlightState.flashDuration = properties.flashDuration
highlightState.class = properties.class
highlightState.deprecatedRegionClass = properties.deprecatedRegionClass
highlightState.regions = @buildHighlightRegions(rangeWithinTile)
for region in highlightState.regions
@repositionRegionWithinTile(region, tileStartRow)
@visibleHighlights[tileStartRow] ?= {}
@visibleHighlights[tileStartRow][decorationId] = true
true
constrainRangeToVisibleRowRange: (screenRange) ->
if screenRange.start.row < @startRow
screenRange.start.row = @startRow
screenRange.start.column = 0
if screenRange.end.row < @startRow
screenRange.end.row = @startRow
screenRange.end.column = 0
if screenRange.start.row >= @endRow
screenRange.start.row = @endRow
screenRange.start.column = 0
if screenRange.end.row >= @endRow
screenRange.end.row = @endRow
screenRange.end.column = 0
repositionRegionWithinTile: (region, tileStartRow) ->
region.top += @scrollTop - tileStartRow * @lineHeight
region.left += @scrollLeft
buildHighlightRegions: (screenRange) ->
lineHeightInPixels = @lineHeight
startPixelPosition = @pixelPositionForScreenPosition(screenRange.start, false)
endPixelPosition = @pixelPositionForScreenPosition(screenRange.end, false)
spannedRows = screenRange.end.row - screenRange.start.row + 1
regions = []
if spannedRows is 1
region =
top: startPixelPosition.top
height: lineHeightInPixels
left: startPixelPosition.left
if screenRange.end.column is Infinity
region.right = 0
else
region.width = endPixelPosition.left - startPixelPosition.left
regions.push(region)
else
# First row, extending from selection start to the right side of screen
regions.push(
top: startPixelPosition.top
left: startPixelPosition.left
height: lineHeightInPixels
right: 0
)
# Middle rows, extending from left side to right side of screen
if spannedRows > 2
regions.push(
top: startPixelPosition.top + lineHeightInPixels
height: endPixelPosition.top - startPixelPosition.top - lineHeightInPixels
left: 0
right: 0
)
# Last row, extending from left side of screen to selection end
if screenRange.end.column > 0
region =
top: endPixelPosition.top
height: lineHeightInPixels
left: 0
if screenRange.end.column is Infinity
region.right = 0
else
region.width = endPixelPosition.left
regions.push(region)
regions
setOverlayDimensions: (decorationId, itemWidth, itemHeight, contentMargin) ->
@overlayDimensions[decorationId] ?= {}
overlayState = @overlayDimensions[decorationId]
dimensionsAreEqual = overlayState.itemWidth is itemWidth and
overlayState.itemHeight is itemHeight and
overlayState.contentMargin is contentMargin
unless dimensionsAreEqual
overlayState.itemWidth = itemWidth
overlayState.itemHeight = itemHeight
overlayState.contentMargin = contentMargin
@shouldUpdateOverlaysState = true
@emitDidUpdateState()
observeCursor: (cursor) ->
didChangePositionDisposable = cursor.onDidChangePosition =>
@shouldUpdateHiddenInputState = true if cursor.isLastCursor()
@shouldUpdateCursorsState = true
@pauseCursorBlinking()
@emitDidUpdateState()
didChangeVisibilityDisposable = cursor.onDidChangeVisibility =>
@shouldUpdateCursorsState = true
@emitDidUpdateState()
didDestroyDisposable = cursor.onDidDestroy =>
@disposables.remove(didChangePositionDisposable)
@disposables.remove(didChangeVisibilityDisposable)
@disposables.remove(didDestroyDisposable)
@shouldUpdateHiddenInputState = true
@shouldUpdateCursorsState = true
@emitDidUpdateState()
@disposables.add(didChangePositionDisposable)
@disposables.add(didChangeVisibilityDisposable)
@disposables.add(didDestroyDisposable)
didAddCursor: (cursor) ->
@observeCursor(cursor)
@shouldUpdateHiddenInputState = true
@shouldUpdateCursorsState = true
@pauseCursorBlinking()
@emitDidUpdateState()
startBlinkingCursors: ->
unless @isCursorBlinking()
@state.content.cursorsVisible = true
@toggleCursorBlinkHandle = setInterval(@toggleCursorBlink.bind(this), @getCursorBlinkPeriod() / 2)
isCursorBlinking: ->
@toggleCursorBlinkHandle?
stopBlinkingCursors: (visible) ->
if @isCursorBlinking()
@state.content.cursorsVisible = visible
clearInterval(@toggleCursorBlinkHandle)
@toggleCursorBlinkHandle = null
toggleCursorBlink: ->
@state.content.cursorsVisible = not @state.content.cursorsVisible
@emitDidUpdateState()
pauseCursorBlinking: ->
if @isCursorBlinking()
@stopBlinkingCursors(true)
@startBlinkingCursorsAfterDelay ?= _.debounce(@startBlinkingCursors, @getCursorBlinkResumeDelay())
@startBlinkingCursorsAfterDelay()
@emitDidUpdateState()
requestAutoscroll: (position) ->
@pendingScrollLogicalPosition = position
@pendingScrollTop = null
@pendingScrollLeft = null
@shouldUpdateCursorsState = true
@shouldUpdateCustomGutterDecorationState = true
@shouldUpdateDecorations = true
@shouldUpdateHiddenInputState = true
@shouldUpdateHorizontalScrollState = true
@shouldUpdateLinesState = true
@shouldUpdateLineNumbersState = true
@shouldUpdateOverlaysState = true
@shouldUpdateScrollPosition = true
@shouldUpdateVerticalScrollState = true
@emitDidUpdateState()
didChangeFirstVisibleScreenRow: (screenRow) ->
@updateScrollTop(screenRow * @lineHeight)
getVerticalScrollMarginInPixels: ->
Math.round(@model.getVerticalScrollMargin() * @lineHeight)
getHorizontalScrollMarginInPixels: ->
Math.round(@model.getHorizontalScrollMargin() * @baseCharacterWidth)
getVerticalScrollbarWidth: ->
@verticalScrollbarWidth
getHorizontalScrollbarHeight: ->
@horizontalScrollbarHeight
commitPendingLogicalScrollTopPosition: ->
return unless @pendingScrollLogicalPosition?
{screenRange, options} = @pendingScrollLogicalPosition
verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels()
top = screenRange.start.row * @lineHeight
bottom = (screenRange.end.row + 1) * @lineHeight
if options?.center
desiredScrollCenter = (top + bottom) / 2
unless @getScrollTop() < desiredScrollCenter < @getScrollBottom()
desiredScrollTop = desiredScrollCenter - @getClientHeight() / 2
desiredScrollBottom = desiredScrollCenter + @getClientHeight() / 2
else
desiredScrollTop = top - verticalScrollMarginInPixels
desiredScrollBottom = bottom + verticalScrollMarginInPixels
if options?.reversed ? true
if desiredScrollBottom > @getScrollBottom()
@setScrollBottom(desiredScrollBottom, false)
if desiredScrollTop < @getScrollTop()
@setScrollTop(desiredScrollTop, false)
else
if desiredScrollTop < @getScrollTop()
@setScrollTop(desiredScrollTop, false)
if desiredScrollBottom > @getScrollBottom()
@setScrollBottom(desiredScrollBottom, false)
commitPendingLogicalScrollLeftPosition: ->
return unless @pendingScrollLogicalPosition?
{screenRange, options} = @pendingScrollLogicalPosition
horizontalScrollMarginInPixels = @getHorizontalScrollMarginInPixels()
{left} = @pixelRectForScreenRange(new Range(screenRange.start, screenRange.start))
{left: right} = @pixelRectForScreenRange(new Range(screenRange.end, screenRange.end))
left += @scrollLeft
right += @scrollLeft
desiredScrollLeft = left - horizontalScrollMarginInPixels
desiredScrollRight = right + horizontalScrollMarginInPixels
if options?.reversed ? true
if desiredScrollRight > @getScrollRight()
@setScrollRight(desiredScrollRight, false)
if desiredScrollLeft < @getScrollLeft()
@setScrollLeft(desiredScrollLeft, false)
else
if desiredScrollLeft < @getScrollLeft()
@setScrollLeft(desiredScrollLeft, false)
if desiredScrollRight > @getScrollRight()
@setScrollRight(desiredScrollRight, false)
commitPendingScrollLeftPosition: ->
if @pendingScrollLeft?
@updateScrollLeft(@pendingScrollLeft)
@pendingScrollLeft = null
commitPendingScrollTopPosition: ->
if @pendingScrollTop?
@updateScrollTop(@pendingScrollTop)
@pendingScrollTop = null
clearPendingScrollPosition: ->
@pendingScrollLogicalPosition = null
@pendingScrollTop = null
@pendingScrollLeft = null
canScrollLeftTo: (scrollLeft) ->
@scrollLeft isnt @constrainScrollLeft(scrollLeft)
canScrollTopTo: (scrollTop) ->
@scrollTop isnt @constrainScrollTop(scrollTop)
restoreScrollTopIfNeeded: ->
unless @scrollTop?
@updateScrollTop(@model.getFirstVisibleScreenRow() * @lineHeight)
restoreScrollLeftIfNeeded: ->
unless @scrollLeft?
@updateScrollLeft(@model.getFirstVisibleScreenColumn() * @baseCharacterWidth)
onDidChangeScrollTop: (callback) ->
@emitter.on 'did-change-scroll-top', callback
onDidChangeScrollLeft: (callback) ->
@emitter.on 'did-change-scroll-left', callback
getVisibleRowRange: ->
[@startRow, @endRow]