mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-11-10 10:17:11 +03:00
Implement vertical autoscroll; still need tests
This commit is contained in:
parent
4c51ae77dd
commit
ff2f9b192a
@ -122,13 +122,13 @@ describe('TextEditorComponent', () => {
|
||||
expect(component.getRenderedStartRow()).toBe(4)
|
||||
expect(component.getRenderedEndRow()).toBe(10)
|
||||
|
||||
editor.setCursorScreenPosition([0, 0]) // out of view
|
||||
editor.addCursorAtScreenPosition([2, 2]) // out of view
|
||||
editor.addCursorAtScreenPosition([4, 0]) // line start
|
||||
editor.addCursorAtScreenPosition([4, 4]) // at token boundary
|
||||
editor.addCursorAtScreenPosition([4, 6]) // within token
|
||||
editor.addCursorAtScreenPosition([5, Infinity]) // line end
|
||||
editor.addCursorAtScreenPosition([10, 2]) // out of view
|
||||
editor.setCursorScreenPosition([0, 0], {autoscroll: false}) // out of view
|
||||
editor.addCursorAtScreenPosition([2, 2], {autoscroll: false}) // out of view
|
||||
editor.addCursorAtScreenPosition([4, 0], {autoscroll: false}) // line start
|
||||
editor.addCursorAtScreenPosition([4, 4], {autoscroll: false}) // at token boundary
|
||||
editor.addCursorAtScreenPosition([4, 6], {autoscroll: false}) // within token
|
||||
editor.addCursorAtScreenPosition([5, Infinity], {autoscroll: false}) // line end
|
||||
editor.addCursorAtScreenPosition([10, 2], {autoscroll: false}) // out of view
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
let cursorNodes = Array.from(element.querySelectorAll('.cursor'))
|
||||
@ -138,14 +138,14 @@ describe('TextEditorComponent', () => {
|
||||
verifyCursorPosition(component, cursorNodes[2], 4, 6)
|
||||
verifyCursorPosition(component, cursorNodes[3], 5, 30)
|
||||
|
||||
editor.setCursorScreenPosition([8, 11])
|
||||
editor.setCursorScreenPosition([8, 11], {autoscroll: false})
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
cursorNodes = Array.from(element.querySelectorAll('.cursor'))
|
||||
expect(cursorNodes.length).toBe(1)
|
||||
verifyCursorPosition(component, cursorNodes[0], 8, 11)
|
||||
|
||||
editor.setCursorScreenPosition([0, 0])
|
||||
editor.setCursorScreenPosition([0, 0], {autoscroll: false})
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
cursorNodes = Array.from(element.querySelectorAll('.cursor'))
|
||||
@ -164,8 +164,6 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
// When out of view, the hidden input is positioned at 0, 0
|
||||
expect(editor.getCursorScreenPosition()).toEqual([0, 0])
|
||||
console.log(hiddenInput.offsetParent);
|
||||
console.log(hiddenInput.offsetTop);
|
||||
expect(hiddenInput.offsetTop).toBe(0)
|
||||
expect(hiddenInput.offsetLeft).toBe(0)
|
||||
|
||||
@ -176,7 +174,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(Math.round(hiddenInput.getBoundingClientRect().left)).toBe(clientLeftForCharacter(component, 7, 4))
|
||||
})
|
||||
|
||||
it('focuses the hidden input elemnent and adds the is-focused class when focused', async () => {
|
||||
it('focuses the hidden input element and adds the is-focused class when focused', async () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
const {hiddenInput} = component.refs
|
||||
|
||||
@ -196,6 +194,55 @@ describe('TextEditorComponent', () => {
|
||||
await component.getNextUpdatePromise()
|
||||
expect(element.classList.contains('is-focused')).toBe(false)
|
||||
})
|
||||
|
||||
describe('autoscroll', () => {
|
||||
it('automatically scrolls vertically when the cursor is within vertical scroll margin of the top or bottom', async () => {
|
||||
const {component, element, editor} = buildComponent({height: 120})
|
||||
const {scroller} = component.refs
|
||||
expect(component.getLastVisibleRow()).toBe(8)
|
||||
|
||||
editor.setCursorScreenPosition([6, 0])
|
||||
await component.getNextUpdatePromise()
|
||||
let scrollBottom = scroller.scrollTop + scroller.clientHeight
|
||||
expect(scrollBottom).toBe((6 + 1 + editor.verticalScrollMargin) * component.measurements.lineHeight)
|
||||
|
||||
editor.setCursorScreenPosition([8, 0])
|
||||
await component.getNextUpdatePromise()
|
||||
scrollBottom = scroller.scrollTop + scroller.clientHeight
|
||||
expect(scrollBottom).toBe((8 + 1 + editor.verticalScrollMargin) * component.measurements.lineHeight)
|
||||
|
||||
editor.setCursorScreenPosition([3, 0])
|
||||
await component.getNextUpdatePromise()
|
||||
expect(scroller.scrollTop).toBe((3 - editor.verticalScrollMargin) * component.measurements.lineHeight)
|
||||
|
||||
editor.setCursorScreenPosition([2, 0])
|
||||
await component.getNextUpdatePromise()
|
||||
expect(scroller.scrollTop).toBe(0)
|
||||
})
|
||||
|
||||
it('does not vertically autoscroll by more than half of the visible lines if the editor is shorter than twice the scroll margin', async () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
const {scroller} = component.refs
|
||||
element.style.height = 5.5 * component.measurements.lineHeight + 'px'
|
||||
await component.getNextUpdatePromise()
|
||||
expect(component.getLastVisibleRow()).toBe(6)
|
||||
const scrollMarginInLines = 2
|
||||
|
||||
editor.setCursorScreenPosition([6, 0])
|
||||
await component.getNextUpdatePromise()
|
||||
let scrollBottom = scroller.scrollTop + scroller.clientHeight
|
||||
expect(scrollBottom).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight)
|
||||
|
||||
editor.setCursorScreenPosition([6, 4])
|
||||
await component.getNextUpdatePromise()
|
||||
scrollBottom = scroller.scrollTop + scroller.clientHeight
|
||||
expect(scrollBottom).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight)
|
||||
|
||||
editor.setCursorScreenPosition([4, 4])
|
||||
await component.getNextUpdatePromise()
|
||||
expect(scroller.scrollTop).toBe((4 - scrollMarginInLines) * component.measurements.lineHeight)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function verifyCursorPosition (component, cursorNode, row, column) {
|
||||
|
@ -29,6 +29,11 @@ class TextEditorComponent {
|
||||
this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horiontal pixel positions
|
||||
this.lineNodesByScreenLineId = new Map()
|
||||
this.textNodesByScreenLineId = new Map()
|
||||
this.pendingAutoscroll = null
|
||||
this.autoscrollTop = -1
|
||||
this.scrollWidthOrHeightChanged = false
|
||||
this.previousScrollWidth = 0
|
||||
this.previousScrollHeight = 0
|
||||
this.lastKeydown = null
|
||||
this.lastKeydownBeforeKeypress = null
|
||||
this.openedAccentedCharacterMenu = false
|
||||
@ -64,7 +69,10 @@ class TextEditorComponent {
|
||||
this.resolveNextUpdatePromise = null
|
||||
}
|
||||
|
||||
if (this.staleMeasurements.editorDimensions) this.measureEditorDimensions()
|
||||
if (this.scrollWidthOrHeightChanged) {
|
||||
this.measureClientDimensions()
|
||||
this.scrollWidthOrHeightChanged = false
|
||||
}
|
||||
|
||||
const longestLine = this.getLongestScreenLine()
|
||||
let measureLongestLine = false
|
||||
@ -74,14 +82,27 @@ class TextEditorComponent {
|
||||
measureLongestLine = true
|
||||
}
|
||||
|
||||
if (this.pendingAutoscroll) {
|
||||
this.autoscrollVertically()
|
||||
}
|
||||
|
||||
this.horizontalPositionsToMeasure.clear()
|
||||
etch.updateSync(this)
|
||||
if (measureLongestLine) this.measureLongestLineWidth(longestLine)
|
||||
|
||||
if (this.autoscrollTop >= 0) {
|
||||
this.refs.scroller.scrollTop = this.autoscrollTop
|
||||
this.autoscrollTop = -1
|
||||
}
|
||||
if (measureLongestLine) {
|
||||
this.measureLongestLineWidth(longestLine)
|
||||
}
|
||||
this.queryCursorsToRender()
|
||||
this.measureHorizontalPositions()
|
||||
this.positionCursorsToRender()
|
||||
|
||||
etch.updateSync(this)
|
||||
|
||||
this.pendingAutoscroll = null
|
||||
}
|
||||
|
||||
render () {
|
||||
@ -220,8 +241,16 @@ class TextEditorComponent {
|
||||
overflow: 'hidden'
|
||||
}
|
||||
if (this.measurements) {
|
||||
const width = this.measurements.scrollWidth + 'px'
|
||||
const height = this.getScrollHeight() + 'px'
|
||||
const scrollWidth = this.getScrollWidth()
|
||||
const scrollHeight = this.getScrollHeight()
|
||||
if (scrollWidth !== this.previousScrollWidth || scrollHeight !== this.previousScrollHeight) {
|
||||
this.scrollWidthOrHeightChanged = true
|
||||
this.previousScrollWidth = scrollWidth
|
||||
this.previousScrollHeight = scrollHeight
|
||||
}
|
||||
|
||||
const width = scrollWidth + 'px'
|
||||
const height = scrollHeight + 'px'
|
||||
style.width = width
|
||||
style.height = height
|
||||
children = [
|
||||
@ -280,7 +309,7 @@ class TextEditorComponent {
|
||||
contain: 'strict',
|
||||
position: 'absolute',
|
||||
height: tileHeight + 'px',
|
||||
width: this.measurements.scrollWidth + 'px',
|
||||
width: width,
|
||||
willChange: 'transform',
|
||||
transform: `translateY(${this.topPixelPositionForRow(tileStartRow)}px)`,
|
||||
backgroundColor: 'inherit'
|
||||
@ -382,6 +411,7 @@ class TextEditorComponent {
|
||||
this.getRenderedEndRow() - 1,
|
||||
]
|
||||
})
|
||||
if (global.debug) debugger
|
||||
const lastCursorMarker = model.getLastCursor().getMarker()
|
||||
|
||||
this.cursorsToRender.length = cursorMarkers.length
|
||||
@ -463,8 +493,8 @@ class TextEditorComponent {
|
||||
|
||||
// Ensure the input is in the visible part of the scrolled content to avoid
|
||||
// the browser trying to auto-scroll to the form-field.
|
||||
hiddenInput.style.top = this.measurements.scrollTop + 'px'
|
||||
hiddenInput.style.left = this.measurements.scrollLeft + 'px'
|
||||
hiddenInput.style.top = this.getScrollTop() + 'px'
|
||||
hiddenInput.style.left = this.getScrollLeft() + 'px'
|
||||
|
||||
hiddenInput.focus()
|
||||
this.focused = true
|
||||
@ -490,12 +520,14 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
didScroll () {
|
||||
this.measureScrollPosition()
|
||||
this.updateSync()
|
||||
if (this.measureScrollPosition()) {
|
||||
this.updateSync()
|
||||
}
|
||||
}
|
||||
|
||||
didResize () {
|
||||
if (this.measureEditorDimensions()) {
|
||||
this.measureClientDimensions()
|
||||
this.scheduleUpdate()
|
||||
}
|
||||
}
|
||||
@ -566,10 +598,60 @@ class TextEditorComponent {
|
||||
this.lastKeydown = null
|
||||
}
|
||||
|
||||
didRequestAutoscroll (autoscroll) {
|
||||
this.pendingAutoscroll = autoscroll
|
||||
this.scheduleUpdate()
|
||||
}
|
||||
|
||||
autoscrollVertically () {
|
||||
const {screenRange, options} = this.pendingAutoscroll
|
||||
|
||||
const screenRangeTop = this.pixelTopForScreenRow(screenRange.start.row)
|
||||
const screenRangeBottom = this.pixelTopForScreenRow(screenRange.end.row) + this.measurements.lineHeight
|
||||
const verticalScrollMargin = this.getVerticalScrollMargin()
|
||||
|
||||
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.autoscrollTop = desiredScrollBottom - this.measurements.clientHeight
|
||||
}
|
||||
if (desiredScrollTop < this.getScrollTop()) {
|
||||
this.autoscrollTop = desiredScrollTop
|
||||
}
|
||||
} else {
|
||||
if (desiredScrollTop < this.getScrollTop()) {
|
||||
this.autoscrollTop = desiredScrollTop
|
||||
}
|
||||
if (desiredScrollBottom > this.getScrollBottom()) {
|
||||
this.autoscrollTop = desiredScrollBottom - this.measurements.clientHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getVerticalScrollMargin () {
|
||||
const {clientHeight, lineHeight} = this.measurements
|
||||
const marginInLines = Math.min(
|
||||
this.getModel().verticalScrollMargin,
|
||||
Math.floor(((clientHeight / lineHeight) - 1) / 2)
|
||||
)
|
||||
return marginInLines * this.measurements.lineHeight
|
||||
}
|
||||
|
||||
performInitialMeasurements () {
|
||||
this.measurements = {}
|
||||
this.staleMeasurements = {}
|
||||
this.measureEditorDimensions()
|
||||
this.measureClientDimensions()
|
||||
this.measureScrollPosition()
|
||||
this.measureCharacterDimensions()
|
||||
this.measureGutterDimensions()
|
||||
@ -591,8 +673,31 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
measureScrollPosition () {
|
||||
this.measurements.scrollTop = this.refs.scroller.scrollTop
|
||||
this.measurements.scrollLeft = this.refs.scroller.scrollLeft
|
||||
let scrollPositionChanged = false
|
||||
const {scrollTop, scrollLeft} = this.refs.scroller
|
||||
if (scrollTop !== this.measurements.scrollTop) {
|
||||
this.measurements.scrollTop = scrollTop
|
||||
scrollPositionChanged = true
|
||||
}
|
||||
if (scrollLeft !== this.measurements.scrollLeft) {
|
||||
this.measurements.scrollLeft = scrollLeft
|
||||
scrollPositionChanged = true
|
||||
}
|
||||
return scrollPositionChanged
|
||||
}
|
||||
|
||||
measureClientDimensions () {
|
||||
let clientDimensionsChanged = false
|
||||
const {clientHeight, clientWidth} = this.refs.scroller
|
||||
if (clientHeight !== this.measurements.clientHeight) {
|
||||
this.measurements.clientHeight = clientHeight
|
||||
clientDimensionsChanged = true
|
||||
}
|
||||
if (clientWidth !== this.measurements.clientWidth) {
|
||||
this.measurements.clientWidth = clientWidth
|
||||
clientDimensionsChanged = true
|
||||
}
|
||||
return clientDimensionsChanged
|
||||
}
|
||||
|
||||
measureCharacterDimensions () {
|
||||
@ -604,7 +709,7 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
measureLongestLineWidth (screenLine) {
|
||||
this.measurements.scrollWidth = this.lineNodesByScreenLineId.get(screenLine.id).firstChild.offsetWidth
|
||||
this.measurements.longestLineWidth = this.lineNodesByScreenLineId.get(screenLine.id).firstChild.offsetWidth
|
||||
}
|
||||
|
||||
measureGutterDimensions () {
|
||||
@ -642,8 +747,6 @@ class TextEditorComponent {
|
||||
let textNodeStartColumn = 0
|
||||
let textNodesIndex = 0
|
||||
|
||||
if (!textNodes) debugger
|
||||
|
||||
columnLoop:
|
||||
for (let columnsIndex = 0; columnsIndex < columnsToMeasure.length; columnsIndex++) {
|
||||
while (textNodesIndex < textNodes.length) {
|
||||
@ -706,6 +809,7 @@ class TextEditorComponent {
|
||||
const scheduleUpdate = this.scheduleUpdate.bind(this)
|
||||
this.disposables.add(model.selectionsMarkerLayer.onDidUpdate(scheduleUpdate))
|
||||
this.disposables.add(model.displayLayer.onDidChangeSync(scheduleUpdate))
|
||||
this.disposables.add(model.onDidRequestAutoscroll(this.didRequestAutoscroll.bind(this)))
|
||||
}
|
||||
|
||||
isVisible () {
|
||||
@ -717,7 +821,17 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
getScrollTop () {
|
||||
return this.measurements ? this.measurements.scrollTop : null
|
||||
if (this.autoscrollTop >= 0) {
|
||||
return this.autoscrollTop
|
||||
} else if (this.measurements != null) {
|
||||
return this.measurements.scrollTop
|
||||
}
|
||||
}
|
||||
|
||||
getScrollBottom () {
|
||||
return this.measurements
|
||||
? this.getScrollTop() + this.measurements.clientHeight
|
||||
: null
|
||||
}
|
||||
|
||||
getScrollLeft () {
|
||||
@ -753,12 +867,13 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
getFirstVisibleRow () {
|
||||
const {scrollTop, lineHeight} = this.measurements
|
||||
const scrollTop = this.getScrollTop()
|
||||
const lineHeight = this.measurements.lineHeight
|
||||
return Math.floor(scrollTop / lineHeight)
|
||||
}
|
||||
|
||||
getLastVisibleRow () {
|
||||
const {scrollTop, scrollerHeight, lineHeight} = this.measurements
|
||||
const {scrollerHeight, lineHeight} = this.measurements
|
||||
return Math.min(
|
||||
this.getModel().getApproximateScreenLineCount() - 1,
|
||||
this.getFirstVisibleRow() + Math.ceil(scrollerHeight / lineHeight)
|
||||
@ -769,6 +884,10 @@ class TextEditorComponent {
|
||||
return row * this.measurements.lineHeight
|
||||
}
|
||||
|
||||
getScrollWidth () {
|
||||
return Math.round(this.measurements.longestLineWidth + this.measurements.baseCharacterWidth)
|
||||
}
|
||||
|
||||
getScrollHeight () {
|
||||
return this.getModel().getApproximateScreenLineCount() * this.measurements.lineHeight
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user