/** @babel */ import {it, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' import TextEditorElement from '../src/text-editor-element' import _, {extend, flatten, last, toArray} from 'underscore-plus' const NBSP = String.fromCharCode(160) const TILE_SIZE = 3 describe('TextEditorComponent', function () { let charWidth, component, componentNode, contentNode, editor, horizontalScrollbarNode, lineHeightInPixels, tileHeightInPixels, verticalScrollbarNode, wrapperNode beforeEach(async function () { jasmine.useRealClock() await atom.packages.activatePackage('language-javascript') editor = await atom.workspace.open('sample.js') contentNode = document.querySelector('#jasmine-content') contentNode.style.width = '1000px' wrapperNode = new TextEditorElement() wrapperNode.tileSize = TILE_SIZE wrapperNode.initialize(editor, atom) wrapperNode.setUpdatedSynchronously(false) jasmine.attachToDOM(wrapperNode) component = wrapperNode.component component.setFontFamily('monospace') component.setLineHeight(1.3) component.setFontSize(20) lineHeightInPixels = editor.getLineHeightInPixels() tileHeightInPixels = TILE_SIZE * lineHeightInPixels charWidth = editor.getDefaultCharWidth() componentNode = component.getDomNode() verticalScrollbarNode = componentNode.querySelector('.vertical-scrollbar') horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') component.measureDimensions() await nextViewUpdatePromise() }) afterEach(function () { contentNode.style.width = '' }) describe('async updates', function () { it('handles corrupted state gracefully', async function () { editor.insertNewline() component.presenter.startRow = -1 component.presenter.endRow = 9999 await nextViewUpdatePromise() // assert an update does occur }) it('does not update when an animation frame was requested but the component got destroyed before its delivery', async function () { editor.setText('You should not see this update.') component.destroy() await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).not.toBe('You should not see this update.') }) }) describe('line rendering', async function () { function expectTileContainsRow (tileNode, screenRow, {top}) { let lineNode = tileNode.querySelector('[data-screen-row="' + screenRow + '"]') let tokenizedLine = editor.tokenizedLineForScreenRow(screenRow) expect(lineNode.offsetTop).toBe(top) if (tokenizedLine.text === '') { expect(lineNode.innerHTML).toBe(' ') } else { expect(lineNode.textContent).toBe(tokenizedLine.text) } } it('gives the lines container the same height as the wrapper node', async function () { let linesNode = componentNode.querySelector('.lines') wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) }) it('renders higher tiles in front of lower ones', async function () { wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() let tilesNodes = component.tileNodesForLines() expect(tilesNodes[0].style.zIndex).toBe('2') expect(tilesNodes[1].style.zIndex).toBe('1') expect(tilesNodes[2].style.zIndex).toBe('0') verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) await nextViewUpdatePromise() tilesNodes = component.tileNodesForLines() expect(tilesNodes[0].style.zIndex).toBe('3') expect(tilesNodes[1].style.zIndex).toBe('2') expect(tilesNodes[2].style.zIndex).toBe('1') expect(tilesNodes[3].style.zIndex).toBe('0') }) it('renders the currently-visible lines in a tiled fashion', async function () { wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() let tilesNodes = component.tileNodesForLines() expect(tilesNodes.length).toBe(3) expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') expect(tilesNodes[0].querySelectorAll('.line').length).toBe(TILE_SIZE) expectTileContainsRow(tilesNodes[0], 0, { top: 0 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[0], 1, { top: 1 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[0], 2, { top: 2 * lineHeightInPixels }) expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels) + 'px, 0px)') expect(tilesNodes[1].querySelectorAll('.line').length).toBe(TILE_SIZE) expectTileContainsRow(tilesNodes[1], 3, { top: 0 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[1], 4, { top: 1 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[1], 5, { top: 2 * lineHeightInPixels }) expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels) + 'px, 0px)') expect(tilesNodes[2].querySelectorAll('.line').length).toBe(TILE_SIZE) expectTileContainsRow(tilesNodes[2], 6, { top: 0 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[2], 7, { top: 1 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[2], 8, { top: 2 * lineHeightInPixels }) expect(component.lineNodeForScreenRow(9)).toBeUndefined() verticalScrollbarNode.scrollTop = TILE_SIZE * lineHeightInPixels + 5 verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) await nextViewUpdatePromise() tilesNodes = component.tileNodesForLines() expect(component.lineNodeForScreenRow(2)).toBeUndefined() expect(tilesNodes.length).toBe(3) expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, ' + (0 * tileHeightInPixels - 5) + 'px, 0px)') expect(tilesNodes[0].querySelectorAll('.line').length).toBe(TILE_SIZE) expectTileContainsRow(tilesNodes[0], 3, { top: 0 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[0], 4, { top: 1 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[0], 5, { top: 2 * lineHeightInPixels }) expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels - 5) + 'px, 0px)') expect(tilesNodes[1].querySelectorAll('.line').length).toBe(TILE_SIZE) expectTileContainsRow(tilesNodes[1], 6, { top: 0 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[1], 7, { top: 1 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[1], 8, { top: 2 * lineHeightInPixels }) expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels - 5) + 'px, 0px)') expect(tilesNodes[2].querySelectorAll('.line').length).toBe(TILE_SIZE) expectTileContainsRow(tilesNodes[2], 9, { top: 0 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[2], 10, { top: 1 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[2], 11, { top: 2 * lineHeightInPixels }) }) it('updates the top position of subsequent tiles when lines are inserted or removed', async function () { wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() editor.getBuffer().deleteRows(0, 1) await nextViewUpdatePromise() let tilesNodes = component.tileNodesForLines() expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') expectTileContainsRow(tilesNodes[0], 0, { top: 0 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[0], 1, { top: 1 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[0], 2, { top: 2 * lineHeightInPixels }) expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels) + 'px, 0px)') expectTileContainsRow(tilesNodes[1], 3, { top: 0 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[1], 4, { top: 1 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[1], 5, { top: 2 * lineHeightInPixels }) editor.getBuffer().insert([0, 0], '\n\n') await nextViewUpdatePromise() tilesNodes = component.tileNodesForLines() expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') expectTileContainsRow(tilesNodes[0], 0, { top: 0 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[0], 1, { top: 1 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[0], 2, { top: 2 * lineHeightInPixels }) expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels) + 'px, 0px)') expectTileContainsRow(tilesNodes[1], 3, { top: 0 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[1], 4, { top: 1 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[1], 5, { top: 2 * lineHeightInPixels }) expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels) + 'px, 0px)') expectTileContainsRow(tilesNodes[2], 6, { top: 0 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[2], 7, { top: 1 * lineHeightInPixels }) expectTileContainsRow(tilesNodes[2], 8, { top: 2 * lineHeightInPixels }) }) it('updates the lines when lines are inserted or removed above the rendered row range', async function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) await nextViewUpdatePromise() let buffer = editor.getBuffer() buffer.insert([0, 0], '\n\n') await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text) buffer.delete([[0, 0], [3, 0]]) await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text) }) it('updates the top position of lines when the line height changes', async function () { let initialLineHeightInPixels = editor.getLineHeightInPixels() component.setLineHeight(2) await nextViewUpdatePromise() let newLineHeightInPixels = editor.getLineHeightInPixels() expect(newLineHeightInPixels).not.toBe(initialLineHeightInPixels) expect(component.lineNodeForScreenRow(1).offsetTop).toBe(1 * newLineHeightInPixels) }) it('updates the top position of lines when the font size changes', async function () { let initialLineHeightInPixels = editor.getLineHeightInPixels() component.setFontSize(10) await nextViewUpdatePromise() let newLineHeightInPixels = editor.getLineHeightInPixels() expect(newLineHeightInPixels).not.toBe(initialLineHeightInPixels) expect(component.lineNodeForScreenRow(1).offsetTop).toBe(1 * newLineHeightInPixels) }) it('renders the .lines div at the full height of the editor if there are not enough lines to scroll vertically', async function () { editor.setText('') wrapperNode.style.height = '300px' component.measureDimensions() await nextViewUpdatePromise() let linesNode = componentNode.querySelector('.lines') expect(linesNode.offsetHeight).toBe(300) }) it('assigns the width of each line so it extends across the full width of the editor', async function () { let gutterWidth = componentNode.querySelector('.gutter').offsetWidth let scrollViewNode = componentNode.querySelector('.scroll-view') let lineNodes = Array.from(componentNode.querySelectorAll('.line')) componentNode.style.width = gutterWidth + (30 * charWidth) + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(wrapperNode.getScrollWidth()).toBeGreaterThan(scrollViewNode.offsetWidth) let editorFullWidth = wrapperNode.getScrollWidth() + wrapperNode.getVerticalScrollbarWidth() for (let lineNode of lineNodes) { expect(lineNode.getBoundingClientRect().width).toBe(editorFullWidth) } componentNode.style.width = gutterWidth + wrapperNode.getScrollWidth() + 100 + 'px' component.measureDimensions() await nextViewUpdatePromise() let scrollViewWidth = scrollViewNode.offsetWidth for (let lineNode of lineNodes) { expect(lineNode.getBoundingClientRect().width).toBe(scrollViewWidth) } }) it('renders an nbsp on empty lines when no line-ending character is defined', function () { atom.config.set('editor.showInvisibles', false) expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP) }) it('gives the lines and tiles divs the same background color as the editor to improve GPU performance', async function () { let linesNode = componentNode.querySelector('.lines') let backgroundColor = getComputedStyle(wrapperNode).backgroundColor expect(linesNode.style.backgroundColor).toBe(backgroundColor) for (let tileNode of component.tileNodesForLines()) { expect(tileNode.style.backgroundColor).toBe(backgroundColor) } wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)' await nextViewUpdatePromise() expect(linesNode.style.backgroundColor).toBe('rgb(255, 0, 0)') for (let tileNode of component.tileNodesForLines()) { expect(tileNode.style.backgroundColor).toBe('rgb(255, 0, 0)') } }) it('applies .leading-whitespace for lines with leading spaces and/or tabs', async function () { editor.setText(' a') await nextViewUpdatePromise() let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(true) expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(false) editor.setText('\ta') await nextViewUpdatePromise() leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(true) expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(false) }) it('applies .trailing-whitespace for lines with trailing spaces and/or tabs', async function () { editor.setText(' ') await nextViewUpdatePromise() let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) editor.setText('\t') await nextViewUpdatePromise() leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) editor.setText('a ') await nextViewUpdatePromise() leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) editor.setText('a\t') await nextViewUpdatePromise() leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) }) it('keeps rebuilding lines when continuous reflow is on', function () { wrapperNode.setContinuousReflow(true) let oldLineNode = componentNode.querySelector('.line') waitsFor(function () { return componentNode.querySelector('.line') !== oldLineNode }) }) describe('when showInvisibles is enabled', function () { const invisibles = { eol: 'E', space: 'S', tab: 'T', cr: 'C' } beforeEach(async function () { atom.config.set('editor.showInvisibles', true) atom.config.set('editor.invisibles', invisibles) await nextViewUpdatePromise() }) it('re-renders the lines when the showInvisibles config option changes', async function () { editor.setText(' a line with tabs\tand spaces \n') await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol) atom.config.set('editor.showInvisibles', false) await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe(' a line with tabs and spaces ') atom.config.set('editor.showInvisibles', true) await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol) }) it('displays leading/trailing spaces, tabs, and newlines as visible characters', async function () { editor.setText(' a line with tabs\tand spaces \n') await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol) let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('invisible-character')).toBe(true) expect(leafNodes[leafNodes.length - 1].classList.contains('invisible-character')).toBe(true) }) it('displays newlines as their own token outside of the other tokens\' scopeDescriptor', async function () { editor.setText('let\n') await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '') }) it('displays trailing carriage returns using a visible, non-empty value', async function () { editor.setText('a line that ends with a carriage return\r\n') await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('a line that ends with a carriage return' + invisibles.cr + invisibles.eol) }) it('renders invisible line-ending characters on empty lines', function () { expect(component.lineNodeForScreenRow(10).textContent).toBe(invisibles.eol) }) it('renders an nbsp on empty lines when the line-ending character is an empty string', async function () { atom.config.set('editor.invisibles', { eol: '' }) await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP) }) it('renders an nbsp on empty lines when the line-ending character is false', async function () { atom.config.set('editor.invisibles', { eol: false }) await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP) }) it('interleaves invisible line-ending characters with indent guides on empty lines', async function () { atom.config.set('editor.showIndentGuide', true) await nextViewUpdatePromise() editor.setTextInBufferRange([[10, 0], [11, 0]], '\r\n', { normalizeLineEndings: false }) await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') editor.setTabLength(3) await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ') editor.setTabLength(1) await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ') editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ') await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') }) describe('when soft wrapping is enabled', function () { beforeEach(async function () { editor.setText('a line that wraps \n') editor.setSoftWrapped(true) await nextViewUpdatePromise() componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() await nextViewUpdatePromise() }) it('does not show end of line invisibles at the end of wrapped lines', function () { expect(component.lineNodeForScreenRow(0).textContent).toBe('a line that ') expect(component.lineNodeForScreenRow(1).textContent).toBe('wraps' + invisibles.space + invisibles.eol) }) }) }) describe('when indent guides are enabled', function () { beforeEach(async function () { atom.config.set('editor.showIndentGuide', true) await nextViewUpdatePromise() }) it('adds an "indent-guide" class to spans comprising the leading whitespace', function () { let line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) expect(line1LeafNodes[0].textContent).toBe(' ') expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false) let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes[0].textContent).toBe(' ') expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[1].textContent).toBe(' ') expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(false) }) it('renders leading whitespace spans with the "indent-guide" class for empty lines', async function () { editor.getBuffer().insert([1, Infinity], '\n') await nextViewUpdatePromise() let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe(2) expect(line2LeafNodes[0].textContent).toBe(' ') expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[1].textContent).toBe(' ') expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) }) it('renders indent guides correctly on lines containing only whitespace', async function () { editor.getBuffer().insert([1, Infinity], '\n ') await nextViewUpdatePromise() let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe(3) expect(line2LeafNodes[0].textContent).toBe(' ') expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[1].textContent).toBe(' ') expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[2].textContent).toBe(' ') expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(true) }) it('renders indent guides correctly on lines containing only whitespace when invisibles are enabled', async function () { atom.config.set('editor.showInvisibles', true) atom.config.set('editor.invisibles', { space: '-', eol: 'x' }) editor.getBuffer().insert([1, Infinity], '\n ') await nextViewUpdatePromise() let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe(4) expect(line2LeafNodes[0].textContent).toBe('--') expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[1].textContent).toBe('--') expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[2].textContent).toBe('--') expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[3].textContent).toBe('x') }) it('does not render indent guides in trailing whitespace for lines containing non whitespace characters', async function () { editor.getBuffer().setText(' hi ') await nextViewUpdatePromise() let line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(line0LeafNodes[0].textContent).toBe(' ') expect(line0LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line0LeafNodes[1].textContent).toBe(' ') expect(line0LeafNodes[1].classList.contains('indent-guide')).toBe(false) }) it('updates the indent guides on empty lines preceding an indentation change', async function () { editor.getBuffer().insert([12, 0], '\n') await nextViewUpdatePromise() editor.getBuffer().insert([13, 0], ' ') await nextViewUpdatePromise() let line12LeafNodes = getLeafNodes(component.lineNodeForScreenRow(12)) expect(line12LeafNodes[0].textContent).toBe(' ') expect(line12LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line12LeafNodes[1].textContent).toBe(' ') expect(line12LeafNodes[1].classList.contains('indent-guide')).toBe(true) }) it('updates the indent guides on empty lines following an indentation change', async function () { editor.getBuffer().insert([12, 2], '\n') await nextViewUpdatePromise() editor.getBuffer().insert([12, 0], ' ') await nextViewUpdatePromise() let line13LeafNodes = getLeafNodes(component.lineNodeForScreenRow(13)) expect(line13LeafNodes[0].textContent).toBe(' ') expect(line13LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line13LeafNodes[1].textContent).toBe(' ') expect(line13LeafNodes[1].classList.contains('indent-guide')).toBe(true) }) }) describe('when indent guides are disabled', function () { beforeEach(function () { expect(atom.config.get('editor.showIndentGuide')).toBe(false) }) it('does not render indent guides on lines containing only whitespace', async function () { editor.getBuffer().insert([1, Infinity], '\n ') await nextViewUpdatePromise() let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe(3) expect(line2LeafNodes[0].textContent).toBe(' ') expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(false) expect(line2LeafNodes[1].textContent).toBe(' ') expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(false) expect(line2LeafNodes[2].textContent).toBe(' ') expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(false) }) }) describe('when the buffer contains null bytes', function () { it('excludes the null byte from character measurement', async function () { editor.setText('a\0b') await nextViewUpdatePromise() expect(wrapperNode.pixelPositionForScreenPosition([0, Infinity]).left).toEqual(2 * charWidth) }) }) describe('when there is a fold', function () { it('renders a fold marker on the folded line', async function () { let foldedLineNode = component.lineNodeForScreenRow(4) expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() editor.foldBufferRow(4) await nextViewUpdatePromise() foldedLineNode = component.lineNodeForScreenRow(4) expect(foldedLineNode.querySelector('.fold-marker')).toBeTruthy() editor.unfoldBufferRow(4) await nextViewUpdatePromise() foldedLineNode = component.lineNodeForScreenRow(4) expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() }) }) }) describe('gutter rendering', function () { function expectTileContainsRow (tileNode, screenRow, {top, text}) { let lineNode = tileNode.querySelector('[data-screen-row="' + screenRow + '"]') expect(lineNode.offsetTop).toBe(top) expect(lineNode.textContent).toBe(text) } it('renders higher tiles in front of lower ones', async function () { wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() let tilesNodes = component.tileNodesForLineNumbers() expect(tilesNodes[0].style.zIndex).toBe('2') expect(tilesNodes[1].style.zIndex).toBe('1') expect(tilesNodes[2].style.zIndex).toBe('0') verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) await nextViewUpdatePromise() tilesNodes = component.tileNodesForLineNumbers() expect(tilesNodes[0].style.zIndex).toBe('3') expect(tilesNodes[1].style.zIndex).toBe('2') expect(tilesNodes[2].style.zIndex).toBe('1') expect(tilesNodes[3].style.zIndex).toBe('0') }) it('gives the line numbers container the same height as the wrapper node', async function () { let linesNode = componentNode.querySelector('.line-numbers') wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) }) it('renders the currently-visible line numbers in a tiled fashion', async function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() let tilesNodes = component.tileNodesForLineNumbers() expect(tilesNodes.length).toBe(3) expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') expect(tilesNodes[0].querySelectorAll('.line-number').length).toBe(3) expectTileContainsRow(tilesNodes[0], 0, { top: lineHeightInPixels * 0, text: '' + NBSP + '1' }) expectTileContainsRow(tilesNodes[0], 1, { top: lineHeightInPixels * 1, text: '' + NBSP + '2' }) expectTileContainsRow(tilesNodes[0], 2, { top: lineHeightInPixels * 2, text: '' + NBSP + '3' }) expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels) + 'px, 0px)') expect(tilesNodes[1].querySelectorAll('.line-number').length).toBe(3) expectTileContainsRow(tilesNodes[1], 3, { top: lineHeightInPixels * 0, text: '' + NBSP + '4' }) expectTileContainsRow(tilesNodes[1], 4, { top: lineHeightInPixels * 1, text: '' + NBSP + '5' }) expectTileContainsRow(tilesNodes[1], 5, { top: lineHeightInPixels * 2, text: '' + NBSP + '6' }) expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels) + 'px, 0px)') expect(tilesNodes[2].querySelectorAll('.line-number').length).toBe(3) expectTileContainsRow(tilesNodes[2], 6, { top: lineHeightInPixels * 0, text: '' + NBSP + '7' }) expectTileContainsRow(tilesNodes[2], 7, { top: lineHeightInPixels * 1, text: '' + NBSP + '8' }) expectTileContainsRow(tilesNodes[2], 8, { top: lineHeightInPixels * 2, text: '' + NBSP + '9' }) verticalScrollbarNode.scrollTop = TILE_SIZE * lineHeightInPixels + 5 verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) await nextViewUpdatePromise() tilesNodes = component.tileNodesForLineNumbers() expect(component.lineNumberNodeForScreenRow(2)).toBeUndefined() expect(tilesNodes.length).toBe(3) expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, ' + (0 * tileHeightInPixels - 5) + 'px, 0px)') expect(tilesNodes[0].querySelectorAll('.line-number').length).toBe(TILE_SIZE) expectTileContainsRow(tilesNodes[0], 3, { top: lineHeightInPixels * 0, text: '' + NBSP + '4' }) expectTileContainsRow(tilesNodes[0], 4, { top: lineHeightInPixels * 1, text: '' + NBSP + '5' }) expectTileContainsRow(tilesNodes[0], 5, { top: lineHeightInPixels * 2, text: '' + NBSP + '6' }) expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels - 5) + 'px, 0px)') expect(tilesNodes[1].querySelectorAll('.line-number').length).toBe(TILE_SIZE) expectTileContainsRow(tilesNodes[1], 6, { top: 0 * lineHeightInPixels, text: '' + NBSP + '7' }) expectTileContainsRow(tilesNodes[1], 7, { top: 1 * lineHeightInPixels, text: '' + NBSP + '8' }) expectTileContainsRow(tilesNodes[1], 8, { top: 2 * lineHeightInPixels, text: '' + NBSP + '9' }) expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels - 5) + 'px, 0px)') expect(tilesNodes[2].querySelectorAll('.line-number').length).toBe(TILE_SIZE) expectTileContainsRow(tilesNodes[2], 9, { top: 0 * lineHeightInPixels, text: '10' }) expectTileContainsRow(tilesNodes[2], 10, { top: 1 * lineHeightInPixels, text: '11' }) expectTileContainsRow(tilesNodes[2], 11, { top: 2 * lineHeightInPixels, text: '12' }) }) it('updates the translation of subsequent line numbers when lines are inserted or removed', async function () { editor.getBuffer().insert([0, 0], '\n\n') await nextViewUpdatePromise() let lineNumberNodes = componentNode.querySelectorAll('.line-number') expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe(0 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe(1 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe(2 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe(0 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe(1 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe(2 * lineHeightInPixels) editor.getBuffer().insert([0, 0], '\n\n') await nextViewUpdatePromise() expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe(0 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe(1 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe(2 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe(0 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe(1 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe(2 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(6).offsetTop).toBe(0 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(7).offsetTop).toBe(1 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(8).offsetTop).toBe(2 * lineHeightInPixels) }) it('renders • characters for soft-wrapped lines', async function () { editor.setSoftWrapped(true) wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 30 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(componentNode.querySelectorAll('.line-number').length).toBe(9 + 1) expect(component.lineNumberNodeForScreenRow(0).textContent).toBe('' + NBSP + '1') expect(component.lineNumberNodeForScreenRow(1).textContent).toBe('' + NBSP + '•') expect(component.lineNumberNodeForScreenRow(2).textContent).toBe('' + NBSP + '2') expect(component.lineNumberNodeForScreenRow(3).textContent).toBe('' + NBSP + '•') expect(component.lineNumberNodeForScreenRow(4).textContent).toBe('' + NBSP + '3') expect(component.lineNumberNodeForScreenRow(5).textContent).toBe('' + NBSP + '•') expect(component.lineNumberNodeForScreenRow(6).textContent).toBe('' + NBSP + '4') expect(component.lineNumberNodeForScreenRow(7).textContent).toBe('' + NBSP + '•') expect(component.lineNumberNodeForScreenRow(8).textContent).toBe('' + NBSP + '•') }) it('pads line numbers to be right-justified based on the maximum number of line number digits', async function () { editor.getBuffer().setText([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].join('\n')) await nextViewUpdatePromise() for (let screenRow = 0; screenRow <= 8; ++screenRow) { expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + (screenRow + 1)) } expect(component.lineNumberNodeForScreenRow(9).textContent).toBe('10') let gutterNode = componentNode.querySelector('.gutter') let initialGutterWidth = gutterNode.offsetWidth editor.getBuffer().delete([[1, 0], [2, 0]]) await nextViewUpdatePromise() for (let screenRow = 0; screenRow <= 8; ++screenRow) { expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + (screenRow + 1)) } expect(gutterNode.offsetWidth).toBeLessThan(initialGutterWidth) editor.getBuffer().insert([0, 0], '\n\n') await nextViewUpdatePromise() for (let screenRow = 0; screenRow <= 8; ++screenRow) { expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + (screenRow + 1)) } expect(component.lineNumberNodeForScreenRow(9).textContent).toBe('10') expect(gutterNode.offsetWidth).toBe(initialGutterWidth) }) it('renders the .line-numbers div at the full height of the editor even if it\'s taller than its content', async function () { wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(componentNode.querySelector('.line-numbers').offsetHeight).toBe(componentNode.offsetHeight) }) it('applies the background color of the gutter or the editor to the line numbers to improve GPU performance', async function () { let gutterNode = componentNode.querySelector('.gutter') let lineNumbersNode = gutterNode.querySelector('.line-numbers') let backgroundColor = getComputedStyle(wrapperNode).backgroundColor expect(lineNumbersNode.style.backgroundColor).toBe(backgroundColor) for (let tileNode of component.tileNodesForLineNumbers()) { expect(tileNode.style.backgroundColor).toBe(backgroundColor) } gutterNode.style.backgroundColor = 'rgb(255, 0, 0)' atom.views.performDocumentPoll() await nextViewUpdatePromise() expect(lineNumbersNode.style.backgroundColor).toBe('rgb(255, 0, 0)') for (let tileNode of component.tileNodesForLineNumbers()) { expect(tileNode.style.backgroundColor).toBe('rgb(255, 0, 0)') } }) it('hides or shows the gutter based on the "::isLineNumberGutterVisible" property on the model and the global "editor.showLineNumbers" config setting', async function () { expect(component.gutterContainerComponent.getLineNumberGutterComponent() != null).toBe(true) editor.setLineNumberGutterVisible(false) await nextViewUpdatePromise() expect(componentNode.querySelector('.gutter').style.display).toBe('none') atom.config.set('editor.showLineNumbers', false) await nextViewUpdatePromise() expect(componentNode.querySelector('.gutter').style.display).toBe('none') editor.setLineNumberGutterVisible(true) await nextViewUpdatePromise() expect(componentNode.querySelector('.gutter').style.display).toBe('none') atom.config.set('editor.showLineNumbers', true) await nextViewUpdatePromise() expect(componentNode.querySelector('.gutter').style.display).toBe('') expect(component.lineNumberNodeForScreenRow(3) != null).toBe(true) }) it('keeps rebuilding line numbers when continuous reflow is on', function () { wrapperNode.setContinuousReflow(true) let oldLineNode = componentNode.querySelectorAll('.line-number')[1] waitsFor(function () { return componentNode.querySelectorAll('.line-number')[1] !== oldLineNode }) }) describe('fold decorations', function () { describe('rendering fold decorations', function () { it('adds the foldable class to line numbers when the line is foldable', function () { expect(lineNumberHasClass(0, 'foldable')).toBe(true) expect(lineNumberHasClass(1, 'foldable')).toBe(true) expect(lineNumberHasClass(2, 'foldable')).toBe(false) expect(lineNumberHasClass(3, 'foldable')).toBe(false) expect(lineNumberHasClass(4, 'foldable')).toBe(true) expect(lineNumberHasClass(5, 'foldable')).toBe(false) }) it('updates the foldable class on the correct line numbers when the foldable positions change', async function () { editor.getBuffer().insert([0, 0], '\n') await nextViewUpdatePromise() expect(lineNumberHasClass(0, 'foldable')).toBe(false) expect(lineNumberHasClass(1, 'foldable')).toBe(true) expect(lineNumberHasClass(2, 'foldable')).toBe(true) expect(lineNumberHasClass(3, 'foldable')).toBe(false) expect(lineNumberHasClass(4, 'foldable')).toBe(false) expect(lineNumberHasClass(5, 'foldable')).toBe(true) expect(lineNumberHasClass(6, 'foldable')).toBe(false) }) it('updates the foldable class on a line number that becomes foldable', async function () { expect(lineNumberHasClass(11, 'foldable')).toBe(false) editor.getBuffer().insert([11, 44], '\n fold me') await nextViewUpdatePromise() expect(lineNumberHasClass(11, 'foldable')).toBe(true) editor.undo() await nextViewUpdatePromise() expect(lineNumberHasClass(11, 'foldable')).toBe(false) }) it('adds, updates and removes the folded class on the correct line number componentNodes', async function () { editor.foldBufferRow(4) await nextViewUpdatePromise() expect(lineNumberHasClass(4, 'folded')).toBe(true) editor.getBuffer().insert([0, 0], '\n') await nextViewUpdatePromise() expect(lineNumberHasClass(4, 'folded')).toBe(false) expect(lineNumberHasClass(5, 'folded')).toBe(true) editor.unfoldBufferRow(5) await nextViewUpdatePromise() expect(lineNumberHasClass(5, 'folded')).toBe(false) }) describe('when soft wrapping is enabled', function () { beforeEach(async function () { editor.setSoftWrapped(true) await nextViewUpdatePromise() componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() await nextViewUpdatePromise() }) it('does not add the foldable class for soft-wrapped lines', function () { expect(lineNumberHasClass(0, 'foldable')).toBe(true) expect(lineNumberHasClass(1, 'foldable')).toBe(false) }) }) }) describe('mouse interactions with fold indicators', function () { let gutterNode function buildClickEvent (target) { return buildMouseEvent('click', { target: target }) } beforeEach(function () { gutterNode = componentNode.querySelector('.gutter') }) describe('when the component is destroyed', function () { it('stops listening for folding events', function () { let lineNumber, target component.destroy() lineNumber = component.lineNumberNodeForScreenRow(1) target = lineNumber.querySelector('.icon-right') return target.dispatchEvent(buildClickEvent(target)) }) }) it('folds and unfolds the block represented by the fold indicator when clicked', async function () { expect(lineNumberHasClass(1, 'folded')).toBe(false) let lineNumber = component.lineNumberNodeForScreenRow(1) let target = lineNumber.querySelector('.icon-right') target.dispatchEvent(buildClickEvent(target)) await nextViewUpdatePromise() expect(lineNumberHasClass(1, 'folded')).toBe(true) lineNumber = component.lineNumberNodeForScreenRow(1) target = lineNumber.querySelector('.icon-right') target.dispatchEvent(buildClickEvent(target)) await nextViewUpdatePromise() expect(lineNumberHasClass(1, 'folded')).toBe(false) }) it('does not fold when the line number componentNode is clicked', function () { let lineNumber = component.lineNumberNodeForScreenRow(1) lineNumber.dispatchEvent(buildClickEvent(lineNumber)) waits(100) runs(function () { expect(lineNumberHasClass(1, 'folded')).toBe(false) }) }) }) }) }) describe('cursor rendering', function () { it('renders the currently visible cursors', async function () { let cursor1 = editor.getLastCursor() cursor1.setScreenPosition([0, 5], { autoscroll: false }) wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() let cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe(1) expect(cursorNodes[0].offsetHeight).toBe(lineHeightInPixels) expect(cursorNodes[0].offsetWidth).toBeCloseTo(charWidth, 0) expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(5 * charWidth)) + 'px, ' + (0 * lineHeightInPixels) + 'px)') let cursor2 = editor.addCursorAtScreenPosition([8, 11], { autoscroll: false }) let cursor3 = editor.addCursorAtScreenPosition([4, 10], { autoscroll: false }) await nextViewUpdatePromise() cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe(2) expect(cursorNodes[0].offsetTop).toBe(0) expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(5 * charWidth)) + 'px, ' + (0 * lineHeightInPixels) + 'px)') expect(cursorNodes[1].style['-webkit-transform']).toBe('translate(' + (Math.round(10 * charWidth)) + 'px, ' + (4 * lineHeightInPixels) + 'px)') verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) await nextViewUpdatePromise() horizontalScrollbarNode.scrollLeft = 3.5 * charWidth horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) await nextViewUpdatePromise() cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe(2) expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(10 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (4 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') expect(cursorNodes[1].style['-webkit-transform']).toBe('translate(' + (Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (8 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') editor.onDidChangeCursorPosition(cursorMovedListener = jasmine.createSpy('cursorMovedListener')) cursor3.setScreenPosition([4, 11], { autoscroll: false }) await nextViewUpdatePromise() expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (4 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') expect(cursorMovedListener).toHaveBeenCalled() cursor3.destroy() await nextViewUpdatePromise() cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe(1) expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (8 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') }) it('accounts for character widths when positioning cursors', async function () { atom.config.set('editor.fontFamily', 'sans-serif') editor.setCursorScreenPosition([0, 16]) await nextViewUpdatePromise() let cursor = componentNode.querySelector('.cursor') let cursorRect = cursor.getBoundingClientRect() let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild let range = document.createRange() range.setStart(cursorLocationTextNode, 0) range.setEnd(cursorLocationTextNode, 1) let rangeRect = range.getBoundingClientRect() expect(cursorRect.left).toBeCloseTo(rangeRect.left, 0) expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) }) it('accounts for the width of paired characters when positioning cursors', async function () { atom.config.set('editor.fontFamily', 'sans-serif') editor.setText('he\u0301y') editor.setCursorBufferPosition([0, 3]) await nextViewUpdatePromise() let cursor = componentNode.querySelector('.cursor') let cursorRect = cursor.getBoundingClientRect() let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2] let range = document.createRange() range.setStart(cursorLocationTextNode, 0) range.setEnd(cursorLocationTextNode, 1) let rangeRect = range.getBoundingClientRect() expect(cursorRect.left).toBeCloseTo(rangeRect.left, 0) expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) }) it('positions cursors correctly after character widths are changed via a stylesheet change', async function () { atom.config.set('editor.fontFamily', 'sans-serif') editor.setCursorScreenPosition([0, 16]) await nextViewUpdatePromise() atom.styles.addStyleSheet('.function.js {\n font-weight: bold;\n}', { context: 'atom-text-editor' }) await nextViewUpdatePromise() let cursor = componentNode.querySelector('.cursor') let cursorRect = cursor.getBoundingClientRect() let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild let range = document.createRange() range.setStart(cursorLocationTextNode, 0) range.setEnd(cursorLocationTextNode, 1) let rangeRect = range.getBoundingClientRect() expect(cursorRect.left).toBeCloseTo(rangeRect.left, 0) expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) atom.themes.removeStylesheet('test') }) it('sets the cursor to the default character width at the end of a line', async function () { editor.setCursorScreenPosition([0, Infinity]) await nextViewUpdatePromise() let cursorNode = componentNode.querySelector('.cursor') expect(cursorNode.offsetWidth).toBeCloseTo(charWidth, 0) }) it('gives the cursor a non-zero width even if it\'s inside atomic tokens', async function () { editor.setCursorScreenPosition([1, 0]) await nextViewUpdatePromise() let cursorNode = componentNode.querySelector('.cursor') expect(cursorNode.offsetWidth).toBeCloseTo(charWidth, 0) }) it('blinks cursors when they are not moving', async function () { let cursorsNode = componentNode.querySelector('.cursors') wrapperNode.focus() await nextViewUpdatePromise() expect(cursorsNode.classList.contains('blink-off')).toBe(false) await conditionPromise(function () { return cursorsNode.classList.contains('blink-off') }) await conditionPromise(function () { return !cursorsNode.classList.contains('blink-off') }) editor.moveRight() await nextViewUpdatePromise() expect(cursorsNode.classList.contains('blink-off')).toBe(false) await conditionPromise(function () { return cursorsNode.classList.contains('blink-off') }) }) it('does not render cursors that are associated with non-empty selections', async function () { editor.setSelectedScreenRange([[0, 4], [4, 6]]) editor.addCursorAtScreenPosition([6, 8]) await nextViewUpdatePromise() let cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe(1) expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(8 * charWidth)) + 'px, ' + (6 * lineHeightInPixels) + 'px)') }) it('updates cursor positions when the line height changes', async function () { editor.setCursorBufferPosition([1, 10]) component.setLineHeight(2) await nextViewUpdatePromise() let cursorNode = componentNode.querySelector('.cursor') expect(cursorNode.style['-webkit-transform']).toBe('translate(' + (Math.round(10 * editor.getDefaultCharWidth())) + 'px, ' + (editor.getLineHeightInPixels()) + 'px)') }) it('updates cursor positions when the font size changes', async function () { editor.setCursorBufferPosition([1, 10]) component.setFontSize(10) await nextViewUpdatePromise() let cursorNode = componentNode.querySelector('.cursor') expect(cursorNode.style['-webkit-transform']).toBe('translate(' + (Math.round(10 * editor.getDefaultCharWidth())) + 'px, ' + (editor.getLineHeightInPixels()) + 'px)') }) it('updates cursor positions when the font family changes', async function () { editor.setCursorBufferPosition([1, 10]) component.setFontFamily('sans-serif') await nextViewUpdatePromise() let cursorNode = componentNode.querySelector('.cursor') let left = wrapperNode.pixelPositionForScreenPosition([1, 10]).left expect(cursorNode.style['-webkit-transform']).toBe('translate(' + (Math.round(left)) + 'px, ' + (editor.getLineHeightInPixels()) + 'px)') }) }) describe('selection rendering', function () { let scrollViewClientLeft, scrollViewNode beforeEach(function () { scrollViewNode = componentNode.querySelector('.scroll-view') scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left }) it('renders 1 region for 1-line selections', async function () { editor.setSelectedScreenRange([[1, 6], [1, 10]]) await nextViewUpdatePromise() let regions = componentNode.querySelectorAll('.selection .region') expect(regions.length).toBe(1) let regionRect = regions[0].getBoundingClientRect() expect(regionRect.top).toBe(1 * lineHeightInPixels) expect(regionRect.height).toBe(1 * lineHeightInPixels) expect(regionRect.left).toBeCloseTo(scrollViewClientLeft + 6 * charWidth, 0) expect(regionRect.width).toBeCloseTo(4 * charWidth, 0) }) it('renders 2 regions for 2-line selections', async function () { editor.setSelectedScreenRange([[1, 6], [2, 10]]) await nextViewUpdatePromise() let tileNode = component.tileNodesForLines()[0] let regions = tileNode.querySelectorAll('.selection .region') expect(regions.length).toBe(2) let region1Rect = regions[0].getBoundingClientRect() expect(region1Rect.top).toBe(1 * lineHeightInPixels) expect(region1Rect.height).toBe(1 * lineHeightInPixels) expect(region1Rect.left).toBeCloseTo(scrollViewClientLeft + 6 * charWidth, 0) expect(region1Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) let region2Rect = regions[1].getBoundingClientRect() expect(region2Rect.top).toBe(2 * lineHeightInPixels) expect(region2Rect.height).toBe(1 * lineHeightInPixels) expect(region2Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) expect(region2Rect.width).toBeCloseTo(10 * charWidth, 0) }) it('renders 3 regions per tile for selections with more than 2 lines', async function () { editor.setSelectedScreenRange([[0, 6], [5, 10]]) await nextViewUpdatePromise() let region1Rect, region2Rect, region3Rect, regions, tileNode tileNode = component.tileNodesForLines()[0] regions = tileNode.querySelectorAll('.selection .region') expect(regions.length).toBe(3) region1Rect = regions[0].getBoundingClientRect() expect(region1Rect.top).toBe(0) expect(region1Rect.height).toBe(1 * lineHeightInPixels) expect(region1Rect.left).toBeCloseTo(scrollViewClientLeft + 6 * charWidth, 0) expect(region1Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) region2Rect = regions[1].getBoundingClientRect() expect(region2Rect.top).toBe(1 * lineHeightInPixels) expect(region2Rect.height).toBe(1 * lineHeightInPixels) expect(region2Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) expect(region2Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) region3Rect = regions[2].getBoundingClientRect() expect(region3Rect.top).toBe(2 * lineHeightInPixels) expect(region3Rect.height).toBe(1 * lineHeightInPixels) expect(region3Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) expect(region3Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) tileNode = component.tileNodesForLines()[1] regions = tileNode.querySelectorAll('.selection .region') expect(regions.length).toBe(3) region1Rect = regions[0].getBoundingClientRect() expect(region1Rect.top).toBe(3 * lineHeightInPixels) expect(region1Rect.height).toBe(1 * lineHeightInPixels) expect(region1Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) expect(region1Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) region2Rect = regions[1].getBoundingClientRect() expect(region2Rect.top).toBe(4 * lineHeightInPixels) expect(region2Rect.height).toBe(1 * lineHeightInPixels) expect(region2Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) expect(region2Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) region3Rect = regions[2].getBoundingClientRect() expect(region3Rect.top).toBe(5 * lineHeightInPixels) expect(region3Rect.height).toBe(1 * lineHeightInPixels) expect(region3Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) expect(region3Rect.width).toBeCloseTo(10 * charWidth, 0) }) it('does not render empty selections', async function () { editor.addSelectionForBufferRange([[2, 2], [2, 2]]) await nextViewUpdatePromise() expect(editor.getSelections()[0].isEmpty()).toBe(true) expect(editor.getSelections()[1].isEmpty()).toBe(true) expect(componentNode.querySelectorAll('.selection').length).toBe(0) }) it('updates selections when the line height changes', async function () { editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setLineHeight(2) await nextViewUpdatePromise() let selectionNode = componentNode.querySelector('.region') expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels()) }) it('updates selections when the font size changes', async function () { editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontSize(10) await nextViewUpdatePromise() let selectionNode = componentNode.querySelector('.region') expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels()) expect(selectionNode.offsetLeft).toBeCloseTo(6 * editor.getDefaultCharWidth(), 0) }) it('updates selections when the font family changes', async function () { editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontFamily('sans-serif') await nextViewUpdatePromise() let selectionNode = componentNode.querySelector('.region') expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels()) expect(selectionNode.offsetLeft).toBeCloseTo(wrapperNode.pixelPositionForScreenPosition([1, 6]).left, 0) }) it('will flash the selection when flash:true is passed to editor::setSelectedBufferRange', async function () { editor.setSelectedBufferRange([[1, 6], [1, 10]], { flash: true }) await nextViewUpdatePromise() let selectionNode = componentNode.querySelector('.selection') expect(selectionNode.classList.contains('flash')).toBe(true) await conditionPromise(function () { return !selectionNode.classList.contains('flash') }) editor.setSelectedBufferRange([[1, 5], [1, 7]], { flash: true }) await nextViewUpdatePromise() expect(selectionNode.classList.contains('flash')).toBe(true) }) }) describe('line decoration rendering', function () { let decoration, marker beforeEach(async function () { marker = editor.addMarkerLayer({ maintainHistory: true }).markBufferRange([[2, 13], [3, 15]], { invalidate: 'inside' }) decoration = editor.decorateMarker(marker, { type: ['line-number', 'line'], 'class': 'a' }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() }) it('applies line decoration classes to lines and line numbers', async function () { expect(lineAndLineNumberHaveClass(2, 'a')).toBe(true) expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() let marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]]) editor.decorateMarker(marker2, { type: ['line-number', 'line'], 'class': 'b' }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(9, 'b')).toBe(true) editor.foldBufferRow(5) await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(9, 'b')).toBe(false) expect(lineAndLineNumberHaveClass(6, 'b')).toBe(true) }) it('only applies decorations to screen rows that are spanned by their marker when lines are soft-wrapped', async function () { editor.setText('a line that wraps, ok') editor.setSoftWrapped(true) componentNode.style.width = 16 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() marker.destroy() marker = editor.markBufferRange([[0, 0], [0, 2]]) editor.decorateMarker(marker, { type: ['line-number', 'line'], 'class': 'b' }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(lineNumberHasClass(0, 'b')).toBe(true) expect(lineNumberHasClass(1, 'b')).toBe(false) marker.setBufferRange([[0, 0], [0, Infinity]]) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(lineNumberHasClass(0, 'b')).toBe(true) expect(lineNumberHasClass(1, 'b')).toBe(true) }) it('updates decorations when markers move', async function () { expect(lineAndLineNumberHaveClass(1, 'a')).toBe(false) expect(lineAndLineNumberHaveClass(2, 'a')).toBe(true) expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) expect(lineAndLineNumberHaveClass(4, 'a')).toBe(false) editor.getBuffer().insert([0, 0], '\n') await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(2, 'a')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) expect(lineAndLineNumberHaveClass(4, 'a')).toBe(true) expect(lineAndLineNumberHaveClass(5, 'a')).toBe(false) marker.setBufferRange([[4, 4], [6, 4]]) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(2, 'a')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'a')).toBe(false) expect(lineAndLineNumberHaveClass(4, 'a')).toBe(true) expect(lineAndLineNumberHaveClass(5, 'a')).toBe(true) expect(lineAndLineNumberHaveClass(6, 'a')).toBe(true) expect(lineAndLineNumberHaveClass(7, 'a')).toBe(false) }) it('remove decoration classes when decorations are removed', async function () { decoration.destroy() await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(lineNumberHasClass(1, 'a')).toBe(false) expect(lineNumberHasClass(2, 'a')).toBe(false) expect(lineNumberHasClass(3, 'a')).toBe(false) expect(lineNumberHasClass(4, 'a')).toBe(false) }) it('removes decorations when their marker is invalidated', async function () { editor.getBuffer().insert([3, 2], 'n') await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(marker.isValid()).toBe(false) expect(lineAndLineNumberHaveClass(1, 'a')).toBe(false) expect(lineAndLineNumberHaveClass(2, 'a')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'a')).toBe(false) expect(lineAndLineNumberHaveClass(4, 'a')).toBe(false) editor.undo() await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(marker.isValid()).toBe(true) expect(lineAndLineNumberHaveClass(1, 'a')).toBe(false) expect(lineAndLineNumberHaveClass(2, 'a')).toBe(true) expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) expect(lineAndLineNumberHaveClass(4, 'a')).toBe(false) }) it('removes decorations when their marker is destroyed', async function () { marker.destroy() await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(lineNumberHasClass(1, 'a')).toBe(false) expect(lineNumberHasClass(2, 'a')).toBe(false) expect(lineNumberHasClass(3, 'a')).toBe(false) expect(lineNumberHasClass(4, 'a')).toBe(false) }) describe('when the decoration\'s "onlyHead" property is true', function () { it('only applies the decoration\'s class to lines containing the marker\'s head', async function () { editor.decorateMarker(marker, { type: ['line-number', 'line'], 'class': 'only-head', onlyHead: true }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(1, 'only-head')).toBe(false) expect(lineAndLineNumberHaveClass(2, 'only-head')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'only-head')).toBe(true) expect(lineAndLineNumberHaveClass(4, 'only-head')).toBe(false) }) }) describe('when the decoration\'s "onlyEmpty" property is true', function () { it('only applies the decoration when its marker is empty', async function () { editor.decorateMarker(marker, { type: ['line-number', 'line'], 'class': 'only-empty', onlyEmpty: true }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe(false) marker.clearTail() await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe(true) }) }) describe('when the decoration\'s "onlyNonEmpty" property is true', function () { it('only applies the decoration when its marker is non-empty', async function () { editor.decorateMarker(marker, { type: ['line-number', 'line'], 'class': 'only-non-empty', onlyNonEmpty: true }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe(true) expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe(true) marker.clearTail() await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe(false) }) }) }) describe('highlight decoration rendering', function () { let decoration, marker, scrollViewClientLeft beforeEach(async function () { scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left marker = editor.addMarkerLayer({ maintainHistory: true }).markBufferRange([[2, 13], [3, 15]], { invalidate: 'inside' }) decoration = editor.decorateMarker(marker, { type: 'highlight', 'class': 'test-highlight' }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() }) it('does not render highlights for off-screen lines until they come on-screen', async function () { wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], { invalidate: 'inside' }) editor.decorateMarker(marker, { type: 'highlight', 'class': 'some-highlight' }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(component.presenter.endRow).toBeLessThan(9) let regions = componentNode.querySelectorAll('.some-highlight .region') expect(regions.length).toBe(0) verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) await nextViewUpdatePromise() expect(component.presenter.endRow).toBeGreaterThan(8) regions = componentNode.querySelectorAll('.some-highlight .region') expect(regions.length).toBe(1) let regionRect = regions[0].style expect(regionRect.top).toBe(0 + 'px') expect(regionRect.height).toBe(1 * lineHeightInPixels + 'px') expect(regionRect.left).toBe(Math.round(2 * charWidth) + 'px') expect(regionRect.width).toBe(Math.round(2 * charWidth) + 'px') }) it('renders highlights decoration\'s marker is added', async function () { let regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe(2) }) it('removes highlights when a decoration is removed', async function () { decoration.destroy() await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() let regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe(0) }) it('does not render a highlight that is within a fold', async function () { editor.foldBufferRow(1) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(componentNode.querySelectorAll('.test-highlight').length).toBe(0) }) it('removes highlights when a decoration\'s marker is destroyed', async function () { marker.destroy() await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() let regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe(0) }) it('only renders highlights when a decoration\'s marker is valid', async function () { editor.getBuffer().insert([3, 2], 'n') await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(marker.isValid()).toBe(false) let regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe(0) editor.getBuffer().undo() await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(marker.isValid()).toBe(true) regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe(2) }) it('allows multiple space-delimited decoration classes', async function () { decoration.setProperties({ type: 'highlight', 'class': 'foo bar' }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(componentNode.querySelectorAll('.foo.bar').length).toBe(2) decoration.setProperties({ type: 'highlight', 'class': 'bar baz' }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(componentNode.querySelectorAll('.bar.baz').length).toBe(2) }) it('renders classes on the regions directly if "deprecatedRegionClass" option is defined', async function () { decoration = editor.decorateMarker(marker, { type: 'highlight', 'class': 'test-highlight', deprecatedRegionClass: 'test-highlight-region' }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() let regions = componentNode.querySelectorAll('.test-highlight .region.test-highlight-region') expect(regions.length).toBe(2) }) describe('when flashing a decoration via Decoration::flash()', function () { let highlightNode beforeEach(async function () { highlightNode = componentNode.querySelectorAll('.test-highlight')[1] }) it('adds and removes the flash class specified in ::flash', async function () { expect(highlightNode.classList.contains('flash-class')).toBe(false) decoration.flash('flash-class', 10) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(highlightNode.classList.contains('flash-class')).toBe(true) await conditionPromise(function () { return !highlightNode.classList.contains('flash-class') }) }) describe('when ::flash is called again before the first has finished', function () { it('removes the class from the decoration highlight before adding it for the second ::flash call', async function () { decoration.flash('flash-class', 500) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(highlightNode.classList.contains('flash-class')).toBe(true) decoration.flash('flash-class', 500) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(highlightNode.classList.contains('flash-class')).toBe(false) await conditionPromise(function () { return highlightNode.classList.contains('flash-class') }) }) }) }) describe('when a decoration\'s marker moves', function () { it('moves rendered highlights when the buffer is changed', async function () { let regionStyle = componentNode.querySelector('.test-highlight .region').style let originalTop = parseInt(regionStyle.top) expect(originalTop).toBe(2 * lineHeightInPixels) editor.getBuffer().insert([0, 0], '\n') await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() regionStyle = componentNode.querySelector('.test-highlight .region').style let newTop = parseInt(regionStyle.top) expect(newTop).toBe(0) }) it('moves rendered highlights when the marker is manually moved', async function () { let regionStyle = componentNode.querySelector('.test-highlight .region').style expect(parseInt(regionStyle.top)).toBe(2 * lineHeightInPixels) marker.setBufferRange([[5, 8], [5, 13]]) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() regionStyle = componentNode.querySelector('.test-highlight .region').style expect(parseInt(regionStyle.top)).toBe(2 * lineHeightInPixels) }) }) describe('when a decoration is updated via Decoration::update', function () { it('renders the decoration\'s new params', async function () { expect(componentNode.querySelector('.test-highlight')).toBeTruthy() decoration.setProperties({ type: 'highlight', 'class': 'new-test-highlight' }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(componentNode.querySelector('.test-highlight')).toBeFalsy() expect(componentNode.querySelector('.new-test-highlight')).toBeTruthy() }) }) }) describe('overlay decoration rendering', function () { let gutterWidth, item beforeEach(function () { item = document.createElement('div') item.classList.add('overlay-test') item.style.background = 'red' gutterWidth = componentNode.querySelector('.gutter').offsetWidth }) describe('when the marker is empty', function () { it('renders an overlay decoration when added and removes the overlay when the decoration is destroyed', async function () { let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], { invalidate: 'never' }) let decoration = editor.decorateMarker(marker, { type: 'overlay', item: item }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() let overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') expect(overlay).toBe(item) decoration.destroy() await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') expect(overlay).toBe(null) }) it('renders the overlay element with the CSS class specified by the decoration', async function () { let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], { invalidate: 'never' }) let decoration = editor.decorateMarker(marker, { type: 'overlay', 'class': 'my-overlay', item: item }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() let overlay = component.getTopmostDOMNode().querySelector('atom-overlay.my-overlay') expect(overlay).not.toBe(null) let child = overlay.querySelector('.overlay-test') expect(child).toBe(item) }) }) describe('when the marker is not empty', function () { it('renders at the head of the marker by default', async function () { let marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], { invalidate: 'never' }) let decoration = editor.decorateMarker(marker, { type: 'overlay', item: item }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() let position = wrapperNode.pixelPositionForBufferPosition([2, 10]) let overlay = component.getTopmostDOMNode().querySelector('atom-overlay') expect(overlay.style.left).toBe(Math.round(position.left + gutterWidth) + 'px') expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') }) }) describe('positioning the overlay when near the edge of the editor', function () { let itemHeight, itemWidth, windowHeight, windowWidth beforeEach(async function () { atom.storeWindowDimensions() itemWidth = Math.round(4 * editor.getDefaultCharWidth()) itemHeight = 4 * editor.getLineHeightInPixels() windowWidth = Math.round(gutterWidth + 30 * editor.getDefaultCharWidth()) windowHeight = 10 * editor.getLineHeightInPixels() item.style.width = itemWidth + 'px' item.style.height = itemHeight + 'px' wrapperNode.style.width = windowWidth + 'px' wrapperNode.style.height = windowHeight + 'px' atom.setWindowDimensions({ width: windowWidth, height: windowHeight }) component.measureDimensions() component.measureWindowSize() await nextViewUpdatePromise() }) afterEach(function () { atom.restoreWindowDimensions() }) it('slides horizontally left when near the right edge on #win32 and #darwin', async function () { let marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], { invalidate: 'never' }) let decoration = editor.decorateMarker(marker, { type: 'overlay', item: item }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() let position = wrapperNode.pixelPositionForBufferPosition([0, 26]) let overlay = component.getTopmostDOMNode().querySelector('atom-overlay') expect(overlay.style.left).toBe(Math.round(position.left + gutterWidth) + 'px') expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') editor.insertText('a') await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(overlay.style.left).toBe(windowWidth - itemWidth + 'px') expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') editor.insertText('b') await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(overlay.style.left).toBe(windowWidth - itemWidth + 'px') expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') }) }) }) describe('hidden input field', function () { it('renders the hidden input field at the position of the last cursor if the cursor is on screen and the editor is focused', async function () { editor.setVerticalScrollMargin(0) editor.setHorizontalScrollMargin(0) let inputNode = componentNode.querySelector('.hidden-input') wrapperNode.style.height = 5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(editor.getCursorScreenPosition()).toEqual([0, 0]) wrapperNode.setScrollTop(3 * lineHeightInPixels) wrapperNode.setScrollLeft(3 * charWidth) await nextViewUpdatePromise() expect(inputNode.offsetTop).toBe(0) expect(inputNode.offsetLeft).toBe(0) editor.setCursorBufferPosition([5, 4], { autoscroll: false }) await decorationsUpdatedPromise(editor) await nextViewUpdatePromise() expect(inputNode.offsetTop).toBe(0) expect(inputNode.offsetLeft).toBe(0) wrapperNode.focus() await nextViewUpdatePromise() expect(inputNode.offsetTop).toBe((5 * lineHeightInPixels) - wrapperNode.getScrollTop()) expect(inputNode.offsetLeft).toBeCloseTo((4 * charWidth) - wrapperNode.getScrollLeft(), 0) inputNode.blur() await nextViewUpdatePromise() expect(inputNode.offsetTop).toBe(0) expect(inputNode.offsetLeft).toBe(0) editor.setCursorBufferPosition([1, 2], { autoscroll: false }) await nextViewUpdatePromise() expect(inputNode.offsetTop).toBe(0) expect(inputNode.offsetLeft).toBe(0) inputNode.focus() await nextViewUpdatePromise() expect(inputNode.offsetTop).toBe(0) expect(inputNode.offsetLeft).toBe(0) }) }) describe('mouse interactions on the lines', function () { let linesNode beforeEach(function () { linesNode = componentNode.querySelector('.lines') }) describe('when the mouse is single-clicked above the first line', function () { it('moves the cursor to the start of file buffer position', async function () { let height editor.setText('foo') editor.setCursorBufferPosition([0, 3]) height = 4.5 * lineHeightInPixels wrapperNode.style.height = height + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() let coordinates = clientCoordinatesForScreenPosition([0, 2]) coordinates.clientY = -1 linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) await nextViewUpdatePromise() expect(editor.getCursorScreenPosition()).toEqual([0, 0]) }) }) describe('when the mouse is single-clicked below the last line', function () { it('moves the cursor to the end of file buffer position', async function () { editor.setText('foo') editor.setCursorBufferPosition([0, 0]) let height = 4.5 * lineHeightInPixels wrapperNode.style.height = height + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() let coordinates = clientCoordinatesForScreenPosition([0, 2]) coordinates.clientY = height * 2 linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) await nextViewUpdatePromise() expect(editor.getCursorScreenPosition()).toEqual([0, 3]) }) }) describe('when a non-folded line is single-clicked', function () { describe('when no modifier keys are held down', function () { it('moves the cursor to the nearest screen position', async function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() wrapperNode.setScrollTop(3.5 * lineHeightInPixels) wrapperNode.setScrollLeft(2 * charWidth) await nextViewUpdatePromise() linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]))) await nextViewUpdatePromise() expect(editor.getCursorScreenPosition()).toEqual([4, 8]) }) }) describe('when the shift key is held down', function () { it('selects to the nearest screen position', async function () { editor.setCursorScreenPosition([3, 4]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), { shiftKey: true })) await nextViewUpdatePromise() expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [5, 6]]) }) }) describe('when the command key is held down', function () { describe('the current cursor position and screen position do not match', function () { it('adds a cursor at the nearest screen position', async function () { editor.setCursorScreenPosition([3, 4]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), { metaKey: true })) await nextViewUpdatePromise() expect(editor.getSelectedScreenRanges()).toEqual([[[3, 4], [3, 4]], [[5, 6], [5, 6]]]) }) }) describe('when there are multiple cursors, and one of the cursor\'s screen position is the same as the mouse click screen position', async function () { it('removes a cursor at the mouse screen position', async function () { editor.setCursorScreenPosition([3, 4]) editor.addCursorAtScreenPosition([5, 2]) editor.addCursorAtScreenPosition([7, 5]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), { metaKey: true })) await nextViewUpdatePromise() expect(editor.getSelectedScreenRanges()).toEqual([[[5, 2], [5, 2]], [[7, 5], [7, 5]]]) }) }) describe('when there is a single cursor and the click occurs at the cursor\'s screen position', async function () { it('neither adds a new cursor nor removes the current cursor', async function () { editor.setCursorScreenPosition([3, 4]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), { metaKey: true })) await nextViewUpdatePromise() expect(editor.getSelectedScreenRanges()).toEqual([[[3, 4], [3, 4]]]) }) }) }) }) describe('when a non-folded line is double-clicked', function () { describe('when no modifier keys are held down', function () { it('selects the word containing the nearest screen position', function () { linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 1 })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 2 })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [5, 13]]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([6, 6]), { detail: 1 })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) expect(editor.getSelectedScreenRange()).toEqual([[6, 6], [6, 6]]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([8, 8]), { detail: 1, shiftKey: true })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) expect(editor.getSelectedScreenRange()).toEqual([[6, 6], [8, 8]]) }) }) describe('when the command key is held down', function () { it('selects the word containing the newly-added cursor', function () { linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 1, metaKey: true })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 2, metaKey: true })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) expect(editor.getSelectedScreenRanges()).toEqual([[[0, 0], [0, 0]], [[5, 6], [5, 13]]]) }) }) }) describe('when a non-folded line is triple-clicked', function () { describe('when no modifier keys are held down', function () { it('selects the line containing the nearest screen position', function () { linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 1 })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 2 })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 3 })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [6, 0]]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([6, 6]), { detail: 1, shiftKey: true })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [7, 0]]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([7, 5]), { detail: 1 })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([8, 8]), { detail: 1, shiftKey: true })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) expect(editor.getSelectedScreenRange()).toEqual([[7, 5], [8, 8]]) }) }) describe('when the command key is held down', function () { it('selects the line containing the newly-added cursor', function () { linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 1, metaKey: true })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 2, metaKey: true })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 3, metaKey: true })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) expect(editor.getSelectedScreenRanges()).toEqual([[[0, 0], [0, 0]], [[5, 0], [6, 0]]]) }) }) }) describe('when the mouse is clicked and dragged', function () { it('selects to the nearest screen position until the mouse button is released', async function () { linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { which: 1 })) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [10, 0]]) linesNode.dispatchEvent(buildMouseEvent('mouseup')) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([12, 0]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [10, 0]]) }) it('autoscrolls when the cursor approaches the boundaries of the editor', async function () { wrapperNode.style.height = '100px' wrapperNode.style.width = '100px' component.measureDimensions() await nextViewUpdatePromise() 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 })) for (let i = 0; i <= 5; ++i) { await nextAnimationFramePromise() } expect(wrapperNode.getScrollTop()).toBe(0) expect(wrapperNode.getScrollLeft()).toBeGreaterThan(0) linesNode.dispatchEvent(buildMouseEvent('mousemove', { clientX: 100, clientY: 100 }, { which: 1 })) for (let i = 0; i <= 5; ++i) { await nextAnimationFramePromise() } expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) let previousScrollTop = wrapperNode.getScrollTop() let previousScrollLeft = wrapperNode.getScrollLeft() linesNode.dispatchEvent(buildMouseEvent('mousemove', { clientX: 10, clientY: 50 }, { which: 1 })) for (let i = 0; i <= 5; ++i) { await nextAnimationFramePromise() } expect(wrapperNode.getScrollTop()).toBe(previousScrollTop) expect(wrapperNode.getScrollLeft()).toBeLessThan(previousScrollLeft) linesNode.dispatchEvent(buildMouseEvent('mousemove', { clientX: 10, clientY: 10 }, { which: 1 })) for (let i = 0; i <= 5; ++i) { await nextAnimationFramePromise() } expect(wrapperNode.getScrollTop()).toBeLessThan(previousScrollTop) }) it('stops selecting if the mouse is dragged into the dev tools', async function () { linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { which: 1 })) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), { which: 0 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) }) it('stops selecting before the buffer is modified during the drag', async function () { linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { which: 1 })) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) editor.insertText('x') await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 5], [2, 5]]) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), { which: 1 })) expect(editor.getSelectedScreenRange()).toEqual([[2, 5], [2, 5]]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { which: 1 })) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([5, 4]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [5, 4]]) editor.delete() await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [2, 4]]) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), { which: 1 })) expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [2, 4]]) }) describe('when the command key is held down', function () { it('adds a new selection and selects to the nearest screen position, then merges intersecting selections when the mouse button is released', async function () { editor.setSelectedScreenRange([[4, 4], [4, 9]]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { which: 1, metaKey: true })) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRanges()).toEqual([[[4, 4], [4, 9]], [[2, 4], [6, 8]]]) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([4, 6]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRanges()).toEqual([[[4, 4], [4, 9]], [[2, 4], [4, 6]]]) linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([4, 6]), { which: 1 })) expect(editor.getSelectedScreenRanges()).toEqual([[[2, 4], [4, 9]]]) }) }) describe('when the editor is destroyed while dragging', function () { it('cleans up the handlers for window.mouseup and window.mousemove', async function () { linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { which: 1 })) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { which: 1 })) await nextAnimationFramePromise() spyOn(window, 'removeEventListener').andCallThrough() linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 10]), { which: 1 })) editor.destroy() await nextAnimationFramePromise() for (let call of window.removeEventListener.calls) { call.args.pop() } expect(window.removeEventListener).toHaveBeenCalledWith('mouseup') expect(window.removeEventListener).toHaveBeenCalledWith('mousemove') }) }) }) describe('when the mouse is double-clicked and dragged', function () { it('expands the selection over the nearest word as the cursor moves', async function () { jasmine.attachToDOM(wrapperNode) wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 1 })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 2 })) expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [5, 13]]) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [12, 2]]) let maximalScrollTop = wrapperNode.getScrollTop() linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([9, 3]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [9, 4]]) expect(wrapperNode.getScrollTop()).toBe(maximalScrollTop) linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), { which: 1 })) }) }) describe('when the mouse is triple-clicked and dragged', function () { it('expands the selection over the nearest line as the cursor moves', async function () { jasmine.attachToDOM(wrapperNode) wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 1 })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 2 })) linesNode.dispatchEvent(buildMouseEvent('mouseup')) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 3 })) expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [6, 0]]) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [12, 2]]) let maximalScrollTop = wrapperNode.getScrollTop() linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 4]), { which: 1 })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [8, 0]]) expect(wrapperNode.getScrollTop()).toBe(maximalScrollTop) linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), { which: 1 })) }) }) describe('when a line is folded', function () { beforeEach(async function () { editor.foldBufferRow(4) await nextViewUpdatePromise() }) describe('when the folded line\'s fold-marker is clicked', function () { it('unfolds the buffer row', function () { let target = component.lineNodeForScreenRow(4).querySelector('.fold-marker') linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), { target: target })) expect(editor.isFoldedAtBufferRow(4)).toBe(false) }) }) }) describe('when the horizontal scrollbar is interacted with', function () { it('clicking on the scrollbar does not move the cursor', function () { let target = horizontalScrollbarNode linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), { target: target })) expect(editor.getCursorScreenPosition()).toEqual([0, 0]) }) }) }) describe('mouse interactions on the gutter', function () { let gutterNode beforeEach(function () { gutterNode = componentNode.querySelector('.gutter') }) describe('when the component is destroyed', function () { it('stops listening for selection events', function () { component.destroy() gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [0, 0]]) }) }) describe('when the gutter is clicked', function () { it('selects the clicked row', function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4))) expect(editor.getSelectedScreenRange()).toEqual([[4, 0], [5, 0]]) }) }) describe('when the gutter is meta-clicked', function () { it('creates a new selection for the clicked row', function () { editor.setSelectedScreenRange([[3, 0], [3, 2]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), { metaKey: true })) expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [5, 0]]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), { metaKey: true })) expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [5, 0]], [[6, 0], [7, 0]]]) }) }) describe('when the gutter is shift-clicked', function () { beforeEach(function () { editor.setSelectedScreenRange([[3, 4], [4, 5]]) }) describe('when the clicked row is before the current selection\'s tail', function () { it('selects to the beginning of the clicked row', function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { shiftKey: true })) expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [3, 4]]) }) }) describe('when the clicked row is after the current selection\'s tail', function () { it('selects to the beginning of the row following the clicked row', function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), { shiftKey: true })) expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [7, 0]]) }) }) }) describe('when the gutter is clicked and dragged', function () { describe('when dragging downward', function () { it('selects the rows between the start and end of the drag', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) await nextAnimationFramePromise() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [7, 0]]) }) }) describe('when dragging upward', function () { it('selects the rows between the start and end of the drag', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) await nextAnimationFramePromise() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2))) expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [7, 0]]) }) }) it('orients the selection appropriately when the mouse moves above or below the initially-clicked row', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) await nextAnimationFramePromise() expect(editor.getLastSelection().isReversed()).toBe(true) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) await nextAnimationFramePromise() expect(editor.getLastSelection().isReversed()).toBe(false) }) it('autoscrolls when the cursor approaches the top or bottom of the editor', async function () { wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) await nextAnimationFramePromise() expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) let maxScrollTop = wrapperNode.getScrollTop() gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(10))) await nextAnimationFramePromise() expect(wrapperNode.getScrollTop()).toBe(maxScrollTop) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) await nextAnimationFramePromise() expect(wrapperNode.getScrollTop()).toBeLessThan(maxScrollTop) }) it('stops selecting if a textInput event occurs during the drag', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [7, 0]]) let inputEvent = new Event('textInput') inputEvent.data = 'x' Object.defineProperty(inputEvent, 'target', { get: function () { return componentNode.querySelector('.hidden-input') } }) componentNode.dispatchEvent(inputEvent) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 1], [2, 1]]) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(12))) expect(editor.getSelectedScreenRange()).toEqual([[2, 1], [2, 1]]) }) }) describe('when the gutter is meta-clicked and dragged', function () { beforeEach(function () { editor.setSelectedScreenRange([[3, 0], [3, 2]]) }) describe('when dragging downward', function () { it('selects the rows between the start and end of the drag', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), { metaKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), { metaKey: true })) await nextAnimationFramePromise() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), { metaKey: true })) expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [7, 0]]]) }) it('merges overlapping selections when the mouse button is released', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), { metaKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), { metaKey: true })) await nextAnimationFramePromise() expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[2, 0], [7, 0]]]) gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), { metaKey: true })) expect(editor.getSelectedScreenRanges()).toEqual([[[2, 0], [7, 0]]]) }) }) describe('when dragging upward', function () { it('selects the rows between the start and end of the drag', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), { metaKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(4), { metaKey: true })) await nextAnimationFramePromise() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(4), { metaKey: true })) expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [7, 0]]]) }) it('merges overlapping selections', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), { metaKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2), { metaKey: true })) await nextAnimationFramePromise() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2), { metaKey: true })) expect(editor.getSelectedScreenRanges()).toEqual([[[2, 0], [7, 0]]]) }) }) }) describe('when the gutter is shift-clicked and dragged', function () { describe('when the shift-click is below the existing selection\'s tail', function () { describe('when dragging downward', function () { it('selects the rows between the existing selection\'s tail and the end of the drag', async function () { editor.setSelectedScreenRange([[3, 4], [4, 5]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), { shiftKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [9, 0]]) }) }) describe('when dragging upward', function () { it('selects the rows between the end of the drag and the tail of the existing selection', async function () { editor.setSelectedScreenRange([[4, 4], [5, 5]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), { shiftKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5))) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[4, 4], [6, 0]]) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [4, 4]]) }) }) }) describe('when the shift-click is above the existing selection\'s tail', function () { describe('when dragging upward', function () { it('selects the rows between the end of the drag and the tail of the existing selection', async function () { editor.setSelectedScreenRange([[4, 4], [5, 5]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), { shiftKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [4, 4]]) }) }) describe('when dragging downward', function () { it('selects the rows between the existing selection\'s tail and the end of the drag', async function () { editor.setSelectedScreenRange([[3, 4], [4, 5]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { shiftKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [3, 4]]) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [9, 0]]) }) }) }) }) describe('when soft wrap is enabled', function () { beforeEach(async function () { gutterNode = componentNode.querySelector('.gutter') editor.setSoftWrapped(true) await nextViewUpdatePromise() componentNode.style.width = 21 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() await nextViewUpdatePromise() }) describe('when the gutter is clicked', function () { it('selects the clicked buffer row', function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [2, 0]]) }) }) describe('when the gutter is meta-clicked', function () { it('creates a new selection for the clicked buffer row', function () { editor.setSelectedScreenRange([[1, 0], [1, 2]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), { metaKey: true })) expect(editor.getSelectedScreenRanges()).toEqual([[[1, 0], [1, 2]], [[2, 0], [5, 0]]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), { metaKey: true })) expect(editor.getSelectedScreenRanges()).toEqual([[[1, 0], [1, 2]], [[2, 0], [5, 0]], [[5, 0], [10, 0]]]) }) }) describe('when the gutter is shift-clicked', function () { beforeEach(function () { return editor.setSelectedScreenRange([[7, 4], [7, 6]]) }) describe('when the clicked row is before the current selection\'s tail', function () { it('selects to the beginning of the clicked buffer row', function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { shiftKey: true })) expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [7, 4]]) }) }) describe('when the clicked row is after the current selection\'s tail', function () { it('selects to the beginning of the screen row following the clicked buffer row', function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), { shiftKey: true })) expect(editor.getSelectedScreenRange()).toEqual([[7, 4], [16, 0]]) }) }) }) describe('when the gutter is clicked and dragged', function () { describe('when dragging downward', function () { it('selects the buffer row containing the click, then screen rows until the end of the drag', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) await nextAnimationFramePromise() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [6, 14]]) }) }) describe('when dragging upward', function () { it('selects the buffer row containing the click, then screen rows until the end of the drag', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) await nextAnimationFramePromise() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(1))) expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [10, 0]]) }) }) }) describe('when the gutter is meta-clicked and dragged', function () { beforeEach(function () { editor.setSelectedScreenRange([[7, 4], [7, 6]]) }) describe('when dragging downward', function () { it('adds a selection from the buffer row containing the click to the screen row containing the end of the drag', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { metaKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3), { metaKey: true })) await nextAnimationFramePromise() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(3), { metaKey: true })) expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[0, 0], [3, 14]]]) }) it('merges overlapping selections on mouseup', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { metaKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7), { metaKey: true })) await nextAnimationFramePromise() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(7), { metaKey: true })) expect(editor.getSelectedScreenRanges()).toEqual([[[0, 0], [7, 12]]]) }) }) describe('when dragging upward', function () { it('adds a selection from the buffer row containing the click to the screen row containing the end of the drag', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), { metaKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11), { metaKey: true })) await nextAnimationFramePromise() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), { metaKey: true })) expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[11, 4], [19, 0]]]) }) it('merges overlapping selections on mouseup', async function () { gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), { metaKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5), { metaKey: true })) await nextAnimationFramePromise() gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), { metaKey: true })) expect(editor.getSelectedScreenRanges()).toEqual([[[5, 0], [19, 0]]]) }) }) }) describe('when the gutter is shift-clicked and dragged', function () { describe('when the shift-click is below the existing selection\'s tail', function () { describe('when dragging downward', function () { it('selects the screen rows between the existing selection\'s tail and the end of the drag', async function () { editor.setSelectedScreenRange([[1, 4], [1, 7]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), { shiftKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11))) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [11, 14]]) }) }) describe('when dragging upward', function () { it('selects the screen rows between the end of the drag and the tail of the existing selection', async function () { editor.setSelectedScreenRange([[1, 4], [1, 7]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), { shiftKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [7, 12]]) }) }) }) describe('when the shift-click is above the existing selection\'s tail', function () { describe('when dragging upward', function () { it('selects the screen rows between the end of the drag and the tail of the existing selection', async function () { editor.setSelectedScreenRange([[7, 4], [7, 6]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(3), { shiftKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [7, 4]]) }) }) describe('when dragging downward', function () { it('selects the screen rows between the existing selection\'s tail and the end of the drag', async function () { editor.setSelectedScreenRange([[7, 4], [7, 6]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { shiftKey: true })) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3))) await nextAnimationFramePromise() expect(editor.getSelectedScreenRange()).toEqual([[3, 2], [7, 4]]) }) }) }) }) }) }) describe('focus handling', async function () { let inputNode beforeEach(function () { inputNode = componentNode.querySelector('.hidden-input') }) it('transfers focus to the hidden input', function () { expect(document.activeElement).toBe(document.body) wrapperNode.focus() expect(document.activeElement).toBe(wrapperNode) expect(wrapperNode.shadowRoot.activeElement).toBe(inputNode) }) it('adds the "is-focused" class to the editor when the hidden input is focused', async function () { expect(document.activeElement).toBe(document.body) inputNode.focus() await nextViewUpdatePromise() expect(componentNode.classList.contains('is-focused')).toBe(true) expect(wrapperNode.classList.contains('is-focused')).toBe(true) inputNode.blur() await nextViewUpdatePromise() expect(componentNode.classList.contains('is-focused')).toBe(false) expect(wrapperNode.classList.contains('is-focused')).toBe(false) }) }) describe('selection handling', function () { let cursor beforeEach(async function () { editor.setCursorScreenPosition([0, 0]) await nextViewUpdatePromise() }) it('adds the "has-selection" class to the editor when there is a selection', async function () { expect(componentNode.classList.contains('has-selection')).toBe(false) editor.selectDown() await nextViewUpdatePromise() expect(componentNode.classList.contains('has-selection')).toBe(true) editor.moveDown() await nextViewUpdatePromise() expect(componentNode.classList.contains('has-selection')).toBe(false) }) }) describe('scrolling', function () { it('updates the vertical scrollbar when the scrollTop is changed in the model', async function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(verticalScrollbarNode.scrollTop).toBe(0) wrapperNode.setScrollTop(10) await nextViewUpdatePromise() expect(verticalScrollbarNode.scrollTop).toBe(10) }) it('updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model', async function () { componentNode.style.width = 30 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() let top = 0 let tilesNodes = component.tileNodesForLines() for (let tileNode of tilesNodes) { expect(tileNode.style['-webkit-transform']).toBe('translate3d(0px, ' + top + 'px, 0px)') top += tileNode.offsetHeight } expect(horizontalScrollbarNode.scrollLeft).toBe(0) wrapperNode.setScrollLeft(100) await nextViewUpdatePromise() top = 0 for (let tileNode of tilesNodes) { expect(tileNode.style['-webkit-transform']).toBe('translate3d(-100px, ' + top + 'px, 0px)') top += tileNode.offsetHeight } expect(horizontalScrollbarNode.scrollLeft).toBe(100) }) it('updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes', async function () { componentNode.style.width = 30 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(wrapperNode.getScrollLeft()).toBe(0) horizontalScrollbarNode.scrollLeft = 100 horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) await nextViewUpdatePromise() expect(wrapperNode.getScrollLeft()).toBe(100) }) it('does not obscure the last line with the horizontal scrollbar', async function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) await nextViewUpdatePromise() let lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) let bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom topOfHorizontalScrollbar = horizontalScrollbarNode.getBoundingClientRect().top expect(bottomOfLastLine).toBe(topOfHorizontalScrollbar) wrapperNode.style.width = 100 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom let bottomOfEditor = componentNode.getBoundingClientRect().bottom expect(bottomOfLastLine).toBe(bottomOfEditor) }) it('does not obscure the last character of the longest line with the vertical scrollbar', async function () { wrapperNode.style.height = 7 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() wrapperNode.setScrollLeft(Infinity) await nextViewUpdatePromise() let rightOfLongestLine = component.lineNodeForScreenRow(6).querySelector('.line > span:last-child').getBoundingClientRect().right let leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left expect(Math.round(rightOfLongestLine)).toBeCloseTo(leftOfVerticalScrollbar - 1, 0) }) it('only displays dummy scrollbars when scrollable in that direction', async function () { expect(verticalScrollbarNode.style.display).toBe('none') expect(horizontalScrollbarNode.style.display).toBe('none') wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = '1000px' component.measureDimensions() await nextViewUpdatePromise() expect(verticalScrollbarNode.style.display).toBe('') expect(horizontalScrollbarNode.style.display).toBe('none') componentNode.style.width = 10 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(verticalScrollbarNode.style.display).toBe('') expect(horizontalScrollbarNode.style.display).toBe('') wrapperNode.style.height = 20 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(verticalScrollbarNode.style.display).toBe('none') expect(horizontalScrollbarNode.style.display).toBe('') }) it('makes the dummy scrollbar divs only as tall/wide as the actual scrollbars', async function () { wrapperNode.style.height = 4 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() atom.styles.addStyleSheet('::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}', { context: 'atom-text-editor' }) await nextAnimationFramePromise() await nextAnimationFramePromise() let scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') expect(verticalScrollbarNode.offsetWidth).toBe(8) expect(horizontalScrollbarNode.offsetHeight).toBe(8) expect(scrollbarCornerNode.offsetWidth).toBe(8) expect(scrollbarCornerNode.offsetHeight).toBe(8) atom.themes.removeStylesheet('test') }) it('assigns the bottom/right of the scrollbars to the width of the opposite scrollbar if it is visible', async function () { let scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') expect(verticalScrollbarNode.style.bottom).toBe('0px') expect(horizontalScrollbarNode.style.right).toBe('0px') wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = '1000px' component.measureDimensions() await nextViewUpdatePromise() expect(verticalScrollbarNode.style.bottom).toBe('0px') expect(horizontalScrollbarNode.style.right).toBe(verticalScrollbarNode.offsetWidth + 'px') expect(scrollbarCornerNode.style.display).toBe('none') componentNode.style.width = 10 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(verticalScrollbarNode.style.bottom).toBe(horizontalScrollbarNode.offsetHeight + 'px') expect(horizontalScrollbarNode.style.right).toBe(verticalScrollbarNode.offsetWidth + 'px') expect(scrollbarCornerNode.style.display).toBe('') wrapperNode.style.height = 20 * lineHeightInPixels + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(verticalScrollbarNode.style.bottom).toBe(horizontalScrollbarNode.offsetHeight + 'px') expect(horizontalScrollbarNode.style.right).toBe('0px') expect(scrollbarCornerNode.style.display).toBe('none') }) it('accounts for the width of the gutter in the scrollWidth of the horizontal scrollbar', async function () { let gutterNode = componentNode.querySelector('.gutter') componentNode.style.width = 10 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() expect(horizontalScrollbarNode.scrollWidth).toBe(wrapperNode.getScrollWidth()) expect(horizontalScrollbarNode.style.left).toBe('0px') }) }) describe('mousewheel events', function () { beforeEach(function () { atom.config.set('editor.scrollSensitivity', 100) }) describe('updating scrollTop and scrollLeft', function () { beforeEach(async function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() }) it('updates the scrollLeft or scrollTop on mousewheel events depending on which delta is greater (x or y)', async function () { expect(verticalScrollbarNode.scrollTop).toBe(0) expect(horizontalScrollbarNode.scrollLeft).toBe(0) componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: -5, wheelDeltaY: -10 })) await nextAnimationFramePromise() expect(verticalScrollbarNode.scrollTop).toBe(10) expect(horizontalScrollbarNode.scrollLeft).toBe(0) componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: -15, wheelDeltaY: -5 })) await nextAnimationFramePromise() expect(verticalScrollbarNode.scrollTop).toBe(10) expect(horizontalScrollbarNode.scrollLeft).toBe(15) }) it('updates the scrollLeft or scrollTop according to the scroll sensitivity', async function () { atom.config.set('editor.scrollSensitivity', 50) componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: -5, wheelDeltaY: -10 })) await nextAnimationFramePromise() expect(horizontalScrollbarNode.scrollLeft).toBe(0) componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: -15, wheelDeltaY: -5 })) await nextAnimationFramePromise() expect(verticalScrollbarNode.scrollTop).toBe(5) expect(horizontalScrollbarNode.scrollLeft).toBe(7) }) it('uses the previous scrollSensitivity when the value is not an int', async function () { atom.config.set('editor.scrollSensitivity', 'nope') componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: 0, wheelDeltaY: -10 })) await nextAnimationFramePromise() expect(verticalScrollbarNode.scrollTop).toBe(10) }) it('parses negative scrollSensitivity values at the minimum', async function () { atom.config.set('editor.scrollSensitivity', -50) componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: 0, wheelDeltaY: -10 })) await nextAnimationFramePromise() expect(verticalScrollbarNode.scrollTop).toBe(1) }) }) describe('when the mousewheel event\'s target is a line', function () { it('keeps the line on the DOM if it is scrolled off-screen', async function () { component.presenter.stoppedScrollingDelay = 3000 // account for slower build machines wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() let lineNode = componentNode.querySelector('.line') let wheelEvent = new WheelEvent('mousewheel', { wheelDeltaX: 0, wheelDeltaY: -500 }) Object.defineProperty(wheelEvent, 'target', { get: function () { return lineNode } }) componentNode.dispatchEvent(wheelEvent) await nextViewUpdatePromise() expect(componentNode.contains(lineNode)).toBe(true) }) it('does not set the mouseWheelScreenRow if scrolling horizontally', async function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() let lineNode = componentNode.querySelector('.line') let wheelEvent = new WheelEvent('mousewheel', { wheelDeltaX: 10, wheelDeltaY: 0 }) Object.defineProperty(wheelEvent, 'target', { get: function () { return lineNode } }) componentNode.dispatchEvent(wheelEvent) await nextAnimationFramePromise() expect(component.presenter.mouseWheelScreenRow).toBe(null) }) it('clears the mouseWheelScreenRow after a delay even if the event does not cause scrolling', async function () { expect(wrapperNode.getScrollTop()).toBe(0) let lineNode = componentNode.querySelector('.line') let wheelEvent = new WheelEvent('mousewheel', { wheelDeltaX: 0, wheelDeltaY: 10 }) Object.defineProperty(wheelEvent, 'target', { get: function () { return lineNode } }) componentNode.dispatchEvent(wheelEvent) expect(wrapperNode.getScrollTop()).toBe(0) expect(component.presenter.mouseWheelScreenRow).toBe(0) await conditionPromise(function () { return component.presenter.mouseWheelScreenRow == null }) }) it('does not preserve the line if it is on screen', function () { let lineNode, lineNodes, wheelEvent expect(componentNode.querySelectorAll('.line-number').length).toBe(14) lineNodes = componentNode.querySelectorAll('.line') expect(lineNodes.length).toBe(13) lineNode = lineNodes[0] wheelEvent = new WheelEvent('mousewheel', { wheelDeltaX: 0, wheelDeltaY: 100 }) Object.defineProperty(wheelEvent, 'target', { get: function () { return lineNode } }) componentNode.dispatchEvent(wheelEvent) expect(component.presenter.mouseWheelScreenRow).toBe(0) editor.insertText('hello') expect(componentNode.querySelectorAll('.line-number').length).toBe(14) expect(componentNode.querySelectorAll('.line').length).toBe(13) }) }) describe('when the mousewheel event\'s target is a line number', function () { it('keeps the line number on the DOM if it is scrolled off-screen', async function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() let lineNumberNode = componentNode.querySelectorAll('.line-number')[1] let wheelEvent = new WheelEvent('mousewheel', { wheelDeltaX: 0, wheelDeltaY: -500 }) Object.defineProperty(wheelEvent, 'target', { get: function () { return lineNumberNode } }) componentNode.dispatchEvent(wheelEvent) await nextAnimationFramePromise() expect(componentNode.contains(lineNumberNode)).toBe(true) }) }) it('only prevents the default action of the mousewheel event if it actually lead to scrolling', async function () { spyOn(WheelEvent.prototype, 'preventDefault').andCallThrough() wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() await nextViewUpdatePromise() componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: 0, wheelDeltaY: 50 })) expect(wrapperNode.getScrollTop()).toBe(0) expect(WheelEvent.prototype.preventDefault).not.toHaveBeenCalled() componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: 0, wheelDeltaY: -3000 })) await nextAnimationFramePromise() let maxScrollTop = wrapperNode.getScrollTop() expect(WheelEvent.prototype.preventDefault).toHaveBeenCalled() WheelEvent.prototype.preventDefault.reset() componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: 0, wheelDeltaY: -30 })) expect(wrapperNode.getScrollTop()).toBe(maxScrollTop) expect(WheelEvent.prototype.preventDefault).not.toHaveBeenCalled() componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: 50, wheelDeltaY: 0 })) expect(wrapperNode.getScrollLeft()).toBe(0) expect(WheelEvent.prototype.preventDefault).not.toHaveBeenCalled() componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: -3000, wheelDeltaY: 0 })) await nextAnimationFramePromise() let maxScrollLeft = wrapperNode.getScrollLeft() expect(WheelEvent.prototype.preventDefault).toHaveBeenCalled() WheelEvent.prototype.preventDefault.reset() componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: -30, wheelDeltaY: 0 })) expect(wrapperNode.getScrollLeft()).toBe(maxScrollLeft) expect(WheelEvent.prototype.preventDefault).not.toHaveBeenCalled() }) }) describe('input events', function () { function buildTextInputEvent ({data, target}) { let event = new Event('textInput') event.data = data Object.defineProperty(event, 'target', { get: function () { return target } }) return event } let inputNode beforeEach(function () { inputNode = componentNode.querySelector('.hidden-input') }) it('inserts the newest character in the input\'s value into the buffer', async function () { componentNode.dispatchEvent(buildTextInputEvent({ data: 'x', target: inputNode })) await nextViewUpdatePromise() expect(editor.lineTextForBufferRow(0)).toBe('xvar quicksort = function () {') componentNode.dispatchEvent(buildTextInputEvent({ data: 'y', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('xyvar quicksort = function () {') }) it('replaces the last character if the length of the input\'s value does not increase, as occurs with the accented character menu', async function () { componentNode.dispatchEvent(buildTextInputEvent({ data: 'u', target: inputNode })) await nextViewUpdatePromise() expect(editor.lineTextForBufferRow(0)).toBe('uvar quicksort = function () {') inputNode.setSelectionRange(0, 1) componentNode.dispatchEvent(buildTextInputEvent({ data: 'ü', target: inputNode })) await nextViewUpdatePromise() expect(editor.lineTextForBufferRow(0)).toBe('üvar quicksort = function () {') }) it('does not handle input events when input is disabled', async function () { component.setInputEnabled(false) componentNode.dispatchEvent(buildTextInputEvent({ data: 'x', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') await nextAnimationFramePromise() expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') }) it('groups events that occur close together in time into single undo entries', function () { let currentTime = 0 spyOn(Date, 'now').andCallFake(function () { return currentTime }) atom.config.set('editor.undoGroupingInterval', 100) editor.setText('') componentNode.dispatchEvent(buildTextInputEvent({ data: 'x', target: inputNode })) currentTime += 99 componentNode.dispatchEvent(buildTextInputEvent({ data: 'y', target: inputNode })) currentTime += 99 componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', { bubbles: true, cancelable: true })) currentTime += 101 componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', { bubbles: true, cancelable: true })) expect(editor.getText()).toBe('xy\nxy\nxy') componentNode.dispatchEvent(new CustomEvent('core:undo', { bubbles: true, cancelable: true })) expect(editor.getText()).toBe('xy\nxy') componentNode.dispatchEvent(new CustomEvent('core:undo', { bubbles: true, cancelable: true })) expect(editor.getText()).toBe('') }) describe('when IME composition is used to insert international characters', function () { function buildIMECompositionEvent (event, {data, target} = {}) { event = new Event(event) event.data = data Object.defineProperty(event, 'target', { get: function () { return target } }) return event } let inputNode beforeEach(function () { inputNode = componentNode.querySelector('.hidden-input') }) describe('when nothing is selected', function () { it('inserts the chosen completion', function () { componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { target: inputNode })) componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { data: 's', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('svar quicksort = function () {') componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { data: 'sd', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('sdvar quicksort = function () {') componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { target: inputNode })) componentNode.dispatchEvent(buildTextInputEvent({ data: '速度', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('速度var quicksort = function () {') }) it('reverts back to the original text when the completion helper is dismissed', function () { componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { target: inputNode })) componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { data: 's', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('svar quicksort = function () {') componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { data: 'sd', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('sdvar quicksort = function () {') componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') }) it('allows multiple accented character to be inserted with the \' on a US international layout', function () { inputNode.value = '\'' inputNode.setSelectionRange(0, 1) componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { target: inputNode })) componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { data: '\'', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('\'var quicksort = function () {') componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { target: inputNode })) componentNode.dispatchEvent(buildTextInputEvent({ data: 'á', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('ávar quicksort = function () {') inputNode.value = '\'' inputNode.setSelectionRange(0, 1) componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { target: inputNode })) componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { data: '\'', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('á\'var quicksort = function () {') componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { target: inputNode })) componentNode.dispatchEvent(buildTextInputEvent({ data: 'á', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('áávar quicksort = function () {') }) }) describe('when a string is selected', function () { beforeEach(function () { editor.setSelectedBufferRanges([[[0, 4], [0, 9]], [[0, 16], [0, 19]]]) }) it('inserts the chosen completion', function () { componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { target: inputNode })) componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { data: 's', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('var ssort = sction () {') componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { data: 'sd', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('var sdsort = sdction () {') componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { target: inputNode })) componentNode.dispatchEvent(buildTextInputEvent({ data: '速度', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('var 速度sort = 速度ction () {') }) it('reverts back to the original text when the completion helper is dismissed', function () { componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { target: inputNode })) componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { data: 's', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('var ssort = sction () {') componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { data: 'sd', target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('var sdsort = sdction () {') componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { target: inputNode })) expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') }) }) }) }) describe('commands', function () { describe('editor:consolidate-selections', function () { it('consolidates selections on the editor model, aborting the key binding if there is only one selection', function () { spyOn(editor, 'consolidateSelections').andCallThrough() let event = new CustomEvent('editor:consolidate-selections', { bubbles: true, cancelable: true }) event.abortKeyBinding = jasmine.createSpy('event.abortKeyBinding') componentNode.dispatchEvent(event) expect(editor.consolidateSelections).toHaveBeenCalled() expect(event.abortKeyBinding).toHaveBeenCalled() }) }) }) describe('when changing the font', async function () { it('measures the default char, the korean char, the double width char and the half width char widths', async function () { expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0) component.setFontSize(10) await nextViewUpdatePromise() expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0) expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0) expect(editor.getDoubleWidthCharWidth()).toBe(10) expect(editor.getHalfWidthCharWidth()).toBe(5) }) }) describe('hiding and showing the editor', function () { describe('when the editor is hidden when it is mounted', function () { it('defers measurement and rendering until the editor becomes visible', function () { wrapperNode.remove() let hiddenParent = document.createElement('div') hiddenParent.style.display = 'none' contentNode.appendChild(hiddenParent) wrapperNode = new TextEditorElement() wrapperNode.tileSize = TILE_SIZE wrapperNode.initialize(editor, atom) hiddenParent.appendChild(wrapperNode) component = wrapperNode.component componentNode = component.getDomNode() expect(componentNode.querySelectorAll('.line').length).toBe(0) hiddenParent.style.display = 'block' atom.views.performDocumentPoll() expect(componentNode.querySelectorAll('.line').length).toBeGreaterThan(0) }) }) describe('when the lineHeight changes while the editor is hidden', function () { it('does not attempt to measure the lineHeightInPixels until the editor becomes visible again', function () { wrapperNode.style.display = 'none' component.checkForVisibilityChange() let initialLineHeightInPixels = editor.getLineHeightInPixels() component.setLineHeight(2) expect(editor.getLineHeightInPixels()).toBe(initialLineHeightInPixels) wrapperNode.style.display = '' component.checkForVisibilityChange() expect(editor.getLineHeightInPixels()).not.toBe(initialLineHeightInPixels) }) }) describe('when the fontSize changes while the editor is hidden', function () { it('does not attempt to measure the lineHeightInPixels or defaultCharWidth until the editor becomes visible again', function () { wrapperNode.style.display = 'none' component.checkForVisibilityChange() let initialLineHeightInPixels = editor.getLineHeightInPixels() let initialCharWidth = editor.getDefaultCharWidth() component.setFontSize(22) expect(editor.getLineHeightInPixels()).toBe(initialLineHeightInPixels) expect(editor.getDefaultCharWidth()).toBe(initialCharWidth) wrapperNode.style.display = '' component.checkForVisibilityChange() expect(editor.getLineHeightInPixels()).not.toBe(initialLineHeightInPixels) expect(editor.getDefaultCharWidth()).not.toBe(initialCharWidth) }) it('does not re-measure character widths until the editor is shown again', async function () { wrapperNode.style.display = 'none' component.checkForVisibilityChange() component.setFontSize(22) editor.getBuffer().insert([0, 0], 'a') wrapperNode.style.display = '' component.checkForVisibilityChange() editor.setCursorBufferPosition([0, Infinity]) await nextViewUpdatePromise() let cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left let line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right expect(cursorLeft).toBeCloseTo(line0Right, 0) }) }) describe('when the fontFamily changes while the editor is hidden', function () { it('does not attempt to measure the defaultCharWidth until the editor becomes visible again', function () { wrapperNode.style.display = 'none' component.checkForVisibilityChange() let initialLineHeightInPixels = editor.getLineHeightInPixels() let initialCharWidth = editor.getDefaultCharWidth() component.setFontFamily('serif') expect(editor.getDefaultCharWidth()).toBe(initialCharWidth) wrapperNode.style.display = '' component.checkForVisibilityChange() expect(editor.getDefaultCharWidth()).not.toBe(initialCharWidth) }) it('does not re-measure character widths until the editor is shown again', async function () { wrapperNode.style.display = 'none' component.checkForVisibilityChange() component.setFontFamily('serif') wrapperNode.style.display = '' component.checkForVisibilityChange() editor.setCursorBufferPosition([0, Infinity]) await nextViewUpdatePromise() let cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left let line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right expect(cursorLeft).toBeCloseTo(line0Right, 0) }) }) describe('when stylesheets change while the editor is hidden', function () { afterEach(function () { atom.themes.removeStylesheet('test') }) it('does not re-measure character widths until the editor is shown again', async function () { atom.config.set('editor.fontFamily', 'sans-serif') wrapperNode.style.display = 'none' component.checkForVisibilityChange() atom.themes.applyStylesheet('test', '.function.js {\n font-weight: bold;\n}') wrapperNode.style.display = '' component.checkForVisibilityChange() editor.setCursorBufferPosition([0, Infinity]) await nextViewUpdatePromise() let cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left let line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right expect(cursorLeft).toBeCloseTo(line0Right, 0) }) }) }) describe('soft wrapping', function () { beforeEach(async function () { editor.setSoftWrapped(true) await nextViewUpdatePromise() }) it('updates the wrap location when the editor is resized', async function () { let newHeight = 4 * editor.getLineHeightInPixels() + 'px' expect(parseInt(newHeight)).toBeLessThan(wrapperNode.offsetHeight) wrapperNode.style.height = newHeight await nextViewUpdatePromise() expect(componentNode.querySelectorAll('.line')).toHaveLength(7) let gutterWidth = componentNode.querySelector('.gutter').offsetWidth componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' atom.views.performDocumentPoll() await nextViewUpdatePromise() expect(componentNode.querySelector('.line').textContent).toBe('var quicksort ') }) it('accounts for the scroll view\'s padding when determining the wrap location', async function () { let scrollViewNode = componentNode.querySelector('.scroll-view') scrollViewNode.style.paddingLeft = 20 + 'px' componentNode.style.width = 30 * charWidth + 'px' atom.views.performDocumentPoll() await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('var quicksort = ') }) }) describe('default decorations', function () { it('applies .cursor-line decorations for line numbers overlapping selections', async function () { editor.setCursorScreenPosition([4, 4]) await nextViewUpdatePromise() expect(lineNumberHasClass(3, 'cursor-line')).toBe(false) expect(lineNumberHasClass(4, 'cursor-line')).toBe(true) expect(lineNumberHasClass(5, 'cursor-line')).toBe(false) editor.setSelectedScreenRange([[3, 4], [4, 4]]) await nextViewUpdatePromise() expect(lineNumberHasClass(3, 'cursor-line')).toBe(true) expect(lineNumberHasClass(4, 'cursor-line')).toBe(true) editor.setSelectedScreenRange([[3, 4], [4, 0]]) await nextViewUpdatePromise() expect(lineNumberHasClass(3, 'cursor-line')).toBe(true) expect(lineNumberHasClass(4, 'cursor-line')).toBe(false) }) it('does not apply .cursor-line to the last line of a selection if it\'s empty', async function () { editor.setSelectedScreenRange([[3, 4], [5, 0]]) await nextViewUpdatePromise() expect(lineNumberHasClass(3, 'cursor-line')).toBe(true) expect(lineNumberHasClass(4, 'cursor-line')).toBe(true) expect(lineNumberHasClass(5, 'cursor-line')).toBe(false) }) it('applies .cursor-line decorations for lines containing the cursor in non-empty selections', async function () { editor.setCursorScreenPosition([4, 4]) await nextViewUpdatePromise() expect(lineHasClass(3, 'cursor-line')).toBe(false) expect(lineHasClass(4, 'cursor-line')).toBe(true) expect(lineHasClass(5, 'cursor-line')).toBe(false) editor.setSelectedScreenRange([[3, 4], [4, 4]]) await nextViewUpdatePromise() expect(lineHasClass(2, 'cursor-line')).toBe(false) expect(lineHasClass(3, 'cursor-line')).toBe(false) expect(lineHasClass(4, 'cursor-line')).toBe(false) expect(lineHasClass(5, 'cursor-line')).toBe(false) }) it('applies .cursor-line-no-selection to line numbers for rows containing the cursor when the selection is empty', async function () { editor.setCursorScreenPosition([4, 4]) await nextViewUpdatePromise() expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe(true) editor.setSelectedScreenRange([[3, 4], [4, 4]]) await nextViewUpdatePromise() expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe(false) }) }) describe('height', function () { describe('when the wrapper view has an explicit height', function () { it('does not assign a height on the component node', async function () { wrapperNode.style.height = '200px' component.measureDimensions() await nextViewUpdatePromise() expect(componentNode.style.height).toBe('') }) }) describe('when the wrapper view does not have an explicit height', function () { it('assigns a height on the component node based on the editor\'s content', function () { expect(wrapperNode.style.height).toBe('') expect(componentNode.style.height).toBe(editor.getScreenLineCount() * lineHeightInPixels + 'px') }) }) }) describe('when the "mini" property is true', function () { beforeEach(async function () { editor.setMini(true) await nextViewUpdatePromise() }) it('does not render the gutter', function () { expect(componentNode.querySelector('.gutter')).toBeNull() }) it('adds the "mini" class to the wrapper view', function () { expect(wrapperNode.classList.contains('mini')).toBe(true) }) it('does not have an opaque background on lines', function () { expect(component.linesComponent.getDomNode().getAttribute('style')).not.toContain('background-color') }) it('does not render invisible characters', function () { atom.config.set('editor.invisibles', { eol: 'E' }) atom.config.set('editor.showInvisibles', true) expect(component.lineNodeForScreenRow(0).textContent).toBe('var quicksort = function () {') }) it('does not assign an explicit line-height on the editor contents', function () { expect(componentNode.style.lineHeight).toBe('') }) it('does not apply cursor-line decorations', function () { expect(component.lineNodeForScreenRow(0).classList.contains('cursor-line')).toBe(false) }) }) describe('when placholderText is specified', function () { it('renders the placeholder text when the buffer is empty', async function () { editor.setPlaceholderText('Hello World') expect(componentNode.querySelector('.placeholder-text')).toBeNull() editor.setText('') await nextViewUpdatePromise() expect(componentNode.querySelector('.placeholder-text').textContent).toBe('Hello World') editor.setText('hey') await nextViewUpdatePromise() expect(componentNode.querySelector('.placeholder-text')).toBeNull() }) }) describe('grammar data attributes', function () { it('adds and updates the grammar data attribute based on the current grammar', function () { expect(wrapperNode.dataset.grammar).toBe('source js') editor.setGrammar(atom.grammars.nullGrammar) expect(wrapperNode.dataset.grammar).toBe('text plain null-grammar') }) }) describe('encoding data attributes', function () { it('adds and updates the encoding data attribute based on the current encoding', function () { expect(wrapperNode.dataset.encoding).toBe('utf8') editor.setEncoding('utf16le') expect(wrapperNode.dataset.encoding).toBe('utf16le') }) }) describe('detaching and reattaching the editor (regression)', function () { it('does not throw an exception', function () { wrapperNode.remove() jasmine.attachToDOM(wrapperNode) atom.commands.dispatch(wrapperNode, 'core:move-right') expect(editor.getCursorBufferPosition()).toEqual([0, 1]) }) }) describe('scoped config settings', function () { let coffeeComponent, coffeeEditor beforeEach(async function () { await atom.packages.activatePackage('language-coffee-script') coffeeEditor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) }) afterEach(function () { atom.packages.deactivatePackages() atom.packages.unloadPackages() }) describe('soft wrap settings', function () { beforeEach(function () { atom.config.set('editor.softWrap', true, { scopeSelector: '.source.coffee' }) atom.config.set('editor.preferredLineLength', 17, { scopeSelector: '.source.coffee' }) atom.config.set('editor.softWrapAtPreferredLineLength', true, { scopeSelector: '.source.coffee' }) editor.setDefaultCharWidth(1) editor.setEditorWidthInChars(20) coffeeEditor.setDefaultCharWidth(1) coffeeEditor.setEditorWidthInChars(20) }) it('wraps lines when editor.softWrap is true for a matching scope', function () { expect(editor.lineTextForScreenRow(2)).toEqual(' if (items.length <= 1) return items;') expect(coffeeEditor.lineTextForScreenRow(3)).toEqual(' return items ') }) it('updates the wrapped lines when editor.preferredLineLength changes', function () { atom.config.set('editor.preferredLineLength', 20, { scopeSelector: '.source.coffee' }) expect(coffeeEditor.lineTextForScreenRow(2)).toEqual(' return items if ') }) it('updates the wrapped lines when editor.softWrapAtPreferredLineLength changes', function () { atom.config.set('editor.softWrapAtPreferredLineLength', false, { scopeSelector: '.source.coffee' }) expect(coffeeEditor.lineTextForScreenRow(2)).toEqual(' return items if ') }) it('updates the wrapped lines when editor.softWrap changes', function () { atom.config.set('editor.softWrap', false, { scopeSelector: '.source.coffee' }) expect(coffeeEditor.lineTextForScreenRow(2)).toEqual(' return items if items.length <= 1') atom.config.set('editor.softWrap', true, { scopeSelector: '.source.coffee' }) expect(coffeeEditor.lineTextForScreenRow(3)).toEqual(' return items ') }) it('updates the wrapped lines when the grammar changes', function () { editor.setGrammar(coffeeEditor.getGrammar()) expect(editor.isSoftWrapped()).toBe(true) expect(editor.lineTextForScreenRow(0)).toEqual('var quicksort = ') }) describe('::isSoftWrapped()', function () { it('returns the correct value based on the scoped settings', function () { expect(editor.isSoftWrapped()).toBe(false) expect(coffeeEditor.isSoftWrapped()).toBe(true) }) }) }) describe('invisibles settings', function () { const jsInvisibles = { eol: 'J', space: 'A', tab: 'V', cr: 'A' } const coffeeInvisibles = { eol: 'C', space: 'O', tab: 'F', cr: 'E' } beforeEach(async function () { atom.config.set('editor.showInvisibles', true, { scopeSelector: '.source.js' }) atom.config.set('editor.invisibles', jsInvisibles, { scopeSelector: '.source.js' }) atom.config.set('editor.showInvisibles', false, { scopeSelector: '.source.coffee' }) atom.config.set('editor.invisibles', coffeeInvisibles, { scopeSelector: '.source.coffee' }) editor.setText(' a line with tabs\tand spaces \n') await nextViewUpdatePromise() }) it('renders the invisibles when editor.showInvisibles is true for a given grammar', function () { expect(component.lineNodeForScreenRow(0).textContent).toBe('' + jsInvisibles.space + 'a line with tabs' + jsInvisibles.tab + 'and spaces' + jsInvisibles.space + jsInvisibles.eol) }) it('does not render the invisibles when editor.showInvisibles is false for a given grammar', async function () { editor.setGrammar(coffeeEditor.getGrammar()) await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe(' a line with tabs and spaces ') }) it('re-renders the invisibles when the invisible settings change', async function () { let jsGrammar = editor.getGrammar() editor.setGrammar(coffeeEditor.getGrammar()) atom.config.set('editor.showInvisibles', true, { scopeSelector: '.source.coffee' }) await nextViewUpdatePromise() let newInvisibles = { eol: 'N', space: 'E', tab: 'W', cr: 'I' } expect(component.lineNodeForScreenRow(0).textContent).toBe('' + coffeeInvisibles.space + 'a line with tabs' + coffeeInvisibles.tab + 'and spaces' + coffeeInvisibles.space + coffeeInvisibles.eol) atom.config.set('editor.invisibles', newInvisibles, { scopeSelector: '.source.coffee' }) await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('' + newInvisibles.space + 'a line with tabs' + newInvisibles.tab + 'and spaces' + newInvisibles.space + newInvisibles.eol) editor.setGrammar(jsGrammar) await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('' + jsInvisibles.space + 'a line with tabs' + jsInvisibles.tab + 'and spaces' + jsInvisibles.space + jsInvisibles.eol) }) }) describe('editor.showIndentGuide', function () { beforeEach(async function () { atom.config.set('editor.showIndentGuide', true, { scopeSelector: '.source.js' }) atom.config.set('editor.showIndentGuide', false, { scopeSelector: '.source.coffee' }) await nextViewUpdatePromise() }) it('has an "indent-guide" class when scoped editor.showIndentGuide is true, but not when scoped editor.showIndentGuide is false', async function () { let line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) expect(line1LeafNodes[0].textContent).toBe(' ') expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false) editor.setGrammar(coffeeEditor.getGrammar()) await nextViewUpdatePromise() line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) expect(line1LeafNodes[0].textContent).toBe(' ') expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(false) expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false) }) it('removes the "indent-guide" class when editor.showIndentGuide to false', async function () { let line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) expect(line1LeafNodes[0].textContent).toBe(' ') expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false) atom.config.set('editor.showIndentGuide', false, { scopeSelector: '.source.js' }) await nextViewUpdatePromise() line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) expect(line1LeafNodes[0].textContent).toBe(' ') expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(false) expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false) }) }) }) describe('autoscroll', function () { beforeEach(async function () { editor.setVerticalScrollMargin(2) editor.setHorizontalScrollMargin(2) component.setLineHeight('10px') component.setFontSize(17) component.measureDimensions() await nextViewUpdatePromise() wrapperNode.setWidth(55) wrapperNode.setHeight(55) component.measureDimensions() await nextViewUpdatePromise() component.presenter.setHorizontalScrollbarHeight(0) component.presenter.setVerticalScrollbarWidth(0) await nextViewUpdatePromise() }) describe('when selecting buffer ranges', function () { it('autoscrolls the selection if it is last unless the "autoscroll" option is false', async function () { expect(wrapperNode.getScrollTop()).toBe(0) editor.setSelectedBufferRange([[5, 6], [6, 8]]) await nextViewUpdatePromise() let right = wrapperNode.pixelPositionForBufferPosition([6, 8 + editor.getHorizontalScrollMargin()]).left expect(wrapperNode.getScrollBottom()).toBe((7 + editor.getVerticalScrollMargin()) * 10) expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) editor.setSelectedBufferRange([[0, 0], [0, 0]]) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) expect(wrapperNode.getScrollLeft()).toBe(0) editor.setSelectedBufferRange([[6, 6], [6, 8]]) await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe((7 + editor.getVerticalScrollMargin()) * 10) expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) }) }) describe('when adding selections for buffer ranges', function () { it('autoscrolls to the added selection if needed', async function () { editor.addSelectionForBufferRange([[8, 10], [8, 15]]) await nextViewUpdatePromise() let right = wrapperNode.pixelPositionForBufferPosition([8, 15]).left expect(wrapperNode.getScrollBottom()).toBe((9 * 10) + (2 * 10)) expect(wrapperNode.getScrollRight()).toBeCloseTo(right + 2 * 10, 0) }) }) describe('when selecting lines containing cursors', function () { it('autoscrolls to the selection', async function () { editor.setCursorScreenPosition([5, 6]) await nextViewUpdatePromise() wrapperNode.scrollToTop() await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.selectLinesContainingCursors() await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe((7 + editor.getVerticalScrollMargin()) * 10) }) }) describe('when inserting text', function () { describe('when there are multiple empty selections on different lines', function () { it('autoscrolls to the last cursor', async function () { editor.setCursorScreenPosition([1, 2], { autoscroll: false }) await nextViewUpdatePromise() editor.addCursorAtScreenPosition([10, 4], { autoscroll: false }) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.insertText('a') await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(75) }) }) }) describe('when scrolled to cursor position', function () { it('scrolls the last cursor into view, centering around the cursor if possible and the "center" option is not false', async function () { editor.setCursorScreenPosition([8, 8], { autoscroll: false }) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) expect(wrapperNode.getScrollLeft()).toBe(0) editor.scrollToCursorPosition() await nextViewUpdatePromise() let right = wrapperNode.pixelPositionForScreenPosition([8, 9 + editor.getHorizontalScrollMargin()]).left expect(wrapperNode.getScrollTop()).toBe((8.8 * 10) - 30) expect(wrapperNode.getScrollBottom()).toBe((8.3 * 10) + 30) expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) 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', function () { it('scrolls down when the last cursor gets closer than ::verticalScrollMargin to the bottom of the editor', async function () { expect(wrapperNode.getScrollTop()).toBe(0) expect(wrapperNode.getScrollBottom()).toBe(5.5 * 10) editor.setCursorScreenPosition([2, 0]) await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe(5.5 * 10) editor.moveDown() await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe(6 * 10) editor.moveDown() await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe(7 * 10) }) it('scrolls up when the last cursor gets closer than ::verticalScrollMargin to the top of the editor', async function () { editor.setCursorScreenPosition([11, 0]) await nextViewUpdatePromise() wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) await nextViewUpdatePromise() editor.moveUp() await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe(wrapperNode.getScrollHeight()) editor.moveUp() await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(7 * 10) editor.moveUp() await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(6 * 10) }) it('scrolls right when the last cursor gets closer than ::horizontalScrollMargin to the right of the editor', async function () { expect(wrapperNode.getScrollLeft()).toBe(0) expect(wrapperNode.getScrollRight()).toBe(5.5 * 10) editor.setCursorScreenPosition([0, 2]) await nextViewUpdatePromise() expect(wrapperNode.getScrollRight()).toBe(5.5 * 10) editor.moveRight() await nextViewUpdatePromise() let margin = component.presenter.getHorizontalScrollMarginInPixels() let right = wrapperNode.pixelPositionForScreenPosition([0, 4]).left + margin expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) editor.moveRight() await nextViewUpdatePromise() right = wrapperNode.pixelPositionForScreenPosition([0, 5]).left + margin expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) }) it('scrolls left when the last cursor gets closer than ::horizontalScrollMargin to the left of the editor', async function () { wrapperNode.setScrollRight(wrapperNode.getScrollWidth()) await nextViewUpdatePromise() expect(wrapperNode.getScrollRight()).toBe(wrapperNode.getScrollWidth()) editor.setCursorScreenPosition([6, 62], { autoscroll: false }) await nextViewUpdatePromise() editor.moveLeft() await nextViewUpdatePromise() let margin = component.presenter.getHorizontalScrollMarginInPixels() let left = wrapperNode.pixelPositionForScreenPosition([6, 61]).left - margin expect(wrapperNode.getScrollLeft()).toBeCloseTo(left, 0) editor.moveLeft() await nextViewUpdatePromise() left = wrapperNode.pixelPositionForScreenPosition([6, 60]).left - margin expect(wrapperNode.getScrollLeft()).toBeCloseTo(left, 0) }) it('scrolls down when inserting lines makes the document longer than the editor\'s height', async function () { editor.setCursorScreenPosition([13, Infinity]) editor.insertNewline() await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe(14 * 10) editor.insertNewline() await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe(15 * 10) }) it('autoscrolls to the cursor when it moves due to undo', async function () { editor.insertText('abc') wrapperNode.setScrollTop(Infinity) await nextViewUpdatePromise() editor.undo() await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) }) it('does not scroll when the cursor moves into the visible area', async function () { editor.setCursorBufferPosition([0, 0]) await nextViewUpdatePromise() wrapperNode.setScrollTop(40) await nextViewUpdatePromise() editor.setCursorBufferPosition([6, 0]) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(40) }) it('honors the autoscroll option on cursor and selection manipulation methods', async function () { expect(wrapperNode.getScrollTop()).toBe(0) editor.addCursorAtScreenPosition([11, 11], {autoscroll: false}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.addCursorAtBufferPosition([11, 11], {autoscroll: false}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.setCursorScreenPosition([11, 11], {autoscroll: false}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.setCursorBufferPosition([11, 11], {autoscroll: false}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.addSelectionForBufferRange([[11, 11], [11, 11]], {autoscroll: false}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.addSelectionForScreenRange([[11, 11], [11, 12]], {autoscroll: false}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.setSelectedBufferRange([[11, 0], [11, 1]], {autoscroll: false}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.setSelectedScreenRange([[11, 0], [11, 6]], {autoscroll: false}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.clearSelections({autoscroll: false}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.addSelectionForScreenRange([[0, 0], [0, 4]]) await nextViewUpdatePromise() editor.getCursors()[0].setScreenPosition([11, 11], {autoscroll: true}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) editor.getCursors()[0].setBufferPosition([0, 0], {autoscroll: true}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], {autoscroll: true}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], {autoscroll: true}) await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) }) }) }) describe('::getVisibleRowRange()', function () { beforeEach(async function () { wrapperNode.style.height = lineHeightInPixels * 8 + 'px' component.measureDimensions() await nextViewUpdatePromise() }) it('returns the first and the last visible rows', async function () { component.setScrollTop(0) await nextViewUpdatePromise() expect(component.getVisibleRowRange()).toEqual([0, 9]) }) it('ends at last buffer row even if there\'s more space available', async function () { wrapperNode.style.height = lineHeightInPixels * 13 + 'px' component.measureDimensions() await nextViewUpdatePromise() component.setScrollTop(60) await nextViewUpdatePromise() expect(component.getVisibleRowRange()).toEqual([0, 13]) }) }) describe('::pixelPositionForScreenPosition()', () => { it('returns the correct horizontal position, even if it is on a row that has not yet been rendered (regression)', () => { editor.setTextInBufferRange([[5, 0], [6, 0]], 'hello world\n') expect(wrapperNode.pixelPositionForScreenPosition([5, Infinity]).left).toBeGreaterThan(0) }) }) describe('middle mouse paste on Linux', function () { let originalPlatform beforeEach(function () { originalPlatform = process.platform Object.defineProperty(process, 'platform', { value: 'linux' }) }) afterEach(function () { Object.defineProperty(process, 'platform', { value: originalPlatform }) }) it('pastes the previously selected text at the clicked location', async function () { let clipboardWrittenTo = false spyOn(require('ipc'), 'send').andCallFake(function (eventName, selectedText) { if (eventName === 'write-text-to-selection-clipboard') { require('../src/safe-clipboard').writeText(selectedText, 'selection') clipboardWrittenTo = true } }) atom.clipboard.write('') component.trackSelectionClipboard() editor.setSelectedBufferRange([[1, 6], [1, 10]]) await conditionPromise(function () { return clipboardWrittenTo }) componentNode.querySelector('.scroll-view').dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([10, 0]), { button: 1 })) componentNode.querySelector('.scroll-view').dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([10, 0]), { which: 2 })) expect(atom.clipboard.read()).toBe('sort') expect(editor.lineTextForBufferRow(10)).toBe('sort') }) }) function buildMouseEvent (type, ...propertiesObjects) { let properties = extend({ bubbles: true, cancelable: true }, ...propertiesObjects) if (properties.detail == null) { properties.detail = 1 } let event = new MouseEvent(type, properties) if (properties.which != null) { Object.defineProperty(event, 'which', { get: function () { return properties.which } }) } if (properties.target != null) { Object.defineProperty(event, 'target', { get: function () { return properties.target } }) Object.defineProperty(event, 'srcObject', { get: function () { return properties.target } }) } return event } function clientCoordinatesForScreenPosition (screenPosition) { let clientX, clientY, positionOffset, scrollViewClientRect positionOffset = wrapperNode.pixelPositionForScreenPosition(screenPosition) scrollViewClientRect = componentNode.querySelector('.scroll-view').getBoundingClientRect() clientX = scrollViewClientRect.left + positionOffset.left - wrapperNode.getScrollLeft() clientY = scrollViewClientRect.top + positionOffset.top - wrapperNode.getScrollTop() return { clientX: clientX, clientY: clientY } } function clientCoordinatesForScreenRowInGutter (screenRow) { let clientX, clientY, gutterClientRect, positionOffset positionOffset = wrapperNode.pixelPositionForScreenPosition([screenRow, Infinity]) gutterClientRect = componentNode.querySelector('.gutter').getBoundingClientRect() clientX = gutterClientRect.left + positionOffset.left - wrapperNode.getScrollLeft() clientY = gutterClientRect.top + positionOffset.top - wrapperNode.getScrollTop() return { clientX: clientX, clientY: clientY } } function lineAndLineNumberHaveClass (screenRow, klass) { return lineHasClass(screenRow, klass) && lineNumberHasClass(screenRow, klass) } function lineNumberHasClass (screenRow, klass) { return component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) } function lineNumberForBufferRowHasClass (bufferRow, klass) { let screenRow screenRow = editor.displayBuffer.screenRowForBufferRow(bufferRow) return component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) } function lineHasClass (screenRow, klass) { return component.lineNodeForScreenRow(screenRow).classList.contains(klass) } function getLeafNodes (node) { if (node.children.length > 0) { return flatten(toArray(node.children).map(getLeafNodes)) } else { return [node] } } function conditionPromise (condition) { let timeoutError = new Error("Timed out waiting on condition") Error.captureStackTrace(timeoutError, conditionPromise) return new Promise(function (resolve, reject) { let interval = window.setInterval(function () { if (condition()) { window.clearInterval(interval) window.clearTimeout(timeout) resolve() } }, 100) let timeout = window.setTimeout(function () { window.clearInterval(interval) reject(timeoutError) }, 5000) }) } function timeoutPromise (timeout) { return new Promise(function (resolve) { window.setTimeout(resolve, timeout) }) } function nextAnimationFramePromise () { return new Promise(function (resolve) { window.requestAnimationFrame(resolve) }) } function nextViewUpdatePromise () { let timeoutError = new Error('Timed out waiting on a view update.') Error.captureStackTrace(timeoutError, nextViewUpdatePromise) return new Promise(function (resolve, reject) { let nextUpdatePromise = atom.views.getNextUpdatePromise() nextUpdatePromise.then(function (ts) { window.clearTimeout(timeout) resolve(ts) }) let timeout = window.setTimeout(function () { timeoutError.message += ' Frame pending? ' + atom.views.animationFrameRequest + ' Same next update promise pending? ' + (nextUpdatePromise === atom.views.nextUpdatePromise) reject(timeoutError) }, 30000) }) } function decorationsUpdatedPromise(editor) { return new Promise(function (resolve) { let disposable = editor.onDidUpdateDecorations(function () { disposable.dispose() resolve() }) }) } })