diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 3d64f7be6..7ed97f0d0 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -47,14 +47,6 @@ describe "DisplayBuffer", -> buffer.insert([0, 0], oneHundredLines) expect(displayBuffer.getLineCount()).toBe 100 + originalLineCount - it "reassigns the scrollTop if it exceeds the max possible value after lines are removed", -> - displayBuffer.setHeight(50) - displayBuffer.setLineHeightInPixels(10) - displayBuffer.setScrollTop(80) - - buffer.delete([[8, 0], [10, 0]]) - expect(displayBuffer.getScrollTop()).toBe 60 - it "updates the display buffer prior to invoking change handlers registered on the buffer", -> buffer.onDidChange -> expect(displayBuffer2.tokenizedLineForScreenRow(0).text).toBe "testing" displayBuffer2 = new DisplayBuffer({buffer, tabLength}) @@ -297,18 +289,6 @@ describe "DisplayBuffer", -> displayBuffer.setEditorWidthInChars(-1) expect(displayBuffer.editorWidthInChars).not.toBe -1 - it "sets ::scrollLeft to 0 and keeps it there when soft wrapping is enabled", -> - displayBuffer.setDefaultCharWidth(10) - displayBuffer.setWidth(85) - - displayBuffer.setSoftWrapped(false) - displayBuffer.setScrollLeft(Infinity) - expect(displayBuffer.getScrollLeft()).toBeGreaterThan 0 - displayBuffer.setSoftWrapped(true) - expect(displayBuffer.getScrollLeft()).toBe 0 - displayBuffer.setScrollLeft(10) - expect(displayBuffer.getScrollLeft()).toBe 0 - describe "primitive folding", -> beforeEach -> displayBuffer.destroy() @@ -738,21 +718,6 @@ describe "DisplayBuffer", -> expect(displayBuffer.clipScreenPosition([0, 1], clip: 'forward')).toEqual [0, tabLength] expect(displayBuffer.clipScreenPosition([0, tabLength], clip: 'forward')).toEqual [0, tabLength] - describe "::screenPositionForPixelPosition(pixelPosition)", -> - it "clips pixel positions above buffer start", -> - displayBuffer.setLineHeightInPixels(20) - expect(displayBuffer.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0] - expect(displayBuffer.screenPositionForPixelPosition(top: -Infinity, left: Infinity)).toEqual [0, 0] - expect(displayBuffer.screenPositionForPixelPosition(top: -1, left: Infinity)).toEqual [0, 0] - expect(displayBuffer.screenPositionForPixelPosition(top: 0, left: Infinity)).toEqual [0, 29] - - it "clips pixel positions below buffer end", -> - displayBuffer.setLineHeightInPixels(20) - expect(displayBuffer.screenPositionForPixelPosition(top: Infinity, left: -Infinity)).toEqual [12, 2] - expect(displayBuffer.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2] - expect(displayBuffer.screenPositionForPixelPosition(top: displayBuffer.getHeight() + 1, left: 0)).toEqual [12, 2] - expect(displayBuffer.screenPositionForPixelPosition(top: displayBuffer.getHeight() - 1, left: 0)).toEqual [12, 0] - describe "::screenPositionForBufferPosition(bufferPosition, options)", -> it "clips the specified buffer position", -> expect(displayBuffer.screenPositionForBufferPosition([0, 2])).toEqual [0, 2] @@ -1186,20 +1151,6 @@ describe "DisplayBuffer", -> expect(marker1.getProperties()).toEqual a: 1, b: 2 expect(marker2.getProperties()).toEqual a: 1, b: 3 - describe "Marker::getPixelRange()", -> - it "returns the start and end positions of the marker based on the line height and character widths assigned to the DisplayBuffer", -> - marker = displayBuffer.markScreenRange([[5, 10], [6, 4]]) - - displayBuffer.setLineHeightInPixels(20) - displayBuffer.setDefaultCharWidth(10) - - for char in ['r', 'e', 't', 'u', 'r', 'n'] - displayBuffer.setScopedCharWidth(["source.js", "keyword.control.js"], char, 11) - - {start, end} = marker.getPixelRange() - expect(start.top).toBe 5 * 20 - expect(start.left).toBe (4 * 10) + (6 * 11) - describe 'when there are multiple DisplayBuffers for a buffer', -> describe 'when a marker is created', -> it 'the second display buffer will not emit a marker-created event when the marker has been deleted in the first marker-created event', -> @@ -1254,157 +1205,18 @@ describe "DisplayBuffer", -> expect(displayBuffer.getDecorations(class: 'two').length).toEqual 0 expect(displayBuffer.getDecorations(class: 'one').length).toEqual 1 - describe "::setScrollTop", -> - beforeEach -> - displayBuffer.setLineHeightInPixels(10) - - it "disallows negative values", -> - displayBuffer.setHeight(displayBuffer.getScrollHeight() + 100) - expect(displayBuffer.setScrollTop(-10)).toBe 0 - expect(displayBuffer.getScrollTop()).toBe 0 - - it "disallows values that would make ::getScrollBottom() exceed ::getScrollHeight()", -> - displayBuffer.setHeight(50) - maxScrollTop = displayBuffer.getScrollHeight() - displayBuffer.getHeight() - - expect(displayBuffer.setScrollTop(maxScrollTop)).toBe maxScrollTop - expect(displayBuffer.getScrollTop()).toBe maxScrollTop - - expect(displayBuffer.setScrollTop(maxScrollTop + 50)).toBe maxScrollTop - expect(displayBuffer.getScrollTop()).toBe maxScrollTop - - describe "editor.scrollPastEnd", -> - describe "when editor.scrollPastEnd is false", -> - beforeEach -> - atom.config.set("editor.scrollPastEnd", false) - displayBuffer.setLineHeightInPixels(10) - - it "does not add the height of the view to the scroll height", -> - lineHeight = displayBuffer.getLineHeightInPixels() - originalScrollHeight = displayBuffer.getScrollHeight() - displayBuffer.setHeight(50) - - expect(displayBuffer.getScrollHeight()).toBe originalScrollHeight - - describe "when editor.scrollPastEnd is true", -> - beforeEach -> - atom.config.set("editor.scrollPastEnd", true) - displayBuffer.setLineHeightInPixels(10) - - it "adds the height of the view to the scroll height", -> - lineHeight = displayBuffer.getLineHeightInPixels() - originalScrollHeight = displayBuffer.getScrollHeight() - displayBuffer.setHeight(50) - - expect(displayBuffer.getScrollHeight()).toEqual(originalScrollHeight + displayBuffer.height - (lineHeight * 3)) - - describe "::setScrollLeft", -> - beforeEach -> - displayBuffer.setLineHeightInPixels(10) - displayBuffer.setDefaultCharWidth(10) - - it "disallows negative values", -> - displayBuffer.setWidth(displayBuffer.getScrollWidth() + 100) - expect(displayBuffer.setScrollLeft(-10)).toBe 0 - expect(displayBuffer.getScrollLeft()).toBe 0 - - it "disallows values that would make ::getScrollRight() exceed ::getScrollWidth()", -> - displayBuffer.setWidth(50) - maxScrollLeft = displayBuffer.getScrollWidth() - displayBuffer.getWidth() - - expect(displayBuffer.setScrollLeft(maxScrollLeft)).toBe maxScrollLeft - expect(displayBuffer.getScrollLeft()).toBe maxScrollLeft - - expect(displayBuffer.setScrollLeft(maxScrollLeft + 50)).toBe maxScrollLeft - expect(displayBuffer.getScrollLeft()).toBe maxScrollLeft - describe "::scrollToScreenPosition(position, [options])", -> - beforeEach -> - displayBuffer.setLineHeightInPixels(10) - displayBuffer.setDefaultCharWidth(10) - displayBuffer.setHorizontalScrollbarHeight(0) - displayBuffer.setHeight(50) - displayBuffer.setWidth(150) - - it "sets the scroll top and scroll left so the given screen position is in view", -> - displayBuffer.scrollToScreenPosition([8, 20]) - expect(displayBuffer.getScrollBottom()).toBe (9 + displayBuffer.getVerticalScrollMargin()) * 10 - expect(displayBuffer.getScrollRight()).toBe (20 + displayBuffer.getHorizontalScrollMargin()) * 10 + it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", -> + scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll") + displayBuffer.onDidRequestAutoscroll(scrollSpy) displayBuffer.scrollToScreenPosition([8, 20]) - expect(displayBuffer.getScrollBottom()).toBe (9 + displayBuffer.getVerticalScrollMargin()) * 10 - expect(displayBuffer.getScrollRight()).toBe (20 + displayBuffer.getHorizontalScrollMargin()) * 10 + displayBuffer.scrollToScreenPosition([8, 20], center: true) + displayBuffer.scrollToScreenPosition([8, 20], center: false, reversed: true) - describe "when the 'center' option is true", -> - it "vertically scrolls to center the given position vertically", -> - displayBuffer.scrollToScreenPosition([8, 20], center: true) - expect(displayBuffer.getScrollTop()).toBe (8 * 10) + 5 - (50 / 2) - expect(displayBuffer.getScrollRight()).toBe (20 + displayBuffer.getHorizontalScrollMargin()) * 10 - - it "does not scroll vertically if the position is already in view", -> - displayBuffer.scrollToScreenPosition([4, 20], center: true) - expect(displayBuffer.getScrollTop()).toBe 0 - - describe "scroll width", -> - cursorWidth = 1 - beforeEach -> - displayBuffer.setDefaultCharWidth(10) - - it "recomputes the scroll width when the default character width changes", -> - expect(displayBuffer.getScrollWidth()).toBe 10 * 65 + cursorWidth - - displayBuffer.setDefaultCharWidth(12) - expect(displayBuffer.getScrollWidth()).toBe 12 * 65 + cursorWidth - - it "recomputes the scroll width when the max line length changes", -> - buffer.insert([6, 12], ' ') - expect(displayBuffer.getScrollWidth()).toBe 10 * 66 + cursorWidth - - buffer.delete([[6, 10], [6, 12]], ' ') - expect(displayBuffer.getScrollWidth()).toBe 10 * 64 + cursorWidth - - it "recomputes the scroll width when the scoped character widths change", -> - operatorWidth = 20 - displayBuffer.setScopedCharWidth(['source.js', 'keyword.operator.js'], '<', operatorWidth) - expect(displayBuffer.getScrollWidth()).toBe 10 * 64 + operatorWidth + cursorWidth - - it "recomputes the scroll width when the scoped character widths change in a batch", -> - operatorWidth = 20 - - displayBuffer.onDidChangeCharacterWidths changedSpy = jasmine.createSpy() - - displayBuffer.batchCharacterMeasurement -> - displayBuffer.setScopedCharWidth(['source.js', 'keyword.operator.js'], '<', operatorWidth) - displayBuffer.setScopedCharWidth(['source.js', 'keyword.operator.js'], '?', operatorWidth) - - expect(displayBuffer.getScrollWidth()).toBe 10 * 63 + operatorWidth * 2 + cursorWidth - expect(changedSpy.callCount).toBe 1 - - describe "::getVisibleRowRange()", -> - beforeEach -> - displayBuffer.setLineHeightInPixels(10) - displayBuffer.setHeight(100) - - it "returns the first and the last visible rows", -> - displayBuffer.setScrollTop(0) - - expect(displayBuffer.getVisibleRowRange()).toEqual [0, 9] - - it "includes partially visible rows in the range", -> - displayBuffer.setScrollTop(5) - - expect(displayBuffer.getVisibleRowRange()).toEqual [0, 10] - - it "returns an empty range when lineHeight is 0", -> - displayBuffer.setLineHeightInPixels(0) - - expect(displayBuffer.getVisibleRowRange()).toEqual [0, 0] - - it "ends at last buffer row even if there's more space available", -> - displayBuffer.setHeight(150) - displayBuffer.setScrollTop(60) - - expect(displayBuffer.getVisibleRowRange()).toEqual [0, 13] + expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {}) + expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: true}) + expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true}) describe "::decorateMarker", -> describe "when decorating gutters", -> diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 3bdfbc59a..27e9864aa 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -285,18 +285,18 @@ describe "TextEditorComponent", -> componentNode.style.width = gutterWidth + (30 * charWidth) + 'px' component.measureDimensions() nextAnimationFrame() - expect(editor.getScrollWidth()).toBeGreaterThan scrollViewNode.offsetWidth + expect(wrapperNode.getScrollWidth()).toBeGreaterThan scrollViewNode.offsetWidth # At the time of writing, using width: 100% to achieve the full-width # lines caused full-screen repaints after switching away from an editor # and back again Please ensure you don't cause a performance regression if # you change this behavior. - editorFullWidth = editor.getScrollWidth() + editor.getVerticalScrollbarWidth() + editorFullWidth = wrapperNode.getScrollWidth() + wrapperNode.getVerticalScrollbarWidth() for lineNode in lineNodes expect(lineNode.getBoundingClientRect().width).toBe(editorFullWidth) - componentNode.style.width = gutterWidth + editor.getScrollWidth() + 100 + 'px' + componentNode.style.width = gutterWidth + wrapperNode.getScrollWidth() + 100 + 'px' component.measureDimensions() nextAnimationFrame() scrollViewWidth = scrollViewNode.offsetWidth @@ -473,7 +473,7 @@ describe "TextEditorComponent", -> editor.setText "a line that wraps \n" editor.setSoftWrapped(true) nextAnimationFrame() - componentNode.style.width = 16 * charWidth + editor.getVerticalScrollbarWidth() + 'px' + componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() nextAnimationFrame() @@ -893,7 +893,7 @@ describe "TextEditorComponent", -> beforeEach -> editor.setSoftWrapped(true) nextAnimationFrame() - componentNode.style.width = 16 * charWidth + editor.getVerticalScrollbarWidth() + 'px' + componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() nextAnimationFrame() @@ -1672,8 +1672,8 @@ describe "TextEditorComponent", -> nextAnimationFrame() expect(editor.getCursorScreenPosition()).toEqual [0, 0] - editor.setScrollTop(3 * lineHeightInPixels) - editor.setScrollLeft(3 * charWidth) + wrapperNode.setScrollTop(3 * lineHeightInPixels) + wrapperNode.setScrollLeft(3 * charWidth) nextAnimationFrame() expect(inputNode.offsetTop).toBe 0 @@ -1688,8 +1688,8 @@ describe "TextEditorComponent", -> # In bounds and focused wrapperNode.focus() # updates via state change nextAnimationFrame() - expect(inputNode.offsetTop).toBe (5 * lineHeightInPixels) - editor.getScrollTop() - expect(inputNode.offsetLeft).toBe (4 * charWidth) - editor.getScrollLeft() + expect(inputNode.offsetTop).toBe (5 * lineHeightInPixels) - wrapperNode.getScrollTop() + expect(inputNode.offsetLeft).toBe (4 * charWidth) - wrapperNode.getScrollLeft() # In bounds, not focused inputNode.blur() # updates via state change @@ -1753,8 +1753,8 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - editor.setScrollTop(3.5 * lineHeightInPixels) - editor.setScrollLeft(2 * charWidth) + wrapperNode.setScrollTop(3.5 * lineHeightInPixels) + wrapperNode.setScrollLeft(2 * charWidth) nextAnimationFrame() linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]))) @@ -1871,33 +1871,33 @@ describe "TextEditorComponent", -> component.measureDimensions() nextAnimationFrame() - expect(editor.getScrollTop()).toBe(0) - expect(editor.getScrollLeft()).toBe(0) + expect(wrapperNode.getScrollTop()).toBe(0) + expect(wrapperNode.getScrollLeft()).toBe(0) linesNode.dispatchEvent(buildMouseEvent('mousedown', {clientX: 0, clientY: 0}, which: 1)) linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 50}, which: 1)) nextAnimationFrame() - expect(editor.getScrollTop()).toBe(0) - expect(editor.getScrollLeft()).toBeGreaterThan(0) + expect(wrapperNode.getScrollTop()).toBe(0) + expect(wrapperNode.getScrollLeft()).toBeGreaterThan(0) linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 100}, which: 1)) nextAnimationFrame() - expect(editor.getScrollTop()).toBeGreaterThan(0) + expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) - previousScrollTop = editor.getScrollTop() - previousScrollLeft = editor.getScrollLeft() + previousScrollTop = wrapperNode.getScrollTop() + previousScrollLeft = wrapperNode.getScrollLeft() linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 50}, which: 1)) nextAnimationFrame() - expect(editor.getScrollTop()).toBe(previousScrollTop) - expect(editor.getScrollLeft()).toBeLessThan(previousScrollLeft) + expect(wrapperNode.getScrollTop()).toBe(previousScrollTop) + expect(wrapperNode.getScrollLeft()).toBeLessThan(previousScrollLeft) linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 10}, which: 1)) nextAnimationFrame() - expect(editor.getScrollTop()).toBeLessThan(previousScrollTop) + expect(wrapperNode.getScrollTop()).toBeLessThan(previousScrollTop) it "stops selecting if the mouse is dragged into the dev tools", -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) @@ -1990,12 +1990,12 @@ describe "TextEditorComponent", -> nextAnimationFrame() expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [12, 2]] - maximalScrollTop = editor.getScrollTop() + maximalScrollTop = wrapperNode.getScrollTop() linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([9, 3]), which: 1)) nextAnimationFrame() expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [9, 4]] - expect(editor.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression) + expect(wrapperNode.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression) linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), which: 1)) @@ -2017,12 +2017,12 @@ describe "TextEditorComponent", -> nextAnimationFrame() expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [12, 2]] - maximalScrollTop = editor.getScrollTop() + maximalScrollTop = wrapperNode.getScrollTop() linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 4]), which: 1)) nextAnimationFrame() expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [8, 0]] - expect(editor.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression) + expect(wrapperNode.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression) linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), which: 1)) @@ -2117,23 +2117,23 @@ describe "TextEditorComponent", -> component.measureDimensions() nextAnimationFrame() - expect(editor.getScrollTop()).toBe 0 + expect(wrapperNode.getScrollTop()).toBe 0 gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) nextAnimationFrame() - expect(editor.getScrollTop()).toBeGreaterThan 0 - maxScrollTop = editor.getScrollTop() + expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 + maxScrollTop = wrapperNode.getScrollTop() gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(10))) nextAnimationFrame() - expect(editor.getScrollTop()).toBe maxScrollTop + expect(wrapperNode.getScrollTop()).toBe maxScrollTop gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) nextAnimationFrame() - expect(editor.getScrollTop()).toBeLessThan maxScrollTop + expect(wrapperNode.getScrollTop()).toBeLessThan maxScrollTop it "stops selecting if a textInput event occurs during the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) @@ -2242,7 +2242,7 @@ describe "TextEditorComponent", -> editor.setSoftWrapped(true) nextAnimationFrame() - componentNode.style.width = 21 * charWidth + editor.getVerticalScrollbarWidth() + 'px' + componentNode.style.width = 21 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() nextAnimationFrame() @@ -2414,7 +2414,7 @@ describe "TextEditorComponent", -> expect(verticalScrollbarNode.scrollTop).toBe 0 - editor.setScrollTop(10) + wrapperNode.setScrollTop(10) nextAnimationFrame() expect(verticalScrollbarNode.scrollTop).toBe 10 @@ -2432,7 +2432,7 @@ describe "TextEditorComponent", -> expect(horizontalScrollbarNode.scrollLeft).toBe 0 - editor.setScrollLeft(100) + wrapperNode.setScrollLeft(100) nextAnimationFrame() top = 0 @@ -2447,18 +2447,18 @@ describe "TextEditorComponent", -> component.measureDimensions() nextAnimationFrame() - expect(editor.getScrollLeft()).toBe 0 + expect(wrapperNode.getScrollLeft()).toBe 0 horizontalScrollbarNode.scrollLeft = 100 horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) nextAnimationFrame() - expect(editor.getScrollLeft()).toBe 100 + expect(wrapperNode.getScrollLeft()).toBe 100 it "does not obscure the last line with the horizontal scrollbar", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - editor.setScrollBottom(editor.getScrollHeight()) + wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) nextAnimationFrame() lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom @@ -2477,7 +2477,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 7 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - editor.setScrollLeft(Infinity) + wrapperNode.setScrollLeft(Infinity) nextAnimationFrame() rightOfLongestLine = component.lineNodeForScreenRow(6).querySelector('.line > span:last-child').getBoundingClientRect().right @@ -2568,7 +2568,7 @@ describe "TextEditorComponent", -> component.measureDimensions() nextAnimationFrame() - expect(horizontalScrollbarNode.scrollWidth).toBe editor.getScrollWidth() + expect(horizontalScrollbarNode.scrollWidth).toBe wrapperNode.getScrollWidth() expect(horizontalScrollbarNode.style.left).toBe '0px' describe "mousewheel events", -> @@ -2647,14 +2647,14 @@ describe "TextEditorComponent", -> expect(component.presenter.mouseWheelScreenRow).toBe null it "clears the mouseWheelScreenRow after a delay even if the event does not cause scrolling", -> - expect(editor.getScrollTop()).toBe 0 + expect(wrapperNode.getScrollTop()).toBe 0 lineNode = componentNode.querySelector('.line') wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 10) Object.defineProperty(wheelEvent, 'target', get: -> lineNode) componentNode.dispatchEvent(wheelEvent) - expect(editor.getScrollTop()).toBe 0 + expect(wrapperNode.getScrollTop()).toBe 0 expect(component.presenter.mouseWheelScreenRow).toBe 0 advanceClock(component.presenter.stoppedScrollingDelay) @@ -2699,36 +2699,36 @@ describe "TextEditorComponent", -> # try to scroll past the top, which is impossible componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 50)) - expect(editor.getScrollTop()).toBe 0 + expect(wrapperNode.getScrollTop()).toBe 0 expect(WheelEvent::preventDefault).not.toHaveBeenCalled() # scroll to the bottom in one huge event componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -3000)) nextAnimationFrame() - maxScrollTop = editor.getScrollTop() + maxScrollTop = wrapperNode.getScrollTop() expect(WheelEvent::preventDefault).toHaveBeenCalled() WheelEvent::preventDefault.reset() # try to scroll past the bottom, which is impossible componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -30)) - expect(editor.getScrollTop()).toBe maxScrollTop + expect(wrapperNode.getScrollTop()).toBe maxScrollTop expect(WheelEvent::preventDefault).not.toHaveBeenCalled() # try to scroll past the left side, which is impossible componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 50, wheelDeltaY: 0)) - expect(editor.getScrollLeft()).toBe 0 + expect(wrapperNode.getScrollLeft()).toBe 0 expect(WheelEvent::preventDefault).not.toHaveBeenCalled() # scroll all the way right componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -3000, wheelDeltaY: 0)) nextAnimationFrame() - maxScrollLeft = editor.getScrollLeft() + maxScrollLeft = wrapperNode.getScrollLeft() expect(WheelEvent::preventDefault).toHaveBeenCalled() WheelEvent::preventDefault.reset() # try to scroll past the right side, which is impossible componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -30, wheelDeltaY: 0)) - expect(editor.getScrollLeft()).toBe maxScrollLeft + expect(wrapperNode.getScrollLeft()).toBe maxScrollLeft expect(WheelEvent::preventDefault).not.toHaveBeenCalled() describe "input events", -> @@ -3054,7 +3054,7 @@ describe "TextEditorComponent", -> expect(componentNode.querySelectorAll('.line')).toHaveLength(6) gutterWidth = componentNode.querySelector('.gutter').offsetWidth - componentNode.style.width = gutterWidth + 14 * charWidth + editor.getVerticalScrollbarWidth() + 'px' + componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' atom.views.performDocumentPoll() nextAnimationFrame() expect(componentNode.querySelector('.line').textContent).toBe "var quicksort " @@ -3324,6 +3324,273 @@ describe "TextEditorComponent", -> expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe false expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false + describe "autoscroll", -> + beforeEach -> + editor.setVerticalScrollMargin(2) + editor.setHorizontalScrollMargin(2) + component.setLineHeight("10px") + component.setFontSize(17) + component.measureDimensions() + nextAnimationFrame() + + wrapperNode.setWidth(55) + wrapperNode.setHeight(55) + component.measureDimensions() + nextAnimationFrame() + + component.presenter.setHorizontalScrollbarHeight(0) + component.presenter.setVerticalScrollbarWidth(0) + nextAnimationFrame() + + describe "when selecting buffer ranges", -> + it "autoscrolls the selection if it is last unless the 'autoscroll' option is false", -> + expect(wrapperNode.getScrollTop()).toBe 0 + + editor.setSelectedBufferRange([[5, 6], [6, 8]]) + nextAnimationFrame() + expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 + expect(wrapperNode.getScrollRight()).toBe (8 + editor.getHorizontalScrollMargin()) * 10 + + editor.setSelectedBufferRange([[0, 0], [0, 0]]) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + expect(wrapperNode.getScrollLeft()).toBe 0 + + editor.setSelectedBufferRange([[6, 6], [6, 8]]) + nextAnimationFrame() + expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 + expect(wrapperNode.getScrollRight()).toBe (8 + editor.getHorizontalScrollMargin()) * 10 + + describe "when adding selections for buffer ranges", -> + it "autoscrolls to the added selection if needed", -> + editor.addSelectionForBufferRange([[8, 10], [8, 15]]) + nextAnimationFrame() + expect(wrapperNode.getScrollBottom()).toBe (9 * 10) + (2 * 10) + expect(wrapperNode.getScrollRight()).toBe (15 * 10) + (2 * 10) + + describe "when selecting lines containing cursors", -> + it "autoscrolls to the selection", -> + editor.setCursorScreenPosition([5, 6]) + nextAnimationFrame() + + wrapperNode.scrollToTop() + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + + editor.selectLinesContainingCursors() + nextAnimationFrame() + expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 + + describe "when inserting text", -> + describe "when there are multiple empty selections on different lines", -> + it "autoscrolls to the last cursor", -> + editor.setCursorScreenPosition([1, 2], autoscroll: false) + nextAnimationFrame() + + editor.addCursorAtScreenPosition([10, 4], autoscroll: false) + nextAnimationFrame() + + expect(wrapperNode.getScrollTop()).toBe 0 + editor.insertText('a') + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 75 + + describe "when scrolled to cursor position", -> + it "scrolls the last cursor into view, centering around the cursor if possible and the 'center' option isn't false", -> + editor.setCursorScreenPosition([8, 8], autoscroll: false) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + expect(wrapperNode.getScrollLeft()).toBe 0 + + editor.scrollToCursorPosition() + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe (8.8 * 10) - 30 + expect(wrapperNode.getScrollBottom()).toBe (8.3 * 10) + 30 + expect(wrapperNode.getScrollRight()).toBe (9 + editor.getHorizontalScrollMargin()) * 10 + + wrapperNode.setScrollTop(0) + editor.scrollToCursorPosition(center: false) + expect(wrapperNode.getScrollTop()).toBe (7.8 - editor.getVerticalScrollMargin()) * 10 + expect(wrapperNode.getScrollBottom()).toBe (9.3 + editor.getVerticalScrollMargin()) * 10 + + describe "moving cursors", -> + it "scrolls down when the last cursor gets closer than ::verticalScrollMargin to the bottom of the editor", -> + expect(wrapperNode.getScrollTop()).toBe 0 + expect(wrapperNode.getScrollBottom()).toBe 5.5 * 10 + + editor.setCursorScreenPosition([2, 0]) + nextAnimationFrame() + expect(wrapperNode.getScrollBottom()).toBe 5.5 * 10 + + editor.moveDown() + nextAnimationFrame() + expect(wrapperNode.getScrollBottom()).toBe 6 * 10 + + editor.moveDown() + nextAnimationFrame() + expect(wrapperNode.getScrollBottom()).toBe 7 * 10 + + it "scrolls up when the last cursor gets closer than ::verticalScrollMargin to the top of the editor", -> + editor.setCursorScreenPosition([11, 0]) + nextAnimationFrame() + wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) + nextAnimationFrame() + + editor.moveUp() + nextAnimationFrame() + expect(wrapperNode.getScrollBottom()).toBe wrapperNode.getScrollHeight() + + editor.moveUp() + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 7 * 10 + + editor.moveUp() + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 6 * 10 + + it "scrolls right when the last cursor gets closer than ::horizontalScrollMargin to the right of the editor", -> + expect(wrapperNode.getScrollLeft()).toBe 0 + expect(wrapperNode.getScrollRight()).toBe 5.5 * 10 + + editor.setCursorScreenPosition([0, 2]) + nextAnimationFrame() + expect(wrapperNode.getScrollRight()).toBe 5.5 * 10 + + editor.moveRight() + nextAnimationFrame() + expect(wrapperNode.getScrollRight()).toBe 6 * 10 + + editor.moveRight() + nextAnimationFrame() + expect(wrapperNode.getScrollRight()).toBe 7 * 10 + + it "scrolls left when the last cursor gets closer than ::horizontalScrollMargin to the left of the editor", -> + wrapperNode.setScrollRight(wrapperNode.getScrollWidth()) + nextAnimationFrame() + expect(wrapperNode.getScrollRight()).toBe wrapperNode.getScrollWidth() + editor.setCursorScreenPosition([6, 62], autoscroll: false) + nextAnimationFrame() + + editor.moveLeft() + nextAnimationFrame() + expect(wrapperNode.getScrollLeft()).toBe 59 * 10 + + editor.moveLeft() + nextAnimationFrame() + expect(wrapperNode.getScrollLeft()).toBe 58 * 10 + + it "scrolls down when inserting lines makes the document longer than the editor's height", -> + editor.setCursorScreenPosition([13, Infinity]) + editor.insertNewline() + nextAnimationFrame() + + expect(wrapperNode.getScrollBottom()).toBe 14 * 10 + editor.insertNewline() + nextAnimationFrame() + expect(wrapperNode.getScrollBottom()).toBe 15 * 10 + + it "autoscrolls to the cursor when it moves due to undo", -> + editor.insertText('abc') + wrapperNode.setScrollTop(Infinity) + nextAnimationFrame() + + editor.undo() + nextAnimationFrame() + + expect(wrapperNode.getScrollTop()).toBe 0 + + it "doesn't scroll when the cursor moves into the visible area", -> + editor.setCursorBufferPosition([0, 0]) + nextAnimationFrame() + + wrapperNode.setScrollTop(40) + nextAnimationFrame() + + editor.setCursorBufferPosition([6, 0]) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 40 + + it "honors the autoscroll option on cursor and selection manipulation methods", -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.addCursorAtScreenPosition([11, 11], autoscroll: false) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + editor.addCursorAtBufferPosition([11, 11], autoscroll: false) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + editor.setCursorScreenPosition([11, 11], autoscroll: false) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + editor.setCursorBufferPosition([11, 11], autoscroll: false) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + editor.addSelectionForBufferRange([[11, 11], [11, 11]], autoscroll: false) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + editor.addSelectionForScreenRange([[11, 11], [11, 12]], autoscroll: false) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + editor.setSelectedBufferRange([[11, 0], [11, 1]], autoscroll: false) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + editor.setSelectedScreenRange([[11, 0], [11, 6]], autoscroll: false) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + editor.clearSelections(autoscroll: false) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + + editor.addSelectionForScreenRange([[0, 0], [0, 4]]) + nextAnimationFrame() + + editor.getCursors()[0].setScreenPosition([11, 11], autoscroll: true) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 + editor.getCursors()[0].setBufferPosition([0, 0], autoscroll: true) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], autoscroll: true) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 + editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], autoscroll: true) + nextAnimationFrame() + expect(wrapperNode.getScrollTop()).toBe 0 + + describe "::screenPositionForPixelPosition(pixelPosition)", -> + it "clips pixel positions above buffer start", -> + expect(component.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0] + expect(component.screenPositionForPixelPosition(top: -Infinity, left: Infinity)).toEqual [0, 0] + expect(component.screenPositionForPixelPosition(top: -1, left: Infinity)).toEqual [0, 0] + expect(component.screenPositionForPixelPosition(top: 0, left: Infinity)).toEqual [0, 29] + + it "clips pixel positions below buffer end", -> + expect(component.screenPositionForPixelPosition(top: Infinity, left: -Infinity)).toEqual [12, 2] + expect(component.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2] + expect(component.screenPositionForPixelPosition(top: component.getScrollHeight() + 1, left: 0)).toEqual [12, 2] + expect(component.screenPositionForPixelPosition(top: component.getScrollHeight() - 1, left: 0)).toEqual [12, 0] + + describe "::getVisibleRowRange()", -> + beforeEach -> + wrapperNode.style.height = lineHeightInPixels * 8 + "px" + component.measureDimensions() + nextAnimationFrame() + + it "returns the first and the last visible rows", -> + component.setScrollTop(0) + nextAnimationFrame() + + expect(component.getVisibleRowRange()).toEqual [0, 9] + + it "ends at last buffer row even if there's more space available", -> + wrapperNode.style.height = lineHeightInPixels * 13 + "px" + component.measureDimensions() + nextAnimationFrame() + + component.setScrollTop(60) + nextAnimationFrame() + + expect(component.getVisibleRowRange()).toEqual [0, 13] + describe "middle mouse paste on Linux", -> originalPlatform = null @@ -3368,15 +3635,15 @@ describe "TextEditorComponent", -> clientCoordinatesForScreenPosition = (screenPosition) -> positionOffset = wrapperNode.pixelPositionForScreenPosition(screenPosition) scrollViewClientRect = componentNode.querySelector('.scroll-view').getBoundingClientRect() - clientX = scrollViewClientRect.left + positionOffset.left - editor.getScrollLeft() - clientY = scrollViewClientRect.top + positionOffset.top - editor.getScrollTop() + clientX = scrollViewClientRect.left + positionOffset.left - wrapperNode.getScrollLeft() + clientY = scrollViewClientRect.top + positionOffset.top - wrapperNode.getScrollTop() {clientX, clientY} clientCoordinatesForScreenRowInGutter = (screenRow) -> positionOffset = wrapperNode.pixelPositionForScreenPosition([screenRow, Infinity]) gutterClientRect = componentNode.querySelector('.gutter').getBoundingClientRect() - clientX = gutterClientRect.left + positionOffset.left - editor.getScrollLeft() - clientY = gutterClientRect.top + positionOffset.top - editor.getScrollTop() + clientX = gutterClientRect.left + positionOffset.left - wrapperNode.getScrollLeft() + clientY = gutterClientRect.top + positionOffset.top - wrapperNode.getScrollTop() {clientX, clientY} lineAndLineNumberHaveClass = (screenRow, klass) -> diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 76589416e..1a2f8badc 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -357,6 +357,18 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setScrollLeft(-300) expect(presenter.getState().horizontalScrollbar.scrollLeft).toBe 0 + it "is always 0 when soft wrapping is enabled", -> + presenter = buildPresenter(scrollLeft: 0, verticalScrollbarWidth: 0, contentFrameWidth: 85, baseCharacterWidth: 10) + + editor.setSoftWrapped(false) + presenter.setScrollLeft(Infinity) + expect(presenter.getState().content.scrollLeft).toBeGreaterThan 0 + + editor.setSoftWrapped(true) + expect(presenter.getState().content.scrollLeft).toBe 0 + presenter.setScrollLeft(10) + expect(presenter.getState().content.scrollLeft).toBe 0 + describe ".verticalScrollbar", -> describe ".visible", -> it "is true if the scrollHeight exceeds the computed client height", -> @@ -504,11 +516,11 @@ describe "TextEditorPresenter", -> expectValues presenter.getState().hiddenInput, {top: 0, left: 0} expectStateUpdate presenter, -> editor.setCursorBufferPosition([11, 43]) - expectValues presenter.getState().hiddenInput, {top: 11 * 10 - editor.getScrollTop(), left: 43 * 10 - editor.getScrollLeft()} + expectValues presenter.getState().hiddenInput, {top: 11 * 10 - presenter.getScrollTop(), left: 43 * 10 - presenter.getScrollLeft()} newCursor = null expectStateUpdate presenter, -> newCursor = editor.addCursorAtBufferPosition([6, 10]) - expectValues presenter.getState().hiddenInput, {top: (6 * 10) - editor.getScrollTop(), left: (10 * 10) - editor.getScrollLeft()} + expectValues presenter.getState().hiddenInput, {top: (6 * 10) - presenter.getScrollTop(), left: (10 * 10) - presenter.getScrollLeft()} expectStateUpdate presenter, -> newCursor.destroy() expectValues presenter.getState().hiddenInput, {top: 50 - 10, left: 300 - 10} @@ -554,6 +566,7 @@ describe "TextEditorPresenter", -> advanceClock(100) expect(presenter.getState().content.scrollingVertically).toBe true presenter.setScrollTop(10) + presenter.getState() # commits scroll position advanceClock(100) expect(presenter.getState().content.scrollingVertically).toBe true expectStateUpdate presenter, -> advanceClock(100) @@ -666,12 +679,55 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1 describe ".scrollTop", -> + it "changes based on the scroll operation that was performed last", -> + presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 20) + expect(presenter.getState().content.scrollTop).toBe(0) + + presenter.setScrollTop(20) + editor.setCursorBufferPosition([5, 0]) + + expect(presenter.getState().content.scrollTop).toBe(50) + + editor.setCursorBufferPosition([8, 0]) + presenter.setScrollTop(10) + + expect(presenter.getState().content.scrollTop).toBe(10) + + it "corresponds to the passed logical coordinates when building the presenter", -> + presenter = buildPresenter(scrollRow: 4, lineHeight: 10, explicitHeight: 20) + expect(presenter.getState().content.scrollTop).toBe(40) + it "tracks the value of ::scrollTop", -> presenter = buildPresenter(scrollTop: 10, lineHeight: 10, explicitHeight: 20) expect(presenter.getState().content.scrollTop).toBe 10 expectStateUpdate presenter, -> presenter.setScrollTop(50) expect(presenter.getState().content.scrollTop).toBe 50 + it "keeps the model up to date with the corresponding logical coordinates", -> + presenter = buildPresenter(scrollTop: 0, explicitHeight: 20, horizontalScrollbarHeight: 10, lineHeight: 10) + + expectStateUpdate presenter, -> presenter.setScrollTop(50) + presenter.getState() # commits scroll position + expect(editor.getScrollRow()).toBe(5) + + expectStateUpdate presenter, -> presenter.setScrollTop(57) + presenter.getState() # commits scroll position + expect(editor.getScrollRow()).toBe(6) + + it "reassigns the scrollTop if it exceeds the max possible value after lines are removed", -> + presenter = buildPresenter(scrollTop: 80, lineHeight: 10, explicitHeight: 50, horizontalScrollbarHeight: 0) + expect(presenter.getState().content.scrollTop).toBe(80) + buffer.deleteRows(10, 9, 8) + expect(presenter.getState().content.scrollTop).toBe(60) + + it "is always rounded to the nearest integer", -> + presenter = buildPresenter(scrollTop: 10, lineHeight: 10, explicitHeight: 20) + expect(presenter.getState().content.scrollTop).toBe 10 + expectStateUpdate presenter, -> presenter.setScrollTop(11.4) + expect(presenter.getState().content.scrollTop).toBe 11 + expectStateUpdate presenter, -> presenter.setScrollTop(12.6) + expect(presenter.getState().content.scrollTop).toBe 13 + it "scrolls down automatically when the model is changed", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10, explicitHeight: 20) @@ -689,17 +745,21 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setExplicitHeight(60) expect(presenter.getState().content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(presenter.getRealScrollTop()).toBe presenter.scrollHeight - presenter.clientHeight expectStateUpdate presenter, -> presenter.setHorizontalScrollbarHeight(5) expect(presenter.getState().content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(presenter.getRealScrollTop()).toBe presenter.scrollHeight - presenter.clientHeight expectStateUpdate presenter, -> editor.getBuffer().delete([[8, 0], [12, 0]]) expect(presenter.getState().content.scrollTop).toBe presenter.scrollHeight - presenter.clientHeight + expect(presenter.getRealScrollTop()).toBe presenter.scrollHeight - presenter.clientHeight # Scroll top only gets smaller when needed as dimensions change, never bigger scrollTopBefore = presenter.getState().verticalScrollbar.scrollTop expectStateUpdate presenter, -> editor.getBuffer().insert([9, Infinity], '\n\n\n') expect(presenter.getState().content.scrollTop).toBe scrollTopBefore + expect(presenter.getRealScrollTop()).toBe scrollTopBefore it "never goes negative", -> presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10) @@ -719,30 +779,72 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.scrollTop).toBe presenter.contentHeight - presenter.clientHeight describe ".scrollLeft", -> + it "changes based on the scroll operation that was performed last", -> + presenter = buildPresenter(scrollLeft: 0, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 10) + expect(presenter.getState().content.scrollLeft).toBe(0) + + presenter.setScrollLeft(20) + editor.setCursorBufferPosition([0, 9]) + + expect(presenter.getState().content.scrollLeft).toBe(90) + + editor.setCursorBufferPosition([0, 18]) + presenter.setScrollLeft(50) + + expect(presenter.getState().content.scrollLeft).toBe(50) + + it "corresponds to the passed logical coordinates when building the presenter", -> + presenter = buildPresenter(scrollColumn: 3, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) + expect(presenter.getState().content.scrollLeft).toBe(30) + it "tracks the value of ::scrollLeft", -> presenter = buildPresenter(scrollLeft: 10, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) expect(presenter.getState().content.scrollLeft).toBe 10 expectStateUpdate presenter, -> presenter.setScrollLeft(50) expect(presenter.getState().content.scrollLeft).toBe 50 + it "keeps the model up to date with the corresponding logical coordinates", -> + presenter = buildPresenter(scrollLeft: 0, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) + + expectStateUpdate presenter, -> presenter.setScrollLeft(50) + presenter.getState() # commits scroll position + expect(editor.getScrollColumn()).toBe(5) + + expectStateUpdate presenter, -> presenter.setScrollLeft(57) + presenter.getState() # commits scroll position + expect(editor.getScrollColumn()).toBe(6) + + it "is always rounded to the nearest integer", -> + presenter = buildPresenter(scrollLeft: 10, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) + expect(presenter.getState().content.scrollLeft).toBe 10 + expectStateUpdate presenter, -> presenter.setScrollLeft(11.4) + expect(presenter.getState().content.scrollLeft).toBe 11 + expectStateUpdate presenter, -> presenter.setScrollLeft(12.6) + expect(presenter.getState().content.scrollLeft).toBe 13 + it "never exceeds the computed scrollWidth minus the computed clientWidth", -> presenter = buildPresenter(scrollLeft: 10, lineHeight: 10, baseCharacterWidth: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) expectStateUpdate presenter, -> presenter.setScrollLeft(300) expect(presenter.getState().content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth + expect(presenter.getRealScrollLeft()).toBe presenter.scrollWidth - presenter.clientWidth expectStateUpdate presenter, -> presenter.setContentFrameWidth(600) expect(presenter.getState().content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth + expect(presenter.getRealScrollLeft()).toBe presenter.scrollWidth - presenter.clientWidth expectStateUpdate presenter, -> presenter.setVerticalScrollbarWidth(5) expect(presenter.getState().content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth + expect(presenter.getRealScrollLeft()).toBe presenter.scrollWidth - presenter.clientWidth expectStateUpdate presenter, -> editor.getBuffer().delete([[6, 0], [6, Infinity]]) expect(presenter.getState().content.scrollLeft).toBe presenter.scrollWidth - presenter.clientWidth + expect(presenter.getRealScrollLeft()).toBe presenter.scrollWidth - presenter.clientWidth # Scroll top only gets smaller when needed as dimensions change, never bigger scrollLeftBefore = presenter.getState().content.scrollLeft expectStateUpdate presenter, -> editor.getBuffer().insert([6, 0], new Array(100).join('x')) expect(presenter.getState().content.scrollLeft).toBe scrollLeftBefore + expect(presenter.getRealScrollLeft()).toBe scrollLeftBefore it "never goes negative", -> presenter = buildPresenter(scrollLeft: 10, verticalScrollbarWidth: 10, contentFrameWidth: 500) @@ -1821,7 +1923,8 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> editor.insertNewline() - editor.setScrollTop(scrollTop) # I'm fighting the editor + presenter.setScrollTop(scrollTop) # I'm fighting the editor + expectValues stateForOverlay(presenter, decoration), { item: item pixelPosition: {top: 6 * 10 - scrollTop - itemHeight, left: gutterWidth} @@ -2098,6 +2201,12 @@ describe "TextEditorPresenter", -> expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 3, softWrapped: true} expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 4, softWrapped: false} + presenter.setContentFrameWidth(500) + + expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false} + expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 5, softWrapped: false} + expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 6, softWrapped: false} + describe ".decorationClasses", -> it "adds decoration classes to the relevant line number state objects, both initially and when decorations change", -> marker1 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index bef0c5948..1141fc78e 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -909,118 +909,6 @@ describe "TextEditor", -> cursor2 = editor.addCursorAtBufferPosition([1, 4]) expect(cursor2.marker).toBe cursor1.marker - describe "autoscroll", -> - beforeEach -> - editor.setVerticalScrollMargin(2) - editor.setHorizontalScrollMargin(2) - editor.setLineHeightInPixels(10) - editor.setDefaultCharWidth(10) - editor.setHorizontalScrollbarHeight(0) - editor.setHeight(5.5 * 10) - editor.setWidth(5.5 * 10) - - it "scrolls down when the last cursor gets closer than ::verticalScrollMargin to the bottom of the editor", -> - expect(editor.getScrollTop()).toBe 0 - expect(editor.getScrollBottom()).toBe 5.5 * 10 - - editor.setCursorScreenPosition([2, 0]) - expect(editor.getScrollBottom()).toBe 5.5 * 10 - - editor.moveDown() - expect(editor.getScrollBottom()).toBe 6 * 10 - - editor.moveDown() - expect(editor.getScrollBottom()).toBe 7 * 10 - - it "scrolls up when the last cursor gets closer than ::verticalScrollMargin to the top of the editor", -> - editor.setCursorScreenPosition([11, 0]) - editor.setScrollBottom(editor.getScrollHeight()) - - editor.moveUp() - expect(editor.getScrollBottom()).toBe editor.getScrollHeight() - - editor.moveUp() - expect(editor.getScrollTop()).toBe 7 * 10 - - editor.moveUp() - expect(editor.getScrollTop()).toBe 6 * 10 - - it "scrolls right when the last cursor gets closer than ::horizontalScrollMargin to the right of the editor", -> - expect(editor.getScrollLeft()).toBe 0 - expect(editor.getScrollRight()).toBe 5.5 * 10 - - editor.setCursorScreenPosition([0, 2]) - expect(editor.getScrollRight()).toBe 5.5 * 10 - - editor.moveRight() - expect(editor.getScrollRight()).toBe 6 * 10 - - editor.moveRight() - expect(editor.getScrollRight()).toBe 7 * 10 - - it "scrolls left when the last cursor gets closer than ::horizontalScrollMargin to the left of the editor", -> - editor.setScrollRight(editor.getScrollWidth()) - expect(editor.getScrollRight()).toBe editor.getScrollWidth() - editor.setCursorScreenPosition([6, 62], autoscroll: false) - - editor.moveLeft() - expect(editor.getScrollLeft()).toBe 59 * 10 - - editor.moveLeft() - expect(editor.getScrollLeft()).toBe 58 * 10 - - it "scrolls down when inserting lines makes the document longer than the editor's height", -> - editor.setCursorScreenPosition([13, Infinity]) - editor.insertNewline() - expect(editor.getScrollBottom()).toBe 14 * 10 - editor.insertNewline() - expect(editor.getScrollBottom()).toBe 15 * 10 - - it "autoscrolls to the cursor when it moves due to undo", -> - editor.insertText('abc') - editor.setScrollTop(Infinity) - editor.undo() - expect(editor.getScrollTop()).toBe 0 - - it "doesn't scroll when the cursor moves into the visible area", -> - editor.setCursorBufferPosition([0, 0]) - editor.setScrollTop(40) - expect(editor.getVisibleRowRange()).toEqual([4, 9]) - editor.setCursorBufferPosition([6, 0]) - expect(editor.getScrollTop()).toBe 40 - - it "honors the autoscroll option on cursor and selection manipulation methods", -> - expect(editor.getScrollTop()).toBe 0 - editor.addCursorAtScreenPosition([11, 11], autoscroll: false) - expect(editor.getScrollTop()).toBe 0 - editor.addCursorAtBufferPosition([11, 11], autoscroll: false) - expect(editor.getScrollTop()).toBe 0 - editor.setCursorScreenPosition([11, 11], autoscroll: false) - expect(editor.getScrollTop()).toBe 0 - editor.setCursorBufferPosition([11, 11], autoscroll: false) - expect(editor.getScrollTop()).toBe 0 - editor.addSelectionForBufferRange([[11, 11], [11, 11]], autoscroll: false) - expect(editor.getScrollTop()).toBe 0 - editor.addSelectionForScreenRange([[11, 11], [11, 12]], autoscroll: false) - expect(editor.getScrollTop()).toBe 0 - editor.setSelectedBufferRange([[11, 0], [11, 1]], autoscroll: false) - expect(editor.getScrollTop()).toBe 0 - editor.setSelectedScreenRange([[11, 0], [11, 6]], autoscroll: false) - expect(editor.getScrollTop()).toBe 0 - editor.clearSelections(autoscroll: false) - expect(editor.getScrollTop()).toBe 0 - - editor.addSelectionForScreenRange([[0, 0], [0, 4]]) - - editor.getCursors()[0].setScreenPosition([11, 11], autoscroll: true) - expect(editor.getScrollTop()).toBeGreaterThan 0 - editor.getCursors()[0].setBufferPosition([0, 0], autoscroll: true) - expect(editor.getScrollTop()).toBe 0 - editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], autoscroll: true) - expect(editor.getScrollTop()).toBeGreaterThan 0 - editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], autoscroll: true) - expect(editor.getScrollTop()).toBe 0 - describe '.logCursorScope()', -> beforeEach -> spyOn(atom.notifications, 'addInfo') @@ -1306,20 +1194,6 @@ describe "TextEditor", -> editor.selectLinesContainingCursors() expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [4, 0]] - it "autoscrolls to the selection", -> - editor.setLineHeightInPixels(10) - editor.setDefaultCharWidth(10) - editor.setHeight(50) - editor.setWidth(50) - editor.setHorizontalScrollbarHeight(0) - editor.setCursorScreenPosition([5, 6]) - - editor.scrollToTop() - expect(editor.getScrollTop()).toBe 0 - - editor.selectLinesContainingCursors() - expect(editor.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 - describe ".selectToBeginningOfWord()", -> it "selects text from cusor position to beginning of word", -> editor.setCursorScreenPosition [0, 13] @@ -1572,30 +1446,6 @@ describe "TextEditor", -> expect(selection1).toBe selection expect(selection1.getScreenRange()).toEqual [[2, 2], [3, 4]] - describe ".setSelectedBufferRange(range)", -> - it "autoscrolls the selection if it is last unless the 'autoscroll' option is false", -> - editor.setVerticalScrollMargin(2) - editor.setHorizontalScrollMargin(2) - editor.setLineHeightInPixels(10) - editor.setDefaultCharWidth(10) - editor.setHeight(70) - editor.setWidth(100) - editor.setHorizontalScrollbarHeight(0) - - expect(editor.getScrollTop()).toBe 0 - - editor.setSelectedBufferRange([[5, 6], [6, 8]]) - expect(editor.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 - expect(editor.getScrollRight()).toBe (8 + editor.getHorizontalScrollMargin()) * 10 - - editor.setSelectedBufferRange([[0, 0], [0, 0]]) - expect(editor.getScrollTop()).toBe 0 - expect(editor.getScrollLeft()).toBe 0 - - editor.setSelectedBufferRange([[6, 6], [6, 8]]) - expect(editor.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 - expect(editor.getScrollRight()).toBe (8 + editor.getHorizontalScrollMargin()) * 10 - describe ".selectMarker(marker)", -> describe "if the marker is valid", -> it "selects the marker's range and returns the selected range", -> @@ -1615,17 +1465,6 @@ describe "TextEditor", -> editor.addSelectionForBufferRange([[3, 4], [5, 6]]) expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 0]], [[3, 4], [5, 6]]] - it "autoscrolls to the added selection if needed", -> - editor.setVerticalScrollMargin(2) - editor.setHorizontalScrollMargin(2) - editor.setLineHeightInPixels(10) - editor.setDefaultCharWidth(10) - editor.setHeight(80) - editor.setWidth(100) - editor.addSelectionForBufferRange([[8, 10], [8, 15]]) - expect(editor.getScrollBottom()).toBe (9 * 10) + (2 * 10) - expect(editor.getScrollRight()).toBe (15 * 10) + (2 * 10) - describe ".addSelectionBelow()", -> describe "when the selection is non-empty", -> it "selects the same region of the line below current selections if possible", -> @@ -1976,16 +1815,6 @@ describe "TextEditor", -> expect(cursor1.getBufferPosition()).toEqual [1, 5] expect(cursor2.getBufferPosition()).toEqual [2, 7] - it "autoscrolls to the last cursor", -> - editor.setCursorScreenPosition([1, 2]) - editor.addCursorAtScreenPosition([10, 4]) - editor.setLineHeightInPixels(10) - editor.setHeight(50) - - expect(editor.getScrollTop()).toBe 0 - editor.insertText('a') - expect(editor.getScrollTop()).toBe 80 - describe "when there are multiple non-empty selections", -> describe "when the selections are on the same line", -> it "replaces each selection range with the inserted characters", -> @@ -4547,30 +4376,10 @@ describe "TextEditor", -> editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) expect(editor.getText()).toBe ' ' - describe ".scrollToCursorPosition()", -> - it "scrolls the last cursor into view, centering around the cursor if possible and the 'center' option isn't false", -> - editor.setCursorScreenPosition([8, 8]) - editor.setLineHeightInPixels(10) - editor.setDefaultCharWidth(10) - editor.setHeight(60) - editor.setWidth(130) - editor.setHorizontalScrollbarHeight(0) - expect(editor.getScrollTop()).toBe 0 - expect(editor.getScrollLeft()).toBe 0 - - editor.scrollToCursorPosition() - expect(editor.getScrollTop()).toBe (8.5 * 10) - 30 - expect(editor.getScrollBottom()).toBe (8.5 * 10) + 30 - expect(editor.getScrollRight()).toBe (9 + editor.getHorizontalScrollMargin()) * 10 - - editor.setScrollTop(0) - editor.scrollToCursorPosition(center: false) - expect(editor.getScrollBottom()).toBe (9 + editor.getVerticalScrollMargin()) * 10 - describe ".pageUp/Down()", -> it "moves the cursor down one page length", -> editor.setLineHeightInPixels(10) - editor.setHeight(50) + editor.setHeight(50, true) expect(editor.getCursorBufferPosition().row).toBe 0 editor.pageDown() @@ -4588,8 +4397,7 @@ describe "TextEditor", -> describe ".selectPageUp/Down()", -> it "selects one screen height of text up or down", -> editor.setLineHeightInPixels(10) - editor.setHeight(50) - expect(editor.getScrollHeight()).toBe 130 + editor.setHeight(50, true) expect(editor.getCursorBufferPosition().row).toBe 0 editor.selectPageDown() diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 425311b13..27d1b44cf 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -26,11 +26,6 @@ class DisplayBuffer extends Model defaultCharWidth: null height: null width: null - scrollTop: 0 - scrollLeft: 0 - scrollWidth: 0 - verticalScrollbarWidth: 15 - horizontalScrollbarHeight: 15 @deserialize: (state) -> state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer) @@ -99,15 +94,11 @@ class DisplayBuffer extends Model id: @id softWrapped: @isSoftWrapped() editorWidthInChars: @editorWidthInChars - scrollTop: @scrollTop - scrollLeft: @scrollLeft tokenizedBuffer: @tokenizedBuffer.serialize() largeFileMode: @largeFileMode copy: -> newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength(), @largeFileMode}) - newDisplayBuffer.setScrollTop(@getScrollTop()) - newDisplayBuffer.setScrollLeft(@getScrollLeft()) for marker in @findMarkers(displayBufferId: @id) marker.copy(displayBufferId: newDisplayBuffer.id) @@ -134,19 +125,8 @@ class DisplayBuffer extends Model onDidChangeCharacterWidths: (callback) -> @emitter.on 'did-change-character-widths', callback - onDidChangeScrollTop: (callback) -> - @emitter.on 'did-change-scroll-top', callback - - onDidChangeScrollLeft: (callback) -> - @emitter.on 'did-change-scroll-left', callback - - observeScrollTop: (callback) -> - callback(@scrollTop) - @onDidChangeScrollTop(callback) - - observeScrollLeft: (callback) -> - callback(@scrollLeft) - @onDidChangeScrollLeft(callback) + onDidRequestAutoscroll: (callback) -> + @emitter.on 'did-request-autoscroll', callback observeDecorations: (callback) -> callback(decoration) for decoration in @getDecorations() @@ -189,105 +169,24 @@ class DisplayBuffer extends Model setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin - getVerticalScrollMarginInPixels: -> @getVerticalScrollMargin() * @getLineHeightInPixels() - getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@getWidth() / @getDefaultCharWidth()) - 1) / 2)) setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin - getHorizontalScrollMarginInPixels: -> scrollMarginInPixels = @getHorizontalScrollMargin() * @getDefaultCharWidth() - - getHorizontalScrollbarHeight: -> @horizontalScrollbarHeight - setHorizontalScrollbarHeight: (@horizontalScrollbarHeight) -> @horizontalScrollbarHeight - - getVerticalScrollbarWidth: -> @verticalScrollbarWidth - setVerticalScrollbarWidth: (@verticalScrollbarWidth) -> @verticalScrollbarWidth - getHeight: -> - if @height? - @height - else - if @horizontallyScrollable() - @getScrollHeight() + @getHorizontalScrollbarHeight() - else - @getScrollHeight() + @height - setHeight: (@height) -> @height - - getClientHeight: (reentrant) -> - if @horizontallyScrollable(reentrant) - @getHeight() - @getHorizontalScrollbarHeight() - else - @getHeight() - - getClientWidth: (reentrant) -> - if @verticallyScrollable(reentrant) - @getWidth() - @getVerticalScrollbarWidth() - else - @getWidth() - - horizontallyScrollable: (reentrant) -> - return false unless @width? - return false if @isSoftWrapped() - if reentrant - @getScrollWidth() > @getWidth() - else - @getScrollWidth() > @getClientWidth(true) - - verticallyScrollable: (reentrant) -> - return false unless @height? - if reentrant - @getScrollHeight() > @getHeight() - else - @getScrollHeight() > @getClientHeight(true) + setHeight: (@height) -> + @height getWidth: -> - if @width? - @width - else - if @verticallyScrollable() - @getScrollWidth() + @getVerticalScrollbarWidth() - else - @getScrollWidth() + @width setWidth: (newWidth) -> oldWidth = @width @width = newWidth @updateWrappedScreenLines() if newWidth isnt oldWidth and @isSoftWrapped() - @setScrollTop(@getScrollTop()) # Ensure scrollTop is still valid in case horizontal scrollbar disappeared @width - getScrollTop: -> @scrollTop - setScrollTop: (scrollTop) -> - scrollTop = Math.round(Math.max(0, Math.min(@getMaxScrollTop(), scrollTop))) - unless scrollTop is @scrollTop - @scrollTop = scrollTop - @emitter.emit 'did-change-scroll-top', @scrollTop - @scrollTop - - getMaxScrollTop: -> - @getScrollHeight() - @getClientHeight() - - getScrollBottom: -> @scrollTop + @getClientHeight() - setScrollBottom: (scrollBottom) -> - @setScrollTop(scrollBottom - @getClientHeight()) - @getScrollBottom() - - getScrollLeft: -> @scrollLeft - setScrollLeft: (scrollLeft) -> - scrollLeft = Math.round(Math.max(0, Math.min(@getScrollWidth() - @getClientWidth(), scrollLeft))) - unless scrollLeft is @scrollLeft - @scrollLeft = scrollLeft - @emitter.emit 'did-change-scroll-left', @scrollLeft - @scrollLeft - - getMaxScrollLeft: -> - @getScrollWidth() - @getClientWidth() - - getScrollRight: -> @scrollLeft + @width - setScrollRight: (scrollRight) -> - @setScrollLeft(scrollRight - @width) - @getScrollRight() - getLineHeightInPixels: -> @lineHeightInPixels setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels @@ -295,7 +194,6 @@ class DisplayBuffer extends Model setDefaultCharWidth: (defaultCharWidth) -> if defaultCharWidth isnt @defaultCharWidth @defaultCharWidth = defaultCharWidth - @computeScrollWidth() defaultCharWidth getCursorWidth: -> 1 @@ -324,84 +222,14 @@ class DisplayBuffer extends Model @characterWidthsChanged() unless @batchingCharacterMeasurement characterWidthsChanged: -> - @computeScrollWidth() @emitter.emit 'did-change-character-widths', @scopedCharacterWidthsChangeCount clearScopedCharWidths: -> @charWidthsByScope = {} - getScrollHeight: -> - lineHeight = @getLineHeightInPixels() - return 0 unless lineHeight > 0 - - scrollHeight = @getLineCount() * lineHeight - if @height? and @configSettings.scrollPastEnd - scrollHeight = scrollHeight + @height - (lineHeight * 3) - - scrollHeight - - getScrollWidth: -> - @scrollWidth - - # Returns an {Array} of two numbers representing the first and the last visible rows. - getVisibleRowRange: -> - return [0, 0] unless @getLineHeightInPixels() > 0 - - startRow = Math.floor(@getScrollTop() / @getLineHeightInPixels()) - endRow = Math.ceil((@getScrollTop() + @getHeight()) / @getLineHeightInPixels()) - 1 - endRow = Math.min(@getLineCount(), endRow) - - [startRow, endRow] - - intersectsVisibleRowRange: (startRow, endRow) -> - [visibleStart, visibleEnd] = @getVisibleRowRange() - not (endRow <= visibleStart or visibleEnd <= startRow) - - selectionIntersectsVisibleRowRange: (selection) -> - {start, end} = selection.getScreenRange() - @intersectsVisibleRowRange(start.row, end.row + 1) - - scrollToScreenRange: (screenRange, options) -> - verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels() - horizontalScrollMarginInPixels = @getHorizontalScrollMarginInPixels() - - {top, left} = @pixelRectForScreenRange(new Range(screenRange.start, screenRange.start)) - {top: endTop, left: endLeft, height: endHeight} = @pixelRectForScreenRange(new Range(screenRange.end, screenRange.end)) - bottom = endTop + endHeight - right = endLeft - - if options?.center - desiredScrollCenter = (top + bottom) / 2 - unless @getScrollTop() < desiredScrollCenter < @getScrollBottom() - desiredScrollTop = desiredScrollCenter - @getHeight() / 2 - desiredScrollBottom = desiredScrollCenter + @getHeight() / 2 - else - desiredScrollTop = top - verticalScrollMarginInPixels - desiredScrollBottom = bottom + verticalScrollMarginInPixels - - desiredScrollLeft = left - horizontalScrollMarginInPixels - desiredScrollRight = right + horizontalScrollMarginInPixels - - if options?.reversed ? true - if desiredScrollBottom > @getScrollBottom() - @setScrollBottom(desiredScrollBottom) - if desiredScrollTop < @getScrollTop() - @setScrollTop(desiredScrollTop) - - if desiredScrollRight > @getScrollRight() - @setScrollRight(desiredScrollRight) - if desiredScrollLeft < @getScrollLeft() - @setScrollLeft(desiredScrollLeft) - else - if desiredScrollTop < @getScrollTop() - @setScrollTop(desiredScrollTop) - if desiredScrollBottom > @getScrollBottom() - @setScrollBottom(desiredScrollBottom) - - if desiredScrollLeft < @getScrollLeft() - @setScrollLeft(desiredScrollLeft) - if desiredScrollRight > @getScrollRight() - @setScrollRight(desiredScrollRight) + scrollToScreenRange: (screenRange, options = {}) -> + scrollEvent = {screenRange, options} + @emitter.emit "did-request-autoscroll", scrollEvent scrollToScreenPosition: (screenPosition, options) -> @scrollToScreenRange(new Range(screenPosition, screenPosition), options) @@ -409,19 +237,6 @@ class DisplayBuffer extends Model scrollToBufferPosition: (bufferPosition, options) -> @scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition), options) - pixelRectForScreenRange: (screenRange) -> - if screenRange.end.row > screenRange.start.row - top = @pixelPositionForScreenPosition(screenRange.start).top - left = 0 - height = (screenRange.end.row - screenRange.start.row + 1) * @getLineHeightInPixels() - width = @getScrollWidth() - else - {top, left} = @pixelPositionForScreenPosition(screenRange.start, false) - height = @getLineHeightInPixels() - width = @pixelPositionForScreenPosition(screenRange.end, false).left - left - - {top, left, width, height} - # Retrieves the current tab length. # # Returns a {Number}. @@ -465,8 +280,7 @@ class DisplayBuffer extends Model # Returns the editor width in characters for soft wrap. getEditorWidthInChars: -> - width = @width ? @getScrollWidth() - width -= @getVerticalScrollbarWidth() + width = @getWidth() if width? and @defaultCharWidth > 0 Math.max(0, Math.floor(width / @defaultCharWidth)) else @@ -678,80 +492,6 @@ class DisplayBuffer extends Model end = @bufferPositionForScreenPosition(screenRange.end) new Range(start, end) - pixelRangeForScreenRange: (screenRange, clip=true) -> - {start, end} = Range.fromObject(screenRange) - {start: @pixelPositionForScreenPosition(start, clip), end: @pixelPositionForScreenPosition(end, clip)} - - pixelPositionForScreenPosition: (screenPosition, clip=true) -> - screenPosition = Point.fromObject(screenPosition) - screenPosition = @clipScreenPosition(screenPosition) if clip - - targetRow = screenPosition.row - targetColumn = screenPosition.column - defaultCharWidth = @defaultCharWidth - - top = targetRow * @lineHeightInPixels - left = 0 - column = 0 - - iterator = @tokenizedLineForScreenRow(targetRow).getTokenIterator() - while iterator.next() - charWidths = @getScopedCharWidths(iterator.getScopes()) - valueIndex = 0 - value = iterator.getText() - while valueIndex < value.length - if iterator.isPairedCharacter() - char = value - charLength = 2 - valueIndex += 2 - else - char = value[valueIndex] - charLength = 1 - valueIndex++ - - return {top, left} if column is targetColumn - left += charWidths[char] ? defaultCharWidth unless char is '\0' - column += charLength - {top, left} - - screenPositionForPixelPosition: (pixelPosition) -> - targetTop = pixelPosition.top - targetLeft = pixelPosition.left - defaultCharWidth = @defaultCharWidth - row = Math.floor(targetTop / @getLineHeightInPixels()) - targetLeft = 0 if row < 0 - targetLeft = Infinity if row > @getLastRow() - row = Math.min(row, @getLastRow()) - row = Math.max(0, row) - - left = 0 - column = 0 - - iterator = @tokenizedLineForScreenRow(row).getTokenIterator() - while iterator.next() - charWidths = @getScopedCharWidths(iterator.getScopes()) - value = iterator.getText() - valueIndex = 0 - while valueIndex < value.length - if iterator.isPairedCharacter() - char = value - charLength = 2 - valueIndex += 2 - else - char = value[valueIndex] - charLength = 1 - valueIndex++ - - charWidth = charWidths[char] ? defaultCharWidth - break if targetLeft <= left + (charWidth / 2) - left += charWidth - column += charLength - - new Point(row, column) - - pixelPositionForBufferPosition: (bufferPosition) -> - @pixelPositionForScreenPosition(@screenPositionForBufferPosition(bufferPosition)) - # Gets the number of screen lines. # # Returns a {Number}. @@ -1191,7 +931,6 @@ class DisplayBuffer extends Model @changeCount = @tokenizedBuffer.changeCount {start, end, delta, bufferChange} = tokenizedBufferChange @updateScreenLines(start, end + 1, delta, refreshMarkers: false) - @setScrollTop(Math.min(@getScrollTop(), @getMaxScrollTop())) if delta < 0 updateScreenLines: (startBufferRow, endBufferRow, bufferDelta=0, options={}) -> return if @largeFileMode @@ -1296,13 +1035,6 @@ class DisplayBuffer extends Model @longestScreenRow = screenRow @maxLineLength = length - @computeScrollWidth() if oldMaxLineLength isnt @maxLineLength - - computeScrollWidth: -> - @scrollWidth = @pixelPositionForScreenPosition([@longestScreenRow, @maxLineLength]).left - @scrollWidth += 1 unless @isSoftWrapped() - @setScrollLeft(Math.min(@getScrollLeft(), @getMaxScrollLeft())) - handleBufferMarkerCreated: (textBufferMarker) => if textBufferMarker.matchesParams(@getFoldMarkerAttributes()) fold = new Fold(this, textBufferMarker) diff --git a/src/marker.coffee b/src/marker.coffee index 29274068d..16f644027 100644 --- a/src/marker.coffee +++ b/src/marker.coffee @@ -368,6 +368,3 @@ class Marker @wasValid = isValid @emitter.emit 'did-change', changeEvent - - getPixelRange: -> - @displayBuffer.pixelRangeForScreenRange(@getScreenRange(), false) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index c5f3b4ccc..ef1711a04 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -50,8 +50,10 @@ class TextEditorComponent @presenter = new TextEditorPresenter model: @editor - scrollTop: @editor.getScrollTop() - scrollLeft: @editor.getScrollLeft() + scrollTop: 0 + scrollLeft: 0 + scrollRow: @editor.getScrollRow() + scrollColumn: @editor.getScrollColumn() tileSize: tileSize cursorBlinkPeriod: @cursorBlinkPeriod cursorBlinkResumeDelay: @cursorBlinkResumeDelay @@ -314,7 +316,7 @@ class TextEditorComponent inputNode.value = event.data if insertedRange onVerticalScroll: (scrollTop) => - return if @updateRequested or scrollTop is @editor.getScrollTop() + return if @updateRequested or scrollTop is @presenter.getScrollTop() animationFramePending = @pendingScrollTop? @pendingScrollTop = scrollTop @@ -323,15 +325,17 @@ class TextEditorComponent pendingScrollTop = @pendingScrollTop @pendingScrollTop = null @presenter.setScrollTop(pendingScrollTop) + @presenter.commitPendingScrollTopPosition() onHorizontalScroll: (scrollLeft) => - return if @updateRequested or scrollLeft is @editor.getScrollLeft() + return if @updateRequested or scrollLeft is @presenter.getScrollLeft() animationFramePending = @pendingScrollLeft? @pendingScrollLeft = scrollLeft unless animationFramePending @requestAnimationFrame => @presenter.setScrollLeft(@pendingScrollLeft) + @presenter.commitPendingScrollLeftPosition() @pendingScrollLeft = null onMouseWheel: (event) => @@ -350,14 +354,18 @@ class TextEditorComponent if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY) # Scrolling horizontally previousScrollLeft = @presenter.getScrollLeft() - @presenter.setScrollLeft(previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity)) - event.preventDefault() unless previousScrollLeft is @presenter.getScrollLeft() + updatedScrollLeft = previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity) + + event.preventDefault() if @presenter.canScrollLeftTo(updatedScrollLeft) + @presenter.setScrollLeft(updatedScrollLeft) else # Scrolling vertically @presenter.setMouseWheelScreenRow(@screenRowForNode(event.target)) previousScrollTop = @presenter.getScrollTop() - @presenter.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity)) - event.preventDefault() unless previousScrollTop is @presenter.getScrollTop() + updatedScrollTop = previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity) + + event.preventDefault() if @presenter.canScrollTopTo(updatedScrollTop) + @presenter.setScrollTop(updatedScrollTop) onScrollViewScroll: => if @mounted @@ -365,6 +373,77 @@ class TextEditorComponent @scrollViewNode.scrollTop = 0 @scrollViewNode.scrollLeft = 0 + onDidChangeScrollTop: (callback) -> + @presenter.onDidChangeScrollTop(callback) + + onDidChangeScrollLeft: (callback) -> + @presenter.onDidChangeScrollLeft(callback) + + setScrollLeft: (scrollLeft) -> + @presenter.setScrollLeft(scrollLeft) + + setScrollRight: (scrollRight) -> + @presenter.setScrollRight(scrollRight) + + setScrollTop: (scrollTop) -> + @presenter.setScrollTop(scrollTop) + + setScrollBottom: (scrollBottom) -> + @presenter.setScrollBottom(scrollBottom) + + getScrollTop: -> + @presenter.getScrollTop() + + getScrollLeft: -> + @presenter.getScrollLeft() + + getScrollRight: -> + @presenter.getScrollRight() + + getScrollBottom: -> + @presenter.getScrollBottom() + + getScrollHeight: -> + @presenter.getScrollHeight() + + getScrollWidth: -> + @presenter.getScrollWidth() + + getVerticalScrollbarWidth: -> + @presenter.getVerticalScrollbarWidth() + + getHorizontalScrollbarHeight: -> + @presenter.getHorizontalScrollbarHeight() + + getVisibleRowRange: -> + @presenter.getVisibleRowRange() + + pixelPositionForScreenPosition: (screenPosition) -> + position = @presenter.pixelPositionForScreenPosition(screenPosition) + position.top += @presenter.getScrollTop() + position.left += @presenter.getScrollLeft() + position + + screenPositionForPixelPosition: (pixelPosition) -> + @presenter.screenPositionForPixelPosition(pixelPosition) + + pixelRectForScreenRange: (screenRange) -> + rect = @presenter.pixelRectForScreenRange(screenRange) + rect.top += @presenter.getScrollTop() + rect.bottom += @presenter.getScrollTop() + rect.left += @presenter.getScrollLeft() + rect.right += @presenter.getScrollLeft() + rect + + pixelRangeForScreenRange: (screenRange, clip=true) -> + {start, end} = Range.fromObject(screenRange) + {start: @pixelPositionForScreenPosition(start, clip), end: @pixelPositionForScreenPosition(end, clip)} + + pixelPositionForBufferPosition: (bufferPosition) -> + @pixelPositionForScreenPosition( + @editor.screenPositionForBufferPosition(bufferPosition) + ) + onMouseDown: (event) => unless event.button is 0 or (event.button is 1 and process.platform is 'linux') # Only handle mouse down events for left mouse button on all platforms @@ -546,9 +625,11 @@ class TextEditorComponent if mouseYDelta? @presenter.setScrollTop(@presenter.getScrollTop() + yDirection * scaleScrollDelta(mouseYDelta)) + @presenter.commitPendingScrollTopPosition() if mouseXDelta? @presenter.setScrollLeft(@presenter.getScrollLeft() + xDirection * scaleScrollDelta(mouseXDelta)) + @presenter.commitPendingScrollLeftPosition() scaleScrollDelta = (scrollDelta) -> Math.pow(scrollDelta / 2, 3) / 280 @@ -774,19 +855,22 @@ class TextEditorComponent screenPositionForMouseEvent: (event, linesClientRect) -> pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect) - @editor.screenPositionForPixelPosition(pixelPosition) + @presenter.screenPositionForPixelPosition(pixelPosition) pixelPositionForMouseEvent: (event, linesClientRect) -> {clientX, clientY} = event linesClientRect ?= @linesComponent.getDomNode().getBoundingClientRect() - top = clientY - linesClientRect.top + @presenter.scrollTop - left = clientX - linesClientRect.left + @presenter.scrollLeft - bottom = linesClientRect.top + @presenter.scrollTop + linesClientRect.height - clientY - right = linesClientRect.left + @presenter.scrollLeft + linesClientRect.width - clientX + top = clientY - linesClientRect.top + @presenter.getRealScrollTop() + left = clientX - linesClientRect.left + @presenter.getRealScrollLeft() + bottom = linesClientRect.top + @presenter.getRealScrollTop() + linesClientRect.height - clientY + right = linesClientRect.left + @presenter.getRealScrollLeft() + linesClientRect.width - clientX {top, left, bottom, right} + getGutterWidth: -> + @presenter.getGutterWidth() + getModel: -> @editor diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 8d5d5c1f7..685877a2e 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -181,7 +181,7 @@ class TextEditorElement extends HTMLElement # # Returns an {Object} with two values: `top` and `left`, representing the pixel position. pixelPositionForBufferPosition: (bufferPosition) -> - @getModel().pixelPositionForBufferPosition(bufferPosition, true) + @component.pixelPositionForBufferPosition(bufferPosition) # Extended: Converts a screen position to a pixel position. # @@ -190,21 +190,21 @@ class TextEditorElement extends HTMLElement # # Returns an {Object} with two values: `top` and `left`, representing the pixel positions. pixelPositionForScreenPosition: (screenPosition) -> - @getModel().pixelPositionForScreenPosition(screenPosition, true) + @component.pixelPositionForScreenPosition(screenPosition) # Extended: Retrieves the number of the row that is visible and currently at the # top of the editor. # # Returns a {Number}. getFirstVisibleScreenRow: -> - @getModel().getFirstVisibleScreenRow(true) + @getVisibleRowRange()[0] # Extended: Retrieves the number of the row that is visible and currently at the # bottom of the editor. # # Returns a {Number}. getLastVisibleScreenRow: -> - @getModel().getLastVisibleScreenRow(true) + @getVisibleRowRange()[1] # Extended: call the given `callback` when the editor is attached to the DOM. # @@ -218,6 +218,90 @@ class TextEditorElement extends HTMLElement onDidDetach: (callback) -> @emitter.on("did-detach", callback) + onDidChangeScrollTop: (callback) -> + @component.onDidChangeScrollTop(callback) + + onDidChangeScrollLeft: (callback) -> + @component.onDidChangeScrollLeft(callback) + + setScrollLeft: (scrollLeft) -> + @component.setScrollLeft(scrollLeft) + + setScrollRight: (scrollRight) -> + @component.setScrollRight(scrollRight) + + setScrollTop: (scrollTop) -> + @component.setScrollTop(scrollTop) + + setScrollBottom: (scrollBottom) -> + @component.setScrollBottom(scrollBottom) + + # Essential: Scrolls the editor to the top + scrollToTop: -> + @setScrollTop(0) + + # Essential: Scrolls the editor to the bottom + scrollToBottom: -> + @setScrollBottom(Infinity) + + getScrollTop: -> + @component.getScrollTop() + + getScrollLeft: -> + @component.getScrollLeft() + + getScrollRight: -> + @component.getScrollRight() + + getScrollBottom: -> + @component.getScrollBottom() + + getScrollHeight: -> + @component.getScrollHeight() + + getScrollWidth: -> + @component.getScrollWidth() + + getVerticalScrollbarWidth: -> + @component.getVerticalScrollbarWidth() + + getHorizontalScrollbarHeight: -> + @component.getHorizontalScrollbarHeight() + + getVisibleRowRange: -> + @component.getVisibleRowRange() + + intersectsVisibleRowRange: (startRow, endRow) -> + [visibleStart, visibleEnd] = @getVisibleRowRange() + not (endRow <= visibleStart or visibleEnd <= startRow) + + selectionIntersectsVisibleRowRange: (selection) -> + {start, end} = selection.getScreenRange() + @intersectsVisibleRowRange(start.row, end.row + 1) + + screenPositionForPixelPosition: (pixelPosition) -> + @component.screenPositionForPixelPosition(pixelPosition) + + pixelRectForScreenRange: (screenRange) -> + @component.pixelRectForScreenRange(screenRange) + + pixelRangeForScreenRange: (screenRange) -> + @component.pixelRangeForScreenRange(screenRange) + + setWidth: (width) -> + @style.width = (@component.getGutterWidth() + width) + "px" + @component.measureDimensions() + + getWidth: -> + @style.offsetWidth - @component.getGutterWidth() + + setHeight: (height) -> + @style.height = height + "px" + @component.measureDimensions() + + getHeight: -> + @style.offsetHeight + stopEventPropagation = (commandListeners) -> newCommandListeners = {} for commandName, commandListener of commandListeners diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index e6a9043c5..658d1672c 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -14,7 +14,7 @@ class TextEditorPresenter minimumReflowInterval: 200 constructor: (params) -> - {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params + {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @scrollColumn, @scrollRow, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params {horizontalScrollbarHeight, verticalScrollbarWidth} = params {@lineHeight, @baseCharacterWidth, @backgroundColor, @gutterBackgroundColor, @tileSize} = params {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params @@ -32,6 +32,7 @@ class TextEditorPresenter @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterNameAndScreenRow = {} @transferMeasurementsToModel() + @transferMeasurementsFromModel() @observeModel() @observeConfig() @buildState() @@ -50,14 +51,11 @@ class TextEditorPresenter @emitter.emit "did-update-state" if @isBatching() transferMeasurementsToModel: -> - @model.setHeight(@explicitHeight) if @explicitHeight? - @model.setWidth(@contentFrameWidth) if @contentFrameWidth? @model.setLineHeightInPixels(@lineHeight) if @lineHeight? @model.setDefaultCharWidth(@baseCharacterWidth) if @baseCharacterWidth? - @model.setScrollTop(@scrollTop) if @scrollTop? - @model.setScrollLeft(@scrollLeft) if @scrollLeft? - @model.setVerticalScrollbarWidth(@measuredVerticalScrollbarWidth) if @measuredVerticalScrollbarWidth? - @model.setHorizontalScrollbarHeight(@measuredHorizontalScrollbarHeight) if @measuredHorizontalScrollbarHeight? + + transferMeasurementsFromModel: -> + @editorWidthInChars = @model.getEditorWidthInChars() # Private: Determines whether {TextEditorPresenter} is currently batching changes. # Returns a {Boolean}, `true` if is collecting changes, `false` if is applying them. @@ -71,6 +69,7 @@ class TextEditorPresenter @updateContentDimensions() @updateScrollbarDimensions() + @updateScrollPosition() @updateStartRow() @updateEndRow() @updateCommonGutterState() @@ -115,8 +114,6 @@ class TextEditorPresenter observeModel: -> @disposables.add @model.onDidChange => - @updateContentDimensions() - @shouldUpdateHeightState = true @shouldUpdateVerticalScrollState = true @shouldUpdateHorizontalScrollState = true @@ -162,8 +159,7 @@ class TextEditorPresenter @disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this)) @disposables.add @model.onDidAddCursor(@didAddCursor.bind(this)) - @disposables.add @model.onDidChangeScrollTop(@setScrollTop.bind(this)) - @disposables.add @model.onDidChangeScrollLeft(@setScrollLeft.bind(this)) + @disposables.add @model.onDidRequestAutoscroll(@requestAutoscroll.bind(this)) @observeDecoration(decoration) for decoration in @model.getDecorations() @observeCursor(cursor) for cursor in @model.getCursors() @disposables.add @model.onDidAddGutter(@didAddGutter.bind(this)) @@ -235,6 +231,7 @@ class TextEditorPresenter @shouldUpdateLineNumbersState = true @updateContentDimensions() + @updateScrollPosition() @updateScrollbarDimensions() @updateStartRow() @updateEndRow() @@ -690,6 +687,8 @@ class TextEditorPresenter return unless @height? and @horizontalScrollbarHeight? clientHeight = @height - @horizontalScrollbarHeight + @model.setHeight(clientHeight, true) + unless @clientHeight is clientHeight @clientHeight = clientHeight @updateScrollHeight() @@ -699,6 +698,8 @@ class TextEditorPresenter return unless @contentFrameWidth? and @verticalScrollbarWidth? clientWidth = @contentFrameWidth - @verticalScrollbarWidth + @model.setWidth(clientWidth, true) unless @editorWidthInChars + unless @clientWidth is clientWidth @clientWidth = clientWidth @updateScrollWidth() @@ -708,6 +709,7 @@ class TextEditorPresenter scrollTop = @constrainScrollTop(@scrollTop) unless @scrollTop is scrollTop @scrollTop = scrollTop + @realScrollTop = @scrollTop @updateStartRow() @updateEndRow() @@ -717,6 +719,7 @@ class TextEditorPresenter updateScrollLeft: -> @scrollLeft = @constrainScrollLeft(@scrollLeft) + @realScrollLeft = @scrollLeft constrainScrollLeft: (scrollLeft) -> return scrollLeft unless scrollLeft? and @scrollWidth? and @clientWidth? @@ -809,26 +812,28 @@ class TextEditorPresenter @emitDidUpdateState() setScrollTop: (scrollTop) -> - scrollTop = @constrainScrollTop(scrollTop) + return unless scrollTop? - unless @scrollTop is scrollTop or Number.isNaN(scrollTop) - @scrollTop = scrollTop - @model.setScrollTop(scrollTop) - @didStartScrolling() - @shouldUpdateVerticalScrollState = true - @shouldUpdateHiddenInputState = true - @shouldUpdateDecorations = true - @shouldUpdateLinesState = true - @shouldUpdateCursorsState = true - @shouldUpdateLineNumbersState = true - @shouldUpdateCustomGutterDecorationState = true - @shouldUpdateOverlaysState = true + @pendingScrollLogicalPosition = null + @pendingScrollTop = scrollTop - @emitDidUpdateState() + @shouldUpdateVerticalScrollState = true + @shouldUpdateHiddenInputState = true + @shouldUpdateDecorations = true + @shouldUpdateLinesState = true + @shouldUpdateCursorsState = true + @shouldUpdateLineNumbersState = true + @shouldUpdateCustomGutterDecorationState = true + @shouldUpdateOverlaysState = true + + @emitDidUpdateState() getScrollTop: -> @scrollTop + getRealScrollTop: -> + @realScrollTop ? @scrollTop + didStartScrolling: -> if @stoppedScrollingTimeoutId? clearTimeout(@stoppedScrollingTimeoutId) @@ -848,28 +853,58 @@ class TextEditorPresenter @emitDidUpdateState() setScrollLeft: (scrollLeft) -> - scrollLeft = @constrainScrollLeft(scrollLeft) - unless @scrollLeft is scrollLeft or Number.isNaN(scrollLeft) - oldScrollLeft = @scrollLeft - @scrollLeft = scrollLeft - @model.setScrollLeft(scrollLeft) - @shouldUpdateHorizontalScrollState = true - @shouldUpdateHiddenInputState = true - @shouldUpdateCursorsState = true - @shouldUpdateOverlaysState = true - @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + return unless scrollLeft? - @emitDidUpdateState() + @pendingScrollLogicalPosition = null + @pendingScrollLeft = scrollLeft + + @shouldUpdateHorizontalScrollState = true + @shouldUpdateHiddenInputState = true + @shouldUpdateCursorsState = true + @shouldUpdateOverlaysState = true + @shouldUpdateDecorations = true + @shouldUpdateLinesState = true + + @emitDidUpdateState() getScrollLeft: -> @scrollLeft + getRealScrollLeft: -> + @realScrollLeft ? @scrollLeft + + getClientHeight: -> + if @clientHeight + @clientHeight + else + @explicitHeight - @horizontalScrollbarHeight + + getClientWidth: -> + if @clientWidth + @clientWidth + else + @contentFrameWidth - @verticalScrollbarWidth + + getScrollBottom: -> @getScrollTop() + @getClientHeight() + setScrollBottom: (scrollBottom) -> + @setScrollTop(scrollBottom - @getClientHeight()) + @getScrollBottom() + + getScrollRight: -> @getScrollLeft() + @getClientWidth() + setScrollRight: (scrollRight) -> + @setScrollLeft(scrollRight - @getClientWidth()) + @getScrollRight() + + getScrollHeight: -> + @scrollHeight + + getScrollWidth: -> + @scrollWidth + setHorizontalScrollbarHeight: (horizontalScrollbarHeight) -> unless @measuredHorizontalScrollbarHeight is horizontalScrollbarHeight oldHorizontalScrollbarHeight = @measuredHorizontalScrollbarHeight @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight - @model.setHorizontalScrollbarHeight(horizontalScrollbarHeight) @shouldUpdateScrollbarsState = true @shouldUpdateVerticalScrollState = true @shouldUpdateHorizontalScrollState = true @@ -881,7 +916,6 @@ class TextEditorPresenter unless @measuredVerticalScrollbarWidth is verticalScrollbarWidth oldVerticalScrollbarWidth = @measuredVerticalScrollbarWidth @measuredVerticalScrollbarWidth = verticalScrollbarWidth - @model.setVerticalScrollbarWidth(verticalScrollbarWidth) @shouldUpdateScrollbarsState = true @shouldUpdateVerticalScrollState = true @shouldUpdateHorizontalScrollState = true @@ -899,7 +933,6 @@ class TextEditorPresenter setExplicitHeight: (explicitHeight) -> unless @explicitHeight is explicitHeight @explicitHeight = explicitHeight - @model.setHeight(explicitHeight) @updateHeight() @shouldUpdateVerticalScrollState = true @shouldUpdateScrollbarsState = true @@ -921,10 +954,10 @@ class TextEditorPresenter @updateEndRow() setContentFrameWidth: (contentFrameWidth) -> - unless @contentFrameWidth is contentFrameWidth + if @contentFrameWidth isnt contentFrameWidth or @editorWidthInChars? oldContentFrameWidth = @contentFrameWidth @contentFrameWidth = contentFrameWidth - @model.setWidth(contentFrameWidth) + @editorWidthInChars = null @updateScrollbarDimensions() @updateClientWidth() @shouldUpdateVerticalScrollState = true @@ -982,6 +1015,9 @@ class TextEditorPresenter @gutterWidth = gutterWidth @updateOverlaysState() + getGutterWidth: -> + @gutterWidth + setLineHeight: (lineHeight) -> unless @lineHeight is lineHeight @lineHeight = lineHeight @@ -1438,3 +1474,179 @@ class TextEditorPresenter @startBlinkingCursorsAfterDelay ?= _.debounce(@startBlinkingCursors, @getCursorBlinkResumeDelay()) @startBlinkingCursorsAfterDelay() @emitDidUpdateState() + + requestAutoscroll: (position) -> + @pendingScrollLogicalPosition = position + @pendingScrollTop = null + @pendingScrollLeft = null + + @shouldUpdateCursorsState = true + @shouldUpdateCustomGutterDecorationState = true + @shouldUpdateDecorations = true + @shouldUpdateHiddenInputState = true + @shouldUpdateHorizontalScrollState = true + @shouldUpdateLinesState = true + @shouldUpdateLineNumbersState = true + @shouldUpdateOverlaysState = true + @shouldUpdateScrollPosition = true + @shouldUpdateVerticalScrollState = true + + @emitDidUpdateState() + + getVerticalScrollMarginInPixels: -> + @model.getVerticalScrollMargin() * @lineHeight + + getHorizontalScrollMarginInPixels: -> + @model.getHorizontalScrollMargin() * @baseCharacterWidth + + getVerticalScrollbarWidth: -> + @verticalScrollbarWidth + + getHorizontalScrollbarHeight: -> + @horizontalScrollbarHeight + + commitPendingLogicalScrollPosition: -> + return unless @pendingScrollLogicalPosition? + + {screenRange, options} = @pendingScrollLogicalPosition + + verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels() + horizontalScrollMarginInPixels = @getHorizontalScrollMarginInPixels() + + {top, left} = @pixelRectForScreenRange(new Range(screenRange.start, screenRange.start)) + {top: endTop, left: endLeft, height: endHeight} = @pixelRectForScreenRange(new Range(screenRange.end, screenRange.end)) + bottom = endTop + endHeight + right = endLeft + + top += @scrollTop + bottom += @scrollTop + left += @scrollLeft + right += @scrollLeft + + if options?.center + desiredScrollCenter = (top + bottom) / 2 + unless @getScrollTop() < desiredScrollCenter < @getScrollBottom() + desiredScrollTop = desiredScrollCenter - @getClientHeight() / 2 + desiredScrollBottom = desiredScrollCenter + @getClientHeight() / 2 + else + desiredScrollTop = top - verticalScrollMarginInPixels + desiredScrollBottom = bottom + verticalScrollMarginInPixels + + desiredScrollLeft = left - horizontalScrollMarginInPixels + desiredScrollRight = right + horizontalScrollMarginInPixels + + if options?.reversed ? true + if desiredScrollBottom > @getScrollBottom() + @setScrollBottom(desiredScrollBottom) + if desiredScrollTop < @getScrollTop() + @setScrollTop(desiredScrollTop) + + if desiredScrollRight > @getScrollRight() + @setScrollRight(desiredScrollRight) + if desiredScrollLeft < @getScrollLeft() + @setScrollLeft(desiredScrollLeft) + else + if desiredScrollTop < @getScrollTop() + @setScrollTop(desiredScrollTop) + if desiredScrollBottom > @getScrollBottom() + @setScrollBottom(desiredScrollBottom) + + if desiredScrollLeft < @getScrollLeft() + @setScrollLeft(desiredScrollLeft) + if desiredScrollRight > @getScrollRight() + @setScrollRight(desiredScrollRight) + + @pendingScrollLogicalPosition = null + + commitPendingScrollLeftPosition: -> + return unless @pendingScrollLeft? + + scrollLeft = @constrainScrollLeft(@pendingScrollLeft) + if scrollLeft isnt @scrollLeft and not Number.isNaN(scrollLeft) + @realScrollLeft = scrollLeft + @scrollLeft = Math.round(scrollLeft) + @scrollColumn = Math.round(@scrollLeft / @baseCharacterWidth) + @model.setScrollColumn(@scrollColumn) + + @emitter.emit 'did-change-scroll-left', @scrollLeft + + @pendingScrollLeft = null + + commitPendingScrollTopPosition: -> + return unless @pendingScrollTop? + + scrollTop = @constrainScrollTop(@pendingScrollTop) + if scrollTop isnt @scrollTop and not Number.isNaN(scrollTop) + @realScrollTop = scrollTop + @scrollTop = Math.round(scrollTop) + @scrollRow = Math.round(@scrollTop / @lineHeight) + @model.setScrollRow(@scrollRow) + + @didStartScrolling() + @emitter.emit 'did-change-scroll-top', @scrollTop + + @pendingScrollTop = null + + restoreScrollPosition: -> + return if @hasRestoredScrollPosition or not @hasPixelPositionRequirements() + + @setScrollTop(@scrollRow * @lineHeight) if @scrollRow? + @setScrollLeft(@scrollColumn * @baseCharacterWidth) if @scrollColumn? + + @hasRestoredScrollPosition = true + + updateScrollPosition: -> + @restoreScrollPosition() + @commitPendingLogicalScrollPosition() + @commitPendingScrollLeftPosition() + @commitPendingScrollTopPosition() + + canScrollLeftTo: (scrollLeft) -> + @scrollLeft isnt @constrainScrollLeft(scrollLeft) + + canScrollTopTo: (scrollTop) -> + @scrollTop isnt @constrainScrollTop(scrollTop) + + onDidChangeScrollTop: (callback) -> + @emitter.on 'did-change-scroll-top', callback + + onDidChangeScrollLeft: (callback) -> + @emitter.on 'did-change-scroll-left', callback + + getVisibleRowRange: -> + [@startRow, @endRow] + + screenPositionForPixelPosition: (pixelPosition) -> + targetTop = pixelPosition.top + targetLeft = pixelPosition.left + defaultCharWidth = @baseCharacterWidth + row = Math.floor(targetTop / @lineHeight) + targetLeft = 0 if row < 0 + targetLeft = Infinity if row > @model.getLastScreenRow() + row = Math.min(row, @model.getLastScreenRow()) + row = Math.max(0, row) + + left = 0 + column = 0 + + iterator = @model.tokenizedLineForScreenRow(row).getTokenIterator() + while iterator.next() + charWidths = @getScopedCharacterWidths(iterator.getScopes()) + value = iterator.getText() + valueIndex = 0 + while valueIndex < value.length + if iterator.isPairedCharacter() + char = value + charLength = 2 + valueIndex += 2 + else + char = value[valueIndex] + charLength = 1 + valueIndex++ + + charWidth = charWidths[char] ? defaultCharWidth + break if targetLeft <= left + (charWidth / 2) + left += charWidth + column += charLength + + new Point(row, column) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 8dce16439..04f386439 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -80,7 +80,7 @@ class TextEditor extends Model state.registerEditor = true new this(state) - constructor: ({@softTabs, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, largeFileMode}={}) -> + constructor: ({@softTabs, @scrollRow, @scrollColumn, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, largeFileMode}={}) -> super @emitter = new Emitter @@ -109,12 +109,6 @@ class TextEditor extends Model @setEncoding(atom.config.get('core.fileEncoding', scope: @getRootScopeDescriptor())) - @disposables.add @displayBuffer.onDidChangeScrollTop (scrollTop) => - @emitter.emit 'did-change-scroll-top', scrollTop - - @disposables.add @displayBuffer.onDidChangeScrollLeft (scrollLeft) => - @emitter.emit 'did-change-scroll-left', scrollLeft - @gutterContainer = new GutterContainer(this) @lineNumberGutter = @gutterContainer.addGutter name: 'line-number' @@ -127,8 +121,8 @@ class TextEditor extends Model deserializer: 'TextEditor' id: @id softTabs: @softTabs - scrollTop: @scrollTop - scrollLeft: @scrollLeft + scrollRow: @getScrollRow() + scrollColumn: @getScrollColumn() displayBuffer: @displayBuffer.serialize() subscribeToBuffer: -> @@ -433,10 +427,17 @@ class TextEditor extends Model @displayBuffer.onDidChangeCharacterWidths(callback) onDidChangeScrollTop: (callback) -> - @emitter.on 'did-change-scroll-top', callback + Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.") + + atom.views.getView(this).onDidChangeScrollTop(callback) onDidChangeScrollLeft: (callback) -> - @emitter.on 'did-change-scroll-left', callback + Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.") + + atom.views.getView(this).onDidChangeScrollLeft(callback) + + onDidRequestAutoscroll: (callback) -> + @displayBuffer.onDidRequestAutoscroll(callback) # TODO Remove once the tabs package no longer uses .on subscriptions onDidChangeIcon: (callback) -> @@ -525,6 +526,10 @@ class TextEditor extends Model setEditorWidthInChars: (editorWidthInChars) -> @displayBuffer.setEditorWidthInChars(editorWidthInChars) + # Returns the editor width in characters. + getEditorWidthInChars: -> + @displayBuffer.getEditorWidthInChars() + ### Section: File Details ### @@ -2852,25 +2857,27 @@ class TextEditor extends Model scrollToScreenPosition: (screenPosition, options) -> @displayBuffer.scrollToScreenPosition(screenPosition, options) - # Essential: Scrolls the editor to the top scrollToTop: -> - @setScrollTop(0) + Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") + + atom.views.getView(this).scrollToTop() - # Essential: Scrolls the editor to the bottom scrollToBottom: -> - @setScrollBottom(Infinity) + Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") + + atom.views.getView(this).scrollToBottom() scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options) - horizontallyScrollable: -> @displayBuffer.horizontallyScrollable() + getHorizontalScrollbarHeight: -> + Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.") - verticallyScrollable: -> @displayBuffer.verticallyScrollable() + atom.views.getView(this).getHorizontalScrollbarHeight() - getHorizontalScrollbarHeight: -> @displayBuffer.getHorizontalScrollbarHeight() - setHorizontalScrollbarHeight: (height) -> @displayBuffer.setHorizontalScrollbarHeight(height) + getVerticalScrollbarWidth: -> + Grim.deprecate("This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.") - getVerticalScrollbarWidth: -> @displayBuffer.getVerticalScrollbarWidth() - setVerticalScrollbarWidth: (width) -> @displayBuffer.setVerticalScrollbarWidth(width) + atom.views.getView(this).getVerticalScrollbarWidth() pageUp: -> @moveUp(@getRowsPerPage()) @@ -2933,25 +2940,21 @@ class TextEditor extends Model @placeholderText = placeholderText @emitter.emit 'did-change-placeholder-text', @placeholderText - getFirstVisibleScreenRow: (suppressDeprecation) -> - unless suppressDeprecation - deprecate("This is now a view method. Call TextEditorElement::getFirstVisibleScreenRow instead.") - @getVisibleRowRange()[0] + getFirstVisibleScreenRow: -> + deprecate("This is now a view method. Call TextEditorElement::getFirstVisibleScreenRow instead.") + atom.views.getView(this).getVisibleRowRange()[0] - getLastVisibleScreenRow: (suppressDeprecation) -> - unless suppressDeprecation - Grim.deprecate("This is now a view method. Call TextEditorElement::getLastVisibleScreenRow instead.") - @getVisibleRowRange()[1] + getLastVisibleScreenRow: -> + Grim.deprecate("This is now a view method. Call TextEditorElement::getLastVisibleScreenRow instead.") + atom.views.getView(this).getVisibleRowRange()[1] - pixelPositionForBufferPosition: (bufferPosition, suppressDeprecation) -> - unless suppressDeprecation - Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead") - @displayBuffer.pixelPositionForBufferPosition(bufferPosition) + pixelPositionForBufferPosition: (bufferPosition) -> + Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead") + atom.views.getView(this).pixelPositionForBufferPosition(bufferPosition) - pixelPositionForScreenPosition: (screenPosition, suppressDeprecation) -> - unless suppressDeprecation - Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead") - @displayBuffer.pixelPositionForScreenPosition(screenPosition) + pixelPositionForScreenPosition: (screenPosition) -> + Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead") + atom.views.getView(this).pixelPositionForScreenPosition(screenPosition) getSelectionMarkerAttributes: -> {type: 'selection', editorId: @id, invalidate: 'never', maintainHistory: true} @@ -2977,38 +2980,110 @@ class TextEditor extends Model getDefaultCharWidth: -> @displayBuffer.getDefaultCharWidth() setDefaultCharWidth: (defaultCharWidth) -> @displayBuffer.setDefaultCharWidth(defaultCharWidth) - setHeight: (height) -> @displayBuffer.setHeight(height) - getHeight: -> @displayBuffer.getHeight() + setHeight: (height, reentrant=false) -> + if reentrant + @displayBuffer.setHeight(height) + else + Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.") + atom.views.getView(this).setHeight(height) + + getHeight: -> + Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.") + @displayBuffer.getHeight() getClientHeight: -> @displayBuffer.getClientHeight() - setWidth: (width) -> @displayBuffer.setWidth(width) - getWidth: -> @displayBuffer.getWidth() + setWidth: (width, reentrant=false) -> + if reentrant + @displayBuffer.setWidth(width) + else + Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.") + atom.views.getView(this).setWidth(width) - getScrollTop: -> @displayBuffer.getScrollTop() - setScrollTop: (scrollTop) -> @displayBuffer.setScrollTop(scrollTop) + getWidth: -> + Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.") + @displayBuffer.getWidth() - getScrollBottom: -> @displayBuffer.getScrollBottom() - setScrollBottom: (scrollBottom) -> @displayBuffer.setScrollBottom(scrollBottom) + getScrollRow: -> @scrollRow + setScrollRow: (@scrollRow) -> - getScrollLeft: -> @displayBuffer.getScrollLeft() - setScrollLeft: (scrollLeft) -> @displayBuffer.setScrollLeft(scrollLeft) + getScrollColumn: -> @scrollColumn + setScrollColumn: (@scrollColumn) -> - getScrollRight: -> @displayBuffer.getScrollRight() - setScrollRight: (scrollRight) -> @displayBuffer.setScrollRight(scrollRight) + getScrollTop: -> + Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.") - getScrollHeight: -> @displayBuffer.getScrollHeight() - getScrollWidth: -> @displayBuffer.getScrollWidth() + atom.views.getView(this).getScrollTop() - getVisibleRowRange: -> @displayBuffer.getVisibleRowRange() + setScrollTop: (scrollTop) -> + Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollTop instead.") - intersectsVisibleRowRange: (startRow, endRow) -> @displayBuffer.intersectsVisibleRowRange(startRow, endRow) + atom.views.getView(this).setScrollTop(scrollTop) - selectionIntersectsVisibleRowRange: (selection) -> @displayBuffer.selectionIntersectsVisibleRowRange(selection) + getScrollBottom: -> + Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollBottom instead.") - screenPositionForPixelPosition: (pixelPosition) -> @displayBuffer.screenPositionForPixelPosition(pixelPosition) + atom.views.getView(this).getScrollBottom() - pixelRectForScreenRange: (screenRange) -> @displayBuffer.pixelRectForScreenRange(screenRange) + setScrollBottom: (scrollBottom) -> + Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollBottom instead.") + + atom.views.getView(this).setScrollBottom(scrollBottom) + + getScrollLeft: -> + Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollLeft instead.") + + atom.views.getView(this).getScrollLeft() + + setScrollLeft: (scrollLeft) -> + Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollLeft instead.") + + atom.views.getView(this).setScrollLeft(scrollLeft) + + getScrollRight: -> + Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollRight instead.") + + atom.views.getView(this).getScrollRight() + + setScrollRight: (scrollRight) -> + Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollRight instead.") + + atom.views.getView(this).setScrollRight(scrollRight) + + getScrollHeight: -> + Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollHeight instead.") + + atom.views.getView(this).getScrollHeight() + + getScrollWidth: -> + Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollWidth instead.") + + atom.views.getView(this).getScrollWidth() + + getVisibleRowRange: -> + Grim.deprecate("This is now a view method. Call TextEditorElement::getVisibleRowRange instead.") + + atom.views.getView(this).getVisibleRowRange() + + intersectsVisibleRowRange: (startRow, endRow) -> + Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.") + + atom.views.getView(this).intersectsVisibleRowRange(startRow, endRow) + + selectionIntersectsVisibleRowRange: (selection) -> + Grim.deprecate("This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.") + + atom.views.getView(this).selectionIntersectsVisibleRowRange(selection) + + screenPositionForPixelPosition: (pixelPosition) -> + Grim.deprecate("This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.") + + atom.views.getView(this).screenPositionForPixelPosition(pixelPosition) + + pixelRectForScreenRange: (screenRange) -> + Grim.deprecate("This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.") + + atom.views.getView(this).pixelRectForScreenRange(screenRange) ### Section: Utility