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", diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 5ed21aa43..d47abdb00 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -2,11 +2,12 @@ _ = require 'underscore-plus' {extend, flatten, toArray, last} = _ ReactEditorView = require '../src/react-editor-view' +EditorComponent = require '../src/editor-component' 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 +21,7 @@ describe "EditorComponent", -> delayAnimationFrames = false nextAnimationFrame = -> throw new Error('No animation frame requested') + spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> if delayAnimationFrames nextAnimationFrame = fn @@ -30,12 +32,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() + {component} = wrapperView + component.performSyncUpdates = false 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/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 66c31f096..7f475fb1b 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -17,10 +17,12 @@ EditorComponent = React.createClass displayName: 'EditorComponent' mixins: [SubscriberMixin] + statics: + performSyncUpdates: false + pendingScrollTop: null pendingScrollLeft: null selectOnMouseMove: false - batchingUpdates: false updateRequested: false cursorsMoved: false selectionChanged: false @@ -168,10 +170,9 @@ EditorComponent = React.createClass editor.setVisible(true) - editor.batchUpdates => - @measureLineHeightAndDefaultCharWidth() - @measureScrollView() - @measureScrollbars() + @measureLineHeightAndDefaultCharWidth() + @measureScrollView() + @measureScrollbars() componentWillUnmount: -> @unsubscribe() @@ -179,24 +180,33 @@ 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 @batchingUpdates - @updateRequested = true - else + if @performSyncUpdates ? EditorComponent.performSyncUpdates @forceUpdate() + else unless @updateRequested + @updateRequested = true + process.nextTick => + @updateRequested = false + @forceUpdate() if @isMounted() getRenderedRowRange: -> {editor, lineOverdrawMargin} = @props @@ -263,8 +273,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 +594,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 +632,7 @@ EditorComponent = React.createClass @requestUpdate() onDecorationChanged: -> - @decorationChangedImmediate ?= setImmediate => - @requestUpdate() if @isMounted() - @decorationChangedImmediate = null + @requestUpdate() onCharacterWidthsChanged: (@scopedCharacterWidthsChangeCount) -> @requestUpdate() @@ -694,14 +690,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 +751,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