mirror of
https://github.com/atom/atom.git
synced 2026-02-06 12:44:59 -05:00
322 lines
10 KiB
JavaScript
322 lines
10 KiB
JavaScript
const etch = require('etch')
|
|
const $ = etch.dom
|
|
const TextEditorElement = require('./text-editor-element')
|
|
|
|
const ROWS_PER_TILE = 6
|
|
const NORMAL_WIDTH_CHARACTER = 'x'
|
|
const DOUBLE_WIDTH_CHARACTER = '我'
|
|
const HALF_WIDTH_CHARACTER = 'ハ'
|
|
const KOREAN_CHARACTER = '세'
|
|
|
|
const characterMeasurementSpans = {}
|
|
const characterMeasurementLineNode = etch.render($.div({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)
|
|
), {refs: characterMeasurementSpans})
|
|
|
|
module.exports =
|
|
class TextEditorComponent {
|
|
constructor (props) {
|
|
this.props = props
|
|
this.element = props.element || new TextEditorElement()
|
|
this.element.initialize(this)
|
|
this.virtualNode = $('atom-text-editor')
|
|
this.virtualNode.domNode = this.element
|
|
this.refs = {}
|
|
etch.updateSync(this)
|
|
}
|
|
|
|
update (props) {
|
|
}
|
|
|
|
updateSync () {
|
|
etch.updateSync(this)
|
|
}
|
|
|
|
render () {
|
|
return $('atom-text-editor', null,
|
|
$.div({ref: 'scroller', onScroll: this.didScroll, className: 'scroll-view'},
|
|
this.renderGutterContainer(),
|
|
this.renderLines()
|
|
)
|
|
)
|
|
}
|
|
|
|
renderGutterContainer () {
|
|
return $.div({className: 'gutter-container'},
|
|
this.measurements ? this.renderLineNumberGutter() : []
|
|
)
|
|
}
|
|
|
|
renderLineNumberGutter () {
|
|
const maxLineNumberDigits = Math.max(2, this.getModel().getLineCount().toString().length)
|
|
|
|
const firstTileStartRow = this.getTileStartRow(this.getFirstVisibleRow())
|
|
const lastTileStartRow = this.getTileStartRow(this.getLastVisibleRow())
|
|
|
|
console.log({firstTileStartRow, lastTileStartRow});
|
|
|
|
let tileNodes = []
|
|
|
|
let currentTileStaticTop = 0
|
|
let previousBufferRow = (firstTileStartRow > 0) ? this.getModel().bufferRowForScreenRow(firstTileStartRow - 1) : -1
|
|
for (let tileStartRow = firstTileStartRow; tileStartRow <= lastTileStartRow; tileStartRow += ROWS_PER_TILE) {
|
|
const currentTileEndRow = tileStartRow + ROWS_PER_TILE
|
|
const lineNumberNodes = []
|
|
|
|
for (let row = tileStartRow; row < currentTileEndRow; row++) {
|
|
const bufferRow = this.getModel().bufferRowForScreenRow(row)
|
|
const foldable = this.getModel().isFoldableAtBufferRow(bufferRow)
|
|
const softWrapped = (bufferRow === previousBufferRow)
|
|
|
|
let className = 'line-number'
|
|
let lineNumber
|
|
if (softWrapped) {
|
|
lineNumber = '•'
|
|
} else {
|
|
if (foldable) className += ' foldable'
|
|
lineNumber = (bufferRow + 1).toString()
|
|
}
|
|
lineNumber = '\u00a0'.repeat(maxLineNumberDigits - lineNumber.length) + lineNumber
|
|
|
|
lineNumberNodes.push($.div({className},
|
|
lineNumber,
|
|
$.div({className: 'icon-right'})
|
|
))
|
|
|
|
previousBufferRow = bufferRow
|
|
}
|
|
|
|
const tileHeight = ROWS_PER_TILE * this.measurements.lineHeight
|
|
const yTranslation = this.topPixelPositionForRow(tileStartRow) - currentTileStaticTop
|
|
|
|
tileNodes.push($.div({
|
|
style: {
|
|
height: tileHeight + 'px',
|
|
width: 'min-content',
|
|
transform: `translateY(${yTranslation}px)`,
|
|
backgroundColor: 'inherit',
|
|
}
|
|
}, lineNumberNodes))
|
|
|
|
currentTileStaticTop += tileHeight
|
|
}
|
|
|
|
return $.div({className: 'gutter line-numbers', 'gutter-name': 'line-number'}, tileNodes)
|
|
}
|
|
|
|
renderLines () {
|
|
const style = (this.measurements)
|
|
? {
|
|
width: this.measurements.scrollWidth + 'px',
|
|
height: this.getScrollHeight() + 'px'
|
|
} : null
|
|
|
|
return $.div({ref: 'lines', className: 'lines', style}, this.renderLineTiles())
|
|
}
|
|
|
|
renderLineTiles () {
|
|
if (!this.measurements) return []
|
|
|
|
const firstTileStartRow = this.getTileStartRow(this.getFirstVisibleRow())
|
|
const lastTileStartRow = this.getTileStartRow(this.getLastVisibleRow())
|
|
const visibleTileCount = lastTileStartRow - firstTileStartRow + 1
|
|
const displayLayer = this.getModel().displayLayer
|
|
const screenLines = displayLayer.getScreenLines(firstTileStartRow, lastTileStartRow + ROWS_PER_TILE)
|
|
|
|
console.log({
|
|
firstVisible: this.getFirstVisibleRow(),
|
|
lastVisible: this.getLastVisibleRow(),
|
|
firstTileStartRow, lastTileStartRow
|
|
});
|
|
|
|
let tileNodes = []
|
|
for (let tileStartRow = firstTileStartRow; tileStartRow <= lastTileStartRow; tileStartRow += ROWS_PER_TILE) {
|
|
const tileEndRow = tileStartRow + ROWS_PER_TILE
|
|
const lineNodes = []
|
|
for (let row = tileStartRow; row < tileEndRow; row++) {
|
|
const screenLine = screenLines[row - firstTileStartRow]
|
|
if (!screenLine) break
|
|
lineNodes.push($(LineComponent, {key: screenLine.id, displayLayer, screenLine}))
|
|
}
|
|
|
|
const tileHeight = ROWS_PER_TILE * this.measurements.lineHeight
|
|
|
|
tileNodes.push($.div({
|
|
key: (tileStartRow / ROWS_PER_TILE) % visibleTileCount,
|
|
style: {
|
|
position: 'absolute',
|
|
height: tileHeight + 'px',
|
|
width: this.measurements.scrollWidth + 'px',
|
|
transform: `translateY(${this.topPixelPositionForRow(tileStartRow)}px)`,
|
|
backgroundColor: 'inherit'
|
|
}
|
|
}, lineNodes))
|
|
}
|
|
|
|
return tileNodes
|
|
}
|
|
|
|
didAttach () {
|
|
this.intersectionObserver = new IntersectionObserver((entries) => {
|
|
const {intersectionRect} = entries[entries.length - 1]
|
|
if (intersectionRect.width > 0 || intersectionRect.height > 0) {
|
|
this.didShow()
|
|
}
|
|
})
|
|
this.intersectionObserver.observe(this.element)
|
|
if (this.isVisible()) this.didShow()
|
|
}
|
|
|
|
didShow () {
|
|
if (!this.measurements) this.performInitialMeasurements()
|
|
etch.updateSync(this)
|
|
}
|
|
|
|
didScroll () {
|
|
this.measureScrollPosition()
|
|
this.updateSync()
|
|
}
|
|
|
|
performInitialMeasurements () {
|
|
this.measurements = {}
|
|
this.measureEditorDimensions()
|
|
this.measureScrollPosition()
|
|
this.measureCharacterDimensions()
|
|
this.measureLongestLineWidth()
|
|
}
|
|
|
|
measureEditorDimensions () {
|
|
this.measurements.scrollerHeight = this.refs.scroller.offsetHeight
|
|
}
|
|
|
|
measureScrollPosition () {
|
|
this.measurements.scrollTop = this.refs.scroller.scrollTop
|
|
this.measurements.scrollLeft = this.refs.scroller.scrollLeft
|
|
}
|
|
|
|
measureCharacterDimensions () {
|
|
this.refs.lines.appendChild(characterMeasurementLineNode)
|
|
this.measurements.lineHeight = characterMeasurementLineNode.getBoundingClientRect().height
|
|
this.measurements.baseCharacterWidth = characterMeasurementSpans.normalWidthCharacterSpan.getBoundingClientRect().width
|
|
this.measurements.doubleWidthCharacterWidth = characterMeasurementSpans.doubleWidthCharacterSpan.getBoundingClientRect().width
|
|
this.measurements.halfWidthCharacterWidth = characterMeasurementSpans.halfWidthCharacterSpan.getBoundingClientRect().width
|
|
this.measurements.koreanCharacterWidth = characterMeasurementSpans.koreanCharacterSpan.getBoundingClientRect().widt
|
|
this.refs.lines.removeChild(characterMeasurementLineNode)
|
|
}
|
|
|
|
measureLongestLineWidth () {
|
|
const displayLayer = this.getModel().displayLayer
|
|
const rightmostPosition = displayLayer.getApproximateRightmostScreenPosition()
|
|
this.measurements.scrollWidth = rightmostPosition.column * this.measurements.baseCharacterWidth
|
|
}
|
|
|
|
getModel () {
|
|
if (!this.props.model) {
|
|
const TextEditor = require('./text-editor')
|
|
this.props.model = new TextEditor()
|
|
}
|
|
return this.props.model
|
|
}
|
|
|
|
isVisible () {
|
|
return this.element.offsetWidth > 0 || this.element.offsetHeight > 0
|
|
}
|
|
|
|
getBaseCharacterWidth () {
|
|
return this.measurements ? this.measurements.baseCharacterWidth : null
|
|
}
|
|
|
|
getScrollTop () {
|
|
return this.measurements ? this.measurements.scrollTop : null
|
|
}
|
|
|
|
getScrollLeft () {
|
|
return this.measurements ? this.measurements.scrollLeft : null
|
|
}
|
|
|
|
getTileStartRow (row) {
|
|
return row - (row % ROWS_PER_TILE)
|
|
}
|
|
|
|
getFirstVisibleRow () {
|
|
const {scrollTop, lineHeight} = this.measurements
|
|
return Math.floor(scrollTop / lineHeight)
|
|
}
|
|
|
|
getLastVisibleRow () {
|
|
const {scrollTop, scrollerHeight, lineHeight} = this.measurements
|
|
return this.getFirstVisibleRow() + Math.ceil(scrollerHeight / lineHeight)
|
|
}
|
|
|
|
topPixelPositionForRow (row) {
|
|
return row * this.measurements.lineHeight
|
|
}
|
|
|
|
getScrollHeight () {
|
|
return this.getModel().getApproximateScreenLineCount() * this.measurements.lineHeight
|
|
}
|
|
}
|
|
|
|
class LineComponent {
|
|
constructor ({displayLayer, screenLine}) {
|
|
const {lineText, tagCodes} = screenLine
|
|
this.element = document.createElement('div')
|
|
this.element.classList.add('line')
|
|
|
|
const textNodes = []
|
|
let startIndex = 0
|
|
let openScopeNode = this.element
|
|
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 += 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)
|
|
this.element.appendChild(textNode)
|
|
textNodes.push(textNode)
|
|
}
|
|
|
|
// this.textNodesByLineId[id] = textNodes
|
|
}
|
|
|
|
update () {}
|
|
}
|
|
|
|
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
|
|
}
|