Files
atom/src/text-editor-component.js
2017-05-05 09:29:28 +02:00

2757 lines
86 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const etch = require('etch')
const {CompositeDisposable} = require('event-kit')
const {Point, Range} = require('text-buffer')
const ResizeDetector = require('element-resize-detector')
const TextEditor = require('./text-editor')
const {isPairedCharacter} = require('./text-utils')
const $ = etch.dom
let TextEditorElement
const DEFAULT_ROWS_PER_TILE = 6
const NORMAL_WIDTH_CHARACTER = 'x'
const DOUBLE_WIDTH_CHARACTER = '我'
const HALF_WIDTH_CHARACTER = 'ハ'
const KOREAN_CHARACTER = '세'
const NBSP_CHARACTER = '\u00a0'
const ZERO_WIDTH_NBSP_CHARACTER = '\ufeff'
const MOUSE_DRAG_AUTOSCROLL_MARGIN = 40
const MOUSE_WHEEL_SCROLL_SENSITIVITY = 0.8
const CURSOR_BLINK_RESUME_DELAY = 300
const CURSOR_BLINK_PERIOD = 800
function scaleMouseDragAutoscrollDelta (delta) {
return Math.pow(delta / 3, 3) / 280
}
module.exports =
class TextEditorComponent {
static setScheduler (scheduler) {
etch.setScheduler(scheduler)
}
static didUpdateScrollbarStyles () {
if (this.attachedComponents) {
this.attachedComponents.forEach((component) => {
component.didUpdateScrollbarStyles()
})
}
}
constructor (props) {
this.props = props
if (!props.model) props.model = new TextEditor()
if (props.element) {
this.element = props.element
} else {
if (!TextEditorElement) TextEditorElement = require('./text-editor-element')
this.element = new TextEditorElement()
}
this.element.initialize(this)
this.virtualNode = $('atom-text-editor')
this.virtualNode.domNode = this.element
this.refs = {}
this.updateSync = this.updateSync.bind(this)
this.didScrollDummyScrollbar = this.didScrollDummyScrollbar.bind(this)
this.didMouseDownOnContent = this.didMouseDownOnContent.bind(this)
this.disposables = new CompositeDisposable()
this.updateScheduled = false
this.measurements = null
this.visible = false
this.cursorsBlinking = false
this.cursorsBlinkedOff = false
this.nextUpdateOnlyBlinksCursors = null
this.horizontalPositionsToMeasure = new Map() // Keys are rows with positions we want to measure, values are arrays of columns to measure
this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horiontal pixel positions
this.lineNodesByScreenLineId = new Map()
this.textNodesByScreenLineId = new Map()
this.shouldRenderDummyScrollbars = true
this.refreshedScrollbarStyle = false
this.pendingAutoscroll = null
this.scrollTopPending = false
this.scrollLeftPending = false
this.scrollTop = 0
this.scrollLeft = 0
this.previousScrollWidth = 0
this.previousScrollHeight = 0
this.lastKeydown = null
this.lastKeydownBeforeKeypress = null
this.accentedCharacterMenuIsOpen = false
this.remeasureGutterDimensions = false
this.guttersToRender = [this.props.model.getLineNumberGutter()]
this.lineNumbersToRender = {
maxDigits: 2,
numbers: [],
keys: [],
foldableFlags: []
}
this.decorationsToRender = {
lineNumbers: null,
lines: null,
highlights: new Map(),
cursors: [],
overlays: [],
customGutter: new Map()
}
this.decorationsToMeasure = {
highlights: new Map(),
cursors: []
}
this.measuredContent = false
this.gutterContainerVnode = null
this.cursorsVnode = null
this.placeholderTextVnode = null
this.queryGuttersToRender()
this.queryMaxLineNumberDigits()
etch.updateSync(this)
this.observeModel()
getElementResizeDetector().listenTo(this.element, this.didResize.bind(this))
if (this.refs.gutterContainer) {
getElementResizeDetector().listenTo(this.refs.gutterContainer, this.didResizeGutterContainer.bind(this))
}
}
update (props) {
this.props = props
this.scheduleUpdate()
}
scheduleUpdate (nextUpdateOnlyBlinksCursors = false) {
if (!this.visible) return
this.nextUpdateOnlyBlinksCursors =
this.nextUpdateOnlyBlinksCursors !== false && nextUpdateOnlyBlinksCursors
if (this.updatedSynchronously) {
this.updateSync()
} else if (!this.updateScheduled) {
this.updateScheduled = true
etch.getScheduler().updateDocument(() => {
if (this.updateScheduled) this.updateSync(true)
})
}
}
updateSync (useScheduler = false) {
this.updateScheduled = false
if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise()
const onlyBlinkingCursors = this.nextUpdateOnlyBlinksCursors
this.nextUpdateOnlyBlinksCursors = null
if (onlyBlinkingCursors) {
this.updateCursorBlinkSync()
return
}
this.measuredContent = false
this.updateSyncBeforeMeasuringContent()
if (useScheduler === true) {
const scheduler = etch.getScheduler()
scheduler.readDocument(() => {
this.measureContentDuringUpdateSync()
this.measuredContent = true
scheduler.updateDocument(() => {
this.updateSyncAfterMeasuringContent()
})
})
} else {
this.measureContentDuringUpdateSync()
this.measuredContent = true
this.updateSyncAfterMeasuringContent()
}
}
updateSyncBeforeMeasuringContent () {
this.horizontalPositionsToMeasure.clear()
if (this.pendingAutoscroll) this.autoscrollVertically()
this.populateVisibleRowRange()
this.queryScreenLinesToRender()
this.queryLineNumbersToRender()
this.queryGuttersToRender()
this.queryDecorationsToRender()
this.shouldRenderDummyScrollbars = !this.refreshedScrollbarStyle
etch.updateSync(this)
this.shouldRenderDummyScrollbars = true
}
measureContentDuringUpdateSync () {
this.measureHorizontalPositions()
this.updateAbsolutePositionedDecorations()
if (this.remeasureGutterDimensions) {
if (this.measureGutterDimensions()) {
this.gutterContainerVnode = null
}
this.remeasureGutterDimensions = false
}
const wasHorizontalScrollbarVisible = this.isHorizontalScrollbarVisible()
this.measureLongestLineWidth()
if (this.pendingAutoscroll) {
this.autoscrollHorizontally()
if (!wasHorizontalScrollbarVisible && this.isHorizontalScrollbarVisible()) {
this.autoscrollVertically()
}
this.pendingAutoscroll = null
}
}
updateSyncAfterMeasuringContent () {
etch.updateSync(this)
this.currentFrameLineNumberGutterProps = null
this.scrollTopPending = false
this.scrollLeftPending = false
if (this.refreshedScrollbarStyle) {
this.measureScrollbarDimensions()
this.refreshedScrollbarStyle = false
etch.updateSync(this)
}
}
updateCursorBlinkSync () {
const className = this.getCursorsClassName()
this.refs.cursors.className = className
this.cursorsVnode.props.className = className
}
render () {
const {model} = this.props
const style = {}
if (!model.getAutoHeight() && !model.getAutoWidth()) {
style.contain = 'size'
}
if (this.measurements) {
if (model.getAutoHeight()) {
style.height = this.getContentHeight() + 'px'
}
if (model.getAutoWidth()) {
style.width = this.getGutterContainerWidth() + this.getContentWidth() + 'px'
}
}
let attributes = null
let className = this.focused ? 'editor is-focused' : 'editor'
if (model.isMini()) {
attributes = {mini: ''}
className = className + ' mini'
}
return $('atom-text-editor',
{
className,
style,
attributes,
tabIndex: -1,
on: {
focus: this.didFocus,
blur: this.didBlur,
mousewheel: this.didMouseWheel
}
},
$.div(
{
ref: 'clientContainer',
style: {
position: 'relative',
contain: 'strict',
overflow: 'hidden',
backgroundColor: 'inherit',
width: '100%',
height: '100%'
}
},
this.renderGutterContainer(),
this.renderScrollContainer()
),
this.renderOverlayDecorations()
)
}
renderGutterContainer () {
if (this.props.model.isMini()) return null
if (!this.measuredContent || !this.gutterContainerVnode) {
const innerStyle = {
willChange: 'transform',
backgroundColor: 'inherit',
display: 'flex'
}
let scrollHeight
if (this.measurements) {
innerStyle.transform = `translateY(${-this.getScrollTop()}px)`
scrollHeight = this.getScrollHeight()
}
return $.div(
{
ref: 'gutterContainer',
className: 'gutter-container',
style: {
position: 'relative',
zIndex: 1,
backgroundColor: 'inherit'
}
},
$.div({style: innerStyle},
this.guttersToRender.map((gutter) => {
if (gutter.name === 'line-number') {
return this.renderLineNumberGutter(gutter)
} else {
return $(CustomGutterComponent, {
key: gutter,
element: gutter.getElement(),
name: gutter.name,
visible: gutter.isVisible(),
height: scrollHeight,
decorations: this.decorationsToRender.customGutter.get(gutter.name)
})
}
})
)
)
}
return this.gutterContainerVnode
}
renderLineNumberGutter (gutter) {
if (!this.props.model.isLineNumberGutterVisible()) return null
if (this.measurements) {
const {maxDigits, keys, numbers, foldableFlags} = this.lineNumbersToRender
return $(LineNumberGutterComponent, {
ref: 'lineNumberGutter',
element: gutter.getElement(),
parentComponent: this,
startRow: this.getRenderedStartRow(),
endRow: this.getRenderedEndRow(),
rowsPerTile: this.getRowsPerTile(),
maxDigits: maxDigits,
keys: keys,
numbers: numbers,
foldableFlags: foldableFlags,
decorations: this.decorationsToRender.lineNumbers,
height: this.getScrollHeight(),
width: this.getLineNumberGutterWidth(),
lineHeight: this.getLineHeight(),
})
} else {
return $(LineNumberGutterComponent, {
ref: 'lineNumberGutter',
element: gutter.getElement(),
maxDigits: this.lineNumbersToRender.maxDigits
})
}
}
renderScrollContainer () {
const style = {
position: 'absolute',
contain: 'strict',
overflow: 'hidden',
top: 0,
bottom: 0,
backgroundColor: 'inherit'
}
if (this.measurements) {
style.left = this.getGutterContainerWidth() + 'px'
style.width = this.getScrollContainerWidth() + 'px'
}
return $.div(
{
ref: 'scrollContainer',
className: 'scroll-view',
style
},
this.renderContent(),
this.renderDummyScrollbars()
)
}
renderContent () {
let children
let style = {
contain: 'strict',
overflow: 'hidden',
backgroundColor: 'inherit'
}
if (this.measurements) {
style.width = this.getScrollWidth() + 'px'
style.height = this.getScrollHeight() + 'px'
style.willChange = 'transform'
style.transform = `translate(${-this.getScrollLeft()}px, ${-this.getScrollTop()}px)`
children = [
this.renderCursorsAndInput(),
this.renderLineTiles(),
this.renderPlaceholderText()
]
} else {
children = $.div({ref: 'characterMeasurementLine', className: 'line'},
$.span({ref: 'normalWidthCharacterSpan'}, NORMAL_WIDTH_CHARACTER),
$.span({ref: 'doubleWidthCharacterSpan'}, DOUBLE_WIDTH_CHARACTER),
$.span({ref: 'halfWidthCharacterSpan'}, HALF_WIDTH_CHARACTER),
$.span({ref: 'koreanCharacterSpan'}, KOREAN_CHARACTER)
)
}
return $.div(
{
ref: 'content',
on: {mousedown: this.didMouseDownOnContent},
style
},
children
)
}
renderLineTiles () {
if (!this.measurements) return []
const {lineNodesByScreenLineId, textNodesByScreenLineId} = this
const startRow = this.getRenderedStartRow()
const endRow = this.getRenderedEndRow()
const rowsPerTile = this.getRowsPerTile()
const tileHeight = this.getLineHeight() * rowsPerTile
const tileWidth = this.getScrollWidth()
const displayLayer = this.props.model.displayLayer
const tileNodes = new Array(this.getRenderedTileCount())
for (let tileStartRow = startRow; tileStartRow < endRow; tileStartRow = tileStartRow + rowsPerTile) {
const tileEndRow = Math.min(endRow, tileStartRow + rowsPerTile)
const tileIndex = this.tileIndexForTileStartRow(tileStartRow)
const highlightDecorations = this.decorationsToRender.highlights.get(tileStartRow)
tileNodes[tileIndex] = $(LinesTileComponent, {
key: tileIndex,
measuredContent: this.measuredContent,
height: tileHeight,
width: tileWidth,
top: this.topPixelPositionForRow(tileStartRow),
lineHeight: this.getLineHeight(),
renderedStartRow: startRow,
tileStartRow, tileEndRow,
screenLines: this.renderedScreenLines,
lineDecorations: this.decorationsToRender.lines,
highlightDecorations,
displayLayer,
lineNodesByScreenLineId,
textNodesByScreenLineId
})
}
if (this.longestLineToMeasure != null && (this.longestLineToMeasureRow < startRow || this.longestLineToMeasureRow >= endRow)) {
tileNodes.push($(LineComponent, {
key: this.longestLineToMeasure.id,
screenLine: this.longestLineToMeasure,
displayLayer,
lineNodesByScreenLineId,
textNodesByScreenLineId
}))
}
return $.div({
key: 'lineTiles',
ref: 'lineTiles',
className: 'lines',
style: {
position: 'absolute',
contain: 'strict',
overflow: 'hidden',
width: this.getScrollWidth() + 'px',
height: this.getScrollHeight() + 'px',
backgroundColor: 'inherit'
}
}, tileNodes)
}
renderCursorsAndInput () {
if (this.measuredContent) {
const className = this.getCursorsClassName()
const cursorHeight = this.getLineHeight() + 'px'
const children = [this.renderHiddenInput()]
for (let i = 0; i < this.decorationsToRender.cursors.length; i++) {
const {pixelLeft, pixelTop, pixelWidth} = this.decorationsToRender.cursors[i]
children.push($.div({
className: 'cursor',
style: {
height: cursorHeight,
width: pixelWidth + 'px',
transform: `translate(${pixelLeft}px, ${pixelTop}px)`
}
}))
}
this.cursorsVnode = $.div({
key: 'cursors',
ref: 'cursors',
className,
style: {
position: 'absolute',
contain: 'strict',
zIndex: 1,
width: this.getScrollWidth() + 'px',
height: this.getScrollHeight() + 'px'
}
}, children)
}
return this.cursorsVnode
}
getCursorsClassName () {
return this.cursorsBlinkedOff ? 'cursors blink-off' : 'cursors'
}
renderPlaceholderText () {
if (!this.measuredContent) {
this.placeholderTextVnode = null
const {model} = this.props
if (model.isEmpty()) {
const placeholderText = model.getPlaceholderText()
if (placeholderText != null) {
this.placeholderTextVnode = $.div({className: 'placeholder-text'}, placeholderText)
}
}
}
return this.placeholderTextVnode
}
renderHiddenInput () {
let top, left
if (this.hiddenInputPosition) {
top = this.hiddenInputPosition.pixelTop
left = this.hiddenInputPosition.pixelLeft
} else {
top = 0
left = 0
}
return $.input({
ref: 'hiddenInput',
key: 'hiddenInput',
className: 'hidden-input',
on: {
blur: this.didBlurHiddenInput,
focus: this.didFocusHiddenInput,
textInput: this.didTextInput,
keydown: this.didKeydown,
keyup: this.didKeyup,
keypress: this.didKeypress,
compositionstart: this.didCompositionStart,
compositionupdate: this.didCompositionUpdate,
compositionend: this.didCompositionEnd
},
tabIndex: -1,
style: {
position: 'absolute',
width: '1px',
height: this.getLineHeight() + 'px',
top: top + 'px',
left: left + 'px',
opacity: 0,
padding: 0,
border: 0
}
})
}
renderDummyScrollbars () {
if (this.shouldRenderDummyScrollbars) {
let scrollHeight, scrollTop, horizontalScrollbarHeight,
scrollWidth, scrollLeft, verticalScrollbarWidth, forceScrollbarVisible
if (this.measurements) {
scrollHeight = this.getScrollHeight()
scrollWidth = this.getScrollWidth()
scrollTop = this.getScrollTop()
scrollLeft = this.getScrollLeft()
horizontalScrollbarHeight =
this.isHorizontalScrollbarVisible()
? this.getHorizontalScrollbarHeight()
: 0
verticalScrollbarWidth =
this.isVerticalScrollbarVisible()
? this.getVerticalScrollbarWidth()
: 0
forceScrollbarVisible = this.refreshedScrollbarStyle
} else {
forceScrollbarVisible = true
}
const dummyScrollbarVnodes = [
$(DummyScrollbarComponent, {
ref: 'verticalScrollbar',
orientation: 'vertical',
didScroll: this.didScrollDummyScrollbar,
didMousedown: this.didMouseDownOnContent,
scrollHeight, scrollTop, horizontalScrollbarHeight, forceScrollbarVisible
}),
$(DummyScrollbarComponent, {
ref: 'horizontalScrollbar',
orientation: 'horizontal',
didScroll: this.didScrollDummyScrollbar,
didMousedown: this.didMouseDownOnContent,
scrollWidth, scrollLeft, verticalScrollbarWidth, forceScrollbarVisible
})
]
// If both scrollbars are visible, push a dummy element to force a "corner"
// to render where the two scrollbars meet at the lower right
if (verticalScrollbarWidth > 0 && horizontalScrollbarHeight > 0) {
dummyScrollbarVnodes.push($.div(
{
ref: 'scrollbarCorner',
style: {
position: 'absolute',
height: '20px',
width: '20px',
bottom: 0,
right: 0,
overflow: 'scroll'
}
}
))
}
return dummyScrollbarVnodes
} else {
return null
}
}
renderOverlayDecorations () {
return this.decorationsToRender.overlays.map((overlayProps) =>
$(OverlayComponent, Object.assign(
{key: overlayProps.element, didResize: this.updateSync},
overlayProps
))
)
}
getPlatform () {
return process.platform
}
queryScreenLinesToRender () {
const {model} = this.props
this.renderedScreenLines = model.displayLayer.getScreenLines(
this.getRenderedStartRow(),
this.getRenderedEndRow()
)
const longestLineRow = model.getApproximateLongestScreenRow()
const longestLine = model.screenLineForScreenRow(longestLineRow)
if (longestLine !== this.previousLongestLine) {
this.longestLineToMeasure = longestLine
this.longestLineToMeasureRow = longestLineRow
this.previousLongestLine = longestLine
}
}
queryLineNumbersToRender () {
const {model} = this.props
if (!model.isLineNumberGutterVisible()) return
this.queryMaxLineNumberDigits()
const startRow = this.getRenderedStartRow()
const endRow = this.getRenderedEndRow()
const renderedRowCount = this.getRenderedRowCount()
const {numbers, keys, foldableFlags} = this.lineNumbersToRender
numbers.length = renderedRowCount
keys.length = renderedRowCount
foldableFlags.length = renderedRowCount
let previousBufferRow = (startRow > 0) ? model.bufferRowForScreenRow(startRow - 1) : -1
let softWrapCount = 0
for (let row = startRow; row < endRow; row++) {
const i = row - startRow
const bufferRow = model.bufferRowForScreenRow(row)
if (bufferRow === previousBufferRow) {
numbers[i] = -1
keys[i] = bufferRow + 1 + '-' + softWrapCount++
foldableFlags[i] = false
} else {
softWrapCount = 0
numbers[i] = bufferRow + 1
keys[i] = bufferRow + 1
foldableFlags[i] = model.isFoldableAtBufferRow(bufferRow)
}
previousBufferRow = bufferRow
}
}
queryMaxLineNumberDigits () {
const {model} = this.props
if (model.isLineNumberGutterVisible()) {
const maxDigits = Math.max(2, model.getLineCount().toString().length)
if (maxDigits !== this.lineNumbersToRender.maxDigits) {
this.remeasureGutterDimensions = true
this.lineNumbersToRender.maxDigits = maxDigits
}
}
}
renderedScreenLineForRow (row) {
return this.renderedScreenLines[row - this.getRenderedStartRow()]
}
queryGuttersToRender () {
const oldGuttersToRender = this.guttersToRender
this.guttersToRender = this.props.model.getGutters()
if (!oldGuttersToRender || oldGuttersToRender.length !== this.guttersToRender.length) {
this.remeasureGutterDimensions = true
} else {
for (let i = 0, length = this.guttersToRender.length; i < length; i++) {
if (this.guttersToRender[i] !== oldGuttersToRender[i]) {
this.remeasureGutterDimensions = true
break
}
}
}
}
queryDecorationsToRender () {
this.decorationsToRender.lineNumbers = []
this.decorationsToRender.lines = []
this.decorationsToRender.overlays.length = 0
this.decorationsToRender.customGutter.clear()
this.decorationsToMeasure.highlights.clear()
this.decorationsToMeasure.cursors.length = 0
const decorationsByMarker =
this.props.model.decorationManager.decorationPropertiesByMarkerForScreenRowRange(
this.getRenderedStartRow(),
this.getRenderedEndRow()
)
decorationsByMarker.forEach((decorations, marker) => {
const screenRange = marker.getScreenRange()
const reversed = marker.isReversed()
for (let i = 0, length = decorations.length; i < decorations.length; i++) {
const decoration = decorations[i]
this.addDecorationToRender(decoration.type, decoration, marker, screenRange, reversed)
}
})
}
addDecorationToRender (type, decoration, marker, screenRange, reversed) {
if (Array.isArray(type)) {
for (let i = 0, length = type.length; i < length; i++) {
this.addDecorationToRender(type[i], decoration, marker, screenRange, reversed)
}
} else {
switch (type) {
case 'line':
case 'line-number':
this.addLineDecorationToRender(type, decoration, screenRange, reversed)
break
case 'highlight':
this.addHighlightDecorationToMeasure(decoration, screenRange, marker.id)
break
case 'cursor':
this.addCursorDecorationToMeasure(marker, screenRange, reversed)
break
case 'overlay':
this.addOverlayDecorationToRender(decoration, marker)
break
case 'gutter':
this.addCustomGutterDecorationToRender(decoration, screenRange)
break
}
}
}
addLineDecorationToRender (type, decoration, screenRange, reversed) {
const decorationsToRender = (type === 'line') ? this.decorationsToRender.lines : this.decorationsToRender.lineNumbers
let omitLastRow = false
if (screenRange.isEmpty()) {
if (decoration.onlyNonEmpty) return
} else {
if (decoration.onlyEmpty) return
if (decoration.omitEmptyLastRow !== false) {
omitLastRow = screenRange.end.column === 0
}
}
const renderedStartRow = this.getRenderedStartRow()
let rangeStartRow = screenRange.start.row
let rangeEndRow = screenRange.end.row
if (decoration.onlyHead) {
if (reversed) {
rangeEndRow = rangeStartRow
} else {
rangeStartRow = rangeEndRow
}
}
rangeStartRow = Math.max(rangeStartRow, this.getRenderedStartRow())
rangeEndRow = Math.min(rangeEndRow, this.getRenderedEndRow() - 1)
for (let row = rangeStartRow; row <= rangeEndRow; row++) {
if (omitLastRow && row === screenRange.end.row) break
const currentClassName = decorationsToRender[row - renderedStartRow]
const newClassName = currentClassName ? currentClassName + ' ' + decoration.class : decoration.class
decorationsToRender[row - renderedStartRow] = newClassName
}
}
addHighlightDecorationToMeasure(decoration, screenRange, key) {
screenRange = constrainRangeToRows(screenRange, this.getRenderedStartRow(), this.getRenderedEndRow())
if (screenRange.isEmpty()) return
const {class: className, flashRequested, flashClass, flashDuration} = decoration
decoration.flashRequested = false
let tileStartRow = this.tileStartRowForRow(screenRange.start.row)
const rowsPerTile = this.getRowsPerTile()
while (tileStartRow <= screenRange.end.row) {
const tileEndRow = tileStartRow + rowsPerTile
const screenRangeInTile = constrainRangeToRows(screenRange, tileStartRow, tileEndRow)
let tileHighlights = this.decorationsToMeasure.highlights.get(tileStartRow)
if (!tileHighlights) {
tileHighlights = []
this.decorationsToMeasure.highlights.set(tileStartRow, tileHighlights)
}
tileHighlights.push({
screenRange: screenRangeInTile,
key, className, flashRequested, flashClass, flashDuration
})
this.requestHorizontalMeasurement(screenRangeInTile.start.row, screenRangeInTile.start.column)
this.requestHorizontalMeasurement(screenRangeInTile.end.row, screenRangeInTile.end.column)
tileStartRow = tileStartRow + rowsPerTile
}
}
addCursorDecorationToMeasure (marker, screenRange, reversed) {
const {model} = this.props
const isLastCursor = model.getLastCursor().getMarker() === marker
const screenPosition = reversed ? screenRange.start : screenRange.end
const {row, column} = screenPosition
if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) return
this.requestHorizontalMeasurement(row, column)
let columnWidth = 0
if (model.lineLengthForScreenRow(row) > column) {
columnWidth = 1
this.requestHorizontalMeasurement(row, column + 1)
}
this.decorationsToMeasure.cursors.push({screenPosition, columnWidth, isLastCursor})
}
addOverlayDecorationToRender (decoration, marker) {
const {class: className, item, position, avoidOverflow} = decoration
const element = TextEditor.viewForItem(item)
const screenPosition = (position === 'tail')
? marker.getTailScreenPosition()
: marker.getHeadScreenPosition()
this.requestHorizontalMeasurement(screenPosition.row, screenPosition.column)
this.decorationsToRender.overlays.push({className, element, avoidOverflow, screenPosition})
}
addCustomGutterDecorationToRender (decoration, screenRange) {
let decorations = this.decorationsToRender.customGutter.get(decoration.gutterName)
if (!decorations) {
decorations = []
this.decorationsToRender.customGutter.set(decoration.gutterName, decorations)
}
const top = this.pixelTopForRow(screenRange.start.row)
const height = this.pixelTopForRow(screenRange.end.row + 1) - top
decorations.push({
className: decoration.class,
element: TextEditor.viewForItem(decoration.item),
top, height
})
}
updateAbsolutePositionedDecorations () {
this.updateHighlightsToRender()
this.updateCursorsToRender()
this.updateOverlaysToRender()
}
updateHighlightsToRender () {
this.decorationsToRender.highlights.clear()
this.decorationsToMeasure.highlights.forEach((highlights, tileRow) => {
for (let i = 0, length = highlights.length; i < length; i++) {
const highlight = highlights[i]
const {start, end} = highlight.screenRange
highlight.startPixelTop = this.pixelTopForRow(start.row)
highlight.startPixelLeft = this.pixelLeftForRowAndColumn(start.row, start.column)
highlight.endPixelTop = this.pixelTopForRow(end.row + 1)
highlight.endPixelLeft = this.pixelLeftForRowAndColumn(end.row, end.column)
}
this.decorationsToRender.highlights.set(tileRow, highlights)
})
}
updateCursorsToRender () {
this.decorationsToRender.cursors.length = 0
const height = this.getLineHeight() + 'px'
for (let i = 0; i < this.decorationsToMeasure.cursors.length; i++) {
const cursor = this.decorationsToMeasure.cursors[i]
const {row, column} = cursor.screenPosition
const pixelTop = this.pixelTopForRow(row)
const pixelLeft = this.pixelLeftForRowAndColumn(row, column)
const pixelRight = (cursor.columnWidth === 0)
? pixelLeft
: this.pixelLeftForRowAndColumn(row, column + 1)
const pixelWidth = pixelRight - pixelLeft
const cursorPosition = {pixelTop, pixelLeft, pixelWidth}
this.decorationsToRender.cursors[i] = cursorPosition
if (cursor.isLastCursor) this.hiddenInputPosition = cursorPosition
}
}
updateOverlaysToRender () {
const overlayCount = this.decorationsToRender.overlays.length
if (overlayCount === 0) return null
const windowInnerHeight = this.getWindowInnerHeight()
const windowInnerWidth = this.getWindowInnerWidth()
const contentClientRect = this.refs.content.getBoundingClientRect()
for (let i = 0; i < overlayCount; i++) {
const decoration = this.decorationsToRender.overlays[i]
const {element, screenPosition, avoidOverflow} = decoration
const {row, column} = screenPosition
let wrapperTop = contentClientRect.top + this.pixelTopForRow(row) + this.getLineHeight()
let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column)
if (avoidOverflow !== false) {
const computedStyle = window.getComputedStyle(element)
const elementHeight = element.offsetHeight
const elementTop = wrapperTop + parseInt(computedStyle.marginTop)
const elementBottom = elementTop + elementHeight
const flippedElementTop = wrapperTop - this.getLineHeight() - elementHeight - parseInt(computedStyle.marginBottom)
const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft)
const elementRight = elementLeft + element.offsetWidth
if (elementBottom > windowInnerHeight && flippedElementTop >= 0) {
wrapperTop -= (elementTop - flippedElementTop)
}
if (elementLeft < 0) {
wrapperLeft -= elementLeft
} else if (elementRight > windowInnerWidth) {
wrapperLeft -= (elementRight - windowInnerWidth)
}
}
decoration.pixelTop = wrapperTop
decoration.pixelLeft = wrapperLeft
}
}
didAttach () {
if (!this.attached) {
this.attached = true
this.intersectionObserver = new IntersectionObserver((entries) => {
const {intersectionRect} = entries[entries.length - 1]
if (intersectionRect.width > 0 || intersectionRect.height > 0) {
this.didShow()
} else {
this.didHide()
}
})
this.intersectionObserver.observe(this.element)
if (this.isVisible()) {
this.didShow()
} else {
this.didHide()
}
if (!this.constructor.attachedComponents) {
this.constructor.attachedComponents = new Set()
}
this.constructor.attachedComponents.add(this)
}
}
didDetach () {
if (this.attached) {
this.didHide()
this.attached = false
this.constructor.attachedComponents.delete(this)
}
}
didShow () {
if (!this.visible) {
this.visible = true
if (!this.measurements) this.performInitialMeasurements()
this.props.model.setVisible(true)
this.updateSync()
}
}
didHide () {
if (this.visible) {
this.visible = false
this.props.model.setVisible(false)
}
}
didFocus () {
// This element can be focused from a parent custom element's
// attachedCallback before *its* attachedCallback is fired. This protects
// against that case.
if (!this.attached) this.didAttach()
// The element can be focused before the intersection observer detects that
// it has been shown for the first time. If this element is being focused,
// it is necessarily visible, so we call `didShow` to ensure the hidden
// input is rendered before we try to shift focus to it.
if (!this.visible) this.didShow()
if (!this.focused) {
this.focused = true
this.startCursorBlinking()
this.scheduleUpdate()
}
// Transfer focus to the hidden input, but first ensure the input is in the
// visible part of the scrolled content to avoid the browser trying to
// auto-scroll to the form-field.
const {hiddenInput} = this.refs
hiddenInput.style.top = this.getScrollTop() + 'px'
hiddenInput.style.left = this.getScrollLeft() + 'px'
hiddenInput.focus()
// Restore the previous position of the field now that it is already focused
// and won't cause unwanted scrolling.
if (this.hiddenInputPosition) {
hiddenInput.style.top = this.hiddenInputPosition.pixelTop + 'px'
hiddenInput.style.left = this.hiddenInputPosition.pixelLeft + 'px'
} else {
hiddenInput.style.top = 0
hiddenInput.style.left = 0
}
}
didBlur (event) {
if (event.relatedTarget === this.refs.hiddenInput) {
event.stopImmediatePropagation()
}
}
didBlurHiddenInput (event) {
if (this.element !== event.relatedTarget && !this.element.contains(event.relatedTarget)) {
this.focused = false
this.stopCursorBlinking()
this.scheduleUpdate()
this.element.dispatchEvent(new FocusEvent(event.type, event))
}
}
didFocusHiddenInput () {
if (!this.focused) {
this.focused = true
this.startCursorBlinking()
this.scheduleUpdate()
}
}
didMouseWheel (eveWt) {
let {deltaX, deltaY} = event
deltaX = deltaX * MOUSE_WHEEL_SCROLL_SENSITIVITY
deltaY = deltaY * MOUSE_WHEEL_SCROLL_SENSITIVITY
const scrollPositionChanged =
this.setScrollLeft(this.getScrollLeft() + deltaX) ||
this.setScrollTop(this.getScrollTop() + deltaY)
if (scrollPositionChanged) this.updateSync()
}
didResize () {
if (this.measureClientContainerDimensions()) {
this.scheduleUpdate()
}
}
didResizeGutterContainer () {
if (this.measureGutterDimensions()) {
this.scheduleUpdate()
}
}
didScrollDummyScrollbar () {
let scrollTopChanged = false
let scrollLeftChanged = false
if (!this.scrollTopPending) {
scrollTopChanged = this.setScrollTop(this.refs.verticalScrollbar.element.scrollTop)
}
if (!this.scrollLeftPending) {
scrollLeftChanged = this.setScrollLeft(this.refs.horizontalScrollbar.element.scrollLeft)
}
if (scrollTopChanged || scrollLeftChanged) this.updateSync()
}
didUpdateScrollbarStyles () {
this.refreshedScrollbarStyle = true
this.scheduleUpdate()
}
didTextInput (event) {
event.stopPropagation()
// WARNING: If we call preventDefault on the input of a space character,
// then the browser interprets the spacebar keypress as a page-down command,
// causing spaces to scroll elements containing editors. This is impossible
// to test.
if (event.data !== ' ') event.preventDefault()
// TODO: Deal with disabled input
// if (!this.isInputEnabled()) return
if (this.compositionCheckpoint) {
this.props.model.revertToCheckpoint(this.compositionCheckpoint)
this.compositionCheckpoint = null
}
// Undo insertion of the original non-accented character so it is discarded
// from the history and does not reappear on undo
if (this.accentedCharacterMenuIsOpen) {
this.props.model.undo()
}
this.props.model.insertText(event.data, {groupUndo: true})
}
// We need to get clever to detect when the accented character menu is
// opened on macOS. Usually, every keydown event that could cause input is
// followed by a corresponding keypress. However, pressing and holding
// long enough to open the accented character menu causes additional keydown
// events to fire that aren't followed by their own keypress and textInput
// events.
//
// Therefore, we assume the accented character menu has been deployed if,
// before observing any keyup event, we observe events in the following
// sequence:
//
// keydown(keyCode: X), keypress, keydown(keyCode: X)
//
// The keyCode X must be the same in the keydown events that bracket the
// keypress, meaning we're *holding* the _same_ key we intially pressed.
// Got that?
didKeydown (event) {
if (this.lastKeydownBeforeKeypress != null) {
if (this.lastKeydownBeforeKeypress.keyCode === event.keyCode) {
this.accentedCharacterMenuIsOpen = true
this.props.model.selectLeft()
}
this.lastKeydownBeforeKeypress = null
} else {
this.lastKeydown = event
}
}
didKeypress () {
this.lastKeydownBeforeKeypress = this.lastKeydown
this.lastKeydown = null
// This cancels the accented character behavior if we type a key normally
// with the menu open.
this.accentedCharacterMenuIsOpen = false
}
didKeyup () {
this.lastKeydownBeforeKeypress = null
this.lastKeydown = null
}
// The IME composition events work like this:
//
// User types 's', chromium pops up the completion helper
// 1. compositionstart fired
// 2. compositionupdate fired; event.data == 's'
// User hits arrow keys to move around in completion helper
// 3. compositionupdate fired; event.data == 's' for each arry key press
// User escape to cancel
// 4. compositionend fired
// OR User chooses a completion
// 4. compositionend fired
// 5. textInput fired; event.data == the completion string
didCompositionStart () {
this.compositionCheckpoint = this.props.model.createCheckpoint()
}
didCompositionUpdate (event) {
this.props.model.insertText(event.data, {select: true})
}
didCompositionEnd (event) {
event.target.value = ''
}
didMouseDownOnContent (event) {
const {model} = this.props
const {target, button, detail, ctrlKey, shiftKey, metaKey} = event
// Only handle mousedown events for left mouse button (or the middle mouse
// button on Linux where it pastes the selection clipboard).
if (!(button === 0 || (this.getPlatform() === 'linux' && button === 1))) return
const screenPosition = this.screenPositionForMouseEvent(event)
if (target && target.matches('.fold-marker')) {
const bufferPosition = model.bufferPositionForScreenPosition(screenPosition)
model.destroyFoldsIntersectingBufferRange(Range(bufferPosition, bufferPosition))
return
}
const addOrRemoveSelection = metaKey || (ctrlKey && this.getPlatform() !== 'darwin')
switch (detail) {
case 1:
if (addOrRemoveSelection) {
const existingSelection = model.getSelectionAtScreenPosition(screenPosition)
if (existingSelection) {
if (model.hasMultipleCursors()) existingSelection.destroy()
} else {
model.addCursorAtScreenPosition(screenPosition)
}
} else {
if (shiftKey) {
model.selectToScreenPosition(screenPosition)
} else {
model.setCursorScreenPosition(screenPosition)
}
}
break
case 2:
if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition)
model.getLastSelection().selectWord({autoscroll: false})
break
case 3:
if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition)
model.getLastSelection().selectLine(null, {autoscroll: false})
break
}
this.handleMouseDragUntilMouseUp({
didDrag: (event) => {
this.autoscrollOnMouseDrag(event)
const screenPosition = this.screenPositionForMouseEvent(event)
model.selectToScreenPosition(screenPosition, {suppressSelectionMerge: true, autoscroll: false})
this.updateSync()
},
didStopDragging: () => {
model.finalizeSelections()
model.mergeIntersectingSelections()
this.updateSync()
}
})
}
didMouseDownOnLineNumberGutter (event) {
const {model} = this.props
const {target, button, ctrlKey, shiftKey, metaKey} = event
// Only handle mousedown events for left mouse button
if (button !== 0) return
const clickedScreenRow = this.screenPositionForMouseEvent(event).row
const startBufferRow = model.bufferPositionForScreenPosition([clickedScreenRow, 0]).row
if (target && target.matches('.foldable .icon-right')) {
model.toggleFoldAtBufferRow(startBufferRow)
return
}
const addOrRemoveSelection = metaKey || (ctrlKey && this.getPlatform() !== 'darwin')
const endBufferRow = model.bufferPositionForScreenPosition([clickedScreenRow, Infinity]).row
const clickedLineBufferRange = Range(Point(startBufferRow, 0), Point(endBufferRow + 1, 0))
let initialBufferRange
if (shiftKey) {
const lastSelection = model.getLastSelection()
initialBufferRange = lastSelection.getBufferRange()
lastSelection.setBufferRange(initialBufferRange.union(clickedLineBufferRange), {
reversed: clickedScreenRow < lastSelection.getScreenRange().start.row,
autoscroll: false,
preserveFolds: true,
suppressSelectionMerge: true
})
} else {
initialBufferRange = clickedLineBufferRange
if (addOrRemoveSelection) {
model.addSelectionForBufferRange(clickedLineBufferRange, {autoscroll: false, preserveFolds: true})
} else {
model.setSelectedBufferRange(clickedLineBufferRange, {autoscroll: false, preserveFolds: true})
}
}
const initialScreenRange = model.screenRangeForBufferRange(initialBufferRange)
this.handleMouseDragUntilMouseUp({
didDrag: (event) => {
this.autoscrollOnMouseDrag(event, true)
const dragRow = this.screenPositionForMouseEvent(event).row
const draggedLineScreenRange = Range(Point(dragRow, 0), Point(dragRow + 1, 0))
model.getLastSelection().setScreenRange(draggedLineScreenRange.union(initialScreenRange), {
reversed: dragRow < initialScreenRange.start.row,
autoscroll: false,
preserveFolds: true
})
this.updateSync()
},
didStopDragging: () => {
model.mergeIntersectingSelections()
this.updateSync()
}
})
}
handleMouseDragUntilMouseUp ({didDrag, didStopDragging}) {
let dragging = false
let lastMousemoveEvent
const animationFrameLoop = () => {
window.requestAnimationFrame(() => {
if (dragging && this.visible) {
didDrag(lastMousemoveEvent)
animationFrameLoop()
}
})
}
function didMouseMove (event) {
lastMousemoveEvent = event
if (!dragging) {
dragging = true
animationFrameLoop()
}
}
function didMouseUp () {
window.removeEventListener('mousemove', didMouseMove)
window.removeEventListener('mouseup', didMouseUp)
if (dragging) {
dragging = false
didStopDragging()
}
}
window.addEventListener('mousemove', didMouseMove)
window.addEventListener('mouseup', didMouseUp)
}
autoscrollOnMouseDrag ({clientX, clientY}, verticalOnly = false) {
var {top, bottom, left, right} = this.refs.scrollContainer.getBoundingClientRect() // Using var to avoid deopt on += assignments below
top += MOUSE_DRAG_AUTOSCROLL_MARGIN
bottom -= MOUSE_DRAG_AUTOSCROLL_MARGIN
left += MOUSE_DRAG_AUTOSCROLL_MARGIN
right -= MOUSE_DRAG_AUTOSCROLL_MARGIN
let yDelta, yDirection
if (clientY < top) {
yDelta = top - clientY
yDirection = -1
} else if (clientY > bottom) {
yDelta = clientY - bottom
yDirection = 1
}
let xDelta, xDirection
if (clientX < left) {
xDelta = left - clientX
xDirection = -1
} else if (clientX > right) {
xDelta = clientX - right
xDirection = 1
}
let scrolled = false
if (yDelta != null) {
const scaledDelta = scaleMouseDragAutoscrollDelta(yDelta) * yDirection
scrolled = this.setScrollTop(this.getScrollTop() + scaledDelta)
}
if (!verticalOnly && xDelta != null) {
const scaledDelta = scaleMouseDragAutoscrollDelta(xDelta) * xDirection
scrolled = this.setScrollLeft(this.getScrollLeft() + scaledDelta)
}
if (scrolled) this.updateSync()
}
screenPositionForMouseEvent ({clientX, clientY}) {
const scrollContainerRect = this.refs.scrollContainer.getBoundingClientRect()
clientX = Math.min(scrollContainerRect.right, Math.max(scrollContainerRect.left, clientX))
clientY = Math.min(scrollContainerRect.bottom, Math.max(scrollContainerRect.top, clientY))
const linesRect = this.refs.lineTiles.getBoundingClientRect()
return this.screenPositionForPixelPosition({
top: clientY - linesRect.top,
left: clientX - linesRect.left
})
}
didUpdateSelections () {
this.pauseCursorBlinking()
this.scheduleUpdate()
}
pauseCursorBlinking () {
this.stopCursorBlinking()
if (this.resumeCursorBlinkingTimeoutHandle) {
window.clearTimeout(this.resumeCursorBlinkingTimeoutHandle)
}
this.resumeCursorBlinkingTimeoutHandle = window.setTimeout(() => {
this.cursorsBlinkedOff = true
this.startCursorBlinking()
this.resumeCursorBlinkingTimeoutHandle = null
}, CURSOR_BLINK_RESUME_DELAY)
}
stopCursorBlinking () {
if (this.cursorsBlinking) {
this.cursorsBlinkedOff = false
this.cursorsBlinking = false
window.clearInterval(this.cursorBlinkIntervalHandle)
this.cursorBlinkIntervalHandle = null
this.scheduleUpdate()
}
}
startCursorBlinking () {
if (!this.cursorsBlinking) {
this.cursorBlinkIntervalHandle = window.setInterval(() => {
this.cursorsBlinkedOff = !this.cursorsBlinkedOff
this.scheduleUpdate(true)
}, CURSOR_BLINK_PERIOD / 2)
this.cursorsBlinking = true
this.scheduleUpdate(true)
}
}
didRequestAutoscroll (autoscroll) {
this.pendingAutoscroll = autoscroll
this.scheduleUpdate()
}
autoscrollVertically () {
const {screenRange, options} = this.pendingAutoscroll
const screenRangeTop = this.pixelTopForRow(screenRange.start.row)
const screenRangeBottom = this.pixelTopForRow(screenRange.end.row) + this.getLineHeight()
const verticalScrollMargin = this.getVerticalAutoscrollMargin()
this.requestHorizontalMeasurement(screenRange.start.row, screenRange.start.column)
this.requestHorizontalMeasurement(screenRange.end.row, screenRange.end.column)
let desiredScrollTop, desiredScrollBottom
if (options && options.center) {
const desiredScrollCenter = (screenRangeTop + screenRangeBottom) / 2
if (desiredScrollCenter < this.getScrollTop() || desiredScrollCenter > this.getScrollBottom()) {
desiredScrollTop = desiredScrollCenter - this.measurements.clientHeight / 2
desiredScrollBottom = desiredScrollCenter + this.measurements.clientHeight / 2
}
} else {
desiredScrollTop = screenRangeTop - verticalScrollMargin
desiredScrollBottom = screenRangeBottom + verticalScrollMargin
}
if (!options || options.reversed !== false) {
if (desiredScrollBottom > this.getScrollBottom()) {
this.setScrollBottom(desiredScrollBottom)
}
if (desiredScrollTop < this.getScrollTop()) {
this.setScrollTop(desiredScrollTop)
}
} else {
if (desiredScrollTop < this.getScrollTop()) {
this.setScrollTop(desiredScrollTop)
}
if (desiredScrollBottom > this.getScrollBottom()) {
this.setScrollBottom(desiredScrollBottom)
}
}
return false
}
autoscrollHorizontally () {
const horizontalScrollMargin = this.getHorizontalAutoscrollMargin()
const {screenRange, options} = this.pendingAutoscroll
const gutterContainerWidth = this.getGutterContainerWidth()
let left = this.pixelLeftForRowAndColumn(screenRange.start.row, screenRange.start.column) + gutterContainerWidth
let right = this.pixelLeftForRowAndColumn(screenRange.end.row, screenRange.end.column) + gutterContainerWidth
const desiredScrollLeft = Math.max(0, left - horizontalScrollMargin - gutterContainerWidth)
const desiredScrollRight = Math.min(this.getScrollWidth(), right + horizontalScrollMargin)
if (!options || options.reversed !== false) {
if (desiredScrollRight > this.getScrollRight()) {
this.setScrollRight(desiredScrollRight)
}
if (desiredScrollLeft < this.getScrollLeft()) {
this.setScrollLeft(desiredScrollLeft)
}
} else {
if (desiredScrollLeft < this.getScrollLeft()) {
this.setScrollLeft(desiredScrollLeft)
}
if (desiredScrollRight > this.getScrollRight()) {
this.setScrollRight(desiredScrollRight)
}
}
}
getVerticalAutoscrollMargin () {
const maxMarginInLines = Math.floor(
(this.getScrollContainerClientHeight() / this.getLineHeight() - 1) / 2
)
const marginInLines = Math.min(
this.props.model.verticalScrollMargin,
maxMarginInLines
)
return marginInLines * this.getLineHeight()
}
getHorizontalAutoscrollMargin () {
const maxMarginInBaseCharacters = Math.floor(
(this.getScrollContainerClientWidth() / this.getBaseCharacterWidth() - 1) / 2
)
const marginInBaseCharacters = Math.min(
this.props.model.horizontalScrollMargin,
maxMarginInBaseCharacters
)
return marginInBaseCharacters * this.getBaseCharacterWidth()
}
performInitialMeasurements () {
this.measurements = {}
this.measureCharacterDimensions()
this.measureGutterDimensions()
this.measureClientContainerDimensions()
this.measureScrollbarDimensions()
}
measureCharacterDimensions () {
this.measurements.lineHeight = this.refs.characterMeasurementLine.getBoundingClientRect().height
this.measurements.baseCharacterWidth = this.refs.normalWidthCharacterSpan.getBoundingClientRect().width
this.measurements.doubleWidthCharacterWidth = this.refs.doubleWidthCharacterSpan.getBoundingClientRect().width
this.measurements.halfWidthCharacterWidth = this.refs.halfWidthCharacterSpan.getBoundingClientRect().width
this.measurements.koreanCharacterWidth = this.refs.koreanCharacterSpan.getBoundingClientRect().widt
this.props.model.setDefaultCharWidth(
this.measurements.baseCharacterWidth,
this.measurements.doubleWidthCharacterWidth,
this.measurements.halfWidthCharacterWidth,
this.measurements.koreanCharacterWidth
)
}
measureGutterDimensions () {
let dimensionsChanged = false
if (this.refs.gutterContainer) {
const gutterContainerWidth = this.refs.gutterContainer.offsetWidth
if (gutterContainerWidth !== this.measurements.gutterContainerWidth) {
dimensionsChanged = true
this.measurements.gutterContainerWidth = gutterContainerWidth
}
} else {
this.measurements.gutterContainerWidth = 0
}
if (this.refs.lineNumberGutter) {
const lineNumberGutterWidth = this.refs.lineNumberGutter.element.offsetWidth
if (lineNumberGutterWidth !== this.measurements.lineNumberGutterWidth) {
dimensionsChanged = true
this.measurements.lineNumberGutterWidth = lineNumberGutterWidth
}
} else {
this.measurements.lineNumberGutterWidth = 0
}
return dimensionsChanged
}
measureClientContainerDimensions () {
if (!this.measurements) return false
let dimensionsChanged = false
const clientContainerHeight = this.refs.clientContainer.offsetHeight
const clientContainerWidth = this.refs.clientContainer.offsetWidth
if (clientContainerHeight !== this.measurements.clientContainerHeight) {
this.measurements.clientContainerHeight = clientContainerHeight
dimensionsChanged = true
}
if (clientContainerWidth !== this.measurements.clientContainerWidth) {
this.measurements.clientContainerWidth = clientContainerWidth
this.props.model.setEditorWidthInChars(this.getScrollContainerWidth() / this.getBaseCharacterWidth())
dimensionsChanged = true
}
return dimensionsChanged
}
measureScrollbarDimensions () {
this.measurements.verticalScrollbarWidth = this.refs.verticalScrollbar.getRealScrollbarWidth()
this.measurements.horizontalScrollbarHeight = this.refs.horizontalScrollbar.getRealScrollbarHeight()
}
measureLongestLineWidth () {
if (this.longestLineToMeasure) {
this.measurements.longestLineWidth = this.lineNodesByScreenLineId.get(this.longestLineToMeasure.id).firstChild.offsetWidth
this.longestLineToMeasureRow = null
this.longestLineToMeasure = null
}
}
requestHorizontalMeasurement (row, column) {
if (column === 0) return
let columns = this.horizontalPositionsToMeasure.get(row)
if (columns == null) {
columns = []
this.horizontalPositionsToMeasure.set(row, columns)
}
columns.push(column)
}
measureHorizontalPositions () {
this.horizontalPositionsToMeasure.forEach((columnsToMeasure, row) => {
columnsToMeasure.sort((a, b) => a - b)
const screenLine = this.renderedScreenLineForRow(row)
const lineNode = this.lineNodesByScreenLineId.get(screenLine.id)
if (!lineNode) {
const error = new Error('Requested measurement of a line that is not currently rendered')
error.metadata = {row, columnsToMeasure}
throw error
}
const textNodes = this.textNodesByScreenLineId.get(screenLine.id)
let positionsForLine = this.horizontalPixelPositionsByScreenLineId.get(screenLine.id)
if (positionsForLine == null) {
positionsForLine = new Map()
this.horizontalPixelPositionsByScreenLineId.set(screenLine.id, positionsForLine)
}
this.measureHorizontalPositionsOnLine(lineNode, textNodes, columnsToMeasure, positionsForLine)
})
}
measureHorizontalPositionsOnLine (lineNode, textNodes, columnsToMeasure, positions) {
let lineNodeClientLeft = -1
let textNodeStartColumn = 0
let textNodesIndex = 0
columnLoop:
for (let columnsIndex = 0; columnsIndex < columnsToMeasure.length; columnsIndex++) {
while (textNodesIndex < textNodes.length) {
const nextColumnToMeasure = columnsToMeasure[columnsIndex]
if (nextColumnToMeasure === 0) {
positions.set(0, 0)
continue columnLoop
}
if (nextColumnToMeasure >= lineNode.textContent.length) {
}
if (positions.has(nextColumnToMeasure)) continue columnLoop
const textNode = textNodes[textNodesIndex]
const textNodeEndColumn = textNodeStartColumn + textNode.textContent.length
if (nextColumnToMeasure <= textNodeEndColumn) {
let clientPixelPosition
if (nextColumnToMeasure === textNodeStartColumn) {
clientPixelPosition = clientRectForRange(textNode, 0, 1).left
} else {
clientPixelPosition = clientRectForRange(textNode, 0, nextColumnToMeasure - textNodeStartColumn).right
}
if (lineNodeClientLeft === -1) lineNodeClientLeft = lineNode.getBoundingClientRect().left
positions.set(nextColumnToMeasure, clientPixelPosition - lineNodeClientLeft)
continue columnLoop
} else {
textNodesIndex++
textNodeStartColumn = textNodeEndColumn
}
}
}
}
pixelTopForRow (row) {
return row * this.getLineHeight()
}
pixelLeftForRowAndColumn (row, column) {
if (column === 0) return 0
const screenLine = this.renderedScreenLineForRow(row)
return this.horizontalPixelPositionsByScreenLineId.get(screenLine.id).get(column)
}
screenPositionForPixelPosition({top, left}) {
const {model} = this.props
const row = Math.min(
Math.max(0, Math.floor(top / this.measurements.lineHeight)),
model.getApproximateScreenLineCount() - 1
)
const linesClientLeft = this.refs.lineTiles.getBoundingClientRect().left
const targetClientLeft = linesClientLeft + Math.max(0, left)
const screenLine = this.renderedScreenLineForRow(row)
const textNodes = this.textNodesByScreenLineId.get(screenLine.id)
let containingTextNodeIndex
{
let low = 0
let high = textNodes.length - 1
while (low <= high) {
const mid = low + ((high - low) >> 1)
const textNode = textNodes[mid]
const textNodeRect = clientRectForRange(textNode, 0, textNode.length)
if (targetClientLeft < textNodeRect.left) {
high = mid - 1
containingTextNodeIndex = Math.max(0, mid - 1)
} else if (targetClientLeft > textNodeRect.right) {
low = mid + 1
containingTextNodeIndex = Math.min(textNodes.length - 1, mid + 1)
} else {
containingTextNodeIndex = mid
break
}
}
}
const containingTextNode = textNodes[containingTextNodeIndex]
let characterIndex = 0
{
let low = 0
let high = containingTextNode.length - 1
while (low <= high) {
const charIndex = low + ((high - low) >> 1)
const nextCharIndex = isPairedCharacter(containingTextNode.textContent, charIndex)
? charIndex + 2
: charIndex + 1
const rangeRect = clientRectForRange(containingTextNode, charIndex, nextCharIndex)
if (targetClientLeft < rangeRect.left) {
high = charIndex - 1
characterIndex = Math.max(0, charIndex - 1)
} else if (targetClientLeft > rangeRect.right) {
low = nextCharIndex
characterIndex = Math.min(containingTextNode.textContent.length, nextCharIndex)
} else {
if (targetClientLeft <= ((rangeRect.left + rangeRect.right) / 2)) {
characterIndex = charIndex
} else {
characterIndex = nextCharIndex
}
break
}
}
}
let textNodeStartColumn = 0
for (let i = 0; i < containingTextNodeIndex; i++) {
textNodeStartColumn = textNodeStartColumn + textNodes[i].length
}
const column = textNodeStartColumn + characterIndex
return Point(row, column)
}
observeModel () {
const {model} = this.props
model.component = this
const scheduleUpdate = this.scheduleUpdate.bind(this)
this.disposables.add(model.displayLayer.onDidChangeSync(scheduleUpdate))
this.disposables.add(model.onDidUpdateDecorations(scheduleUpdate))
this.disposables.add(model.onDidAddGutter(scheduleUpdate))
this.disposables.add(model.onDidRemoveGutter(scheduleUpdate))
this.disposables.add(model.selectionsMarkerLayer.onDidUpdate(this.didUpdateSelections.bind(this)))
this.disposables.add(model.onDidRequestAutoscroll(this.didRequestAutoscroll.bind(this)))
}
isVisible () {
return this.element.offsetWidth > 0 || this.element.offsetHeight > 0
}
getWindowInnerHeight () {
return window.innerHeight
}
getWindowInnerWidth () {
return window.innerWidth
}
getLineHeight () {
return this.measurements.lineHeight
}
getBaseCharacterWidth () {
return this.measurements ? this.measurements.baseCharacterWidth : null
}
getLongestLineWidth () {
return this.measurements.longestLineWidth
}
getClientContainerHeight () {
return this.measurements.clientContainerHeight
}
getClientContainerWidth () {
return this.measurements.clientContainerWidth
}
getScrollContainerWidth () {
if (this.props.model.getAutoWidth()) {
return this.getScrollWidth()
} else {
return this.getClientContainerWidth() - this.getGutterContainerWidth()
}
}
getScrollContainerHeight () {
if (this.props.model.getAutoHeight()) {
return this.getScrollHeight()
} else {
return this.getClientContainerHeight()
}
}
getScrollContainerHeightInLines () {
return Math.ceil(this.getScrollContainerHeight() / this.getLineHeight())
}
getScrollContainerClientWidth () {
if (this.isVerticalScrollbarVisible()) {
return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()
} else {
return this.getScrollContainerWidth()
}
}
getScrollContainerClientHeight () {
if (this.isHorizontalScrollbarVisible()) {
return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()
} else {
return this.getScrollContainerHeight()
}
}
isVerticalScrollbarVisible () {
return (
this.getContentHeight() > this.getScrollContainerHeight() ||
(
this.getContentWidth() > this.getScrollContainerWidth() &&
this.getContentHeight() > (this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight())
)
)
}
isHorizontalScrollbarVisible () {
return (
!this.props.model.isSoftWrapped() &&
(
this.getContentWidth() > this.getScrollContainerWidth() ||
(
this.getContentHeight() > this.getScrollContainerHeight() &&
this.getContentWidth() > (this.getScrollContainerWidth() - this.getVerticalScrollbarWidth())
)
)
)
}
getScrollHeight () {
if (this.props.model.getScrollPastEnd()) {
return this.getContentHeight() + Math.max(
3 * this.getLineHeight(),
this.getScrollContainerClientHeight() - (3 * this.getLineHeight())
)
} else {
return this.getContentHeight()
}
}
getScrollWidth () {
const {model} = this.props
if (model.isSoftWrapped()) {
return this.getScrollContainerClientWidth()
} else if (model.getAutoWidth()) {
return this.getContentWidth()
} else {
return Math.max(this.getContentWidth(), this.getScrollContainerClientWidth())
}
}
getContentHeight () {
return this.props.model.getApproximateScreenLineCount() * this.getLineHeight()
}
getContentWidth () {
return Math.round(this.getLongestLineWidth() + this.getBaseCharacterWidth())
}
getGutterContainerWidth () {
return this.measurements.gutterContainerWidth
}
getLineNumberGutterWidth () {
return this.measurements.lineNumberGutterWidth
}
getVerticalScrollbarWidth () {
return this.measurements.verticalScrollbarWidth
}
getHorizontalScrollbarHeight () {
return this.measurements.horizontalScrollbarHeight
}
getRowsPerTile () {
return this.props.rowsPerTile || DEFAULT_ROWS_PER_TILE
}
tileStartRowForRow (row) {
return row - (row % this.getRowsPerTile())
}
tileIndexForTileStartRow (startRow) {
return (startRow / this.getRowsPerTile()) % this.getRenderedTileCount()
}
getFirstTileStartRow () {
return this.tileStartRowForRow(this.getFirstVisibleRow())
}
getRenderedStartRow () {
return this.getFirstTileStartRow()
}
getRenderedEndRow () {
return Math.min(
this.props.model.getApproximateScreenLineCount(),
this.getFirstTileStartRow() + this.getVisibleTileCount() * this.getRowsPerTile()
)
}
getRenderedRowCount () {
return Math.max(0, this.getRenderedEndRow() - this.getRenderedStartRow())
}
getRenderedTileCount () {
return Math.ceil(this.getRenderedRowCount() / this.getRowsPerTile())
}
getFirstVisibleRow () {
return Math.floor(this.getScrollTop() / this.getLineHeight())
}
getLastVisibleRow () {
return Math.min(
this.props.model.getApproximateScreenLineCount() - 1,
this.getFirstVisibleRow() + this.getScrollContainerHeightInLines()
)
}
getVisibleTileCount () {
return Math.floor((this.getLastVisibleRow() - this.getFirstVisibleRow()) / this.getRowsPerTile()) + 2
}
getScrollTop () {
this.scrollTop = Math.min(this.getMaxScrollTop(), this.scrollTop)
return this.scrollTop
}
setScrollTop (scrollTop) {
scrollTop = Math.round(Math.max(0, Math.min(this.getMaxScrollTop(), scrollTop)))
if (scrollTop !== this.scrollTop) {
this.scrollTopPending = true
this.scrollTop = scrollTop
return true
} else {
return false
}
}
getMaxScrollTop () {
return Math.max(0, this.getScrollHeight() - this.getScrollContainerClientHeight())
}
getScrollBottom () {
return this.getScrollTop() + this.getScrollContainerClientHeight()
}
setScrollBottom (scrollBottom) {
return this.setScrollTop(scrollBottom - this.getScrollContainerClientHeight())
}
getScrollLeft () {
// this.scrollLeft = Math.min(this.getMaxScrollLeft(), this.scrollLeft)
return this.scrollLeft
}
setScrollLeft (scrollLeft) {
scrollLeft = Math.round(Math.max(0, Math.min(this.getMaxScrollLeft(), scrollLeft)))
if (scrollLeft !== this.scrollLeft) {
this.scrollLeftPending = true
this.scrollLeft = scrollLeft
return true
} else {
return false
}
}
getMaxScrollLeft () {
return Math.max(0, this.getScrollWidth() - this.getScrollContainerClientWidth())
}
getScrollRight () {
return this.getScrollLeft() + this.getScrollContainerClientWidth()
}
setScrollRight (scrollRight) {
return this.setScrollLeft(scrollRight - this.getScrollContainerClientWidth())
}
// Ensure the spatial index is populated with rows that are currently
// visible so we *at least* get the longest row in the visible range.
populateVisibleRowRange () {
const endRow = this.getFirstTileStartRow() + this.getVisibleTileCount() * this.getRowsPerTile()
this.props.model.displayLayer.populateSpatialIndexIfNeeded(Infinity, endRow)
}
topPixelPositionForRow (row) {
return row * this.getLineHeight()
}
getNextUpdatePromise () {
if (!this.nextUpdatePromise) {
this.nextUpdatePromise = new Promise((resolve) => {
this.resolveNextUpdatePromise = () => {
this.nextUpdatePromise = null
this.resolveNextUpdatePromise = null
resolve()
}
})
}
return this.nextUpdatePromise
}
}
class DummyScrollbarComponent {
constructor (props) {
this.props = props
etch.initialize(this)
if (this.props.orientation === 'horizontal') {
this.element.scrollLeft = this.props.scrollLeft
} else {
this.element.scrollTop = this.props.scrollTop
}
}
update (newProps) {
const oldProps = this.props
this.props = newProps
etch.updateSync(this)
if (this.props.orientation === 'horizontal') {
if (newProps.scrollLeft !== oldProps.scrollLeft) {
this.element.scrollLeft = this.props.scrollLeft
}
} else {
if (newProps.scrollTop !== oldProps.scrollTop) {
this.element.scrollTop = this.props.scrollTop
}
}
}
// Scroll position must be updated after the inner element is updated to
// ensure the element has an adequate scrollHeight/scrollWidth
updateScrollPosition () {
if (this.props.orientation === 'horizontal') {
this.element.scrollLeft = this.props.scrollLeft
} else {
this.element.scrollTop = this.props.scrollTop
}
}
render () {
const outerStyle = {
position: 'absolute',
contain: 'strict',
zIndex: 1
}
const innerStyle = {}
if (this.props.orientation === 'horizontal') {
let right = (this.props.verticalScrollbarWidth || 0)
outerStyle.bottom = 0
outerStyle.left = 0
outerStyle.right = right + 'px'
outerStyle.height = '20px'
outerStyle.overflowY = 'hidden'
outerStyle.overflowX = this.props.forceScrollbarVisible ? 'scroll' : 'auto'
innerStyle.height = '20px'
innerStyle.width = (this.props.scrollWidth || 0) + 'px'
} else {
let bottom = (this.props.horizontalScrollbarHeight || 0)
outerStyle.right = 0
outerStyle.top = 0
outerStyle.bottom = bottom + 'px'
outerStyle.width = '20px'
outerStyle.overflowX = 'hidden'
outerStyle.overflowY = this.props.forceScrollbarVisible ? 'scroll' : 'auto'
innerStyle.width = '20px'
innerStyle.height = (this.props.scrollHeight || 0) + 'px'
}
return $.div(
{
style: outerStyle,
on: {
scroll: this.props.didScroll,
mousedown: this.didMousedown
}
},
$.div({style: innerStyle})
)
}
didMousedown (event) {
let {bottom, right} = this.element.getBoundingClientRect()
const clickedOnScrollbar = (this.props.orientation === 'horizontal')
? event.clientY >= (bottom - this.getRealScrollbarHeight())
: event.clientX >= (right - this.getRealScrollbarWidth())
if (!clickedOnScrollbar) this.props.didMousedown(event)
}
getRealScrollbarWidth () {
return this.element.offsetWidth - this.element.clientWidth
}
getRealScrollbarHeight () {
return this.element.offsetHeight - this.element.clientHeight
}
}
class LineNumberGutterComponent {
constructor (props) {
this.props = props
this.element = this.props.element
this.virtualNode = $.div(null)
this.virtualNode.domNode = this.element
etch.updateSync(this)
}
update (newProps) {
if (this.shouldUpdate(newProps)) {
this.props = newProps
etch.updateSync(this)
}
}
render () {
const {
parentComponent, height, width, lineHeight, startRow, endRow, rowsPerTile,
maxDigits, keys, numbers, foldableFlags, decorations
} = this.props
let children = null
if (numbers) {
const renderedTileCount = parentComponent.getRenderedTileCount()
children = new Array(renderedTileCount)
const tileHeight = rowsPerTile * lineHeight + 'px'
const tileWidth = width + 'px'
let softWrapCount = 0
for (let tileStartRow = startRow; tileStartRow < endRow; tileStartRow = tileStartRow + rowsPerTile) {
const tileEndRow = Math.min(endRow, tileStartRow + rowsPerTile)
const tileChildren = new Array(tileEndRow - tileStartRow)
for (let row = tileStartRow; row < tileEndRow; row++) {
const i = row - startRow
const key = keys[i]
const foldable = foldableFlags[i]
let number = numbers[i]
let className = 'line-number'
if (foldable) className = className + ' foldable'
const decorationsForRow = decorations[row - startRow]
if (decorationsForRow) className = className + ' ' + decorationsForRow
if (number === -1) number = '•'
number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number
tileChildren[row - tileStartRow] = $.div({key, className},
number,
$.div({className: 'icon-right'})
)
}
const tileIndex = parentComponent.tileIndexForTileStartRow(tileStartRow)
const top = tileStartRow * lineHeight
children[tileIndex] = $.div({
key: tileIndex,
style: {
contain: 'strict',
overflow: 'hidden',
position: 'absolute',
top: 0,
height: tileHeight,
width: tileWidth,
willChange: 'transform',
transform: `translateY(${top}px)`,
backgroundColor: 'inherit'
}
}, ...tileChildren)
}
}
return $.div(
{
className: 'gutter line-numbers',
attributes: {'gutter-name': 'line-number'},
style: {position: 'relative', height: height + 'px'},
on: {
mousedown: this.didMouseDown
},
},
$.div({key: 'placeholder', className: 'line-number', style: {visibility: 'hidden'}},
'0'.repeat(maxDigits),
$.div({className: 'icon-right'})
),
children
)
}
shouldUpdate (newProps) {
const oldProps = this.props
if (oldProps.height !== newProps.height) return true
if (oldProps.width !== newProps.width) return true
if (oldProps.lineHeight !== newProps.lineHeight) return true
if (oldProps.startRow !== newProps.startRow) return true
if (oldProps.endRow !== newProps.endRow) return true
if (oldProps.rowsPerTile !== newProps.rowsPerTile) return true
if (oldProps.maxDigits !== newProps.maxDigits) return true
if (!arraysEqual(oldProps.keys, newProps.keys)) return true
if (!arraysEqual(oldProps.numbers, newProps.numbers)) return true
if (!arraysEqual(oldProps.foldableFlags, newProps.foldableFlags)) return true
if (!arraysEqual(oldProps.decorations, newProps.decorations)) return true
return false
}
didMouseDown (event) {
this.props.parentComponent.didMouseDownOnLineNumberGutter(event)
}
}
class CustomGutterComponent {
constructor (props) {
this.props = props
this.element = this.props.element
this.virtualNode = $.div(null)
this.virtualNode.domNode = this.element
etch.updateSync(this)
}
update (props) {
this.props = props
etch.updateSync(this)
}
destroy () {
etch.destroy(this)
}
render () {
return $.div(
{
className: 'gutter',
attributes: {'gutter-name': this.props.name},
style: {
display: this.props.visible ? '' : 'none'
}
},
$.div(
{
className: 'custom-decorations',
style: {height: this.props.height + 'px'}
},
this.renderDecorations()
)
)
}
renderDecorations () {
if (!this.props.decorations) return null
return this.props.decorations.map(({className, element, top, height}) => {
return $(CustomGutterDecorationComponent, {
className,
element,
top,
height
})
})
}
}
class CustomGutterDecorationComponent {
constructor (props) {
this.props = props
this.element = document.createElement('div')
const {top, height, className, element} = this.props
this.element.style.position = 'absolute'
this.element.style.top = top + 'px'
this.element.style.height = height + 'px'
if (className != null) this.element.className = className
if (element != null) this.element.appendChild(element)
}
update (newProps) {
const oldProps = this.props
this.props = newProps
if (newProps.top != oldProps.top) this.element.style.top = newProps.top + 'px'
if (newProps.height != oldProps.height) this.element.style.height = newProps.height + 'px'
if (newProps.className != oldProps.className) this.element.className = newProps.className || ''
if (newProps.element != oldProps.element) {
if (this.element.firstChild) this.element.firstChild.remove()
this.element.appendChild(newProps.element)
}
}
}
class LinesTileComponent {
constructor (props) {
this.props = props
this.linesVnode = null
this.highlightsVnode = null
etch.initialize(this)
}
update (newProps) {
if (this.shouldUpdate(newProps)) {
if (newProps.width !== this.props.width) {
this.linesVnode = null
}
this.props = newProps
etch.updateSync(this)
}
}
render () {
const {height, width, top} = this.props
return $.div(
{
style: {
contain: 'strict',
position: 'absolute',
height: height + 'px',
width: width + 'px',
willChange: 'transform',
transform: `translateY(${top}px)`,
backgroundColor: 'inherit'
}
},
this.renderHighlights(),
this.renderLines()
)
}
renderHighlights () {
const {measuredContent, top, height, width, lineHeight, highlightDecorations} = this.props
if (measuredContent) {
let children = null
if (highlightDecorations) {
const decorationCount = highlightDecorations.length
children = new Array(decorationCount)
for (let i = 0; i < decorationCount; i++) {
const highlightProps = Object.assign(
{parentTileTop: top, lineHeight},
highlightDecorations[i]
)
children[i] = $(HighlightComponent, highlightProps)
highlightDecorations[i].flashRequested = false
}
}
this.highlightsVnode = $.div(
{
style: {
position: 'absolute',
contain: 'strict',
height: height + 'px',
width: width + 'px'
},
}, children
)
}
return this.highlightsVnode
}
renderLines () {
const {
measuredContent, height, width, top,
renderedStartRow, tileStartRow, tileEndRow,
screenLines, lineDecorations, displayLayer,
lineNodesByScreenLineId, textNodesByScreenLineId,
} = this.props
if (!measuredContent || !this.linesVnode) {
const children = new Array(tileEndRow - tileStartRow)
for (let row = tileStartRow; row < tileEndRow; row++) {
const screenLine = screenLines[row - renderedStartRow]
if (!screenLine) {
children.length = i
break
}
children[row - tileStartRow] = $(LineComponent, {
key: screenLine.id,
screenLine,
lineDecoration: lineDecorations[row - renderedStartRow],
displayLayer,
lineNodesByScreenLineId,
textNodesByScreenLineId
})
}
this.linesVnode = $.div({
style: {
position: 'absolute',
contain: 'strict',
height: height + 'px',
width: width + 'px'
}
}, children)
}
return this.linesVnode
}
shouldUpdate (newProps) {
const oldProps = this.props
if (oldProps.top !== newProps.top) return true
if (oldProps.height !== newProps.height) return true
if (oldProps.width !== newProps.width) return true
if (!arraysEqual(oldProps.screenLines, newProps.screenLines)) return true
if (!arraysEqual(oldProps.lineDecorations, newProps.lineDecorations)) return true
if (!oldProps.highlightDecorations && newProps.highlightDecorations) return true
if (oldProps.highlightDecorations && !newProps.highlightDecorations) return true
if (oldProps.highlightDecorations && newProps.highlightDecorations) {
if (oldProps.highlightDecorations.length !== newProps.highlightDecorations.length) return true
for (let i = 0, length = oldProps.highlightDecorations.length; i < length; i++) {
const oldHighlight = oldProps.highlightDecorations[i]
const newHighlight = newProps.highlightDecorations[i]
if (oldHighlight.className !== newHighlight.className) return true
if (newHighlight.flashRequested) return true
if (oldHighlight.startPixelLeft !== newHighlight.startPixelLeft) return true
if (oldHighlight.endPixelLeft !== newHighlight.endPixelLeft) return true
if (!oldHighlight.screenRange.isEqual(newHighlight.screenRange)) return true
}
}
return false
}
}
class LineComponent {
constructor (props) {
const {displayLayer, screenLine, lineDecoration, lineNodesByScreenLineId, textNodesByScreenLineId} = props
this.props = props
this.element = document.createElement('div')
this.element.className = this.buildClassName()
lineNodesByScreenLineId.set(screenLine.id, this.element)
const textNodes = []
textNodesByScreenLineId.set(screenLine.id, textNodes)
const {lineText, tagCodes} = screenLine
let startIndex = 0
let openScopeNode = document.createElement('span')
this.element.appendChild(openScopeNode)
for (let i = 0; i < tagCodes.length; i++) {
const tagCode = tagCodes[i]
if (tagCode !== 0) {
if (displayLayer.isCloseTagCode(tagCode)) {
openScopeNode = openScopeNode.parentElement
} else if (displayLayer.isOpenTagCode(tagCode)) {
const scope = displayLayer.tagForCode(tagCode)
const newScopeNode = document.createElement('span')
newScopeNode.className = classNameForScopeName(scope)
openScopeNode.appendChild(newScopeNode)
openScopeNode = newScopeNode
} else {
const textNode = document.createTextNode(lineText.substr(startIndex, tagCode))
startIndex = startIndex + tagCode
openScopeNode.appendChild(textNode)
textNodes.push(textNode)
}
}
}
if (startIndex === 0) {
const textNode = document.createTextNode(' ')
this.element.appendChild(textNode)
textNodes.push(textNode)
}
if (lineText.endsWith(displayLayer.foldCharacter)) {
// Insert a zero-width non-breaking whitespace, so that LinesYardstick can
// take the fold-marker::after pseudo-element into account during
// measurements when such marker is the last character on the line.
const textNode = document.createTextNode(ZERO_WIDTH_NBSP_CHARACTER)
this.element.appendChild(textNode)
textNodes.push(textNode)
}
}
update (newProps) {
if (this.props.lineDecoration !== newProps.lineDecoration) {
this.props = newProps
this.element.className = this.buildClassName()
}
}
destroy () {
const {lineNodesByScreenLineId, textNodesByScreenLineId, screenLine} = this.props
if (lineNodesByScreenLineId.get(screenLine.id) === this.element) {
lineNodesByScreenLineId.delete(screenLine.id)
textNodesByScreenLineId.delete(screenLine.id)
}
}
buildClassName () {
const {lineDecoration} = this.props
let className = 'line'
if (lineDecoration != null) className = className + ' ' + lineDecoration
return className
}
}
class HighlightComponent {
constructor (props) {
this.props = props
etch.initialize(this)
if (this.props.flashRequested) this.performFlash()
}
update (newProps) {
this.props = newProps
etch.updateSync(this)
if (newProps.flashRequested) this.performFlash()
}
performFlash () {
const {flashClass, flashDuration} = this.props
if (!this.timeoutsByClassName) this.timeoutsByClassName = new Map()
// If a flash of this class is already in progress, clear it early and
// flash again on the next frame to ensure CSS transitions apply to the
// second flash.
if (this.timeoutsByClassName.has(flashClass)) {
window.clearTimeout(this.timeoutsByClassName.get(flashClass))
this.timeoutsByClassName.delete(flashClass)
this.element.classList.remove(flashClass)
requestAnimationFrame(() => this.performFlash())
} else {
this.element.classList.add(flashClass)
this.timeoutsByClassName.set(flashClass, window.setTimeout(() => {
this.element.classList.remove(flashClass)
}, flashDuration))
}
}
render () {
let {startPixelTop, endPixelTop} = this.props
const {
className, screenRange, parentTileTop, lineHeight,
startPixelLeft, endPixelLeft,
} = this.props
startPixelTop -= parentTileTop
endPixelTop -= parentTileTop
let children
if (screenRange.start.row === screenRange.end.row) {
children = $.div({
className: 'region',
style: {
position: 'absolute',
boxSizing: 'border-box',
top: startPixelTop + 'px',
left: startPixelLeft + 'px',
width: endPixelLeft - startPixelLeft + 'px',
height: lineHeight + 'px'
}
})
} else {
children = []
children.push($.div({
className: 'region',
style: {
position: 'absolute',
boxSizing: 'border-box',
top: startPixelTop + 'px',
left: startPixelLeft + 'px',
right: 0,
height: lineHeight + 'px'
}
}))
if (screenRange.end.row - screenRange.start.row > 1) {
children.push($.div({
className: 'region',
style: {
position: 'absolute',
boxSizing: 'border-box',
top: startPixelTop + lineHeight + 'px',
left: 0,
right: 0,
height: endPixelTop - startPixelTop - (lineHeight * 2) + 'px'
}
}))
}
if (endPixelLeft > 0) {
children.push($.div({
className: 'region',
style: {
position: 'absolute',
boxSizing: 'border-box',
top: endPixelTop - lineHeight + 'px',
left: 0,
width: endPixelLeft + 'px',
height: lineHeight + 'px'
}
}))
}
}
return $.div({className: 'highlight ' + className}, children)
}
}
class OverlayComponent {
constructor (props) {
this.props = props
this.element = document.createElement('atom-overlay')
if (this.props.className != null) this.element.classList.add(this.props.className)
this.element.appendChild(this.props.element)
this.element.style.position = 'fixed'
this.element.style.zIndex = 4
this.element.style.top = (this.props.pixelTop || 0) + 'px'
this.element.style.left = (this.props.pixelLeft || 0) + 'px'
getElementResizeDetector().listenTo(this.element, this.props.didResize)
}
update (newProps) {
const oldProps = this.props
this.props = newProps
if (this.props.pixelTop != null) this.element.style.top = this.props.pixelTop + 'px'
if (this.props.pixelLeft != null) this.element.style.left = this.props.pixelLeft + 'px'
if (newProps.className !== oldProps.className) {
if (oldProps.className != null) this.element.classList.remove(oldProps.className)
if (newProps.className != null) this.element.classList.add(newProps.className)
}
}
}
class ComponentWrapper {
constructor (props) {
this.component = props.component
this.element = this.component.element
this.component.update(props)
}
update (props) {
this.component.update(props)
}
destroy () {
this.component.destroy()
}
}
const classNamesByScopeName = new Map()
function classNameForScopeName (scopeName) {
let classString = classNamesByScopeName.get(scopeName)
if (classString == null) {
classString = scopeName.replace(/\.+/g, ' ')
classNamesByScopeName.set(scopeName, classString)
}
return classString
}
let rangeForMeasurement
function clientRectForRange (textNode, startIndex, endIndex) {
if (!rangeForMeasurement) rangeForMeasurement = document.createRange()
rangeForMeasurement.setStart(textNode, startIndex)
rangeForMeasurement.setEnd(textNode, endIndex)
return rangeForMeasurement.getBoundingClientRect()
}
let resizeDetector
function getElementResizeDetector () {
if (resizeDetector == null) resizeDetector = ResizeDetector({strategy: 'scroll'})
return resizeDetector
}
function arraysEqual(a, b) {
if (a.length !== b.length) return false
for (let i = 0, length = a.length; i < length; i++) {
if (a[i] !== b[i]) return false
}
return true
}
function constrainRangeToRows (range, startRow, endRow) {
if (range.start.row < startRow || range.end.row >= endRow) {
range = range.copy()
if (range.start.row < startRow) {
range.start.row = startRow
range.start.column = 0
}
if (range.end.row >= endRow) {
range.end.row = endRow
range.end.column = 0
}
}
return range
}