diff --git a/tests/frontend/specs/scroll.js b/tests/frontend/specs/scroll.js new file mode 100644 index 000000000..08abd84fc --- /dev/null +++ b/tests/frontend/specs/scroll.js @@ -0,0 +1,644 @@ +describe('scroll when focus line is out of viewport', function () { + before(function (done) { + helper.newPad(function(){ + cleanPad(function(){ + forceUseMonospacedFont(); + scrollWhenPlaceCaretInTheLastLineOfViewport(); + createPadWithSeveralLines(function(){ + resizeEditorHeight(); + done(); + }); + }); + }); + this.timeout(20000); + }); + + context('when user presses any arrow keys on a line above the viewport', function(){ + context('and scroll percentage config is set to 0.2 on settings.json', function(){ + var lineCloseOfTopOfPad = 10; + before(function (done) { + setScrollPercentageWhenFocusLineIsOutOfViewport(0.2, true); + scrollEditorToBottomOfPad(); + + placeCaretInTheBeginningOfLine(lineCloseOfTopOfPad, function(){ // place caret in the 10th line + // warning: even pressing right arrow, the caret does not change of position + // the column where the caret is, it has not importance, only the line + pressAndReleaseRightArrow(); + done(); + }); + }); + + it('keeps the focus line scrolled 20% from the top of the viewport', function (done) { + // default behavior is to put the line in the top of viewport, but as + // scrollPercentageWhenFocusLineIsOutOfViewport is set to 0.2, we have an extra 20% of lines scrolled + // (2 lines, which are the 20% of the 10 that are visible on viewport) + var firstLineOfViewport = getFirstLineVisibileOfViewport(); + expect(lineCloseOfTopOfPad).to.be(firstLineOfViewport + 2); + done(); + }); + }); + }); + + context('when user presses any arrow keys on a line below the viewport', function(){ + context('and scroll percentage config is set to 0.7 on settings.json', function(){ + var lineCloseToBottomOfPad = 50; + before(function (done) { + setScrollPercentageWhenFocusLineIsOutOfViewport(0.7); + + // firstly, scroll to make the lineCloseToBottomOfPad visible. After that, scroll to make it out of viewport + scrollEditorToTopOfPad(); + placeCaretAtTheEndOfLine(lineCloseToBottomOfPad); // place caret in the 50th line + setTimeout(function() { + // warning: even pressing right arrow, the caret does not change of position + pressAndReleaseLeftArrow(); + done(); + }, 1000); + }); + it('keeps the focus line scrolled 70% from the bottom of the viewport', function (done) { + // default behavior is to put the line in the top of viewport, but as + // scrollPercentageWhenFocusLineIsOutOfViewport is set to 0.7, we have an extra 70% of lines scrolled + // (7 lines, which are the 70% of the 10 that are visible on viewport) + var lastLineOfViewport = getLastLineVisibleOfViewport(); + expect(lineCloseToBottomOfPad).to.be(lastLineOfViewport - 8); + done(); + }); + }); + }); + + context('when user presses arrow up on the first line of the viewport', function(){ + context('and percentageToScrollWhenUserPressesArrowUp is set to 0.3', function () { + var lineOnTopOfViewportWhenThePadIsScrolledDown; + before(function (done) { + setPercentageToScrollWhenUserPressesArrowUp(0.3); + + // we need some room to make the scroll up + scrollEditorToBottomOfPad(); + lineOnTopOfViewportWhenThePadIsScrolledDown = 91; + placeCaretAtTheEndOfLine(lineOnTopOfViewportWhenThePadIsScrolledDown); + setTimeout(function() { + // warning: even pressing up arrow, the caret does not change of position + pressAndReleaseUpArrow(); + done(); + }, 1000); + }); + it('keeps the focus line scrolled 30% of the top of the viewport', function (done) { + // default behavior is to put the line in the top of viewport, but as + // PercentageToScrollWhenUserPressesArrowUp is set to 0.3, we have an extra 30% of lines scrolled + // (3 lines, which are the 30% of the 10 that are visible on viewport) + var firstLineOfViewport = getFirstLineVisibileOfViewport(); + expect(firstLineOfViewport).to.be(lineOnTopOfViewportWhenThePadIsScrolledDown - 2); + done(); + }) + }); + }); + + context('when user edits the last line of viewport', function(){ + context('and scroll percentage config is set to 0 on settings.json', function(){ + var lastLineOfViewportBeforeEnter = 10; + before(function () { + // the default value + resetScrollPercentageWhenFocusLineIsOutOfViewport(); + + // make sure the last line on viewport is the 10th one + scrollEditorToTopOfPad(); + placeCaretAtTheEndOfLine(lastLineOfViewportBeforeEnter); + pressEnter(); + }); + it('keeps the focus line on the bottom of the viewport', function (done) { + var lastLineOfViewportAfterEnter = getLastLineVisibleOfViewport(); + expect(lastLineOfViewportAfterEnter).to.be(lastLineOfViewportBeforeEnter +2); + done(); + }); + }); + + context('and scrollPercentageWhenFocusLineIsOutOfViewport is set to 0.3', function(){ // this value is arbitrary + var lastLineOfViewportBeforeEnter = 9; + before(function () { + setScrollPercentageWhenFocusLineIsOutOfViewport(0.3); + + // make sure the last line on viewport is the 10th one + scrollEditorToTopOfPad(); + placeCaretAtTheEndOfLine(lastLineOfViewportBeforeEnter); + pressBackspace(); + }); + it('scrolls 30% of viewport up', function (done) { + var lastLineOfViewportAfterEnter = getLastLineVisibleOfViewport(); + // default behavior is to scroll one line at the bottom of viewport, but as + // scrollPercentageWhenFocusLineIsOutOfViewport is set to 0.3, we have an extra 30% of lines scrolled + // (3 lines, which are the 30% of the 10 that are visible on viewport) + expect(lastLineOfViewportAfterEnter).to.be(lastLineOfViewportBeforeEnter + 4); + done(); + }); + }); + + context('and it is set to a value that overflow the interval [0, 1]', function(){ + var lastLineOfViewportBeforeEnter = 10; + before(function(){ + var scrollPercentageWhenFocusLineIsOutOfViewport = 1.5; + scrollEditorToTopOfPad(); + placeCaretAtTheEndOfLine(lastLineOfViewportBeforeEnter); + setScrollPercentageWhenFocusLineIsOutOfViewport(scrollPercentageWhenFocusLineIsOutOfViewport); + pressEnter(); + }); + it('keeps the default behavior of moving the focus line on the bottom of the viewport', function (done) { + var lastLineOfViewportAfterEnter = getLastLineVisibleOfViewport(); + expect(lastLineOfViewportAfterEnter).to.be(lastLineOfViewportBeforeEnter + 2); + done(); + }); + }); + }); + + context('when user edits a line above the viewport', function(){ + context('and scroll percentage config is set to 0 on settings.json', function(){ + var lineCloseOfTopOfPad = 10; + before(function () { + // the default value + setScrollPercentageWhenFocusLineIsOutOfViewport(0); + + // firstly, scroll to make the lineCloseOfTopOfPad visible. After that, scroll to make it out of viewport + scrollEditorToTopOfPad(); + placeCaretAtTheEndOfLine(lineCloseOfTopOfPad); // place caret in the 10th line + scrollEditorToBottomOfPad(); + pressBackspace(); // edit the line where the caret is, which is above the viewport + }); + + it('keeps the focus line on the top of the viewport', function (done) { + var firstLineOfViewportAfterEnter = getFirstLineVisibileOfViewport(); + expect(firstLineOfViewportAfterEnter).to.be(lineCloseOfTopOfPad); + done(); + }); + }); + + context('and scrollPercentageWhenFocusLineIsOutOfViewport is set to 0.2', function(){ // this value is arbitrary + var lineCloseToBottomOfPad = 50; + before(function () { + // we force the line edited to be above the top of the viewport + setScrollPercentageWhenFocusLineIsOutOfViewport(0.2, true); // set scroll jump to 20% + scrollEditorToTopOfPad(); + placeCaretAtTheEndOfLine(lineCloseToBottomOfPad); + scrollEditorToBottomOfPad(); + pressBackspace(); // edit line + }); + + it('scrolls 20% of viewport down', function (done) { + // default behavior is to scroll one line at the top of viewport, but as + // scrollPercentageWhenFocusLineIsOutOfViewport is set to 0.2, we have an extra 20% of lines scrolled + // (2 lines, which are the 20% of the 10 that are visible on viewport) + var firstLineVisibileOfViewport = getFirstLineVisibileOfViewport(); + expect(lineCloseToBottomOfPad).to.be(firstLineVisibileOfViewport + 2); + done(); + }); + }); + }); + + context('when user places the caret at the last line visible of viewport', function(){ + var lastLineVisible; + context('and scroll percentage config is set to 0 on settings.json', function(){ + before(function (done) { + // reset to the default value + resetScrollPercentageWhenFocusLineIsOutOfViewport(); + + placeCaretInTheBeginningOfLine(0, function(){ // reset caret position + scrollEditorToTopOfPad(); + lastLineVisible = getLastLineVisibleOfViewport(); + placeCaretInTheBeginningOfLine(lastLineVisible, done); // place caret in the 9th line + }); + + }); + + it('does not scroll', function(done){ + setTimeout(function() { + var lastLineOfViewport = getLastLineVisibleOfViewport(); + var lineDoesNotScroll = lastLineOfViewport === lastLineVisible; + expect(lineDoesNotScroll).to.be(true); + done(); + }, 1000); + }); + }); + + context('and scroll percentage config is set to 0.5 on settings.json', function(){ + before(function (done) { + setScrollPercentageWhenFocusLineIsOutOfViewport(0.5); + scrollEditorToTopOfPad(); + placeCaretInTheBeginningOfLine(0, function(){ // reset caret position + // this timeout inside a callback is ugly but it necessary to give time to aceSelectionChange + // realizes that the selection has been changed + setTimeout(function() { + lastLineVisible = getLastLineVisibleOfViewport(); + placeCaretInTheBeginningOfLine(lastLineVisible, done); // place caret in the 9th line + }, 1000); + }); + }); + // TODO: I'm broken, fix me + xit('scrolls line to 50% of the viewport', function(done){ + helper.waitFor(function(){ + var lastLineOfViewport = getLastLineVisibleOfViewport(); + var lastLinesScrolledFiveLinesUp = lastLineOfViewport - 5 === lastLineVisible; + return lastLinesScrolledFiveLinesUp; + }).done(done); + }); + }); + }); + + // This is a special case. When user is selecting a text with arrow down or arrow left we have + // to keep the last line selected on focus + context('when the first line selected is out of the viewport and user presses shift arrow down', function(){ + var lastLineOfPad = 99; + before(function (done) { + scrollEditorToTopOfPad(); + + // make a selection bigger than the viewport height + var $firstLineOfSelection = getLine(0); + var $lastLineOfSelection = getLine(lastLineOfPad); + var lengthOfLastLine = $lastLineOfSelection.text().length; + helper.selectLines($firstLineOfSelection, $lastLineOfSelection, 0, lengthOfLastLine); + + // place the last line selected on the viewport + scrollEditorToBottomOfPad(); + + // press a key to make the selection goes down + // although we can't simulate the extending of selection. It's possible to send a key event + // which is captured on ace2_inner scroll function. + pressAndReleaseLeftArrow(true); + done(); + }); + + it('keeps the last line selected on focus', function (done) { + var lastLineOfSelectionIsVisible = isLineOnViewport(lastLineOfPad); + expect(lastLineOfSelectionIsVisible).to.be(true); + done(); + }); + }); + + // In this scenario we avoid the bouncing scroll. E.g Let's suppose we have a big line that is + // the size of the viewport, and its top is above the viewport. When user presses '<-', this line + // will scroll down because the top is out of the viewport. When it scrolls down, the bottom of + // line gets below the viewport so when user presses '<-' again it scrolls up to make the bottom + // of line visible. If user presses arrow keys more than one time, the editor will keep scrolling up and down + + // TODO: This test is broken, fix it. Possibly related to https://github.com/ether/etherpad-lite/pull/3837 +/* + context('when the line height is bigger than the scroll amount percentage * viewport height', function(){ + var scrollOfEditorBeforePressKey; + var BIG_LINE_NUMBER = 0; + var MIDDLE_OF_BIG_LINE = 51; + before(function (done) { + createPadWithALineHigherThanViewportHeight(this, BIG_LINE_NUMBER, function(){ + setScrollPercentageWhenFocusLineIsOutOfViewport(0.5); // set any value to force scroll to outside to viewport + var $bigLine = getLine(BIG_LINE_NUMBER); + + // each line has about 5 chars, we place the caret in the middle of the line + helper.selectLines($bigLine, $bigLine, MIDDLE_OF_BIG_LINE, MIDDLE_OF_BIG_LINE); + + scrollEditorToLeaveTopAndBottomOfBigLineOutOfViewport($bigLine); + scrollOfEditorBeforePressKey = getEditorScroll(); + + // press a key to force to scroll + pressAndReleaseRightArrow(); + done(); + }); + }); + + // reset pad to the original text + after(function (done) { + this.timeout(5000); + cleanPad(function(){ + createPadWithSeveralLines(function(){ + resetEditorWidth(); + done(); + }); + }); + }); + + // as the editor.line is inside of the viewport, it should not scroll + it('should not scroll', function (done) { + var scrollOfEditorAfterPressKey = getEditorScroll(); + expect(scrollOfEditorAfterPressKey).to.be(scrollOfEditorBeforePressKey); + done(); + }); + }); +*/ + + // Some plugins, for example the ep_page_view, change the editor dimensions. This plugin, for example, + // adds padding-top to the ace_outer, which changes the viewport height + describe('integration with plugins which changes the margin of editor', function(){ + context('when editor dimensions changes', function(){ + before(function () { + // reset the size of editor. Now we show more than 10 lines as in the other tests + resetResizeOfEditorHeight(); + scrollEditorToTopOfPad(); + + // height of the editor viewport + var editorHeight = getEditorHeight(); + + // add a big padding-top, 50% of the viewport + var paddingTopOfAceOuter = editorHeight/2; + var chrome$ = helper.padChrome$; + var $outerIframe = chrome$('iframe'); + $outerIframe.css('padding-top', paddingTopOfAceOuter); + + // we set a big value to check if the scroll is made + setScrollPercentageWhenFocusLineIsOutOfViewport(1); + }); + + context('and user places the caret in the last line visible of the pad', function(){ + var lastLineVisible; + beforeEach(function (done) { + lastLineVisible = getLastLineVisibleOfViewport(); // returned undefined, TODO: fixme. + placeCaretInTheBeginningOfLine(lastLineVisible, done); + }); + // TODO: I'm broken, fix me + xit('scrolls the line where caret is', function(done){ + helper.waitFor(function(){ + var firstLineVisibileOfViewport = getFirstLineVisibileOfViewport(); + var linesScrolled = firstLineVisibileOfViewport !== 0; + return linesScrolled; + }).done(done); + }); + }); + }); + }); + + /* ********************* Helper functions/constants ********************* */ + var TOP_OF_PAGE = 0; + var BOTTOM_OF_PAGE = 5000; // we use a big value to force the page to be scrolled all the way down + var LINES_OF_PAD = 100; + var ENTER = 13; + var BACKSPACE = 8; + var LEFT_ARROW = 37; + var UP_ARROW = 38; + var RIGHT_ARROW = 39; + var LINES_ON_VIEWPORT = 10; + var WIDTH_OF_EDITOR_RESIZED = 100; + var LONG_TEXT_CHARS = 100; + + var cleanPad = function(callback) { + var inner$ = helper.padInner$; + var $padContent = inner$('#innerdocbody'); + $padContent.html(''); + + // wait for Etherpad to re-create first line + helper.waitFor(function(){ + var lineNumber = inner$('div').length; + return lineNumber === 1; + }, 2000).done(callback); + }; + + var createPadWithSeveralLines = function(done) { + var line = 'a
'; + var $firstLine = helper.padInner$('div').first(); + var lines = line.repeat(LINES_OF_PAD); //arbitrary number, we need to create lines that is over the viewport + $firstLine.html(lines); + + helper.waitFor(function(){ + var linesCreated = helper.padInner$('div').length; + return linesCreated === LINES_OF_PAD; + }, 4000).done(done); + }; + + var createPadWithALineHigherThanViewportHeight = function(test, line, done) { + var viewportHeight = 160; //10 lines * 16px (height of line) + test.timeout(5000); + cleanPad(function(){ + // make the editor smaller to make test easier + // with that width the each line has about 5 chars + resizeEditorWidth(); + + // we create a line with 100 chars, which makes about 20 lines + setLongTextOnLine(line); + helper.waitFor(function () { + var $firstLine = getLine(line); + + var heightOfLine = $firstLine.get(0).getBoundingClientRect().height; + return heightOfLine >= viewportHeight; + }, 4000).done(done); + }); + }; + + var setLongTextOnLine = function(line) { + var $line = getLine(line); + var longText = 'a'.repeat(LONG_TEXT_CHARS); + $line.html(longText); + }; + + // resize the editor to make the tests easier + var resizeEditorHeight = function() { + var chrome$ = helper.padChrome$; + chrome$('#editorcontainer').css('height', getSizeOfViewport()); + }; + + // this makes about 5 chars per line + var resizeEditorWidth = function() { + var chrome$ = helper.padChrome$; + chrome$('#editorcontainer').css('width', WIDTH_OF_EDITOR_RESIZED); + }; + + var resetResizeOfEditorHeight = function() { + var chrome$ = helper.padChrome$; + chrome$('#editorcontainer').css('height', ''); + }; + + var resetEditorWidth = function () { + var chrome$ = helper.padChrome$; + chrome$('#editorcontainer').css('width', ''); + }; + + var getEditorHeight = function() { + var chrome$ = helper.padChrome$; + var $editor = chrome$('#editorcontainer'); + var editorHeight = $editor.get(0).clientHeight; + return editorHeight; + }; + + var getSizeOfViewport = function() { + return getLinePositionOnViewport(LINES_ON_VIEWPORT) - getLinePositionOnViewport(0); + }; + + var scrollPageTo = function(value) { + var outer$ = helper.padOuter$; + var $ace_outer = outer$('#outerdocbody').parent(); + $ace_outer.parent().scrollTop(value); + }; + + var scrollEditorToTopOfPad = function() { + scrollPageTo(TOP_OF_PAGE); + }; + + var scrollEditorToBottomOfPad = function() { + scrollPageTo(BOTTOM_OF_PAGE); + }; + + var scrollEditorToLeaveTopAndBottomOfBigLineOutOfViewport = function ($bigLine) { + var lineHeight = $bigLine.get(0).getBoundingClientRect().height; + var middleOfLine = lineHeight/2; + scrollPageTo(middleOfLine); + }; + + var getLine = function(lineNum) { + var inner$ = helper.padInner$; + var $line = inner$('div').eq(lineNum); + return $line; + }; + + var placeCaretAtTheEndOfLine = function(lineNum) { + var $targetLine = getLine(lineNum); + var lineLength = $targetLine.text().length; + helper.selectLines($targetLine, $targetLine, lineLength, lineLength); + }; + + var placeCaretInTheBeginningOfLine = function(lineNum, cb) { + var $targetLine = getLine(lineNum); + helper.selectLines($targetLine, $targetLine, 0, 0); + helper.waitFor(function() { + var $lineWhereCaretIs = getLineWhereCaretIs(); + return $targetLine.get(0) === $lineWhereCaretIs.get(0); + }).done(cb); + }; + + var getLineWhereCaretIs = function() { + var inner$ = helper.padInner$; + var nodeWhereCaretIs = inner$.document.getSelection().anchorNode; + var $lineWhereCaretIs = $(nodeWhereCaretIs).closest('div'); + return $lineWhereCaretIs; + }; + + var getFirstLineVisibileOfViewport = function() { + return _.find(_.range(0, LINES_OF_PAD - 1), isLineOnViewport); + }; + + var getLastLineVisibleOfViewport = function() { + return _.find(_.range(LINES_OF_PAD - 1, 0, -1), isLineOnViewport); + }; + + var pressKey = function(keyCode, shiftIsPressed){ + var inner$ = helper.padInner$; + var evtType = 'keydown'; + var e = inner$.Event(evtType); + e.shiftKey = shiftIsPressed; + e.keyCode = keyCode; + e.which = keyCode; // etherpad listens to 'which' + inner$('#innerdocbody').trigger(e); + }; + + var releaseKey = function(keyCode){ + var inner$ = helper.padInner$; + var evtType = 'keyup'; + var e = inner$.Event(evtType); + e.keyCode = keyCode; + e.which = keyCode; // etherpad listens to 'which' + inner$('#innerdocbody').trigger(e); + }; + + var pressEnter = function() { + pressKey(ENTER); + }; + + var pressBackspace = function() { + pressKey(BACKSPACE); + }; + + var pressAndReleaseUpArrow = function() { + pressKey(UP_ARROW); + releaseKey(UP_ARROW); + }; + + var pressAndReleaseRightArrow = function() { + pressKey(RIGHT_ARROW); + releaseKey(RIGHT_ARROW); + }; + + var pressAndReleaseLeftArrow = function(shiftIsPressed) { + pressKey(LEFT_ARROW, shiftIsPressed); + releaseKey(LEFT_ARROW); + }; + + var isLineOnViewport = function(lineNumber) { + // in the function scrollNodeVerticallyIntoView from ace2_inner.js, iframePadTop is used to calculate + // how much scroll is needed. Although the name refers to padding-top, this value is not set on the + // padding-top. + var iframePadTop = 8; + var $line = getLine(lineNumber); + var linePosition = $line.get(0).getBoundingClientRect(); + + // position relative to the current viewport + var linePositionTopOnViewport = linePosition.top - getEditorScroll() + iframePadTop; + var linePositionBottomOnViewport = linePosition.bottom - getEditorScroll(); + + var lineBellowTop = linePositionBottomOnViewport > 0; + var lineAboveBottom = linePositionTopOnViewport < getClientHeightVisible(); + var isVisible = lineBellowTop && lineAboveBottom; + + return isVisible; + }; + + var getEditorScroll = function () { + var outer$ = helper.padOuter$; + var scrollTopFirefox = outer$('#outerdocbody').parent().scrollTop(); // works only on firefox + var scrollTop = outer$('#outerdocbody').scrollTop() || scrollTopFirefox; + return scrollTop; + }; + + // clientHeight includes padding, so we have to subtract it and consider only the visible viewport + var getClientHeightVisible = function () { + var outer$ = helper.padOuter$; + var $ace_outer = outer$('#outerdocbody').parent(); + var ace_outerHeight = $ace_outer.get(0).clientHeight; + var ace_outerPaddingTop = getIntValueOfCSSProperty($ace_outer, 'padding-top'); + var paddingAddedWhenPageViewIsEnable = getPaddingAddedWhenPageViewIsEnable(); + var clientHeight = ace_outerHeight - ( ace_outerPaddingTop + paddingAddedWhenPageViewIsEnable); + + return clientHeight; + }; + + // ep_page_view changes the dimensions of the editor. We have to guarantee + // the viewport height is calculated right + var getPaddingAddedWhenPageViewIsEnable = function () { + var chrome$ = helper.padChrome$; + var $outerIframe = chrome$('iframe'); + var paddingAddedWhenPageViewIsEnable = parseInt($outerIframe.css('padding-top')); + return paddingAddedWhenPageViewIsEnable; + }; + + var getIntValueOfCSSProperty = function($element, property){ + var valueString = $element.css(property); + return parseInt(valueString) || 0; + }; + + var forceUseMonospacedFont = function () { + helper.padChrome$.window.clientVars.padOptions.useMonospaceFont = true; + }; + + var setScrollPercentageWhenFocusLineIsOutOfViewport = function(value, editionAboveViewport) { + var scrollSettings = helper.padChrome$.window.clientVars.scrollWhenFocusLineIsOutOfViewport; + if (editionAboveViewport) { + scrollSettings.percentage.editionAboveViewport = value; + }else{ + scrollSettings.percentage.editionBelowViewport = value; + } + }; + + var resetScrollPercentageWhenFocusLineIsOutOfViewport = function() { + var scrollSettings = helper.padChrome$.window.clientVars.scrollWhenFocusLineIsOutOfViewport; + scrollSettings.percentage.editionAboveViewport = 0; + scrollSettings.percentage.editionBelowViewport = 0; + }; + + var setPercentageToScrollWhenUserPressesArrowUp = function (value) { + var scrollSettings = helper.padChrome$.window.clientVars.scrollWhenFocusLineIsOutOfViewport; + scrollSettings.percentageToScrollWhenUserPressesArrowUp = value; + }; + + var scrollWhenPlaceCaretInTheLastLineOfViewport = function() { + var scrollSettings = helper.padChrome$.window.clientVars.scrollWhenFocusLineIsOutOfViewport; + scrollSettings.scrollWhenCaretIsInTheLastLineOfViewport = true; + }; + + var getLinePositionOnViewport = function(lineNumber) { + var $line = getLine(lineNumber); + var linePosition = $line.get(0).getBoundingClientRect(); + + // position relative to the current viewport + return linePosition.top - getEditorScroll(); + }; +}); +