mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-11-10 10:17:11 +03:00
Introduce synthetic scrolling
We previously thought scroll events had changed somehow to become synchronous, but were wrong. This introduces synthetic scrolling where we use GPU translation of the contents of the gutter and scroll containers to simulate scrolling and explicitly capture mousewheel events. Still need to add dummy scrollbars and deal with their footprint in clientHeight and clientWidth.
This commit is contained in:
parent
0e747a400d
commit
5a47f179e3
@ -37,7 +37,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(element.querySelectorAll('.line-number').length).toBe(9)
|
||||
expect(element.querySelectorAll('.line').length).toBe(9)
|
||||
|
||||
component.refs.scroller.scrollTop = 5 * component.measurements.lineHeight
|
||||
component.setScrollTop(5 * component.getLineHeight())
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
// After scrolling down beyond > 3 rows, the order of line numbers and lines
|
||||
@ -58,7 +58,7 @@ describe('TextEditorComponent', () => {
|
||||
editor.lineTextForScreenRow(8)
|
||||
])
|
||||
|
||||
component.refs.scroller.scrollTop = 2.5 * component.measurements.lineHeight
|
||||
component.setScrollTop(2.5 * component.getLineHeight())
|
||||
await component.getNextUpdatePromise()
|
||||
expect(Array.from(element.querySelectorAll('.line-number')).map(element => element.textContent.trim())).toEqual([
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9'
|
||||
@ -88,25 +88,24 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
it('honors the scrollPastEnd option by adding empty space equivalent to the clientHeight to the end of the content area', async () => {
|
||||
const {component, element, editor} = buildComponent({autoHeight: false, autoWidth: false})
|
||||
const {scroller} = component.refs
|
||||
const {scrollContainer} = component.refs
|
||||
|
||||
await editor.update({scrollPastEnd: true})
|
||||
await setEditorHeightInLines(component, 6)
|
||||
|
||||
// scroll to end
|
||||
scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight
|
||||
component.setScrollTop(scrollContainer.scrollHeight - scrollContainer.clientHeight)
|
||||
await component.getNextUpdatePromise()
|
||||
expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() - 3)
|
||||
|
||||
editor.update({scrollPastEnd: false})
|
||||
await component.getNextUpdatePromise() // wait for scrollable content resize
|
||||
await component.getNextUpdatePromise() // wait for async scroll event due to scrollbar shrinking
|
||||
expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() - 6)
|
||||
|
||||
// Always allows at least 3 lines worth of overscroll if the editor is short
|
||||
await setEditorHeightInLines(component, 2)
|
||||
await editor.update({scrollPastEnd: true})
|
||||
scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight
|
||||
component.setScrollTop(scrollContainer.scrollHeight - scrollContainer.clientHeight)
|
||||
await component.getNextUpdatePromise()
|
||||
expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() + 1)
|
||||
})
|
||||
@ -125,18 +124,9 @@ describe('TextEditorComponent', () => {
|
||||
expect(gutterElement.firstChild.style.contain).toBe('strict')
|
||||
})
|
||||
|
||||
it('translates the gutter so it is always visible when scrolling to the right', async () => {
|
||||
const {component, element, editor} = buildComponent({width: 100})
|
||||
|
||||
expect(component.refs.gutterContainer.style.transform).toBe('translateX(0px)')
|
||||
component.refs.scroller.scrollLeft = 100
|
||||
await component.getNextUpdatePromise()
|
||||
expect(component.refs.gutterContainer.style.transform).toBe('translateX(100px)')
|
||||
})
|
||||
|
||||
it('renders cursors within the visible row range', async () => {
|
||||
const {component, element, editor} = buildComponent({height: 40, rowsPerTile: 2})
|
||||
component.refs.scroller.scrollTop = 100
|
||||
component.setScrollTop(100)
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
expect(component.getRenderedStartRow()).toBe(4)
|
||||
@ -180,8 +170,8 @@ describe('TextEditorComponent', () => {
|
||||
it('places the hidden input element at the location of the last cursor if it is visible', async () => {
|
||||
const {component, element, editor} = buildComponent({height: 60, width: 120, rowsPerTile: 2})
|
||||
const {hiddenInput} = component.refs
|
||||
component.refs.scroller.scrollTop = 100
|
||||
component.refs.scroller.scrollLeft = 40
|
||||
component.setScrollTop(100)
|
||||
component.setScrollLeft(40)
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
expect(component.getRenderedStartRow()).toBe(4)
|
||||
@ -205,6 +195,7 @@ describe('TextEditorComponent', () => {
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
expect(getBaseCharacterWidth(component)).toBe(55)
|
||||
console.log('running expectation');
|
||||
expect(lineNodeForScreenRow(component, 3).textContent).toBe(
|
||||
' var pivot = items.shift(), current, left = [], '
|
||||
)
|
||||
@ -220,8 +211,8 @@ describe('TextEditorComponent', () => {
|
||||
' = [], right = [];'
|
||||
)
|
||||
|
||||
const {scroller} = component.refs
|
||||
expect(scroller.clientWidth).toBe(scroller.scrollWidth)
|
||||
const {scrollContainer} = component.refs
|
||||
expect(scrollContainer.clientWidth).toBe(scrollContainer.scrollWidth)
|
||||
})
|
||||
|
||||
it('decorates the line numbers of folded lines', async () => {
|
||||
@ -231,29 +222,30 @@ describe('TextEditorComponent', () => {
|
||||
expect(lineNumberNodeForScreenRow(component, 1).classList.contains('folded')).toBe(true)
|
||||
})
|
||||
|
||||
it('makes lines at least as wide as the scroller', async () => {
|
||||
it('makes lines at least as wide as the scrollContainer', async () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
const {scroller, gutterContainer} = component.refs
|
||||
const {scrollContainer, gutterContainer} = component.refs
|
||||
editor.setText('a')
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
expect(element.querySelector('.line').offsetWidth).toBe(scroller.offsetWidth - gutterContainer.offsetWidth)
|
||||
expect(element.querySelector('.line').offsetWidth).toBe(scrollContainer.offsetWidth)
|
||||
})
|
||||
|
||||
it('resizes based on the content when the autoHeight and/or autoWidth options are true', async () => {
|
||||
const {component, element, editor} = buildComponent({autoHeight: true, autoWidth: true})
|
||||
const {gutterContainer, scrollContainer} = component.refs
|
||||
const initialWidth = element.offsetWidth
|
||||
const initialHeight = element.offsetHeight
|
||||
expect(initialWidth).toBe(component.refs.scroller.scrollWidth)
|
||||
expect(initialHeight).toBe(component.refs.scroller.scrollHeight)
|
||||
expect(initialWidth).toBe(gutterContainer.offsetWidth + scrollContainer.scrollWidth)
|
||||
expect(initialHeight).toBe(scrollContainer.scrollHeight)
|
||||
editor.setCursorScreenPosition([6, Infinity])
|
||||
editor.insertText('x'.repeat(50))
|
||||
await component.getNextUpdatePromise()
|
||||
expect(element.offsetWidth).toBe(component.refs.scroller.scrollWidth)
|
||||
expect(element.offsetWidth).toBe(gutterContainer.offsetWidth + scrollContainer.scrollWidth)
|
||||
expect(element.offsetWidth).toBeGreaterThan(initialWidth)
|
||||
editor.insertText('\n'.repeat(5))
|
||||
await component.getNextUpdatePromise()
|
||||
expect(element.offsetHeight).toBe(component.refs.scroller.scrollHeight)
|
||||
expect(element.offsetHeight).toBe(scrollContainer.scrollHeight)
|
||||
expect(element.offsetHeight).toBeGreaterThan(initialHeight)
|
||||
})
|
||||
|
||||
@ -369,32 +361,29 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
describe('autoscroll on cursor movement', () => {
|
||||
it('automatically scrolls vertically when the requested range is within the vertical scroll margin of the top or bottom', async () => {
|
||||
const {component, element, editor} = buildComponent({height: 120})
|
||||
const {scroller} = component.refs
|
||||
const {component, editor} = buildComponent({height: 120})
|
||||
expect(component.getLastVisibleRow()).toBe(8)
|
||||
|
||||
editor.scrollToScreenRange([[4, 0], [6, 0]])
|
||||
await component.getNextUpdatePromise()
|
||||
let scrollBottom = scroller.scrollTop + scroller.clientHeight
|
||||
expect(scrollBottom).toBe((6 + 1 + editor.verticalScrollMargin) * component.measurements.lineHeight)
|
||||
expect(component.getScrollBottom()).toBe((6 + 1 + editor.verticalScrollMargin) * component.getLineHeight())
|
||||
|
||||
editor.scrollToScreenPosition([8, 0])
|
||||
await component.getNextUpdatePromise()
|
||||
scrollBottom = scroller.scrollTop + scroller.clientHeight
|
||||
expect(scrollBottom).toBe((8 + 1 + editor.verticalScrollMargin) * component.measurements.lineHeight)
|
||||
expect(component.getScrollBottom()).toBe((8 + 1 + editor.verticalScrollMargin) * component.measurements.lineHeight)
|
||||
|
||||
editor.scrollToScreenPosition([3, 0])
|
||||
await component.getNextUpdatePromise()
|
||||
expect(scroller.scrollTop).toBe((3 - editor.verticalScrollMargin) * component.measurements.lineHeight)
|
||||
expect(component.getScrollTop()).toBe((3 - editor.verticalScrollMargin) * component.measurements.lineHeight)
|
||||
|
||||
editor.scrollToScreenPosition([2, 0])
|
||||
await component.getNextUpdatePromise()
|
||||
expect(scroller.scrollTop).toBe(0)
|
||||
expect(component.getScrollTop()).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({autoHeight: false})
|
||||
const {scroller} = component.refs
|
||||
const {scrollContainer} = component.refs
|
||||
element.style.height = 5.5 * component.measurements.lineHeight + 'px'
|
||||
await component.getNextUpdatePromise()
|
||||
expect(component.getLastVisibleRow()).toBe(6)
|
||||
@ -402,26 +391,24 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
editor.scrollToScreenPosition([6, 0])
|
||||
await component.getNextUpdatePromise()
|
||||
let scrollBottom = scroller.scrollTop + scroller.clientHeight
|
||||
expect(scrollBottom).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight)
|
||||
expect(component.getScrollBottom()).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight)
|
||||
|
||||
editor.scrollToScreenPosition([6, 4])
|
||||
await component.getNextUpdatePromise()
|
||||
scrollBottom = scroller.scrollTop + scroller.clientHeight
|
||||
expect(scrollBottom).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight)
|
||||
expect(component.getScrollBottom()).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight)
|
||||
|
||||
editor.scrollToScreenRange([[4, 4], [6, 4]])
|
||||
await component.getNextUpdatePromise()
|
||||
expect(scroller.scrollTop).toBe((4 - scrollMarginInLines) * component.measurements.lineHeight)
|
||||
expect(component.getScrollTop()).toBe((4 - scrollMarginInLines) * component.measurements.lineHeight)
|
||||
|
||||
editor.scrollToScreenRange([[4, 4], [6, 4]], {reversed: false})
|
||||
await component.getNextUpdatePromise()
|
||||
expect(scrollBottom).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight)
|
||||
expect(component.getScrollBottom()).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight)
|
||||
})
|
||||
|
||||
it('automatically scrolls horizontally when the requested range is within the horizontal scroll margin of the right edge of the gutter or right edge of the screen', async () => {
|
||||
it('automatically scrolls horizontally when the requested range is within the horizontal scroll margin of the right edge of the gutter or right edge of the scroll container', async () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
const {scroller} = component.refs
|
||||
const {scrollContainer} = component.refs
|
||||
element.style.width =
|
||||
component.getGutterContainerWidth() +
|
||||
3 * editor.horizontalScrollMargin * component.measurements.baseCharacterWidth + 'px'
|
||||
@ -429,32 +416,30 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
editor.scrollToScreenRange([[1, 12], [2, 28]])
|
||||
await component.getNextUpdatePromise()
|
||||
let expectedScrollLeft = Math.floor(
|
||||
let expectedScrollLeft = Math.round(
|
||||
clientLeftForCharacter(component, 1, 12) -
|
||||
lineNodeForScreenRow(component, 1).getBoundingClientRect().left -
|
||||
(editor.horizontalScrollMargin * component.measurements.baseCharacterWidth)
|
||||
)
|
||||
expect(scroller.scrollLeft).toBe(expectedScrollLeft)
|
||||
expect(component.getScrollLeft()).toBe(expectedScrollLeft)
|
||||
|
||||
editor.scrollToScreenRange([[1, 12], [2, 28]], {reversed: false})
|
||||
await component.getNextUpdatePromise()
|
||||
expectedScrollLeft = Math.floor(
|
||||
expectedScrollLeft = Math.round(
|
||||
component.getGutterContainerWidth() +
|
||||
clientLeftForCharacter(component, 2, 28) -
|
||||
lineNodeForScreenRow(component, 2).getBoundingClientRect().left +
|
||||
(editor.horizontalScrollMargin * component.measurements.baseCharacterWidth) -
|
||||
scroller.clientWidth
|
||||
scrollContainer.clientWidth
|
||||
)
|
||||
expect(scroller.scrollLeft).toBe(expectedScrollLeft)
|
||||
expect(component.getScrollLeft()).toBe(expectedScrollLeft)
|
||||
})
|
||||
|
||||
it('does not horizontally autoscroll by more than half of the visible "base-width" characters if the editor is narrower than twice the scroll margin', async () => {
|
||||
const {component, element, editor} = buildComponent({autoHeight: false})
|
||||
const {scroller, gutterContainer} = component.refs
|
||||
const {component, editor} = buildComponent({autoHeight: false})
|
||||
await setEditorWidthInCharacters(component, 1.5 * editor.horizontalScrollMargin)
|
||||
|
||||
const contentWidth = scroller.clientWidth - gutterContainer.offsetWidth
|
||||
const contentWidthInCharacters = Math.floor(contentWidth / component.measurements.baseCharacterWidth)
|
||||
const contentWidthInCharacters = Math.floor(component.getScrollContainerClientWidth() / component.getBaseCharacterWidth())
|
||||
expect(contentWidthInCharacters).toBe(9)
|
||||
|
||||
editor.scrollToScreenRange([[6, 10], [6, 15]])
|
||||
@ -462,27 +447,26 @@ describe('TextEditorComponent', () => {
|
||||
let expectedScrollLeft = Math.floor(
|
||||
clientLeftForCharacter(component, 6, 10) -
|
||||
lineNodeForScreenRow(component, 1).getBoundingClientRect().left -
|
||||
(4 * component.measurements.baseCharacterWidth)
|
||||
(4 * component.getBaseCharacterWidth())
|
||||
)
|
||||
expect(scroller.scrollLeft).toBe(expectedScrollLeft)
|
||||
expect(component.getScrollLeft()).toBe(expectedScrollLeft)
|
||||
})
|
||||
|
||||
it('correctly autoscrolls after inserting a line that exceeds the current content width', async () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
const {scroller} = component.refs
|
||||
element.style.width = component.getGutterContainerWidth() + component.measurements.longestLineWidth + 'px'
|
||||
element.style.width = component.getGutterContainerWidth() + component.getContentWidth() + 'px'
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
editor.setCursorScreenPosition([0, Infinity])
|
||||
editor.insertText('x'.repeat(100))
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
expect(scroller.scrollLeft).toBe(component.getScrollWidth() - scroller.clientWidth)
|
||||
expect(component.getScrollLeft()).toBe(component.getScrollWidth() - component.getScrollContainerClientWidth())
|
||||
})
|
||||
|
||||
it('accounts for the presence of horizontal scrollbars that appear during the same frame as the autoscroll', async () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
const {scroller} = component.refs
|
||||
const {scrollContainer} = component.refs
|
||||
element.style.height = component.getScrollHeight() + 'px'
|
||||
element.style.width = component.getScrollWidth() + 'px'
|
||||
await component.getNextUpdatePromise()
|
||||
@ -491,8 +475,8 @@ describe('TextEditorComponent', () => {
|
||||
editor.insertText('\n\n' + 'x'.repeat(100))
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
expect(scroller.scrollTop).toBe(component.getScrollHeight() - scroller.clientHeight)
|
||||
expect(scroller.scrollLeft).toBe(component.getScrollWidth() - scroller.clientWidth)
|
||||
expect(component.getScrollTop()).toBe(component.getScrollHeight() - component.getScrollContainerClientHeight())
|
||||
expect(component.getScrollLeft()).toBe(component.getScrollWidth() - component.getScrollContainerClientWidth())
|
||||
})
|
||||
})
|
||||
|
||||
@ -735,7 +719,7 @@ describe('TextEditorComponent', () => {
|
||||
)
|
||||
|
||||
// Don't flash on next update if another flash wasn't requested
|
||||
component.refs.scroller.scrollTop = 100
|
||||
component.setScrollTop(100)
|
||||
await component.getNextUpdatePromise()
|
||||
expect(highlights[0].classList.contains('b')).toBe(false)
|
||||
expect(highlights[1].classList.contains('b')).toBe(false)
|
||||
@ -1156,29 +1140,29 @@ describe('TextEditorComponent', () => {
|
||||
expect(editor.getCursorScreenPosition()).toEqual([0, 0])
|
||||
})
|
||||
|
||||
it('autoscrolls the content when dragging near the edge of the screen', async () => {
|
||||
const {component, editor} = buildComponent({width: 200, height: 200})
|
||||
const {scroller} = component.refs
|
||||
it('autoscrolls the content when dragging near the edge of the scroll container', async () => {
|
||||
const {component, element, editor} = buildComponent({width: 200, height: 200})
|
||||
spyOn(component, 'handleMouseDragUntilMouseUp')
|
||||
|
||||
let previousScrollTop = 0
|
||||
let previousScrollLeft = 0
|
||||
function assertScrolledDownAndRight () {
|
||||
expect(scroller.scrollTop).toBeGreaterThan(previousScrollTop)
|
||||
previousScrollTop = scroller.scrollTop
|
||||
expect(scroller.scrollLeft).toBeGreaterThan(previousScrollLeft)
|
||||
previousScrollLeft = scroller.scrollLeft
|
||||
expect(component.getScrollTop()).toBeGreaterThan(previousScrollTop)
|
||||
previousScrollTop = component.getScrollTop()
|
||||
expect(component.getScrollLeft()).toBeGreaterThan(previousScrollLeft)
|
||||
previousScrollLeft = component.getScrollLeft()
|
||||
}
|
||||
|
||||
function assertScrolledUpAndLeft () {
|
||||
expect(scroller.scrollTop).toBeLessThan(previousScrollTop)
|
||||
previousScrollTop = scroller.scrollTop
|
||||
expect(scroller.scrollLeft).toBeLessThan(previousScrollLeft)
|
||||
previousScrollLeft = scroller.scrollLeft
|
||||
expect(component.getScrollTop()).toBeLessThan(previousScrollTop)
|
||||
previousScrollTop = component.getScrollTop()
|
||||
expect(component.getScrollLeft()).toBeLessThan(previousScrollLeft)
|
||||
previousScrollLeft = component.getScrollLeft()
|
||||
}
|
||||
|
||||
component.didMouseDownOnContent({detail: 1, button: 0, clientX: 100, clientY: 100})
|
||||
const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[0][0]
|
||||
|
||||
didDrag({clientX: 199, clientY: 199})
|
||||
assertScrolledDownAndRight()
|
||||
didDrag({clientX: 199, clientY: 199})
|
||||
@ -1192,27 +1176,24 @@ describe('TextEditorComponent', () => {
|
||||
didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1})
|
||||
assertScrolledUpAndLeft()
|
||||
|
||||
// Don't artificially update scroll measurements beyond the minimum or
|
||||
// maximum possible scroll positions
|
||||
expect(scroller.scrollTop).toBe(0)
|
||||
expect(scroller.scrollLeft).toBe(0)
|
||||
// Don't artificially update scroll position beyond possible values
|
||||
expect(component.getScrollTop()).toBe(0)
|
||||
expect(component.getScrollLeft()).toBe(0)
|
||||
didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1})
|
||||
expect(component.measurements.scrollTop).toBe(0)
|
||||
expect(scroller.scrollTop).toBe(0)
|
||||
expect(component.measurements.scrollLeft).toBe(0)
|
||||
expect(scroller.scrollLeft).toBe(0)
|
||||
expect(component.getScrollTop()).toBe(0)
|
||||
expect(component.getScrollLeft()).toBe(0)
|
||||
|
||||
const maxScrollTop = scroller.scrollHeight - scroller.clientHeight
|
||||
const maxScrollLeft = scroller.scrollWidth - scroller.clientWidth
|
||||
scroller.scrollTop = maxScrollTop
|
||||
scroller.scrollLeft = maxScrollLeft
|
||||
const maxScrollTop = component.getMaxScrollTop()
|
||||
const maxScrollLeft = component.getMaxScrollLeft()
|
||||
component.setScrollTop(maxScrollTop)
|
||||
component.setScrollLeft(maxScrollLeft)
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
didDrag({clientX: 199, clientY: 199})
|
||||
didDrag({clientX: 199, clientY: 199})
|
||||
didDrag({clientX: 199, clientY: 199})
|
||||
expect(component.measurements.scrollTop).toBe(maxScrollTop)
|
||||
expect(component.measurements.scrollLeft).toBe(maxScrollLeft)
|
||||
expect(component.getScrollTop()).toBe(maxScrollTop)
|
||||
expect(component.getScrollLeft()).toBe(maxScrollLeft)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1387,25 +1368,25 @@ describe('TextEditorComponent', () => {
|
||||
expect(editor.isFoldedAtScreenRow(1)).toBe(false)
|
||||
})
|
||||
|
||||
it('autoscrolls the content when dragging near the edge of the screen', async () => {
|
||||
it('autoscrolls when dragging near the top or bottom of the gutter', async () => {
|
||||
const {component, editor} = buildComponent({width: 200, height: 200})
|
||||
const {scroller} = component.refs
|
||||
const {scrollContainer} = component.refs
|
||||
spyOn(component, 'handleMouseDragUntilMouseUp')
|
||||
|
||||
let previousScrollTop = 0
|
||||
let previousScrollLeft = 0
|
||||
function assertScrolledDown () {
|
||||
expect(scroller.scrollTop).toBeGreaterThan(previousScrollTop)
|
||||
previousScrollTop = scroller.scrollTop
|
||||
expect(scroller.scrollLeft).toBe(previousScrollLeft)
|
||||
previousScrollLeft = scroller.scrollLeft
|
||||
expect(component.getScrollTop()).toBeGreaterThan(previousScrollTop)
|
||||
previousScrollTop = component.getScrollTop()
|
||||
expect(component.getScrollLeft()).toBe(previousScrollLeft)
|
||||
previousScrollLeft = component.getScrollLeft()
|
||||
}
|
||||
|
||||
function assertScrolledUp () {
|
||||
expect(scroller.scrollTop).toBeLessThan(previousScrollTop)
|
||||
previousScrollTop = scroller.scrollTop
|
||||
expect(scroller.scrollLeft).toBe(previousScrollLeft)
|
||||
previousScrollLeft = scroller.scrollLeft
|
||||
expect(component.getScrollTop()).toBeLessThan(previousScrollTop)
|
||||
previousScrollTop = component.getScrollTop()
|
||||
expect(component.getScrollLeft()).toBe(previousScrollLeft)
|
||||
previousScrollLeft = component.getScrollLeft()
|
||||
}
|
||||
|
||||
component.didMouseDownOnLineNumberGutter({detail: 1, button: 0, clientX: 0, clientY: 100})
|
||||
@ -1425,25 +1406,23 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
// Don't artificially update scroll measurements beyond the minimum or
|
||||
// maximum possible scroll positions
|
||||
expect(scroller.scrollTop).toBe(0)
|
||||
expect(scroller.scrollLeft).toBe(0)
|
||||
expect(component.getScrollTop()).toBe(0)
|
||||
expect(component.getScrollLeft()).toBe(0)
|
||||
didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1})
|
||||
expect(component.measurements.scrollTop).toBe(0)
|
||||
expect(scroller.scrollTop).toBe(0)
|
||||
expect(component.measurements.scrollLeft).toBe(0)
|
||||
expect(scroller.scrollLeft).toBe(0)
|
||||
expect(component.getScrollTop()).toBe(0)
|
||||
expect(component.getScrollLeft()).toBe(0)
|
||||
|
||||
const maxScrollTop = scroller.scrollHeight - scroller.clientHeight
|
||||
const maxScrollLeft = scroller.scrollWidth - scroller.clientWidth
|
||||
scroller.scrollTop = maxScrollTop
|
||||
scroller.scrollLeft = maxScrollLeft
|
||||
const maxScrollTop = component.getMaxScrollTop()
|
||||
const maxScrollLeft = component.getMaxScrollLeft()
|
||||
component.setScrollTop(maxScrollTop)
|
||||
component.setScrollLeft(maxScrollLeft)
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
didDrag({clientX: 199, clientY: 199})
|
||||
didDrag({clientX: 199, clientY: 199})
|
||||
didDrag({clientX: 199, clientY: 199})
|
||||
expect(component.measurements.scrollTop).toBe(maxScrollTop)
|
||||
expect(component.measurements.scrollLeft).toBe(maxScrollLeft)
|
||||
expect(component.getScrollTop()).toBe(maxScrollTop)
|
||||
expect(component.getScrollLeft()).toBe(maxScrollLeft)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1475,10 +1454,7 @@ function buildComponent (params = {}) {
|
||||
}
|
||||
|
||||
function getBaseCharacterWidth (component) {
|
||||
return Math.round(
|
||||
(component.refs.scroller.clientWidth - component.getGutterContainerWidth()) /
|
||||
component.measurements.baseCharacterWidth
|
||||
)
|
||||
return Math.round(component.getScrollContainerWidth() / component.getBaseCharacterWidth())
|
||||
}
|
||||
|
||||
async function setEditorHeightInLines(component, heightInLines) {
|
||||
|
@ -16,6 +16,7 @@ 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
|
||||
|
||||
function scaleMouseDragAutoscrollDelta (delta) {
|
||||
return Math.pow(delta / 3, 3) / 280
|
||||
@ -47,7 +48,8 @@ class TextEditorComponent {
|
||||
this.lineNodesByScreenLineId = new Map()
|
||||
this.textNodesByScreenLineId = new Map()
|
||||
this.pendingAutoscroll = null
|
||||
this.autoscrollTop = null
|
||||
this.scrollTop = 0
|
||||
this.scrollLeft = 0
|
||||
this.previousScrollWidth = 0
|
||||
this.previousScrollHeight = 0
|
||||
this.lastKeydown = null
|
||||
@ -97,7 +99,7 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
this.horizontalPositionsToMeasure.clear()
|
||||
if (this.pendingAutoscroll) this.initiateAutoscroll()
|
||||
if (this.pendingAutoscroll) this.autoscrollVertically()
|
||||
this.populateVisibleRowRange()
|
||||
const longestLineToMeasure = this.checkForNewLongestLine()
|
||||
this.queryScreenLinesToRender()
|
||||
@ -111,19 +113,10 @@ class TextEditorComponent {
|
||||
|
||||
etch.updateSync(this)
|
||||
|
||||
// If scrollHeight or scrollWidth changed, we may have shown or hidden
|
||||
// scrollbars, affecting the clientWidth or clientHeight
|
||||
if (this.checkIfScrollDimensionsChanged()) {
|
||||
this.measureClientDimensions()
|
||||
// If the clientHeight changed, our previous vertical autoscroll may have
|
||||
// been off by the height of the horizontal scrollbar. If we *still* need
|
||||
// to autoscroll, just re-render the frame.
|
||||
if (this.pendingAutoscroll && this.initiateAutoscroll()) {
|
||||
this.updateSync()
|
||||
return
|
||||
}
|
||||
if (this.pendingAutoscroll) {
|
||||
this.autoscrollHorizontally()
|
||||
this.pendingAutoscroll = null
|
||||
}
|
||||
if (this.pendingAutoscroll) this.finalizeAutoscroll()
|
||||
this.currentFrameLineNumberGutterProps = null
|
||||
}
|
||||
|
||||
@ -142,103 +135,82 @@ class TextEditorComponent {
|
||||
render () {
|
||||
const {model} = this.props
|
||||
|
||||
const style = {
|
||||
overflow: 'hidden',
|
||||
}
|
||||
const style = {}
|
||||
if (!model.getAutoHeight() && !model.getAutoWidth()) {
|
||||
style.contain = 'strict'
|
||||
}
|
||||
|
||||
if (this.measurements) {
|
||||
if (model.getAutoHeight()) {
|
||||
style.height = this.getScrollHeight() + 'px'
|
||||
style.height = this.getContentHeight() + 'px'
|
||||
}
|
||||
if (model.getAutoWidth()) {
|
||||
style.width = this.getScrollWidth() + 'px'
|
||||
style.width = this.getGutterContainerWidth() + this.getContentWidth() + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
let attributes = null
|
||||
let className = 'editor'
|
||||
if (this.focused) {
|
||||
className += ' is-focused'
|
||||
}
|
||||
if (this.focused) className += ' is-focused'
|
||||
if (model.isMini()) {
|
||||
attributes = {mini: ''}
|
||||
className += ' mini'
|
||||
}
|
||||
|
||||
const scrollerOverflowX = (model.isMini() || model.isSoftWrapped()) ? 'hidden' : 'auto'
|
||||
const scrollerOverflowY = model.isMini() ? 'hidden' : 'auto'
|
||||
|
||||
return $('atom-text-editor',
|
||||
{
|
||||
className,
|
||||
attributes,
|
||||
style,
|
||||
attributes,
|
||||
tabIndex: -1,
|
||||
on: {
|
||||
focus: this.didFocus,
|
||||
blur: this.didBlur
|
||||
blur: this.didBlur,
|
||||
mousewheel: this.didMouseWheel
|
||||
}
|
||||
},
|
||||
$.div(
|
||||
{
|
||||
ref: 'clientContainer',
|
||||
style: {
|
||||
position: 'relative',
|
||||
contain: 'strict',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'inherit',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'inherit'
|
||||
height: '100%'
|
||||
}
|
||||
},
|
||||
$.div(
|
||||
{
|
||||
ref: 'scroller',
|
||||
className: 'scroll-view',
|
||||
on: {scroll: this.didScroll},
|
||||
style: {
|
||||
position: 'absolute',
|
||||
contain: 'strict',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
overflowX: scrollerOverflowX,
|
||||
overflowY: scrollerOverflowY,
|
||||
backgroundColor: 'inherit'
|
||||
}
|
||||
},
|
||||
$.div(
|
||||
{
|
||||
style: {
|
||||
isolate: 'content',
|
||||
width: 'max-content',
|
||||
height: 'max-content',
|
||||
backgroundColor: 'inherit'
|
||||
}
|
||||
},
|
||||
this.renderGutterContainer(),
|
||||
this.renderContent()
|
||||
)
|
||||
)
|
||||
this.renderGutterContainer(),
|
||||
this.renderScrollContainer()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
renderGutterContainer () {
|
||||
if (this.props.model.isMini()) return null
|
||||
const props = {ref: 'gutterContainer', className: 'gutter-container'}
|
||||
|
||||
const innerStyle = {
|
||||
willChange: 'transform',
|
||||
backgroundColor: 'inherit'
|
||||
}
|
||||
if (this.measurements) {
|
||||
props.style = {
|
||||
position: 'relative',
|
||||
willChange: 'transform',
|
||||
transform: `translateX(${this.measurements.scrollLeft}px)`,
|
||||
zIndex: 1
|
||||
}
|
||||
innerStyle.transform = `translateY(${-this.getScrollTop()}px)`
|
||||
}
|
||||
|
||||
return $.div(props, this.renderLineNumberGutter())
|
||||
return $.div(
|
||||
{
|
||||
ref: 'gutterContainer',
|
||||
className: 'gutter-container',
|
||||
style: {
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
backgroundColor: 'inherit'
|
||||
}
|
||||
},
|
||||
$.div({style: innerStyle},
|
||||
this.renderLineNumberGutter()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
renderLineNumberGutter () {
|
||||
@ -278,8 +250,8 @@ class TextEditorComponent {
|
||||
ref: 'lineNumberGutter',
|
||||
parentComponent: this,
|
||||
height: this.getScrollHeight(),
|
||||
width: this.measurements.lineNumberGutterWidth,
|
||||
lineHeight: this.measurements.lineHeight,
|
||||
width: this.getLineNumberGutterWidth(),
|
||||
lineHeight: this.getLineHeight(),
|
||||
startRow, endRow, rowsPerTile, maxLineNumberDigits,
|
||||
bufferRows, lineNumberDecorations, softWrappedFlags,
|
||||
foldableFlags
|
||||
@ -301,6 +273,31 @@ class TextEditorComponent {
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
renderContent () {
|
||||
let children
|
||||
let style = {
|
||||
@ -309,15 +306,13 @@ class TextEditorComponent {
|
||||
backgroundColor: 'inherit'
|
||||
}
|
||||
if (this.measurements) {
|
||||
const contentWidth = this.getContentWidth()
|
||||
const scrollHeight = this.getScrollHeight()
|
||||
const width = contentWidth + 'px'
|
||||
const height = scrollHeight + 'px'
|
||||
style.width = width
|
||||
style.height = height
|
||||
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(width, height),
|
||||
this.renderLineTiles(width, height),
|
||||
this.renderCursorsAndInput(),
|
||||
this.renderLineTiles(),
|
||||
this.renderPlaceholderText()
|
||||
]
|
||||
} else {
|
||||
@ -339,7 +334,7 @@ class TextEditorComponent {
|
||||
)
|
||||
}
|
||||
|
||||
renderLineTiles (width, height) {
|
||||
renderLineTiles () {
|
||||
if (!this.measurements) return []
|
||||
|
||||
const {lineNodesByScreenLineId, textNodesByScreenLineId} = this
|
||||
@ -347,8 +342,8 @@ class TextEditorComponent {
|
||||
const startRow = this.getRenderedStartRow()
|
||||
const endRow = this.getRenderedEndRow()
|
||||
const rowsPerTile = this.getRowsPerTile()
|
||||
const tileHeight = this.measurements.lineHeight * rowsPerTile
|
||||
const tileWidth = this.getContentWidth()
|
||||
const tileHeight = this.getLineHeight() * rowsPerTile
|
||||
const tileWidth = this.getScrollWidth()
|
||||
|
||||
const displayLayer = this.props.model.displayLayer
|
||||
const tileNodes = new Array(this.getRenderedTileCount())
|
||||
@ -368,7 +363,7 @@ class TextEditorComponent {
|
||||
height: tileHeight,
|
||||
width: tileWidth,
|
||||
top: this.topPixelPositionForRow(tileStartRow),
|
||||
lineHeight: this.measurements.lineHeight,
|
||||
lineHeight: this.getLineHeight(),
|
||||
screenLines: this.renderedScreenLines.slice(tileStartRow - startRow, tileEndRow - startRow),
|
||||
lineDecorations,
|
||||
highlightDecorations,
|
||||
@ -395,14 +390,16 @@ class TextEditorComponent {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
contain: 'strict',
|
||||
width, height,
|
||||
overflow: 'hidden',
|
||||
width: this.getScrollWidth() + 'px',
|
||||
height: this.getScrollHeight() + 'px',
|
||||
backgroundColor: 'inherit'
|
||||
}
|
||||
}, tileNodes)
|
||||
}
|
||||
|
||||
renderCursorsAndInput (width, height) {
|
||||
const cursorHeight = this.measurements.lineHeight + 'px'
|
||||
renderCursorsAndInput () {
|
||||
const cursorHeight = this.getLineHeight() + 'px'
|
||||
|
||||
const children = [this.renderHiddenInput()]
|
||||
|
||||
@ -425,7 +422,8 @@ class TextEditorComponent {
|
||||
position: 'absolute',
|
||||
contain: 'strict',
|
||||
zIndex: 1,
|
||||
width, height
|
||||
width: this.getScrollWidth() + 'px',
|
||||
height: this.getScrollHeight() + 'px'
|
||||
}
|
||||
}, children)
|
||||
}
|
||||
@ -470,7 +468,7 @@ class TextEditorComponent {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
width: '1px',
|
||||
height: this.measurements.lineHeight + 'px',
|
||||
height: this.getLineHeight() + 'px',
|
||||
top: top + 'px',
|
||||
left: left + 'px',
|
||||
opacity: 0,
|
||||
@ -646,7 +644,7 @@ class TextEditorComponent {
|
||||
updateCursorsToRender () {
|
||||
this.decorationsToRender.cursors.length = 0
|
||||
|
||||
const height = this.measurements.lineHeight + 'px'
|
||||
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
|
||||
@ -765,15 +763,20 @@ class TextEditorComponent {
|
||||
}
|
||||
}
|
||||
|
||||
didScroll () {
|
||||
if (this.measureScrollPosition(true)) {
|
||||
this.updateSync()
|
||||
}
|
||||
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.measureEditorDimensions()) {
|
||||
this.measureClientDimensions()
|
||||
if (this.measureClientContainerDimensions()) {
|
||||
this.scheduleUpdate()
|
||||
}
|
||||
}
|
||||
@ -1023,10 +1026,10 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
autoscrollOnMouseDrag ({clientX, clientY}, verticalOnly = false) {
|
||||
let {top, bottom, left, right} = this.refs.scroller.getBoundingClientRect()
|
||||
let {top, bottom, left, right} = this.refs.scrollContainer.getBoundingClientRect()
|
||||
top += MOUSE_DRAG_AUTOSCROLL_MARGIN
|
||||
bottom -= MOUSE_DRAG_AUTOSCROLL_MARGIN
|
||||
left += this.getGutterContainerWidth() + MOUSE_DRAG_AUTOSCROLL_MARGIN
|
||||
left += MOUSE_DRAG_AUTOSCROLL_MARGIN
|
||||
right -= MOUSE_DRAG_AUTOSCROLL_MARGIN
|
||||
|
||||
let yDelta, yDirection
|
||||
@ -1050,31 +1053,21 @@ class TextEditorComponent {
|
||||
let scrolled = false
|
||||
if (yDelta != null) {
|
||||
const scaledDelta = scaleMouseDragAutoscrollDelta(yDelta) * yDirection
|
||||
const newScrollTop = this.constrainScrollTop(this.measurements.scrollTop + scaledDelta)
|
||||
if (newScrollTop !== this.measurements.scrollTop) {
|
||||
this.measurements.scrollTop = newScrollTop
|
||||
this.refs.scroller.scrollTop = newScrollTop
|
||||
scrolled = true
|
||||
}
|
||||
scrolled = this.setScrollTop(this.getScrollTop() + scaledDelta)
|
||||
}
|
||||
|
||||
if (!verticalOnly && xDelta != null) {
|
||||
const scaledDelta = scaleMouseDragAutoscrollDelta(xDelta) * xDirection
|
||||
const newScrollLeft = this.constrainScrollLeft(this.measurements.scrollLeft + scaledDelta)
|
||||
if (newScrollLeft !== this.measurements.scrollLeft) {
|
||||
this.measurements.scrollLeft = newScrollLeft
|
||||
this.refs.scroller.scrollLeft = newScrollLeft
|
||||
scrolled = true
|
||||
}
|
||||
scrolled = this.setScrollLeft(this.getScrollLeft() + scaledDelta)
|
||||
}
|
||||
|
||||
if (scrolled) this.updateSync()
|
||||
}
|
||||
|
||||
screenPositionForMouseEvent ({clientX, clientY}) {
|
||||
const scrollerRect = this.refs.scroller.getBoundingClientRect()
|
||||
clientX = Math.min(scrollerRect.right, Math.max(scrollerRect.left, clientX))
|
||||
clientY = Math.min(scrollerRect.bottom, Math.max(scrollerRect.top, 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,
|
||||
@ -1087,12 +1080,12 @@ class TextEditorComponent {
|
||||
this.scheduleUpdate()
|
||||
}
|
||||
|
||||
initiateAutoscroll () {
|
||||
autoscrollVertically () {
|
||||
const {screenRange, options} = this.pendingAutoscroll
|
||||
|
||||
const screenRangeTop = this.pixelTopForRow(screenRange.start.row)
|
||||
const screenRangeBottom = this.pixelTopForRow(screenRange.end.row) + this.measurements.lineHeight
|
||||
const verticalScrollMargin = this.getVerticalScrollMargin()
|
||||
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)
|
||||
@ -1109,43 +1102,27 @@ class TextEditorComponent {
|
||||
desiredScrollBottom = screenRangeBottom + verticalScrollMargin
|
||||
}
|
||||
|
||||
if (desiredScrollTop != null) {
|
||||
desiredScrollTop = this.constrainScrollTop(desiredScrollTop)
|
||||
}
|
||||
|
||||
if (desiredScrollBottom != null) {
|
||||
desiredScrollBottom = this.constrainScrollTop(desiredScrollBottom - this.getClientHeight()) + this.getClientHeight()
|
||||
}
|
||||
|
||||
if (!options || options.reversed !== false) {
|
||||
if (desiredScrollBottom > this.getScrollBottom()) {
|
||||
this.autoscrollTop = desiredScrollBottom - this.measurements.clientHeight
|
||||
this.measurements.scrollTop = this.autoscrollTop
|
||||
return true
|
||||
return this.setScrollBottom(desiredScrollBottom, true)
|
||||
}
|
||||
if (desiredScrollTop < this.getScrollTop()) {
|
||||
this.autoscrollTop = desiredScrollTop
|
||||
this.measurements.scrollTop = this.autoscrollTop
|
||||
return true
|
||||
return this.setScrollTop(desiredScrollTop, true)
|
||||
}
|
||||
} else {
|
||||
if (desiredScrollTop < this.getScrollTop()) {
|
||||
this.autoscrollTop = desiredScrollTop
|
||||
this.measurements.scrollTop = this.autoscrollTop
|
||||
return true
|
||||
return this.setScrollTop(desiredScrollTop, true)
|
||||
}
|
||||
if (desiredScrollBottom > this.getScrollBottom()) {
|
||||
this.autoscrollTop = desiredScrollBottom - this.measurements.clientHeight
|
||||
this.measurements.scrollTop = this.autoscrollTop
|
||||
return true
|
||||
return this.setScrollBottom(desiredScrollBottom, true)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
finalizeAutoscroll () {
|
||||
const horizontalScrollMargin = this.getHorizontalScrollMargin()
|
||||
autoscrollHorizontally () {
|
||||
const horizontalScrollMargin = this.getHorizontalAutoscrollMargin()
|
||||
|
||||
const {screenRange, options} = this.pendingAutoscroll
|
||||
const gutterContainerWidth = this.getGutterContainerWidth()
|
||||
@ -1154,121 +1131,70 @@ class TextEditorComponent {
|
||||
const desiredScrollLeft = Math.max(0, left - horizontalScrollMargin - gutterContainerWidth)
|
||||
const desiredScrollRight = Math.min(this.getScrollWidth(), right + horizontalScrollMargin)
|
||||
|
||||
let autoscrollLeft
|
||||
if (!options || options.reversed !== false) {
|
||||
if (desiredScrollRight > this.getScrollRight()) {
|
||||
autoscrollLeft = desiredScrollRight - this.getClientWidth()
|
||||
this.measurements.scrollLeft = autoscrollLeft
|
||||
this.setScrollRight(desiredScrollRight, true)
|
||||
}
|
||||
if (desiredScrollLeft < this.getScrollLeft()) {
|
||||
autoscrollLeft = desiredScrollLeft
|
||||
this.measurements.scrollLeft = autoscrollLeft
|
||||
this.setScrollLeft(desiredScrollLeft, true)
|
||||
}
|
||||
} else {
|
||||
if (desiredScrollLeft < this.getScrollLeft()) {
|
||||
autoscrollLeft = desiredScrollLeft
|
||||
this.measurements.scrollLeft = autoscrollLeft
|
||||
this.setScrollLeft(desiredScrollLeft, true)
|
||||
}
|
||||
if (desiredScrollRight > this.getScrollRight()) {
|
||||
autoscrollLeft = desiredScrollRight - this.getClientWidth()
|
||||
this.measurements.scrollLeft = autoscrollLeft
|
||||
this.setScrollRight(desiredScrollRight, true)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.autoscrollTop != null) {
|
||||
this.refs.scroller.scrollTop = this.autoscrollTop
|
||||
this.autoscrollTop = null
|
||||
}
|
||||
|
||||
if (autoscrollLeft != null) {
|
||||
this.refs.scroller.scrollLeft = autoscrollLeft
|
||||
}
|
||||
|
||||
this.pendingAutoscroll = null
|
||||
}
|
||||
|
||||
getVerticalScrollMargin () {
|
||||
const {clientHeight, lineHeight} = this.measurements
|
||||
getVerticalAutoscrollMargin () {
|
||||
const maxMarginInLines = Math.floor(
|
||||
(this.getScrollContainerClientHeight() / this.getLineHeight() - 1) / 2
|
||||
)
|
||||
const marginInLines = Math.min(
|
||||
this.props.model.verticalScrollMargin,
|
||||
Math.floor(((clientHeight / lineHeight) - 1) / 2)
|
||||
maxMarginInLines
|
||||
)
|
||||
return marginInLines * lineHeight
|
||||
return marginInLines * this.getLineHeight()
|
||||
}
|
||||
|
||||
getHorizontalScrollMargin () {
|
||||
const {clientWidth, baseCharacterWidth} = this.measurements
|
||||
const contentClientWidth = clientWidth - this.getGutterContainerWidth()
|
||||
getHorizontalAutoscrollMargin () {
|
||||
const maxMarginInBaseCharacters = Math.floor(
|
||||
(this.getScrollContainerClientWidth() / this.getBaseCharacterWidth() - 1) / 2
|
||||
)
|
||||
const marginInBaseCharacters = Math.min(
|
||||
this.props.model.horizontalScrollMargin,
|
||||
Math.floor(((contentClientWidth / baseCharacterWidth) - 1) / 2)
|
||||
)
|
||||
return marginInBaseCharacters * baseCharacterWidth
|
||||
}
|
||||
|
||||
constrainScrollTop (desiredScrollTop) {
|
||||
return Math.max(
|
||||
0, Math.min(desiredScrollTop, this.getScrollHeight() - this.getClientHeight())
|
||||
)
|
||||
}
|
||||
|
||||
constrainScrollLeft (desiredScrollLeft) {
|
||||
return Math.max(
|
||||
0, Math.min(desiredScrollLeft, this.getScrollWidth() - this.getClientWidth())
|
||||
maxMarginInBaseCharacters
|
||||
)
|
||||
return marginInBaseCharacters * this.getBaseCharacterWidth()
|
||||
}
|
||||
|
||||
performInitialMeasurements () {
|
||||
this.measurements = {}
|
||||
this.measureGutterDimensions()
|
||||
this.measureEditorDimensions()
|
||||
this.measureClientDimensions()
|
||||
this.measureScrollPosition()
|
||||
this.measureCharacterDimensions()
|
||||
this.measureGutterDimensions()
|
||||
this.measureClientContainerDimensions()
|
||||
}
|
||||
|
||||
measureEditorDimensions () {
|
||||
measureClientContainerDimensions () {
|
||||
if (!this.measurements) return false
|
||||
|
||||
let dimensionsChanged = false
|
||||
const scrollerHeight = this.refs.scroller.offsetHeight
|
||||
const scrollerWidth = this.refs.scroller.offsetWidth
|
||||
if (scrollerHeight !== this.measurements.scrollerHeight) {
|
||||
this.measurements.scrollerHeight = scrollerHeight
|
||||
const clientContainerHeight = this.refs.clientContainer.offsetHeight
|
||||
const clientContainerWidth = this.refs.clientContainer.offsetWidth
|
||||
if (clientContainerHeight !== this.measurements.clientContainerHeight) {
|
||||
this.measurements.clientContainerHeight = clientContainerHeight
|
||||
dimensionsChanged = true
|
||||
}
|
||||
if (scrollerWidth !== this.measurements.scrollerWidth) {
|
||||
this.measurements.scrollerWidth = scrollerWidth
|
||||
if (clientContainerWidth !== this.measurements.clientContainerWidth) {
|
||||
this.measurements.clientContainerWidth = clientContainerWidth
|
||||
this.props.model.setEditorWidthInChars(this.getScrollContainerWidth() / this.getBaseCharacterWidth())
|
||||
dimensionsChanged = true
|
||||
}
|
||||
return dimensionsChanged
|
||||
}
|
||||
|
||||
measureScrollPosition () {
|
||||
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 () {
|
||||
const {clientHeight, clientWidth} = this.refs.scroller
|
||||
if (clientHeight !== this.measurements.clientHeight) {
|
||||
this.measurements.clientHeight = clientHeight
|
||||
}
|
||||
if (clientWidth !== this.measurements.clientWidth) {
|
||||
this.measurements.clientWidth = clientWidth
|
||||
this.props.model.setWidth(clientWidth - this.getGutterContainerWidth(), true)
|
||||
}
|
||||
}
|
||||
|
||||
measureCharacterDimensions () {
|
||||
this.measurements.lineHeight = this.refs.characterMeasurementLine.getBoundingClientRect().height
|
||||
this.measurements.baseCharacterWidth = this.refs.normalWidthCharacterSpan.getBoundingClientRect().width
|
||||
@ -1383,7 +1309,7 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
pixelTopForRow (row) {
|
||||
return row * this.measurements.lineHeight
|
||||
return row * this.getLineHeight()
|
||||
}
|
||||
|
||||
pixelLeftForRowAndColumn (row, column) {
|
||||
@ -1478,73 +1404,91 @@ class TextEditorComponent {
|
||||
return this.element.offsetWidth > 0 || this.element.offsetHeight > 0
|
||||
}
|
||||
|
||||
getLineHeight () {
|
||||
return this.measurements.lineHeight
|
||||
}
|
||||
|
||||
getBaseCharacterWidth () {
|
||||
return this.measurements ? this.measurements.baseCharacterWidth : null
|
||||
}
|
||||
|
||||
getScrollTop () {
|
||||
if (this.measurements != null) {
|
||||
return this.measurements.scrollTop
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
getScrollBottom () {
|
||||
return this.measurements
|
||||
? this.measurements.scrollTop + this.measurements.clientHeight
|
||||
: null
|
||||
getScrollContainerHeight () {
|
||||
if (this.props.model.getAutoHeight()) {
|
||||
return this.getScrollHeight()
|
||||
} else {
|
||||
return this.getClientContainerHeight()
|
||||
}
|
||||
}
|
||||
|
||||
getScrollLeft () {
|
||||
return this.measurements ? this.measurements.scrollLeft : null
|
||||
getScrollContainerHeightInLines () {
|
||||
return Math.ceil(this.getScrollContainerHeight() / this.getLineHeight())
|
||||
}
|
||||
|
||||
getScrollRight () {
|
||||
return this.measurements
|
||||
? this.measurements.scrollLeft + this.measurements.clientWidth
|
||||
: null
|
||||
getScrollContainerClientWidth () {
|
||||
return this.getScrollContainerWidth()
|
||||
}
|
||||
|
||||
getScrollContainerClientHeight () {
|
||||
return this.getScrollContainerHeight()
|
||||
}
|
||||
|
||||
getScrollHeight () {
|
||||
const {model} = this.props
|
||||
const contentHeight = model.getApproximateScreenLineCount() * this.measurements.lineHeight
|
||||
if (model.getScrollPastEnd()) {
|
||||
const extraScrollHeight = Math.max(
|
||||
3 * this.measurements.lineHeight,
|
||||
this.getClientHeight() - 3 * this.measurements.lineHeight
|
||||
if (this.props.model.getScrollPastEnd()) {
|
||||
return this.getContentHeight() + Math.max(
|
||||
3 * this.getLineHeight(),
|
||||
this.getScrollContainerClientHeight() - (3 * this.getLineHeight())
|
||||
)
|
||||
return contentHeight + extraScrollHeight
|
||||
} else {
|
||||
return contentHeight
|
||||
return this.getContentHeight()
|
||||
}
|
||||
}
|
||||
|
||||
getScrollWidth () {
|
||||
return this.getContentWidth() + this.getGutterContainerWidth()
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
getClientHeight () {
|
||||
return this.measurements.clientHeight
|
||||
}
|
||||
|
||||
getClientWidth () {
|
||||
return this.measurements.clientWidth
|
||||
}
|
||||
|
||||
getGutterContainerWidth () {
|
||||
return this.measurements.lineNumberGutterWidth
|
||||
getContentHeight () {
|
||||
return this.props.model.getApproximateScreenLineCount() * this.getLineHeight()
|
||||
}
|
||||
|
||||
getContentWidth () {
|
||||
if (this.props.model.isSoftWrapped()) {
|
||||
return this.getClientWidth() - this.getGutterContainerWidth()
|
||||
} else if (this.props.model.getAutoWidth()) {
|
||||
return Math.round(this.measurements.longestLineWidth + this.measurements.baseCharacterWidth)
|
||||
} else {
|
||||
return Math.max(
|
||||
Math.round(this.measurements.longestLineWidth + this.measurements.baseCharacterWidth),
|
||||
this.measurements.scrollerWidth - this.getGutterContainerWidth()
|
||||
)
|
||||
}
|
||||
return Math.round(this.getLongestLineWidth() + this.getBaseCharacterWidth())
|
||||
}
|
||||
|
||||
getGutterContainerWidth () {
|
||||
return this.getLineNumberGutterWidth()
|
||||
}
|
||||
|
||||
getLineNumberGutterWidth () {
|
||||
return this.measurements.lineNumberGutterWidth
|
||||
}
|
||||
|
||||
getRowsPerTile () {
|
||||
@ -1583,16 +1527,13 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
getFirstVisibleRow () {
|
||||
const scrollTop = this.getScrollTop()
|
||||
const lineHeight = this.measurements.lineHeight
|
||||
return Math.floor(scrollTop / lineHeight)
|
||||
return Math.floor(this.getScrollTop() / this.getLineHeight())
|
||||
}
|
||||
|
||||
getLastVisibleRow () {
|
||||
const {scrollerHeight, lineHeight} = this.measurements
|
||||
return Math.min(
|
||||
this.props.model.getApproximateScreenLineCount() - 1,
|
||||
this.getFirstVisibleRow() + Math.ceil(scrollerHeight / lineHeight)
|
||||
this.getFirstVisibleRow() + this.getScrollContainerHeightInLines()
|
||||
)
|
||||
}
|
||||
|
||||
@ -1600,6 +1541,63 @@ class TextEditorComponent {
|
||||
return Math.floor((this.getLastVisibleRow() - this.getFirstVisibleRow()) / this.getRowsPerTile()) + 2
|
||||
}
|
||||
|
||||
|
||||
getScrollTop () {
|
||||
this.scrollTop = Math.min(this.getMaxScrollTop(), this.scrollTop)
|
||||
return this.scrollTop
|
||||
}
|
||||
|
||||
setScrollTop (scrollTop, suppressUpdate = false) {
|
||||
scrollTop = Math.round(Math.max(0, Math.min(this.getMaxScrollTop(), scrollTop)))
|
||||
if (scrollTop !== this.scrollTop) {
|
||||
this.scrollTop = scrollTop
|
||||
if (!suppressUpdate) this.scheduleUpdate()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
getMaxScrollTop () {
|
||||
return Math.max(0, this.getScrollHeight() - this.getScrollContainerClientHeight())
|
||||
}
|
||||
|
||||
getScrollBottom () {
|
||||
return this.getScrollTop() + this.getScrollContainerClientHeight()
|
||||
}
|
||||
|
||||
setScrollBottom (scrollBottom, suppressUpdate = false) {
|
||||
return this.setScrollTop(scrollBottom - this.getScrollContainerClientHeight(), suppressUpdate)
|
||||
}
|
||||
|
||||
getScrollLeft () {
|
||||
// this.scrollLeft = Math.min(this.getMaxScrollLeft(), this.scrollLeft)
|
||||
return this.scrollLeft
|
||||
}
|
||||
|
||||
setScrollLeft (scrollLeft, suppressUpdate = false) {
|
||||
scrollLeft = Math.round(Math.max(0, Math.min(this.getMaxScrollLeft(), scrollLeft)))
|
||||
if (scrollLeft !== this.scrollLeft) {
|
||||
this.scrollLeft = scrollLeft
|
||||
if (!suppressUpdate) this.scheduleUpdate()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
getMaxScrollLeft () {
|
||||
return Math.max(0, this.getScrollWidth() - this.getScrollContainerClientWidth())
|
||||
}
|
||||
|
||||
getScrollRight () {
|
||||
return this.getScrollLeft() + this.getScrollContainerClientWidth()
|
||||
}
|
||||
|
||||
setScrollRight (scrollRight, suppressUpdate = false) {
|
||||
return this.setScrollLeft(scrollRight - this.getScrollContainerClientWidth(), suppressUpdate)
|
||||
}
|
||||
|
||||
// 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 () {
|
||||
@ -1608,7 +1606,7 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
topPixelPositionForRow (row) {
|
||||
return row * this.measurements.lineHeight
|
||||
return row * this.getLineHeight()
|
||||
}
|
||||
|
||||
getNextUpdatePromise () {
|
||||
@ -1829,7 +1827,7 @@ class LinesTileComponent {
|
||||
position: 'absolute',
|
||||
contain: 'strict',
|
||||
height: height + 'px',
|
||||
width: width + 'px',
|
||||
width: width + 'px'
|
||||
}
|
||||
}, children)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ class TextEditorElement extends HTMLElement {
|
||||
}
|
||||
|
||||
getModel () {
|
||||
return this.getComponent().getModel()
|
||||
return this.getComponent().props.model
|
||||
}
|
||||
|
||||
setModel (model) {
|
||||
|
Loading…
Reference in New Issue
Block a user