diff --git a/exports/atom.coffee b/exports/atom.coffee index 773b24f3a..7b6318c7a 100644 --- a/exports/atom.coffee +++ b/exports/atom.coffee @@ -15,7 +15,10 @@ unless process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE module.exports.$ = $ module.exports.$$ = $$ module.exports.$$$ = $$$ - module.exports.EditorView = require '../src/editor-view' + if atom.config.get('core.useReactMiniEditors') + module.exports.EditorView = require '../src/react-editor-view' + else + module.exports.EditorView = require '../src/editor-view' module.exports.ScrollView = require '../src/scroll-view' module.exports.SelectListView = require '../src/select-list-view' module.exports.Task = require '../src/task' diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index da551e2b4..69ee93c4c 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -6,7 +6,7 @@ EditorComponent = require '../src/editor-component' nbsp = String.fromCharCode(160) describe "EditorComponent", -> - [contentNode, editor, wrapperView, component, node, verticalScrollbarNode, horizontalScrollbarNode] = [] + [contentNode, editor, wrapperView, wrapperNode, component, componentNode, verticalScrollbarNode, horizontalScrollbarNode] = [] [lineHeightInPixels, charWidth, delayAnimationFrames, nextAnimationFrame, runSetImmediateCallbacks, lineOverdrawMargin] = [] beforeEach -> @@ -48,6 +48,7 @@ describe "EditorComponent", -> wrapperView = new ReactEditorView(editor, {lineOverdrawMargin}) wrapperView.attachToDom() + wrapperNode = wrapperView.element {component} = wrapperView component.performSyncUpdates = false @@ -56,13 +57,11 @@ describe "EditorComponent", -> lineHeightInPixels = editor.getLineHeightInPixels() charWidth = editor.getDefaultCharWidth() - node = component.getDOMNode() - verticalScrollbarNode = node.querySelector('.vertical-scrollbar') - horizontalScrollbarNode = node.querySelector('.horizontal-scrollbar') + componentNode = component.getDOMNode() + verticalScrollbarNode = componentNode.querySelector('.vertical-scrollbar') + horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') - node.style.height = editor.getLineCount() * lineHeightInPixels + 'px' - node.style.width = '1000px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() afterEach -> @@ -70,13 +69,13 @@ describe "EditorComponent", -> describe "line rendering", -> it "renders the currently-visible lines plus the overdraw margin", -> - node.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() - linesNode = node.querySelector('.lines') + linesNode = componentNode.querySelector('.lines') expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(node.querySelectorAll('.line').length).toBe 6 + 2 # no margin above + expect(componentNode.querySelectorAll('.line').length).toBe 6 + 2 # no margin above expect(component.lineNodeForScreenRow(0).textContent).toBe editor.lineForScreenRow(0).text expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 expect(component.lineNodeForScreenRow(5).textContent).toBe editor.lineForScreenRow(5).text @@ -86,7 +85,7 @@ describe "EditorComponent", -> verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, #{-4.5 * lineHeightInPixels}px, 0px)" - expect(node.querySelectorAll('.line').length).toBe 6 + 4 # margin above and below + expect(componentNode.querySelectorAll('.line').length).toBe 6 + 4 # margin above and below expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels expect(component.lineNodeForScreenRow(2).textContent).toBe editor.lineForScreenRow(2).text expect(component.lineNodeForScreenRow(9).offsetTop).toBe 9 * lineHeightInPixels @@ -96,7 +95,7 @@ describe "EditorComponent", -> editor.getBuffer().deleteRows(0, 1) runSetImmediateCallbacks() - lineNodes = node.querySelectorAll('.line') + lineNodes = componentNode.querySelectorAll('.line') expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels @@ -104,7 +103,7 @@ describe "EditorComponent", -> editor.getBuffer().insert([0, 0], '\n\n') runSetImmediateCallbacks() - lineNodes = node.querySelectorAll('.line') + lineNodes = componentNode.querySelectorAll('.line') expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels @@ -112,8 +111,8 @@ describe "EditorComponent", -> expect(component.lineNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels it "updates the lines when lines are inserted or removed above the rendered row range", -> - node.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) @@ -161,20 +160,20 @@ describe "EditorComponent", -> it "renders the .lines div at the full height of the editor if there aren't enough lines to scroll vertically", -> editor.setText('') - node.style.height = '300px' - component.measureScrollView() + wrapperNode.style.height = '300px' + component.measureHeightAndWidth() runSetImmediateCallbacks() - linesNode = node.querySelector('.lines') + 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", -> - gutterWidth = node.querySelector('.gutter').offsetWidth - scrollViewNode = node.querySelector('.scroll-view') - lineNodes = node.querySelectorAll('.line') + gutterWidth = componentNode.querySelector('.gutter').offsetWidth + scrollViewNode = componentNode.querySelector('.scroll-view') + lineNodes = componentNode.querySelectorAll('.line') - node.style.width = gutterWidth + (30 * charWidth) + 'px' - component.measureScrollView() + componentNode.style.width = gutterWidth + (30 * charWidth) + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(editor.getScrollWidth()).toBeGreaterThan scrollViewNode.offsetWidth @@ -185,8 +184,8 @@ describe "EditorComponent", -> for lineNode in lineNodes expect(lineNode.style.width).toBe editor.getScrollWidth() + 'px' - node.style.width = gutterWidth + editor.getScrollWidth() + 100 + 'px' - component.measureScrollView() + componentNode.style.width = gutterWidth + editor.getScrollWidth() + 100 + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() scrollViewWidth = scrollViewNode.offsetWidth @@ -259,8 +258,8 @@ describe "EditorComponent", -> editor.setText "a line that wraps " editor.setSoftWrap(true) runSetImmediateCallbacks() - node.style.width = 16 * charWidth + 'px' - component.measureScrollView() + componentNode.style.width = 16 * charWidth + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() it "doesn't show end of line invisibles at the end of wrapped lines", -> @@ -396,18 +395,18 @@ describe "EditorComponent", -> {gutter} = component.refs it "renders the currently-visible line numbers", -> - node.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() - expect(node.querySelectorAll('.line-number').length).toBe 6 + 2 + 1 # line overdraw margin below + dummy line number + expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 2 + 1 # line overdraw margin below + dummy line number expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}6" verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - expect(node.querySelectorAll('.line-number').length).toBe 6 + 4 + 1 # line overdraw margin above/below + dummy line number + expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 4 + 1 # line overdraw margin above/below + dummy line number expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}3" expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels @@ -418,7 +417,7 @@ describe "EditorComponent", -> editor.getBuffer().insert([0, 0], '\n\n') runSetImmediateCallbacks() - lineNumberNodes = node.querySelectorAll('.line-number') + lineNumberNodes = componentNode.querySelectorAll('.line-number') expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels @@ -438,12 +437,12 @@ describe "EditorComponent", -> it "renders • characters for soft-wrapped lines", -> editor.setSoftWrap(true) - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = 30 * charWidth + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 30 * charWidth + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() - expect(node.querySelectorAll('.line-number').length).toBe 6 + lineOverdrawMargin + 1 # 1 dummy line node + expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + lineOverdrawMargin + 1 # 1 dummy line componentNode expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" expect(component.lineNumberNodeForScreenRow(1).textContent).toBe "#{nbsp}•" expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}2" @@ -458,7 +457,7 @@ describe "EditorComponent", -> expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" - gutterNode = node.querySelector('.gutter') + gutterNode = componentNode.querySelector('.gutter') initialGutterWidth = gutterNode.offsetWidth # Removes padding when the max number of digits goes down @@ -477,10 +476,10 @@ describe "EditorComponent", -> 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", -> - node.style.height = node.offsetHeight + 100 + 'px' - component.measureScrollView() + wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() - expect(node.querySelector('.line-numbers').offsetHeight).toBe node.offsetHeight + expect(componentNode.querySelector('.line-numbers').offsetHeight).toBe componentNode.offsetHeight describe "when the editor.showLineNumbers config is false", -> it "doesn't render any line numbers", -> @@ -523,7 +522,7 @@ describe "EditorComponent", -> runSetImmediateCallbacks() expect(lineNumberHasClass(11, 'foldable')).toBe false - it "adds, updates and removes the folded class on the correct line number nodes", -> + it "adds, updates and removes the folded class on the correct line number componentNodes", -> editor.foldBufferRow(4) runSetImmediateCallbacks() expect(lineNumberHasClass(4, 'folded')).toBe true @@ -544,7 +543,7 @@ describe "EditorComponent", -> buildMouseEvent('click', {target}) beforeEach -> - gutterNode = node.querySelector('.gutter') + gutterNode = componentNode.querySelector('.gutter') it "folds and unfolds the block represented by the fold indicator when clicked", -> expect(lineNumberHasClass(1, 'folded')).toBe false @@ -561,7 +560,7 @@ describe "EditorComponent", -> runSetImmediateCallbacks() expect(lineNumberHasClass(1, 'folded')).toBe false - it "does not fold when the line number node is clicked", -> + it "does not fold when the line number componentNode is clicked", -> lineNumber = component.lineNumberNodeForScreenRow(1) lineNumber.dispatchEvent(buildClickEvent(lineNumber)) runSetImmediateCallbacks() @@ -572,12 +571,12 @@ describe "EditorComponent", -> cursor1 = editor.getCursor() cursor1.setScreenPosition([0, 5]) - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = 20 * lineHeightInPixels + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * lineHeightInPixels + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() - cursorNodes = node.querySelectorAll('.cursor') + cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 expect(cursorNodes[0].offsetHeight).toBe lineHeightInPixels expect(cursorNodes[0].offsetWidth).toBe charWidth @@ -587,7 +586,7 @@ describe "EditorComponent", -> cursor3 = editor.addCursorAtScreenPosition([4, 10]) runSetImmediateCallbacks() - cursorNodes = node.querySelectorAll('.cursor') + cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 expect(cursorNodes[0].offsetTop).toBe 0 expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{5 * charWidth}px, #{0 * lineHeightInPixels}px, 0px)" @@ -598,14 +597,14 @@ describe "EditorComponent", -> horizontalScrollbarNode.scrollLeft = 3.5 * charWidth horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - cursorNodes = node.querySelectorAll('.cursor') + cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{(11 - 3.5) * charWidth}px, #{(8 - 4.5) * lineHeightInPixels}px, 0px)" expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{(10 - 3.5) * charWidth}px, #{(4 - 4.5) * lineHeightInPixels}px, 0px)" cursor3.destroy() runSetImmediateCallbacks() - cursorNodes = node.querySelectorAll('.cursor') + cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{(11 - 3.5) * charWidth}px, #{(6 - 2.5) * lineHeightInPixels}px, 0px)" @@ -614,7 +613,7 @@ describe "EditorComponent", -> editor.setCursorScreenPosition([0, 16]) runSetImmediateCallbacks() - cursor = node.querySelector('.cursor') + cursor = componentNode.querySelector('.cursor') cursorRect = cursor.getBoundingClientRect() cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild @@ -639,7 +638,7 @@ describe "EditorComponent", -> runSetImmediateCallbacks() # re-measure characters once for a synchronous set of stylesheet changes runSetImmediateCallbacks() # update based on new measurements - cursor = node.querySelector('.cursor') + cursor = componentNode.querySelector('.cursor') cursorRect = cursor.getBoundingClientRect() cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild @@ -656,18 +655,18 @@ describe "EditorComponent", -> it "sets the cursor to the default character width at the end of a line", -> editor.setCursorScreenPosition([0, Infinity]) runSetImmediateCallbacks() - cursorNode = node.querySelector('.cursor') + cursorNode = componentNode.querySelector('.cursor') expect(cursorNode.offsetWidth).toBe charWidth it "gives the cursor a non-zero width even if it's inside atomic tokens", -> editor.setCursorScreenPosition([1, 0]) runSetImmediateCallbacks() - cursorNode = node.querySelector('.cursor') + cursorNode = componentNode.querySelector('.cursor') expect(cursorNode.offsetWidth).toBe charWidth it "blinks cursors when they aren't moving", -> spyOn(_._, 'now').andCallFake -> window.now # Ensure _.debounce is based on our fake spec timeline - cursorsNode = node.querySelector('.cursors') + cursorsNode = componentNode.querySelector('.cursors') expect(cursorsNode.classList.contains('blink-off')).toBe false advanceClock(component.props.cursorBlinkPeriod / 2) @@ -689,7 +688,7 @@ describe "EditorComponent", -> editor.addCursorAtScreenPosition([6, 8]) runSetImmediateCallbacks() - cursorNodes = node.querySelectorAll('.cursor') + cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{8 * charWidth}px, #{6 * lineHeightInPixels}px, 0px)" @@ -697,21 +696,21 @@ describe "EditorComponent", -> editor.setCursorBufferPosition([1, 10]) component.setLineHeight(2) runSetImmediateCallbacks() - cursorNode = node.querySelector('.cursor') + cursorNode = componentNode.querySelector('.cursor') expect(cursorNode.style['-webkit-transform']).toBe "translate3d(#{10 * editor.getDefaultCharWidth()}px, #{editor.getLineHeightInPixels()}px, 0px)" it "updates cursor positions when the font size changes", -> editor.setCursorBufferPosition([1, 10]) component.setFontSize(10) runSetImmediateCallbacks() - cursorNode = node.querySelector('.cursor') + cursorNode = componentNode.querySelector('.cursor') expect(cursorNode.style['-webkit-transform']).toBe "translate3d(#{10 * editor.getDefaultCharWidth()}px, #{editor.getLineHeightInPixels()}px, 0px)" it "updates cursor positions when the font family changes", -> editor.setCursorBufferPosition([1, 10]) component.setFontFamily('sans-serif') runSetImmediateCallbacks() - cursorNode = node.querySelector('.cursor') + cursorNode = componentNode.querySelector('.cursor') {left} = editor.pixelPositionForScreenPosition([1, 10]) expect(cursorNode.style['-webkit-transform']).toBe "translate3d(#{left}px, #{editor.getLineHeightInPixels()}px, 0px)" @@ -720,14 +719,14 @@ describe "EditorComponent", -> [scrollViewNode, scrollViewClientLeft] = [] beforeEach -> - scrollViewNode = node.querySelector('.scroll-view') - scrollViewClientLeft = node.querySelector('.scroll-view').getBoundingClientRect().left + scrollViewNode = componentNode.querySelector('.scroll-view') + scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left it "renders 1 region for 1-line selections", -> # 1-line selection editor.setSelectedScreenRange([[1, 6], [1, 10]]) runSetImmediateCallbacks() - regions = node.querySelectorAll('.selection .region') + regions = componentNode.querySelectorAll('.selection .region') expect(regions.length).toBe 1 regionRect = regions[0].getBoundingClientRect() @@ -739,7 +738,7 @@ describe "EditorComponent", -> it "renders 2 regions for 2-line selections", -> editor.setSelectedScreenRange([[1, 6], [2, 10]]) runSetImmediateCallbacks() - regions = node.querySelectorAll('.selection .region') + regions = componentNode.querySelectorAll('.selection .region') expect(regions.length).toBe 2 region1Rect = regions[0].getBoundingClientRect() @@ -757,7 +756,7 @@ describe "EditorComponent", -> it "renders 3 regions for selections with more than 2 lines", -> editor.setSelectedScreenRange([[1, 6], [5, 10]]) runSetImmediateCallbacks() - regions = node.querySelectorAll('.selection .region') + regions = componentNode.querySelectorAll('.selection .region') expect(regions.length).toBe 3 region1Rect = regions[0].getBoundingClientRect() @@ -784,20 +783,20 @@ describe "EditorComponent", -> expect(editor.getSelection(0).isEmpty()).toBe true expect(editor.getSelection(1).isEmpty()).toBe true - expect(node.querySelectorAll('.selection').length).toBe 0 + expect(componentNode.querySelectorAll('.selection').length).toBe 0 it "updates selections when the line height changes", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setLineHeight(2) runSetImmediateCallbacks() - selectionNode = node.querySelector('.region') + selectionNode = componentNode.querySelector('.region') expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() it "updates selections when the font size changes", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontSize(10) runSetImmediateCallbacks() - selectionNode = node.querySelector('.region') + selectionNode = componentNode.querySelector('.region') expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() expect(selectionNode.offsetLeft).toBe 6 * editor.getDefaultCharWidth() @@ -805,14 +804,14 @@ describe "EditorComponent", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontFamily('sans-serif') runSetImmediateCallbacks() - selectionNode = node.querySelector('.region') + selectionNode = componentNode.querySelector('.region') expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() expect(selectionNode.offsetLeft).toBe editor.pixelPositionForScreenPosition([1, 6]).left it "will flash the selection when flash:true is passed to editor::setSelectedBufferRange", -> editor.setSelectedBufferRange([[1, 6], [1, 10]], flash: true) runSetImmediateCallbacks() - selectionNode = node.querySelector('.selection') + selectionNode = componentNode.querySelector('.selection') expect(selectionNode.classList.contains('flash')).toBe true advanceClock editor.selectionFlashDuration @@ -836,8 +835,8 @@ describe "EditorComponent", -> expect(lineAndLineNumberHaveClass(3, 'a')).toBe true # Shrink editor vertically - node.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() # Add decorations that are out of range @@ -859,8 +858,8 @@ describe "EditorComponent", -> it "only applies decorations to screen rows that are spanned by their marker when lines are soft-wrapped", -> editor.setText("a line that wraps, ok") editor.setSoftWrap(true) - node.style.width = 16 * charWidth + 'px' - component.measureScrollView() + componentNode.style.width = 16 * charWidth + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() marker.destroy() @@ -968,15 +967,15 @@ describe "EditorComponent", -> describe "highlight decoration rendering", -> [marker, decoration, decorationParams, scrollViewClientLeft] = [] beforeEach -> - scrollViewClientLeft = node.querySelector('.scroll-view').getBoundingClientRect().left + scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], invalidate: 'inside') decorationParams = {type: 'highlight', class: 'test-highlight'} decoration = editor.decorateMarker(marker, decorationParams) runSetImmediateCallbacks() it "does not render highlights for off-screen lines until they come on-screen", -> - node.style.height = 2.5 * lineHeightInPixels + 'px' - component.measureScrollView() + wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], invalidate: 'inside') @@ -986,7 +985,7 @@ describe "EditorComponent", -> # Should not be rendering range containing the marker expect(component.getRenderedRowRange()[1]).toBeLessThan 9 - regions = node.querySelectorAll('.some-highlight .region') + regions = componentNode.querySelectorAll('.some-highlight .region') # Nothing when outside the rendered row range expect(regions.length).toBe 0 @@ -994,7 +993,7 @@ describe "EditorComponent", -> verticalScrollbarNode.scrollTop = 3.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - regions = node.querySelectorAll('.some-highlight .region') + regions = componentNode.querySelectorAll('.some-highlight .region') expect(regions.length).toBe 1 regionRect = regions[0].style @@ -1004,24 +1003,24 @@ describe "EditorComponent", -> expect(regionRect.width).toBe 2 * charWidth + 'px' it "renders highlights decoration's marker is added", -> - regions = node.querySelectorAll('.test-highlight .region') + regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe 2 it "removes highlights when a decoration is removed", -> decoration.destroy() runSetImmediateCallbacks() - regions = node.querySelectorAll('.test-highlight .region') + regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe 0 it "does not render a highlight that is within a fold", -> editor.foldBufferRow(1) runSetImmediateCallbacks() - expect(node.querySelectorAll('.test-highlight').length).toBe 0 + expect(componentNode.querySelectorAll('.test-highlight').length).toBe 0 it "removes highlights when a decoration's marker is destroyed", -> marker.destroy() runSetImmediateCallbacks() - regions = node.querySelectorAll('.test-highlight .region') + regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe 0 it "only renders highlights when a decoration's marker is valid", -> @@ -1029,20 +1028,20 @@ describe "EditorComponent", -> runSetImmediateCallbacks() expect(marker.isValid()).toBe false - regions = node.querySelectorAll('.test-highlight .region') + regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe 0 editor.getBuffer().undo() runSetImmediateCallbacks() expect(marker.isValid()).toBe true - regions = node.querySelectorAll('.test-highlight .region') + regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe 2 describe "when flashing a decoration via Decoration::flash()", -> highlightNode = null beforeEach -> - highlightNode = node.querySelector('.test-highlight') + highlightNode = componentNode.querySelector('.test-highlight') it "adds and removes the flash class specified in ::flash", -> expect(highlightNode.classList.contains('flash-class')).toBe false @@ -1074,46 +1073,46 @@ describe "EditorComponent", -> describe "when a decoration's marker moves", -> it "moves rendered highlights when the buffer is changed", -> - regionStyle = node.querySelector('.test-highlight .region').style + regionStyle = componentNode.querySelector('.test-highlight .region').style originalTop = parseInt(regionStyle.top) editor.getBuffer().insert([0, 0], '\n') runSetImmediateCallbacks() - regionStyle = node.querySelector('.test-highlight .region').style + regionStyle = componentNode.querySelector('.test-highlight .region').style newTop = parseInt(regionStyle.top) expect(newTop).toBe originalTop + lineHeightInPixels it "moves rendered highlights when the marker is manually moved", -> - regionStyle = node.querySelector('.test-highlight .region').style + regionStyle = componentNode.querySelector('.test-highlight .region').style expect(parseInt(regionStyle.top)).toBe 2 * lineHeightInPixels marker.setBufferRange([[5, 8], [5, 13]]) runSetImmediateCallbacks() - regionStyle = node.querySelector('.test-highlight .region').style + regionStyle = componentNode.querySelector('.test-highlight .region').style expect(parseInt(regionStyle.top)).toBe 5 * lineHeightInPixels describe "when a decoration is updated via Decoration::update", -> it "renders the decoration's new params", -> - expect(node.querySelector('.test-highlight')).toBeTruthy() + expect(componentNode.querySelector('.test-highlight')).toBeTruthy() decoration.update(type: 'highlight', class: 'new-test-highlight') runSetImmediateCallbacks() - expect(node.querySelector('.test-highlight')).toBeFalsy() - expect(node.querySelector('.new-test-highlight')).toBeTruthy() + expect(componentNode.querySelector('.test-highlight')).toBeFalsy() + expect(componentNode.querySelector('.new-test-highlight')).toBeTruthy() describe "hidden input field", -> it "renders the hidden input field at the position of the last cursor if the cursor is on screen and the editor is focused", -> editor.setVerticalScrollMargin(0) editor.setHorizontalScrollMargin(0) - inputNode = node.querySelector('.hidden-input') - node.style.height = 5 * lineHeightInPixels + 'px' - node.style.width = 10 * charWidth + 'px' - component.measureScrollView() + inputNode = componentNode.querySelector('.hidden-input') + wrapperNode.style.height = 5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(editor.getCursorScreenPosition()).toEqual [0, 0] @@ -1156,14 +1155,14 @@ describe "EditorComponent", -> beforeEach -> delayAnimationFrames = true - linesNode = node.querySelector('.lines') + linesNode = componentNode.querySelector('.lines') describe "when a non-folded line is single-clicked", -> describe "when no modifier keys are held down", -> it "moves the cursor to the nearest screen position", -> - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = 10 * charWidth + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureHeightAndWidth() editor.setScrollTop(3.5 * lineHeightInPixels) editor.setScrollLeft(2 * charWidth) runSetImmediateCallbacks() @@ -1288,7 +1287,7 @@ describe "EditorComponent", -> gutterNode = null beforeEach -> - gutterNode = node.querySelector('.gutter') + gutterNode = componentNode.querySelector('.gutter') describe "when the gutter is clicked", -> it "moves the cursor to the beginning of the clicked row", -> @@ -1383,20 +1382,20 @@ describe "EditorComponent", -> inputNode = null beforeEach -> - inputNode = node.querySelector('.hidden-input') + inputNode = componentNode.querySelector('.hidden-input') it "transfers focus to the hidden input", -> expect(document.activeElement).toBe document.body - node.focus() + componentNode.focus() expect(document.activeElement).toBe inputNode it "adds the 'is-focused' class to the editor when the hidden input is focused", -> expect(document.activeElement).toBe document.body inputNode.focus() - expect(node.classList.contains('is-focused')).toBe true + expect(componentNode.classList.contains('is-focused')).toBe true expect(wrapperView.hasClass('is-focused')).toBe true inputNode.blur() - expect(node.classList.contains('is-focused')).toBe false + expect(componentNode.classList.contains('is-focused')).toBe false expect(wrapperView.hasClass('is-focused')).toBe false describe "selection handling", -> @@ -1408,20 +1407,20 @@ describe "EditorComponent", -> runSetImmediateCallbacks() it "adds the 'has-selection' class to the editor when there is a selection", -> - expect(node.classList.contains('has-selection')).toBe false + expect(componentNode.classList.contains('has-selection')).toBe false editor.selectDown() runSetImmediateCallbacks() - expect(node.classList.contains('has-selection')).toBe true + expect(componentNode.classList.contains('has-selection')).toBe true cursor.moveDown() runSetImmediateCallbacks() - expect(node.classList.contains('has-selection')).toBe false + expect(componentNode.classList.contains('has-selection')).toBe false describe "scrolling", -> it "updates the vertical scrollbar when the scrollTop is changed in the model", -> - node.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(verticalScrollbarNode.scrollTop).toBe 0 @@ -1431,11 +1430,11 @@ describe "EditorComponent", -> expect(verticalScrollbarNode.scrollTop).toBe 10 it "updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model", -> - node.style.width = 30 * charWidth + 'px' - component.measureScrollView() + componentNode.style.width = 30 * charWidth + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() - linesNode = node.querySelector('.lines') + linesNode = componentNode.querySelector('.lines') expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" expect(horizontalScrollbarNode.scrollLeft).toBe 0 @@ -1445,8 +1444,8 @@ describe "EditorComponent", -> expect(horizontalScrollbarNode.scrollLeft).toBe 100 it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> - node.style.width = 30 * charWidth + 'px' - component.measureScrollView() + componentNode.style.width = 30 * charWidth + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(editor.getScrollLeft()).toBe 0 @@ -1456,9 +1455,9 @@ describe "EditorComponent", -> expect(editor.getScrollLeft()).toBe 100 it "does not obscure the last line with the horizontal scrollbar", -> - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = 10 * charWidth + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureHeightAndWidth() editor.setScrollBottom(editor.getScrollHeight()) runSetImmediateCallbacks() lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) @@ -1467,17 +1466,17 @@ describe "EditorComponent", -> expect(bottomOfLastLine).toBe topOfHorizontalScrollbar # Scroll so there's no space below the last line when the horizontal scrollbar disappears - node.style.width = 100 * charWidth + 'px' - component.measureScrollView() + wrapperNode.style.width = 100 * charWidth + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom - bottomOfEditor = node.getBoundingClientRect().bottom + bottomOfEditor = componentNode.getBoundingClientRect().bottom expect(bottomOfLastLine).toBe bottomOfEditor it "does not obscure the last character of the longest line with the vertical scrollbar", -> - node.style.height = 7 * lineHeightInPixels + 'px' - node.style.width = 10 * charWidth + 'px' - component.measureScrollView() + wrapperNode.style.height = 7 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureHeightAndWidth() editor.setScrollLeft(Infinity) runSetImmediateCallbacks() @@ -1489,32 +1488,32 @@ describe "EditorComponent", -> expect(verticalScrollbarNode.style.display).toBe 'none' expect(horizontalScrollbarNode.style.display).toBe 'none' - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = '1000px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = '1000px' + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.display).toBe '' expect(horizontalScrollbarNode.style.display).toBe 'none' - node.style.width = 10 * charWidth + 'px' - component.measureScrollView() + componentNode.style.width = 10 * charWidth + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.display).toBe '' expect(horizontalScrollbarNode.style.display).toBe '' - node.style.height = 20 * lineHeightInPixels + 'px' - component.measureScrollView() + wrapperNode.style.height = 20 * lineHeightInPixels + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() 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", -> - node.style.height = 4 * lineHeightInPixels + 'px' - node.style.width = 10 * charWidth + 'px' - component.measureScrollView() + wrapperNode.style.height = 4 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() atom.themes.applyStylesheet "test", """ @@ -1524,44 +1523,46 @@ describe "EditorComponent", -> } """ - scrollbarCornerNode = node.querySelector('.scrollbar-corner') + 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", -> - scrollbarCornerNode = node.querySelector('.scrollbar-corner') + scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') expect(verticalScrollbarNode.style.bottom).toBe '' expect(horizontalScrollbarNode.style.right).toBe '' - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = '1000px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = '1000px' + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.bottom).toBe '' expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' expect(scrollbarCornerNode.style.display).toBe 'none' - node.style.width = 10 * charWidth + 'px' - component.measureScrollView() + componentNode.style.width = 10 * charWidth + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' expect(scrollbarCornerNode.style.display).toBe '' - node.style.height = 20 * lineHeightInPixels + 'px' - component.measureScrollView() + wrapperNode.style.height = 20 * lineHeightInPixels + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' expect(horizontalScrollbarNode.style.right).toBe '' expect(scrollbarCornerNode.style.display).toBe 'none' it "accounts for the width of the gutter in the scrollWidth of the horizontal scrollbar", -> - gutterNode = node.querySelector('.gutter') - node.style.width = 10 * charWidth + 'px' - component.measureScrollView() + gutterNode = componentNode.querySelector('.gutter') + componentNode.style.width = 10 * charWidth + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(horizontalScrollbarNode.scrollWidth).toBe gutterNode.offsetWidth + editor.getScrollWidth() @@ -1572,64 +1573,72 @@ describe "EditorComponent", -> describe "updating scrollTop and scrollLeft", -> beforeEach -> - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = 20 * charWidth + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + component.measureHeightAndWidth() runSetImmediateCallbacks() it "updates the scrollLeft or scrollTop on mousewheel events depending on which delta is greater (x or y)", -> expect(verticalScrollbarNode.scrollTop).toBe 0 expect(horizontalScrollbarNode.scrollLeft).toBe 0 - node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10)) + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10)) + runSetImmediateCallbacks() expect(verticalScrollbarNode.scrollTop).toBe 10 expect(horizontalScrollbarNode.scrollLeft).toBe 0 - node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) + runSetImmediateCallbacks() expect(verticalScrollbarNode.scrollTop).toBe 10 expect(horizontalScrollbarNode.scrollLeft).toBe 15 it "updates the scrollLeft or scrollTop according to the scroll sensitivity", -> atom.config.set('editor.scrollSensitivity', 50) - node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10)) + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10)) + runSetImmediateCallbacks() expect(horizontalScrollbarNode.scrollLeft).toBe 0 - node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) + runSetImmediateCallbacks() expect(verticalScrollbarNode.scrollTop).toBe 5 expect(horizontalScrollbarNode.scrollLeft).toBe 7 it "uses the previous scrollSensitivity when the value is not an int", -> atom.config.set('editor.scrollSensitivity', 'nope') - node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -10)) + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -10)) + runSetImmediateCallbacks() expect(verticalScrollbarNode.scrollTop).toBe 10 it "parses negative scrollSensitivity values as positive", -> atom.config.set('editor.scrollSensitivity', -50) - node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -10)) + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -10)) + runSetImmediateCallbacks() expect(verticalScrollbarNode.scrollTop).toBe 5 describe "when the mousewheel event's target is a line", -> it "keeps the line on the DOM if it is scrolled off-screen", -> - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = 20 * charWidth + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + component.measureHeightAndWidth() - lineNode = node.querySelector('.line') + lineNode = componentNode.querySelector('.line') wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) Object.defineProperty(wheelEvent, 'target', get: -> lineNode) - node.dispatchEvent(wheelEvent) + componentNode.dispatchEvent(wheelEvent) + runSetImmediateCallbacks() - expect(node.contains(lineNode)).toBe true + expect(componentNode.contains(lineNode)).toBe true it "does not set the mouseWheelScreenRow if scrolling horizontally", -> - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = 20 * charWidth + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + component.measureHeightAndWidth() - lineNode = node.querySelector('.line') + lineNode = componentNode.querySelector('.line') wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 10, wheelDeltaY: 0) Object.defineProperty(wheelEvent, 'target', get: -> lineNode) - node.dispatchEvent(wheelEvent) + componentNode.dispatchEvent(wheelEvent) + runSetImmediateCallbacks() expect(component.mouseWheelScreenRow).toBe null @@ -1638,10 +1647,11 @@ describe "EditorComponent", -> expect(editor.getScrollTop()).toBe 0 - lineNode = node.querySelector('.line') + lineNode = componentNode.querySelector('.line') wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 10) Object.defineProperty(wheelEvent, 'target', get: -> lineNode) - node.dispatchEvent(wheelEvent) + componentNode.dispatchEvent(wheelEvent) + runSetImmediateCallbacks() expect(editor.getScrollTop()).toBe 0 @@ -1650,38 +1660,76 @@ describe "EditorComponent", -> expect(component.mouseWheelScreenRow).toBe null it "does not preserve the line if it is on screen", -> - expect(node.querySelectorAll('.line-number').length).toBe 14 # dummy line - lineNodes = node.querySelectorAll('.line') + expect(componentNode.querySelectorAll('.line-number').length).toBe 14 # dummy line + lineNodes = componentNode.querySelectorAll('.line') expect(lineNodes.length).toBe 13 lineNode = lineNodes[0] wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 100) # goes nowhere, we're already at scrollTop 0 Object.defineProperty(wheelEvent, 'target', get: -> lineNode) - node.dispatchEvent(wheelEvent) + componentNode.dispatchEvent(wheelEvent) + runSetImmediateCallbacks() expect(component.mouseWheelScreenRow).toBe 0 editor.insertText("hello") - expect(node.querySelectorAll('.line-number').length).toBe 14 # dummy line - expect(node.querySelectorAll('.line').length).toBe 13 + expect(componentNode.querySelectorAll('.line-number').length).toBe 14 # dummy line + expect(componentNode.querySelectorAll('.line').length).toBe 13 describe "when the mousewheel event's target is a line number", -> it "keeps the line number on the DOM if it is scrolled off-screen", -> - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = 20 * charWidth + 'px' - component.measureScrollView() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + component.measureHeightAndWidth() - lineNumberNode = node.querySelectorAll('.line-number')[1] + lineNumberNode = componentNode.querySelectorAll('.line-number')[1] wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) Object.defineProperty(wheelEvent, 'target', get: -> lineNumberNode) - node.dispatchEvent(wheelEvent) + componentNode.dispatchEvent(wheelEvent) + runSetImmediateCallbacks() - expect(node.contains(lineNumberNode)).toBe true + expect(componentNode.contains(lineNumberNode)).toBe true + + it "only prevents the default action of the mousewheel event if it actually lead to scrolling", -> + spyOn(WheelEvent::, 'preventDefault').andCallThrough() + + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + component.measureHeightAndWidth() + runSetImmediateCallbacks() + + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 50)) + expect(editor.getScrollTop()).toBe 0 + expect(WheelEvent::preventDefault).not.toHaveBeenCalled() + + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -3000)) + runSetImmediateCallbacks() + expect(editor.getScrollTop()).toBe editor.getScrollHeight() - editor.getHeight() + 15 + expect(WheelEvent::preventDefault).toHaveBeenCalled() + WheelEvent::preventDefault.reset() + + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -30)) + expect(editor.getScrollTop()).toBe editor.getScrollHeight() - editor.getHeight() + 15 + expect(WheelEvent::preventDefault).not.toHaveBeenCalled() + + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 50, wheelDeltaY: 0)) + expect(editor.getScrollLeft()).toBe 0 + expect(WheelEvent::preventDefault).not.toHaveBeenCalled() + + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -3000, wheelDeltaY: 0)) + runSetImmediateCallbacks() + expect(editor.getScrollLeft()).toBe editor.getScrollWidth() - editor.getWidth() + 15 + expect(WheelEvent::preventDefault).toHaveBeenCalled() + WheelEvent::preventDefault.reset() + + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -30, wheelDeltaY: 0)) + expect(editor.getScrollLeft()).toBe editor.getScrollWidth() - editor.getWidth() + 15 + expect(WheelEvent::preventDefault).not.toHaveBeenCalled() describe "input events", -> inputNode = null beforeEach -> - inputNode = node.querySelector('.hidden-input') + inputNode = componentNode.querySelector('.hidden-input') buildTextInputEvent = ({data, target}) -> event = new Event('textInput') @@ -1690,28 +1738,28 @@ describe "EditorComponent", -> event it "inserts the newest character in the input's value into the buffer", -> - node.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) + componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) runSetImmediateCallbacks() expect(editor.lineForBufferRow(0)).toBe 'xvar quicksort = function () {' - node.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode)) + componentNode.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode)) runSetImmediateCallbacks() expect(editor.lineForBufferRow(0)).toBe 'xyvar quicksort = function () {' it "replaces the last character if the length of the input's value doesn't increase, as occurs with the accented character menu", -> - node.dispatchEvent(buildTextInputEvent(data: 'u', target: inputNode)) + componentNode.dispatchEvent(buildTextInputEvent(data: 'u', target: inputNode)) runSetImmediateCallbacks() expect(editor.lineForBufferRow(0)).toBe 'uvar quicksort = function () {' # simulate the accented character suggestion's selection of the previous character inputNode.setSelectionRange(0, 1) - node.dispatchEvent(buildTextInputEvent(data: 'ü', target: inputNode)) + componentNode.dispatchEvent(buildTextInputEvent(data: 'ü', target: inputNode)) runSetImmediateCallbacks() expect(editor.lineForBufferRow(0)).toBe 'üvar quicksort = function () {' it "does not handle input events when input is disabled", -> component.setInputEnabled(false) - node.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) + componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) runSetImmediateCallbacks() expect(editor.lineForBufferRow(0)).toBe 'var quicksort = function () {' @@ -1725,51 +1773,51 @@ describe "EditorComponent", -> event beforeEach -> - inputNode = inputNode = node.querySelector('.hidden-input') + inputNode = inputNode = componentNode.querySelector('.hidden-input') describe "when nothing is selected", -> it "inserts the chosen completion", -> - node.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) - node.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe 'svar quicksort = function () {' - node.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe 'sdvar quicksort = function () {' - node.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) - node.dispatchEvent(buildTextInputEvent(data: '速度', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) + componentNode.dispatchEvent(buildTextInputEvent(data: '速度', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe '速度var quicksort = function () {' it "reverts back to the original text when the completion helper is dismissed", -> - node.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) - node.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe 'svar quicksort = function () {' - node.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe 'sdvar quicksort = function () {' - node.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe 'var quicksort = function () {' it "allows multiple accented character to be inserted with the ' on a US international layout", -> inputNode.value = "'" inputNode.setSelectionRange(0, 1) - node.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) - node.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: "'", target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: "'", target: inputNode)) expect(editor.lineForBufferRow(0)).toBe "'var quicksort = function () {" - node.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) - node.dispatchEvent(buildTextInputEvent(data: 'á', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) + componentNode.dispatchEvent(buildTextInputEvent(data: 'á', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe "ávar quicksort = function () {" inputNode.value = "'" inputNode.setSelectionRange(0, 1) - node.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) - node.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: "'", target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: "'", target: inputNode)) expect(editor.lineForBufferRow(0)).toBe "á'var quicksort = function () {" - node.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) - node.dispatchEvent(buildTextInputEvent(data: 'á', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) + componentNode.dispatchEvent(buildTextInputEvent(data: 'á', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe "áávar quicksort = function () {" describe "when a string is selected", -> @@ -1777,26 +1825,26 @@ describe "EditorComponent", -> editor.setSelectedBufferRange [[0, 4], [0, 9]] # select 'quick' it "inserts the chosen completion", -> - node.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) - node.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe 'var ssort = function () {' - node.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe 'var sdsort = function () {' - node.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) - node.dispatchEvent(buildTextInputEvent(data: '速度', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) + componentNode.dispatchEvent(buildTextInputEvent(data: '速度', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe 'var 速度sort = function () {' it "reverts back to the original text when the completion helper is dismissed", -> - node.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) - node.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe 'var ssort = function () {' - node.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe 'var sdsort = function () {' - node.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) expect(editor.lineForBufferRow(0)).toBe 'var quicksort = function () {' describe "commands", -> @@ -1806,12 +1854,34 @@ describe "EditorComponent", -> event = new CustomEvent('editor:consolidate-selections', bubbles: true, cancelable: true) event.abortKeyBinding = jasmine.createSpy("event.abortKeyBinding") - node.dispatchEvent(event) + componentNode.dispatchEvent(event) expect(editor.consolidateSelections).toHaveBeenCalled() expect(event.abortKeyBinding).toHaveBeenCalled() describe "hiding and showing the editor", -> + describe "when the editor is hidden when it is mounted", -> + it "defers measurement and rendering until the editor becomes visible", -> + wrapperView.remove() + + hiddenParent = document.createElement('div') + hiddenParent.style.display = 'none' + contentNode.appendChild(hiddenParent) + + wrapperView = new ReactEditorView(editor, {lineOverdrawMargin}) + wrapperNode = wrapperView.element + wrapperView.appendTo(hiddenParent) + + {component} = wrapperView + componentNode = component.getDOMNode() + expect(componentNode.querySelectorAll('.line').length).toBe 0 + + hiddenParent.style.display = 'block' + advanceClock(component.domPollingInterval) + runSetImmediateCallbacks() + + expect(componentNode.querySelectorAll('.line').length).toBeGreaterThan 0 + describe "when the lineHeight changes while the editor is hidden", -> it "does not attempt to measure the lineHeightInPixels until the editor becomes visible again", -> wrapperView.hide() @@ -1849,8 +1919,8 @@ describe "EditorComponent", -> editor.setCursorBufferPosition([0, Infinity]) runSetImmediateCallbacks() - cursorLeft = node.querySelector('.cursor').getBoundingClientRect().left - line0Right = node.querySelector('.line > span:last-child').getBoundingClientRect().right + cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left + line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right expect(cursorLeft).toBe line0Right describe "when the fontFamily changes while the editor is hidden", -> @@ -1876,8 +1946,8 @@ describe "EditorComponent", -> editor.setCursorBufferPosition([0, Infinity]) runSetImmediateCallbacks() - cursorLeft = node.querySelector('.cursor').getBoundingClientRect().left - line0Right = node.querySelector('.line > span:last-child').getBoundingClientRect().right + cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left + line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right expect(cursorLeft).toBe line0Right describe "when stylesheets change while the editor is hidden", -> @@ -1899,8 +1969,8 @@ describe "EditorComponent", -> editor.setCursorBufferPosition([0, Infinity]) runSetImmediateCallbacks() - cursorLeft = node.querySelector('.cursor').getBoundingClientRect().left - line0Right = node.querySelector('.line > span:last-child').getBoundingClientRect().right + cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left + line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right expect(cursorLeft).toBe line0Right describe "when lines are changed while the editor is hidden", -> @@ -1911,7 +1981,7 @@ describe "EditorComponent", -> editor.setCursorBufferPosition([0, Infinity]) runSetImmediateCallbacks() wrapperView.show() - expect(node.querySelector('.cursor').style['-webkit-transform']).toBe "translate3d(#{9 * charWidth}px, 0px, 0px)" + expect(componentNode.querySelector('.cursor').style['-webkit-transform']).toBe "translate3d(#{9 * charWidth}px, 0px, 0px)" describe "soft wrapping", -> beforeEach -> @@ -1919,25 +1989,25 @@ describe "EditorComponent", -> it "updates the wrap location when the editor is resized", -> newHeight = 4 * editor.getLineHeightInPixels() + "px" - expect(newHeight).toBeLessThan node.style.height - node.style.height = newHeight + expect(parseInt(newHeight)).toBeLessThan wrapperNode.offsetHeight + wrapperNode.style.height = newHeight - advanceClock(component.scrollViewMeasurementInterval) + advanceClock(component.domPollingInterval) runSetImmediateCallbacks() - expect(node.querySelectorAll('.line')).toHaveLength(4 + lineOverdrawMargin + 1) + expect(componentNode.querySelectorAll('.line')).toHaveLength(4 + lineOverdrawMargin + 1) - gutterWidth = node.querySelector('.gutter').offsetWidth - node.style.width = gutterWidth + 14 * charWidth + 'px' - advanceClock(component.scrollViewMeasurementInterval) + gutterWidth = componentNode.querySelector('.gutter').offsetWidth + componentNode.style.width = gutterWidth + 14 * charWidth + 'px' + advanceClock(component.domPollingInterval) runSetImmediateCallbacks() - expect(node.querySelector('.line').textContent).toBe "var quicksort " + expect(componentNode.querySelector('.line').textContent).toBe "var quicksort " it "accounts for the scroll view's padding when determining the wrap location", -> - scrollViewNode = node.querySelector('.scroll-view') + scrollViewNode = componentNode.querySelector('.scroll-view') scrollViewNode.style.paddingLeft = 20 + 'px' - node.style.width = 30 * charWidth + 'px' + componentNode.style.width = 30 * charWidth + 'px' - advanceClock(component.scrollViewMeasurementInterval) + advanceClock(component.domPollingInterval) runSetImmediateCallbacks() expect(component.lineNodeForScreenRow(0).textContent).toBe "var quicksort = " @@ -1990,6 +2060,50 @@ describe "EditorComponent", -> runSetImmediateCallbacks() expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe false + describe "height", -> + describe "when the wrapper view has an explicit height", -> + it "does not assign a height on the component node", -> + wrapperNode.style.height = '200px' + component.measureHeightAndWidth() + expect(componentNode.style.height).toBe '' + + describe "when the wrapper view does not have an explicit height", -> + it "assigns a height on the component node based on the editor's content", -> + expect(wrapperNode.style.height).toBe '' + expect(componentNode.style.height).toBe editor.getScreenLineCount() * lineHeightInPixels + 'px' + + describe "when the 'mini' property is true", -> + beforeEach -> + component.setProps(mini: true) + + it "does not render the gutter", -> + expect(componentNode.querySelector('.gutter')).toBeNull() + + it "adds the 'mini' class to the wrapper view", -> + expect(wrapperNode.classList.contains('mini')).toBe true + + it "does not render invisible characters", -> + component.setInvisibles(eol: 'E') + component.setShowInvisibles(true) + expect(component.lineNodeForScreenRow(0).textContent).toBe 'var quicksort = function () {' + + it "does not assign an explicit line-height on the editor contents", -> + expect(componentNode.style.lineHeight).toBe '' + + it "does not apply cursor-line decorations", -> + expect(component.lineNodeForScreenRow(0).classList.contains('cursor-line')).toBe false + + describe "when placholderText is specified", -> + it "renders the placeholder text when the buffer is empty", -> + component.setProps(placeholderText: 'Hello World') + expect(componentNode.querySelector('.placeholder-text')).toBeNull() + editor.setText('') + runSetImmediateCallbacks() + expect(componentNode.querySelector('.placeholder-text').textContent).toBe "Hello World" + editor.setText('hey') + runSetImmediateCallbacks() + expect(componentNode.querySelector('.placeholder-text')).toBeNull() + describe "legacy editor compatibility", -> it "triggers the screen-lines-changed event before the editor:display-update event", -> editor.setSoftWrap(true) @@ -2014,14 +2128,14 @@ describe "EditorComponent", -> clientCoordinatesForScreenPosition = (screenPosition) -> positionOffset = editor.pixelPositionForScreenPosition(screenPosition) - scrollViewClientRect = node.querySelector('.scroll-view').getBoundingClientRect() + scrollViewClientRect = componentNode.querySelector('.scroll-view').getBoundingClientRect() clientX = scrollViewClientRect.left + positionOffset.left - editor.getScrollLeft() clientY = scrollViewClientRect.top + positionOffset.top - editor.getScrollTop() {clientX, clientY} clientCoordinatesForScreenRowInGutter = (screenRow) -> positionOffset = editor.pixelPositionForScreenPosition([screenRow, 1]) - gutterClientRect = node.querySelector('.gutter').getBoundingClientRect() + gutterClientRect = componentNode.querySelector('.gutter').getBoundingClientRect() clientX = gutterClientRect.left + positionOffset.left - editor.getScrollLeft() clientY = gutterClientRect.top + positionOffset.top - editor.getScrollTop() {clientX, clientY} diff --git a/spec/jasmine-helper.coffee b/spec/jasmine-helper.coffee index 87765634c..3de6d065f 100644 --- a/spec/jasmine-helper.coffee +++ b/spec/jasmine-helper.coffee @@ -1,7 +1,8 @@ fs = require 'fs' module.exports.runSpecSuite = (specSuite, logFile, logErrors=true) -> - {$, $$} = require 'atom' + {$, $$} = require '../src/space-pen-extensions' + window[key] = value for key, value of require '../vendor/jasmine' {TerminalReporter} = require 'jasmine-tagged' diff --git a/spec/spec-suite.coffee b/spec/spec-suite.coffee index e0adaf24b..817de7986 100644 --- a/spec/spec-suite.coffee +++ b/spec/spec-suite.coffee @@ -1,6 +1,5 @@ _ = require 'underscore-plus' fs = require 'fs-plus' -{Git} = require 'atom' path = require 'path' require './spec-helper' diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index b33e231e5..503a8fe9b 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -12,14 +12,14 @@ CursorsComponent = React.createClass cursorBlinkIntervalHandle: null render: -> - {cursorPixelRects, scrollTop, scrollLeft, defaultCharWidth, useHardwareAcceleration} = @props + {performedInitialMeasurement, cursorPixelRects, scrollTop, scrollLeft, defaultCharWidth, useHardwareAcceleration} = @props {blinkOff} = @state className = 'cursors' className += ' blink-off' if blinkOff div {className}, - if @isMounted() + if performedInitialMeasurement for key, pixelRect of cursorPixelRects CursorComponent({key, pixelRect, scrollTop, scrollLeft, defaultCharWidth, useHardwareAcceleration}) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 1d1ce4318..fa3939cb6 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -22,6 +22,8 @@ EditorComponent = React.createClass statics: performSyncUpdates: false + visible: false + autoHeight: false pendingScrollTop: null pendingScrollLeft: null selectOnMouseMove: false @@ -35,27 +37,28 @@ EditorComponent = React.createClass gutterWidth: 0 refreshingScrollbars: false measuringScrollbars: true - pendingVerticalScrollDelta: 0 - pendingHorizontalScrollDelta: 0 mouseWheelScreenRow: null mouseWheelScreenRowClearDelay: 150 scrollSensitivity: 0.4 - scrollViewMeasurementRequested: false + heightAndWidthMeasurementRequested: false measureLineHeightAndDefaultCharWidthWhenShown: false remeasureCharacterWidthsIfVisibleAfterNextUpdate: false inputEnabled: true - scrollViewMeasurementInterval: 100 scopedCharacterWidthsChangeCount: null - scrollViewMeasurementPaused: false + domPollingInterval: 100 + domPollingIntervalId: null + domPollingPaused: false render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide, showInvisibles, showLineNumbers, visible} = @state - {editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props + {editor, mini, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props maxLineNumberDigits = editor.getLineCount().toString().length - invisibles = if showInvisibles then @state.invisibles else {} + invisibles = if showInvisibles and not mini then @state.invisibles else {} hasSelection = editor.getSelection()? and !editor.getSelection().isEmpty() + style = {fontSize, fontFamily} + style.lineHeight = lineHeight unless mini - if @isMounted() + if @performedInitialMeasurement renderedRowRange = @getRenderedRowRange() [renderedStartRow, renderedEndRow] = renderedRowRange cursorPixelRects = @getCursorPixelRects(renderedRowRange) @@ -63,6 +66,7 @@ EditorComponent = React.createClass decorations = editor.decorationsForScreenRowRange(renderedStartRow, renderedEndRow) highlightDecorations = @getHighlightDecorations(decorations) lineDecorations = @getLineDecorations(decorations) + placeholderText = @props.placeholderText if @props.placeholderText? and editor.isEmpty() scrollHeight = editor.getScrollHeight() scrollWidth = editor.getScrollWidth() @@ -81,12 +85,14 @@ EditorComponent = React.createClass if @mouseWheelScreenRow? and not (renderedStartRow <= @mouseWheelScreenRow < renderedEndRow) mouseWheelScreenRow = @mouseWheelScreenRow - className = 'editor-contents editor-colors' + style.height = scrollViewHeight if @autoHeight + + className = 'editor-contents' className += ' is-focused' if focused className += ' has-selection' if hasSelection - div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, - if showLineNumbers + div {className, style, tabIndex: -1}, + if not mini and showLineNumbers GutterComponent { ref: 'gutter', onMouseDown: @onGutterMouseDown, onWidthChanged: @onGutterWidthChanged, lineDecorations, defaultCharWidth, editor, renderedRowRange, maxLineNumberDigits, scrollViewHeight, @@ -103,14 +109,16 @@ EditorComponent = React.createClass CursorsComponent { scrollTop, scrollLeft, cursorPixelRects, cursorBlinkPeriod, cursorBlinkResumeDelay, - lineHeightInPixels, defaultCharWidth, @scopedCharacterWidthsChangeCount, @useHardwareAcceleration + lineHeightInPixels, defaultCharWidth, @scopedCharacterWidthsChangeCount, @useHardwareAcceleration, + @performedInitialMeasurement } LinesComponent { ref: 'lines', editor, lineHeightInPixels, defaultCharWidth, lineDecorations, highlightDecorations, showIndentGuide, renderedRowRange, @pendingChanges, scrollTop, scrollLeft, @scrollingVertically, scrollHeight, scrollWidth, mouseWheelScreenRow, invisibles, - visible, scrollViewHeight, @scopedCharacterWidthsChangeCount, lineWidth, @useHardwareAcceleration + visible, scrollViewHeight, @scopedCharacterWidthsChangeCount, lineWidth, @useHardwareAcceleration, + placeholderText, @performedInitialMeasurement } ScrollbarComponent @@ -149,8 +157,7 @@ EditorComponent = React.createClass {editor} = @props Math.max(1, Math.ceil(editor.getHeight() / editor.getLineHeightInPixels())) - getInitialState: -> - visible: true + getInitialState: -> {} getDefaultProps: -> cursorBlinkPeriod: 800 @@ -166,7 +173,7 @@ EditorComponent = React.createClass componentDidMount: -> {editor} = @props - @scrollViewMeasurementIntervalId = setInterval(@measureScrollView, @scrollViewMeasurementInterval) + @domPollingIntervalId = setInterval(@pollDOM, @domPollingInterval) @observeEditor() @listenForDOMEvents() @@ -175,17 +182,14 @@ EditorComponent = React.createClass @subscribe atom.themes, 'stylesheet-added stylsheet-removed', @onStylesheetsChanged @subscribe scrollbarStyle.changes, @refreshScrollbars - editor.setVisible(true) - - @measureLineHeightAndDefaultCharWidth() - @measureScrollView() - @measureScrollbars() + if @visible = @isVisible() + @performInitialMeasurement() componentWillUnmount: -> @props.parentView.trigger 'editor:will-be-removed', [@props.parentView] @unsubscribe() - clearInterval(@scrollViewMeasurementIntervalId) - @scrollViewMeasurementIntervalId = null + clearInterval(@domPollingIntervalId) + @domPollingIntervalId = null componentDidUpdate: (prevProps, prevState) -> cursorsMoved = @cursorsMoved @@ -197,13 +201,26 @@ EditorComponent = React.createClass if @props.editor.isAlive() @updateParentViewFocusedClassIfNeeded(prevState) + @updateParentViewMiniClassIfNeeded(prevState) @props.parentView.trigger 'cursor:moved' if cursorsMoved @props.parentView.trigger 'selection:changed' if selectionChanged @props.parentView.trigger 'editor:display-updated' - @measureScrollbars() if @measuringScrollbars - @measureLineHeightAndCharWidthsIfNeeded(prevState) - @remeasureCharacterWidthsIfNeeded(prevState) + @visible = @isVisible() + if @performedInitialMeasurement + @measureScrollbars() if @measuringScrollbars + @measureLineHeightAndDefaultCharWidthIfNeeded(prevState) + @remeasureCharacterWidthsIfNeeded(prevState) + + performInitialMeasurement: -> + @updatesPaused = true + @measureLineHeightAndDefaultCharWidth() + @measureHeightAndWidth() + @measureScrollbars() + @props.editor.setVisible(true) + @updatesPaused = false + @performedInitialMeasurement = true + @requestUpdate() requestUpdate: -> if @updatesPaused @@ -220,7 +237,7 @@ EditorComponent = React.createClass requestAnimationFrame: (fn) -> @updatesPaused = true - @pauseScrollViewMeasurement() + @pauseDOMPolling() requestAnimationFrame => fn() @updatesPaused = false @@ -273,7 +290,9 @@ EditorComponent = React.createClass cursorPixelRects getLineDecorations: (decorationsByMarkerId) -> - {editor} = @props + {editor, mini} = @props + return {} if mini + decorationsByScreenRow = {} for markerId, decorations of decorationsByMarkerId marker = editor.getMarker(markerId) @@ -350,7 +369,7 @@ EditorComponent = React.createClass scrollViewNode = @refs.scrollView.getDOMNode() scrollViewNode.addEventListener 'scroll', @onScrollViewScroll - window.addEventListener 'resize', @requestScrollViewMeasurement + window.addEventListener 'resize', @requestHeightAndWidthMeasurement @listenForIMEEvents() @@ -557,28 +576,23 @@ EditorComponent = React.createClass @pendingScrollLeft = null onMouseWheel: (event) -> - event.preventDefault() - animationFramePending = @pendingHorizontalScrollDelta isnt 0 or @pendingVerticalScrollDelta isnt 0 + {editor} = @props # Only scroll in one direction at a time {wheelDeltaX, wheelDeltaY} = event if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY) # Scrolling horizontally - @pendingHorizontalScrollDelta -= Math.round(wheelDeltaX * @scrollSensitivity) + previousScrollLeft = editor.getScrollLeft() + editor.setScrollLeft(previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity)) + event.preventDefault() unless previousScrollLeft is editor.getScrollLeft() else # Scrolling vertically - @pendingVerticalScrollDelta -= Math.round(wheelDeltaY * @scrollSensitivity) @mouseWheelScreenRow = @screenRowForNode(event.target) @clearMouseWheelScreenRowAfterDelay ?= debounce(@clearMouseWheelScreenRow, @mouseWheelScreenRowClearDelay) @clearMouseWheelScreenRowAfterDelay() - - unless animationFramePending - @requestAnimationFrame => - {editor} = @props - editor.setScrollTop(editor.getScrollTop() + @pendingVerticalScrollDelta) - editor.setScrollLeft(editor.getScrollLeft() + @pendingHorizontalScrollDelta) - @pendingVerticalScrollDelta = 0 - @pendingHorizontalScrollDelta = 0 + previousScrollTop = editor.getScrollTop() + editor.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity)) + event.preventDefault() unless previousScrollTop is editor.getScrollTop() onScrollViewScroll: -> if @isMounted() @@ -656,7 +670,7 @@ EditorComponent = React.createClass onStylesheetsChanged: (stylesheet) -> @refreshScrollbars() if @containsScrollbarSelector(stylesheet) @remeasureCharacterWidthsIfVisibleAfterNextUpdate = true - @requestUpdate() if @state.visible + @requestUpdate() if @visible onScreenLinesChanged: (change) -> {editor} = @props @@ -733,68 +747,87 @@ EditorComponent = React.createClass window.addEventListener('mousemove', onMouseMove) window.addEventListener('mouseup', onMouseUp) - pauseScrollViewMeasurement: -> - @scrollViewMeasurementPaused = true - @resumeScrollViewMeasurementAfterDelay ?= debounce(@resumeScrollViewMeasurement, 100) - @resumeScrollViewMeasurementAfterDelay() + isVisible: -> + node = @getDOMNode() + node.offsetHeight > 0 or node.offsetWidth > 0 - resumeScrollViewMeasurement: -> - @scrollViewMeasurementPaused = false + pauseDOMPolling: -> + @domPollingPaused = true + @resumeDOMPollingAfterDelay ?= debounce(@resumeDOMPolling, 100) + @resumeDOMPollingAfterDelay() - resumeScrollViewMeasurementAfterDelay: null # created lazily + resumeDOMPolling: -> + @domPollingPaused = false - requestScrollViewMeasurement: -> - return if @scrollViewMeasurementRequested + resumeDOMPollingAfterDelay: null # created lazily - @scrollViewMeasurementRequested = true + pollDOM: -> + return if @domPollingPaused or not @isMounted() + + wasVisible = @visible + if @visible = @isVisible() + if wasVisible + @measureHeightAndWidth() + else + @performInitialMeasurement() + + requestHeightAndWidthMeasurement: -> + return if @heightAndWidthMeasurementRequested + + @heightAndWidthMeasurementRequested = true requestAnimationFrame => - @scrollViewMeasurementRequested = false - @measureScrollView() + @heightAndWidthMeasurementRequested = false + @measureHeightAndWidth() # Measure explicitly-styled height and width and relay them to the model. If # these values aren't explicitly styled, we assume the editor is unconstrained # and use the scrollHeight / scrollWidth as its height and width in # calculations. - measureScrollView: -> - return if @scrollViewMeasurementPaused + measureHeightAndWidth: -> return unless @isMounted() - {editor} = @props - editorNode = @getDOMNode() + {editor, parentView} = @props + parentNode = parentView.element scrollViewNode = @refs.scrollView.getDOMNode() - {position} = getComputedStyle(editorNode) - {width, height} = editorNode.style + {position} = getComputedStyle(parentNode) + {height} = parentNode.style if position is 'absolute' or height + if @autoHeight + @autoHeight = false + @forceUpdate() + clientHeight = scrollViewNode.clientHeight editor.setHeight(clientHeight) if clientHeight > 0 + else + editor.setHeight(null) + @autoHeight = true - if position is 'absolute' or width - clientWidth = scrollViewNode.clientWidth - paddingLeft = parseInt(getComputedStyle(scrollViewNode).paddingLeft) - clientWidth -= paddingLeft - editor.setWidth(clientWidth) if clientWidth > 0 + clientWidth = scrollViewNode.clientWidth + paddingLeft = parseInt(getComputedStyle(scrollViewNode).paddingLeft) + clientWidth -= paddingLeft + editor.setWidth(clientWidth) if clientWidth > 0 - measureLineHeightAndCharWidthsIfNeeded: (prevState) -> + measureLineHeightAndDefaultCharWidthIfNeeded: (prevState) -> if not isEqualForProperties(prevState, @state, 'lineHeight', 'fontSize', 'fontFamily') - if @state.visible + if @visible @measureLineHeightAndDefaultCharWidth() else @measureLineHeightAndDefaultCharWidthWhenShown = true - else if @measureLineHeightAndDefaultCharWidthWhenShown and @state.visible and not prevState.visible + else if @measureLineHeightAndDefaultCharWidthWhenShown and @visible + @measureLineHeightAndDefaultCharWidthWhenShown = false @measureLineHeightAndDefaultCharWidth() measureLineHeightAndDefaultCharWidth: -> - @measureLineHeightAndDefaultCharWidthWhenShown = false @refs.lines.measureLineHeightAndDefaultCharWidth() remeasureCharacterWidthsIfNeeded: (prevState) -> if not isEqualForProperties(prevState, @state, 'fontSize', 'fontFamily') - if @state.visible + if @visible @remeasureCharacterWidths() else @remeasureCharacterWidthsIfVisibleAfterNextUpdate = true - else if @remeasureCharacterWidthsIfVisibleAfterNextUpdate and @state.visible + else if @remeasureCharacterWidthsIfVisibleAfterNextUpdate and @visible @remeasureCharacterWidthsIfVisibleAfterNextUpdate = false @remeasureCharacterWidths() @@ -805,6 +838,7 @@ EditorComponent = React.createClass @requestUpdate() measureScrollbars: -> + return unless @visible @measuringScrollbars = false {editor} = @props @@ -861,12 +895,6 @@ EditorComponent = React.createClass node = node.parentNode null - hide: -> - @setState(visible: false) - - show: -> - @setState(visible: true) - getFontSize: -> @state.fontSize @@ -940,6 +968,10 @@ EditorComponent = React.createClass if prevState.focused isnt @state.focused @props.parentView.toggleClass('is-focused', @props.focused) + updateParentViewMiniClassIfNeeded: (prevProps) -> + if prevProps.mini isnt @props.mini + @props.parentView.toggleClass('mini', @props.mini) + runScrollBenchmark: -> unless process.env.NODE_ENV is 'production' ReactPerf = require 'react-atom-fork/lib/ReactDefaultPerf' diff --git a/src/editor.coffee b/src/editor.coffee index 0e622b3a0..870c14b6c 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -518,6 +518,8 @@ class Editor extends Model # {Delegates to: TextBuffer.isModified} isModified: -> @buffer.isModified() + isEmpty: -> @buffer.isEmpty() + # Public: Determine whether the user should be prompted to save before closing # this editor. shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors() diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 9dc6fb3e7..223b5a077 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -19,9 +19,7 @@ GutterComponent = React.createClass {scrollHeight, scrollViewHeight, onMouseDown} = @props div className: 'gutter', onClick: @onClick, onMouseDown: onMouseDown, - # The line-numbers div must have the 'editor-colors' class so it has an - # opaque background to avoid sub-pixel anti-aliasing problems on the GPU - div className: 'gutter line-numbers editor-colors', ref: 'lineNumbers', style: + div className: 'gutter line-numbers', ref: 'lineNumbers', style: height: Math.max(scrollHeight, scrollViewHeight) WebkitTransform: @getTransform() @@ -53,6 +51,8 @@ GutterComponent = React.createClass ) {renderedRowRange, pendingChanges, lineDecorations} = newProps + return false unless renderedRowRange? + for change in pendingChanges when Math.abs(change.screenDelta) > 0 or Math.abs(change.bufferDelta) > 0 return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start diff --git a/src/highlights-component.coffee b/src/highlights-component.coffee index 2cadcb289..dd0749ffd 100644 --- a/src/highlights-component.coffee +++ b/src/highlights-component.coffee @@ -9,7 +9,7 @@ HighlightsComponent = React.createClass render: -> div className: 'highlights', - @renderHighlights() if @isMounted() + @renderHighlights() if @props.performedInitialMeasurement renderHighlights: -> {editor, highlightDecorations, lineHeightInPixels} = @props diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 52c81ba06..d1b1aba94 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -16,18 +16,19 @@ LinesComponent = React.createClass displayName: 'LinesComponent' render: -> - if @isMounted() - {editor, highlightDecorations, scrollHeight, scrollWidth} = @props + {performedInitialMeasurement} = @props + + if performedInitialMeasurement + {editor, highlightDecorations, scrollHeight, scrollWidth, placeholderText} = @props {lineHeightInPixels, defaultCharWidth, scrollViewHeight, scopedCharacterWidthsChangeCount} = @props style = height: Math.max(scrollHeight, scrollViewHeight) width: scrollWidth WebkitTransform: @getTransform() - # The lines div must have the 'editor-colors' class so it has an opaque - # background to avoid sub-pixel anti-aliasing problems on the GPU - div {className: 'lines editor-colors', style}, - HighlightsComponent({editor, highlightDecorations, lineHeightInPixels, defaultCharWidth, scopedCharacterWidthsChangeCount}) + div {className: 'lines', style}, + div className: 'placeholder-text', placeholderText if placeholderText? + HighlightsComponent({editor, highlightDecorations, lineHeightInPixels, defaultCharWidth, scopedCharacterWidthsChangeCount, performedInitialMeasurement}) getTransform: -> {scrollTop, scrollLeft, useHardwareAcceleration} = @props @@ -48,10 +49,13 @@ LinesComponent = React.createClass return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'lineDecorations', 'highlightDecorations', 'lineHeightInPixels', 'defaultCharWidth', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically', 'invisibles', 'visible', - 'scrollViewHeight', 'mouseWheelScreenRow', 'scopedCharacterWidthsChangeCount', 'lineWidth', 'useHardwareAcceleration' + 'scrollViewHeight', 'mouseWheelScreenRow', 'scopedCharacterWidthsChangeCount', 'lineWidth', 'useHardwareAcceleration', + 'placeholderText', 'performedInitialMeasurement' ) {renderedRowRange, pendingChanges} = newProps + return false unless renderedRowRange? + [renderedStartRow, renderedEndRow] = renderedRowRange for change in pendingChanges if change.screenDelta is 0 diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index 7a97d5355..80f21ef57 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -1,15 +1,33 @@ {View, $} = require 'space-pen' React = require 'react-atom-fork' -EditorComponent = require './editor-component' {defaults} = require 'underscore-plus' +TextBuffer = require 'text-buffer' +Editor = require './editor' +EditorComponent = require './editor-component' module.exports = class ReactEditorView extends View - @content: -> @div class: 'editor react' + @content: (params) -> + attributes = params.attributes ? {} + attributes.class = 'editor react editor-colors' + @div attributes focusOnAttach: false - constructor: (@editor, @props) -> + constructor: (editorOrParams, @props) -> + if editorOrParams instanceof Editor + @editor = editorOrParams + else + {@editor, mini, placeholderText} = editorOrParams + @props ?= {} + @props.mini = mini + @props.placeholderText = placeholderText + @editor ?= new Editor + buffer: new TextBuffer + softWrap: false + tabLength: 2 + softTabs: true + super getEditor: -> @editor @@ -122,7 +140,7 @@ class ReactEditorView extends View pane?.splitDown(pane?.copyActiveItem()).activeView getPane: -> - @closest('.pane').view() + @parent('.item-views').parents('.pane').view() focus: -> if @component? @@ -132,11 +150,18 @@ class ReactEditorView extends View hide: -> super - @component?.hide() + @pollComponentDOM() show: -> super - @component?.show() + @pollComponentDOM() + + pollComponentDOM: -> + return unless @component? + valueToRestore = @component.performSyncUpdates + @component.performSyncUpdates = true + @component.pollDOM() + @component.performSyncUpdates = valueToRestore pageDown: -> @editor.pageDown() @@ -208,3 +233,9 @@ class ReactEditorView extends View resetDisplay: -> # No-op shim for package specs redraw: -> # No-op shim + + setPlaceholderText: (placeholderText) -> + if @component? + @component.setProps({placeholderText}) + else + @props.placeholderText = placeholderText diff --git a/static/editor.less b/static/editor.less index 03ce26693..ed8d6ba84 100644 --- a/static/editor.less +++ b/static/editor.less @@ -3,6 +3,10 @@ @import "octicon-mixins"; .editor.react { + .editor-contents { + width: 100%; + } + .underlayer { position: absolute; top: 0; @@ -81,15 +85,18 @@ } } +.editor { + z-index: 0; + font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier; + line-height: 1.3; +} + .editor, .editor-contents { overflow: hidden; cursor: text; display: -webkit-flex; -webkit-user-select: none; position: relative; - z-index: 0; - font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier; - line-height: 1.3; } .editor .gutter .line-number.cursor-line {