From f16ea63a957dcc4f5788c67f8911c649e5f062e1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 9 Jul 2014 09:26:34 -0600 Subject: [PATCH 01/24] Export ReactEditorView as EditorView from 'atom' module Also, remove a few early requires of 'exports/atom.coffee' in the spec suite that were causing failures. --- exports/atom.coffee | 2 +- spec/jasmine-helper.coffee | 3 ++- spec/spec-suite.coffee | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/exports/atom.coffee b/exports/atom.coffee index 773b24f3a..b852d68a7 100644 --- a/exports/atom.coffee +++ b/exports/atom.coffee @@ -15,7 +15,7 @@ unless process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE module.exports.$ = $ module.exports.$$ = $$ module.exports.$$$ = $$$ - module.exports.EditorView = require '../src/editor-view' + module.exports.EditorView = require '../src/react-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/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' From e999ef00e7e5eb150c6e0bfa292231d5ded11fcc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 13 Jul 2014 11:56:14 -0600 Subject: [PATCH 02/24] Base editor dimensions on the wrapper view The goal is to make the editor behave like a standard block-level element. The horizontal behavior is simple: we stretch horizontally to fill our container. The vertical behavior is more nuanced. If an explicit height is assigned on the wrapper view, we honor that height. But if no explicit height is assigned, the editor stretches vertically so that its contents are visible. This prepares us to support mini editors, which need to be 1-line tall without an explicit height assignment. --- spec/editor-component-spec.coffee | 395 +++++++++++++++--------------- src/editor-component.coffee | 19 +- static/editor.less | 4 + 3 files changed, 212 insertions(+), 206 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index da551e2b4..42f7bdb3b 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,12 +57,12 @@ 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' + wrapperNode.style.height = editor.getLineCount() * lineHeightInPixels + 'px' + wrapperNode.style.width = '1000px' component.measureScrollView() runSetImmediateCallbacks() @@ -70,13 +71,13 @@ describe "EditorComponent", -> describe "line rendering", -> it "renders the currently-visible lines plus the overdraw margin", -> - node.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() 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 +87,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 +97,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 +105,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,7 +113,7 @@ 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' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() runSetImmediateCallbacks() verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels @@ -161,19 +162,19 @@ 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' + wrapperNode.style.height = '300px' component.measureScrollView() 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' + componentNode.style.width = gutterWidth + (30 * charWidth) + 'px' component.measureScrollView() runSetImmediateCallbacks() expect(editor.getScrollWidth()).toBeGreaterThan scrollViewNode.offsetWidth @@ -185,7 +186,7 @@ describe "EditorComponent", -> for lineNode in lineNodes expect(lineNode.style.width).toBe editor.getScrollWidth() + 'px' - node.style.width = gutterWidth + editor.getScrollWidth() + 100 + 'px' + componentNode.style.width = gutterWidth + editor.getScrollWidth() + 100 + 'px' component.measureScrollView() runSetImmediateCallbacks() scrollViewWidth = scrollViewNode.offsetWidth @@ -259,7 +260,7 @@ describe "EditorComponent", -> editor.setText "a line that wraps " editor.setSoftWrap(true) runSetImmediateCallbacks() - node.style.width = 16 * charWidth + 'px' + componentNode.style.width = 16 * charWidth + 'px' component.measureScrollView() runSetImmediateCallbacks() @@ -396,18 +397,18 @@ describe "EditorComponent", -> {gutter} = component.refs it "renders the currently-visible line numbers", -> - node.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() 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 +419,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 +439,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' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 30 * charWidth + 'px' component.measureScrollView() 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 +459,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 +478,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' + wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px' component.measureScrollView() 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 +524,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 +545,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 +562,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 +573,12 @@ describe "EditorComponent", -> cursor1 = editor.getCursor() cursor1.setScreenPosition([0, 5]) - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = 20 * lineHeightInPixels + 'px' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * lineHeightInPixels + 'px' component.measureScrollView() 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 +588,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 +599,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 +615,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 +640,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 +657,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 +690,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 +698,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 +721,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 +740,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 +758,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 +785,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 +806,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,7 +837,7 @@ describe "EditorComponent", -> expect(lineAndLineNumberHaveClass(3, 'a')).toBe true # Shrink editor vertically - node.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() runSetImmediateCallbacks() @@ -859,7 +860,7 @@ 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' + componentNode.style.width = 16 * charWidth + 'px' component.measureScrollView() runSetImmediateCallbacks() @@ -968,14 +969,14 @@ 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' + wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px' component.measureScrollView() runSetImmediateCallbacks() @@ -986,7 +987,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 +995,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 +1005,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 +1030,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,45 +1075,45 @@ 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' + inputNode = componentNode.querySelector('.hidden-input') + wrapperNode.style.height = 5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' component.measureScrollView() runSetImmediateCallbacks() @@ -1156,13 +1157,13 @@ 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' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' component.measureScrollView() editor.setScrollTop(3.5 * lineHeightInPixels) editor.setScrollLeft(2 * charWidth) @@ -1288,7 +1289,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 +1384,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,19 +1409,19 @@ 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' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() runSetImmediateCallbacks() @@ -1431,11 +1432,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' + componentNode.style.width = 30 * charWidth + 'px' component.measureScrollView() 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,7 +1446,7 @@ 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' + componentNode.style.width = 30 * charWidth + 'px' component.measureScrollView() runSetImmediateCallbacks() @@ -1456,8 +1457,8 @@ 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' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' component.measureScrollView() editor.setScrollBottom(editor.getScrollHeight()) runSetImmediateCallbacks() @@ -1467,16 +1468,16 @@ 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' + wrapperNode.style.width = 100 * charWidth + 'px' component.measureScrollView() 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' + wrapperNode.style.height = 7 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' component.measureScrollView() editor.setScrollLeft(Infinity) runSetImmediateCallbacks() @@ -1489,22 +1490,22 @@ 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' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = '1000px' component.measureScrollView() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.display).toBe '' expect(horizontalScrollbarNode.style.display).toBe 'none' - node.style.width = 10 * charWidth + 'px' + componentNode.style.width = 10 * charWidth + 'px' component.measureScrollView() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.display).toBe '' expect(horizontalScrollbarNode.style.display).toBe '' - node.style.height = 20 * lineHeightInPixels + 'px' + wrapperNode.style.height = 20 * lineHeightInPixels + 'px' component.measureScrollView() runSetImmediateCallbacks() @@ -1512,8 +1513,8 @@ describe "EditorComponent", -> 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' + wrapperNode.style.height = 4 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' component.measureScrollView() runSetImmediateCallbacks() @@ -1524,34 +1525,34 @@ 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 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' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = '1000px' component.measureScrollView() 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' + componentNode.style.width = 10 * charWidth + 'px' component.measureScrollView() 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' + wrapperNode.style.height = 20 * lineHeightInPixels + 'px' component.measureScrollView() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' @@ -1559,8 +1560,8 @@ describe "EditorComponent", -> 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' + gutterNode = componentNode.querySelector('.gutter') + componentNode.style.width = 10 * charWidth + 'px' component.measureScrollView() runSetImmediateCallbacks() @@ -1572,8 +1573,8 @@ describe "EditorComponent", -> describe "updating scrollTop and scrollLeft", -> beforeEach -> - node.style.height = 4.5 * lineHeightInPixels + 'px' - node.style.width = 20 * charWidth + 'px' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' component.measureScrollView() runSetImmediateCallbacks() @@ -1581,55 +1582,55 @@ describe "EditorComponent", -> 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)) 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)) 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)) expect(horizontalScrollbarNode.scrollLeft).toBe 0 - node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) 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)) 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)) 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' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' component.measureScrollView() - 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) - 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' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' component.measureScrollView() - 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) expect(component.mouseWheelScreenRow).toBe null @@ -1638,10 +1639,10 @@ 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) expect(editor.getScrollTop()).toBe 0 @@ -1650,38 +1651,38 @@ 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) 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' + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' component.measureScrollView() - 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) - expect(node.contains(lineNumberNode)).toBe true + expect(componentNode.contains(lineNumberNode)).toBe true 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 +1691,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 +1726,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 +1778,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,7 +1807,7 @@ 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() @@ -1849,8 +1850,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 +1877,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 +1900,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 +1912,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,23 +1920,23 @@ 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(newHeight).toBeLessThan wrapperNode.style.height + wrapperNode.style.height = newHeight advanceClock(component.scrollViewMeasurementInterval) 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' + gutterWidth = componentNode.querySelector('.gutter').offsetWidth + componentNode.style.width = gutterWidth + 14 * charWidth + 'px' advanceClock(component.scrollViewMeasurementInterval) 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) runSetImmediateCallbacks() @@ -2014,14 +2015,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/src/editor-component.coffee b/src/editor-component.coffee index 1d1ce4318..217b1c20f 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -759,21 +759,22 @@ EditorComponent = React.createClass return if @scrollViewMeasurementPaused 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 clientHeight = scrollViewNode.clientHeight editor.setHeight(clientHeight) if clientHeight > 0 + else + editor.setHeight(null) - 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) -> if not isEqualForProperties(prevState, @state, 'lineHeight', 'fontSize', 'fontFamily') diff --git a/static/editor.less b/static/editor.less index 03ce26693..345096c7c 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; From 4020ed153585bbb7c3551f6010b7de804ccfa08d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 13 Jul 2014 12:07:36 -0600 Subject: [PATCH 03/24] Support ReactEditorView construction with params hash --- src/react-editor-view.coffee | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index 7a97d5355..ecb5b24b7 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -1,7 +1,9 @@ {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 @@ -9,7 +11,20 @@ class ReactEditorView extends View 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 From cc8b7b13b3a75c7d718cf0e6be6906fdf6a37d88 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 13 Jul 2014 13:06:37 -0600 Subject: [PATCH 04/24] Don't show the gutter when 'mini' is true on React editors --- spec/editor-component-spec.coffee | 7 +++++++ src/editor-component.coffee | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 42f7bdb3b..2c2f8ed16 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -1991,6 +1991,13 @@ describe "EditorComponent", -> runSetImmediateCallbacks() expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe false + describe "when the 'mini' property is true", -> + beforeEach -> + component.setProps(mini: true) + + it "does not render the gutter", -> + expect(componentNode.querySelector('.gutter')).toBeNull() + describe "legacy editor compatibility", -> it "triggers the screen-lines-changed event before the editor:display-update event", -> editor.setSoftWrap(true) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 217b1c20f..59d969c3a 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -50,7 +50,7 @@ EditorComponent = React.createClass 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 {} hasSelection = editor.getSelection()? and !editor.getSelection().isEmpty() @@ -86,7 +86,7 @@ EditorComponent = React.createClass className += ' has-selection' if hasSelection div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, - if showLineNumbers + if not mini and showLineNumbers GutterComponent { ref: 'gutter', onMouseDown: @onGutterMouseDown, onWidthChanged: @onGutterWidthChanged, lineDecorations, defaultCharWidth, editor, renderedRowRange, maxLineNumberDigits, scrollViewHeight, From 635f288050feb6474e6ead20f960e6b100aa46bd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 13 Jul 2014 13:50:33 -0600 Subject: [PATCH 05/24] Explicitly assign height of editor-contents when height is auto --- spec/editor-component-spec.coffee | 16 +++++++++++++--- src/editor-component.coffee | 10 +++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 2c2f8ed16..d81d114d7 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -61,8 +61,6 @@ describe "EditorComponent", -> verticalScrollbarNode = componentNode.querySelector('.vertical-scrollbar') horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') - wrapperNode.style.height = editor.getLineCount() * lineHeightInPixels + 'px' - wrapperNode.style.width = '1000px' component.measureScrollView() runSetImmediateCallbacks() @@ -1920,7 +1918,7 @@ describe "EditorComponent", -> it "updates the wrap location when the editor is resized", -> newHeight = 4 * editor.getLineHeightInPixels() + "px" - expect(newHeight).toBeLessThan wrapperNode.style.height + expect(parseInt(newHeight)).toBeLessThan wrapperNode.offsetHeight wrapperNode.style.height = newHeight advanceClock(component.scrollViewMeasurementInterval) @@ -1991,6 +1989,18 @@ 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.measureScrollView() + 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) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 59d969c3a..1a15e553a 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -47,6 +47,7 @@ EditorComponent = React.createClass scrollViewMeasurementInterval: 100 scopedCharacterWidthsChangeCount: null scrollViewMeasurementPaused: false + autoHeight: false render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide, showInvisibles, showLineNumbers, visible} = @state @@ -54,6 +55,7 @@ EditorComponent = React.createClass maxLineNumberDigits = editor.getLineCount().toString().length invisibles = if showInvisibles then @state.invisibles else {} hasSelection = editor.getSelection()? and !editor.getSelection().isEmpty() + style = {fontSize, lineHeight, fontFamily} if @isMounted() renderedRowRange = @getRenderedRowRange() @@ -80,12 +82,13 @@ EditorComponent = React.createClass hiddenInputStyle.WebkitTransform = 'translateZ(0)' if @useHardwareAcceleration if @mouseWheelScreenRow? and not (renderedStartRow <= @mouseWheelScreenRow < renderedEndRow) mouseWheelScreenRow = @mouseWheelScreenRow + style.height = scrollViewHeight if @autoHeight className = 'editor-contents editor-colors' className += ' is-focused' if focused className += ' has-selection' if hasSelection - div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1, + div {className, style, tabIndex: -1}, if not mini and showLineNumbers GutterComponent { ref: 'gutter', onMouseDown: @onGutterMouseDown, onWidthChanged: @onGutterWidthChanged, @@ -766,10 +769,15 @@ EditorComponent = React.createClass {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 clientWidth = scrollViewNode.clientWidth paddingLeft = parseInt(getComputedStyle(scrollViewNode).paddingLeft) From 56c9f75e8c6a568d37caffdfbdba761a282ae0d6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 13 Jul 2014 13:59:32 -0600 Subject: [PATCH 06/24] Add the 'mini' class to the React wrapper view for mini editors --- spec/editor-component-spec.coffee | 3 +++ src/editor-component.coffee | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index d81d114d7..d3e859ac4 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -2008,6 +2008,9 @@ describe "EditorComponent", -> 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 + describe "legacy editor compatibility", -> it "triggers the screen-lines-changed event before the editor:display-update event", -> editor.setSoftWrap(true) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 1a15e553a..0f1a0a5d9 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -200,6 +200,7 @@ 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' @@ -949,6 +950,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' From 759dbc061de327ca2d38c5ab56a9e6dcb88d3abf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 13 Jul 2014 14:03:51 -0600 Subject: [PATCH 07/24] Don't render invisible characters in React mini editors --- spec/editor-component-spec.coffee | 5 +++++ src/editor-component.coffee | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index d3e859ac4..c4e01ddd2 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -2011,6 +2011,11 @@ describe "EditorComponent", -> 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 () {' + describe "legacy editor compatibility", -> it "triggers the screen-lines-changed event before the editor:display-update event", -> editor.setSoftWrap(true) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 0f1a0a5d9..748db128e 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -53,7 +53,7 @@ EditorComponent = React.createClass {focused, fontSize, lineHeight, fontFamily, showIndentGuide, showInvisibles, showLineNumbers, visible} = @state {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, lineHeight, fontFamily} From 544c759fd1e0e8273718f456271acf8de5126e95 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 13 Jul 2014 14:45:38 -0600 Subject: [PATCH 08/24] Don't set an explicit line height on mini editors This allows the line height to be styled via CSS. I would actually like to allow all these properties to be assigned via CSS rather than explicitly via the settings view, but that can be deferred until the old editor is removed. --- spec/editor-component-spec.coffee | 3 +++ src/editor-component.coffee | 3 ++- static/editor.less | 9 ++++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index c4e01ddd2..d6a807c8a 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -2016,6 +2016,9 @@ describe "EditorComponent", -> 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 '' + describe "legacy editor compatibility", -> it "triggers the screen-lines-changed event before the editor:display-update event", -> editor.setSoftWrap(true) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 748db128e..5527c9fc0 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -55,7 +55,8 @@ EditorComponent = React.createClass maxLineNumberDigits = editor.getLineCount().toString().length invisibles = if showInvisibles and not mini then @state.invisibles else {} hasSelection = editor.getSelection()? and !editor.getSelection().isEmpty() - style = {fontSize, lineHeight, fontFamily} + style = {fontSize, fontFamily} + style.lineHeight = lineHeight unless mini if @isMounted() renderedRowRange = @getRenderedRowRange() diff --git a/static/editor.less b/static/editor.less index 345096c7c..ed8d6ba84 100644 --- a/static/editor.less +++ b/static/editor.less @@ -85,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 { From a9c7842a5049abd0c43241f23c66211abf22ccfb Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 13 Jul 2014 14:56:34 -0600 Subject: [PATCH 09/24] Don't render line decorations on mini editors --- spec/editor-component-spec.coffee | 3 +++ src/editor-component.coffee | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index d6a807c8a..3ec8d5123 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -2019,6 +2019,9 @@ describe "EditorComponent", -> 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 "legacy editor compatibility", -> it "triggers the screen-lines-changed event before the editor:display-update event", -> editor.setSoftWrap(true) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 5527c9fc0..a5f7fa008 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -278,7 +278,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) From d0893ccdaf8106aa945420b9fc066f489680c046 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 13 Jul 2014 15:08:48 -0600 Subject: [PATCH 10/24] Add placeholderText to React editors --- spec/editor-component-spec.coffee | 11 +++++++++++ src/editor-component.coffee | 4 +++- src/editor.coffee | 2 ++ src/lines-component.coffee | 6 ++++-- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 3ec8d5123..a1b214043 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -2022,6 +2022,17 @@ describe "EditorComponent", -> 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) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index a5f7fa008..d12f1b870 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -66,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() @@ -114,7 +115,8 @@ EditorComponent = React.createClass 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 } ScrollbarComponent 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/lines-component.coffee b/src/lines-component.coffee index 52c81ba06..eeee69f01 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -17,7 +17,7 @@ LinesComponent = React.createClass render: -> if @isMounted() - {editor, highlightDecorations, scrollHeight, scrollWidth} = @props + {editor, highlightDecorations, scrollHeight, scrollWidth, placeholderText} = @props {lineHeightInPixels, defaultCharWidth, scrollViewHeight, scopedCharacterWidthsChangeCount} = @props style = height: Math.max(scrollHeight, scrollViewHeight) @@ -27,6 +27,7 @@ LinesComponent = React.createClass # 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}, + div className: 'placeholder-text', placeholderText if placeholderText? HighlightsComponent({editor, highlightDecorations, lineHeightInPixels, defaultCharWidth, scopedCharacterWidthsChangeCount}) getTransform: -> @@ -48,7 +49,8 @@ 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' ) {renderedRowRange, pendingChanges} = newProps From a0f75f163936d5cf3c9d61fcef56920bdcc13e8f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 13 Jul 2014 19:49:36 -0600 Subject: [PATCH 11/24] Determine visibility by checking offsetWidth/Height of the editor's node This could still use some cleanup --- src/editor-component.coffee | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index d12f1b870..080a47c6d 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -84,6 +84,7 @@ EditorComponent = React.createClass hiddenInputStyle.WebkitTransform = 'translateZ(0)' if @useHardwareAcceleration if @mouseWheelScreenRow? and not (renderedStartRow <= @mouseWheelScreenRow < renderedEndRow) mouseWheelScreenRow = @mouseWheelScreenRow + style.height = scrollViewHeight if @autoHeight className = 'editor-contents editor-colors' @@ -155,8 +156,7 @@ EditorComponent = React.createClass {editor} = @props Math.max(1, Math.ceil(editor.getHeight() / editor.getLineHeightInPixels())) - getInitialState: -> - visible: true + getInitialState: -> {} getDefaultProps: -> cursorBlinkPeriod: 800 @@ -183,6 +183,7 @@ EditorComponent = React.createClass editor.setVisible(true) + @visible = @isVisible() @measureLineHeightAndDefaultCharWidth() @measureScrollView() @measureScrollbars() @@ -208,8 +209,9 @@ EditorComponent = React.createClass @props.parentView.trigger 'selection:changed' if selectionChanged @props.parentView.trigger 'editor:display-updated' + @visible = @isVisible() @measureScrollbars() if @measuringScrollbars - @measureLineHeightAndCharWidthsIfNeeded(prevState) + @measureLineHeightAndDefaultCharWidthIfNeeded(prevState) @remeasureCharacterWidthsIfNeeded(prevState) requestUpdate: -> @@ -665,7 +667,7 @@ EditorComponent = React.createClass onStylesheetsChanged: (stylesheet) -> @refreshScrollbars() if @containsScrollbarSelector(stylesheet) @remeasureCharacterWidthsIfVisibleAfterNextUpdate = true - @requestUpdate() if @state.visible + @requestUpdate() if @visible onScreenLinesChanged: (change) -> {editor} = @props @@ -742,6 +744,10 @@ EditorComponent = React.createClass window.addEventListener('mousemove', onMouseMove) window.addEventListener('mouseup', onMouseUp) + isVisible: -> + node = @getDOMNode() + node.offsetHeight > 0 and node.offsetWidth > 0 + pauseScrollViewMeasurement: -> @scrollViewMeasurementPaused = true @resumeScrollViewMeasurementAfterDelay ?= debounce(@resumeScrollViewMeasurement, 100) @@ -790,26 +796,26 @@ EditorComponent = React.createClass 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() @@ -877,10 +883,12 @@ EditorComponent = React.createClass null hide: -> - @setState(visible: false) + @visible = false show: -> - @setState(visible: true) + unless @visible + @visible = true + @forceUpdate() getFontSize: -> @state.fontSize From e81db5d706a682912c6e4c3732455f0dda47a18e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Jul 2014 12:10:39 -0600 Subject: [PATCH 12/24] Pull out EditorComponent::pollDOM method This makes the actions that we perform in the poll loop explicit, and will prevent the accumulation of polling-related behavior in the ::measureScrollView method. --- spec/editor-component-spec.coffee | 6 ++--- src/editor-component.coffee | 43 +++++++++++++++++++------------ 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index a1b214043..2330bc556 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -1921,13 +1921,13 @@ describe "EditorComponent", -> expect(parseInt(newHeight)).toBeLessThan wrapperNode.offsetHeight wrapperNode.style.height = newHeight - advanceClock(component.scrollViewMeasurementInterval) + advanceClock(component.domPollingInterval) runSetImmediateCallbacks() expect(componentNode.querySelectorAll('.line')).toHaveLength(4 + lineOverdrawMargin + 1) gutterWidth = componentNode.querySelector('.gutter').offsetWidth componentNode.style.width = gutterWidth + 14 * charWidth + 'px' - advanceClock(component.scrollViewMeasurementInterval) + advanceClock(component.domPollingInterval) runSetImmediateCallbacks() expect(componentNode.querySelector('.line').textContent).toBe "var quicksort " @@ -1936,7 +1936,7 @@ describe "EditorComponent", -> scrollViewNode.style.paddingLeft = 20 + 'px' componentNode.style.width = 30 * charWidth + 'px' - advanceClock(component.scrollViewMeasurementInterval) + advanceClock(component.domPollingInterval) runSetImmediateCallbacks() expect(component.lineNodeForScreenRow(0).textContent).toBe "var quicksort = " diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 080a47c6d..cd51d39d6 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -22,6 +22,8 @@ EditorComponent = React.createClass statics: performSyncUpdates: false + visible: true + autoHeight: false pendingScrollTop: null pendingScrollLeft: null selectOnMouseMove: false @@ -44,10 +46,10 @@ EditorComponent = React.createClass measureLineHeightAndDefaultCharWidthWhenShown: false remeasureCharacterWidthsIfVisibleAfterNextUpdate: false inputEnabled: true - scrollViewMeasurementInterval: 100 scopedCharacterWidthsChangeCount: null - scrollViewMeasurementPaused: false - autoHeight: false + domPollingInterval: 100 + domPollingIntervalId: null + domPollingPaused: false render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide, showInvisibles, showLineNumbers, visible} = @state @@ -172,7 +174,7 @@ EditorComponent = React.createClass componentDidMount: -> {editor} = @props - @scrollViewMeasurementIntervalId = setInterval(@measureScrollView, @scrollViewMeasurementInterval) + @domPollingIntervalId = setInterval(@pollDOM, @domPollingInterval) @observeEditor() @listenForDOMEvents() @@ -183,7 +185,6 @@ EditorComponent = React.createClass editor.setVisible(true) - @visible = @isVisible() @measureLineHeightAndDefaultCharWidth() @measureScrollView() @measureScrollbars() @@ -191,8 +192,8 @@ EditorComponent = React.createClass componentWillUnmount: -> @props.parentView.trigger 'editor:will-be-removed', [@props.parentView] @unsubscribe() - clearInterval(@scrollViewMeasurementIntervalId) - @scrollViewMeasurementIntervalId = null + clearInterval(@domPollingIntervalId) + @domPollingIntervalId = null componentDidUpdate: (prevProps, prevState) -> cursorsMoved = @cursorsMoved @@ -229,7 +230,7 @@ EditorComponent = React.createClass requestAnimationFrame: (fn) -> @updatesPaused = true - @pauseScrollViewMeasurement() + @pauseDOMPolling() requestAnimationFrame => fn() @updatesPaused = false @@ -748,15 +749,26 @@ EditorComponent = React.createClass node = @getDOMNode() node.offsetHeight > 0 and node.offsetWidth > 0 - pauseScrollViewMeasurement: -> - @scrollViewMeasurementPaused = true - @resumeScrollViewMeasurementAfterDelay ?= debounce(@resumeScrollViewMeasurement, 100) - @resumeScrollViewMeasurementAfterDelay() + pauseDOMPolling: -> + @domPollingPaused = true + @resumeDOMPollingAfterDelay ?= debounce(@resumeDOMPolling, 100) + @resumeDOMPollingAfterDelay() - resumeScrollViewMeasurement: -> - @scrollViewMeasurementPaused = false + resumeDOMPolling: -> + @domPollingPaused = false - resumeScrollViewMeasurementAfterDelay: null # created lazily + resumeDOMPollingAfterDelay: null # created lazily + + pollDOM: -> + return if @domPollingPaused or not @isMounted() + + wasVisible = @visible + @visible = @isVisible() + if @visible + if wasVisible + @measureScrollView() + else + @requestUpdate() requestScrollViewMeasurement: -> return if @scrollViewMeasurementRequested @@ -771,7 +783,6 @@ EditorComponent = React.createClass # and use the scrollHeight / scrollWidth as its height and width in # calculations. measureScrollView: -> - return if @scrollViewMeasurementPaused return unless @isMounted() {editor, parentView} = @props From 783ef730e202f02f59f729f72e557197dbf82893 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Jul 2014 12:16:32 -0600 Subject: [PATCH 13/24] Rename EditorComponent::measureScrollView to ::measureHeightAndWidth Since we also check if we're auto-height in this method, this name seems like a better description of the objectives of this method. --- spec/editor-component-spec.coffee | 70 +++++++++++++++---------------- src/editor-component.coffee | 20 ++++----- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 2330bc556..d7acac7b3 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -61,7 +61,7 @@ describe "EditorComponent", -> verticalScrollbarNode = componentNode.querySelector('.vertical-scrollbar') horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() afterEach -> @@ -70,7 +70,7 @@ describe "EditorComponent", -> describe "line rendering", -> it "renders the currently-visible lines plus the overdraw margin", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() linesNode = componentNode.querySelector('.lines') @@ -112,7 +112,7 @@ describe "EditorComponent", -> it "updates the lines when lines are inserted or removed above the rendered row range", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) @@ -161,7 +161,7 @@ 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('') wrapperNode.style.height = '300px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() linesNode = componentNode.querySelector('.lines') @@ -173,7 +173,7 @@ describe "EditorComponent", -> lineNodes = componentNode.querySelectorAll('.line') componentNode.style.width = gutterWidth + (30 * charWidth) + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(editor.getScrollWidth()).toBeGreaterThan scrollViewNode.offsetWidth @@ -185,7 +185,7 @@ describe "EditorComponent", -> expect(lineNode.style.width).toBe editor.getScrollWidth() + 'px' componentNode.style.width = gutterWidth + editor.getScrollWidth() + 100 + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() scrollViewWidth = scrollViewNode.offsetWidth @@ -259,7 +259,7 @@ describe "EditorComponent", -> editor.setSoftWrap(true) runSetImmediateCallbacks() componentNode.style.width = 16 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() it "doesn't show end of line invisibles at the end of wrapped lines", -> @@ -396,7 +396,7 @@ describe "EditorComponent", -> it "renders the currently-visible line numbers", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 2 + 1 # line overdraw margin below + dummy line number @@ -439,7 +439,7 @@ describe "EditorComponent", -> editor.setSoftWrap(true) wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 30 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + lineOverdrawMargin + 1 # 1 dummy line componentNode @@ -477,7 +477,7 @@ describe "EditorComponent", -> it "renders the .line-numbers div at the full height of the editor even if it's taller than its content", -> wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(componentNode.querySelector('.line-numbers').offsetHeight).toBe componentNode.offsetHeight @@ -573,7 +573,7 @@ describe "EditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * lineHeightInPixels + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() cursorNodes = componentNode.querySelectorAll('.cursor') @@ -836,7 +836,7 @@ describe "EditorComponent", -> # Shrink editor vertically wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() # Add decorations that are out of range @@ -859,7 +859,7 @@ describe "EditorComponent", -> editor.setText("a line that wraps, ok") editor.setSoftWrap(true) componentNode.style.width = 16 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() marker.destroy() @@ -975,7 +975,7 @@ describe "EditorComponent", -> it "does not render highlights for off-screen lines until they come on-screen", -> wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], invalidate: 'inside') @@ -1112,7 +1112,7 @@ describe "EditorComponent", -> inputNode = componentNode.querySelector('.hidden-input') wrapperNode.style.height = 5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(editor.getCursorScreenPosition()).toEqual [0, 0] @@ -1162,7 +1162,7 @@ describe "EditorComponent", -> it "moves the cursor to the nearest screen position", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() editor.setScrollTop(3.5 * lineHeightInPixels) editor.setScrollLeft(2 * charWidth) runSetImmediateCallbacks() @@ -1420,7 +1420,7 @@ describe "EditorComponent", -> describe "scrolling", -> it "updates the vertical scrollbar when the scrollTop is changed in the model", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(verticalScrollbarNode.scrollTop).toBe 0 @@ -1431,7 +1431,7 @@ describe "EditorComponent", -> it "updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model", -> componentNode.style.width = 30 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() linesNode = componentNode.querySelector('.lines') @@ -1445,7 +1445,7 @@ describe "EditorComponent", -> it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> componentNode.style.width = 30 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(editor.getScrollLeft()).toBe 0 @@ -1457,7 +1457,7 @@ describe "EditorComponent", -> it "does not obscure the last line with the horizontal scrollbar", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() editor.setScrollBottom(editor.getScrollHeight()) runSetImmediateCallbacks() lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) @@ -1467,7 +1467,7 @@ describe "EditorComponent", -> # Scroll so there's no space below the last line when the horizontal scrollbar disappears wrapperNode.style.width = 100 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom bottomOfEditor = componentNode.getBoundingClientRect().bottom @@ -1476,7 +1476,7 @@ describe "EditorComponent", -> it "does not obscure the last character of the longest line with the vertical scrollbar", -> wrapperNode.style.height = 7 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() editor.setScrollLeft(Infinity) runSetImmediateCallbacks() @@ -1490,21 +1490,21 @@ describe "EditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = '1000px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.display).toBe '' expect(horizontalScrollbarNode.style.display).toBe 'none' componentNode.style.width = 10 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.display).toBe '' expect(horizontalScrollbarNode.style.display).toBe '' wrapperNode.style.height = 20 * lineHeightInPixels + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.display).toBe 'none' @@ -1513,7 +1513,7 @@ describe "EditorComponent", -> it "makes the dummy scrollbar divs only as tall/wide as the actual scrollbars", -> wrapperNode.style.height = 4 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() atom.themes.applyStylesheet "test", """ @@ -1537,21 +1537,21 @@ describe "EditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = '1000px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.bottom).toBe '' expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' expect(scrollbarCornerNode.style.display).toBe 'none' componentNode.style.width = 10 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() 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.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' expect(horizontalScrollbarNode.style.right).toBe '' @@ -1560,7 +1560,7 @@ describe "EditorComponent", -> it "accounts for the width of the gutter in the scrollWidth of the horizontal scrollbar", -> gutterNode = componentNode.querySelector('.gutter') componentNode.style.width = 10 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() expect(horizontalScrollbarNode.scrollWidth).toBe gutterNode.offsetWidth + editor.getScrollWidth() @@ -1573,7 +1573,7 @@ describe "EditorComponent", -> beforeEach -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() runSetImmediateCallbacks() it "updates the scrollLeft or scrollTop on mousewheel events depending on which delta is greater (x or y)", -> @@ -1611,7 +1611,7 @@ describe "EditorComponent", -> it "keeps the line on the DOM if it is scrolled off-screen", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() lineNode = componentNode.querySelector('.line') wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) @@ -1623,7 +1623,7 @@ describe "EditorComponent", -> it "does not set the mouseWheelScreenRow if scrolling horizontally", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() lineNode = componentNode.querySelector('.line') wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 10, wheelDeltaY: 0) @@ -1667,7 +1667,7 @@ describe "EditorComponent", -> it "keeps the line number on the DOM if it is scrolled off-screen", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' - component.measureScrollView() + component.measureHeightAndWidth() lineNumberNode = componentNode.querySelectorAll('.line-number')[1] wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) @@ -1993,7 +1993,7 @@ describe "EditorComponent", -> describe "when the wrapper view has an explicit height", -> it "does not assign a height on the component node", -> wrapperNode.style.height = '200px' - component.measureScrollView() + component.measureHeightAndWidth() expect(componentNode.style.height).toBe '' describe "when the wrapper view does not have an explicit height", -> diff --git a/src/editor-component.coffee b/src/editor-component.coffee index cd51d39d6..064663a89 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -42,7 +42,7 @@ EditorComponent = React.createClass mouseWheelScreenRow: null mouseWheelScreenRowClearDelay: 150 scrollSensitivity: 0.4 - scrollViewMeasurementRequested: false + heightAndWidthMeasurementRequested: false measureLineHeightAndDefaultCharWidthWhenShown: false remeasureCharacterWidthsIfVisibleAfterNextUpdate: false inputEnabled: true @@ -186,7 +186,7 @@ EditorComponent = React.createClass editor.setVisible(true) @measureLineHeightAndDefaultCharWidth() - @measureScrollView() + @measureHeightAndWidth() @measureScrollbars() componentWillUnmount: -> @@ -362,7 +362,7 @@ EditorComponent = React.createClass scrollViewNode = @refs.scrollView.getDOMNode() scrollViewNode.addEventListener 'scroll', @onScrollViewScroll - window.addEventListener 'resize', @requestScrollViewMeasurement + window.addEventListener 'resize', @requestHeightAndWidthMeasurement @listenForIMEEvents() @@ -766,23 +766,23 @@ EditorComponent = React.createClass @visible = @isVisible() if @visible if wasVisible - @measureScrollView() + @measureHeightAndWidth() else @requestUpdate() - requestScrollViewMeasurement: -> - return if @scrollViewMeasurementRequested + requestHeightAndWidthMeasurement: -> + return if @heightAndWidthMeasurementRequested - @scrollViewMeasurementRequested = true + @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: -> + measureHeightAndWidth: -> return unless @isMounted() {editor, parentView} = @props From 99704517bbedf91e6d306075694aea8c3297caf5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Jul 2014 12:44:18 -0600 Subject: [PATCH 14/24] Remove animation frame batching of mousewheel events This doesn't seem to adversely affect the scroll experience, and it's much simpler. I want to avoid preventing the default action of mousewheel events if they don't actually lead to scrolling, and making the behavior synchronous will make that a lot easier. --- spec/editor-component-spec.coffee | 11 +++++++++++ src/editor-component.coffee | 16 +++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index d7acac7b3..00f71c3ed 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -1581,30 +1581,36 @@ describe "EditorComponent", -> expect(horizontalScrollbarNode.scrollLeft).toBe 0 componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10)) + runSetImmediateCallbacks() expect(verticalScrollbarNode.scrollTop).toBe 10 expect(horizontalScrollbarNode.scrollLeft).toBe 0 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) componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10)) + runSetImmediateCallbacks() expect(horizontalScrollbarNode.scrollLeft).toBe 0 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') 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) 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", -> @@ -1617,6 +1623,7 @@ describe "EditorComponent", -> wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) Object.defineProperty(wheelEvent, 'target', get: -> lineNode) componentNode.dispatchEvent(wheelEvent) + runSetImmediateCallbacks() expect(componentNode.contains(lineNode)).toBe true @@ -1629,6 +1636,7 @@ describe "EditorComponent", -> wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 10, wheelDeltaY: 0) Object.defineProperty(wheelEvent, 'target', get: -> lineNode) componentNode.dispatchEvent(wheelEvent) + runSetImmediateCallbacks() expect(component.mouseWheelScreenRow).toBe null @@ -1641,6 +1649,7 @@ describe "EditorComponent", -> wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 10) Object.defineProperty(wheelEvent, 'target', get: -> lineNode) componentNode.dispatchEvent(wheelEvent) + runSetImmediateCallbacks() expect(editor.getScrollTop()).toBe 0 @@ -1657,6 +1666,7 @@ describe "EditorComponent", -> wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 100) # goes nowhere, we're already at scrollTop 0 Object.defineProperty(wheelEvent, 'target', get: -> lineNode) componentNode.dispatchEvent(wheelEvent) + runSetImmediateCallbacks() expect(component.mouseWheelScreenRow).toBe 0 editor.insertText("hello") @@ -1673,6 +1683,7 @@ describe "EditorComponent", -> wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) Object.defineProperty(wheelEvent, 'target', get: -> lineNumberNode) componentNode.dispatchEvent(wheelEvent) + runSetImmediateCallbacks() expect(componentNode.contains(lineNumberNode)).toBe true diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 064663a89..ea281a24d 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -37,8 +37,6 @@ EditorComponent = React.createClass gutterWidth: 0 refreshingScrollbars: false measuringScrollbars: true - pendingVerticalScrollDelta: 0 - pendingHorizontalScrollDelta: 0 mouseWheelScreenRow: null mouseWheelScreenRowClearDelay: 150 scrollSensitivity: 0.4 @@ -570,27 +568,19 @@ EditorComponent = React.createClass 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) + editor.setScrollLeft(editor.getScrollLeft() - Math.round(wheelDeltaX * @scrollSensitivity)) 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 + editor.setScrollTop(editor.getScrollTop() - Math.round(wheelDeltaY * @scrollSensitivity)) onScrollViewScroll: -> if @isMounted() From 0346e5809aab88d3c14e232a52e326a409982e97 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Jul 2014 13:06:14 -0600 Subject: [PATCH 15/24] Only prevent default on mousewheel events if editor actually scrolls This prevents mini editors from capturing scroll events. --- spec/editor-component-spec.coffee | 36 +++++++++++++++++++++++++++++++ src/editor-component.coffee | 9 +++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 00f71c3ed..c7ef4a726 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -1687,6 +1687,42 @@ describe "EditorComponent", -> 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 diff --git a/src/editor-component.coffee b/src/editor-component.coffee index ea281a24d..993dbe8c7 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -567,20 +567,23 @@ EditorComponent = React.createClass @pendingScrollLeft = null onMouseWheel: (event) -> - event.preventDefault() {editor} = @props # Only scroll in one direction at a time {wheelDeltaX, wheelDeltaY} = event if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY) # Scrolling horizontally - editor.setScrollLeft(editor.getScrollLeft() - Math.round(wheelDeltaX * @scrollSensitivity)) + previousScrollLeft = editor.getScrollLeft() + editor.setScrollLeft(previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity)) + event.preventDefault() unless previousScrollLeft is editor.getScrollLeft() else # Scrolling vertically @mouseWheelScreenRow = @screenRowForNode(event.target) @clearMouseWheelScreenRowAfterDelay ?= debounce(@clearMouseWheelScreenRow, @mouseWheelScreenRowClearDelay) @clearMouseWheelScreenRowAfterDelay() - editor.setScrollTop(editor.getScrollTop() - Math.round(wheelDeltaY * @scrollSensitivity)) + previousScrollTop = editor.getScrollTop() + editor.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity)) + event.preventDefault() unless previousScrollTop is editor.getScrollTop() onScrollViewScroll: -> if @isMounted() From 3206fdce9ea52bfc0ee921787ba5ae46a02faba1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Jul 2014 13:14:44 -0600 Subject: [PATCH 16/24] Add ReactEditorView::setPlaceholderText shim --- src/react-editor-view.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index ecb5b24b7..f06a93238 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -223,3 +223,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 From c4177aba3ec8e21b56b0571004f32fc6f580d794 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Jul 2014 13:15:05 -0600 Subject: [PATCH 17/24] Handle 'attributes' param to ReactEditorView --- src/react-editor-view.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index f06a93238..568388043 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -7,7 +7,10 @@ EditorComponent = require './editor-component' module.exports = class ReactEditorView extends View - @content: -> @div class: 'editor react' + @content: (params) -> + attributes = params.attributes ? {} + attributes.class = 'editor react' + @div attributes focusOnAttach: false From 4a8ac85ffb5bfaad24e32141e9d8367cf865835d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Jul 2014 13:22:38 -0600 Subject: [PATCH 18/24] Restrict ReactEditorView::getPane implementation --- src/react-editor-view.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index 568388043..413ad6d75 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -140,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? From ca1220a682f2ff75fcdf0ed7b1aab301b85d7a42 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 17 Jul 2014 17:56:06 -0700 Subject: [PATCH 19/24] Delay initial measurement until the editor becomes visible Previously, these measurements were always performed when the editor component was mounted. This didn't work in situations where the component was mounted in a non-visible state. This commit includes a visibility check in the resize polling we were already doing, kicking off the measurement process as soon as the editor is visible. --- spec/editor-component-spec.coffee | 24 +++++++++++++++++++ src/cursors-component.coffee | 4 ++-- src/editor-component.coffee | 39 +++++++++++++++++++------------ src/highlights-component.coffee | 2 +- src/lines-component.coffee | 10 +++++--- 5 files changed, 58 insertions(+), 21 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index c7ef4a726..69ee93c4c 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -1529,6 +1529,8 @@ describe "EditorComponent", -> 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 = componentNode.querySelector('.scrollbar-corner') @@ -1858,6 +1860,28 @@ describe "EditorComponent", -> 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() 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 993dbe8c7..e4ffa3692 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -58,7 +58,7 @@ EditorComponent = React.createClass style = {fontSize, fontFamily} style.lineHeight = lineHeight unless mini - if @isMounted() + if @performedInitialMeasurement renderedRowRange = @getRenderedRowRange() [renderedStartRow, renderedEndRow] = renderedRowRange cursorPixelRects = @getCursorPixelRects(renderedRowRange) @@ -109,7 +109,8 @@ EditorComponent = React.createClass CursorsComponent { scrollTop, scrollLeft, cursorPixelRects, cursorBlinkPeriod, cursorBlinkResumeDelay, - lineHeightInPixels, defaultCharWidth, @scopedCharacterWidthsChangeCount, @useHardwareAcceleration + lineHeightInPixels, defaultCharWidth, @scopedCharacterWidthsChangeCount, @useHardwareAcceleration, + @performedInitialMeasurement } LinesComponent { ref: 'lines', @@ -117,7 +118,7 @@ EditorComponent = React.createClass showIndentGuide, renderedRowRange, @pendingChanges, scrollTop, scrollLeft, @scrollingVertically, scrollHeight, scrollWidth, mouseWheelScreenRow, invisibles, visible, scrollViewHeight, @scopedCharacterWidthsChangeCount, lineWidth, @useHardwareAcceleration, - placeholderText + placeholderText, @performedInitialMeasurement } ScrollbarComponent @@ -181,11 +182,8 @@ EditorComponent = React.createClass @subscribe atom.themes, 'stylesheet-added stylsheet-removed', @onStylesheetsChanged @subscribe scrollbarStyle.changes, @refreshScrollbars - editor.setVisible(true) - - @measureLineHeightAndDefaultCharWidth() - @measureHeightAndWidth() - @measureScrollbars() + if @visible = @isVisible() + @performInitialMeasurement() componentWillUnmount: -> @props.parentView.trigger 'editor:will-be-removed', [@props.parentView] @@ -209,9 +207,20 @@ EditorComponent = React.createClass @props.parentView.trigger 'editor:display-updated' @visible = @isVisible() - @measureScrollbars() if @measuringScrollbars - @measureLineHeightAndDefaultCharWidthIfNeeded(prevState) - @remeasureCharacterWidthsIfNeeded(prevState) + 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 @@ -740,7 +749,7 @@ EditorComponent = React.createClass isVisible: -> node = @getDOMNode() - node.offsetHeight > 0 and node.offsetWidth > 0 + node.offsetHeight > 0 or node.offsetWidth > 0 pauseDOMPolling: -> @domPollingPaused = true @@ -756,12 +765,11 @@ EditorComponent = React.createClass return if @domPollingPaused or not @isMounted() wasVisible = @visible - @visible = @isVisible() - if @visible + if @visible = @isVisible() if wasVisible @measureHeightAndWidth() else - @requestUpdate() + @performInitialMeasurement() requestHeightAndWidthMeasurement: -> return if @heightAndWidthMeasurementRequested @@ -830,6 +838,7 @@ EditorComponent = React.createClass @requestUpdate() measureScrollbars: -> + return unless @visible @measuringScrollbars = false {editor} = @props 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 eeee69f01..9704aaf35 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -16,7 +16,9 @@ LinesComponent = React.createClass displayName: 'LinesComponent' render: -> - if @isMounted() + {performedInitialMeasurement} = @props + + if performedInitialMeasurement {editor, highlightDecorations, scrollHeight, scrollWidth, placeholderText} = @props {lineHeightInPixels, defaultCharWidth, scrollViewHeight, scopedCharacterWidthsChangeCount} = @props style = @@ -28,7 +30,7 @@ LinesComponent = React.createClass # background to avoid sub-pixel anti-aliasing problems on the GPU div {className: 'lines editor-colors', style}, div className: 'placeholder-text', placeholderText if placeholderText? - HighlightsComponent({editor, highlightDecorations, lineHeightInPixels, defaultCharWidth, scopedCharacterWidthsChangeCount}) + HighlightsComponent({editor, highlightDecorations, lineHeightInPixels, defaultCharWidth, scopedCharacterWidthsChangeCount, performedInitialMeasurement}) getTransform: -> {scrollTop, scrollLeft, useHardwareAcceleration} = @props @@ -50,10 +52,12 @@ LinesComponent = React.createClass 'renderedRowRange', 'lineDecorations', 'highlightDecorations', 'lineHeightInPixels', 'defaultCharWidth', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically', 'invisibles', 'visible', 'scrollViewHeight', 'mouseWheelScreenRow', 'scopedCharacterWidthsChangeCount', 'lineWidth', 'useHardwareAcceleration', - 'placeholderText' + 'placeholderText', 'performedInitialMeasurement' ) {renderedRowRange, pendingChanges} = newProps + return false unless renderedRowRange? + [renderedStartRow, renderedEndRow] = renderedRowRange for change in pendingChanges if change.screenDelta is 0 From df7f816c883d5e81e6ae010cd2236e30291e1397 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Jul 2014 13:43:00 -0700 Subject: [PATCH 20/24] Move 'editor-colors' to wrapper view to support padding on mini editors --- src/editor-component.coffee | 2 +- src/react-editor-view.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index e4ffa3692..ac7187017 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -87,7 +87,7 @@ EditorComponent = React.createClass style.height = scrollViewHeight if @autoHeight - className = 'editor-contents editor-colors' + className = 'editor-contents' className += ' is-focused' if focused className += ' has-selection' if hasSelection diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index 413ad6d75..d806f71c8 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -9,7 +9,7 @@ module.exports = class ReactEditorView extends View @content: (params) -> attributes = params.attributes ? {} - attributes.class = 'editor react' + attributes.class = 'editor react editor-colors' @div attributes focusOnAttach: false From 38b286f9896b6371c378a175e3fd5bc7fff8bcb3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Jul 2014 13:50:58 -0700 Subject: [PATCH 21/24] Remove 'editor-colors' class from lines and gutter for mini editors Having the editor-colors class on these elements was causing the theme to be applied to lines in mini editors in the settings view, which caused a black inset box to appear with dark syntax themes. This was added to give the lines an opaque background which was supposed to enable sub pixel anti-aliasing despite being on the GPU, but it didn't seem to be working. Perhaps we can revisit this issue after the Chrome 35 upgrade to see if sub pixel antialiasing works with opaque backgrounds afterward. --- src/gutter-component.coffee | 4 +--- src/lines-component.coffee | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 9dc6fb3e7..826723575 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() diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 9704aaf35..d1b1aba94 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -26,9 +26,7 @@ LinesComponent = React.createClass 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}, + div {className: 'lines', style}, div className: 'placeholder-text', placeholderText if placeholderText? HighlightsComponent({editor, highlightDecorations, lineHeightInPixels, defaultCharWidth, scopedCharacterWidthsChangeCount, performedInitialMeasurement}) From 0c4da92d6b37f51e98789ee551a51fa12a2d2c2c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Jul 2014 14:09:16 -0700 Subject: [PATCH 22/24] Handle hide and show entirely in the wrapper view Enable sync updates and manually invoke the pollDOM function on the component when we call hide/show on the wrapper view. This ensures that we perform initial measurements when showing the editor for the first time. --- src/editor-component.coffee | 10 +--------- src/react-editor-view.coffee | 11 +++++++++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index ac7187017..fa3939cb6 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -22,7 +22,7 @@ EditorComponent = React.createClass statics: performSyncUpdates: false - visible: true + visible: false autoHeight: false pendingScrollTop: null pendingScrollLeft: null @@ -895,14 +895,6 @@ EditorComponent = React.createClass node = node.parentNode null - hide: -> - @visible = false - - show: -> - unless @visible - @visible = true - @forceUpdate() - getFontSize: -> @state.fontSize diff --git a/src/react-editor-view.coffee b/src/react-editor-view.coffee index d806f71c8..80f21ef57 100644 --- a/src/react-editor-view.coffee +++ b/src/react-editor-view.coffee @@ -150,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() From fb4361e976ba46fe7a803f0cdf988f352bf4d3e2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Jul 2014 14:13:05 -0700 Subject: [PATCH 23/24] Guard React mini-editors with core.useReactMiniEditors feature flag I'm not adding a default for this one so it won't show up in the settings view unless the user sets it in their config explicitly. I'm hoping it won't need to be flagged for very long. --- exports/atom.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/exports/atom.coffee b/exports/atom.coffee index b852d68a7..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/react-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' From a68b9a793f5cb61b0195d14053d0243efcd9242d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Jul 2014 14:28:48 -0700 Subject: [PATCH 24/24] Don't update the GutterComponent if there's no renderedRowRange --- src/gutter-component.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index 826723575..223b5a077 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -51,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