Files
atom/src/text-editor-component.js
Nathan Sobo f237d70357 WIP
2017-05-05 09:29:27 +02:00

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
}