From 64f3938f5cc710b92c7d5a1e14a52dea12e76927 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 19 Jun 2014 16:48:40 -0600 Subject: [PATCH 1/5] Batch all editor updates together automatically via process.nextTick --- spec/editor-component-spec.coffee | 418 +++++++++++++++++++----------- src/editor-component.coffee | 50 ++-- src/editor.coffee | 21 +- src/lines-component.coffee | 14 +- 4 files changed, 290 insertions(+), 213 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 5ed21aa43..1892a37ec 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -6,7 +6,7 @@ nbsp = String.fromCharCode(160) describe "EditorComponent", -> [contentNode, editor, wrapperView, component, node, verticalScrollbarNode, horizontalScrollbarNode] = [] - [lineHeightInPixels, charWidth, delayAnimationFrames, nextAnimationFrame, lineOverdrawMargin] = [] + [lineHeightInPixels, charWidth, delayAnimationFrames, nextAnimationFrame, nextTick, lineOverdrawMargin] = [] beforeEach -> lineOverdrawMargin = 2 @@ -20,6 +20,7 @@ describe "EditorComponent", -> delayAnimationFrames = false nextAnimationFrame = -> throw new Error('No animation frame requested') + spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> if delayAnimationFrames nextAnimationFrame = fn @@ -30,11 +31,25 @@ describe "EditorComponent", -> atom.project.open('sample.js').then (o) -> editor = o runs -> + nextTickFns = [] + nextTick = -> + if nextTickFns.length is 0 + throw new Error("nextTick not requested") + else + fns = nextTickFns.slice() + nextTickFns.length = 0 + fn() for fn in fns + + spyOn(process, 'nextTick').andCallFake (fn) -> nextTickFns.push(fn) + contentNode = document.querySelector('#jasmine-content') contentNode.style.width = '1000px' wrapperView = new ReactEditorView(editor, {lineOverdrawMargin}) wrapperView.attachToDom() + nextTick() + nextTick() + {component} = wrapperView component.setLineHeight(1.3) component.setFontSize(20) @@ -48,6 +63,7 @@ describe "EditorComponent", -> node.style.height = editor.getLineCount() * lineHeightInPixels + 'px' node.style.width = '1000px' component.measureScrollView() + nextTick() afterEach -> contentNode.style.width = '' @@ -56,6 +72,7 @@ describe "EditorComponent", -> it "renders the currently-visible lines plus the overdraw margin", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() + nextTick() linesNode = node.querySelector('.lines') expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" @@ -67,6 +84,7 @@ describe "EditorComponent", -> verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + nextTick() 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 @@ -77,12 +95,16 @@ describe "EditorComponent", -> it "updates the top position of subsequent lines when lines are inserted or removed", -> editor.getBuffer().deleteRows(0, 1) + nextTick() + lineNodes = node.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 editor.getBuffer().insert([0, 0], '\n\n') + nextTick() + lineNodes = node.querySelectorAll('.line') expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels @@ -93,6 +115,7 @@ describe "EditorComponent", -> it "updates the top position of lines when the line height changes", -> initialLineHeightInPixels = editor.getLineHeightInPixels() component.setLineHeight(2) + nextTick() newLineHeightInPixels = editor.getLineHeightInPixels() expect(newLineHeightInPixels).not.toBe initialLineHeightInPixels @@ -101,6 +124,7 @@ describe "EditorComponent", -> it "updates the top position of lines when the font size changes", -> initialLineHeightInPixels = editor.getLineHeightInPixels() component.setFontSize(10) + nextTick() newLineHeightInPixels = editor.getLineHeightInPixels() expect(newLineHeightInPixels).not.toBe initialLineHeightInPixels @@ -113,6 +137,7 @@ describe "EditorComponent", -> initialLineHeightInPixels = editor.getLineHeightInPixels() component.setFontFamily('sans-serif') + nextTick() expect(linesComponent.measureLineHeightAndDefaultCharWidth).toHaveBeenCalled() newLineHeightInPixels = editor.getLineHeightInPixels() @@ -123,6 +148,7 @@ describe "EditorComponent", -> editor.setText('') node.style.height = '300px' component.measureScrollView() + nextTick() linesNode = node.querySelector('.lines') expect(linesNode.offsetHeight).toBe 300 @@ -142,31 +168,38 @@ describe "EditorComponent", -> it "re-renders the lines when the showInvisibles config option changes", -> editor.setText " a line with tabs\tand spaces " - + nextTick() expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}" + atom.config.set("editor.showInvisibles", false) expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces " + atom.config.set("editor.showInvisibles", true) expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}" it "displays spaces, tabs, and newlines as visible characters", -> editor.setText " a line with tabs\tand spaces " + nextTick() expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}" it "displays newlines as their own token outside of the other tokens' scopes", -> editor.setText "var" + nextTick() expect(component.lineNodeForScreenRow(0).innerHTML).toBe "var#{invisibles.eol}" it "displays trailing carriage returns using a visible, non-empty value", -> editor.setText "a line that ends with a carriage return\r\n" + nextTick() expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that ends with a carriage return#{invisibles.cr}#{invisibles.eol}" describe "when soft wrapping is enabled", -> beforeEach -> editor.setText "a line that wraps " editor.setSoftWrap(true) + nextTick() node.style.width = 16 * charWidth + 'px' component.measureScrollView() + nextTick() it "doesn't show end of line invisibles at the end of wrapped lines", -> expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that " @@ -175,6 +208,7 @@ describe "EditorComponent", -> describe "when indent guides are enabled", -> beforeEach -> component.setShowIndentGuide(true) + nextTick() it "adds an 'indent-guide' class to spans comprising the leading whitespace", -> line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) @@ -191,6 +225,7 @@ describe "EditorComponent", -> it "renders leading whitespace spans with the 'indent-guide' class for empty lines", -> editor.getBuffer().insert([1, Infinity], '\n') + nextTick() line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) @@ -204,6 +239,8 @@ describe "EditorComponent", -> it "renders indent guides correctly on lines containing only whitespace", -> editor.getBuffer().insert([1, Infinity], '\n ') + nextTick() + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe 3 expect(line2LeafNodes[0].textContent).toBe ' ' @@ -215,6 +252,8 @@ describe "EditorComponent", -> it "does not render indent guides in trailing whitespace for lines containing non whitespace characters", -> editor.getBuffer().setText " hi " + nextTick() + line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(line0LeafNodes[0].textContent).toBe ' ' expect(line0LeafNodes[0].classList.contains('indent-guide')).toBe true @@ -230,6 +269,7 @@ describe "EditorComponent", -> describe "when the buffer contains null bytes", -> it "excludes the null byte from character measurement", -> editor.setText("a\0b") + nextTick() expect(editor.pixelPositionForScreenPosition([0, Infinity]).left).toEqual 2 * charWidth describe "when there is a fold", -> @@ -238,10 +278,12 @@ describe "EditorComponent", -> expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() editor.foldBufferRow(4) + nextTick() foldedLineNode = component.lineNodeForScreenRow(4) expect(foldedLineNode.querySelector('.fold-marker')).toBeTruthy() editor.unfoldBufferRow(4) + nextTick() foldedLineNode = component.lineNodeForScreenRow(4) expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() @@ -255,11 +297,12 @@ describe "EditorComponent", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], invalidate: 'inside') decoration = {type: 'line', class: 'someclass'} editor.addDecorationForMarker(marker, decoration) - waitsFor -> not component.decorationChangedImmediate? + nextTick() it "does not render off-screen lines with line number classes until they are with in the rendered row range", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() + nextTick() expect(component.lineNodeForScreenRow(9)).not.toBeDefined() @@ -269,6 +312,7 @@ describe "EditorComponent", -> verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + nextTick() expect(lineHasClass(9, 'fancy-class')).toBe true expect(lineHasClass(9, 'nope-class')).toBe false @@ -281,51 +325,47 @@ describe "EditorComponent", -> it "removes line classes when a decoration's marker is invalidated", -> editor.getBuffer().insert([3, 2], 'n') + nextTick() - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(marker.isValid()).toBe false - expect(lineHasClass(1, 'someclass')).toBe false - expect(lineHasClass(2, 'someclass')).toBe false - expect(lineHasClass(3, 'someclass')).toBe false - expect(lineHasClass(4, 'someclass')).toBe false - editor.getBuffer().undo() + expect(marker.isValid()).toBe false + expect(lineHasClass(1, 'someclass')).toBe false + expect(lineHasClass(2, 'someclass')).toBe false + expect(lineHasClass(3, 'someclass')).toBe false + expect(lineHasClass(4, 'someclass')).toBe false - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(marker.isValid()).toBe true - expect(lineHasClass(1, 'someclass')).toBe false - expect(lineHasClass(2, 'someclass')).toBe true - expect(lineHasClass(3, 'someclass')).toBe true - expect(lineHasClass(4, 'someclass')).toBe false + editor.getBuffer().undo() + nextTick() + + expect(marker.isValid()).toBe true + expect(lineHasClass(1, 'someclass')).toBe false + expect(lineHasClass(2, 'someclass')).toBe true + expect(lineHasClass(3, 'someclass')).toBe true + expect(lineHasClass(4, 'someclass')).toBe false it "removes the classes and unsubscribes from the marker when decoration is removed", -> editor.removeDecorationForMarker(marker, decoration) + nextTick() - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(lineHasClass(1, 'someclass')).toBe false - expect(lineHasClass(2, 'someclass')).toBe false - expect(lineHasClass(3, 'someclass')).toBe false - expect(lineHasClass(4, 'someclass')).toBe false + expect(lineHasClass(1, 'someclass')).toBe false + expect(lineHasClass(2, 'someclass')).toBe false + expect(lineHasClass(3, 'someclass')).toBe false + expect(lineHasClass(4, 'someclass')).toBe false editor.getBuffer().insert([0, 0], '\n') + nextTick() - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(lineHasClass(2, 'someclass')).toBe false - expect(lineHasClass(3, 'someclass')).toBe false + expect(lineHasClass(2, 'someclass')).toBe false + expect(lineHasClass(3, 'someclass')).toBe false it "removes the line number classes when the decoration's marker is destroyed", -> marker.destroy() + nextTick() - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(lineHasClass(1, 'someclass')).toBe false - expect(lineHasClass(2, 'someclass')).toBe false - expect(lineHasClass(3, 'someclass')).toBe false - expect(lineHasClass(4, 'someclass')).toBe false + expect(lineHasClass(1, 'someclass')).toBe false + expect(lineHasClass(2, 'someclass')).toBe false + expect(lineHasClass(3, 'someclass')).toBe false + expect(lineHasClass(4, 'someclass')).toBe false describe "gutter rendering", -> [gutter] = [] @@ -343,6 +383,7 @@ describe "EditorComponent", -> it "renders the currently-visible line numbers", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() + nextTick() expect(node.querySelectorAll('.line-number').length).toBe 6 + 2 + 1 # line overdraw margin below + dummy line number expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" @@ -350,17 +391,18 @@ describe "EditorComponent", -> verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + nextTick() expect(node.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 - return expect(component.lineNumberNodeForScreenRow(7).textContent).toBe "#{nbsp}8" expect(component.lineNumberNodeForScreenRow(7).offsetTop).toBe 7 * lineHeightInPixels it "updates the translation of subsequent line numbers when lines are inserted or removed", -> editor.getBuffer().insert([0, 0], '\n\n') + nextTick() lineNumberNodes = node.querySelectorAll('.line-number') expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 @@ -370,6 +412,8 @@ describe "EditorComponent", -> expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels editor.getBuffer().insert([0, 0], '\n\n') + nextTick() + expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels @@ -383,6 +427,7 @@ describe "EditorComponent", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 30 * charWidth + 'px' component.measureScrollView() + nextTick() expect(node.querySelectorAll('.line-number').length).toBe 6 + lineOverdrawMargin + 1 # 1 dummy line node expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" @@ -394,6 +439,7 @@ describe "EditorComponent", -> it "pads line numbers to be right-justified based on the maximum number of line number digits", -> editor.getBuffer().setText([1..10].join('\n')) + nextTick() for screenRow in [0..8] expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" @@ -403,12 +449,14 @@ describe "EditorComponent", -> # Removes padding when the max number of digits goes down editor.getBuffer().delete([[1, 0], [2, 0]]) + nextTick() for screenRow in [0..8] expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{screenRow + 1}" expect(gutterNode.offsetWidth).toBeLessThan initialGutterWidth # Increases padding when the max number of digits goes up editor.getBuffer().insert([0, 0], '\n\n') + nextTick() for screenRow in [0..8] expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" @@ -417,6 +465,7 @@ describe "EditorComponent", -> it "renders the .line-numbers div at the full height of the editor even if it's taller than its content", -> node.style.height = node.offsetHeight + 100 + 'px' component.measureScrollView() + nextTick() expect(node.querySelector('.line-numbers').offsetHeight).toBe node.offsetHeight describe "fold decorations", -> @@ -431,6 +480,7 @@ describe "EditorComponent", -> it "updates the foldable class on the correct line numbers when the foldable positions change", -> editor.getBuffer().insert([0, 0], '\n') + nextTick() expect(lineNumberHasClass(0, 'foldable')).toBe false expect(lineNumberHasClass(1, 'foldable')).toBe true expect(lineNumberHasClass(2, 'foldable')).toBe true @@ -443,20 +493,25 @@ describe "EditorComponent", -> expect(lineNumberHasClass(11, 'foldable')).toBe false editor.getBuffer().insert([11, 44], '\n fold me') + nextTick() expect(lineNumberHasClass(11, 'foldable')).toBe true editor.undo() + nextTick() expect(lineNumberHasClass(11, 'foldable')).toBe false it "adds, updates and removes the folded class on the correct line number nodes", -> editor.foldBufferRow(4) + nextTick() expect(lineNumberHasClass(4, 'folded')).toBe true editor.getBuffer().insert([0, 0], '\n') + nextTick() expect(lineNumberHasClass(4, 'folded')).toBe false expect(lineNumberHasClass(5, 'folded')).toBe true editor.unfoldBufferRow(5) + nextTick() expect(lineNumberHasClass(5, 'folded')).toBe false describe "mouse interactions with fold indicators", -> @@ -473,19 +528,20 @@ describe "EditorComponent", -> lineNumber = component.lineNumberNodeForScreenRow(1) target = lineNumber.querySelector('.icon-right') - target.dispatchEvent(buildClickEvent(target)) + nextTick() expect(lineNumberHasClass(1, 'folded')).toBe true lineNumber = component.lineNumberNodeForScreenRow(1) target = lineNumber.querySelector('.icon-right') - target.dispatchEvent(buildClickEvent(target)) + nextTick() expect(lineNumberHasClass(1, 'folded')).toBe false it "does not fold when the line number node is clicked", -> lineNumber = component.lineNumberNodeForScreenRow(1) lineNumber.dispatchEvent(buildClickEvent(lineNumber)) + nextTick() expect(lineNumberHasClass(1, 'folded')).toBe false describe "cursor-line decorations", -> @@ -495,9 +551,11 @@ describe "EditorComponent", -> it "modifies the cursor-line decoration when the cursor moves", -> cursor.setScreenPosition([0, 0]) + nextTick() expect(lineNumberHasClass(0, 'cursor-line')).toBe true cursor.setScreenPosition([1, 0]) + nextTick() expect(lineNumberHasClass(0, 'cursor-line')).toBe false expect(lineNumberHasClass(1, 'cursor-line')).toBe true @@ -505,17 +563,20 @@ describe "EditorComponent", -> cursor.setScreenPosition([2, 0]) cursor2 = editor.addCursorAtScreenPosition([8, 0]) cursor3 = editor.addCursorAtScreenPosition([10, 0]) + nextTick() expect(lineNumberHasClass(2, 'cursor-line')).toBe true expect(lineNumberHasClass(8, 'cursor-line')).toBe true expect(lineNumberHasClass(10, 'cursor-line')).toBe true cursor2.destroy() + nextTick() expect(lineNumberHasClass(2, 'cursor-line')).toBe true expect(lineNumberHasClass(8, 'cursor-line')).toBe false expect(lineNumberHasClass(10, 'cursor-line')).toBe true cursor3.destroy() + nextTick() expect(lineNumberHasClass(2, 'cursor-line')).toBe true expect(lineNumberHasClass(8, 'cursor-line')).toBe false expect(lineNumberHasClass(10, 'cursor-line')).toBe false @@ -523,6 +584,7 @@ describe "EditorComponent", -> it "adds cursor-line decorations to multiple lines when a selection is performed", -> cursor.setScreenPosition([1, 5]) editor.selectDown(2) + nextTick() expect(lineNumberHasClass(0, 'cursor-line')).toBe false expect(lineNumberHasClass(1, 'cursor-line')).toBe true expect(lineNumberHasClass(2, 'cursor-line')).toBe true @@ -532,6 +594,7 @@ describe "EditorComponent", -> it "does not render a cursor-line decoration for the last line of a multi-line selection of the selection ends at column 0", -> cursor.setScreenPosition([1, 0]) editor.selectDown(2) + nextTick() expect(lineNumberHasClass(0, 'cursor-line')).toBe false expect(lineNumberHasClass(1, 'cursor-line')).toBe true expect(lineNumberHasClass(2, 'cursor-line')).toBe true @@ -543,12 +606,12 @@ describe "EditorComponent", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], invalidate: 'inside') decoration = {type: 'gutter', class: 'someclass'} editor.addDecorationForMarker(marker, decoration) - waitsFor -> not component.decorationChangedImmediate? + nextTick() it "does not render off-screen lines with line number classes until they are with in the rendered row range", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() - + nextTick() expect(component.lineNumberNodeForScreenRow(9)).not.toBeDefined() marker = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]], invalidate: 'inside') @@ -557,6 +620,7 @@ describe "EditorComponent", -> verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + nextTick() expect(lineNumberHasClass(9, 'fancy-class')).toBe true expect(lineNumberHasClass(9, 'nope-class')).toBe false @@ -564,16 +628,14 @@ describe "EditorComponent", -> it "renders classes on correct screen lines when the user folds a block of code", -> marker = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]], invalidate: 'inside') editor.addDecorationForMarker(marker, decoration) + nextTick() + expect(lineNumberForBufferRowHasClass(9, 'someclass')).toBe true - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(lineNumberForBufferRowHasClass(9, 'someclass')).toBe true - editor.foldBufferRow(5) - editor.removeDecorationForMarker(marker, decoration) - - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(lineNumberForBufferRowHasClass(9, 'someclass')).toBe false + editor.foldBufferRow(5) + nextTick() # TODO: Removing this nextTick causes the spec to fail because of flaws in decoration updating + editor.removeDecorationForMarker(marker, decoration) + nextTick() + expect(lineNumberForBufferRowHasClass(9, 'someclass')).toBe false it "updates line number classes when the marker moves", -> expect(lineNumberHasClass(1, 'someclass')).toBe false @@ -582,71 +644,56 @@ describe "EditorComponent", -> expect(lineNumberHasClass(4, 'someclass')).toBe false editor.getBuffer().insert([0, 0], '\n') + nextTick() + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe true + expect(lineNumberHasClass(4, 'someclass')).toBe true + expect(lineNumberHasClass(5, 'someclass')).toBe false - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(lineNumberHasClass(2, 'someclass')).toBe false - expect(lineNumberHasClass(3, 'someclass')).toBe true - expect(lineNumberHasClass(4, 'someclass')).toBe true - expect(lineNumberHasClass(5, 'someclass')).toBe false - - editor.getBuffer().deleteRows(0, 1) - - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(lineNumberHasClass(0, 'someclass')).toBe false - expect(lineNumberHasClass(1, 'someclass')).toBe true - expect(lineNumberHasClass(2, 'someclass')).toBe true - expect(lineNumberHasClass(3, 'someclass')).toBe false + editor.getBuffer().deleteRows(0, 1) + nextTick() + expect(lineNumberHasClass(0, 'someclass')).toBe false + expect(lineNumberHasClass(1, 'someclass')).toBe true + expect(lineNumberHasClass(2, 'someclass')).toBe true + expect(lineNumberHasClass(3, 'someclass')).toBe false it "removes line number classes when a decoration's marker is invalidated", -> editor.getBuffer().insert([3, 2], 'n') + nextTick() + expect(marker.isValid()).toBe false + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + expect(lineNumberHasClass(4, 'someclass')).toBe false - waitsFor -> not component.decorationChangedImmediate? - runs -> - - expect(marker.isValid()).toBe false - expect(lineNumberHasClass(1, 'someclass')).toBe false - expect(lineNumberHasClass(2, 'someclass')).toBe false - expect(lineNumberHasClass(3, 'someclass')).toBe false - expect(lineNumberHasClass(4, 'someclass')).toBe false - - editor.getBuffer().undo() - - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(marker.isValid()).toBe true - expect(lineNumberHasClass(1, 'someclass')).toBe false - expect(lineNumberHasClass(2, 'someclass')).toBe true - expect(lineNumberHasClass(3, 'someclass')).toBe true - expect(lineNumberHasClass(4, 'someclass')).toBe false + editor.getBuffer().undo() + nextTick() + expect(marker.isValid()).toBe true + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe true + expect(lineNumberHasClass(3, 'someclass')).toBe true + expect(lineNumberHasClass(4, 'someclass')).toBe false it "removes the classes and unsubscribes from the marker when decoration is removed", -> editor.removeDecorationForMarker(marker, decoration) - - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(lineNumberHasClass(1, 'someclass')).toBe false - expect(lineNumberHasClass(2, 'someclass')).toBe false - expect(lineNumberHasClass(3, 'someclass')).toBe false - expect(lineNumberHasClass(4, 'someclass')).toBe false + nextTick() + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + expect(lineNumberHasClass(4, 'someclass')).toBe false editor.getBuffer().insert([0, 0], '\n') - - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(lineNumberHasClass(2, 'someclass')).toBe false - expect(lineNumberHasClass(3, 'someclass')).toBe false + nextTick() + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false it "removes the line number classes when the decoration's marker is destroyed", -> marker.destroy() - - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(lineNumberHasClass(1, 'someclass')).toBe false - expect(lineNumberHasClass(2, 'someclass')).toBe false - expect(lineNumberHasClass(3, 'someclass')).toBe false - expect(lineNumberHasClass(4, 'someclass')).toBe false + nextTick() + expect(lineNumberHasClass(1, 'someclass')).toBe false + expect(lineNumberHasClass(2, 'someclass')).toBe false + expect(lineNumberHasClass(3, 'someclass')).toBe false + expect(lineNumberHasClass(4, 'someclass')).toBe false describe "when soft wrapping is enabled", -> beforeEach -> @@ -654,24 +701,21 @@ describe "EditorComponent", -> editor.setSoftWrap(true) node.style.width = 16 * charWidth + 'px' component.measureScrollView() + nextTick() it "applies decoration only to the first row when marker range does not wrap", -> marker = editor.displayBuffer.markBufferRange([[0, 0], [0, 0]]) editor.addDecorationForMarker(marker, type: 'gutter', class: 'someclass') - - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(lineNumberHasClass(0, 'someclass')).toBe true - expect(lineNumberHasClass(1, 'someclass')).toBe false + nextTick() + expect(lineNumberHasClass(0, 'someclass')).toBe true + expect(lineNumberHasClass(1, 'someclass')).toBe false it "applies decoration to both rows when marker wraps", -> marker = editor.displayBuffer.markBufferRange([[0, 0], [0, Infinity]]) editor.addDecorationForMarker(marker, type: 'gutter', class: 'someclass') - - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(lineNumberHasClass(0, 'someclass')).toBe true - expect(lineNumberHasClass(1, 'someclass')).toBe true + nextTick() + expect(lineNumberHasClass(0, 'someclass')).toBe true + expect(lineNumberHasClass(1, 'someclass')).toBe true describe "cursor rendering", -> it "renders the currently visible cursors, translated relative to the scroll position", -> @@ -681,6 +725,7 @@ describe "EditorComponent", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 20 * lineHeightInPixels + 'px' component.measureScrollView() + nextTick() cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 @@ -690,6 +735,7 @@ describe "EditorComponent", -> cursor2 = editor.addCursorAtScreenPosition([8, 11]) cursor3 = editor.addCursorAtScreenPosition([4, 10]) + nextTick() cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 @@ -701,6 +747,7 @@ describe "EditorComponent", -> verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) horizontalScrollbarNode.scrollLeft = 3.5 * charWidth horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + nextTick() cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 @@ -708,6 +755,7 @@ describe "EditorComponent", -> expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{(10 - 3.5) * charWidth}px, #{(4 - 4.5) * lineHeightInPixels}px, 0px)" cursor3.destroy() + nextTick() cursorNodes = node.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)" @@ -715,6 +763,7 @@ describe "EditorComponent", -> it "accounts for character widths when positioning cursors", -> atom.config.set('editor.fontFamily', 'sans-serif') editor.setCursorScreenPosition([0, 16]) + nextTick() cursor = node.querySelector('.cursor') cursorRect = cursor.getBoundingClientRect() @@ -730,11 +779,13 @@ describe "EditorComponent", -> it "sets the cursor to the default character width at the end of a line", -> editor.setCursorScreenPosition([0, Infinity]) + nextTick() cursorNode = node.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]) + nextTick() cursorNode = node.querySelector('.cursor') expect(cursorNode.offsetWidth).toBe charWidth @@ -760,6 +811,7 @@ describe "EditorComponent", -> it "does not render cursors that are associated with non-empty selections", -> editor.setSelectedScreenRange([[0, 4], [4, 6]]) editor.addCursorAtScreenPosition([6, 8]) + nextTick() cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 @@ -768,18 +820,21 @@ describe "EditorComponent", -> it "updates cursor positions when the line height changes", -> editor.setCursorBufferPosition([1, 10]) component.setLineHeight(2) + nextTick() cursorNode = node.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) + nextTick() cursorNode = node.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') + nextTick() cursorNode = node.querySelector('.cursor') {left} = editor.pixelPositionForScreenPosition([1, 10]) @@ -795,6 +850,7 @@ describe "EditorComponent", -> it "renders 1 region for 1-line selections", -> # 1-line selection editor.setSelectedScreenRange([[1, 6], [1, 10]]) + nextTick() regions = node.querySelectorAll('.selection .region') expect(regions.length).toBe 1 @@ -806,6 +862,7 @@ describe "EditorComponent", -> it "renders 2 regions for 2-line selections", -> editor.setSelectedScreenRange([[1, 6], [2, 10]]) + nextTick() regions = node.querySelectorAll('.selection .region') expect(regions.length).toBe 2 @@ -823,6 +880,7 @@ describe "EditorComponent", -> it "renders 3 regions for selections with more than 2 lines", -> editor.setSelectedScreenRange([[1, 6], [5, 10]]) + nextTick() regions = node.querySelectorAll('.selection .region') expect(regions.length).toBe 3 @@ -846,6 +904,7 @@ describe "EditorComponent", -> it "does not render empty selections", -> editor.addSelectionForBufferRange([[2, 2], [2, 2]]) + nextTick() expect(editor.getSelection(0).isEmpty()).toBe true expect(editor.getSelection(1).isEmpty()).toBe true @@ -854,12 +913,14 @@ describe "EditorComponent", -> it "updates selections when the line height changes", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setLineHeight(2) + nextTick() selectionNode = node.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) + nextTick() selectionNode = node.querySelector('.region') expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() expect(selectionNode.offsetLeft).toBe 6 * editor.getDefaultCharWidth() @@ -867,6 +928,7 @@ describe "EditorComponent", -> it "updates selections when the font family changes", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontFamily('sans-serif') + nextTick() selectionNode = node.querySelector('.region') expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() expect(selectionNode.offsetLeft).toBe editor.pixelPositionForScreenPosition([1, 6]).left @@ -878,36 +940,37 @@ describe "EditorComponent", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], invalidate: 'inside') decoration = {type: 'highlight', class: 'test-highlight'} editor.addDecorationForMarker(marker, decoration) - waitsFor -> not component.decorationChangedImmediate? + nextTick() it "does not render highlights for off-screen lines until they come on-screen", -> node.style.height = 2.5 * lineHeightInPixels + 'px' component.measureScrollView() + nextTick() marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], invalidate: 'inside') editor.addDecorationForMarker(marker, type: 'highlight', class: 'some-highlight') + nextTick() - waitsFor -> not component.decorationChangedImmediate? - runs -> - # Should not be rendering range containing the marker - expect(component.getRenderedRowRange()[1]).toBeLessThan 9 + # Should not be rendering range containing the marker + expect(component.getRenderedRowRange()[1]).toBeLessThan 9 - regions = node.querySelectorAll('.some-highlight .region') + regions = node.querySelectorAll('.some-highlight .region') - # Nothing when outside the rendered row range - expect(regions.length).toBe 0 + # Nothing when outside the rendered row range + expect(regions.length).toBe 0 - verticalScrollbarNode.scrollTop = 3.5 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + verticalScrollbarNode.scrollTop = 3.5 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + nextTick() - regions = node.querySelectorAll('.some-highlight .region') + regions = node.querySelectorAll('.some-highlight .region') - expect(regions.length).toBe 1 - regionRect = regions[0].style - expect(regionRect.top).toBe 9 * lineHeightInPixels + 'px' - expect(regionRect.height).toBe 1 * lineHeightInPixels + 'px' - expect(regionRect.left).toBe 2 * charWidth + 'px' - expect(regionRect.width).toBe 2 * charWidth + 'px' + expect(regions.length).toBe 1 + regionRect = regions[0].style + expect(regionRect.top).toBe 9 * lineHeightInPixels + 'px' + expect(regionRect.height).toBe 1 * lineHeightInPixels + 'px' + expect(regionRect.left).toBe 2 * charWidth + 'px' + expect(regionRect.width).toBe 2 * charWidth + 'px' it "renders highlights decoration's marker is added", -> regions = node.querySelectorAll('.test-highlight .region') @@ -915,24 +978,21 @@ describe "EditorComponent", -> it "removes highlights when a decoration is removed", -> editor.removeDecorationForMarker(marker, decoration) - - waitsFor -> not component.decorationChangedImmediate? - runs -> - regions = node.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe 0 + nextTick() + regions = node.querySelectorAll('.test-highlight .region') + expect(regions.length).toBe 0 it "does not render a highlight that is within a fold", -> editor.foldBufferRow(1) - - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(node.querySelectorAll('.test-highlight').length).toBe 0 + nextTick() + expect(node.querySelectorAll('.test-highlight').length).toBe 0 it "moves rendered highlights when the marker moves", -> regionStyle = node.querySelector('.test-highlight .region').style originalTop = parseInt(regionStyle.top) editor.getBuffer().insert([0, 0], '\n') + nextTick() regionStyle = node.querySelector('.test-highlight .region').style newTop = parseInt(regionStyle.top) @@ -941,28 +1001,24 @@ describe "EditorComponent", -> it "removes highlights when a decoration's marker is destroyed", -> marker.destroy() - - waitsFor -> not component.decorationChangedImmediate? - runs -> - regions = node.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe 0 + nextTick() + regions = node.querySelectorAll('.test-highlight .region') + expect(regions.length).toBe 0 it "only renders highlights when a decoration's marker is valid", -> editor.getBuffer().insert([3, 2], 'n') + nextTick() - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(marker.isValid()).toBe false - regions = node.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe 0 + expect(marker.isValid()).toBe false + regions = node.querySelectorAll('.test-highlight .region') + expect(regions.length).toBe 0 - editor.getBuffer().undo() + editor.getBuffer().undo() + nextTick() - waitsFor -> not component.decorationChangedImmediate? - runs -> - expect(marker.isValid()).toBe true - regions = node.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe 2 + expect(marker.isValid()).toBe true + regions = node.querySelectorAll('.test-highlight .region') + expect(regions.length).toBe 2 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", -> @@ -973,36 +1029,40 @@ describe "EditorComponent", -> node.style.height = 5 * lineHeightInPixels + 'px' node.style.width = 10 * charWidth + 'px' component.measureScrollView() + nextTick() expect(editor.getCursorScreenPosition()).toEqual [0, 0] editor.setScrollTop(3 * lineHeightInPixels) editor.setScrollLeft(3 * charWidth) + nextTick() expect(inputNode.offsetTop).toBe 0 expect(inputNode.offsetLeft).toBe 0 # In bounds, not focused editor.setCursorBufferPosition([5, 4]) + nextTick() expect(inputNode.offsetTop).toBe 0 expect(inputNode.offsetLeft).toBe 0 # In bounds and focused - inputNode.focus() + inputNode.focus() # updates via state change expect(inputNode.offsetTop).toBe (5 * lineHeightInPixels) - editor.getScrollTop() expect(inputNode.offsetLeft).toBe (4 * charWidth) - editor.getScrollLeft() # In bounds, not focused - inputNode.blur() + inputNode.blur() # updates via state change expect(inputNode.offsetTop).toBe 0 expect(inputNode.offsetLeft).toBe 0 # Out of bounds, not focused editor.setCursorBufferPosition([1, 2]) + nextTick() expect(inputNode.offsetTop).toBe 0 expect(inputNode.offsetLeft).toBe 0 # Out of bounds, focused - inputNode.focus() + inputNode.focus() # updates via state change expect(inputNode.offsetTop).toBe 0 expect(inputNode.offsetLeft).toBe 0 @@ -1021,20 +1081,24 @@ describe "EditorComponent", -> component.measureScrollView() editor.setScrollTop(3.5 * lineHeightInPixels) editor.setScrollLeft(2 * charWidth) + nextTick() linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]))) + nextTick() expect(editor.getCursorScreenPosition()).toEqual [4, 8] describe "when the shift key is held down", -> it "selects to the nearest screen position", -> editor.setCursorScreenPosition([3, 4]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), shiftKey: true)) + nextTick() expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [5, 6]] describe "when the command key is held down", -> it "adds a cursor at the nearest screen position", -> editor.setCursorScreenPosition([3, 4]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), metaKey: true)) + nextTick() expect(editor.getSelectedScreenRanges()).toEqual [[[3, 4], [3, 4]], [[5, 6], [5, 6]]] describe "when a non-folded line is double-clicked", -> @@ -1100,6 +1164,7 @@ describe "EditorComponent", -> describe "when a line is folded", -> beforeEach -> editor.foldBufferRow 4 + nextTick() describe "when the folded line's fold-marker is clicked", -> it "unfolds the buffer row", -> @@ -1228,45 +1293,54 @@ describe "EditorComponent", -> beforeEach -> cursor = editor.getCursor() cursor.setScreenPosition([0, 0]) + nextTick() it "adds the 'has-selection' class to the editor when there is a selection", -> expect(node.classList.contains('has-selection')).toBe false editor.selectDown() + nextTick() expect(node.classList.contains('has-selection')).toBe true cursor.moveDown() + nextTick() expect(node.classList.contains('has-selection')).toBe false describe "scrolling", -> it "updates the vertical scrollbar when the scrollTop is changed in the model", -> node.style.height = 4.5 * lineHeightInPixels + 'px' component.measureScrollView() + nextTick() expect(verticalScrollbarNode.scrollTop).toBe 0 editor.setScrollTop(10) + nextTick() expect(verticalScrollbarNode.scrollTop).toBe 10 it "updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model", -> node.style.width = 30 * charWidth + 'px' component.measureScrollView() + nextTick() linesNode = node.querySelector('.lines') expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" expect(horizontalScrollbarNode.scrollLeft).toBe 0 editor.setScrollLeft(100) + nextTick() expect(linesNode.style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0px)" expect(horizontalScrollbarNode.scrollLeft).toBe 100 it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> node.style.width = 30 * charWidth + 'px' component.measureScrollView() + nextTick() expect(editor.getScrollLeft()).toBe 0 horizontalScrollbarNode.scrollLeft = 100 horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + nextTick() expect(editor.getScrollLeft()).toBe 100 @@ -1275,6 +1349,7 @@ describe "EditorComponent", -> node.style.width = 10 * charWidth + 'px' component.measureScrollView() editor.setScrollBottom(editor.getScrollHeight()) + nextTick() lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom topOfHorizontalScrollbar = horizontalScrollbarNode.getBoundingClientRect().top @@ -1283,6 +1358,7 @@ describe "EditorComponent", -> # Scroll so there's no space below the last line when the horizontal scrollbar disappears node.style.width = 100 * charWidth + 'px' component.measureScrollView() + nextTick() bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom bottomOfEditor = node.getBoundingClientRect().bottom expect(bottomOfLastLine).toBe bottomOfEditor @@ -1291,8 +1367,8 @@ describe "EditorComponent", -> node.style.height = 7 * lineHeightInPixels + 'px' node.style.width = 10 * charWidth + 'px' component.measureScrollView() - editor.setScrollLeft(Infinity) + nextTick() rightOfLongestLine = component.lineNodeForScreenRow(6).getBoundingClientRect().right leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left @@ -1305,18 +1381,21 @@ describe "EditorComponent", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = '1000px' component.measureScrollView() + nextTick() expect(verticalScrollbarNode.style.display).toBe '' expect(horizontalScrollbarNode.style.display).toBe 'none' node.style.width = 10 * charWidth + 'px' component.measureScrollView() + nextTick() expect(verticalScrollbarNode.style.display).toBe '' expect(horizontalScrollbarNode.style.display).toBe '' node.style.height = 20 * lineHeightInPixels + 'px' component.measureScrollView() + nextTick() expect(verticalScrollbarNode.style.display).toBe 'none' expect(horizontalScrollbarNode.style.display).toBe '' @@ -1325,6 +1404,7 @@ describe "EditorComponent", -> node.style.height = 4 * lineHeightInPixels + 'px' node.style.width = 10 * charWidth + 'px' component.measureScrollView() + nextTick() atom.themes.applyStylesheet "test", """ ::-webkit-scrollbar { @@ -1348,18 +1428,21 @@ describe "EditorComponent", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = '1000px' component.measureScrollView() + nextTick() expect(verticalScrollbarNode.style.bottom).toBe '' expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' expect(scrollbarCornerNode.style.display).toBe 'none' node.style.width = 10 * charWidth + 'px' component.measureScrollView() + nextTick() expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' expect(scrollbarCornerNode.style.display).toBe '' node.style.height = 20 * lineHeightInPixels + 'px' component.measureScrollView() + nextTick() expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' expect(horizontalScrollbarNode.style.right).toBe '' expect(scrollbarCornerNode.style.display).toBe 'none' @@ -1368,6 +1451,7 @@ describe "EditorComponent", -> gutterNode = node.querySelector('.gutter') node.style.width = 10 * charWidth + 'px' component.measureScrollView() + nextTick() expect(horizontalScrollbarNode.scrollWidth).toBe gutterNode.offsetWidth + editor.getScrollWidth() @@ -1380,37 +1464,44 @@ describe "EditorComponent", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 20 * charWidth + 'px' component.measureScrollView() + nextTick() it "updates the scrollLeft or scrollTop on mousewheel events depending on which delta is greater (x or y)", -> expect(verticalScrollbarNode.scrollTop).toBe 0 expect(horizontalScrollbarNode.scrollLeft).toBe 0 node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10)) + nextTick() expect(verticalScrollbarNode.scrollTop).toBe 10 expect(horizontalScrollbarNode.scrollLeft).toBe 0 node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) + nextTick() 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)) + nextTick() expect(verticalScrollbarNode.scrollTop).toBe 5 expect(horizontalScrollbarNode.scrollLeft).toBe 0 node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) + nextTick() 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)) + nextTick() 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)) + nextTick() expect(verticalScrollbarNode.scrollTop).toBe 5 describe "when the mousewheel event's target is a line", -> @@ -1496,23 +1587,28 @@ describe "EditorComponent", -> it "inserts the newest character in the input's value into the buffer", -> node.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) + nextTick() expect(editor.lineForBufferRow(0)).toBe 'xvar quicksort = function () {' node.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode)) + nextTick() 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)) + nextTick() 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)) + nextTick() 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)) + nextTick() expect(editor.lineForBufferRow(0)).toBe 'var quicksort = function () {' describe "when IME composition is used to insert international characters", -> @@ -1592,6 +1688,7 @@ describe "EditorComponent", -> initialLineHeightInPixels = editor.getLineHeightInPixels() component.setLineHeight(2) + nextTick() expect(editor.getLineHeightInPixels()).toBe initialLineHeightInPixels wrapperView.show() @@ -1604,6 +1701,7 @@ describe "EditorComponent", -> initialCharWidth = editor.getDefaultCharWidth() component.setFontSize(22) + nextTick() expect(editor.getLineHeightInPixels()).toBe initialLineHeightInPixels expect(editor.getDefaultCharWidth()).toBe initialCharWidth @@ -1615,9 +1713,11 @@ describe "EditorComponent", -> wrapperView.hide() component.setFontSize(22) + nextTick() wrapperView.show() editor.setCursorBufferPosition([0, Infinity]) + nextTick() cursorLeft = node.querySelector('.cursor').getBoundingClientRect().left line0Right = node.querySelector('.line').getBoundingClientRect().right @@ -1630,6 +1730,7 @@ describe "EditorComponent", -> initialCharWidth = editor.getDefaultCharWidth() component.setFontFamily('sans-serif') + nextTick() expect(editor.getDefaultCharWidth()).toBe initialCharWidth wrapperView.show() @@ -1639,9 +1740,11 @@ describe "EditorComponent", -> wrapperView.hide() component.setFontFamily('sans-serif') + nextTick() wrapperView.show() editor.setCursorBufferPosition([0, Infinity]) + nextTick() cursorLeft = node.querySelector('.cursor').getBoundingClientRect().left line0Right = node.querySelector('.line').getBoundingClientRect().right @@ -1653,6 +1756,7 @@ describe "EditorComponent", -> wrapperView.hide() editor.setText('var z = 1') editor.setCursorBufferPosition([0, Infinity]) + nextTick() wrapperView.show() expect(node.querySelector('.cursor').style['-webkit-transform']).toBe "translate3d(#{9 * charWidth}px, 0px, 0px)" @@ -1664,11 +1768,13 @@ describe "EditorComponent", -> node.style.height = newHeight advanceClock(component.scrollViewMeasurementInterval) + nextTick() expect(node.querySelectorAll('.line')).toHaveLength(4 + lineOverdrawMargin + 1) gutterWidth = node.querySelector('.gutter').offsetWidth node.style.width = gutterWidth + 14 * charWidth + 'px' advanceClock(component.scrollViewMeasurementInterval) + nextTick() expect(node.querySelector('.line').textContent).toBe "var quicksort " buildMouseEvent = (type, properties...) -> diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 66c31f096..c02da334c 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -20,7 +20,6 @@ EditorComponent = React.createClass pendingScrollTop: null pendingScrollLeft: null selectOnMouseMove: false - batchingUpdates: false updateRequested: false cursorsMoved: false selectionChanged: false @@ -168,10 +167,9 @@ EditorComponent = React.createClass editor.setVisible(true) - editor.batchUpdates => - @measureLineHeightAndDefaultCharWidth() - @measureScrollView() - @measureScrollbars() + @measureLineHeightAndDefaultCharWidth() + @measureScrollView() + @measureScrollbars() componentWillUnmount: -> @unsubscribe() @@ -193,10 +191,11 @@ EditorComponent = React.createClass @props.parentView.trigger 'editor:display-updated' requestUpdate: -> - if @batchingUpdates + unless @updateRequested @updateRequested = true - else - @forceUpdate() + process.nextTick => + @updateRequested = false + @forceUpdate() getRenderedRowRange: -> {editor, lineOverdrawMargin} = @props @@ -263,8 +262,6 @@ EditorComponent = React.createClass observeEditor: -> {editor} = @props - @subscribe editor, 'batched-updates-started', @onBatchedUpdatesStarted - @subscribe editor, 'batched-updates-ended', @onBatchedUpdatesEnded @subscribe editor, 'screen-lines-changed', @onScreenLinesChanged @subscribe editor, 'cursors-moved', @onCursorsMoved @subscribe editor, 'selection-removed selection-screen-range-changed', @onSelectionChanged @@ -586,16 +583,6 @@ EditorComponent = React.createClass onStylesheetsChanged: (stylesheet) -> @refreshScrollbars() if @containsScrollbarSelector(stylesheet) - onBatchedUpdatesStarted: -> - @batchingUpdates = true - - onBatchedUpdatesEnded: -> - updateRequested = @updateRequested - @updateRequested = false - @batchingUpdates = false - if updateRequested - @requestUpdate() - onScreenLinesChanged: (change) -> {editor} = @props @pendingChanges.push(change) @@ -634,9 +621,7 @@ EditorComponent = React.createClass @requestUpdate() onDecorationChanged: -> - @decorationChangedImmediate ?= setImmediate => - @requestUpdate() if @isMounted() - @decorationChangedImmediate = null + @requestUpdate() onCharacterWidthsChanged: (@scopedCharacterWidthsChangeCount) -> @requestUpdate() @@ -694,14 +679,13 @@ EditorComponent = React.createClass {position} = getComputedStyle(editorNode) {width, height} = editorNode.style - editor.batchUpdates -> - if position is 'absolute' or height - clientHeight = scrollViewNode.clientHeight - editor.setHeight(clientHeight) if clientHeight > 0 + if position is 'absolute' or height + clientHeight = scrollViewNode.clientHeight + editor.setHeight(clientHeight) if clientHeight > 0 - if position is 'absolute' or width - clientWidth = scrollViewNode.clientWidth - editor.setWidth(clientWidth) if clientWidth > 0 + if position is 'absolute' or width + clientWidth = scrollViewNode.clientWidth + editor.setWidth(clientWidth) if clientWidth > 0 measureLineHeightAndCharWidthsIfNeeded: (prevState) -> if not isEqualForProperties(prevState, @state, 'lineHeight', 'fontSize', 'fontFamily') @@ -756,17 +740,17 @@ EditorComponent = React.createClass # visible, so first we need to hide scrollbars so we can redisplay them and # force Chromium to apply updates. @refreshingScrollbars = true - @requestUpdate() + @forceUpdate() # Next, we display only the scrollbar corner so we can measure the new # scrollbar dimensions. The ::measuringScrollbars property will be set back # to false after the scrollbars are measured. @measuringScrollbars = true - @requestUpdate() + @forceUpdate() # Finally, we restore the scrollbars based on the newly-measured dimensions # if the editor's content and dimensions require them to be visible. - @requestUpdate() + @forceUpdate() clearMouseWheelScreenRow: -> if @mouseWheelScreenRow? diff --git a/src/editor.coffee b/src/editor.coffee index 0703ae6ae..df0d69270 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1601,11 +1601,10 @@ class Editor extends Model moveCursors: (fn) -> @movingCursors = true - @batchUpdates => - fn(cursor) for cursor in @getCursors() - @mergeCursors() - @movingCursors = false - @emit 'cursors-moved' + fn(cursor) for cursor in @getCursors() + @mergeCursors() + @movingCursors = false + @emit 'cursors-moved' cursorMoved: (event) -> @emit 'cursor-moved', event @@ -1925,9 +1924,7 @@ class Editor extends Model # execution and revert any changes performed up to the abortion. # # fn - A {Function} to call inside the transaction. - transact: (fn) -> - @batchUpdates => - @buffer.transact(fn) + transact: (fn) -> @buffer.transact(fn) # Public: Start an open-ended transaction. # @@ -1947,14 +1944,6 @@ class Editor extends Model # within the transaction. abortTransaction: -> @buffer.abortTransaction() - batchUpdates: (fn) -> - @emit 'batched-updates-started' if @updateBatchDepth is 0 - @updateBatchDepth++ - result = fn() - @updateBatchDepth-- - @emit 'batched-updates-ended' if @updateBatchDepth is 0 - result - inspect: -> "" diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 5c9473ef4..afbeb1b57 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -236,9 +236,8 @@ LinesComponent = React.createClass node.removeChild(DummyLineNode) {editor} = @props - editor.batchUpdates -> - editor.setLineHeightInPixels(lineHeightInPixels) - editor.setDefaultCharWidth(charWidth) + editor.setLineHeightInPixels(lineHeightInPixels) + editor.setDefaultCharWidth(charWidth) remeasureCharacterWidths: -> @clearScopedCharWidths() @@ -249,11 +248,10 @@ LinesComponent = React.createClass [visibleStartRow, visibleEndRow] = @props.renderedRowRange node = @getDOMNode() - editor.batchUpdates => - for tokenizedLine in editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) - unless @measuredLines.has(tokenizedLine) - lineNode = @lineNodesByLineId[tokenizedLine.id] - @measureCharactersInLine(tokenizedLine, lineNode) + for tokenizedLine in editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) + unless @measuredLines.has(tokenizedLine) + lineNode = @lineNodesByLineId[tokenizedLine.id] + @measureCharactersInLine(tokenizedLine, lineNode) measureCharactersInLine: (tokenizedLine, lineNode) -> {editor} = @props From 68d0a99c6e0c0c0e3cfc8cb1ae78750fdf4ae7ed Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 19 Jun 2014 17:31:06 -0600 Subject: [PATCH 2/5] Default EditorComponent to updating synchronously in specs This commit adds a static property, EditorComponent.performSyncUpdates, which can be used to control the update behavior of all editor components. In addition, an instance property called performSyncUpdates be assigned to control the update behavior of a specific instance. --- spec/editor-component-spec.coffee | 4 ++-- spec/spec-helper.coffee | 3 +++ src/editor-component.coffee | 7 ++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 1892a37ec..d47abdb00 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -2,6 +2,7 @@ _ = require 'underscore-plus' {extend, flatten, toArray, last} = _ ReactEditorView = require '../src/react-editor-view' +EditorComponent = require '../src/editor-component' nbsp = String.fromCharCode(160) describe "EditorComponent", -> @@ -47,10 +48,9 @@ describe "EditorComponent", -> wrapperView = new ReactEditorView(editor, {lineOverdrawMargin}) wrapperView.attachToDom() - nextTick() - nextTick() {component} = wrapperView + component.performSyncUpdates = false component.setLineHeight(1.3) component.setFontSize(20) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index c97a5602f..893f0569c 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -15,6 +15,7 @@ Project = require '../src/project' Editor = require '../src/editor' EditorView = require '../src/editor-view' TokenizedBuffer = require '../src/tokenized-buffer' +EditorComponent = require '../src/editor-component' pathwatcher = require 'pathwatcher' clipboard = require 'clipboard' @@ -100,6 +101,8 @@ beforeEach -> # make editor display updates synchronous spyOn(EditorView.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay() + EditorComponent.performSyncUpdates = true + spyOn(WorkspaceView.prototype, 'setTitle').andCallFake (@title) -> spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout diff --git a/src/editor-component.coffee b/src/editor-component.coffee index c02da334c..d8d407c5c 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -17,6 +17,9 @@ EditorComponent = React.createClass displayName: 'EditorComponent' mixins: [SubscriberMixin] + statics: + performSyncUpdates: false + pendingScrollTop: null pendingScrollLeft: null selectOnMouseMove: false @@ -191,7 +194,9 @@ EditorComponent = React.createClass @props.parentView.trigger 'editor:display-updated' requestUpdate: -> - unless @updateRequested + if @performSyncUpdates ? EditorComponent.performSyncUpdates + @forceUpdate() + else unless @updateRequested @updateRequested = true process.nextTick => @updateRequested = false From ef1ec9b693e55e7585897cefe533e9f4cc3a22a5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 20 Jun 2014 13:29:01 -0600 Subject: [PATCH 3/5] Emit events *after* update to prevent requesting update during an update When updating synchronously in specs, we can't get away with requesting an update before the previous update is completed. If we emit events before the update, we have the potential for one of those events to cause this to happen. Moving them to after is more correct anyway. --- src/editor-component.coffee | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index d8d407c5c..e6ab32a33 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -180,18 +180,24 @@ EditorComponent = React.createClass @scrollViewMeasurementIntervalId = null componentWillUpdate: -> - if @props.editor.isAlive() - @props.parentView.trigger 'cursor:moved' if @cursorsMoved - @props.parentView.trigger 'selection:changed' if @selectionChanged componentDidUpdate: (prevProps, prevState) -> + cursorsMoved = @cursorsMoved + selectionChanged = @selectionChanged @pendingChanges.length = 0 + @cursorsMoved = false + @selectionChanged = false @refreshingScrollbars = false - @updateParentViewFocusedClassIfNeeded(prevState) + + if @props.editor.isAlive() + @updateParentViewFocusedClassIfNeeded(prevState) + @props.parentView.trigger 'cursor:moved' if cursorsMoved + @props.parentView.trigger 'selection:changed' if selectionChanged + @props.parentView.trigger 'editor:display-updated' + @measureScrollbars() if @measuringScrollbars @measureLineHeightAndCharWidthsIfNeeded(prevState) @remeasureCharacterWidthsIfNeeded(prevState) - @props.parentView.trigger 'editor:display-updated' requestUpdate: -> if @performSyncUpdates ? EditorComponent.performSyncUpdates From ef2bdf6365b530a8280c8e94c3b75309aef90b5f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 20 Jun 2014 13:56:50 -0600 Subject: [PATCH 4/5] Only forceUpdate of EditorComponent on nextTick if it's mounted --- src/editor-component.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor-component.coffee b/src/editor-component.coffee index e6ab32a33..7f475fb1b 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -206,7 +206,7 @@ EditorComponent = React.createClass @updateRequested = true process.nextTick => @updateRequested = false - @forceUpdate() + @forceUpdate() if @isMounted() getRenderedRowRange: -> {editor, lineOverdrawMargin} = @props From 36a0da01cca19c423b0265f008b0642d2699b206 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 20 Jun 2014 14:52:25 -0600 Subject: [PATCH 5/5] Update markdown-preview to fix specs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index db6aeea7f..016e1903c 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "image-view": "0.35.0", "keybinding-resolver": "0.18.0", "link": "0.24.0", - "markdown-preview": "0.82.0", + "markdown-preview": "0.83.0", "metrics": "0.32.0", "open-on-github": "0.28.0", "package-generator": "0.31.0",