Skip to content
This repository has been archived by the owner on Mar 3, 2023. It is now read-only.

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
okbel authored and Antonio Scandurra committed May 5, 2017
1 parent 1f068bd commit f237d70
Show file tree
Hide file tree
Showing 8 changed files with 501 additions and 73 deletions.
File renamed without changes.
321 changes: 321 additions & 0 deletions src/text-editor-component.js
@@ -0,0 +1,321 @@
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
}
File renamed without changes.
57 changes: 57 additions & 0 deletions src/text-editor-element.js
@@ -0,0 +1,57 @@
const {Emitter} = require('atom')
const TextEditorComponent = require('./text-editor-component')

class TextEditorElement extends HTMLElement {
initialize (component) {
this.component = component
this.emitter = new Emitter()
return this
}

attachedCallback () {
this.getComponent().didAttach()
this.emitter.emit('did-attach')
}

getModel () {
return this.getComponent().getModel()
}

setModel (model) {
this.getComponent().setModel(model)
}

onDidAttach (callback) {
return this.emitter.on('did-attach', callback)
}

onDidChangeScrollLeft (callback) {
return this.emitter.on('did-change-scroll-left', callback)
}

onDidChangeScrollTop (callback) {
return this.emitter.on('did-change-scrol-top', callback)
}

getDefaultCharacterWidth () {
return this.getComponent().getBaseCharacterWidth()
}

getScrollTop () {
return this.getComponent().getScrollTop()
}

getScrollLeft () {
return this.getComponent().getScrollLeft()
}

getComponent () {
if (!this.component) this.component = new TextEditorComponent({element: this})
return this.component
}
}

module.exports =
document.registerElement('atom-text-editor', {
prototype: TextEditorElement.prototype
})
5 changes: 3 additions & 2 deletions src/text-editor.coffee
Expand Up @@ -12,7 +12,7 @@ Model = require './model'
Selection = require './selection'
TextMateScopeSelector = require('first-mate').ScopeSelector
GutterContainer = require './gutter-container'
TextEditorElement = require './text-editor-element'
TextEditorComponent = require './text-editor-component'
{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils'

ZERO_WIDTH_NBSP = '\ufeff'
Expand Down Expand Up @@ -3543,7 +3543,8 @@ class TextEditor extends Model

# Get the Element for the editor.
getElement: ->
@editorElement ?= new TextEditorElement().initialize(this, atom)
@component ?= new TextEditorComponent({model: this})
@component.element

# Essential: Retrieves the greyed out placeholder of a mini editor.
#
Expand Down

0 comments on commit f237d70

Please sign in to comment.