From 50d5151a3eaff69f13895d7967d848a3f098bb8c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 19 Oct 2015 17:19:09 -0700 Subject: [PATCH 01/53] :arrow_up: text-buffer (pre-release) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a9a5a76c..c246678ed 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "7.1.3", + "text-buffer": "^7.2.0-pre-marker-layers.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From cdaac9dfccad9acaff117defcd2b82c491aec0c4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 23 Oct 2015 16:54:34 -0700 Subject: [PATCH 02/53] wip --- spec/text-editor-component-spec.coffee | 4 +- spec/text-editor-presenter-spec.coffee | 521 ++++++++++++++----------- src/decoration.coffee | 3 + src/display-buffer.coffee | 44 ++- src/text-editor-presenter.coffee | 64 +-- src/text-editor.coffee | 6 + 6 files changed, 332 insertions(+), 310 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 91020f299..e5953a293 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -4,7 +4,7 @@ _ = require 'underscore-plus' TextEditorElement = require '../src/text-editor-element' nbsp = String.fromCharCode(160) -describe "TextEditorComponent", -> +fdescribe "TextEditorComponent", -> [contentNode, editor, wrapperNode, component, componentNode, verticalScrollbarNode, horizontalScrollbarNode] = [] [lineHeightInPixels, charWidth, nextAnimationFrame, noAnimationFrame, tileSize, tileHeightInPixels] = [] @@ -1264,7 +1264,7 @@ describe "TextEditorComponent", -> nextAnimationFrame() expect(selectionNode.classList.contains('flash')).toBe true - describe "line decoration rendering", -> + ffdescribe "line decoration rendering", -> [marker, decoration, decorationParams] = [] beforeEach -> diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 1804b7b6b..aa716714f 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -62,6 +62,13 @@ describe "TextEditorPresenter", -> expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn) + waitsForStateToUpdate = (presenter, fn) -> + waitsFor "presenter state to update", 1000, (done) -> + fn?() + disposable = presenter.onDidUpdateState -> + disposable.dispose() + done() + tiledContentContract = (stateFn) -> it "contains states for tiles that are visible on screen", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) @@ -1153,49 +1160,56 @@ describe "TextEditorPresenter", -> marker2 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true) decoration2 = editor.decorateMarker(marker2, type: 'line', class: 'b') - expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + waitsForStateToUpdate presenter + runs -> + expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] + expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] + expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] + expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - expectStateUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x') - expect(marker1.isValid()).toBe false - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() + waitsForStateToUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x') + runs -> + expect(marker1.isValid()).toBe false + expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - expectStateUpdate presenter, -> editor.undo() - expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + waitsForStateToUpdate presenter, -> editor.undo() + runs -> + expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] + expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] + expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] + expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - expectStateUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]]) - expect(lineStateForScreenRow(presenter, 1).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a'] - expect(lineStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a'] - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] - expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + waitsForStateToUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]]) + runs -> + expect(lineStateForScreenRow(presenter, 1).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a'] + expect(lineStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a'] + expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] + expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] + expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] + expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - expectStateUpdate presenter, -> decoration1.destroy() - expect(lineStateForScreenRow(presenter, 2).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b'] - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] - expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + waitsForStateToUpdate presenter, -> decoration1.destroy() + runs -> + expect(lineStateForScreenRow(presenter, 2).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b'] + expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] + expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] + expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - expectStateUpdate presenter, -> marker2.destroy() - expect(lineStateForScreenRow(presenter, 2).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + waitsForStateToUpdate presenter, -> marker2.destroy() + runs -> + expect(lineStateForScreenRow(presenter, 2).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 7).decorationClasses).toBeNull() it "honors the 'onlyEmpty' option on line decorations", -> presenter = buildPresenter() @@ -1206,11 +1220,12 @@ describe "TextEditorPresenter", -> expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - expectStateUpdate presenter, -> marker.clearTail() + waitsForStateToUpdate presenter, -> marker.clearTail() - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] + runs -> + expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] it "honors the 'onlyNonEmpty' option on line decorations", -> presenter = buildPresenter() @@ -1221,40 +1236,49 @@ describe "TextEditorPresenter", -> expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] - expectStateUpdate presenter, -> marker.clearTail() + waitsForStateToUpdate presenter, -> marker.clearTail() - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() + runs -> + expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() it "honors the 'onlyHead' option on line decorations", -> presenter = buildPresenter() - marker = editor.markBufferRange([[4, 0], [6, 2]]) - decoration = editor.decorateMarker(marker, type: 'line', class: 'a', onlyHead: true) + waitsForStateToUpdate presenter, -> + marker = editor.markBufferRange([[4, 0], [6, 2]]) + editor.decorateMarker(marker, type: 'line', class: 'a', onlyHead: true) - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] + runs -> + expect(lineStateForScreenRow(presenter, 4).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 5).decorationClasses).toBeNull() + expect(lineStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] it "does not decorate the last line of a non-empty line decoration range if it ends at column 0", -> presenter = buildPresenter() - marker = editor.markBufferRange([[4, 0], [6, 0]]) - decoration = editor.decorateMarker(marker, type: 'line', class: 'a') + waitsForStateToUpdate presenter, -> + marker = editor.markBufferRange([[4, 0], [6, 0]]) + editor.decorateMarker(marker, type: 'line', class: 'a') - expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a'] - expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] - expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() + runs -> + expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a'] + expect(lineStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] + expect(lineStateForScreenRow(presenter, 6).decorationClasses).toBeNull() it "does not apply line decorations to mini editors", -> editor.setMini(true) presenter = buildPresenter(explicitHeight: 10) - marker = editor.markBufferRange([[0, 0], [0, 0]]) - decoration = editor.decorateMarker(marker, type: 'line', class: 'a') - expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull() - expectStateUpdate presenter, -> editor.setMini(false) - expect(lineStateForScreenRow(presenter, 0).decorationClasses).toEqual ['cursor-line', 'a'] + waitsForStateToUpdate presenter, -> + marker = editor.markBufferRange([[0, 0], [0, 0]]) + decoration = editor.decorateMarker(marker, type: 'line', class: 'a') - expectStateUpdate presenter, -> editor.setMini(true) - expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull() + runs -> + expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull() + + expectStateUpdate presenter, -> editor.setMini(false) + expect(lineStateForScreenRow(presenter, 0).decorationClasses).toEqual ['cursor-line', 'a'] + + expectStateUpdate presenter, -> editor.setMini(true) + expect(lineStateForScreenRow(presenter, 0).decorationClasses).toBeNull() it "only applies decorations to screen rows that are spanned by their marker when lines are soft-wrapped", -> editor.setText("a line that wraps, ok") @@ -1268,9 +1292,12 @@ describe "TextEditorPresenter", -> expect(lineStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' expect(lineStateForScreenRow(presenter, 1).decorationClasses).toBeNull() - marker.setBufferRange([[0, 0], [0, Infinity]]) - expect(lineStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' - expect(lineStateForScreenRow(presenter, 1).decorationClasses).toContain 'a' + waitsForStateToUpdate presenter, -> + marker.setBufferRange([[0, 0], [0, Infinity]]) + + runs -> + expect(lineStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' + expect(lineStateForScreenRow(presenter, 1).decorationClasses).toContain 'a' describe ".cursors", -> stateForCursor = (presenter, cursorIndex) -> @@ -1740,41 +1767,51 @@ describe "TextEditorPresenter", -> expectUndefinedStateForSelection(presenter, 1) # moving into view - expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false) - expectValues stateForSelectionInTile(presenter, 1, 2), { - regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] - } + waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false) + runs -> + expectValues stateForSelectionInTile(presenter, 1, 2), { + regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] + } # becoming empty - expectStateUpdate presenter, -> editor.getSelections()[1].clear(autoscroll: false) - expectUndefinedStateForSelection(presenter, 1) + waitsForStateToUpdate presenter, -> editor.getSelections()[1].clear(autoscroll: false) + runs -> + expectUndefinedStateForSelection(presenter, 1) # becoming non-empty - expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false) - expectValues stateForSelectionInTile(presenter, 1, 2), { - regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] - } + waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false) + runs -> + expectValues stateForSelectionInTile(presenter, 1, 2), { + regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] + } # moving out of view - expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false) - expectUndefinedStateForSelection(presenter, 1) + waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false) + runs -> + expectUndefinedStateForSelection(presenter, 1) # adding - expectStateUpdate presenter, -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false) - expectValues stateForSelectionInTile(presenter, 2, 0), { - regions: [{top: 10, left: 4 * 10, width: 2 * 10, height: 10}] - } + waitsForStateToUpdate presenter, -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false) + runs -> + expectValues stateForSelectionInTile(presenter, 2, 0), { + regions: [{top: 10, left: 4 * 10, width: 2 * 10, height: 10}] + } # moving added selection - expectStateUpdate presenter, -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false) - expectValues stateForSelectionInTile(presenter, 2, 0), { - regions: [{top: 10, left: 4 * 10, width: 4 * 10, height: 10}] - } + waitsForStateToUpdate presenter, -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false) - # destroying - destroyedSelection = editor.getSelections()[2] - expectStateUpdate presenter, -> destroyedSelection.destroy() - expectUndefinedStateForHighlight(presenter, destroyedSelection.decoration) + destroyedSelection = null + runs -> + expectValues stateForSelectionInTile(presenter, 2, 0), { + regions: [{top: 10, left: 4 * 10, width: 4 * 10, height: 10}] + } + + # destroying + destroyedSelection = editor.getSelections()[2] + + waitsForStateToUpdate presenter, -> destroyedSelection.destroy() + runs -> + expectUndefinedStateForHighlight(presenter, destroyedSelection.decoration) it "updates when highlight decorations' properties are updated", -> marker = editor.markBufferPosition([2, 2]) @@ -1784,44 +1821,45 @@ describe "TextEditorPresenter", -> expectUndefinedStateForHighlight(presenter, highlight) - expectStateUpdate presenter, -> + waitsForStateToUpdate presenter, -> marker.setBufferRange([[2, 2], [2, 4]]) highlight.setProperties(class: 'b', type: 'highlight') - expectValues stateForHighlightInTile(presenter, highlight, 2), {class: 'b'} + runs -> + expectValues stateForHighlightInTile(presenter, highlight, 2), {class: 'b'} it "increments the .flashCount and sets the .flashClass and .flashDuration when the highlight model flashes", -> presenter = buildPresenter(explicitHeight: 30, scrollTop: 20, tileSize: 2) marker = editor.markBufferPosition([2, 2]) highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a') - expectStateUpdate presenter, -> + waitsForStateToUpdate presenter, -> marker.setBufferRange([[2, 2], [5, 2]]) highlight.flash('b', 500) + runs -> + expectValues stateForHighlightInTile(presenter, highlight, 2), { + flashClass: 'b' + flashDuration: 500 + flashCount: 1 + } + expectValues stateForHighlightInTile(presenter, highlight, 4), { + flashClass: 'b' + flashDuration: 500 + flashCount: 1 + } - expectValues stateForHighlightInTile(presenter, highlight, 2), { - flashClass: 'b' - flashDuration: 500 - flashCount: 1 - } - expectValues stateForHighlightInTile(presenter, highlight, 4), { - flashClass: 'b' - flashDuration: 500 - flashCount: 1 - } - - expectStateUpdate presenter, -> highlight.flash('c', 600) - - expectValues stateForHighlightInTile(presenter, highlight, 2), { - flashClass: 'c' - flashDuration: 600 - flashCount: 2 - } - expectValues stateForHighlightInTile(presenter, highlight, 4), { - flashClass: 'c' - flashDuration: 600 - flashCount: 2 - } + waitsForStateToUpdate presenter, -> highlight.flash('c', 600) + runs -> + expectValues stateForHighlightInTile(presenter, highlight, 2), { + flashClass: 'c' + flashDuration: 600 + flashCount: 2 + } + expectValues stateForHighlightInTile(presenter, highlight, 4), { + flashClass: 'c' + flashDuration: 600 + flashCount: 2 + } describe ".overlays", -> [item] = [] @@ -1840,40 +1878,47 @@ describe "TextEditorPresenter", -> } # Change range - expectStateUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]]) - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10} - } + waitsForStateToUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]]) + runs -> + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10} + } - # Valid -> invalid - expectStateUpdate presenter, -> editor.getBuffer().insert([2, 14], 'x') - expect(stateForOverlay(presenter, decoration)).toBeUndefined() + # Valid -> invalid + waitsForStateToUpdate presenter, -> editor.getBuffer().insert([2, 14], 'x') + runs -> + expect(stateForOverlay(presenter, decoration)).toBeUndefined() - # Invalid -> valid - expectStateUpdate presenter, -> editor.undo() - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10} - } + # Invalid -> valid + waitsForStateToUpdate presenter, -> editor.undo() + runs -> + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 5 * 10 - presenter.state.content.scrollTop, left: 6 * 10} + } # Reverse direction - expectStateUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]], reversed: true) - expectValues stateForOverlay(presenter, decoration), { - item: item - pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} - } + waitsForStateToUpdate presenter, -> marker.setBufferRange([[2, 13], [4, 6]], reversed: true) + runs -> + expectValues stateForOverlay(presenter, decoration), { + item: item + pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} + } # Destroy - decoration.destroy() - expect(stateForOverlay(presenter, decoration)).toBeUndefined() + waitsForStateToUpdate presenter, -> decoration.destroy() + runs -> + expect(stateForOverlay(presenter, decoration)).toBeUndefined() # Add - decoration2 = editor.decorateMarker(marker, {type: 'overlay', item}) - expectValues stateForOverlay(presenter, decoration2), { - item: item - pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} - } + decoration2 = null + waitsForStateToUpdate presenter, -> decoration2 = editor.decorateMarker(marker, {type: 'overlay', item}) + runs -> + expectValues stateForOverlay(presenter, decoration2), { + item: item + pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} + } it "updates when character widths changes", -> scrollTop = 20 @@ -2310,9 +2355,9 @@ describe "TextEditorPresenter", -> it "adds decoration classes to the relevant line number state objects, both initially and when decorations change", -> marker1 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true) decoration1 = editor.decorateMarker(marker1, type: 'line-number', class: 'a') - presenter = buildPresenter() marker2 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true) decoration2 = editor.decorateMarker(marker2, type: 'line-number', class: 'b') + presenter = buildPresenter() expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] @@ -2320,85 +2365,92 @@ describe "TextEditorPresenter", -> expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - expectStateUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x') - expect(marker1.isValid()).toBe false - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() + waitsForStateToUpdate presenter, -> editor.getBuffer().insert([5, 0], 'x') + runs -> + expect(marker1.isValid()).toBe false + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - expectStateUpdate presenter, -> editor.undo() - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + waitsForStateToUpdate presenter, -> editor.undo() + runs -> + expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a', 'b'] + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a', 'b'] + expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - expectStateUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]]) - expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a'] - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + waitsForStateToUpdate presenter, -> marker1.setBufferRange([[2, 0], [4, 2]]) + runs -> + expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toEqual ['a'] + expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toEqual ['a'] + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b'] + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] + expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - expectStateUpdate presenter, -> decoration1.destroy() - expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + waitsForStateToUpdate presenter, -> decoration1.destroy() + runs -> + expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['b'] + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['b'] + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['b'] + expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() - expectStateUpdate presenter, -> marker2.destroy() - expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() + waitsForStateToUpdate presenter, -> marker2.destroy() + runs -> + expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 7).decorationClasses).toBeNull() it "honors the 'onlyEmpty' option on line-number decorations", -> - presenter = buildPresenter() marker = editor.markBufferRange([[4, 0], [6, 1]]) decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyEmpty: true) + presenter = buildPresenter() expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() - expectStateUpdate presenter, -> marker.clearTail() + waitsForStateToUpdate presenter, -> marker.clearTail() - expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] + runs -> + expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] it "honors the 'onlyNonEmpty' option on line-number decorations", -> - presenter = buildPresenter() marker = editor.markBufferRange([[4, 0], [6, 2]]) decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyNonEmpty: true) + presenter = buildPresenter() expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a'] expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] - expectStateUpdate presenter, -> marker.clearTail() + waitsForStateToUpdate presenter, -> marker.clearTail() - expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() + runs -> + expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toBeNull() it "honors the 'onlyHead' option on line-number decorations", -> - presenter = buildPresenter() marker = editor.markBufferRange([[4, 0], [6, 2]]) decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a', onlyHead: true) + presenter = buildPresenter() expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toBeNull() expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toBeNull() expect(lineNumberStateForScreenRow(presenter, 6).decorationClasses).toEqual ['a'] it "does not decorate the last line of a non-empty line-number decoration range if it ends at column 0", -> - presenter = buildPresenter() marker = editor.markBufferRange([[4, 0], [6, 0]]) decoration = editor.decorateMarker(marker, type: 'line-number', class: 'a') + presenter = buildPresenter() expect(lineNumberStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a'] expect(lineNumberStateForScreenRow(presenter, 5).decorationClasses).toEqual ['a'] @@ -2430,9 +2482,10 @@ describe "TextEditorPresenter", -> expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull() - marker.setBufferRange([[0, 0], [0, Infinity]]) - expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' - expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a' + waitsForStateToUpdate presenter, -> marker.setBufferRange([[0, 0], [0, Infinity]]) + runs -> + expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a' + expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a' describe ".foldable", -> it "marks line numbers at the start of a foldable region as foldable", -> @@ -2565,14 +2618,15 @@ describe "TextEditorPresenter", -> it "updates when a decoration's marker is modified", -> # This update will move decoration1 out of view. - expectStateUpdate presenter, -> + waitsForStateToUpdate presenter, -> newRange = new Range([13, 0], [14, 0]) marker1.setBufferRange(newRange) - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id]).toBeUndefined() + runs -> + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id].top).toBeDefined() + expect(decorationState[decoration3.id]).toBeUndefined() describe "when a decoration's properties are modified", -> it "updates the item applied to the decoration, if the decoration item is changed", -> @@ -2584,12 +2638,14 @@ describe "TextEditorPresenter", -> gutterName: 'test-gutter' class: 'test-class' item: newItem - expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams) - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].item).toBe newItem - expect(decorationState[decoration2.id].item).toBe decorationItem - expect(decorationState[decoration3.id]).toBeUndefined() + waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams) + + runs -> + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].item).toBe newItem + expect(decorationState[decoration2.id].item).toBe decorationItem + expect(decorationState[decoration3.id]).toBeUndefined() it "updates the class applied to the decoration, if the decoration class is changed", -> # This changes the decoration item. The visibility of the decoration should not be affected. @@ -2598,12 +2654,13 @@ describe "TextEditorPresenter", -> gutterName: 'test-gutter' class: 'new-test-class' item: decorationItem - expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams) + waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams) - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id].class).toBe 'new-test-class' - expect(decorationState[decoration2.id].class).toBe 'test-class' - expect(decorationState[decoration3.id]).toBeUndefined() + runs -> + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id].class).toBe 'new-test-class' + expect(decorationState[decoration2.id].class).toBe 'test-class' + expect(decorationState[decoration3.id]).toBeUndefined() it "updates the type of the decoration, if the decoration type is changed", -> # This changes the type of the decoration. This should remove the decoration from the gutter. @@ -2612,12 +2669,13 @@ describe "TextEditorPresenter", -> gutterName: 'test-gutter' # This is an invalid/meaningless option here, but it shouldn't matter. class: 'test-class' item: decorationItem - expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams) + waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams) - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id]).toBeUndefined() + runs -> + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id].top).toBeDefined() + expect(decorationState[decoration3.id]).toBeUndefined() it "updates the gutter the decoration targets, if the decoration gutterName is changed", -> # This changes which gutter this decoration applies to. Since this gutter does not exist, @@ -2627,24 +2685,25 @@ describe "TextEditorPresenter", -> gutterName: 'test-gutter-2' class: 'new-test-class' item: decorationItem - expectStateUpdate presenter, -> decoration1.setProperties(newDecorationParams) + waitsForStateToUpdate presenter, -> decoration1.setProperties(newDecorationParams) - decorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id].top).toBeDefined() - expect(decorationState[decoration3.id]).toBeUndefined() + runs -> + decorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id].top).toBeDefined() + expect(decorationState[decoration3.id]).toBeUndefined() - # After adding the targeted gutter, the decoration will appear in the state for that gutter, - # since it should be visible. - expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'}) - newGutterDecorationState = getContentForGutterWithName(presenter, 'test-gutter-2') - expect(newGutterDecorationState[decoration1.id].top).toBeDefined() - expect(newGutterDecorationState[decoration2.id]).toBeUndefined() - expect(newGutterDecorationState[decoration3.id]).toBeUndefined() - oldGutterDecorationState = getContentForGutterWithName(presenter, 'test-gutter') - expect(oldGutterDecorationState[decoration1.id]).toBeUndefined() - expect(oldGutterDecorationState[decoration2.id].top).toBeDefined() - expect(oldGutterDecorationState[decoration3.id]).toBeUndefined() + # After adding the targeted gutter, the decoration will appear in the state for that gutter, + # since it should be visible. + expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'}) + newGutterDecorationState = getContentForGutterWithName(presenter, 'test-gutter-2') + expect(newGutterDecorationState[decoration1.id].top).toBeDefined() + expect(newGutterDecorationState[decoration2.id]).toBeUndefined() + expect(newGutterDecorationState[decoration3.id]).toBeUndefined() + oldGutterDecorationState = getContentForGutterWithName(presenter, 'test-gutter') + expect(oldGutterDecorationState[decoration1.id]).toBeUndefined() + expect(oldGutterDecorationState[decoration2.id].top).toBeDefined() + expect(oldGutterDecorationState[decoration3.id]).toBeUndefined() it "updates when the editor's mini state changes, and is cleared when the editor is mini", -> expectStateUpdate presenter, -> editor.setMini(true) @@ -2679,13 +2738,17 @@ describe "TextEditorPresenter", -> class: 'test-class' marker4 = editor.markBufferRange([[0, 0], [1, 0]]) decoration4 = editor.decorateMarker(marker4, decorationParams) - expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'}) - decorationState = getContentForGutterWithName(presenter, 'test-gutter-2') - expect(decorationState[decoration1.id]).toBeUndefined() - expect(decorationState[decoration2.id]).toBeUndefined() - expect(decorationState[decoration3.id]).toBeUndefined() - expect(decorationState[decoration4.id].top).toBeDefined() + waitsForStateToUpdate presenter + + runs -> + expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'}) + + decorationState = getContentForGutterWithName(presenter, 'test-gutter-2') + expect(decorationState[decoration1.id]).toBeUndefined() + expect(decorationState[decoration2.id]).toBeUndefined() + expect(decorationState[decoration3.id]).toBeUndefined() + expect(decorationState[decoration4.id].top).toBeDefined() it "updates when editor lines are folded", -> oldDimensionsForDecoration1 = diff --git a/src/decoration.coffee b/src/decoration.coffee index 154900ce5..a65a417eb 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -81,6 +81,7 @@ class Decoration @markerDestroyDisposable.dispose() @markerDestroyDisposable = null @destroyed = true + @displayBuffer.didDestroyDecoration(this) @emitter.emit 'did-destroy' @emitter.dispose() @@ -153,6 +154,7 @@ class Decoration @properties.id = @id if newProperties.type? @displayBuffer.decorationDidChangeType(this) + @displayBuffer.scheduleUpdateDecorationsEvent() @emitter.emit 'did-change-properties', {oldProperties, newProperties} ### @@ -172,6 +174,7 @@ class Decoration flashObject = {class: klass, duration} @flashQueue ?= [] @flashQueue.push(flashObject) + @displayBuffer.scheduleUpdateDecorationsEvent() @emitter.emit 'did-flash' consumeNextFlash: -> diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 3da9b8ea5..68ef0a80b 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -25,6 +25,7 @@ class DisplayBuffer extends Model defaultCharWidth: null height: null width: null + didUpdateDecorationsEventScheduled: false @deserialize: (state, atomEnvironment) -> state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) @@ -59,7 +60,8 @@ class DisplayBuffer extends Model @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings @disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange @disposables.add @buffer.onDidCreateMarker @handleBufferMarkerCreated - @disposables.add @buffer.onDidUpdateMarkers => @emitter.emit 'did-update-markers' + @disposables.add @buffer.getDefaultMarkerLayer().onDidUpdate => @scheduleUpdateDecorationsEvent() + @foldMarkerAttributes = Object.freeze({class: 'fold', displayBufferId: @id}) folds = (new Fold(this, marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes())) @updateAllScreenLines() @@ -158,6 +160,9 @@ class DisplayBuffer extends Model onDidUpdateMarkers: (callback) -> @emitter.on 'did-update-markers', callback + onDidUpdateDecorations: (callback) -> + @emitter.on 'did-update-decorations', callback + emitDidChange: (eventProperties, refreshMarkers=true) -> @emitter.emit 'did-change', eventProperties if refreshMarkers @@ -772,29 +777,14 @@ class DisplayBuffer extends Model decorateMarker: (marker, decorationParams) -> marker = @getMarker(marker.id) decoration = new Decoration(marker, this, decorationParams) - decorationDestroyedDisposable = decoration.onDidDestroy => - @removeDecoration(decoration) - @disposables.remove(decorationDestroyedDisposable) - @disposables.add(decorationDestroyedDisposable) @decorationsByMarkerId[marker.id] ?= [] @decorationsByMarkerId[marker.id].push(decoration) @overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay') @decorationsById[decoration.id] = decoration + @scheduleUpdateDecorationsEvent() @emitter.emit 'did-add-decoration', decoration decoration - removeDecoration: (decoration) -> - {marker} = decoration - return unless decorations = @decorationsByMarkerId[marker.id] - index = decorations.indexOf(decoration) - - if index > -1 - decorations.splice(index, 1) - delete @decorationsById[decoration.id] - @emitter.emit 'did-remove-decoration', decoration - delete @decorationsByMarkerId[marker.id] if decorations.length is 0 - delete @overlayDecorationsById[decoration.id] - decorationsForMarkerId: (markerId) -> @decorationsByMarkerId[markerId] @@ -1083,6 +1073,13 @@ class DisplayBuffer extends Model # this one. Only emit when the marker still exists. @emitter.emit 'did-create-marker', marker + scheduleUpdateDecorationsEvent: -> + unless @didUpdateDecorationsEventScheduled + @didUpdateDecorationsEventScheduled = true + process.nextTick => + @didUpdateDecorationsEventScheduled = false + @emitter.emit 'did-update-decorations' + decorateFold: (fold) -> @decorateMarker(fold.marker, type: 'line-number', class: 'folded') @@ -1095,6 +1092,19 @@ class DisplayBuffer extends Model else delete @overlayDecorationsById[decoration.id] + didDestroyDecoration: (decoration) -> + {marker} = decoration + return unless decorations = @decorationsByMarkerId[marker.id] + index = decorations.indexOf(decoration) + + if index > -1 + decorations.splice(index, 1) + delete @decorationsById[decoration.id] + @emitter.emit 'did-remove-decoration', decoration + delete @decorationsByMarkerId[marker.id] if decorations.length is 0 + delete @overlayDecorationsById[decoration.id] + @scheduleUpdateDecorationsEvent() + checkScreenLinesInvariant: -> return if @isSoftWrapped() return if _.size(@foldsByMarkerId) > 0 diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 2e1d73c56..eabb3ed79 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -185,7 +185,7 @@ class TextEditorPresenter @shouldUpdateCustomGutterDecorationState = true @emitDidUpdateState() - @disposables.add @model.onDidUpdateMarkers => + @disposables.add @model.onDidUpdateDecorations => @shouldUpdateLinesState = true @shouldUpdateLineNumbersState = true @shouldUpdateDecorations = true @@ -214,10 +214,8 @@ class TextEditorPresenter @shouldUpdateGutterOrderState = true @emitDidUpdateState() - @disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this)) @disposables.add @model.onDidAddCursor(@didAddCursor.bind(this)) @disposables.add @model.onDidRequestAutoscroll(@requestAutoscroll.bind(this)) - @observeDecoration(decoration) for decoration in @model.getDecorations() @observeCursor(cursor) for cursor in @model.getCursors() @disposables.add @model.onDidAddGutter(@didAddGutter.bind(this)) return @@ -890,6 +888,7 @@ class TextEditorPresenter @shouldUpdateFocusedState = true @shouldUpdateHiddenInputState = true + console.log 'emitDidUpdateState' @emitDidUpdateState() setScrollTop: (scrollTop, overrideScroll=true) -> @@ -1183,65 +1182,6 @@ class TextEditorPresenter rect - observeDecoration: (decoration) -> - decorationDisposables = new CompositeDisposable - if decoration.isType('highlight') - decorationDisposables.add decoration.onDidFlash => - @shouldUpdateDecorations = true - @emitDidUpdateState() - - decorationDisposables.add decoration.onDidChangeProperties (event) => - @decorationPropertiesDidChange(decoration, event) - decorationDisposables.add decoration.onDidDestroy => - @disposables.remove(decorationDisposables) - decorationDisposables.dispose() - @didDestroyDecoration(decoration) - @disposables.add(decorationDisposables) - - decorationPropertiesDidChange: (decoration, {oldProperties}) -> - @shouldUpdateDecorations = true - if decoration.isType('line') or decoration.isType('gutter') - if decoration.isType('line') or Decoration.isType(oldProperties, 'line') - @shouldUpdateLinesState = true - if decoration.isType('line-number') or Decoration.isType(oldProperties, 'line-number') - @shouldUpdateLineNumbersState = true - if (decoration.isType('gutter') and not decoration.isType('line-number')) or - (Decoration.isType(oldProperties, 'gutter') and not Decoration.isType(oldProperties, 'line-number')) - @shouldUpdateCustomGutterDecorationState = true - else if decoration.isType('overlay') - @shouldUpdateOverlaysState = true - @emitDidUpdateState() - - didDestroyDecoration: (decoration) -> - @shouldUpdateDecorations = true - if decoration.isType('line') or decoration.isType('gutter') - @shouldUpdateLinesState = true if decoration.isType('line') - if decoration.isType('line-number') - @shouldUpdateLineNumbersState = true - else if decoration.isType('gutter') - @shouldUpdateCustomGutterDecorationState = true - if decoration.isType('overlay') - @shouldUpdateOverlaysState = true - - @emitDidUpdateState() - - didAddDecoration: (decoration) -> - @observeDecoration(decoration) - - if decoration.isType('line') or decoration.isType('gutter') - @shouldUpdateDecorations = true - @shouldUpdateLinesState = true if decoration.isType('line') - if decoration.isType('line-number') - @shouldUpdateLineNumbersState = true - else if decoration.isType('gutter') - @shouldUpdateCustomGutterDecorationState = true - else if decoration.isType('highlight') - @shouldUpdateDecorations = true - else if decoration.isType('overlay') - @shouldUpdateOverlaysState = true - - @emitDidUpdateState() - fetchDecorations: -> @decorations = [] diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 36639251b..6fe9c2586 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -436,6 +436,9 @@ class TextEditor extends Model onDidRemoveDecoration: (callback) -> @displayBuffer.onDidRemoveDecoration(callback) + onDidUpdateDecorations: (callback) -> + @displayBuffer.onDidUpdateDecorations(callback) + # Extended: Calls your `callback` when the placeholder text is changed. # # * `callback` {Function} @@ -468,6 +471,9 @@ class TextEditor extends Model onDidUpdateMarkers: (callback) -> @displayBuffer.onDidUpdateMarkers(callback) + onDidUpdateDecorations: (callback) -> + @displayBuffer.onDidUpdateDecorations(callback) + # Essential: Retrieves the current {TextBuffer}. getBuffer: -> @buffer From 1aefb22789793cbfe3f51ede79a42aa63eccf8df Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 23 Oct 2015 18:13:30 -0600 Subject: [PATCH 03/53] Add ViewRegistry.prototype.getNextUpdatePromise Signed-off-by: Max Brunsfeld --- spec/view-registry-spec.coffee | 18 ++++++++++++++++++ src/view-registry.coffee | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee index fcddd325a..a2b4965a5 100644 --- a/spec/view-registry-spec.coffee +++ b/spec/view-registry-spec.coffee @@ -209,3 +209,21 @@ describe "ViewRegistry", -> window.dispatchEvent(new UIEvent('resize')) expect(events).toEqual ['poll 1', 'poll 2'] + + describe "::getNextUpdatePromise()", -> + it "returns a promise that resolves at the end of the next update cycle", -> + updateCalled = false + readCalled = false + pollCalled = false + + waitsFor 'getNextUpdatePromise to resolve', (done) -> + registry.getNextUpdatePromise().then -> + expect(updateCalled).toBe true + expect(readCalled).toBe true + expect(pollCalled).toBe true + done() + + registry.updateDocument -> updateCalled = true + registry.readDocument -> readCalled = true + registry.pollDocument -> pollCalled = true + registry.pollAfterNextUpdate() diff --git a/src/view-registry.coffee b/src/view-registry.coffee index 3a46aa87a..c21622c04 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -195,6 +195,10 @@ class ViewRegistry pollAfterNextUpdate: -> @performDocumentPollAfterUpdate = true + getNextUpdatePromise: -> + @nextUpdatePromise ?= new Promise (resolve) => + @resolveNextUpdatePromise = resolve + clearDocumentRequests: -> @documentReaders = [] @documentWriters = [] @@ -220,6 +224,9 @@ class ViewRegistry # process updates requested as a result of reads writer() while writer = @documentWriters.shift() + @nextUpdatePromise = null + @resolveNextUpdatePromise?() + startPollingDocument: -> window.addEventListener('resize', @requestDocumentPoll) @observer.observe(document, {subtree: true, childList: true, attributes: true}) From 4b5e4d02eb29baaa3566a392bbd42580a335ddfc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 24 Oct 2015 00:04:58 -0600 Subject: [PATCH 04/53] Use real clock in text-editor-component-spec This will make it much easier to test asynchronous update logic. --- spec/text-editor-component-spec.coffee | 3760 ++++++++++++++---------- src/text-editor-component.coffee | 2 +- 2 files changed, 2180 insertions(+), 1582 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index e5953a293..474646bd5 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -4,28 +4,17 @@ _ = require 'underscore-plus' TextEditorElement = require '../src/text-editor-element' nbsp = String.fromCharCode(160) -fdescribe "TextEditorComponent", -> +describe "TextEditorComponent", -> [contentNode, editor, wrapperNode, component, componentNode, verticalScrollbarNode, horizontalScrollbarNode] = [] - [lineHeightInPixels, charWidth, nextAnimationFrame, noAnimationFrame, tileSize, tileHeightInPixels] = [] + [lineHeightInPixels, charWidth, tileSize, tileHeightInPixels] = [] beforeEach -> tileSize = 3 + jasmine.useRealClock() waitsForPromise -> atom.packages.activatePackage('language-javascript') - runs -> - spyOn(window, "setInterval").andCallFake window.fakeSetInterval - spyOn(window, "clearInterval").andCallFake window.fakeClearInterval - - noAnimationFrame = -> throw new Error('No animation frame requested') - nextAnimationFrame = noAnimationFrame - - spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> - nextAnimationFrame = -> - nextAnimationFrame = noAnimationFrame - fn() - waitsForPromise -> atom.workspace.open('sample.js').then (o) -> editor = o @@ -52,11 +41,8 @@ fdescribe "TextEditorComponent", -> horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') component.measureDimensions() - nextAnimationFrame() - # Mutating the DOM in the previous frame causes a document poll; clear it here - waits 0 - runs -> nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() afterEach -> contentNode.style.width = '' @@ -70,16 +56,16 @@ fdescribe "TextEditorComponent", -> component.presenter.startRow = -1 component.presenter.endRow = 9999 - expect(nextAnimationFrame).not.toThrow() + waitsForPromise -> atom.views.getNextUpdatePromise() it "doesn't update when an animation frame was requested but the component got destroyed before its delivery", -> editor.setText("You shouldn't see this update.") - expect(nextAnimationFrame).not.toBe(noAnimationFrame) - component.destroy() - nextAnimationFrame() - expect(component.lineNodeForScreenRow(0).textContent).not.toBe("You shouldn't see this update.") + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(0).textContent).not.toBe("You shouldn't see this update.") describe "line rendering", -> expectTileContainsRow = (tileNode, screenRow, {top}) -> @@ -97,189 +83,201 @@ fdescribe "TextEditorComponent", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) + runs -> + expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) - wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' - component.measureDimensions() - nextAnimationFrame() + wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' + component.measureDimensions() - expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) it "renders higher tiles in front of lower ones", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - tilesNodes = component.tileNodesForLines() + runs -> + tilesNodes = component.tileNodesForLines() - expect(tilesNodes[0].style.zIndex).toBe("2") - expect(tilesNodes[1].style.zIndex).toBe("1") - expect(tilesNodes[2].style.zIndex).toBe("0") + expect(tilesNodes[0].style.zIndex).toBe("2") + expect(tilesNodes[1].style.zIndex).toBe("1") + expect(tilesNodes[2].style.zIndex).toBe("0") - verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - nextAnimationFrame() + verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - tilesNodes = component.tileNodesForLines() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(tilesNodes[0].style.zIndex).toBe("3") - expect(tilesNodes[1].style.zIndex).toBe("2") - expect(tilesNodes[2].style.zIndex).toBe("1") - expect(tilesNodes[3].style.zIndex).toBe("0") + runs -> + tilesNodes = component.tileNodesForLines() + + expect(tilesNodes[0].style.zIndex).toBe("3") + expect(tilesNodes[1].style.zIndex).toBe("2") + expect(tilesNodes[2].style.zIndex).toBe("1") + expect(tilesNodes[3].style.zIndex).toBe("0") it "renders the currently-visible lines in a tiled fashion", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - tilesNodes = component.tileNodesForLines() + runs -> + tilesNodes = component.tileNodesForLines() - expect(tilesNodes.length).toBe(3) + expect(tilesNodes.length).toBe(3) - expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(tilesNodes[0].querySelectorAll(".line").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(tilesNodes[0].querySelectorAll(".line").length).toBe(tileSize) + expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) - expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)" - expect(tilesNodes[1].querySelectorAll(".line").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)" + expect(tilesNodes[1].querySelectorAll(".line").length).toBe(tileSize) + expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) - expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels}px, 0px)" - expect(tilesNodes[2].querySelectorAll(".line").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) + expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels}px, 0px)" + expect(tilesNodes[2].querySelectorAll(".line").length).toBe(tileSize) + expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) - expect(component.lineNodeForScreenRow(9)).toBeUndefined() + expect(component.lineNodeForScreenRow(9)).toBeUndefined() - verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5 - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - nextAnimationFrame() + verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5 + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - tilesNodes = component.tileNodesForLines() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(component.lineNodeForScreenRow(2)).toBeUndefined() - expect(tilesNodes.length).toBe(3) + runs -> + tilesNodes = component.tileNodesForLines() - expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{0 * tileHeightInPixels - 5}px, 0px)" - expect(tilesNodes[0].querySelectorAll(".line").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[0], 3, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 4, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 5, top: 2 * lineHeightInPixels) + expect(component.lineNodeForScreenRow(2)).toBeUndefined() + expect(tilesNodes.length).toBe(3) - expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels - 5}px, 0px)" - expect(tilesNodes[1].querySelectorAll(".line").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[1], 6, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 7, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 8, top: 2 * lineHeightInPixels) + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{0 * tileHeightInPixels - 5}px, 0px)" + expect(tilesNodes[0].querySelectorAll(".line").length).toBe(tileSize) + expectTileContainsRow(tilesNodes[0], 3, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 4, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 5, top: 2 * lineHeightInPixels) - expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels - 5}px, 0px)" - expect(tilesNodes[2].querySelectorAll(".line").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[2], 9, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[2], 10, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[2], 11, top: 2 * lineHeightInPixels) + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels - 5}px, 0px)" + expect(tilesNodes[1].querySelectorAll(".line").length).toBe(tileSize) + expectTileContainsRow(tilesNodes[1], 6, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 7, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 8, top: 2 * lineHeightInPixels) + + expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels - 5}px, 0px)" + expect(tilesNodes[2].querySelectorAll(".line").length).toBe(tileSize) + expectTileContainsRow(tilesNodes[2], 9, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 10, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 11, top: 2 * lineHeightInPixels) it "updates the top position of subsequent tiles when lines are inserted or removed", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() editor.getBuffer().deleteRows(0, 1) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - tilesNodes = component.tileNodesForLines() + runs -> + tilesNodes = component.tileNodesForLines() - expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) - expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)" - expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)" + expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) - editor.getBuffer().insert([0, 0], '\n\n') - nextAnimationFrame() + editor.getBuffer().insert([0, 0], '\n\n') - tilesNodes = component.tileNodesForLines() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) + runs -> + tilesNodes = component.tileNodesForLines() - expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)" - expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) - expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels}px, 0px)" - expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)" + expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) + + expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels}px, 0px)" + expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) it "updates the lines when lines are inserted or removed above the rendered row range", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() - verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - nextAnimationFrame() - buffer = editor.getBuffer() + waitsForPromise -> atom.views.getNextUpdatePromise() - buffer.insert([0, 0], '\n\n') - nextAnimationFrame() - expect(component.lineNodeForScreenRow(3).textContent).toBe editor.tokenizedLineForScreenRow(3).text + runs -> + verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - buffer.delete([[0, 0], [3, 0]]) - nextAnimationFrame() - expect(component.lineNodeForScreenRow(3).textContent).toBe editor.tokenizedLineForScreenRow(3).text + waitsForPromise -> atom.views.getNextUpdatePromise() + + buffer = null + runs -> + buffer = editor.getBuffer() + buffer.insert([0, 0], '\n\n') + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(3).textContent).toBe editor.tokenizedLineForScreenRow(3).text + + buffer.delete([[0, 0], [3, 0]]) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(3).textContent).toBe editor.tokenizedLineForScreenRow(3).text it "updates the top position of lines when the line height changes", -> initialLineHeightInPixels = editor.getLineHeightInPixels() component.setLineHeight(2) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - newLineHeightInPixels = editor.getLineHeightInPixels() - expect(newLineHeightInPixels).not.toBe initialLineHeightInPixels - expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * newLineHeightInPixels + runs -> + newLineHeightInPixels = editor.getLineHeightInPixels() + expect(newLineHeightInPixels).not.toBe initialLineHeightInPixels + expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * newLineHeightInPixels it "updates the top position of lines when the font size changes", -> initialLineHeightInPixels = editor.getLineHeightInPixels() component.setFontSize(10) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - newLineHeightInPixels = editor.getLineHeightInPixels() - expect(newLineHeightInPixels).not.toBe initialLineHeightInPixels - expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * newLineHeightInPixels - - xit "updates the top position of lines when the font family changes", -> - # Can't find a font that changes the line height, but we think one might exist - linesComponent = component.refs.lines - spyOn(linesComponent, 'measureLineHeightAndDefaultCharWidth').andCallFake -> editor.setLineHeightInPixels(10) - - initialLineHeightInPixels = editor.getLineHeightInPixels() - component.setFontFamily('sans-serif') - nextAnimationFrame() - - expect(linesComponent.measureLineHeightAndDefaultCharWidth).toHaveBeenCalled() - newLineHeightInPixels = editor.getLineHeightInPixels() - expect(newLineHeightInPixels).not.toBe initialLineHeightInPixels - expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * newLineHeightInPixels + runs -> + newLineHeightInPixels = editor.getLineHeightInPixels() + expect(newLineHeightInPixels).not.toBe initialLineHeightInPixels + expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * newLineHeightInPixels it "renders the .lines div at the full height of the editor if there aren't enough lines to scroll vertically", -> editor.setText('') wrapperNode.style.height = '300px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - linesNode = componentNode.querySelector('.lines') - expect(linesNode.offsetHeight).toBe 300 + runs -> + linesNode = componentNode.querySelector('.lines') + expect(linesNode.offsetHeight).toBe 300 it "assigns the width of each line so it extends across the full width of the editor", -> gutterWidth = componentNode.querySelector('.gutter').offsetWidth @@ -288,25 +286,30 @@ fdescribe "TextEditorComponent", -> componentNode.style.width = gutterWidth + (30 * charWidth) + 'px' component.measureDimensions() - nextAnimationFrame() - expect(wrapperNode.getScrollWidth()).toBeGreaterThan scrollViewNode.offsetWidth + waitsForPromise -> atom.views.getNextUpdatePromise() - # At the time of writing, using width: 100% to achieve the full-width - # lines caused full-screen repaints after switching away from an editor - # and back again Please ensure you don't cause a performance regression if - # you change this behavior. - editorFullWidth = wrapperNode.getScrollWidth() + wrapperNode.getVerticalScrollbarWidth() + runs -> + expect(wrapperNode.getScrollWidth()).toBeGreaterThan scrollViewNode.offsetWidth - for lineNode in lineNodes - expect(lineNode.getBoundingClientRect().width).toBe(editorFullWidth) + # At the time of writing, using width: 100% to achieve the full-width + # lines caused full-screen repaints after switching away from an editor + # and back again Please ensure you don't cause a performance regression if + # you change this behavior. + editorFullWidth = wrapperNode.getScrollWidth() + wrapperNode.getVerticalScrollbarWidth() - componentNode.style.width = gutterWidth + wrapperNode.getScrollWidth() + 100 + 'px' - component.measureDimensions() - nextAnimationFrame() - scrollViewWidth = scrollViewNode.offsetWidth + for lineNode in lineNodes + expect(lineNode.getBoundingClientRect().width).toBe(editorFullWidth) - for lineNode in lineNodes - expect(lineNode.getBoundingClientRect().width).toBe(scrollViewWidth) + componentNode.style.width = gutterWidth + wrapperNode.getScrollWidth() + 100 + 'px' + component.measureDimensions() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + scrollViewWidth = scrollViewNode.offsetWidth + + for lineNode in lineNodes + expect(lineNode.getBoundingClientRect().width).toBe(scrollViewWidth) it "renders an nbsp on empty lines when no line-ending character is defined", -> atom.config.set("editor.showInvisibles", false) @@ -321,76 +324,79 @@ fdescribe "TextEditorComponent", -> expect(tileNode.style.backgroundColor).toBe(backgroundColor) wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)' - atom.views.performDocumentPoll() - advanceClock(atom.views.documentPollingInterval) - nextAnimationFrame() - expect(linesNode.style.backgroundColor).toBe 'rgb(255, 0, 0)' - for tileNode in component.tileNodesForLines() - expect(tileNode.style.backgroundColor).toBe("rgb(255, 0, 0)") + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(linesNode.style.backgroundColor).toBe 'rgb(255, 0, 0)' + for tileNode in component.tileNodesForLines() + expect(tileNode.style.backgroundColor).toBe("rgb(255, 0, 0)") it "applies .leading-whitespace for lines with leading spaces and/or tabs", -> editor.setText(' a') - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe true - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe false + runs -> + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe true + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe false - editor.setText('\ta') - nextAnimationFrame() + editor.setText('\ta') - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe true - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe true + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe false it "applies .trailing-whitespace for lines with trailing spaces and/or tabs", -> editor.setText(' ') - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false + runs -> + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false - editor.setText('\t') - nextAnimationFrame() + editor.setText('\t') - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.setText('a ') - nextAnimationFrame() + runs -> + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false + editor.setText('a ') - editor.setText('a\t') - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false + runs -> + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false + + editor.setText('a\t') + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false it "keeps rebuilding lines when continuous reflow is on", -> wrapperNode.setContinuousReflow(true) oldLineNodes = componentNode.querySelectorAll(".line") - advanceClock(10) - expect(nextAnimationFrame).toBe(noAnimationFrame) + waits 300 - advanceClock(component.presenter.minimumReflowInterval - 10) - nextAnimationFrame() + runs -> + newLineNodes = componentNode.querySelectorAll(".line") + expect(oldLineNodes).not.toEqual(newLineNodes) - newLineNodes = componentNode.querySelectorAll(".line") - expect(oldLineNodes).not.toEqual(newLineNodes) - - wrapperNode.setContinuousReflow(false) - advanceClock(component.presenter.minimumReflowInterval) - expect(nextAnimationFrame).toBe(noAnimationFrame) + wrapperNode.setContinuousReflow(false) describe "when showInvisibles is enabled", -> invisibles = null @@ -404,82 +410,114 @@ fdescribe "TextEditorComponent", -> atom.config.set("editor.showInvisibles", true) atom.config.set("editor.invisibles", invisibles) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() it "re-renders the lines when the showInvisibles config option changes", -> editor.setText " a line with tabs\tand spaces \n" - nextAnimationFrame() - expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}" + waitsForPromise -> atom.views.getNextUpdatePromise() - atom.config.set("editor.showInvisibles", false) - nextAnimationFrame() - expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces " + runs -> + 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", true) - nextAnimationFrame() - 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) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces " + + atom.config.set("editor.showInvisibles", true) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}" it "displays leading/trailing spaces, tabs, and newlines as visible characters", -> editor.setText " a line with tabs\tand spaces \n" - nextAnimationFrame() - expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}" + waitsForPromise -> atom.views.getNextUpdatePromise() - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('invisible-character')).toBe true - expect(leafNodes[leafNodes.length - 1].classList.contains('invisible-character')).toBe true + runs -> + expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}" + + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('invisible-character')).toBe true + expect(leafNodes[leafNodes.length - 1].classList.contains('invisible-character')).toBe true it "displays newlines as their own token outside of the other tokens' scopeDescriptor", -> editor.setText "var\n" - nextAnimationFrame() - expect(component.lineNodeForScreenRow(0).innerHTML).toBe "var#{invisibles.eol}" + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + 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" - nextAnimationFrame() - expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that ends with a carriage return#{invisibles.cr}#{invisibles.eol}" + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that ends with a carriage return#{invisibles.cr}#{invisibles.eol}" it "renders invisible line-ending characters on empty lines", -> expect(component.lineNodeForScreenRow(10).textContent).toBe invisibles.eol it "renders an nbsp on empty lines when the line-ending character is an empty string", -> atom.config.set("editor.invisibles", eol: '') - nextAnimationFrame() - expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp it "renders an nbsp on empty lines when the line-ending character is false", -> atom.config.set("editor.invisibles", eol: false) - nextAnimationFrame() - expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp it "interleaves invisible line-ending characters with indent guides on empty lines", -> atom.config.set "editor.showIndentGuide", true - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.setTextInBufferRange([[10, 0], [11, 0]], "\r\n", normalizeLineEndings: false) - nextAnimationFrame() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' + runs -> + editor.setTextInBufferRange([[10, 0], [11, 0]], "\r\n", normalizeLineEndings: false) - editor.setTabLength(3) - nextAnimationFrame() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE ' + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.setTabLength(1) - nextAnimationFrame() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' + runs -> + expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' - editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ') - editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ') - nextAnimationFrame() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' + editor.setTabLength(3) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE ' + + editor.setTabLength(1) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' + + editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ') + editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ') + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' describe "when soft wrapping is enabled", -> beforeEach -> editor.setText "a line that wraps \n" editor.setSoftWrapped(true) - nextAnimationFrame() - componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' + component.measureDimensions() + waitsForPromise -> atom.views.getNextUpdatePromise() it "doesn't show end of line invisibles at the end of wrapped lines", -> expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that " @@ -488,7 +526,7 @@ fdescribe "TextEditorComponent", -> describe "when indent guides are enabled", -> beforeEach -> atom.config.set "editor.showIndentGuide", true - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() it "adds an 'indent-guide' class to spans comprising the leading whitespace", -> line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) @@ -505,78 +543,91 @@ fdescribe "TextEditorComponent", -> it "renders leading whitespace spans with the 'indent-guide' class for empty lines", -> editor.getBuffer().insert([1, Infinity], '\n') - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) + runs -> + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes.length).toBe 2 - expect(line2LeafNodes[0].textContent).toBe ' ' - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[1].textContent).toBe ' ' - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true + expect(line2LeafNodes.length).toBe 2 + expect(line2LeafNodes[0].textContent).toBe ' ' + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true + expect(line2LeafNodes[1].textContent).toBe ' ' + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true it "renders indent guides correctly on lines containing only whitespace", -> editor.getBuffer().insert([1, Infinity], '\n ') - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes.length).toBe 3 - expect(line2LeafNodes[0].textContent).toBe ' ' - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[1].textContent).toBe ' ' - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[2].textContent).toBe ' ' - expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe true + runs -> + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) + expect(line2LeafNodes.length).toBe 3 + expect(line2LeafNodes[0].textContent).toBe ' ' + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true + expect(line2LeafNodes[1].textContent).toBe ' ' + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true + expect(line2LeafNodes[2].textContent).toBe ' ' + expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe true it "renders indent guides correctly on lines containing only whitespace when invisibles are enabled", -> atom.config.set 'editor.showInvisibles', true atom.config.set 'editor.invisibles', space: '-', eol: 'x' editor.getBuffer().insert([1, Infinity], '\n ') - nextAnimationFrame() - line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes.length).toBe 4 - expect(line2LeafNodes[0].textContent).toBe '--' - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[1].textContent).toBe '--' - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[2].textContent).toBe '--' - expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[3].textContent).toBe 'x' + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) + expect(line2LeafNodes.length).toBe 4 + expect(line2LeafNodes[0].textContent).toBe '--' + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true + expect(line2LeafNodes[1].textContent).toBe '--' + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true + expect(line2LeafNodes[2].textContent).toBe '--' + expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe true + expect(line2LeafNodes[3].textContent).toBe 'x' it "does not render indent guides in trailing whitespace for lines containing non whitespace characters", -> editor.getBuffer().setText " hi " - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(line0LeafNodes[0].textContent).toBe ' ' - expect(line0LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line0LeafNodes[1].textContent).toBe ' ' - expect(line0LeafNodes[1].classList.contains('indent-guide')).toBe false + runs -> + line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(line0LeafNodes[0].textContent).toBe ' ' + expect(line0LeafNodes[0].classList.contains('indent-guide')).toBe true + expect(line0LeafNodes[1].textContent).toBe ' ' + expect(line0LeafNodes[1].classList.contains('indent-guide')).toBe false it "updates the indent guides on empty lines preceding an indentation change", -> editor.getBuffer().insert([12, 0], '\n') - nextAnimationFrame() - editor.getBuffer().insert([13, 0], ' ') - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - line12LeafNodes = getLeafNodes(component.lineNodeForScreenRow(12)) - expect(line12LeafNodes[0].textContent).toBe ' ' - expect(line12LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line12LeafNodes[1].textContent).toBe ' ' - expect(line12LeafNodes[1].classList.contains('indent-guide')).toBe true + runs -> + editor.getBuffer().insert([13, 0], ' ') + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + line12LeafNodes = getLeafNodes(component.lineNodeForScreenRow(12)) + expect(line12LeafNodes[0].textContent).toBe ' ' + expect(line12LeafNodes[0].classList.contains('indent-guide')).toBe true + expect(line12LeafNodes[1].textContent).toBe ' ' + expect(line12LeafNodes[1].classList.contains('indent-guide')).toBe true it "updates the indent guides on empty lines following an indentation change", -> editor.getBuffer().insert([12, 2], '\n') - nextAnimationFrame() - editor.getBuffer().insert([12, 0], ' ') - nextAnimationFrame() - line13LeafNodes = getLeafNodes(component.lineNodeForScreenRow(13)) - expect(line13LeafNodes[0].textContent).toBe ' ' - expect(line13LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line13LeafNodes[1].textContent).toBe ' ' - expect(line13LeafNodes[1].classList.contains('indent-guide')).toBe true + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> editor.getBuffer().insert([12, 0], ' ') + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + line13LeafNodes = getLeafNodes(component.lineNodeForScreenRow(13)) + expect(line13LeafNodes[0].textContent).toBe ' ' + expect(line13LeafNodes[0].classList.contains('indent-guide')).toBe true + expect(line13LeafNodes[1].textContent).toBe ' ' + expect(line13LeafNodes[1].classList.contains('indent-guide')).toBe true describe "when indent guides are disabled", -> beforeEach -> @@ -584,22 +635,27 @@ fdescribe "TextEditorComponent", -> it "does not render indent guides on lines containing only whitespace", -> editor.getBuffer().insert([1, Infinity], '\n ') - nextAnimationFrame() - line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes.length).toBe 3 - expect(line2LeafNodes[0].textContent).toBe ' ' - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe false - expect(line2LeafNodes[1].textContent).toBe ' ' - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe false - expect(line2LeafNodes[2].textContent).toBe ' ' - expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) + expect(line2LeafNodes.length).toBe 3 + expect(line2LeafNodes[0].textContent).toBe ' ' + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe false + expect(line2LeafNodes[1].textContent).toBe ' ' + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe false + expect(line2LeafNodes[2].textContent).toBe ' ' + expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe false describe "when the buffer contains null bytes", -> it "excludes the null byte from character measurement", -> editor.setText("a\0b") - nextAnimationFrame() - expect(wrapperNode.pixelPositionForScreenPosition([0, Infinity]).left).toEqual 2 * charWidth + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(wrapperNode.pixelPositionForScreenPosition([0, Infinity]).left).toEqual 2 * charWidth describe "when there is a fold", -> it "renders a fold marker on the folded line", -> @@ -607,14 +663,19 @@ fdescribe "TextEditorComponent", -> expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() editor.foldBufferRow(4) - nextAnimationFrame() - foldedLineNode = component.lineNodeForScreenRow(4) - expect(foldedLineNode.querySelector('.fold-marker')).toBeTruthy() + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.unfoldBufferRow(4) - nextAnimationFrame() - foldedLineNode = component.lineNodeForScreenRow(4) - expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() + runs -> + foldedLineNode = component.lineNodeForScreenRow(4) + expect(foldedLineNode.querySelector('.fold-marker')).toBeTruthy() + + editor.unfoldBufferRow(4) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + foldedLineNode = component.lineNodeForScreenRow(4) + expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() describe "gutter rendering", -> expectTileContainsRow = (tileNode, screenRow, {top, text}) -> @@ -626,167 +687,195 @@ fdescribe "TextEditorComponent", -> it "renders higher tiles in front of lower ones", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - tilesNodes = component.tileNodesForLineNumbers() + runs -> + tilesNodes = component.tileNodesForLineNumbers() - expect(tilesNodes[0].style.zIndex).toBe("2") - expect(tilesNodes[1].style.zIndex).toBe("1") - expect(tilesNodes[2].style.zIndex).toBe("0") + expect(tilesNodes[0].style.zIndex).toBe("2") + expect(tilesNodes[1].style.zIndex).toBe("1") + expect(tilesNodes[2].style.zIndex).toBe("0") - verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - nextAnimationFrame() + verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - tilesNodes = component.tileNodesForLineNumbers() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(tilesNodes[0].style.zIndex).toBe("3") - expect(tilesNodes[1].style.zIndex).toBe("2") - expect(tilesNodes[2].style.zIndex).toBe("1") - expect(tilesNodes[3].style.zIndex).toBe("0") + runs -> + tilesNodes = component.tileNodesForLineNumbers() + + expect(tilesNodes[0].style.zIndex).toBe("3") + expect(tilesNodes[1].style.zIndex).toBe("2") + expect(tilesNodes[2].style.zIndex).toBe("1") + expect(tilesNodes[3].style.zIndex).toBe("0") it "gives the line numbers container the same height as the wrapper node", -> linesNode = componentNode.querySelector(".line-numbers") wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) + runs -> + expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) - wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' - component.measureDimensions() - nextAnimationFrame() + wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' + component.measureDimensions() - expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) it "renders the currently-visible line numbers in a tiled fashion", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - tilesNodes = component.tileNodesForLineNumbers() + runs -> + tilesNodes = component.tileNodesForLineNumbers() - expect(tilesNodes.length).toBe(3) - expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(tilesNodes.length).toBe(3) + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(tilesNodes[0].querySelectorAll('.line-number').length).toBe 3 - expectTileContainsRow(tilesNodes[0], 0, top: lineHeightInPixels * 0, text: "#{nbsp}1") - expectTileContainsRow(tilesNodes[0], 1, top: lineHeightInPixels * 1, text: "#{nbsp}2") - expectTileContainsRow(tilesNodes[0], 2, top: lineHeightInPixels * 2, text: "#{nbsp}3") + expect(tilesNodes[0].querySelectorAll('.line-number').length).toBe 3 + expectTileContainsRow(tilesNodes[0], 0, top: lineHeightInPixels * 0, text: "#{nbsp}1") + expectTileContainsRow(tilesNodes[0], 1, top: lineHeightInPixels * 1, text: "#{nbsp}2") + expectTileContainsRow(tilesNodes[0], 2, top: lineHeightInPixels * 2, text: "#{nbsp}3") - expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)" - expect(tilesNodes[1].querySelectorAll('.line-number').length).toBe 3 - expectTileContainsRow(tilesNodes[1], 3, top: lineHeightInPixels * 0, text: "#{nbsp}4") - expectTileContainsRow(tilesNodes[1], 4, top: lineHeightInPixels * 1, text: "#{nbsp}5") - expectTileContainsRow(tilesNodes[1], 5, top: lineHeightInPixels * 2, text: "#{nbsp}6") + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)" + expect(tilesNodes[1].querySelectorAll('.line-number').length).toBe 3 + expectTileContainsRow(tilesNodes[1], 3, top: lineHeightInPixels * 0, text: "#{nbsp}4") + expectTileContainsRow(tilesNodes[1], 4, top: lineHeightInPixels * 1, text: "#{nbsp}5") + expectTileContainsRow(tilesNodes[1], 5, top: lineHeightInPixels * 2, text: "#{nbsp}6") - expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels}px, 0px)" - expect(tilesNodes[2].querySelectorAll('.line-number').length).toBe 3 - expectTileContainsRow(tilesNodes[2], 6, top: lineHeightInPixels * 0, text: "#{nbsp}7") - expectTileContainsRow(tilesNodes[2], 7, top: lineHeightInPixels * 1, text: "#{nbsp}8") - expectTileContainsRow(tilesNodes[2], 8, top: lineHeightInPixels * 2, text: "#{nbsp}9") + expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels}px, 0px)" + expect(tilesNodes[2].querySelectorAll('.line-number').length).toBe 3 + expectTileContainsRow(tilesNodes[2], 6, top: lineHeightInPixels * 0, text: "#{nbsp}7") + expectTileContainsRow(tilesNodes[2], 7, top: lineHeightInPixels * 1, text: "#{nbsp}8") + expectTileContainsRow(tilesNodes[2], 8, top: lineHeightInPixels * 2, text: "#{nbsp}9") - verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5 - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - nextAnimationFrame() + verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5 + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - tilesNodes = component.tileNodesForLineNumbers() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(component.lineNumberNodeForScreenRow(2)).toBeUndefined() - expect(tilesNodes.length).toBe(3) + runs -> + tilesNodes = component.tileNodesForLineNumbers() - expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{0 * tileHeightInPixels - 5}px, 0px)" - expect(tilesNodes[0].querySelectorAll(".line-number").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[0], 3, top: lineHeightInPixels * 0, text: "#{nbsp}4") - expectTileContainsRow(tilesNodes[0], 4, top: lineHeightInPixels * 1, text: "#{nbsp}5") - expectTileContainsRow(tilesNodes[0], 5, top: lineHeightInPixels * 2, text: "#{nbsp}6") + expect(component.lineNumberNodeForScreenRow(2)).toBeUndefined() + expect(tilesNodes.length).toBe(3) - expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels - 5}px, 0px)" - expect(tilesNodes[1].querySelectorAll(".line-number").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[1], 6, top: 0 * lineHeightInPixels, text: "#{nbsp}7") - expectTileContainsRow(tilesNodes[1], 7, top: 1 * lineHeightInPixels, text: "#{nbsp}8") - expectTileContainsRow(tilesNodes[1], 8, top: 2 * lineHeightInPixels, text: "#{nbsp}9") + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{0 * tileHeightInPixels - 5}px, 0px)" + expect(tilesNodes[0].querySelectorAll(".line-number").length).toBe(tileSize) + expectTileContainsRow(tilesNodes[0], 3, top: lineHeightInPixels * 0, text: "#{nbsp}4") + expectTileContainsRow(tilesNodes[0], 4, top: lineHeightInPixels * 1, text: "#{nbsp}5") + expectTileContainsRow(tilesNodes[0], 5, top: lineHeightInPixels * 2, text: "#{nbsp}6") - expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels - 5}px, 0px)" - expect(tilesNodes[2].querySelectorAll(".line-number").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[2], 9, top: 0 * lineHeightInPixels, text: "10") - expectTileContainsRow(tilesNodes[2], 10, top: 1 * lineHeightInPixels, text: "11") - expectTileContainsRow(tilesNodes[2], 11, top: 2 * lineHeightInPixels, text: "12") + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels - 5}px, 0px)" + expect(tilesNodes[1].querySelectorAll(".line-number").length).toBe(tileSize) + expectTileContainsRow(tilesNodes[1], 6, top: 0 * lineHeightInPixels, text: "#{nbsp}7") + expectTileContainsRow(tilesNodes[1], 7, top: 1 * lineHeightInPixels, text: "#{nbsp}8") + expectTileContainsRow(tilesNodes[1], 8, top: 2 * lineHeightInPixels, text: "#{nbsp}9") + + expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels - 5}px, 0px)" + expect(tilesNodes[2].querySelectorAll(".line-number").length).toBe(tileSize) + expectTileContainsRow(tilesNodes[2], 9, top: 0 * lineHeightInPixels, text: "10") + expectTileContainsRow(tilesNodes[2], 10, top: 1 * lineHeightInPixels, text: "11") + expectTileContainsRow(tilesNodes[2], 11, top: 2 * lineHeightInPixels, text: "12") it "updates the translation of subsequent line numbers when lines are inserted or removed", -> editor.getBuffer().insert([0, 0], '\n\n') - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - lineNumberNodes = componentNode.querySelectorAll('.line-number') - expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 0 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 2 * lineHeightInPixels + runs -> + lineNumberNodes = componentNode.querySelectorAll('.line-number') + expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 0 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 2 * lineHeightInPixels - editor.getBuffer().insert([0, 0], '\n\n') - nextAnimationFrame() + editor.getBuffer().insert([0, 0], '\n\n') - expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 0 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 2 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(6).offsetTop).toBe 0 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(7).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(8).offsetTop).toBe 2 * lineHeightInPixels + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 0 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 2 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(6).offsetTop).toBe 0 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(7).offsetTop).toBe 1 * lineHeightInPixels + expect(component.lineNumberNodeForScreenRow(8).offsetTop).toBe 2 * lineHeightInPixels it "renders • characters for soft-wrapped lines", -> editor.setSoftWrapped(true) wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 30 * charWidth + 'px' component.measureDimensions() - nextAnimationFrame() - expect(componentNode.querySelectorAll('.line-number').length).toBe 9 + 1 # 3 line-numbers tiles + 1 dummy line - expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" - expect(component.lineNumberNodeForScreenRow(1).textContent).toBe "#{nbsp}•" - expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}2" - expect(component.lineNumberNodeForScreenRow(3).textContent).toBe "#{nbsp}•" - expect(component.lineNumberNodeForScreenRow(4).textContent).toBe "#{nbsp}3" - expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}•" - expect(component.lineNumberNodeForScreenRow(6).textContent).toBe "#{nbsp}4" - expect(component.lineNumberNodeForScreenRow(7).textContent).toBe "#{nbsp}•" - expect(component.lineNumberNodeForScreenRow(8).textContent).toBe "#{nbsp}•" + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.querySelectorAll('.line-number').length).toBe 9 + 1 # 3 line-numbers tiles + 1 dummy line + expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" + expect(component.lineNumberNodeForScreenRow(1).textContent).toBe "#{nbsp}•" + expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}2" + expect(component.lineNumberNodeForScreenRow(3).textContent).toBe "#{nbsp}•" + expect(component.lineNumberNodeForScreenRow(4).textContent).toBe "#{nbsp}3" + expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}•" + expect(component.lineNumberNodeForScreenRow(6).textContent).toBe "#{nbsp}4" + expect(component.lineNumberNodeForScreenRow(7).textContent).toBe "#{nbsp}•" + expect(component.lineNumberNodeForScreenRow(8).textContent).toBe "#{nbsp}•" it "pads line numbers to be right-justified based on the maximum number of line number digits", -> editor.getBuffer().setText([1..10].join('\n')) - nextAnimationFrame() - for screenRow in [0..8] - expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" - expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" - gutterNode = componentNode.querySelector('.gutter') - initialGutterWidth = gutterNode.offsetWidth + waitsForPromise -> atom.views.getNextUpdatePromise() - # Removes padding when the max number of digits goes down - editor.getBuffer().delete([[1, 0], [2, 0]]) - nextAnimationFrame() - for screenRow in [0..8] - expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{screenRow + 1}" - expect(gutterNode.offsetWidth).toBeLessThan initialGutterWidth + [gutterNode, initialGutterWidth] = [] - # Increases padding when the max number of digits goes up - editor.getBuffer().insert([0, 0], '\n\n') - nextAnimationFrame() - for screenRow in [0..8] - expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" - expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" - expect(gutterNode.offsetWidth).toBe initialGutterWidth + runs -> + for screenRow in [0..8] + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" + expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" + + gutterNode = componentNode.querySelector('.gutter') + initialGutterWidth = gutterNode.offsetWidth + + # Removes padding when the max number of digits goes down + editor.getBuffer().delete([[1, 0], [2, 0]]) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + 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') + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + for screenRow in [0..8] + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" + expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" + expect(gutterNode.offsetWidth).toBe initialGutterWidth it "renders the .line-numbers div at the full height of the editor even if it's taller than its content", -> wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px' component.measureDimensions() - nextAnimationFrame() - expect(componentNode.querySelector('.line-numbers').offsetHeight).toBe componentNode.offsetHeight + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.querySelector('.line-numbers').offsetHeight).toBe componentNode.offsetHeight it "applies the background color of the gutter or the editor to the line numbers to improve GPU performance", -> gutterNode = componentNode.querySelector('.gutter') @@ -798,54 +887,57 @@ fdescribe "TextEditorComponent", -> # favor gutter color if it's assigned gutterNode.style.backgroundColor = 'rgb(255, 0, 0)' - atom.views.performDocumentPoll() + atom.views.performDocumentPoll() # required due to DOM change not being detected inside shadow DOM - nextAnimationFrame() - expect(lineNumbersNode.style.backgroundColor).toBe 'rgb(255, 0, 0)' - for tileNode in component.tileNodesForLineNumbers() - expect(tileNode.style.backgroundColor).toBe("rgb(255, 0, 0)") + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumbersNode.style.backgroundColor).toBe 'rgb(255, 0, 0)' + for tileNode in component.tileNodesForLineNumbers() + expect(tileNode.style.backgroundColor).toBe("rgb(255, 0, 0)") it "hides or shows the gutter based on the '::isLineNumberGutterVisible' property on the model and the global 'editor.showLineNumbers' config setting", -> expect(component.gutterContainerComponent.getLineNumberGutterComponent()?).toBe true editor.setLineNumberGutterVisible(false) - nextAnimationFrame() - expect(componentNode.querySelector('.gutter').style.display).toBe 'none' + waitsForPromise -> atom.views.getNextUpdatePromise() - atom.config.set("editor.showLineNumbers", false) - nextAnimationFrame() + runs -> + expect(componentNode.querySelector('.gutter').style.display).toBe 'none' - expect(componentNode.querySelector('.gutter').style.display).toBe 'none' + atom.config.set("editor.showLineNumbers", false) - editor.setLineNumberGutterVisible(true) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(componentNode.querySelector('.gutter').style.display).toBe 'none' + runs -> + expect(componentNode.querySelector('.gutter').style.display).toBe 'none' - atom.config.set("editor.showLineNumbers", true) - nextAnimationFrame() + editor.setLineNumberGutterVisible(true) - expect(componentNode.querySelector('.gutter').style.display).toBe '' - expect(component.lineNumberNodeForScreenRow(3)?).toBe true + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.querySelector('.gutter').style.display).toBe 'none' + + atom.config.set("editor.showLineNumbers", true) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.querySelector('.gutter').style.display).toBe '' + expect(component.lineNumberNodeForScreenRow(3)?).toBe true it "keeps rebuilding line numbers when continuous reflow is on", -> wrapperNode.setContinuousReflow(true) oldLineNodes = componentNode.querySelectorAll(".line-number") - advanceClock(10) - expect(nextAnimationFrame).toBe(noAnimationFrame) + waits 300 - advanceClock(component.presenter.minimumReflowInterval - 10) - nextAnimationFrame() - - newLineNodes = componentNode.querySelectorAll(".line-number") - expect(oldLineNodes).not.toEqual(newLineNodes) - - wrapperNode.setContinuousReflow(false) - advanceClock(component.presenter.minimumReflowInterval) - expect(nextAnimationFrame).toBe(noAnimationFrame) + runs -> + newLineNodes = componentNode.querySelectorAll(".line-number") + expect(oldLineNodes).not.toEqual(newLineNodes) describe "fold decorations", -> describe "rendering fold decorations", -> @@ -859,47 +951,64 @@ fdescribe "TextEditorComponent", -> it "updates the foldable class on the correct line numbers when the foldable positions change", -> editor.getBuffer().insert([0, 0], '\n') - nextAnimationFrame() - expect(lineNumberHasClass(0, 'foldable')).toBe false - expect(lineNumberHasClass(1, 'foldable')).toBe true - expect(lineNumberHasClass(2, 'foldable')).toBe true - expect(lineNumberHasClass(3, 'foldable')).toBe false - expect(lineNumberHasClass(4, 'foldable')).toBe false - expect(lineNumberHasClass(5, 'foldable')).toBe true - expect(lineNumberHasClass(6, 'foldable')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(0, 'foldable')).toBe false + expect(lineNumberHasClass(1, 'foldable')).toBe true + expect(lineNumberHasClass(2, 'foldable')).toBe true + expect(lineNumberHasClass(3, 'foldable')).toBe false + expect(lineNumberHasClass(4, 'foldable')).toBe false + expect(lineNumberHasClass(5, 'foldable')).toBe true + expect(lineNumberHasClass(6, 'foldable')).toBe false it "updates the foldable class on a line number that becomes foldable", -> expect(lineNumberHasClass(11, 'foldable')).toBe false editor.getBuffer().insert([11, 44], '\n fold me') - nextAnimationFrame() - expect(lineNumberHasClass(11, 'foldable')).toBe true - editor.undo() - nextAnimationFrame() - expect(lineNumberHasClass(11, 'foldable')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(11, 'foldable')).toBe true + editor.undo() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(11, 'foldable')).toBe false it "adds, updates and removes the folded class on the correct line number componentNodes", -> editor.foldBufferRow(4) - nextAnimationFrame() - expect(lineNumberHasClass(4, 'folded')).toBe true + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.getBuffer().insert([0, 0], '\n') - nextAnimationFrame() - expect(lineNumberHasClass(4, 'folded')).toBe false - expect(lineNumberHasClass(5, 'folded')).toBe true + runs -> + expect(lineNumberHasClass(4, 'folded')).toBe true + editor.getBuffer().insert([0, 0], '\n') - editor.unfoldBufferRow(5) - nextAnimationFrame() - expect(lineNumberHasClass(5, 'folded')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(4, 'folded')).toBe false + expect(lineNumberHasClass(5, 'folded')).toBe true + + editor.unfoldBufferRow(5) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(5, 'folded')).toBe false describe "when soft wrapping is enabled", -> beforeEach -> editor.setSoftWrapped(true) - nextAnimationFrame() - componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' + component.measureDimensions() + + waitsForPromise -> atom.views.getNextUpdatePromise() it "doesn't add the foldable class for soft-wrapped lines", -> expect(lineNumberHasClass(0, 'foldable')).toBe true @@ -916,220 +1025,254 @@ fdescribe "TextEditorComponent", -> describe "when the component is destroyed", -> it "stops listening for folding events", -> - nextAnimationFrame() unless nextAnimationFrame is noAnimationFrame # clear pending frame request if needed - component.destroy() lineNumber = component.lineNumberNodeForScreenRow(1) target = lineNumber.querySelector('.icon-right') target.dispatchEvent(buildClickEvent(target)) - expect(nextAnimationFrame).toBe(noAnimationFrame) - it "folds and unfolds the block represented by the fold indicator when clicked", -> expect(lineNumberHasClass(1, 'folded')).toBe false lineNumber = component.lineNumberNodeForScreenRow(1) target = lineNumber.querySelector('.icon-right') target.dispatchEvent(buildClickEvent(target)) - nextAnimationFrame() - expect(lineNumberHasClass(1, 'folded')).toBe true + waitsForPromise -> atom.views.getNextUpdatePromise() - lineNumber = component.lineNumberNodeForScreenRow(1) - target = lineNumber.querySelector('.icon-right') - target.dispatchEvent(buildClickEvent(target)) - nextAnimationFrame() - expect(lineNumberHasClass(1, 'folded')).toBe false + runs -> + expect(lineNumberHasClass(1, 'folded')).toBe true + + lineNumber = component.lineNumberNodeForScreenRow(1) + target = lineNumber.querySelector('.icon-right') + target.dispatchEvent(buildClickEvent(target)) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(1, 'folded')).toBe false it "does not fold when the line number componentNode is clicked", -> - nextAnimationFrame() unless nextAnimationFrame is noAnimationFrame # clear pending frame request if needed - lineNumber = component.lineNumberNodeForScreenRow(1) lineNumber.dispatchEvent(buildClickEvent(lineNumber)) - expect(nextAnimationFrame).toBe noAnimationFrame - expect(lineNumberHasClass(1, 'folded')).toBe false + waits 100 + runs -> + expect(lineNumberHasClass(1, 'folded')).toBe false describe "cursor rendering", -> it "renders the currently visible cursors", -> + [cursor1, cursor2, cursor3, cursorNodes] = [] + cursor1 = editor.getLastCursor() cursor1.setScreenPosition([0, 5], autoscroll: false) wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() - cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].offsetHeight).toBe lineHeightInPixels - expect(cursorNodes[0].offsetWidth).toBeCloseTo charWidth, 0 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(5 * charWidth)}px, #{0 * lineHeightInPixels}px)" + waitsForPromise -> atom.views.getNextUpdatePromise() - cursor2 = editor.addCursorAtScreenPosition([8, 11], autoscroll: false) - cursor3 = editor.addCursorAtScreenPosition([4, 10], autoscroll: false) - nextAnimationFrame() + runs -> + cursorNodes = componentNode.querySelectorAll('.cursor') + expect(cursorNodes.length).toBe 1 + expect(cursorNodes[0].offsetHeight).toBe lineHeightInPixels + expect(cursorNodes[0].offsetWidth).toBeCloseTo charWidth, 0 + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(5 * charWidth)}px, #{0 * lineHeightInPixels}px)" - cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe 2 - expect(cursorNodes[0].offsetTop).toBe 0 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(5 * charWidth)}px, #{0 * lineHeightInPixels}px)" - expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{Math.round(10 * charWidth)}px, #{4 * lineHeightInPixels}px)" + cursor2 = editor.addCursorAtScreenPosition([8, 11], autoscroll: false) + cursor3 = editor.addCursorAtScreenPosition([4, 10], autoscroll: false) - verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - nextAnimationFrame() - horizontalScrollbarNode.scrollLeft = 3.5 * charWidth - horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe 2 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(10 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" - expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" + runs -> + cursorNodes = componentNode.querySelectorAll('.cursor') + expect(cursorNodes.length).toBe 2 + expect(cursorNodes[0].offsetTop).toBe 0 + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(5 * charWidth)}px, #{0 * lineHeightInPixels}px)" + expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{Math.round(10 * charWidth)}px, #{4 * lineHeightInPixels}px)" - editor.onDidChangeCursorPosition cursorMovedListener = jasmine.createSpy('cursorMovedListener') - cursor3.setScreenPosition([4, 11], autoscroll: false) - nextAnimationFrame() - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" - expect(cursorMovedListener).toHaveBeenCalled() + verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels - cursor3.destroy() - nextAnimationFrame() - cursorNodes = componentNode.querySelectorAll('.cursor') + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" + runs -> + horizontalScrollbarNode.scrollLeft = 3.5 * charWidth + + waitsForPromise -> atom.views.getNextUpdatePromise() + + cursorMovedListener = null + runs -> + cursorNodes = componentNode.querySelectorAll('.cursor') + expect(cursorNodes.length).toBe 2 + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(10 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" + expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" + + editor.onDidChangeCursorPosition cursorMovedListener = jasmine.createSpy('cursorMovedListener') + cursor3.setScreenPosition([4, 11], autoscroll: false) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" + expect(cursorMovedListener).toHaveBeenCalled() + + cursor3.destroy() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + cursorNodes = componentNode.querySelectorAll('.cursor') + + expect(cursorNodes.length).toBe 1 + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" it "accounts for character widths when positioning cursors", -> atom.config.set('editor.fontFamily', 'sans-serif') editor.setCursorScreenPosition([0, 16]) - nextAnimationFrame() - cursor = componentNode.querySelector('.cursor') - cursorRect = cursor.getBoundingClientRect() + waitsForPromise -> atom.views.getNextUpdatePromise() - cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild - range = document.createRange() - range.setStart(cursorLocationTextNode, 0) - range.setEnd(cursorLocationTextNode, 1) - rangeRect = range.getBoundingClientRect() + runs -> + cursor = componentNode.querySelector('.cursor') + cursorRect = cursor.getBoundingClientRect() - expect(cursorRect.left).toBeCloseTo rangeRect.left, 0 - expect(cursorRect.width).toBeCloseTo rangeRect.width, 0 + cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild + range = document.createRange() + range.setStart(cursorLocationTextNode, 0) + range.setEnd(cursorLocationTextNode, 1) + rangeRect = range.getBoundingClientRect() + + expect(cursorRect.left).toBeCloseTo rangeRect.left, 0 + expect(cursorRect.width).toBeCloseTo rangeRect.width, 0 it "accounts for the width of paired characters when positioning cursors", -> atom.config.set('editor.fontFamily', 'sans-serif') editor.setText('he\u0301y') # e with an accent mark editor.setCursorBufferPosition([0, 3]) - nextAnimationFrame() - cursor = componentNode.querySelector('.cursor') - cursorRect = cursor.getBoundingClientRect() + waitsForPromise -> atom.views.getNextUpdatePromise() - cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2] + runs -> + cursor = componentNode.querySelector('.cursor') + cursorRect = cursor.getBoundingClientRect() - range = document.createRange() - range.setStart(cursorLocationTextNode, 0) - range.setEnd(cursorLocationTextNode, 1) - rangeRect = range.getBoundingClientRect() + cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2] - expect(cursorRect.left).toBeCloseTo rangeRect.left, 0 - expect(cursorRect.width).toBeCloseTo rangeRect.width, 0 + range = document.createRange() + range.setStart(cursorLocationTextNode, 0) + range.setEnd(cursorLocationTextNode, 1) + rangeRect = range.getBoundingClientRect() + + expect(cursorRect.left).toBeCloseTo rangeRect.left, 0 + expect(cursorRect.width).toBeCloseTo rangeRect.width, 0 it "positions cursors correctly after character widths are changed via a stylesheet change", -> atom.config.set('editor.fontFamily', 'sans-serif') editor.setCursorScreenPosition([0, 16]) - nextAnimationFrame() - atom.styles.addStyleSheet """ - .function.js { - font-weight: bold; - } - """, context: 'atom-text-editor' - nextAnimationFrame() # update based on new measurements + waitsForPromise -> atom.views.getNextUpdatePromise() - cursor = componentNode.querySelector('.cursor') - cursorRect = cursor.getBoundingClientRect() + runs -> + atom.styles.addStyleSheet """ + .function.js { + font-weight: bold; + } + """, context: 'atom-text-editor' - cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild - range = document.createRange() - range.setStart(cursorLocationTextNode, 0) - range.setEnd(cursorLocationTextNode, 1) - rangeRect = range.getBoundingClientRect() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(cursorRect.left).toBeCloseTo rangeRect.left, 0 - expect(cursorRect.width).toBeCloseTo rangeRect.width, 0 + runs -> + cursor = componentNode.querySelector('.cursor') + cursorRect = cursor.getBoundingClientRect() - atom.themes.removeStylesheet('test') + cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild + range = document.createRange() + range.setStart(cursorLocationTextNode, 0) + range.setEnd(cursorLocationTextNode, 1) + rangeRect = range.getBoundingClientRect() + + expect(cursorRect.left).toBeCloseTo rangeRect.left, 0 + expect(cursorRect.width).toBeCloseTo rangeRect.width, 0 + + atom.themes.removeStylesheet('test') it "sets the cursor to the default character width at the end of a line", -> editor.setCursorScreenPosition([0, Infinity]) - nextAnimationFrame() - cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.offsetWidth).toBeCloseTo charWidth, 0 + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + cursorNode = componentNode.querySelector('.cursor') + expect(cursorNode.offsetWidth).toBeCloseTo charWidth, 0 it "gives the cursor a non-zero width even if it's inside atomic tokens", -> editor.setCursorScreenPosition([1, 0]) - nextAnimationFrame() - cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.offsetWidth).toBeCloseTo charWidth, 0 + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + cursorNode = componentNode.querySelector('.cursor') + expect(cursorNode.offsetWidth).toBeCloseTo charWidth, 0 it "blinks cursors when they aren't moving", -> cursorsNode = componentNode.querySelector('.cursors') - wrapperNode.focus() - nextAnimationFrame() - expect(cursorsNode.classList.contains('blink-off')).toBe false - advanceClock(component.cursorBlinkPeriod / 2) - nextAnimationFrame() - expect(cursorsNode.classList.contains('blink-off')).toBe true + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> expect(cursorsNode.classList.contains('blink-off')).toBe false - advanceClock(component.cursorBlinkPeriod / 2) - nextAnimationFrame() - expect(cursorsNode.classList.contains('blink-off')).toBe false + waitsFor -> cursorsNode.classList.contains('blink-off') + waitsFor -> not cursorsNode.classList.contains('blink-off') - # Stop blinking after moving the cursor - editor.moveRight() - nextAnimationFrame() - expect(cursorsNode.classList.contains('blink-off')).toBe false + runs -> + # Stop blinking after moving the cursor + editor.moveRight() - advanceClock(component.cursorBlinkResumeDelay) - advanceClock(component.cursorBlinkPeriod / 2) - nextAnimationFrame() - expect(cursorsNode.classList.contains('blink-off')).toBe true + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(cursorsNode.classList.contains('blink-off')).toBe false + + waitsFor -> cursorsNode.classList.contains('blink-off') it "does not render cursors that are associated with non-empty selections", -> editor.setSelectedScreenRange([[0, 4], [4, 6]]) editor.addCursorAtScreenPosition([6, 8]) - nextAnimationFrame() - cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(8 * charWidth)}px, #{6 * lineHeightInPixels}px)" + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + cursorNodes = componentNode.querySelectorAll('.cursor') + expect(cursorNodes.length).toBe 1 + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(8 * charWidth)}px, #{6 * lineHeightInPixels}px)" it "updates cursor positions when the line height changes", -> editor.setCursorBufferPosition([1, 10]) component.setLineHeight(2) - nextAnimationFrame() - cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(10 * editor.getDefaultCharWidth())}px, #{editor.getLineHeightInPixels()}px)" + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + cursorNode = componentNode.querySelector('.cursor') + expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(10 * editor.getDefaultCharWidth())}px, #{editor.getLineHeightInPixels()}px)" it "updates cursor positions when the font size changes", -> editor.setCursorBufferPosition([1, 10]) component.setFontSize(10) - nextAnimationFrame() - cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(10 * editor.getDefaultCharWidth())}px, #{editor.getLineHeightInPixels()}px)" + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + cursorNode = componentNode.querySelector('.cursor') + expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(10 * editor.getDefaultCharWidth())}px, #{editor.getLineHeightInPixels()}px)" it "updates cursor positions when the font family changes", -> editor.setCursorBufferPosition([1, 10]) component.setFontFamily('sans-serif') - nextAnimationFrame() - cursorNode = componentNode.querySelector('.cursor') + waitsForPromise -> atom.views.getNextUpdatePromise() - {left} = wrapperNode.pixelPositionForScreenPosition([1, 10]) - expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(left)}px, #{editor.getLineHeightInPixels()}px)" + runs -> + cursorNode = componentNode.querySelector('.cursor') + + {left} = wrapperNode.pixelPositionForScreenPosition([1, 10]) + expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(left)}px, #{editor.getLineHeightInPixels()}px)" describe "selection rendering", -> [scrollViewNode, scrollViewClientLeft] = [] @@ -1141,137 +1284,160 @@ fdescribe "TextEditorComponent", -> it "renders 1 region for 1-line selections", -> # 1-line selection editor.setSelectedScreenRange([[1, 6], [1, 10]]) - nextAnimationFrame() - regions = componentNode.querySelectorAll('.selection .region') + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(regions.length).toBe 1 - regionRect = regions[0].getBoundingClientRect() - expect(regionRect.top).toBe 1 * lineHeightInPixels - expect(regionRect.height).toBe 1 * lineHeightInPixels - expect(regionRect.left).toBeCloseTo scrollViewClientLeft + 6 * charWidth, 0 - expect(regionRect.width).toBeCloseTo 4 * charWidth, 0 + runs -> + regions = componentNode.querySelectorAll('.selection .region') + + expect(regions.length).toBe 1 + regionRect = regions[0].getBoundingClientRect() + expect(regionRect.top).toBe 1 * lineHeightInPixels + expect(regionRect.height).toBe 1 * lineHeightInPixels + expect(regionRect.left).toBeCloseTo scrollViewClientLeft + 6 * charWidth, 0 + expect(regionRect.width).toBeCloseTo 4 * charWidth, 0 it "renders 2 regions for 2-line selections", -> editor.setSelectedScreenRange([[1, 6], [2, 10]]) - nextAnimationFrame() - tileNode = component.tileNodesForLines()[0] - regions = tileNode.querySelectorAll('.selection .region') - expect(regions.length).toBe 2 + waitsForPromise -> atom.views.getNextUpdatePromise() - region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe 1 * lineHeightInPixels - expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBeCloseTo scrollViewClientLeft + 6 * charWidth, 0 - expect(region1Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 + runs -> + tileNode = component.tileNodesForLines()[0] + regions = tileNode.querySelectorAll('.selection .region') + expect(regions.length).toBe 2 - region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe 2 * lineHeightInPixels - expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 - expect(region2Rect.width).toBeCloseTo 10 * charWidth, 0 + region1Rect = regions[0].getBoundingClientRect() + expect(region1Rect.top).toBe 1 * lineHeightInPixels + expect(region1Rect.height).toBe 1 * lineHeightInPixels + expect(region1Rect.left).toBeCloseTo scrollViewClientLeft + 6 * charWidth, 0 + expect(region1Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 + + region2Rect = regions[1].getBoundingClientRect() + expect(region2Rect.top).toBe 2 * lineHeightInPixels + expect(region2Rect.height).toBe 1 * lineHeightInPixels + expect(region2Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 + expect(region2Rect.width).toBeCloseTo 10 * charWidth, 0 it "renders 3 regions per tile for selections with more than 2 lines", -> editor.setSelectedScreenRange([[0, 6], [5, 10]]) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - # Tile 0 - tileNode = component.tileNodesForLines()[0] - regions = tileNode.querySelectorAll('.selection .region') - expect(regions.length).toBe(3) + runs -> + # Tile 0 + tileNode = component.tileNodesForLines()[0] + regions = tileNode.querySelectorAll('.selection .region') + expect(regions.length).toBe(3) - region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe 0 - expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBeCloseTo scrollViewClientLeft + 6 * charWidth, 0 - expect(region1Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 + region1Rect = regions[0].getBoundingClientRect() + expect(region1Rect.top).toBe 0 + expect(region1Rect.height).toBe 1 * lineHeightInPixels + expect(region1Rect.left).toBeCloseTo scrollViewClientLeft + 6 * charWidth, 0 + expect(region1Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 - region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe 1 * lineHeightInPixels - expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 - expect(region2Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 + region2Rect = regions[1].getBoundingClientRect() + expect(region2Rect.top).toBe 1 * lineHeightInPixels + expect(region2Rect.height).toBe 1 * lineHeightInPixels + expect(region2Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 + expect(region2Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 - region3Rect = regions[2].getBoundingClientRect() - expect(region3Rect.top).toBe 2 * lineHeightInPixels - expect(region3Rect.height).toBe 1 * lineHeightInPixels - expect(region3Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 - expect(region3Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 + region3Rect = regions[2].getBoundingClientRect() + expect(region3Rect.top).toBe 2 * lineHeightInPixels + expect(region3Rect.height).toBe 1 * lineHeightInPixels + expect(region3Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 + expect(region3Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 - # Tile 3 - tileNode = component.tileNodesForLines()[1] - regions = tileNode.querySelectorAll('.selection .region') - expect(regions.length).toBe(3) + # Tile 3 + tileNode = component.tileNodesForLines()[1] + regions = tileNode.querySelectorAll('.selection .region') + expect(regions.length).toBe(3) - region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe 3 * lineHeightInPixels - expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 - expect(region1Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 + region1Rect = regions[0].getBoundingClientRect() + expect(region1Rect.top).toBe 3 * lineHeightInPixels + expect(region1Rect.height).toBe 1 * lineHeightInPixels + expect(region1Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 + expect(region1Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 - region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe 4 * lineHeightInPixels - expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 - expect(region2Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 + region2Rect = regions[1].getBoundingClientRect() + expect(region2Rect.top).toBe 4 * lineHeightInPixels + expect(region2Rect.height).toBe 1 * lineHeightInPixels + expect(region2Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 + expect(region2Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 - region3Rect = regions[2].getBoundingClientRect() - expect(region3Rect.top).toBe 5 * lineHeightInPixels - expect(region3Rect.height).toBe 1 * lineHeightInPixels - expect(region3Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 - expect(region3Rect.width).toBeCloseTo 10 * charWidth, 0 + region3Rect = regions[2].getBoundingClientRect() + expect(region3Rect.top).toBe 5 * lineHeightInPixels + expect(region3Rect.height).toBe 1 * lineHeightInPixels + expect(region3Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 + expect(region3Rect.width).toBeCloseTo 10 * charWidth, 0 it "does not render empty selections", -> editor.addSelectionForBufferRange([[2, 2], [2, 2]]) - nextAnimationFrame() - expect(editor.getSelections()[0].isEmpty()).toBe true - expect(editor.getSelections()[1].isEmpty()).toBe true + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(componentNode.querySelectorAll('.selection').length).toBe 0 + runs -> + expect(editor.getSelections()[0].isEmpty()).toBe true + expect(editor.getSelections()[1].isEmpty()).toBe true + + expect(componentNode.querySelectorAll('.selection').length).toBe 0 it "updates selections when the line height changes", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setLineHeight(2) - nextAnimationFrame() - selectionNode = componentNode.querySelector('.region') - expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + selectionNode = componentNode.querySelector('.region') + expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() it "updates selections when the font size changes", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontSize(10) - nextAnimationFrame() - selectionNode = componentNode.querySelector('.region') - expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() - expect(selectionNode.offsetLeft).toBeCloseTo 6 * editor.getDefaultCharWidth(), 0 + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + selectionNode = componentNode.querySelector('.region') + expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() + expect(selectionNode.offsetLeft).toBeCloseTo 6 * editor.getDefaultCharWidth(), 0 it "updates selections when the font family changes", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontFamily('sans-serif') - nextAnimationFrame() - selectionNode = componentNode.querySelector('.region') - expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() - expect(selectionNode.offsetLeft).toBeCloseTo wrapperNode.pixelPositionForScreenPosition([1, 6]).left, 0 + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + selectionNode = componentNode.querySelector('.region') + expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() + expect(selectionNode.offsetLeft).toBeCloseTo wrapperNode.pixelPositionForScreenPosition([1, 6]).left, 0 it "will flash the selection when flash:true is passed to editor::setSelectedBufferRange", -> editor.setSelectedBufferRange([[1, 6], [1, 10]], flash: true) - nextAnimationFrame() - selectionNode = componentNode.querySelector('.selection') - expect(selectionNode.classList.contains('flash')).toBe true - advanceClock editor.selectionFlashDuration - expect(selectionNode.classList.contains('flash')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.setSelectedBufferRange([[1, 5], [1, 7]], flash: true) - nextAnimationFrame() - expect(selectionNode.classList.contains('flash')).toBe true + selectionNode = null + runs -> + selectionNode = componentNode.querySelector('.selection') + expect(selectionNode.classList.contains('flash')).toBe true - ffdescribe "line decoration rendering", -> + waitsFor -> not selectionNode.classList.contains('flash') + + runs -> + editor.setSelectedBufferRange([[1, 5], [1, 7]], flash: true) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(selectionNode.classList.contains('flash')).toBe true + + describe "line decoration rendering", -> [marker, decoration, decorationParams] = [] beforeEach -> marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], invalidate: 'inside', maintainHistory: true) decorationParams = {type: ['line-number', 'line'], class: 'a'} decoration = editor.decorateMarker(marker, decorationParams) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() it "applies line decoration classes to lines and line numbers", -> expect(lineAndLineNumberHaveClass(2, 'a')).toBe true @@ -1280,43 +1446,61 @@ fdescribe "TextEditorComponent", -> # Shrink editor vertically wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() - # Add decorations that are out of range - marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]]) - editor.decorateMarker(marker2, type: ['line-number', 'line'], class: 'b') - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - # Scroll decorations into view - verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - nextAnimationFrame() - expect(lineAndLineNumberHaveClass(9, 'b')).toBe true + runs -> + # Add decorations that are out of range + marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]]) + editor.decorateMarker(marker2, type: ['line-number', 'line'], class: 'b') - # Fold a line to move the decorations - editor.foldBufferRow(5) - nextAnimationFrame() - expect(lineAndLineNumberHaveClass(9, 'b')).toBe false - expect(lineAndLineNumberHaveClass(6, 'b')).toBe true + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + # Scroll decorations into view + verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineAndLineNumberHaveClass(9, 'b')).toBe true + + # Fold a line to move the decorations + editor.foldBufferRow(5) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineAndLineNumberHaveClass(9, 'b')).toBe false + expect(lineAndLineNumberHaveClass(6, 'b')).toBe true it "only applies decorations to screen rows that are spanned by their marker when lines are soft-wrapped", -> editor.setText("a line that wraps, ok") editor.setSoftWrapped(true) componentNode.style.width = 16 * charWidth + 'px' component.measureDimensions() - nextAnimationFrame() - marker.destroy() - marker = editor.markBufferRange([[0, 0], [0, 2]]) - editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'b') - nextAnimationFrame() - expect(lineNumberHasClass(0, 'b')).toBe true - expect(lineNumberHasClass(1, 'b')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() - marker.setBufferRange([[0, 0], [0, Infinity]]) - nextAnimationFrame() - expect(lineNumberHasClass(0, 'b')).toBe true - expect(lineNumberHasClass(1, 'b')).toBe true + runs -> + marker.destroy() + marker = editor.markBufferRange([[0, 0], [0, 2]]) + editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'b') + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(0, 'b')).toBe true + expect(lineNumberHasClass(1, 'b')).toBe false + + marker.setBufferRange([[0, 0], [0, Infinity]]) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(0, 'b')).toBe true + expect(lineNumberHasClass(1, 'b')).toBe true it "updates decorations when markers move", -> expect(lineAndLineNumberHaveClass(1, 'a')).toBe false @@ -1325,86 +1509,117 @@ fdescribe "TextEditorComponent", -> expect(lineAndLineNumberHaveClass(4, 'a')).toBe false editor.getBuffer().insert([0, 0], '\n') - nextAnimationFrame() - expect(lineAndLineNumberHaveClass(2, 'a')).toBe false - expect(lineAndLineNumberHaveClass(3, 'a')).toBe true - expect(lineAndLineNumberHaveClass(4, 'a')).toBe true - expect(lineAndLineNumberHaveClass(5, 'a')).toBe false - marker.setBufferRange([[4, 4], [6, 4]]) - nextAnimationFrame() - expect(lineAndLineNumberHaveClass(2, 'a')).toBe false - expect(lineAndLineNumberHaveClass(3, 'a')).toBe false - expect(lineAndLineNumberHaveClass(4, 'a')).toBe true - expect(lineAndLineNumberHaveClass(5, 'a')).toBe true - expect(lineAndLineNumberHaveClass(6, 'a')).toBe true - expect(lineAndLineNumberHaveClass(7, 'a')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineAndLineNumberHaveClass(2, 'a')).toBe false + expect(lineAndLineNumberHaveClass(3, 'a')).toBe true + expect(lineAndLineNumberHaveClass(4, 'a')).toBe true + expect(lineAndLineNumberHaveClass(5, 'a')).toBe false + + marker.setBufferRange([[4, 4], [6, 4]]) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineAndLineNumberHaveClass(2, 'a')).toBe false + expect(lineAndLineNumberHaveClass(3, 'a')).toBe false + expect(lineAndLineNumberHaveClass(4, 'a')).toBe true + expect(lineAndLineNumberHaveClass(5, 'a')).toBe true + expect(lineAndLineNumberHaveClass(6, 'a')).toBe true + expect(lineAndLineNumberHaveClass(7, 'a')).toBe false it "remove decoration classes when decorations are removed", -> decoration.destroy() - nextAnimationFrame() - expect(lineNumberHasClass(1, 'a')).toBe false - expect(lineNumberHasClass(2, 'a')).toBe false - expect(lineNumberHasClass(3, 'a')).toBe false - expect(lineNumberHasClass(4, 'a')).toBe false + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(1, 'a')).toBe false + expect(lineNumberHasClass(2, 'a')).toBe false + expect(lineNumberHasClass(3, 'a')).toBe false + expect(lineNumberHasClass(4, 'a')).toBe false it "removes decorations when their marker is invalidated", -> editor.getBuffer().insert([3, 2], 'n') - nextAnimationFrame() - expect(marker.isValid()).toBe false - expect(lineAndLineNumberHaveClass(1, 'a')).toBe false - expect(lineAndLineNumberHaveClass(2, 'a')).toBe false - expect(lineAndLineNumberHaveClass(3, 'a')).toBe false - expect(lineAndLineNumberHaveClass(4, 'a')).toBe false - editor.undo() - nextAnimationFrame() - expect(marker.isValid()).toBe true - expect(lineAndLineNumberHaveClass(1, 'a')).toBe false - expect(lineAndLineNumberHaveClass(2, 'a')).toBe true - expect(lineAndLineNumberHaveClass(3, 'a')).toBe true - expect(lineAndLineNumberHaveClass(4, 'a')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(marker.isValid()).toBe false + expect(lineAndLineNumberHaveClass(1, 'a')).toBe false + expect(lineAndLineNumberHaveClass(2, 'a')).toBe false + expect(lineAndLineNumberHaveClass(3, 'a')).toBe false + expect(lineAndLineNumberHaveClass(4, 'a')).toBe false + + editor.undo() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(marker.isValid()).toBe true + expect(lineAndLineNumberHaveClass(1, 'a')).toBe false + expect(lineAndLineNumberHaveClass(2, 'a')).toBe true + expect(lineAndLineNumberHaveClass(3, 'a')).toBe true + expect(lineAndLineNumberHaveClass(4, 'a')).toBe false it "removes decorations when their marker is destroyed", -> marker.destroy() - nextAnimationFrame() - expect(lineNumberHasClass(1, 'a')).toBe false - expect(lineNumberHasClass(2, 'a')).toBe false - expect(lineNumberHasClass(3, 'a')).toBe false - expect(lineNumberHasClass(4, 'a')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(1, 'a')).toBe false + expect(lineNumberHasClass(2, 'a')).toBe false + expect(lineNumberHasClass(3, 'a')).toBe false + expect(lineNumberHasClass(4, 'a')).toBe false describe "when the decoration's 'onlyHead' property is true", -> it "only applies the decoration's class to lines containing the marker's head", -> editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-head', onlyHead: true) - nextAnimationFrame() - expect(lineAndLineNumberHaveClass(1, 'only-head')).toBe false - expect(lineAndLineNumberHaveClass(2, 'only-head')).toBe false - expect(lineAndLineNumberHaveClass(3, 'only-head')).toBe true - expect(lineAndLineNumberHaveClass(4, 'only-head')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineAndLineNumberHaveClass(1, 'only-head')).toBe false + expect(lineAndLineNumberHaveClass(2, 'only-head')).toBe false + expect(lineAndLineNumberHaveClass(3, 'only-head')).toBe true + expect(lineAndLineNumberHaveClass(4, 'only-head')).toBe false describe "when the decoration's 'onlyEmpty' property is true", -> it "only applies the decoration when its marker is empty", -> editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-empty', onlyEmpty: true) - nextAnimationFrame() - expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe false - expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe false - marker.clearTail() - nextAnimationFrame() - expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe false - expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe true + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe false + expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe false + + marker.clearTail() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe false + expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe true describe "when the decoration's 'onlyNonEmpty' property is true", -> it "only applies the decoration when its marker is non-empty", -> editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-non-empty', onlyNonEmpty: true) - nextAnimationFrame() - expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe true - expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe true - marker.clearTail() - nextAnimationFrame() - expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe false - expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe true + expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe true + + marker.clearTail() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe false + expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe false describe "highlight decoration rendering", -> [marker, decoration, decorationParams, scrollViewClientLeft] = [] @@ -1413,38 +1628,45 @@ fdescribe "TextEditorComponent", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], invalidate: 'inside', maintainHistory: true) decorationParams = {type: 'highlight', class: 'test-highlight'} decoration = editor.decorateMarker(marker, decorationParams) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() it "does not render highlights for off-screen lines until they come on-screen", -> wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() - marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], invalidate: 'inside') - editor.decorateMarker(marker, type: 'highlight', class: 'some-highlight') - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - # Should not be rendering range containing the marker - expect(component.presenter.endRow).toBeLessThan 9 + runs -> + marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], invalidate: 'inside') + editor.decorateMarker(marker, type: 'highlight', class: 'some-highlight') - regions = componentNode.querySelectorAll('.some-highlight .region') + waitsForPromise -> atom.views.getNextUpdatePromise() - # Nothing when outside the rendered row range - expect(regions.length).toBe 0 + runs -> + # Should not be rendering range containing the marker + expect(component.presenter.endRow).toBeLessThan 9 - verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - nextAnimationFrame() - expect(component.presenter.endRow).toBeGreaterThan(8) + regions = componentNode.querySelectorAll('.some-highlight .region') - regions = componentNode.querySelectorAll('.some-highlight .region') + # Nothing when outside the rendered row range + expect(regions.length).toBe 0 - expect(regions.length).toBe 1 - regionRect = regions[0].style - expect(regionRect.top).toBe (0 + 'px') - expect(regionRect.height).toBe 1 * lineHeightInPixels + 'px' - expect(regionRect.left).toBe Math.round(2 * charWidth) + 'px' - expect(regionRect.width).toBe Math.round(2 * charWidth) + 'px' + verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.presenter.endRow).toBeGreaterThan(8) + + regions = componentNode.querySelectorAll('.some-highlight .region') + + expect(regions.length).toBe 1 + regionRect = regions[0].style + expect(regionRect.top).toBe (0 + 'px') + expect(regionRect.height).toBe 1 * lineHeightInPixels + 'px' + expect(regionRect.left).toBe Math.round(2 * charWidth) + 'px' + expect(regionRect.width).toBe Math.round(2 * charWidth) + 'px' it "renders highlights decoration's marker is added", -> regions = componentNode.querySelectorAll('.test-highlight .region') @@ -1452,50 +1674,66 @@ fdescribe "TextEditorComponent", -> it "removes highlights when a decoration is removed", -> decoration.destroy() - nextAnimationFrame() - regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe 0 + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + regions = componentNode.querySelectorAll('.test-highlight .region') + expect(regions.length).toBe 0 it "does not render a highlight that is within a fold", -> editor.foldBufferRow(1) - nextAnimationFrame() - expect(componentNode.querySelectorAll('.test-highlight').length).toBe 0 + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.querySelectorAll('.test-highlight').length).toBe 0 it "removes highlights when a decoration's marker is destroyed", -> marker.destroy() - nextAnimationFrame() - regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe 0 + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + regions = componentNode.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') - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(marker.isValid()).toBe false - regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe 0 + runs -> + expect(marker.isValid()).toBe false + regions = componentNode.querySelectorAll('.test-highlight .region') + expect(regions.length).toBe 0 - editor.getBuffer().undo() - nextAnimationFrame() + editor.getBuffer().undo() - expect(marker.isValid()).toBe true - regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe 2 + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(marker.isValid()).toBe true + regions = componentNode.querySelectorAll('.test-highlight .region') + expect(regions.length).toBe 2 it "allows multiple space-delimited decoration classes", -> decoration.setProperties(type: 'highlight', class: 'foo bar') - nextAnimationFrame() - expect(componentNode.querySelectorAll('.foo.bar').length).toBe 2 - decoration.setProperties(type: 'highlight', class: 'bar baz') - nextAnimationFrame() - expect(componentNode.querySelectorAll('.bar.baz').length).toBe 2 + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.querySelectorAll('.foo.bar').length).toBe 2 + decoration.setProperties(type: 'highlight', class: 'bar baz') + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.querySelectorAll('.bar.baz').length).toBe 2 it "renders classes on the regions directly if 'deprecatedRegionClass' option is defined", -> decoration = editor.decorateMarker(marker, type: 'highlight', class: 'test-highlight', deprecatedRegionClass: 'test-highlight-region') - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - regions = componentNode.querySelectorAll('.test-highlight .region.test-highlight-region') - expect(regions.length).toBe 2 + runs -> + regions = componentNode.querySelectorAll('.test-highlight .region.test-highlight-region') + expect(regions.length).toBe 2 describe "when flashing a decoration via Decoration::flash()", -> highlightNode = null @@ -1506,30 +1744,23 @@ fdescribe "TextEditorComponent", -> expect(highlightNode.classList.contains('flash-class')).toBe false decoration.flash('flash-class', 10) - nextAnimationFrame() - expect(highlightNode.classList.contains('flash-class')).toBe true + waitsForPromise -> atom.views.getNextUpdatePromise() - advanceClock(10) - expect(highlightNode.classList.contains('flash-class')).toBe false + runs -> + expect(highlightNode.classList.contains('flash-class')).toBe true + + waitsFor -> !highlightNode.classList.contains('flash-class') describe "when ::flash is called again before the first has finished", -> it "removes the class from the decoration highlight before adding it for the second ::flash call", -> - decoration.flash('flash-class', 10) - nextAnimationFrame() - expect(highlightNode.classList.contains('flash-class')).toBe true - advanceClock(2) - - decoration.flash('flash-class', 10) - nextAnimationFrame() - - # Removed for 1 frame to force CSS transition to restart - expect(highlightNode.classList.contains('flash-class')).toBe false - - nextAnimationFrame() - expect(highlightNode.classList.contains('flash-class')).toBe true - - advanceClock(10) - expect(highlightNode.classList.contains('flash-class')).toBe false + decoration.flash('flash-class', 30) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> expect(highlightNode.classList.contains('flash-class')).toBe true + waits 2 + runs -> decoration.flash('flash-class', 10) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> expect(highlightNode.classList.contains('flash-class')).toBe false + waitsFor -> highlightNode.classList.contains('flash-class') describe "when a decoration's marker moves", -> it "moves rendered highlights when the buffer is changed", -> @@ -1539,32 +1770,38 @@ fdescribe "TextEditorComponent", -> expect(originalTop).toBe(2 * lineHeightInPixels) editor.getBuffer().insert([0, 0], '\n') - nextAnimationFrame() - regionStyle = componentNode.querySelector('.test-highlight .region').style - newTop = parseInt(regionStyle.top) + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(newTop).toBe(0) + runs -> + regionStyle = componentNode.querySelector('.test-highlight .region').style + newTop = parseInt(regionStyle.top) + + expect(newTop).toBe(0) it "moves rendered highlights when the marker is manually moved", -> regionStyle = componentNode.querySelector('.test-highlight .region').style expect(parseInt(regionStyle.top)).toBe 2 * lineHeightInPixels marker.setBufferRange([[5, 8], [5, 13]]) - nextAnimationFrame() - regionStyle = componentNode.querySelector('.test-highlight .region').style - expect(parseInt(regionStyle.top)).toBe 2 * lineHeightInPixels + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + regionStyle = componentNode.querySelector('.test-highlight .region').style + expect(parseInt(regionStyle.top)).toBe 2 * lineHeightInPixels describe "when a decoration is updated via Decoration::update", -> it "renders the decoration's new params", -> expect(componentNode.querySelector('.test-highlight')).toBeTruthy() decoration.setProperties(type: 'highlight', class: 'new-test-highlight') - nextAnimationFrame() - expect(componentNode.querySelector('.test-highlight')).toBeFalsy() - expect(componentNode.querySelector('.new-test-highlight')).toBeTruthy() + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.querySelector('.test-highlight')).toBeFalsy() + expect(componentNode.querySelector('.new-test-highlight')).toBeTruthy() describe "overlay decoration rendering", -> [item, gutterWidth] = [] @@ -1578,40 +1815,46 @@ fdescribe "TextEditorComponent", -> it "renders an overlay decoration when added and removes the overlay when the decoration is destroyed", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') - expect(overlay).toBe item + runs -> + overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') + expect(overlay).toBe item - decoration.destroy() - nextAnimationFrame() + decoration.destroy() - overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') - expect(overlay).toBe null + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') + expect(overlay).toBe null it "renders the overlay element with the CSS class specified by the decoration", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', class: 'my-overlay', item}) - nextAnimationFrame() - overlay = component.getTopmostDOMNode().querySelector('atom-overlay.my-overlay') - expect(overlay).not.toBe null + waitsForPromise -> atom.views.getNextUpdatePromise() - child = overlay.querySelector('.overlay-test') - expect(child).toBe item + runs -> + overlay = component.getTopmostDOMNode().querySelector('atom-overlay.my-overlay') + expect(overlay).not.toBe null + + child = overlay.querySelector('.overlay-test') + expect(child).toBe item describe "when the marker is not empty", -> it "renders at the head of the marker by default", -> marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - nextAnimationFrame() - nextAnimationFrame() - position = wrapperNode.pixelPositionForBufferPosition([2, 10]) + waitsForPromise -> atom.views.getNextUpdatePromise() - overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe Math.round(position.left + gutterWidth) + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' + runs -> + position = wrapperNode.pixelPositionForBufferPosition([2, 10]) + + overlay = component.getTopmostDOMNode().querySelector('atom-overlay') + expect(overlay.style.left).toBe Math.round(position.left + gutterWidth) + 'px' + expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' describe "positioning the overlay when near the edge of the editor", -> [itemWidth, itemHeight, windowWidth, windowHeight] = [] @@ -1634,35 +1877,41 @@ fdescribe "TextEditorComponent", -> component.measureDimensions() component.measureWindowSize() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() afterEach -> atom.restoreWindowDimensions() # This spec should actually run on Linux as well, see TextEditorComponent#measureWindowSize for further information. it "slides horizontally left when near the right edge on #win32 and #darwin", -> + [overlay, position] = [] + marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - nextAnimationFrame() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - position = wrapperNode.pixelPositionForBufferPosition([0, 26]) + runs -> + position = wrapperNode.pixelPositionForBufferPosition([0, 26]) - overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe Math.round(position.left + gutterWidth) + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' + overlay = component.getTopmostDOMNode().querySelector('atom-overlay') + expect(overlay.style.left).toBe Math.round(position.left + gutterWidth) + 'px' + expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - editor.insertText('a') - nextAnimationFrame() + editor.insertText('a') - expect(overlay.style.left).toBe windowWidth - itemWidth + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.insertText('b') - nextAnimationFrame() + runs -> + expect(overlay.style.left).toBe windowWidth - itemWidth + 'px' + expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - expect(overlay.style.left).toBe windowWidth - itemWidth + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' + editor.insertText('b') + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(overlay.style.left).toBe windowWidth - itemWidth + 'px' + expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' 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", -> @@ -1673,45 +1922,64 @@ fdescribe "TextEditorComponent", -> wrapperNode.style.height = 5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - nextAnimationFrame() - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - wrapperNode.setScrollTop(3 * lineHeightInPixels) - wrapperNode.setScrollLeft(3 * charWidth) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(inputNode.offsetTop).toBe 0 - expect(inputNode.offsetLeft).toBe 0 + runs -> + expect(editor.getCursorScreenPosition()).toEqual [0, 0] + wrapperNode.setScrollTop(3 * lineHeightInPixels) + wrapperNode.setScrollLeft(3 * charWidth) - # In bounds, not focused - editor.setCursorBufferPosition([5, 4], autoscroll: false) - nextAnimationFrame() - expect(inputNode.offsetTop).toBe 0 - expect(inputNode.offsetLeft).toBe 0 + waitsForPromise -> atom.views.getNextUpdatePromise() - # In bounds and focused - wrapperNode.focus() # updates via state change - nextAnimationFrame() - expect(inputNode.offsetTop).toBe (5 * lineHeightInPixels) - wrapperNode.getScrollTop() - expect(inputNode.offsetLeft).toBeCloseTo (4 * charWidth) - wrapperNode.getScrollLeft(), 0 + runs -> + expect(inputNode.offsetTop).toBe 0 + expect(inputNode.offsetLeft).toBe 0 - # In bounds, not focused - inputNode.blur() # updates via state change - nextAnimationFrame() - expect(inputNode.offsetTop).toBe 0 - expect(inputNode.offsetLeft).toBe 0 + # In bounds, not focused + editor.setCursorBufferPosition([5, 4], autoscroll: false) - # Out of bounds, not focused - editor.setCursorBufferPosition([1, 2], autoscroll: false) - nextAnimationFrame() - expect(inputNode.offsetTop).toBe 0 - expect(inputNode.offsetLeft).toBe 0 + waitsForPromise -> atom.views.getNextUpdatePromise() - # Out of bounds, focused - inputNode.focus() # updates via state change - nextAnimationFrame() - expect(inputNode.offsetTop).toBe 0 - expect(inputNode.offsetLeft).toBe 0 + runs -> + expect(inputNode.offsetTop).toBe 0 + expect(inputNode.offsetLeft).toBe 0 + + # In bounds and focused + wrapperNode.focus() # updates via state change + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(inputNode.offsetTop).toBe (5 * lineHeightInPixels) - wrapperNode.getScrollTop() + expect(inputNode.offsetLeft).toBeCloseTo (4 * charWidth) - wrapperNode.getScrollLeft(), 0 + + # In bounds, not focused + inputNode.blur() # updates via state change + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(inputNode.offsetTop).toBe 0 + expect(inputNode.offsetLeft).toBe 0 + + # Out of bounds, not focused + editor.setCursorBufferPosition([1, 2], autoscroll: false) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(inputNode.offsetTop).toBe 0 + expect(inputNode.offsetLeft).toBe 0 + + # Out of bounds, focused + inputNode.focus() # updates via state change + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(inputNode.offsetTop).toBe 0 + expect(inputNode.offsetLeft).toBe 0 describe "mouse interactions on the lines", -> linesNode = null @@ -1727,13 +1995,17 @@ fdescribe "TextEditorComponent", -> wrapperNode.style.height = height + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - coordinates = clientCoordinatesForScreenPosition([0, 2]) - coordinates.clientY = -1 - linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) - nextAnimationFrame() - expect(editor.getCursorScreenPosition()).toEqual [0, 0] + runs -> + coordinates = clientCoordinatesForScreenPosition([0, 2]) + coordinates.clientY = -1 + linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(editor.getCursorScreenPosition()).toEqual [0, 0] describe "when the mouse is single-clicked below the last line", -> it "moves the cursor to the end of file buffer position", -> @@ -1743,13 +2015,16 @@ fdescribe "TextEditorComponent", -> wrapperNode.style.height = height + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - coordinates = clientCoordinatesForScreenPosition([0, 2]) - coordinates.clientY = height * 2 - linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) - nextAnimationFrame() - expect(editor.getCursorScreenPosition()).toEqual [0, 3] + runs -> + coordinates = clientCoordinatesForScreenPosition([0, 2]) + coordinates.clientY = height * 2 + linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(editor.getCursorScreenPosition()).toEqual [0, 3] describe "when a non-folded line is single-clicked", -> describe "when no modifier keys are held down", -> @@ -1759,26 +2034,34 @@ fdescribe "TextEditorComponent", -> component.measureDimensions() wrapperNode.setScrollTop(3.5 * lineHeightInPixels) wrapperNode.setScrollLeft(2 * charWidth) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]))) - nextAnimationFrame() - expect(editor.getCursorScreenPosition()).toEqual [4, 8] + runs -> + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]))) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + 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)) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [5, 6]] + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [5, 6]] describe "when the command key is held down", -> describe "the current cursor position and screen position do not match", -> it "adds a cursor at the nearest screen position", -> editor.setCursorScreenPosition([3, 4]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), metaKey: true)) - nextAnimationFrame() - expect(editor.getSelectedScreenRanges()).toEqual [[[3, 4], [3, 4]], [[5, 6], [5, 6]]] + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(editor.getSelectedScreenRanges()).toEqual [[[3, 4], [3, 4]], [[5, 6], [5, 6]]] describe "when there are multiple cursors, and one of the cursor's screen position is the same as the mouse click screen position", -> it "removes a cursor at the mouse screen position", -> @@ -1786,15 +2069,19 @@ fdescribe "TextEditorComponent", -> editor.addCursorAtScreenPosition([5, 2]) editor.addCursorAtScreenPosition([7, 5]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), metaKey: true)) - nextAnimationFrame() - expect(editor.getSelectedScreenRanges()).toEqual [[[5, 2], [5, 2]], [[7, 5], [7, 5]]] + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(editor.getSelectedScreenRanges()).toEqual [[[5, 2], [5, 2]], [[7, 5], [7, 5]]] describe "when there is a single cursor and the click occurs at the cursor's screen position", -> it "neither adds a new cursor nor removes the current cursor", -> editor.setCursorScreenPosition([3, 4]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), metaKey: true)) - nextAnimationFrame() - expect(editor.getSelectedScreenRanges()).toEqual [[[3, 4], [3, 4]]] + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(editor.getSelectedScreenRanges()).toEqual [[[3, 4], [3, 4]]] describe "when a non-folded line is double-clicked", -> describe "when no modifier keys are held down", -> @@ -1857,94 +2144,131 @@ fdescribe "TextEditorComponent", -> it "selects to the nearest screen position until the mouse button is released", -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1)) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), which: 1)) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]] + waitsForAnimationFrame() - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([12, 0]), which: 1)) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]] + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] + + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), which: 1)) + + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]] + + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([12, 0]), which: 1)) + + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]] it "autoscrolls when the cursor approaches the boundaries of the editor", -> wrapperNode.style.height = '100px' wrapperNode.style.width = '100px' component.measureDimensions() - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe(0) - expect(wrapperNode.getScrollLeft()).toBe(0) + waitsForPromise -> atom.views.getNextUpdatePromise() - linesNode.dispatchEvent(buildMouseEvent('mousedown', {clientX: 0, clientY: 0}, which: 1)) - linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 50}, which: 1)) - nextAnimationFrame() + runs -> + expect(wrapperNode.getScrollTop()).toBe(0) + expect(wrapperNode.getScrollLeft()).toBe(0) - expect(wrapperNode.getScrollTop()).toBe(0) - expect(wrapperNode.getScrollLeft()).toBeGreaterThan(0) + linesNode.dispatchEvent(buildMouseEvent('mousedown', {clientX: 0, clientY: 0}, which: 1)) + linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 50}, which: 1)) - linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 100}, which: 1)) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) + waitsForAnimationFrame() for i in [0..5] - previousScrollTop = wrapperNode.getScrollTop() - previousScrollLeft = wrapperNode.getScrollLeft() + runs -> + expect(wrapperNode.getScrollTop()).toBe(0) + expect(wrapperNode.getScrollLeft()).toBeGreaterThan(0) - linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 50}, which: 1)) - nextAnimationFrame() + linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 100}, which: 1)) - expect(wrapperNode.getScrollTop()).toBe(previousScrollTop) - expect(wrapperNode.getScrollLeft()).toBeLessThan(previousScrollLeft) + waitsForAnimationFrame() for i in [0..5] - linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 10}, which: 1)) - nextAnimationFrame() + [previousScrollTop, previousScrollLeft] = [] - expect(wrapperNode.getScrollTop()).toBeLessThan(previousScrollTop) + runs -> + expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) + + previousScrollTop = wrapperNode.getScrollTop() + previousScrollLeft = wrapperNode.getScrollLeft() + + linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 50}, which: 1)) + + waitsForAnimationFrame() for i in [0..5] + + runs -> + expect(wrapperNode.getScrollTop()).toBe(previousScrollTop) + expect(wrapperNode.getScrollLeft()).toBeLessThan(previousScrollLeft) + + linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 10}, which: 1)) + + waitsForAnimationFrame() for i in [0..5] + + runs -> + expect(wrapperNode.getScrollTop()).toBeLessThan(previousScrollTop) it "stops selecting if the mouse is dragged into the dev tools", -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1)) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] + waitsForAnimationFrame() - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), which: 0)) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1)) - expect(nextAnimationFrame).toBe noAnimationFrame - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), which: 0)) + + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] + + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1)) + + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] it "stops selecting before the buffer is modified during the drag", -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1)) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] + waitsForAnimationFrame() - editor.insertText('x') - nextAnimationFrame() + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] - expect(editor.getSelectedScreenRange()).toEqual [[2, 5], [2, 5]] + editor.insertText('x') - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1)) - expect(nextAnimationFrame).toBe noAnimationFrame - expect(editor.getSelectedScreenRange()).toEqual [[2, 5], [2, 5]] + waitsForAnimationFrame() - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([5, 4]), which: 1)) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [5, 4]] + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 5], [2, 5]] - editor.delete() - nextAnimationFrame() + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1)) + expect(editor.getSelectedScreenRange()).toEqual [[2, 5], [2, 5]] - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [2, 4]] + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([5, 4]), which: 1)) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1)) - expect(nextAnimationFrame).toBe noAnimationFrame - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [2, 4]] + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [5, 4]] + + editor.delete() + + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [2, 4]] + + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1)) + expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [2, 4]] describe "when the command key is held down", -> it "adds a new selection and selects to the nearest screen position, then merges intersecting selections when the mouse button is released", -> @@ -1952,88 +2276,113 @@ fdescribe "TextEditorComponent", -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1, metaKey: true)) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1)) - nextAnimationFrame() - expect(editor.getSelectedScreenRanges()).toEqual [[[4, 4], [4, 9]], [[2, 4], [6, 8]]] - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([4, 6]), which: 1)) - nextAnimationFrame() - expect(editor.getSelectedScreenRanges()).toEqual [[[4, 4], [4, 9]], [[2, 4], [4, 6]]] + waitsForAnimationFrame() - linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([4, 6]), which: 1)) - expect(editor.getSelectedScreenRanges()).toEqual [[[2, 4], [4, 9]]] + runs -> + expect(editor.getSelectedScreenRanges()).toEqual [[[4, 4], [4, 9]], [[2, 4], [6, 8]]] + + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([4, 6]), which: 1)) + + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRanges()).toEqual [[[4, 4], [4, 9]], [[2, 4], [4, 6]]] + + linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([4, 6]), which: 1)) + expect(editor.getSelectedScreenRanges()).toEqual [[[2, 4], [4, 9]]] describe "when the editor is destroyed while dragging", -> it "cleans up the handlers for window.mouseup and window.mousemove", -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1)) - nextAnimationFrame() + waitsForAnimationFrame() - spyOn(window, 'removeEventListener').andCallThrough() + runs -> + spyOn(window, 'removeEventListener').andCallThrough() - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 10]), which: 1)) - editor.destroy() - nextAnimationFrame() + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 10]), which: 1)) + editor.destroy() - call.args.pop() for call in window.removeEventListener.calls - expect(window.removeEventListener).toHaveBeenCalledWith('mouseup') - expect(window.removeEventListener).toHaveBeenCalledWith('mousemove') + waitsForAnimationFrame() + + runs -> + call.args.pop() for call in window.removeEventListener.calls + expect(window.removeEventListener).toHaveBeenCalledWith('mouseup') + expect(window.removeEventListener).toHaveBeenCalledWith('mousemove') describe "when the mouse is double-clicked and dragged", -> it "expands the selection over the nearest word as the cursor moves", -> jasmine.attachToDOM(wrapperNode) wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 1)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 2)) - expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [5, 13]] + runs -> + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 1)) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 2)) + expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [5, 13]] - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1)) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [12, 2]] + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1)) - maximalScrollTop = wrapperNode.getScrollTop() + waitsForAnimationFrame() - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([9, 3]), which: 1)) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [9, 4]] - expect(wrapperNode.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression) + maximalScrollTop = null + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [12, 2]] - linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), which: 1)) + maximalScrollTop = wrapperNode.getScrollTop() + + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([9, 3]), which: 1)) + + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [9, 4]] + expect(wrapperNode.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression) + + linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), which: 1)) describe "when the mouse is triple-clicked and dragged", -> it "expands the selection over the nearest line as the cursor moves", -> jasmine.attachToDOM(wrapperNode) wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 1)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 2)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 3)) - expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [6, 0]] + runs -> + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 1)) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 2)) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 3)) + expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [6, 0]] - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1)) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [12, 2]] + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1)) - maximalScrollTop = wrapperNode.getScrollTop() + waitsForAnimationFrame() - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 4]), which: 1)) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [8, 0]] - expect(wrapperNode.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression) + maximalScrollTop = null + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [12, 2]] - linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), which: 1)) + maximalScrollTop = wrapperNode.getScrollTop() + + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 4]), which: 1)) + + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [8, 0]] + expect(wrapperNode.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression) + + linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), which: 1)) describe "when a line is folded", -> beforeEach -> editor.foldBufferRow 4 - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() describe "when the folded line's fold-marker is clicked", -> it "unfolds the buffer row", -> @@ -2095,67 +2444,89 @@ fdescribe "TextEditorComponent", -> it "selects the rows between the start and end of the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - nextAnimationFrame() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) - expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]] + waitsForAnimationFrame() + + runs -> + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) + expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]] describe "when dragging upward", -> it "selects the rows between the start and end of the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) - nextAnimationFrame() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2))) - expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]] + + waitsForAnimationFrame() + + runs -> + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2))) + expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]] it "orients the selection appropriately when the mouse moves above or below the initially-clicked row", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) - nextAnimationFrame() - expect(editor.getLastSelection().isReversed()).toBe true - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - nextAnimationFrame() - expect(editor.getLastSelection().isReversed()).toBe false + waitsForAnimationFrame() + + runs -> + expect(editor.getLastSelection().isReversed()).toBe true + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) + + waitsForAnimationFrame() + + runs -> + expect(editor.getLastSelection().isReversed()).toBe false it "autoscrolls when the cursor approaches the top or bottom of the editor", -> wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(wrapperNode.getScrollTop()).toBe 0 + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) - nextAnimationFrame() + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) - expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 - maxScrollTop = wrapperNode.getScrollTop() + waitsForAnimationFrame() + maxScrollTop = null + runs -> + expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 + maxScrollTop = wrapperNode.getScrollTop() - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(10))) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe maxScrollTop + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(10))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBeLessThan maxScrollTop + waitsForAnimationFrame() + + runs -> + expect(wrapperNode.getScrollTop()).toBe maxScrollTop + + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) + + waitsForAnimationFrame() + + runs -> + expect(wrapperNode.getScrollTop()).toBeLessThan maxScrollTop it "stops selecting if a textInput event occurs during the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]] + waitsForAnimationFrame() - inputEvent = new Event('textInput') - inputEvent.data = 'x' - Object.defineProperty(inputEvent, 'target', get: -> componentNode.querySelector('.hidden-input')) - componentNode.dispatchEvent(inputEvent) - nextAnimationFrame() + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]] - expect(editor.getSelectedScreenRange()).toEqual [[2, 1], [2, 1]] + inputEvent = new Event('textInput') + inputEvent.data = 'x' + Object.defineProperty(inputEvent, 'target', get: -> componentNode.querySelector('.hidden-input')) + componentNode.dispatchEvent(inputEvent) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(12))) - expect(nextAnimationFrame).toBe noAnimationFrame - expect(editor.getSelectedScreenRange()).toEqual [[2, 1], [2, 1]] + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 1], [2, 1]] + + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(12))) + expect(editor.getSelectedScreenRange()).toEqual [[2, 1], [2, 1]] describe "when the gutter is meta-clicked and dragged", -> beforeEach -> @@ -2165,33 +2536,45 @@ fdescribe "TextEditorComponent", -> it "selects the rows between the start and end of the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - nextAnimationFrame() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [7, 0]]] + + waitsForAnimationFrame() + + runs -> + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [7, 0]]] it "merges overlapping selections when the mouse button is released", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - nextAnimationFrame() - expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[2, 0], [7, 0]]] - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[2, 0], [7, 0]]] + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[2, 0], [7, 0]]] + + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[2, 0], [7, 0]]] describe "when dragging upward", -> it "selects the rows between the start and end of the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) - nextAnimationFrame() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [7, 0]]] + + waitsForAnimationFrame() + + runs -> + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [7, 0]]] it "merges overlapping selections", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) - nextAnimationFrame() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[2, 0], [7, 0]]] + + waitsForAnimationFrame() + + runs -> + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[2, 0], [7, 0]]] describe "when the gutter is shift-clicked and dragged", -> describe "when the shift-click is below the existing selection's tail", -> @@ -2199,23 +2582,28 @@ fdescribe "TextEditorComponent", -> it "selects the rows between the existing selection's tail and the end of the drag", -> editor.setSelectedScreenRange([[3, 4], [4, 5]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), shiftKey: true)) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [9, 0]] + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [9, 0]] describe "when dragging upward", -> it "selects the rows between the end of the drag and the tail of the existing selection", -> editor.setSelectedScreenRange([[4, 4], [5, 5]]) gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), shiftKey: true)) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5))) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[4, 4], [6, 0]] + waitsForAnimationFrame() - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [4, 4]] + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[4, 4], [6, 0]] + + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) + + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [4, 4]] describe "when the shift-click is above the existing selection's tail", -> describe "when dragging upward", -> @@ -2224,8 +2612,10 @@ fdescribe "TextEditorComponent", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), shiftKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [4, 4]] + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [4, 4]] describe "when dragging downward", -> it "selects the rows between the existing selection's tail and the end of the drag", -> @@ -2233,22 +2623,28 @@ fdescribe "TextEditorComponent", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), shiftKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [3, 4]] - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [9, 0]] + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [3, 4]] + + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) + + waitsForAnimationFrame() + + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [9, 0]] describe "when soft wrap is enabled", -> beforeEach -> gutterNode = componentNode.querySelector('.gutter') - editor.setSoftWrapped(true) - nextAnimationFrame() - componentNode.style.width = 21 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + componentNode.style.width = 21 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' + component.measureDimensions() + waitsForPromise -> atom.views.getNextUpdatePromise() describe "when the gutter is clicked", -> it "selects the clicked buffer row", -> @@ -2284,17 +2680,19 @@ fdescribe "TextEditorComponent", -> it "selects the buffer row containing the click, then screen rows until the end of the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - nextAnimationFrame() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) - expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [6, 14]] + waitsForAnimationFrame() + runs -> + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) + expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [6, 14]] describe "when dragging upward", -> it "selects the buffer row containing the click, then screen rows until the end of the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - nextAnimationFrame() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(1))) - expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [10, 0]] + waitsForAnimationFrame() + runs -> + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(1))) + expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [10, 0]] describe "when the gutter is meta-clicked and dragged", -> beforeEach -> @@ -2304,31 +2702,35 @@ fdescribe "TextEditorComponent", -> it "adds a selection from the buffer row containing the click to the screen row containing the end of the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3), metaKey: true)) - nextAnimationFrame() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(3), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[0, 0], [3, 14]]] + waitsForAnimationFrame() + runs -> + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(3), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[0, 0], [3, 14]]] it "merges overlapping selections on mouseup", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7), metaKey: true)) - nextAnimationFrame() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(7), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[0, 0], [7, 12]]] + waitsForAnimationFrame() + runs -> + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(7), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[0, 0], [7, 12]]] describe "when dragging upward", -> it "adds a selection from the buffer row containing the click to the screen row containing the end of the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11), metaKey: true)) - nextAnimationFrame() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[11, 4], [19, 0]]] + waitsForAnimationFrame() + runs -> + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[11, 4], [19, 0]]] it "merges overlapping selections on mouseup", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5), metaKey: true)) - nextAnimationFrame() - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[5, 0], [19, 0]]] + waitsForAnimationFrame() + runs -> + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), metaKey: true)) + expect(editor.getSelectedScreenRanges()).toEqual [[[5, 0], [19, 0]]] describe "when the gutter is shift-clicked and dragged", -> describe "when the shift-click is below the existing selection's tail", -> @@ -2338,8 +2740,9 @@ fdescribe "TextEditorComponent", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), shiftKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11))) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[1, 4], [11, 14]] + waitsForAnimationFrame() + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[1, 4], [11, 14]] describe "when dragging upward", -> it "selects the screen rows between the end of the drag and the tail of the existing selection", -> @@ -2347,8 +2750,9 @@ fdescribe "TextEditorComponent", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), shiftKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[1, 4], [7, 12]] + waitsForAnimationFrame() + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[1, 4], [7, 12]] describe "when the shift-click is above the existing selection's tail", -> describe "when dragging upward", -> @@ -2357,8 +2761,9 @@ fdescribe "TextEditorComponent", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(3), shiftKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [7, 4]] + waitsForAnimationFrame() + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [7, 4]] describe "when dragging downward", -> it "selects the screen rows between the existing selection's tail and the end of the drag", -> @@ -2366,8 +2771,9 @@ fdescribe "TextEditorComponent", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), shiftKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3))) - nextAnimationFrame() - expect(editor.getSelectedScreenRange()).toEqual [[3, 2], [7, 4]] + waitsForAnimationFrame() + runs -> + expect(editor.getSelectedScreenRange()).toEqual [[3, 2], [7, 4]] describe "focus handling", -> inputNode = null @@ -2384,109 +2790,136 @@ fdescribe "TextEditorComponent", -> it "adds the 'is-focused' class to the editor when the hidden input is focused", -> expect(document.activeElement).toBe document.body inputNode.focus() - nextAnimationFrame() - expect(componentNode.classList.contains('is-focused')).toBe true - expect(wrapperNode.classList.contains('is-focused')).toBe true - inputNode.blur() - nextAnimationFrame() - expect(componentNode.classList.contains('is-focused')).toBe false - expect(wrapperNode.classList.contains('is-focused')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(componentNode.classList.contains('is-focused')).toBe true + expect(wrapperNode.classList.contains('is-focused')).toBe true + inputNode.blur() + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(componentNode.classList.contains('is-focused')).toBe false + expect(wrapperNode.classList.contains('is-focused')).toBe false describe "selection handling", -> cursor = null beforeEach -> - cursor = editor.getLastCursor() - cursor.setScreenPosition([0, 0]) + console.log editor.getText() + editor.setCursorScreenPosition([0, 0]) + waitsForPromise -> atom.views.getNextUpdatePromise() it "adds the 'has-selection' class to the editor when there is a selection", -> expect(componentNode.classList.contains('has-selection')).toBe false - editor.selectDown() - nextAnimationFrame() - expect(componentNode.classList.contains('has-selection')).toBe true - cursor.moveDown() - nextAnimationFrame() - expect(componentNode.classList.contains('has-selection')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.classList.contains('has-selection')).toBe true + editor.moveDown() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.classList.contains('has-selection')).toBe false describe "scrolling", -> it "updates the vertical scrollbar when the scrollTop is changed in the model", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - nextAnimationFrame() - expect(verticalScrollbarNode.scrollTop).toBe 0 + waitsForPromise -> atom.views.getNextUpdatePromise() - wrapperNode.setScrollTop(10) - nextAnimationFrame() - expect(verticalScrollbarNode.scrollTop).toBe 10 + runs -> + expect(verticalScrollbarNode.scrollTop).toBe 0 + wrapperNode.setScrollTop(10) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(verticalScrollbarNode.scrollTop).toBe 10 it "updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model", -> componentNode.style.width = 30 * charWidth + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - tilesNodes = component.tileNodesForLines() + tilesNodes = null + runs -> + tilesNodes = component.tileNodesForLines() - top = 0 - for tileNode in tilesNodes - expect(tileNode.style['-webkit-transform']).toBe "translate3d(0px, #{top}px, 0px)" - top += tileNode.offsetHeight + top = 0 + for tileNode in tilesNodes + expect(tileNode.style['-webkit-transform']).toBe "translate3d(0px, #{top}px, 0px)" + top += tileNode.offsetHeight - expect(horizontalScrollbarNode.scrollLeft).toBe 0 + expect(horizontalScrollbarNode.scrollLeft).toBe 0 - wrapperNode.setScrollLeft(100) - nextAnimationFrame() + wrapperNode.setScrollLeft(100) - top = 0 - for tileNode in tilesNodes - expect(tileNode.style['-webkit-transform']).toBe "translate3d(-100px, #{top}px, 0px)" - top += tileNode.offsetHeight + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(horizontalScrollbarNode.scrollLeft).toBe 100 + runs -> + top = 0 + for tileNode in tilesNodes + expect(tileNode.style['-webkit-transform']).toBe "translate3d(-100px, #{top}px, 0px)" + top += tileNode.offsetHeight + + expect(horizontalScrollbarNode.scrollLeft).toBe 100 it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> componentNode.style.width = 30 * charWidth + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(wrapperNode.getScrollLeft()).toBe 0 - horizontalScrollbarNode.scrollLeft = 100 - horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - nextAnimationFrame() + runs -> + expect(wrapperNode.getScrollLeft()).toBe 0 + horizontalScrollbarNode.scrollLeft = 100 + horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - expect(wrapperNode.getScrollLeft()).toBe 100 + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(wrapperNode.getScrollLeft()).toBe 100 it "does not obscure the last line with the horizontal scrollbar", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) - nextAnimationFrame() - lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) - bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom - topOfHorizontalScrollbar = horizontalScrollbarNode.getBoundingClientRect().top - expect(bottomOfLastLine).toBe topOfHorizontalScrollbar - # Scroll so there's no space below the last line when the horizontal scrollbar disappears - wrapperNode.style.width = 100 * charWidth + 'px' - component.measureDimensions() - nextAnimationFrame() - bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom - bottomOfEditor = componentNode.getBoundingClientRect().bottom - expect(bottomOfLastLine).toBe bottomOfEditor + waitsForPromise -> atom.views.getNextUpdatePromise() + + lastLineNode = null + runs -> + lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) + bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom + topOfHorizontalScrollbar = horizontalScrollbarNode.getBoundingClientRect().top + expect(bottomOfLastLine).toBe topOfHorizontalScrollbar + + # Scroll so there's no space below the last line when the horizontal scrollbar disappears + wrapperNode.style.width = 100 * charWidth + 'px' + component.measureDimensions() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom + bottomOfEditor = componentNode.getBoundingClientRect().bottom + expect(bottomOfLastLine).toBe bottomOfEditor it "does not obscure the last character of the longest line with the vertical scrollbar", -> wrapperNode.style.height = 7 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() wrapperNode.setScrollLeft(Infinity) - nextAnimationFrame() - rightOfLongestLine = component.lineNodeForScreenRow(6).querySelector('.line > span:last-child').getBoundingClientRect().right - leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left - expect(Math.round(rightOfLongestLine)).toBeCloseTo leftOfVerticalScrollbar - 1, 0 # Leave 1 px so the cursor is visible on the end of the line + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + rightOfLongestLine = component.lineNodeForScreenRow(6).querySelector('.line > span:last-child').getBoundingClientRect().right + leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left + expect(Math.round(rightOfLongestLine)).toBeCloseTo leftOfVerticalScrollbar - 1, 0 # Leave 1 px so the cursor is visible on the end of the line it "only displays dummy scrollbars when scrollable in that direction", -> expect(verticalScrollbarNode.style.display).toBe 'none' @@ -2495,48 +2928,57 @@ fdescribe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = '1000px' component.measureDimensions() - nextAnimationFrame() - expect(verticalScrollbarNode.style.display).toBe '' - expect(horizontalScrollbarNode.style.display).toBe 'none' + waitsForPromise -> atom.views.getNextUpdatePromise() - componentNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - nextAnimationFrame() + runs -> + expect(verticalScrollbarNode.style.display).toBe '' + expect(horizontalScrollbarNode.style.display).toBe 'none' - expect(verticalScrollbarNode.style.display).toBe '' - expect(horizontalScrollbarNode.style.display).toBe '' + componentNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() - wrapperNode.style.height = 20 * lineHeightInPixels + 'px' - component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(verticalScrollbarNode.style.display).toBe 'none' - expect(horizontalScrollbarNode.style.display).toBe '' + runs -> + expect(verticalScrollbarNode.style.display).toBe '' + expect(horizontalScrollbarNode.style.display).toBe '' + + wrapperNode.style.height = 20 * lineHeightInPixels + 'px' + component.measureDimensions() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(verticalScrollbarNode.style.display).toBe 'none' + expect(horizontalScrollbarNode.style.display).toBe '' it "makes the dummy scrollbar divs only as tall/wide as the actual scrollbars", -> wrapperNode.style.height = 4 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - atom.styles.addStyleSheet """ - ::-webkit-scrollbar { - width: 8px; - height: 8px; - } - """, context: 'atom-text-editor' - nextAnimationFrame() # handle stylesheet change event - nextAnimationFrame() # perform requested update + runs -> + atom.styles.addStyleSheet """ + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + """, context: 'atom-text-editor' - scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') - expect(verticalScrollbarNode.offsetWidth).toBe 8 - expect(horizontalScrollbarNode.offsetHeight).toBe 8 - expect(scrollbarCornerNode.offsetWidth).toBe 8 - expect(scrollbarCornerNode.offsetHeight).toBe 8 + waitsForAnimationFrame() # handle stylesheet change event + waitsForAnimationFrame() # perform requested update - atom.themes.removeStylesheet('test') + runs -> + scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') + expect(verticalScrollbarNode.offsetWidth).toBe 8 + expect(horizontalScrollbarNode.offsetHeight).toBe 8 + expect(scrollbarCornerNode.offsetWidth).toBe 8 + expect(scrollbarCornerNode.offsetHeight).toBe 8 + + atom.themes.removeStylesheet('test') it "assigns the bottom/right of the scrollbars to the width of the opposite scrollbar if it is visible", -> scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') @@ -2547,33 +2989,43 @@ fdescribe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = '1000px' component.measureDimensions() - nextAnimationFrame() - expect(verticalScrollbarNode.style.bottom).toBe '0px' - expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' - expect(scrollbarCornerNode.style.display).toBe 'none' - componentNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - nextAnimationFrame() - expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' - expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' - expect(scrollbarCornerNode.style.display).toBe '' + waitsForPromise -> atom.views.getNextUpdatePromise() - wrapperNode.style.height = 20 * lineHeightInPixels + 'px' - component.measureDimensions() - nextAnimationFrame() - expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' - expect(horizontalScrollbarNode.style.right).toBe '0px' - expect(scrollbarCornerNode.style.display).toBe 'none' + runs -> + expect(verticalScrollbarNode.style.bottom).toBe '0px' + expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' + expect(scrollbarCornerNode.style.display).toBe 'none' + + componentNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' + expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' + expect(scrollbarCornerNode.style.display).toBe '' + + wrapperNode.style.height = 20 * lineHeightInPixels + 'px' + component.measureDimensions() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' + expect(horizontalScrollbarNode.style.right).toBe '0px' + expect(scrollbarCornerNode.style.display).toBe 'none' it "accounts for the width of the gutter in the scrollWidth of the horizontal scrollbar", -> gutterNode = componentNode.querySelector('.gutter') componentNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(horizontalScrollbarNode.scrollWidth).toBe wrapperNode.getScrollWidth() - expect(horizontalScrollbarNode.style.left).toBe '0px' + runs -> + expect(horizontalScrollbarNode.scrollWidth).toBe wrapperNode.getScrollWidth() + expect(horizontalScrollbarNode.style.left).toBe '0px' describe "mousewheel events", -> beforeEach -> @@ -2584,71 +3036,96 @@ fdescribe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() 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 componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10)) - nextAnimationFrame() - expect(verticalScrollbarNode.scrollTop).toBe 10 - expect(horizontalScrollbarNode.scrollLeft).toBe 0 - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) - nextAnimationFrame() - expect(verticalScrollbarNode.scrollTop).toBe 10 - expect(horizontalScrollbarNode.scrollLeft).toBe 15 + waitsForAnimationFrame() + + runs -> + expect(verticalScrollbarNode.scrollTop).toBe 10 + expect(horizontalScrollbarNode.scrollLeft).toBe 0 + + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) + + waitsForAnimationFrame() + + runs -> + expect(verticalScrollbarNode.scrollTop).toBe 10 + expect(horizontalScrollbarNode.scrollLeft).toBe 15 it "updates the scrollLeft or scrollTop according to the scroll sensitivity", -> atom.config.set('editor.scrollSensitivity', 50) componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10)) - nextAnimationFrame() - expect(horizontalScrollbarNode.scrollLeft).toBe 0 + waitsForAnimationFrame() - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) - nextAnimationFrame() - expect(verticalScrollbarNode.scrollTop).toBe 5 - expect(horizontalScrollbarNode.scrollLeft).toBe 7 + runs -> + expect(horizontalScrollbarNode.scrollLeft).toBe 0 + + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) + + waitsForAnimationFrame() + + runs -> + expect(verticalScrollbarNode.scrollTop).toBe 5 + expect(horizontalScrollbarNode.scrollLeft).toBe 7 it "uses the previous scrollSensitivity when the value is not an int", -> atom.config.set('editor.scrollSensitivity', 'nope') componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -10)) - nextAnimationFrame() - expect(verticalScrollbarNode.scrollTop).toBe 10 + waitsForAnimationFrame() + + runs -> + expect(verticalScrollbarNode.scrollTop).toBe 10 it "parses negative scrollSensitivity values at the minimum", -> atom.config.set('editor.scrollSensitivity', -50) componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -10)) - nextAnimationFrame() - expect(verticalScrollbarNode.scrollTop).toBe 1 + waitsForAnimationFrame() + + runs -> + expect(verticalScrollbarNode.scrollTop).toBe 1 describe "when the mousewheel event's target is a line", -> it "keeps the line on the DOM if it is scrolled off-screen", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() + waitsForPromise -> atom.views.getNextUpdatePromise() - lineNode = componentNode.querySelector('.line') - wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) - Object.defineProperty(wheelEvent, 'target', get: -> lineNode) - componentNode.dispatchEvent(wheelEvent) - nextAnimationFrame() + lineNode = null + runs -> + lineNode = componentNode.querySelector('.line') + wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) + Object.defineProperty(wheelEvent, 'target', get: -> lineNode) + componentNode.dispatchEvent(wheelEvent) - expect(componentNode.contains(lineNode)).toBe true + waitsForAnimationFrame() + + runs -> + expect(componentNode.contains(lineNode)).toBe true it "does not set the mouseWheelScreenRow if scrolling horizontally", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() + waitsForPromise -> atom.views.getNextUpdatePromise() - lineNode = componentNode.querySelector('.line') - wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 10, wheelDeltaY: 0) - Object.defineProperty(wheelEvent, 'target', get: -> lineNode) - componentNode.dispatchEvent(wheelEvent) - nextAnimationFrame() + lineNode = null + runs -> + lineNode = componentNode.querySelector('.line') + wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 10, wheelDeltaY: 0) + Object.defineProperty(wheelEvent, 'target', get: -> lineNode) + componentNode.dispatchEvent(wheelEvent) - expect(component.presenter.mouseWheelScreenRow).toBe null + waitsForAnimationFrame() + + runs -> + expect(component.presenter.mouseWheelScreenRow).toBe null it "clears the mouseWheelScreenRow after a delay even if the event does not cause scrolling", -> expect(wrapperNode.getScrollTop()).toBe 0 @@ -2661,8 +3138,8 @@ fdescribe "TextEditorComponent", -> expect(wrapperNode.getScrollTop()).toBe 0 expect(component.presenter.mouseWheelScreenRow).toBe 0 - advanceClock(component.presenter.stoppedScrollingDelay) - expect(component.presenter.mouseWheelScreenRow).toBe null + + waitsFor -> not component.presenter.mouseWheelScreenRow? it "does not preserve the line if it is on screen", -> expect(componentNode.querySelectorAll('.line-number').length).toBe 14 # dummy line @@ -2684,14 +3161,19 @@ fdescribe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() + waitsForPromise -> atom.views.getNextUpdatePromise() - lineNumberNode = componentNode.querySelectorAll('.line-number')[1] - wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) - Object.defineProperty(wheelEvent, 'target', get: -> lineNumberNode) - componentNode.dispatchEvent(wheelEvent) - nextAnimationFrame() + lineNumberNode = null + runs -> + lineNumberNode = componentNode.querySelectorAll('.line-number')[1] + wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) + Object.defineProperty(wheelEvent, 'target', get: -> lineNumberNode) + componentNode.dispatchEvent(wheelEvent) - expect(componentNode.contains(lineNumberNode)).toBe true + waitsForAnimationFrame() + + runs -> + expect(componentNode.contains(lineNumberNode)).toBe true it "only prevents the default action of the mousewheel event if it actually lead to scrolling", -> spyOn(WheelEvent::, 'preventDefault').andCallThrough() @@ -2699,41 +3181,48 @@ fdescribe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - # try to scroll past the top, which is impossible - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 50)) - expect(wrapperNode.getScrollTop()).toBe 0 - expect(WheelEvent::preventDefault).not.toHaveBeenCalled() + runs -> + # try to scroll past the top, which is impossible + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 50)) + expect(wrapperNode.getScrollTop()).toBe 0 + expect(WheelEvent::preventDefault).not.toHaveBeenCalled() - # scroll to the bottom in one huge event - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -3000)) - nextAnimationFrame() - maxScrollTop = wrapperNode.getScrollTop() - expect(WheelEvent::preventDefault).toHaveBeenCalled() - WheelEvent::preventDefault.reset() + # scroll to the bottom in one huge event + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -3000)) - # try to scroll past the bottom, which is impossible - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -30)) - expect(wrapperNode.getScrollTop()).toBe maxScrollTop - expect(WheelEvent::preventDefault).not.toHaveBeenCalled() + waitsForAnimationFrame() - # try to scroll past the left side, which is impossible - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 50, wheelDeltaY: 0)) - expect(wrapperNode.getScrollLeft()).toBe 0 - expect(WheelEvent::preventDefault).not.toHaveBeenCalled() + runs -> + maxScrollTop = wrapperNode.getScrollTop() + expect(WheelEvent::preventDefault).toHaveBeenCalled() + WheelEvent::preventDefault.reset() - # scroll all the way right - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -3000, wheelDeltaY: 0)) - nextAnimationFrame() - maxScrollLeft = wrapperNode.getScrollLeft() - expect(WheelEvent::preventDefault).toHaveBeenCalled() - WheelEvent::preventDefault.reset() + # try to scroll past the bottom, which is impossible + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -30)) + expect(wrapperNode.getScrollTop()).toBe maxScrollTop + expect(WheelEvent::preventDefault).not.toHaveBeenCalled() - # try to scroll past the right side, which is impossible - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -30, wheelDeltaY: 0)) - expect(wrapperNode.getScrollLeft()).toBe maxScrollLeft - expect(WheelEvent::preventDefault).not.toHaveBeenCalled() + # try to scroll past the left side, which is impossible + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 50, wheelDeltaY: 0)) + expect(wrapperNode.getScrollLeft()).toBe 0 + expect(WheelEvent::preventDefault).not.toHaveBeenCalled() + + # scroll all the way right + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -3000, wheelDeltaY: 0)) + + waitsForAnimationFrame() + + runs -> + maxScrollLeft = wrapperNode.getScrollLeft() + expect(WheelEvent::preventDefault).toHaveBeenCalled() + WheelEvent::preventDefault.reset() + + # try to scroll past the right side, which is impossible + componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -30, wheelDeltaY: 0)) + expect(wrapperNode.getScrollLeft()).toBe maxScrollLeft + expect(WheelEvent::preventDefault).not.toHaveBeenCalled() describe "input events", -> inputNode = null @@ -2749,30 +3238,34 @@ fdescribe "TextEditorComponent", -> it "inserts the newest character in the input's value into the buffer", -> componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) - nextAnimationFrame() - expect(editor.lineTextForBufferRow(0)).toBe 'xvar quicksort = function () {' - - componentNode.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode)) - nextAnimationFrame() - expect(editor.lineTextForBufferRow(0)).toBe 'xyvar quicksort = function () {' + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(editor.lineTextForBufferRow(0)).toBe 'xvar quicksort = function () {' + componentNode.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode)) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(editor.lineTextForBufferRow(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", -> componentNode.dispatchEvent(buildTextInputEvent(data: 'u', target: inputNode)) - nextAnimationFrame() - expect(editor.lineTextForBufferRow(0)).toBe 'uvar quicksort = function () {' + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(editor.lineTextForBufferRow(0)).toBe 'uvar quicksort = function () {' - # simulate the accented character suggestion's selection of the previous character - inputNode.setSelectionRange(0, 1) - componentNode.dispatchEvent(buildTextInputEvent(data: 'ü', target: inputNode)) - nextAnimationFrame() - expect(editor.lineTextForBufferRow(0)).toBe 'üvar quicksort = function () {' + # simulate the accented character suggestion's selection of the previous character + inputNode.setSelectionRange(0, 1) + componentNode.dispatchEvent(buildTextInputEvent(data: 'ü', target: inputNode)) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(editor.lineTextForBufferRow(0)).toBe 'üvar quicksort = function () {' it "does not handle input events when input is disabled", -> - nextAnimationFrame = noAnimationFrame # This spec is flaky on the build machine, so this. component.setInputEnabled(false) componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) - expect(nextAnimationFrame).toBe noAnimationFrame expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () {' + waitsForAnimationFrame() + runs -> + expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () {' it "groups events that occur close together in time into single undo entries", -> currentTime = 0 @@ -2900,12 +3393,13 @@ fdescribe "TextEditorComponent", -> expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0) component.setFontSize(10) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0) - expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0) - expect(editor.getDoubleWidthCharWidth()).toBe(10) - expect(editor.getHalfWidthCharWidth()).toBe(5) + runs -> + expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0) + expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0) + expect(editor.getDoubleWidthCharWidth()).toBe(10) + expect(editor.getHalfWidthCharWidth()).toBe(5) describe "hiding and showing the editor", -> describe "when the editor is hidden when it is mounted", -> @@ -2975,11 +3469,12 @@ fdescribe "TextEditorComponent", -> component.checkForVisibilityChange() editor.setCursorBufferPosition([0, Infinity]) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left - line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right - expect(cursorLeft).toBeCloseTo line0Right, 0 + runs -> + cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left + line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right + expect(cursorLeft).toBeCloseTo line0Right, 0 describe "when the fontFamily changes while the editor is hidden", -> it "does not attempt to measure the defaultCharWidth until the editor becomes visible again", -> @@ -3007,11 +3502,12 @@ fdescribe "TextEditorComponent", -> component.checkForVisibilityChange() editor.setCursorBufferPosition([0, Infinity]) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left - line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right - expect(cursorLeft).toBeCloseTo line0Right, 0 + runs -> + cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left + line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right + expect(cursorLeft).toBeCloseTo line0Right, 0 describe "when stylesheets change while the editor is hidden", -> afterEach -> @@ -3033,49 +3529,36 @@ fdescribe "TextEditorComponent", -> component.checkForVisibilityChange() editor.setCursorBufferPosition([0, Infinity]) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left - line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right - expect(cursorLeft).toBeCloseTo line0Right, 0 - - describe "when lines are changed while the editor is hidden", -> - xit "does not measure new characters until the editor is shown again", -> - # TODO: This spec fails. Check if we need to keep it or not. - - editor.setText('') - - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - - editor.setText('var z = 1') - editor.setCursorBufferPosition([0, Infinity]) - nextAnimationFrame() - - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - - expect(componentNode.querySelector('.cursor').style['-webkit-transform']).toBe "translate(#{9 * charWidth}px, 0px)" + runs -> + cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left + line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right + expect(cursorLeft).toBeCloseTo line0Right, 0 describe "soft wrapping", -> beforeEach -> editor.setSoftWrapped(true) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() it "updates the wrap location when the editor is resized", -> newHeight = 4 * editor.getLineHeightInPixels() + "px" expect(parseInt(newHeight)).toBeLessThan wrapperNode.offsetHeight wrapperNode.style.height = newHeight - atom.views.performDocumentPoll() - nextAnimationFrame() - expect(componentNode.querySelectorAll('.line')).toHaveLength(7) # visible rows + model longest screen row + waitsForPromise -> atom.views.getNextUpdatePromise() - gutterWidth = componentNode.querySelector('.gutter').offsetWidth - componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - atom.views.performDocumentPoll() - nextAnimationFrame() - expect(componentNode.querySelector('.line').textContent).toBe "var quicksort " + runs -> + expect(componentNode.querySelectorAll('.line')).toHaveLength(7) # visible rows + model longest screen row + + gutterWidth = componentNode.querySelector('.gutter').offsetWidth + componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' + atom.views.performDocumentPoll() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.querySelector('.line').textContent).toBe "var quicksort " it "accounts for the scroll view's padding when determining the wrap location", -> scrollViewNode = componentNode.querySelector('.scroll-view') @@ -3083,65 +3566,85 @@ fdescribe "TextEditorComponent", -> componentNode.style.width = 30 * charWidth + 'px' atom.views.performDocumentPoll() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(component.lineNodeForScreenRow(0).textContent).toBe "var quicksort = " + runs -> + expect(component.lineNodeForScreenRow(0).textContent).toBe "var quicksort = " describe "default decorations", -> it "applies .cursor-line decorations for line numbers overlapping selections", -> editor.setCursorScreenPosition([4, 4]) - nextAnimationFrame() - expect(lineNumberHasClass(3, 'cursor-line')).toBe false - expect(lineNumberHasClass(4, 'cursor-line')).toBe true - expect(lineNumberHasClass(5, 'cursor-line')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.setSelectedScreenRange([[3, 4], [4, 4]]) - nextAnimationFrame() - expect(lineNumberHasClass(3, 'cursor-line')).toBe true - expect(lineNumberHasClass(4, 'cursor-line')).toBe true + runs -> + expect(lineNumberHasClass(3, 'cursor-line')).toBe false + expect(lineNumberHasClass(4, 'cursor-line')).toBe true + expect(lineNumberHasClass(5, 'cursor-line')).toBe false - editor.setSelectedScreenRange([[3, 4], [4, 0]]) - nextAnimationFrame() - expect(lineNumberHasClass(3, 'cursor-line')).toBe true - expect(lineNumberHasClass(4, 'cursor-line')).toBe false + editor.setSelectedScreenRange([[3, 4], [4, 4]]) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(3, 'cursor-line')).toBe true + expect(lineNumberHasClass(4, 'cursor-line')).toBe true + + editor.setSelectedScreenRange([[3, 4], [4, 0]]) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(3, 'cursor-line')).toBe true + expect(lineNumberHasClass(4, 'cursor-line')).toBe false it "does not apply .cursor-line to the last line of a selection if it's empty", -> editor.setSelectedScreenRange([[3, 4], [5, 0]]) - nextAnimationFrame() - expect(lineNumberHasClass(3, 'cursor-line')).toBe true - expect(lineNumberHasClass(4, 'cursor-line')).toBe true - expect(lineNumberHasClass(5, 'cursor-line')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(lineNumberHasClass(3, 'cursor-line')).toBe true + expect(lineNumberHasClass(4, 'cursor-line')).toBe true + expect(lineNumberHasClass(5, 'cursor-line')).toBe false it "applies .cursor-line decorations for lines containing the cursor in non-empty selections", -> editor.setCursorScreenPosition([4, 4]) - nextAnimationFrame() - expect(lineHasClass(3, 'cursor-line')).toBe false - expect(lineHasClass(4, 'cursor-line')).toBe true - expect(lineHasClass(5, 'cursor-line')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(lineHasClass(3, 'cursor-line')).toBe false + expect(lineHasClass(4, 'cursor-line')).toBe true + expect(lineHasClass(5, 'cursor-line')).toBe false - editor.setSelectedScreenRange([[3, 4], [4, 4]]) - nextAnimationFrame() - expect(lineHasClass(2, 'cursor-line')).toBe false - expect(lineHasClass(3, 'cursor-line')).toBe false - expect(lineHasClass(4, 'cursor-line')).toBe false - expect(lineHasClass(5, 'cursor-line')).toBe false + editor.setSelectedScreenRange([[3, 4], [4, 4]]) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineHasClass(2, 'cursor-line')).toBe false + expect(lineHasClass(3, 'cursor-line')).toBe false + expect(lineHasClass(4, 'cursor-line')).toBe false + expect(lineHasClass(5, 'cursor-line')).toBe false it "applies .cursor-line-no-selection to line numbers for rows containing the cursor when the selection is empty", -> editor.setCursorScreenPosition([4, 4]) - nextAnimationFrame() - expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe true + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.setSelectedScreenRange([[3, 4], [4, 4]]) - nextAnimationFrame() - expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe false + runs -> + expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe true + + editor.setSelectedScreenRange([[3, 4], [4, 4]]) + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe false describe "height", -> describe "when the wrapper view has an explicit height", -> it "does not assign a height on the component node", -> wrapperNode.style.height = '200px' component.measureDimensions() - nextAnimationFrame() - expect(componentNode.style.height).toBe '' + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.style.height).toBe '' describe "when the wrapper view does not have an explicit height", -> it "assigns a height on the component node based on the editor's content", -> @@ -3151,7 +3654,7 @@ fdescribe "TextEditorComponent", -> describe "when the 'mini' property is true", -> beforeEach -> editor.setMini(true) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() it "does not render the gutter", -> expect(componentNode.querySelector('.gutter')).toBeNull() @@ -3178,11 +3681,16 @@ fdescribe "TextEditorComponent", -> editor.setPlaceholderText('Hello World') expect(componentNode.querySelector('.placeholder-text')).toBeNull() editor.setText('') - nextAnimationFrame() - expect(componentNode.querySelector('.placeholder-text').textContent).toBe "Hello World" - editor.setText('hey') - nextAnimationFrame() - expect(componentNode.querySelector('.placeholder-text')).toBeNull() + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.querySelector('.placeholder-text').textContent).toBe "Hello World" + editor.setText('hey') + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(componentNode.querySelector('.placeholder-text')).toBeNull() describe "grammar data attributes", -> it "adds and updates the grammar data attribute based on the current grammar", -> @@ -3280,41 +3788,49 @@ fdescribe "TextEditorComponent", -> atom.config.set 'editor.invisibles', coffeeInvisibles, scopeSelector: '.source.coffee' editor.setText " a line with tabs\tand spaces \n" - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() it "renders the invisibles when editor.showInvisibles is true for a given grammar", -> expect(component.lineNodeForScreenRow(0).textContent).toBe "#{jsInvisibles.space}a line with tabs#{jsInvisibles.tab}and spaces#{jsInvisibles.space}#{jsInvisibles.eol}" it "does not render the invisibles when editor.showInvisibles is false for a given grammar", -> editor.setGrammar(coffeeEditor.getGrammar()) - nextAnimationFrame() - expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces " + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces " it "re-renders the invisibles when the invisible settings change", -> jsGrammar = editor.getGrammar() editor.setGrammar(coffeeEditor.getGrammar()) atom.config.set 'editor.showInvisibles', true, scopeSelector: '.source.coffee' - nextAnimationFrame() - expect(component.lineNodeForScreenRow(0).textContent).toBe "#{coffeeInvisibles.space}a line with tabs#{coffeeInvisibles.tab}and spaces#{coffeeInvisibles.space}#{coffeeInvisibles.eol}" + waitsForPromise -> atom.views.getNextUpdatePromise() newInvisibles = eol: 'N' space: 'E' tab: 'W' cr: 'I' - atom.config.set 'editor.invisibles', newInvisibles, scopeSelector: '.source.coffee' - nextAnimationFrame() - expect(component.lineNodeForScreenRow(0).textContent).toBe "#{newInvisibles.space}a line with tabs#{newInvisibles.tab}and spaces#{newInvisibles.space}#{newInvisibles.eol}" - editor.setGrammar(jsGrammar) - nextAnimationFrame() - expect(component.lineNodeForScreenRow(0).textContent).toBe "#{jsInvisibles.space}a line with tabs#{jsInvisibles.tab}and spaces#{jsInvisibles.space}#{jsInvisibles.eol}" + runs -> + expect(component.lineNodeForScreenRow(0).textContent).toBe "#{coffeeInvisibles.space}a line with tabs#{coffeeInvisibles.tab}and spaces#{coffeeInvisibles.space}#{coffeeInvisibles.eol}" + atom.config.set 'editor.invisibles', newInvisibles, scopeSelector: '.source.coffee' + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(0).textContent).toBe "#{newInvisibles.space}a line with tabs#{newInvisibles.tab}and spaces#{newInvisibles.space}#{newInvisibles.eol}" + editor.setGrammar(jsGrammar) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.lineNodeForScreenRow(0).textContent).toBe "#{jsInvisibles.space}a line with tabs#{jsInvisibles.tab}and spaces#{jsInvisibles.space}#{jsInvisibles.eol}" describe 'editor.showIndentGuide', -> beforeEach -> atom.config.set 'editor.showIndentGuide', true, scopeSelector: '.source.js' atom.config.set 'editor.showIndentGuide', false, scopeSelector: '.source.coffee' - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() it "has an 'indent-guide' class when scoped editor.showIndentGuide is true, but not when scoped editor.showIndentGuide is false", -> line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) @@ -3323,12 +3839,14 @@ fdescribe "TextEditorComponent", -> expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false editor.setGrammar(coffeeEditor.getGrammar()) - nextAnimationFrame() - line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) - expect(line1LeafNodes[0].textContent).toBe ' ' - expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe false - expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + expect(line1LeafNodes[0].textContent).toBe ' ' + expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe false + expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false it "removes the 'indent-guide' class when editor.showIndentGuide to false", -> line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) @@ -3337,12 +3855,13 @@ fdescribe "TextEditorComponent", -> expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false atom.config.set 'editor.showIndentGuide', false, scopeSelector: '.source.js' - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) - expect(line1LeafNodes[0].textContent).toBe ' ' - expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe false - expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false + runs -> + line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + expect(line1LeafNodes[0].textContent).toBe ' ' + expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe false + expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false describe "autoscroll", -> beforeEach -> @@ -3351,91 +3870,112 @@ fdescribe "TextEditorComponent", -> component.setLineHeight("10px") component.setFontSize(17) component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - wrapperNode.setWidth(55) - wrapperNode.setHeight(55) - component.measureDimensions() - nextAnimationFrame() + runs -> + wrapperNode.setWidth(55) + wrapperNode.setHeight(55) + component.measureDimensions() - component.presenter.setHorizontalScrollbarHeight(0) - component.presenter.setVerticalScrollbarWidth(0) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + component.presenter.setHorizontalScrollbarHeight(0) + component.presenter.setVerticalScrollbarWidth(0) + + waitsForPromise -> atom.views.getNextUpdatePromise() describe "when selecting buffer ranges", -> it "autoscrolls the selection if it is last unless the 'autoscroll' option is false", -> expect(wrapperNode.getScrollTop()).toBe 0 editor.setSelectedBufferRange([[5, 6], [6, 8]]) - nextAnimationFrame() - right = wrapperNode.pixelPositionForBufferPosition([6, 8 + editor.getHorizontalScrollMargin()]).left - expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 - expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.setSelectedBufferRange([[0, 0], [0, 0]]) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - expect(wrapperNode.getScrollLeft()).toBe 0 + right = null + runs -> + right = wrapperNode.pixelPositionForBufferPosition([6, 8 + editor.getHorizontalScrollMargin()]).left + expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 + expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 - editor.setSelectedBufferRange([[6, 6], [6, 8]]) - nextAnimationFrame() - expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 - expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 + editor.setSelectedBufferRange([[0, 0], [0, 0]]) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + expect(wrapperNode.getScrollLeft()).toBe 0 + + editor.setSelectedBufferRange([[6, 6], [6, 8]]) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 + expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 describe "when adding selections for buffer ranges", -> it "autoscrolls to the added selection if needed", -> editor.addSelectionForBufferRange([[8, 10], [8, 15]]) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - right = wrapperNode.pixelPositionForBufferPosition([8, 15]).left - expect(wrapperNode.getScrollBottom()).toBe (9 * 10) + (2 * 10) - expect(wrapperNode.getScrollRight()).toBeCloseTo(right + 2 * 10, 0) + runs -> + right = wrapperNode.pixelPositionForBufferPosition([8, 15]).left + expect(wrapperNode.getScrollBottom()).toBe (9 * 10) + (2 * 10) + expect(wrapperNode.getScrollRight()).toBeCloseTo(right + 2 * 10, 0) describe "when selecting lines containing cursors", -> it "autoscrolls to the selection", -> editor.setCursorScreenPosition([5, 6]) - nextAnimationFrame() - - wrapperNode.scrollToTop() - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - - editor.selectLinesContainingCursors() - nextAnimationFrame() - expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + wrapperNode.scrollToTop() + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.selectLinesContainingCursors() + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 describe "when inserting text", -> describe "when there are multiple empty selections on different lines", -> it "autoscrolls to the last cursor", -> editor.setCursorScreenPosition([1, 2], autoscroll: false) - nextAnimationFrame() - - editor.addCursorAtScreenPosition([10, 4], autoscroll: false) - nextAnimationFrame() - - expect(wrapperNode.getScrollTop()).toBe 0 - editor.insertText('a') - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 75 + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + editor.addCursorAtScreenPosition([10, 4], autoscroll: false) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.insertText('a') + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 75 describe "when scrolled to cursor position", -> it "scrolls the last cursor into view, centering around the cursor if possible and the 'center' option isn't false", -> editor.setCursorScreenPosition([8, 8], autoscroll: false) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - expect(wrapperNode.getScrollLeft()).toBe 0 + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.scrollToCursorPosition() - nextAnimationFrame() - right = wrapperNode.pixelPositionForScreenPosition([8, 9 + editor.getHorizontalScrollMargin()]).left - expect(wrapperNode.getScrollTop()).toBe (8.8 * 10) - 30 - expect(wrapperNode.getScrollBottom()).toBe (8.3 * 10) + 30 - expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + expect(wrapperNode.getScrollLeft()).toBe 0 - wrapperNode.setScrollTop(0) - editor.scrollToCursorPosition(center: false) - expect(wrapperNode.getScrollTop()).toBe (7.8 - editor.getVerticalScrollMargin()) * 10 - expect(wrapperNode.getScrollBottom()).toBe (9.3 + editor.getVerticalScrollMargin()) * 10 + editor.scrollToCursorPosition() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + right = wrapperNode.pixelPositionForScreenPosition([8, 9 + editor.getHorizontalScrollMargin()]).left + expect(wrapperNode.getScrollTop()).toBe (8.8 * 10) - 30 + expect(wrapperNode.getScrollBottom()).toBe (8.3 * 10) + 30 + expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 + + wrapperNode.setScrollTop(0) + editor.scrollToCursorPosition(center: false) + expect(wrapperNode.getScrollTop()).toBe (7.8 - editor.getVerticalScrollMargin()) * 10 + expect(wrapperNode.getScrollBottom()).toBe (9.3 + editor.getVerticalScrollMargin()) * 10 describe "moving cursors", -> it "scrolls down when the last cursor gets closer than ::verticalScrollMargin to the bottom of the editor", -> @@ -3443,172 +3983,228 @@ fdescribe "TextEditorComponent", -> expect(wrapperNode.getScrollBottom()).toBe 5.5 * 10 editor.setCursorScreenPosition([2, 0]) - nextAnimationFrame() - expect(wrapperNode.getScrollBottom()).toBe 5.5 * 10 + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.moveDown() - nextAnimationFrame() - expect(wrapperNode.getScrollBottom()).toBe 6 * 10 + runs -> + expect(wrapperNode.getScrollBottom()).toBe 5.5 * 10 + editor.moveDown() - editor.moveDown() - nextAnimationFrame() - expect(wrapperNode.getScrollBottom()).toBe 7 * 10 + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(wrapperNode.getScrollBottom()).toBe 6 * 10 + + editor.moveDown() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(wrapperNode.getScrollBottom()).toBe 7 * 10 it "scrolls up when the last cursor gets closer than ::verticalScrollMargin to the top of the editor", -> editor.setCursorScreenPosition([11, 0]) - nextAnimationFrame() - wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) - nextAnimationFrame() - editor.moveUp() - nextAnimationFrame() - expect(wrapperNode.getScrollBottom()).toBe wrapperNode.getScrollHeight() - - editor.moveUp() - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 7 * 10 - - editor.moveUp() - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 6 * 10 + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + editor.moveUp() + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollBottom()).toBe wrapperNode.getScrollHeight() + editor.moveUp() + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 7 * 10 + editor.moveUp() + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 6 * 10 it "scrolls right when the last cursor gets closer than ::horizontalScrollMargin to the right of the editor", -> expect(wrapperNode.getScrollLeft()).toBe 0 expect(wrapperNode.getScrollRight()).toBe 5.5 * 10 editor.setCursorScreenPosition([0, 2]) - nextAnimationFrame() - expect(wrapperNode.getScrollRight()).toBe 5.5 * 10 - editor.moveRight() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - margin = component.presenter.getHorizontalScrollMarginInPixels() - right = wrapperNode.pixelPositionForScreenPosition([0, 4]).left + margin - expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 + runs -> + expect(wrapperNode.getScrollRight()).toBe 5.5 * 10 + editor.moveRight() - editor.moveRight() - nextAnimationFrame() - right = wrapperNode.pixelPositionForScreenPosition([0, 5]).left + margin - expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 + waitsForPromise -> atom.views.getNextUpdatePromise() + + margin = null + runs -> + margin = component.presenter.getHorizontalScrollMarginInPixels() + right = wrapperNode.pixelPositionForScreenPosition([0, 4]).left + margin + expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 + editor.moveRight() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + right = wrapperNode.pixelPositionForScreenPosition([0, 5]).left + margin + expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 it "scrolls left when the last cursor gets closer than ::horizontalScrollMargin to the left of the editor", -> wrapperNode.setScrollRight(wrapperNode.getScrollWidth()) - nextAnimationFrame() - expect(wrapperNode.getScrollRight()).toBe wrapperNode.getScrollWidth() - editor.setCursorScreenPosition([6, 62], autoscroll: false) - nextAnimationFrame() - editor.moveLeft() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - margin = component.presenter.getHorizontalScrollMarginInPixels() - left = wrapperNode.pixelPositionForScreenPosition([6, 61]).left - margin - expect(wrapperNode.getScrollLeft()).toBeCloseTo left, 0 + runs -> + expect(wrapperNode.getScrollRight()).toBe wrapperNode.getScrollWidth() + editor.setCursorScreenPosition([6, 62], autoscroll: false) - editor.moveLeft() - nextAnimationFrame() - left = wrapperNode.pixelPositionForScreenPosition([6, 60]).left - margin - expect(wrapperNode.getScrollLeft()).toBeCloseTo left, 0 + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + editor.moveLeft() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + margin = null + runs -> + margin = component.presenter.getHorizontalScrollMarginInPixels() + left = wrapperNode.pixelPositionForScreenPosition([6, 61]).left - margin + expect(wrapperNode.getScrollLeft()).toBeCloseTo left, 0 + editor.moveLeft() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + left = wrapperNode.pixelPositionForScreenPosition([6, 60]).left - margin + expect(wrapperNode.getScrollLeft()).toBeCloseTo left, 0 it "scrolls down when inserting lines makes the document longer than the editor's height", -> editor.setCursorScreenPosition([13, Infinity]) editor.insertNewline() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(wrapperNode.getScrollBottom()).toBe 14 * 10 - editor.insertNewline() - nextAnimationFrame() - expect(wrapperNode.getScrollBottom()).toBe 15 * 10 + runs -> + expect(wrapperNode.getScrollBottom()).toBe 14 * 10 + editor.insertNewline() + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(wrapperNode.getScrollBottom()).toBe 15 * 10 it "autoscrolls to the cursor when it moves due to undo", -> editor.insertText('abc') wrapperNode.setScrollTop(Infinity) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - editor.undo() - nextAnimationFrame() + runs -> + editor.undo() - expect(wrapperNode.getScrollTop()).toBe 0 + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 it "doesn't scroll when the cursor moves into the visible area", -> editor.setCursorBufferPosition([0, 0]) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - wrapperNode.setScrollTop(40) - nextAnimationFrame() + runs -> + wrapperNode.setScrollTop(40) - editor.setCursorBufferPosition([6, 0]) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 40 + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + editor.setCursorBufferPosition([6, 0]) + + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(wrapperNode.getScrollTop()).toBe 40 it "honors the autoscroll option on cursor and selection manipulation methods", -> expect(wrapperNode.getScrollTop()).toBe 0 editor.addCursorAtScreenPosition([11, 11], autoscroll: false) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - editor.addCursorAtBufferPosition([11, 11], autoscroll: false) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - editor.setCursorScreenPosition([11, 11], autoscroll: false) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - editor.setCursorBufferPosition([11, 11], autoscroll: false) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - editor.addSelectionForBufferRange([[11, 11], [11, 11]], autoscroll: false) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - editor.addSelectionForScreenRange([[11, 11], [11, 12]], autoscroll: false) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - editor.setSelectedBufferRange([[11, 0], [11, 1]], autoscroll: false) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - editor.setSelectedScreenRange([[11, 0], [11, 6]], autoscroll: false) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - editor.clearSelections(autoscroll: false) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - - editor.addSelectionForScreenRange([[0, 0], [0, 4]]) - nextAnimationFrame() - - editor.getCursors()[0].setScreenPosition([11, 11], autoscroll: true) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 - editor.getCursors()[0].setBufferPosition([0, 0], autoscroll: true) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 - editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], autoscroll: true) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 - editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], autoscroll: true) - nextAnimationFrame() - expect(wrapperNode.getScrollTop()).toBe 0 + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.addCursorAtBufferPosition([11, 11], autoscroll: false) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.setCursorScreenPosition([11, 11], autoscroll: false) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.setCursorBufferPosition([11, 11], autoscroll: false) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.addSelectionForBufferRange([[11, 11], [11, 11]], autoscroll: false) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.addSelectionForScreenRange([[11, 11], [11, 12]], autoscroll: false) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.setSelectedBufferRange([[11, 0], [11, 1]], autoscroll: false) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.setSelectedScreenRange([[11, 0], [11, 6]], autoscroll: false) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.clearSelections(autoscroll: false) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.addSelectionForScreenRange([[0, 0], [0, 4]]) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + editor.getCursors()[0].setScreenPosition([11, 11], autoscroll: true) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 + editor.getCursors()[0].setBufferPosition([0, 0], autoscroll: true) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 + editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], autoscroll: true) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 + editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], autoscroll: true) + waitsForPromise -> atom.views.getNextUpdatePromise() + runs -> + expect(wrapperNode.getScrollTop()).toBe 0 describe "::getVisibleRowRange()", -> beforeEach -> wrapperNode.style.height = lineHeightInPixels * 8 + "px" component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() it "returns the first and the last visible rows", -> component.setScrollTop(0) - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - expect(component.getVisibleRowRange()).toEqual [0, 9] + runs -> + expect(component.getVisibleRowRange()).toEqual [0, 9] it "ends at last buffer row even if there's more space available", -> wrapperNode.style.height = lineHeightInPixels * 13 + "px" component.measureDimensions() - nextAnimationFrame() + waitsForPromise -> atom.views.getNextUpdatePromise() - component.setScrollTop(60) - nextAnimationFrame() + runs -> + component.setScrollTop(60) - expect(component.getVisibleRowRange()).toEqual [0, 13] + waitsForPromise -> atom.views.getNextUpdatePromise() + + runs -> + expect(component.getVisibleRowRange()).toEqual [0, 13] describe "middle mouse paste on Linux", -> originalPlatform = null @@ -3621,7 +4217,6 @@ fdescribe "TextEditorComponent", -> Object.defineProperty process, 'platform', value: originalPlatform it "pastes the previously selected text at the clicked location", -> - jasmine.unspy(window, 'setTimeout') clipboardWrittenTo = false spyOn(require('ipc'), 'send').andCallFake (eventName, selectedText) -> if eventName is 'write-text-to-selection-clipboard' @@ -3683,3 +4278,6 @@ fdescribe "TextEditorComponent", -> flatten(toArray(node.children).map(getLeafNodes)) else [node] + + waitsForAnimationFrame = -> + waitsFor 'next animation frame', (done) -> requestAnimationFrame(done) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index d72c50382..99938ef5f 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -220,7 +220,7 @@ class TextEditorComponent @updatesPaused = false if @updateRequestedWhilePaused and @canUpdate() @updateRequestedWhilePaused = false - @updateSync() + @requestUpdate() getTopmostDOMNode: -> @hostElement From 0ac42a12f3e0d39e52d6ff261256f7493ab93ff7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 25 Oct 2015 09:59:47 -0600 Subject: [PATCH 05/53] Add waitsForNextDOMUpdate helper function --- spec/text-editor-component-spec.coffee | 594 +++++++++++++------------ 1 file changed, 298 insertions(+), 296 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 474646bd5..ec1155baf 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -41,8 +41,7 @@ describe "TextEditorComponent", -> horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') component.measureDimensions() - - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() afterEach -> contentNode.style.width = '' @@ -56,13 +55,13 @@ describe "TextEditorComponent", -> component.presenter.startRow = -1 component.presenter.endRow = 9999 - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "doesn't update when an animation frame was requested but the component got destroyed before its delivery", -> editor.setText("You shouldn't see this update.") component.destroy() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(0).textContent).not.toBe("You shouldn't see this update.") @@ -83,7 +82,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) @@ -91,7 +90,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) @@ -99,7 +98,7 @@ describe "TextEditorComponent", -> it "renders higher tiles in front of lower ones", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLines() @@ -111,7 +110,7 @@ describe "TextEditorComponent", -> verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLines() @@ -124,7 +123,7 @@ describe "TextEditorComponent", -> it "renders the currently-visible lines in a tiled fashion", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLines() @@ -154,7 +153,7 @@ describe "TextEditorComponent", -> verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5 verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLines() @@ -184,7 +183,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() editor.getBuffer().deleteRows(0, 1) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLines() @@ -201,7 +200,7 @@ describe "TextEditorComponent", -> editor.getBuffer().insert([0, 0], '\n\n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLines() @@ -224,27 +223,27 @@ describe "TextEditorComponent", -> it "updates the lines when lines are inserted or removed above the rendered row range", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() buffer = null runs -> buffer = editor.getBuffer() buffer.insert([0, 0], '\n\n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(3).textContent).toBe editor.tokenizedLineForScreenRow(3).text buffer.delete([[0, 0], [3, 0]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(3).textContent).toBe editor.tokenizedLineForScreenRow(3).text @@ -252,7 +251,7 @@ describe "TextEditorComponent", -> it "updates the top position of lines when the line height changes", -> initialLineHeightInPixels = editor.getLineHeightInPixels() component.setLineHeight(2) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> newLineHeightInPixels = editor.getLineHeightInPixels() @@ -262,7 +261,7 @@ describe "TextEditorComponent", -> it "updates the top position of lines when the font size changes", -> initialLineHeightInPixels = editor.getLineHeightInPixels() component.setFontSize(10) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> newLineHeightInPixels = editor.getLineHeightInPixels() @@ -273,7 +272,7 @@ describe "TextEditorComponent", -> editor.setText('') wrapperNode.style.height = '300px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> linesNode = componentNode.querySelector('.lines') @@ -286,7 +285,7 @@ describe "TextEditorComponent", -> componentNode.style.width = gutterWidth + (30 * charWidth) + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollWidth()).toBeGreaterThan scrollViewNode.offsetWidth @@ -303,7 +302,7 @@ describe "TextEditorComponent", -> componentNode.style.width = gutterWidth + wrapperNode.getScrollWidth() + 100 + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> scrollViewWidth = scrollViewNode.offsetWidth @@ -325,7 +324,7 @@ describe "TextEditorComponent", -> wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)' - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(linesNode.style.backgroundColor).toBe 'rgb(255, 0, 0)' @@ -334,7 +333,7 @@ describe "TextEditorComponent", -> it "applies .leading-whitespace for lines with leading spaces and/or tabs", -> editor.setText(' a') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) @@ -343,7 +342,7 @@ describe "TextEditorComponent", -> editor.setText('\ta') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) @@ -352,7 +351,7 @@ describe "TextEditorComponent", -> it "applies .trailing-whitespace for lines with trailing spaces and/or tabs", -> editor.setText(' ') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) @@ -361,7 +360,7 @@ describe "TextEditorComponent", -> editor.setText('\t') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) @@ -370,7 +369,7 @@ describe "TextEditorComponent", -> editor.setText('a ') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) @@ -378,7 +377,7 @@ describe "TextEditorComponent", -> expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false editor.setText('a\t') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) @@ -410,32 +409,32 @@ describe "TextEditorComponent", -> atom.config.set("editor.showInvisibles", true) atom.config.set("editor.invisibles", invisibles) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "re-renders the lines when the showInvisibles config option changes", -> editor.setText " a line with tabs\tand spaces \n" - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> 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) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces " atom.config.set("editor.showInvisibles", true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}" it "displays leading/trailing spaces, tabs, and newlines as visible characters", -> editor.setText " a line with tabs\tand spaces \n" - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}" @@ -446,14 +445,14 @@ describe "TextEditorComponent", -> it "displays newlines as their own token outside of the other tokens' scopeDescriptor", -> editor.setText "var\n" - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> 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" - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that ends with a carriage return#{invisibles.cr}#{invisibles.eol}" @@ -463,40 +462,40 @@ describe "TextEditorComponent", -> it "renders an nbsp on empty lines when the line-ending character is an empty string", -> atom.config.set("editor.invisibles", eol: '') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp it "renders an nbsp on empty lines when the line-ending character is false", -> atom.config.set("editor.invisibles", eol: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp it "interleaves invisible line-ending characters with indent guides on empty lines", -> atom.config.set "editor.showIndentGuide", true - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> editor.setTextInBufferRange([[10, 0], [11, 0]], "\r\n", normalizeLineEndings: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' editor.setTabLength(3) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE ' editor.setTabLength(1) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' @@ -504,7 +503,7 @@ describe "TextEditorComponent", -> editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ') editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' @@ -513,11 +512,11 @@ describe "TextEditorComponent", -> beforeEach -> editor.setText "a line that wraps \n" editor.setSoftWrapped(true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "doesn't show end of line invisibles at the end of wrapped lines", -> expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that " @@ -526,7 +525,7 @@ describe "TextEditorComponent", -> describe "when indent guides are enabled", -> beforeEach -> atom.config.set "editor.showIndentGuide", true - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "adds an 'indent-guide' class to spans comprising the leading whitespace", -> line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) @@ -543,7 +542,7 @@ describe "TextEditorComponent", -> it "renders leading whitespace spans with the 'indent-guide' class for empty lines", -> editor.getBuffer().insert([1, Infinity], '\n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) @@ -556,7 +555,7 @@ describe "TextEditorComponent", -> it "renders indent guides correctly on lines containing only whitespace", -> editor.getBuffer().insert([1, Infinity], '\n ') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) @@ -573,7 +572,7 @@ describe "TextEditorComponent", -> atom.config.set 'editor.invisibles', space: '-', eol: 'x' editor.getBuffer().insert([1, Infinity], '\n ') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) @@ -588,7 +587,7 @@ describe "TextEditorComponent", -> it "does not render indent guides in trailing whitespace for lines containing non whitespace characters", -> editor.getBuffer().setText " hi " - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) @@ -599,12 +598,12 @@ describe "TextEditorComponent", -> it "updates the indent guides on empty lines preceding an indentation change", -> editor.getBuffer().insert([12, 0], '\n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> editor.getBuffer().insert([13, 0], ' ') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> line12LeafNodes = getLeafNodes(component.lineNodeForScreenRow(12)) @@ -616,11 +615,11 @@ describe "TextEditorComponent", -> it "updates the indent guides on empty lines following an indentation change", -> editor.getBuffer().insert([12, 2], '\n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> editor.getBuffer().insert([12, 0], ' ') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> line13LeafNodes = getLeafNodes(component.lineNodeForScreenRow(13)) @@ -636,7 +635,7 @@ describe "TextEditorComponent", -> it "does not render indent guides on lines containing only whitespace", -> editor.getBuffer().insert([1, Infinity], '\n ') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) @@ -652,7 +651,7 @@ describe "TextEditorComponent", -> it "excludes the null byte from character measurement", -> editor.setText("a\0b") - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.pixelPositionForScreenPosition([0, Infinity]).left).toEqual 2 * charWidth @@ -663,7 +662,7 @@ describe "TextEditorComponent", -> expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() editor.foldBufferRow(4) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> foldedLineNode = component.lineNodeForScreenRow(4) @@ -671,7 +670,7 @@ describe "TextEditorComponent", -> editor.unfoldBufferRow(4) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> foldedLineNode = component.lineNodeForScreenRow(4) @@ -687,7 +686,7 @@ describe "TextEditorComponent", -> it "renders higher tiles in front of lower ones", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLineNumbers() @@ -699,7 +698,7 @@ describe "TextEditorComponent", -> verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLineNumbers() @@ -714,7 +713,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) @@ -722,7 +721,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) @@ -730,7 +729,7 @@ describe "TextEditorComponent", -> it "renders the currently-visible line numbers in a tiled fashion", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLineNumbers() @@ -758,7 +757,7 @@ describe "TextEditorComponent", -> verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5 verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLineNumbers() @@ -786,7 +785,7 @@ describe "TextEditorComponent", -> it "updates the translation of subsequent line numbers when lines are inserted or removed", -> editor.getBuffer().insert([0, 0], '\n\n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> lineNumberNodes = componentNode.querySelectorAll('.line-number') @@ -799,7 +798,7 @@ describe "TextEditorComponent", -> editor.getBuffer().insert([0, 0], '\n\n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels @@ -818,7 +817,7 @@ describe "TextEditorComponent", -> wrapperNode.style.width = 30 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelectorAll('.line-number').length).toBe 9 + 1 # 3 line-numbers tiles + 1 dummy line @@ -835,7 +834,7 @@ describe "TextEditorComponent", -> it "pads line numbers to be right-justified based on the maximum number of line number digits", -> editor.getBuffer().setText([1..10].join('\n')) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() [gutterNode, initialGutterWidth] = [] @@ -850,7 +849,7 @@ describe "TextEditorComponent", -> # Removes padding when the max number of digits goes down editor.getBuffer().delete([[1, 0], [2, 0]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> for screenRow in [0..8] @@ -860,7 +859,7 @@ describe "TextEditorComponent", -> # Increases padding when the max number of digits goes up editor.getBuffer().insert([0, 0], '\n\n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> for screenRow in [0..8] @@ -872,7 +871,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.line-numbers').offsetHeight).toBe componentNode.offsetHeight @@ -889,7 +888,7 @@ describe "TextEditorComponent", -> gutterNode.style.backgroundColor = 'rgb(255, 0, 0)' atom.views.performDocumentPoll() # required due to DOM change not being detected inside shadow DOM - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumbersNode.style.backgroundColor).toBe 'rgb(255, 0, 0)' @@ -901,28 +900,28 @@ describe "TextEditorComponent", -> editor.setLineNumberGutterVisible(false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.gutter').style.display).toBe 'none' atom.config.set("editor.showLineNumbers", false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.gutter').style.display).toBe 'none' editor.setLineNumberGutterVisible(true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.gutter').style.display).toBe 'none' atom.config.set("editor.showLineNumbers", true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.gutter').style.display).toBe '' @@ -951,7 +950,7 @@ describe "TextEditorComponent", -> it "updates the foldable class on the correct line numbers when the foldable positions change", -> editor.getBuffer().insert([0, 0], '\n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(0, 'foldable')).toBe false @@ -967,26 +966,26 @@ describe "TextEditorComponent", -> editor.getBuffer().insert([11, 44], '\n fold me') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(11, 'foldable')).toBe true editor.undo() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(11, 'foldable')).toBe false it "adds, updates and removes the folded class on the correct line number componentNodes", -> editor.foldBufferRow(4) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(4, 'folded')).toBe true editor.getBuffer().insert([0, 0], '\n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(4, 'folded')).toBe false @@ -994,7 +993,7 @@ describe "TextEditorComponent", -> editor.unfoldBufferRow(5) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(5, 'folded')).toBe false @@ -1002,13 +1001,13 @@ describe "TextEditorComponent", -> describe "when soft wrapping is enabled", -> beforeEach -> editor.setSoftWrapped(true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "doesn't add the foldable class for soft-wrapped lines", -> expect(lineNumberHasClass(0, 'foldable')).toBe true @@ -1037,7 +1036,7 @@ describe "TextEditorComponent", -> lineNumber = component.lineNumberNodeForScreenRow(1) target = lineNumber.querySelector('.icon-right') target.dispatchEvent(buildClickEvent(target)) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(1, 'folded')).toBe true @@ -1046,7 +1045,7 @@ describe "TextEditorComponent", -> target = lineNumber.querySelector('.icon-right') target.dispatchEvent(buildClickEvent(target)) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(1, 'folded')).toBe false @@ -1069,7 +1068,7 @@ describe "TextEditorComponent", -> wrapperNode.style.width = 20 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursorNodes = componentNode.querySelectorAll('.cursor') @@ -1081,7 +1080,7 @@ describe "TextEditorComponent", -> cursor2 = editor.addCursorAtScreenPosition([8, 11], autoscroll: false) cursor3 = editor.addCursorAtScreenPosition([4, 10], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursorNodes = componentNode.querySelectorAll('.cursor') @@ -1092,12 +1091,12 @@ describe "TextEditorComponent", -> verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> horizontalScrollbarNode.scrollLeft = 3.5 * charWidth - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() cursorMovedListener = null runs -> @@ -1109,7 +1108,7 @@ describe "TextEditorComponent", -> editor.onDidChangeCursorPosition cursorMovedListener = jasmine.createSpy('cursorMovedListener') cursor3.setScreenPosition([4, 11], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" @@ -1117,7 +1116,7 @@ describe "TextEditorComponent", -> cursor3.destroy() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursorNodes = componentNode.querySelectorAll('.cursor') @@ -1129,7 +1128,7 @@ describe "TextEditorComponent", -> atom.config.set('editor.fontFamily', 'sans-serif') editor.setCursorScreenPosition([0, 16]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursor = componentNode.querySelector('.cursor') @@ -1149,7 +1148,7 @@ describe "TextEditorComponent", -> editor.setText('he\u0301y') # e with an accent mark editor.setCursorBufferPosition([0, 3]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursor = componentNode.querySelector('.cursor') @@ -1169,7 +1168,7 @@ describe "TextEditorComponent", -> atom.config.set('editor.fontFamily', 'sans-serif') editor.setCursorScreenPosition([0, 16]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> atom.styles.addStyleSheet """ @@ -1178,7 +1177,7 @@ describe "TextEditorComponent", -> } """, context: 'atom-text-editor' - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursor = componentNode.querySelector('.cursor') @@ -1198,7 +1197,7 @@ describe "TextEditorComponent", -> it "sets the cursor to the default character width at the end of a line", -> editor.setCursorScreenPosition([0, Infinity]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursorNode = componentNode.querySelector('.cursor') @@ -1207,7 +1206,7 @@ describe "TextEditorComponent", -> it "gives the cursor a non-zero width even if it's inside atomic tokens", -> editor.setCursorScreenPosition([1, 0]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursorNode = componentNode.querySelector('.cursor') @@ -1217,7 +1216,7 @@ describe "TextEditorComponent", -> cursorsNode = componentNode.querySelector('.cursors') wrapperNode.focus() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(cursorsNode.classList.contains('blink-off')).toBe false waitsFor -> cursorsNode.classList.contains('blink-off') @@ -1227,7 +1226,7 @@ describe "TextEditorComponent", -> # Stop blinking after moving the cursor editor.moveRight() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(cursorsNode.classList.contains('blink-off')).toBe false @@ -1238,7 +1237,7 @@ describe "TextEditorComponent", -> editor.setSelectedScreenRange([[0, 4], [4, 6]]) editor.addCursorAtScreenPosition([6, 8]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursorNodes = componentNode.querySelectorAll('.cursor') @@ -1248,7 +1247,7 @@ describe "TextEditorComponent", -> it "updates cursor positions when the line height changes", -> editor.setCursorBufferPosition([1, 10]) component.setLineHeight(2) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursorNode = componentNode.querySelector('.cursor') @@ -1257,7 +1256,7 @@ describe "TextEditorComponent", -> it "updates cursor positions when the font size changes", -> editor.setCursorBufferPosition([1, 10]) component.setFontSize(10) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursorNode = componentNode.querySelector('.cursor') @@ -1266,7 +1265,7 @@ describe "TextEditorComponent", -> it "updates cursor positions when the font family changes", -> editor.setCursorBufferPosition([1, 10]) component.setFontFamily('sans-serif') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursorNode = componentNode.querySelector('.cursor') @@ -1284,7 +1283,7 @@ describe "TextEditorComponent", -> it "renders 1 region for 1-line selections", -> # 1-line selection editor.setSelectedScreenRange([[1, 6], [1, 10]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> regions = componentNode.querySelectorAll('.selection .region') @@ -1298,7 +1297,7 @@ describe "TextEditorComponent", -> it "renders 2 regions for 2-line selections", -> editor.setSelectedScreenRange([[1, 6], [2, 10]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> tileNode = component.tileNodesForLines()[0] @@ -1319,7 +1318,7 @@ describe "TextEditorComponent", -> it "renders 3 regions per tile for selections with more than 2 lines", -> editor.setSelectedScreenRange([[0, 6], [5, 10]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> # Tile 0 @@ -1370,7 +1369,7 @@ describe "TextEditorComponent", -> it "does not render empty selections", -> editor.addSelectionForBufferRange([[2, 2], [2, 2]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.getSelections()[0].isEmpty()).toBe true @@ -1382,7 +1381,7 @@ describe "TextEditorComponent", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setLineHeight(2) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> selectionNode = componentNode.querySelector('.region') @@ -1392,7 +1391,7 @@ describe "TextEditorComponent", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontSize(10) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> selectionNode = componentNode.querySelector('.region') @@ -1403,7 +1402,7 @@ describe "TextEditorComponent", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontFamily('sans-serif') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> selectionNode = componentNode.querySelector('.region') @@ -1413,7 +1412,7 @@ describe "TextEditorComponent", -> it "will flash the selection when flash:true is passed to editor::setSelectedBufferRange", -> editor.setSelectedBufferRange([[1, 6], [1, 10]], flash: true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() selectionNode = null runs -> @@ -1425,7 +1424,7 @@ describe "TextEditorComponent", -> runs -> editor.setSelectedBufferRange([[1, 5], [1, 7]], flash: true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(selectionNode.classList.contains('flash')).toBe true @@ -1437,7 +1436,7 @@ describe "TextEditorComponent", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], invalidate: 'inside', maintainHistory: true) decorationParams = {type: ['line-number', 'line'], class: 'a'} decoration = editor.decorateMarker(marker, decorationParams) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "applies line decoration classes to lines and line numbers", -> expect(lineAndLineNumberHaveClass(2, 'a')).toBe true @@ -1447,21 +1446,21 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> # Add decorations that are out of range marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]]) editor.decorateMarker(marker2, type: ['line-number', 'line'], class: 'b') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> # Scroll decorations into view verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(9, 'b')).toBe true @@ -1469,7 +1468,7 @@ describe "TextEditorComponent", -> # Fold a line to move the decorations editor.foldBufferRow(5) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(9, 'b')).toBe false @@ -1481,14 +1480,14 @@ describe "TextEditorComponent", -> componentNode.style.width = 16 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> marker.destroy() marker = editor.markBufferRange([[0, 0], [0, 2]]) editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'b') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(0, 'b')).toBe true @@ -1496,7 +1495,7 @@ describe "TextEditorComponent", -> marker.setBufferRange([[0, 0], [0, Infinity]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(0, 'b')).toBe true @@ -1510,7 +1509,7 @@ describe "TextEditorComponent", -> editor.getBuffer().insert([0, 0], '\n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(2, 'a')).toBe false @@ -1520,7 +1519,7 @@ describe "TextEditorComponent", -> marker.setBufferRange([[4, 4], [6, 4]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(2, 'a')).toBe false @@ -1533,7 +1532,7 @@ describe "TextEditorComponent", -> it "remove decoration classes when decorations are removed", -> decoration.destroy() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(1, 'a')).toBe false @@ -1544,7 +1543,7 @@ describe "TextEditorComponent", -> it "removes decorations when their marker is invalidated", -> editor.getBuffer().insert([3, 2], 'n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(marker.isValid()).toBe false @@ -1555,7 +1554,7 @@ describe "TextEditorComponent", -> editor.undo() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(marker.isValid()).toBe true @@ -1566,7 +1565,7 @@ describe "TextEditorComponent", -> it "removes decorations when their marker is destroyed", -> marker.destroy() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(1, 'a')).toBe false @@ -1577,7 +1576,7 @@ describe "TextEditorComponent", -> describe "when the decoration's 'onlyHead' property is true", -> it "only applies the decoration's class to lines containing the marker's head", -> editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-head', onlyHead: true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(1, 'only-head')).toBe false @@ -1589,7 +1588,7 @@ describe "TextEditorComponent", -> it "only applies the decoration when its marker is empty", -> editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-empty', onlyEmpty: true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe false @@ -1597,7 +1596,7 @@ describe "TextEditorComponent", -> marker.clearTail() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe false @@ -1607,7 +1606,7 @@ describe "TextEditorComponent", -> it "only applies the decoration when its marker is non-empty", -> editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-non-empty', onlyNonEmpty: true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe true @@ -1615,7 +1614,7 @@ describe "TextEditorComponent", -> marker.clearTail() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe false @@ -1628,19 +1627,19 @@ describe "TextEditorComponent", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], invalidate: 'inside', maintainHistory: true) decorationParams = {type: 'highlight', class: 'test-highlight'} decoration = editor.decorateMarker(marker, decorationParams) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "does not render highlights for off-screen lines until they come on-screen", -> wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], invalidate: 'inside') editor.decorateMarker(marker, type: 'highlight', class: 'some-highlight') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> # Should not be rendering range containing the marker @@ -1654,7 +1653,7 @@ describe "TextEditorComponent", -> verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.presenter.endRow).toBeGreaterThan(8) @@ -1674,7 +1673,7 @@ describe "TextEditorComponent", -> it "removes highlights when a decoration is removed", -> decoration.destroy() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> regions = componentNode.querySelectorAll('.test-highlight .region') @@ -1682,14 +1681,14 @@ describe "TextEditorComponent", -> it "does not render a highlight that is within a fold", -> editor.foldBufferRow(1) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelectorAll('.test-highlight').length).toBe 0 it "removes highlights when a decoration's marker is destroyed", -> marker.destroy() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> regions = componentNode.querySelectorAll('.test-highlight .region') @@ -1697,7 +1696,7 @@ describe "TextEditorComponent", -> it "only renders highlights when a decoration's marker is valid", -> editor.getBuffer().insert([3, 2], 'n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(marker.isValid()).toBe false @@ -1706,7 +1705,7 @@ describe "TextEditorComponent", -> editor.getBuffer().undo() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(marker.isValid()).toBe true @@ -1716,20 +1715,20 @@ describe "TextEditorComponent", -> it "allows multiple space-delimited decoration classes", -> decoration.setProperties(type: 'highlight', class: 'foo bar') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelectorAll('.foo.bar').length).toBe 2 decoration.setProperties(type: 'highlight', class: 'bar baz') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelectorAll('.bar.baz').length).toBe 2 it "renders classes on the regions directly if 'deprecatedRegionClass' option is defined", -> decoration = editor.decorateMarker(marker, type: 'highlight', class: 'test-highlight', deprecatedRegionClass: 'test-highlight-region') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> regions = componentNode.querySelectorAll('.test-highlight .region.test-highlight-region') @@ -1744,7 +1743,7 @@ describe "TextEditorComponent", -> expect(highlightNode.classList.contains('flash-class')).toBe false decoration.flash('flash-class', 10) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(highlightNode.classList.contains('flash-class')).toBe true @@ -1754,11 +1753,11 @@ describe "TextEditorComponent", -> describe "when ::flash is called again before the first has finished", -> it "removes the class from the decoration highlight before adding it for the second ::flash call", -> decoration.flash('flash-class', 30) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(highlightNode.classList.contains('flash-class')).toBe true waits 2 runs -> decoration.flash('flash-class', 10) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(highlightNode.classList.contains('flash-class')).toBe false waitsFor -> highlightNode.classList.contains('flash-class') @@ -1771,7 +1770,7 @@ describe "TextEditorComponent", -> editor.getBuffer().insert([0, 0], '\n') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> regionStyle = componentNode.querySelector('.test-highlight .region').style @@ -1785,7 +1784,7 @@ describe "TextEditorComponent", -> marker.setBufferRange([[5, 8], [5, 13]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> regionStyle = componentNode.querySelector('.test-highlight .region').style @@ -1797,7 +1796,7 @@ describe "TextEditorComponent", -> decoration.setProperties(type: 'highlight', class: 'new-test-highlight') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.test-highlight')).toBeFalsy() @@ -1815,7 +1814,7 @@ describe "TextEditorComponent", -> it "renders an overlay decoration when added and removes the overlay when the decoration is destroyed", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') @@ -1823,7 +1822,7 @@ describe "TextEditorComponent", -> decoration.destroy() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') @@ -1833,7 +1832,7 @@ describe "TextEditorComponent", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', class: 'my-overlay', item}) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> overlay = component.getTopmostDOMNode().querySelector('atom-overlay.my-overlay') @@ -1847,7 +1846,7 @@ describe "TextEditorComponent", -> marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> position = wrapperNode.pixelPositionForBufferPosition([2, 10]) @@ -1877,7 +1876,7 @@ describe "TextEditorComponent", -> component.measureDimensions() component.measureWindowSize() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() afterEach -> atom.restoreWindowDimensions() @@ -1888,7 +1887,7 @@ describe "TextEditorComponent", -> marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> position = wrapperNode.pixelPositionForBufferPosition([0, 26]) @@ -1899,7 +1898,7 @@ describe "TextEditorComponent", -> editor.insertText('a') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(overlay.style.left).toBe windowWidth - itemWidth + 'px' @@ -1907,7 +1906,7 @@ describe "TextEditorComponent", -> editor.insertText('b') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(overlay.style.left).toBe windowWidth - itemWidth + 'px' @@ -1923,14 +1922,14 @@ describe "TextEditorComponent", -> wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.getCursorScreenPosition()).toEqual [0, 0] wrapperNode.setScrollTop(3 * lineHeightInPixels) wrapperNode.setScrollLeft(3 * charWidth) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(inputNode.offsetTop).toBe 0 @@ -1939,7 +1938,7 @@ describe "TextEditorComponent", -> # In bounds, not focused editor.setCursorBufferPosition([5, 4], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(inputNode.offsetTop).toBe 0 @@ -1948,7 +1947,7 @@ describe "TextEditorComponent", -> # In bounds and focused wrapperNode.focus() # updates via state change - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(inputNode.offsetTop).toBe (5 * lineHeightInPixels) - wrapperNode.getScrollTop() @@ -1957,7 +1956,7 @@ describe "TextEditorComponent", -> # In bounds, not focused inputNode.blur() # updates via state change - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(inputNode.offsetTop).toBe 0 @@ -1966,7 +1965,7 @@ describe "TextEditorComponent", -> # Out of bounds, not focused editor.setCursorBufferPosition([1, 2], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(inputNode.offsetTop).toBe 0 @@ -1975,7 +1974,7 @@ describe "TextEditorComponent", -> # Out of bounds, focused inputNode.focus() # updates via state change - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(inputNode.offsetTop).toBe 0 @@ -1995,14 +1994,14 @@ describe "TextEditorComponent", -> wrapperNode.style.height = height + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> coordinates = clientCoordinatesForScreenPosition([0, 2]) coordinates.clientY = -1 linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.getCursorScreenPosition()).toEqual [0, 0] @@ -2015,13 +2014,13 @@ describe "TextEditorComponent", -> wrapperNode.style.height = height + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> coordinates = clientCoordinatesForScreenPosition([0, 2]) coordinates.clientY = height * 2 linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.getCursorScreenPosition()).toEqual [0, 3] @@ -2034,12 +2033,12 @@ describe "TextEditorComponent", -> component.measureDimensions() wrapperNode.setScrollTop(3.5 * lineHeightInPixels) wrapperNode.setScrollLeft(2 * charWidth) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]))) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.getCursorScreenPosition()).toEqual [4, 8] @@ -2048,7 +2047,7 @@ describe "TextEditorComponent", -> it "selects to the nearest screen position", -> editor.setCursorScreenPosition([3, 4]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), shiftKey: true)) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [5, 6]] @@ -2058,7 +2057,7 @@ describe "TextEditorComponent", -> it "adds a cursor at the nearest screen position", -> editor.setCursorScreenPosition([3, 4]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), metaKey: true)) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.getSelectedScreenRanges()).toEqual [[[3, 4], [3, 4]], [[5, 6], [5, 6]]] @@ -2069,7 +2068,7 @@ describe "TextEditorComponent", -> editor.addCursorAtScreenPosition([5, 2]) editor.addCursorAtScreenPosition([7, 5]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), metaKey: true)) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.getSelectedScreenRanges()).toEqual [[[5, 2], [5, 2]], [[7, 5], [7, 5]]] @@ -2078,7 +2077,7 @@ describe "TextEditorComponent", -> it "neither adds a new cursor nor removes the current cursor", -> editor.setCursorScreenPosition([3, 4]) linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), metaKey: true)) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.getSelectedScreenRanges()).toEqual [[[3, 4], [3, 4]]] @@ -2170,7 +2169,7 @@ describe "TextEditorComponent", -> wrapperNode.style.width = '100px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe(0) @@ -2316,7 +2315,7 @@ describe "TextEditorComponent", -> jasmine.attachToDOM(wrapperNode) wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 1)) @@ -2349,7 +2348,7 @@ describe "TextEditorComponent", -> jasmine.attachToDOM(wrapperNode) wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 1)) @@ -2382,7 +2381,7 @@ describe "TextEditorComponent", -> describe "when a line is folded", -> beforeEach -> editor.foldBufferRow 4 - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() describe "when the folded line's fold-marker is clicked", -> it "unfolds the buffer row", -> @@ -2478,7 +2477,7 @@ describe "TextEditorComponent", -> it "autoscrolls when the cursor approaches the top or bottom of the editor", -> wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 @@ -2640,11 +2639,11 @@ describe "TextEditorComponent", -> beforeEach -> gutterNode = componentNode.querySelector('.gutter') editor.setSoftWrapped(true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> componentNode.style.width = 21 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() describe "when the gutter is clicked", -> it "selects the clicked buffer row", -> @@ -2790,12 +2789,12 @@ describe "TextEditorComponent", -> it "adds the 'is-focused' class to the editor when the hidden input is focused", -> expect(document.activeElement).toBe document.body inputNode.focus() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.classList.contains('is-focused')).toBe true expect(wrapperNode.classList.contains('is-focused')).toBe true inputNode.blur() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.classList.contains('is-focused')).toBe false expect(wrapperNode.classList.contains('is-focused')).toBe false @@ -2806,19 +2805,19 @@ describe "TextEditorComponent", -> beforeEach -> console.log editor.getText() editor.setCursorScreenPosition([0, 0]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "adds the 'has-selection' class to the editor when there is a selection", -> expect(componentNode.classList.contains('has-selection')).toBe false editor.selectDown() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.classList.contains('has-selection')).toBe true editor.moveDown() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.classList.contains('has-selection')).toBe false @@ -2828,13 +2827,13 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.scrollTop).toBe 0 wrapperNode.setScrollTop(10) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.scrollTop).toBe 10 @@ -2842,7 +2841,7 @@ describe "TextEditorComponent", -> it "updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model", -> componentNode.style.width = 30 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() tilesNodes = null runs -> @@ -2857,7 +2856,7 @@ describe "TextEditorComponent", -> wrapperNode.setScrollLeft(100) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> top = 0 @@ -2870,14 +2869,14 @@ describe "TextEditorComponent", -> it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> componentNode.style.width = 30 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollLeft()).toBe 0 horizontalScrollbarNode.scrollLeft = 100 horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollLeft()).toBe 100 @@ -2888,7 +2887,7 @@ describe "TextEditorComponent", -> component.measureDimensions() wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() lastLineNode = null runs -> @@ -2901,7 +2900,7 @@ describe "TextEditorComponent", -> wrapperNode.style.width = 100 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom @@ -2914,7 +2913,7 @@ describe "TextEditorComponent", -> component.measureDimensions() wrapperNode.setScrollLeft(Infinity) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> rightOfLongestLine = component.lineNodeForScreenRow(6).querySelector('.line > span:last-child').getBoundingClientRect().right @@ -2929,7 +2928,7 @@ describe "TextEditorComponent", -> wrapperNode.style.width = '1000px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.style.display).toBe '' @@ -2938,7 +2937,7 @@ describe "TextEditorComponent", -> componentNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.style.display).toBe '' @@ -2947,7 +2946,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 20 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.style.display).toBe 'none' @@ -2957,7 +2956,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> @@ -2990,7 +2989,7 @@ describe "TextEditorComponent", -> wrapperNode.style.width = '1000px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.style.bottom).toBe '0px' @@ -3000,7 +2999,7 @@ describe "TextEditorComponent", -> componentNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' @@ -3010,7 +3009,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 20 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' @@ -3021,7 +3020,7 @@ describe "TextEditorComponent", -> gutterNode = componentNode.querySelector('.gutter') componentNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(horizontalScrollbarNode.scrollWidth).toBe wrapperNode.getScrollWidth() @@ -3036,7 +3035,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "updates the scrollLeft or scrollTop on mousewheel events depending on which delta is greater (x or y)", -> expect(verticalScrollbarNode.scrollTop).toBe 0 @@ -3095,7 +3094,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() lineNode = null runs -> @@ -3113,7 +3112,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() lineNode = null runs -> @@ -3161,7 +3160,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() lineNumberNode = null runs -> @@ -3181,7 +3180,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> # try to scroll past the top, which is impossible @@ -3238,24 +3237,25 @@ describe "TextEditorComponent", -> it "inserts the newest character in the input's value into the buffer", -> componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.lineTextForBufferRow(0)).toBe 'xvar quicksort = function () {' componentNode.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode)) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.lineTextForBufferRow(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", -> componentNode.dispatchEvent(buildTextInputEvent(data: 'u', target: inputNode)) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.lineTextForBufferRow(0)).toBe 'uvar quicksort = function () {' # simulate the accented character suggestion's selection of the previous character inputNode.setSelectionRange(0, 1) componentNode.dispatchEvent(buildTextInputEvent(data: 'ü', target: inputNode)) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() + runs -> expect(editor.lineTextForBufferRow(0)).toBe 'üvar quicksort = function () {' @@ -3393,7 +3393,7 @@ describe "TextEditorComponent", -> expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0) component.setFontSize(10) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0) @@ -3469,7 +3469,7 @@ describe "TextEditorComponent", -> component.checkForVisibilityChange() editor.setCursorBufferPosition([0, Infinity]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left @@ -3502,7 +3502,7 @@ describe "TextEditorComponent", -> component.checkForVisibilityChange() editor.setCursorBufferPosition([0, Infinity]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left @@ -3529,7 +3529,7 @@ describe "TextEditorComponent", -> component.checkForVisibilityChange() editor.setCursorBufferPosition([0, Infinity]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left @@ -3539,14 +3539,14 @@ describe "TextEditorComponent", -> describe "soft wrapping", -> beforeEach -> editor.setSoftWrapped(true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "updates the wrap location when the editor is resized", -> newHeight = 4 * editor.getLineHeightInPixels() + "px" expect(parseInt(newHeight)).toBeLessThan wrapperNode.offsetHeight wrapperNode.style.height = newHeight - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelectorAll('.line')).toHaveLength(7) # visible rows + model longest screen row @@ -3555,7 +3555,7 @@ describe "TextEditorComponent", -> componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' atom.views.performDocumentPoll() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.line').textContent).toBe "var quicksort " @@ -3566,7 +3566,7 @@ describe "TextEditorComponent", -> componentNode.style.width = 30 * charWidth + 'px' atom.views.performDocumentPoll() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(0).textContent).toBe "var quicksort = " @@ -3574,7 +3574,7 @@ describe "TextEditorComponent", -> describe "default decorations", -> it "applies .cursor-line decorations for line numbers overlapping selections", -> editor.setCursorScreenPosition([4, 4]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(3, 'cursor-line')).toBe false @@ -3583,7 +3583,7 @@ describe "TextEditorComponent", -> editor.setSelectedScreenRange([[3, 4], [4, 4]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(3, 'cursor-line')).toBe true @@ -3591,7 +3591,7 @@ describe "TextEditorComponent", -> editor.setSelectedScreenRange([[3, 4], [4, 0]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(3, 'cursor-line')).toBe true @@ -3599,7 +3599,7 @@ describe "TextEditorComponent", -> it "does not apply .cursor-line to the last line of a selection if it's empty", -> editor.setSelectedScreenRange([[3, 4], [5, 0]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(3, 'cursor-line')).toBe true expect(lineNumberHasClass(4, 'cursor-line')).toBe true @@ -3607,7 +3607,7 @@ describe "TextEditorComponent", -> it "applies .cursor-line decorations for lines containing the cursor in non-empty selections", -> editor.setCursorScreenPosition([4, 4]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineHasClass(3, 'cursor-line')).toBe false expect(lineHasClass(4, 'cursor-line')).toBe true @@ -3615,7 +3615,7 @@ describe "TextEditorComponent", -> editor.setSelectedScreenRange([[3, 4], [4, 4]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineHasClass(2, 'cursor-line')).toBe false @@ -3625,13 +3625,13 @@ describe "TextEditorComponent", -> it "applies .cursor-line-no-selection to line numbers for rows containing the cursor when the selection is empty", -> editor.setCursorScreenPosition([4, 4]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe true editor.setSelectedScreenRange([[3, 4], [4, 4]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe false @@ -3641,7 +3641,7 @@ describe "TextEditorComponent", -> it "does not assign a height on the component node", -> wrapperNode.style.height = '200px' component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.style.height).toBe '' @@ -3654,7 +3654,7 @@ describe "TextEditorComponent", -> describe "when the 'mini' property is true", -> beforeEach -> editor.setMini(true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "does not render the gutter", -> expect(componentNode.querySelector('.gutter')).toBeNull() @@ -3681,13 +3681,13 @@ describe "TextEditorComponent", -> editor.setPlaceholderText('Hello World') expect(componentNode.querySelector('.placeholder-text')).toBeNull() editor.setText('') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.placeholder-text').textContent).toBe "Hello World" editor.setText('hey') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.placeholder-text')).toBeNull() @@ -3788,14 +3788,14 @@ describe "TextEditorComponent", -> atom.config.set 'editor.invisibles', coffeeInvisibles, scopeSelector: '.source.coffee' editor.setText " a line with tabs\tand spaces \n" - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "renders the invisibles when editor.showInvisibles is true for a given grammar", -> expect(component.lineNodeForScreenRow(0).textContent).toBe "#{jsInvisibles.space}a line with tabs#{jsInvisibles.tab}and spaces#{jsInvisibles.space}#{jsInvisibles.eol}" it "does not render the invisibles when editor.showInvisibles is false for a given grammar", -> editor.setGrammar(coffeeEditor.getGrammar()) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces " @@ -3803,7 +3803,7 @@ describe "TextEditorComponent", -> jsGrammar = editor.getGrammar() editor.setGrammar(coffeeEditor.getGrammar()) atom.config.set 'editor.showInvisibles', true, scopeSelector: '.source.coffee' - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() newInvisibles = eol: 'N' @@ -3815,13 +3815,13 @@ describe "TextEditorComponent", -> expect(component.lineNodeForScreenRow(0).textContent).toBe "#{coffeeInvisibles.space}a line with tabs#{coffeeInvisibles.tab}and spaces#{coffeeInvisibles.space}#{coffeeInvisibles.eol}" atom.config.set 'editor.invisibles', newInvisibles, scopeSelector: '.source.coffee' - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(0).textContent).toBe "#{newInvisibles.space}a line with tabs#{newInvisibles.tab}and spaces#{newInvisibles.space}#{newInvisibles.eol}" editor.setGrammar(jsGrammar) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(0).textContent).toBe "#{jsInvisibles.space}a line with tabs#{jsInvisibles.tab}and spaces#{jsInvisibles.space}#{jsInvisibles.eol}" @@ -3830,7 +3830,7 @@ describe "TextEditorComponent", -> beforeEach -> atom.config.set 'editor.showIndentGuide', true, scopeSelector: '.source.js' atom.config.set 'editor.showIndentGuide', false, scopeSelector: '.source.coffee' - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "has an 'indent-guide' class when scoped editor.showIndentGuide is true, but not when scoped editor.showIndentGuide is false", -> line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) @@ -3840,7 +3840,7 @@ describe "TextEditorComponent", -> editor.setGrammar(coffeeEditor.getGrammar()) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) @@ -3855,7 +3855,7 @@ describe "TextEditorComponent", -> expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false atom.config.set 'editor.showIndentGuide', false, scopeSelector: '.source.js' - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) @@ -3870,27 +3870,27 @@ describe "TextEditorComponent", -> component.setLineHeight("10px") component.setFontSize(17) component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> wrapperNode.setWidth(55) wrapperNode.setHeight(55) component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> component.presenter.setHorizontalScrollbarHeight(0) component.presenter.setVerticalScrollbarWidth(0) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() describe "when selecting buffer ranges", -> it "autoscrolls the selection if it is last unless the 'autoscroll' option is false", -> expect(wrapperNode.getScrollTop()).toBe 0 editor.setSelectedBufferRange([[5, 6], [6, 8]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() right = null runs -> @@ -3900,7 +3900,7 @@ describe "TextEditorComponent", -> editor.setSelectedBufferRange([[0, 0], [0, 0]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 @@ -3908,7 +3908,7 @@ describe "TextEditorComponent", -> editor.setSelectedBufferRange([[6, 6], [6, 8]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 @@ -3917,7 +3917,7 @@ describe "TextEditorComponent", -> describe "when adding selections for buffer ranges", -> it "autoscrolls to the added selection if needed", -> editor.addSelectionForBufferRange([[8, 10], [8, 15]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> right = wrapperNode.pixelPositionForBufferPosition([8, 15]).left @@ -3927,14 +3927,14 @@ describe "TextEditorComponent", -> describe "when selecting lines containing cursors", -> it "autoscrolls to the selection", -> editor.setCursorScreenPosition([5, 6]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> wrapperNode.scrollToTop() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.selectLinesContainingCursors() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 @@ -3942,29 +3942,28 @@ describe "TextEditorComponent", -> describe "when there are multiple empty selections on different lines", -> it "autoscrolls to the last cursor", -> editor.setCursorScreenPosition([1, 2], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> editor.addCursorAtScreenPosition([10, 4], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.insertText('a') - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 75 describe "when scrolled to cursor position", -> it "scrolls the last cursor into view, centering around the cursor if possible and the 'center' option isn't false", -> editor.setCursorScreenPosition([8, 8], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 expect(wrapperNode.getScrollLeft()).toBe 0 editor.scrollToCursorPosition() - - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> right = wrapperNode.pixelPositionForScreenPosition([8, 9 + editor.getHorizontalScrollMargin()]).left @@ -3983,20 +3982,20 @@ describe "TextEditorComponent", -> expect(wrapperNode.getScrollBottom()).toBe 5.5 * 10 editor.setCursorScreenPosition([2, 0]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe 5.5 * 10 editor.moveDown() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe 6 * 10 editor.moveDown() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe 7 * 10 @@ -4004,21 +4003,21 @@ describe "TextEditorComponent", -> it "scrolls up when the last cursor gets closer than ::verticalScrollMargin to the top of the editor", -> editor.setCursorScreenPosition([11, 0]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> editor.moveUp() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe wrapperNode.getScrollHeight() editor.moveUp() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 7 * 10 editor.moveUp() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 6 * 10 @@ -4028,13 +4027,13 @@ describe "TextEditorComponent", -> editor.setCursorScreenPosition([0, 2]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollRight()).toBe 5.5 * 10 editor.moveRight() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() margin = null runs -> @@ -4043,7 +4042,7 @@ describe "TextEditorComponent", -> expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 editor.moveRight() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> right = wrapperNode.pixelPositionForScreenPosition([0, 5]).left + margin @@ -4052,18 +4051,18 @@ describe "TextEditorComponent", -> it "scrolls left when the last cursor gets closer than ::horizontalScrollMargin to the left of the editor", -> wrapperNode.setScrollRight(wrapperNode.getScrollWidth()) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollRight()).toBe wrapperNode.getScrollWidth() editor.setCursorScreenPosition([6, 62], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> editor.moveLeft() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() margin = null runs -> @@ -4072,7 +4071,7 @@ describe "TextEditorComponent", -> expect(wrapperNode.getScrollLeft()).toBeCloseTo left, 0 editor.moveLeft() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> left = wrapperNode.pixelPositionForScreenPosition([6, 60]).left - margin @@ -4081,13 +4080,13 @@ describe "TextEditorComponent", -> it "scrolls down when inserting lines makes the document longer than the editor's height", -> editor.setCursorScreenPosition([13, Infinity]) editor.insertNewline() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe 14 * 10 editor.insertNewline() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe 15 * 10 @@ -4095,29 +4094,29 @@ describe "TextEditorComponent", -> it "autoscrolls to the cursor when it moves due to undo", -> editor.insertText('abc') wrapperNode.setScrollTop(Infinity) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> editor.undo() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 it "doesn't scroll when the cursor moves into the visible area", -> editor.setCursorBufferPosition([0, 0]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> wrapperNode.setScrollTop(40) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> editor.setCursorBufferPosition([6, 0]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 40 @@ -4125,58 +4124,58 @@ describe "TextEditorComponent", -> it "honors the autoscroll option on cursor and selection manipulation methods", -> expect(wrapperNode.getScrollTop()).toBe 0 editor.addCursorAtScreenPosition([11, 11], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.addCursorAtBufferPosition([11, 11], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.setCursorScreenPosition([11, 11], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.setCursorBufferPosition([11, 11], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.addSelectionForBufferRange([[11, 11], [11, 11]], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.addSelectionForScreenRange([[11, 11], [11, 12]], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.setSelectedBufferRange([[11, 0], [11, 1]], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.setSelectedScreenRange([[11, 0], [11, 6]], autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.clearSelections(autoscroll: false) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.addSelectionForScreenRange([[0, 0], [0, 4]]) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> editor.getCursors()[0].setScreenPosition([11, 11], autoscroll: true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 editor.getCursors()[0].setBufferPosition([0, 0], autoscroll: true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], autoscroll: true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], autoscroll: true) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 @@ -4184,11 +4183,11 @@ describe "TextEditorComponent", -> beforeEach -> wrapperNode.style.height = lineHeightInPixels * 8 + "px" component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() it "returns the first and the last visible rows", -> component.setScrollTop(0) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.getVisibleRowRange()).toEqual [0, 9] @@ -4196,12 +4195,12 @@ describe "TextEditorComponent", -> it "ends at last buffer row even if there's more space available", -> wrapperNode.style.height = lineHeightInPixels * 13 + "px" component.measureDimensions() - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> component.setScrollTop(60) - waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForNextDOMUpdate() runs -> expect(component.getVisibleRowRange()).toEqual [0, 13] @@ -4279,5 +4278,8 @@ describe "TextEditorComponent", -> else [node] + waitsForNextDOMUpdate = -> + waitsForPromise -> atom.views.getNextUpdatePromise() + waitsForAnimationFrame = -> waitsFor 'next animation frame', (done) -> requestAnimationFrame(done) From 0169c1160edb256e19db86cfc777e9a5e737bf90 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 25 Oct 2015 10:13:37 -0600 Subject: [PATCH 06/53] Wait for next DOM update synchronously after change to avoid flakiness --- spec/text-editor-component-spec.coffee | 396 ++++++++----------------- 1 file changed, 130 insertions(+), 266 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index ec1155baf..6264e5ba0 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -41,7 +41,7 @@ describe "TextEditorComponent", -> horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') component.measureDimensions() - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() afterEach -> contentNode.style.width = '' @@ -54,13 +54,11 @@ describe "TextEditorComponent", -> # simulate state corruption component.presenter.startRow = -1 component.presenter.endRow = 9999 - waitsForNextDOMUpdate() it "doesn't update when an animation frame was requested but the component got destroyed before its delivery", -> editor.setText("You shouldn't see this update.") component.destroy() - waitsForNextDOMUpdate() runs -> @@ -89,8 +87,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' component.measureDimensions() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) @@ -109,8 +106,7 @@ describe "TextEditorComponent", -> verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLines() @@ -152,8 +148,7 @@ describe "TextEditorComponent", -> verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5 verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLines() @@ -199,8 +194,7 @@ describe "TextEditorComponent", -> expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) editor.getBuffer().insert([0, 0], '\n\n') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLines() @@ -228,22 +222,19 @@ describe "TextEditorComponent", -> runs -> verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() buffer = null runs -> buffer = editor.getBuffer() buffer.insert([0, 0], '\n\n') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(3).textContent).toBe editor.tokenizedLineForScreenRow(3).text buffer.delete([[0, 0], [3, 0]]) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(3).textContent).toBe editor.tokenizedLineForScreenRow(3).text @@ -301,8 +292,7 @@ describe "TextEditorComponent", -> componentNode.style.width = gutterWidth + wrapperNode.getScrollWidth() + 100 + 'px' component.measureDimensions() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> scrollViewWidth = scrollViewNode.offsetWidth @@ -323,7 +313,6 @@ describe "TextEditorComponent", -> expect(tileNode.style.backgroundColor).toBe(backgroundColor) wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)' - waitsForNextDOMUpdate() runs -> @@ -341,8 +330,7 @@ describe "TextEditorComponent", -> expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe false editor.setText('\ta') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) @@ -359,8 +347,7 @@ describe "TextEditorComponent", -> expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false editor.setText('\t') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) @@ -368,8 +355,7 @@ describe "TextEditorComponent", -> expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false editor.setText('a ') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) @@ -377,7 +363,7 @@ describe "TextEditorComponent", -> expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false editor.setText('a\t') - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) @@ -419,15 +405,13 @@ describe "TextEditorComponent", -> 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) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces " atom.config.set("editor.showInvisibles", true) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}" @@ -480,30 +464,26 @@ describe "TextEditorComponent", -> runs -> editor.setTextInBufferRange([[10, 0], [11, 0]], "\r\n", normalizeLineEndings: false) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' editor.setTabLength(3) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE ' editor.setTabLength(1) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ') editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' @@ -516,7 +496,7 @@ describe "TextEditorComponent", -> runs -> componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() it "doesn't show end of line invisibles at the end of wrapped lines", -> expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that " @@ -602,8 +582,7 @@ describe "TextEditorComponent", -> runs -> editor.getBuffer().insert([13, 0], ' ') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> line12LeafNodes = getLeafNodes(component.lineNodeForScreenRow(12)) @@ -617,9 +596,9 @@ describe "TextEditorComponent", -> waitsForNextDOMUpdate() - runs -> editor.getBuffer().insert([12, 0], ' ') - - waitsForNextDOMUpdate() + runs -> + editor.getBuffer().insert([12, 0], ' ') + waitsForNextDOMUpdate() runs -> line13LeafNodes = getLeafNodes(component.lineNodeForScreenRow(13)) @@ -669,8 +648,7 @@ describe "TextEditorComponent", -> expect(foldedLineNode.querySelector('.fold-marker')).toBeTruthy() editor.unfoldBufferRow(4) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> foldedLineNode = component.lineNodeForScreenRow(4) @@ -697,8 +675,7 @@ describe "TextEditorComponent", -> verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLineNumbers() @@ -720,8 +697,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' component.measureDimensions() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) @@ -756,8 +732,7 @@ describe "TextEditorComponent", -> verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5 verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> tilesNodes = component.tileNodesForLineNumbers() @@ -797,8 +772,7 @@ describe "TextEditorComponent", -> expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 2 * lineHeightInPixels editor.getBuffer().insert([0, 0], '\n\n') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels @@ -816,7 +790,6 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 30 * charWidth + 'px' component.measureDimensions() - waitsForNextDOMUpdate() runs -> @@ -848,8 +821,7 @@ describe "TextEditorComponent", -> # Removes padding when the max number of digits goes down editor.getBuffer().delete([[1, 0], [2, 0]]) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> for screenRow in [0..8] @@ -858,8 +830,7 @@ describe "TextEditorComponent", -> # Increases padding when the max number of digits goes up editor.getBuffer().insert([0, 0], '\n\n') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> for screenRow in [0..8] @@ -870,7 +841,6 @@ describe "TextEditorComponent", -> it "renders the .line-numbers div at the full height of the editor even if it's taller than its content", -> wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px' component.measureDimensions() - waitsForNextDOMUpdate() runs -> @@ -887,7 +857,6 @@ describe "TextEditorComponent", -> # favor gutter color if it's assigned gutterNode.style.backgroundColor = 'rgb(255, 0, 0)' atom.views.performDocumentPoll() # required due to DOM change not being detected inside shadow DOM - waitsForNextDOMUpdate() runs -> @@ -899,29 +868,25 @@ describe "TextEditorComponent", -> expect(component.gutterContainerComponent.getLineNumberGutterComponent()?).toBe true editor.setLineNumberGutterVisible(false) - waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.gutter').style.display).toBe 'none' atom.config.set("editor.showLineNumbers", false) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.gutter').style.display).toBe 'none' editor.setLineNumberGutterVisible(true) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.gutter').style.display).toBe 'none' atom.config.set("editor.showLineNumbers", true) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.gutter').style.display).toBe '' @@ -965,14 +930,12 @@ describe "TextEditorComponent", -> expect(lineNumberHasClass(11, 'foldable')).toBe false editor.getBuffer().insert([11, 44], '\n fold me') - waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(11, 'foldable')).toBe true editor.undo() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(11, 'foldable')).toBe false @@ -984,16 +947,14 @@ describe "TextEditorComponent", -> runs -> expect(lineNumberHasClass(4, 'folded')).toBe true editor.getBuffer().insert([0, 0], '\n') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(4, 'folded')).toBe false expect(lineNumberHasClass(5, 'folded')).toBe true editor.unfoldBufferRow(5) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(5, 'folded')).toBe false @@ -1006,8 +967,7 @@ describe "TextEditorComponent", -> runs -> componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() it "doesn't add the foldable class for soft-wrapped lines", -> expect(lineNumberHasClass(0, 'foldable')).toBe true @@ -1044,8 +1004,7 @@ describe "TextEditorComponent", -> lineNumber = component.lineNumberNodeForScreenRow(1) target = lineNumber.querySelector('.icon-right') target.dispatchEvent(buildClickEvent(target)) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(1, 'folded')).toBe false @@ -1067,7 +1026,6 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForNextDOMUpdate() runs -> @@ -1079,8 +1037,7 @@ describe "TextEditorComponent", -> cursor2 = editor.addCursorAtScreenPosition([8, 11], autoscroll: false) cursor3 = editor.addCursorAtScreenPosition([4, 10], autoscroll: false) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> cursorNodes = componentNode.querySelectorAll('.cursor') @@ -1090,13 +1047,11 @@ describe "TextEditorComponent", -> expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{Math.round(10 * charWidth)}px, #{4 * lineHeightInPixels}px)" verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> horizontalScrollbarNode.scrollLeft = 3.5 * charWidth - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() cursorMovedListener = null runs -> @@ -1107,16 +1062,14 @@ describe "TextEditorComponent", -> editor.onDidChangeCursorPosition cursorMovedListener = jasmine.createSpy('cursorMovedListener') cursor3.setScreenPosition([4, 11], autoscroll: false) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" expect(cursorMovedListener).toHaveBeenCalled() cursor3.destroy() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> cursorNodes = componentNode.querySelectorAll('.cursor') @@ -1127,7 +1080,6 @@ describe "TextEditorComponent", -> it "accounts for character widths when positioning cursors", -> atom.config.set('editor.fontFamily', 'sans-serif') editor.setCursorScreenPosition([0, 16]) - waitsForNextDOMUpdate() runs -> @@ -1147,7 +1099,6 @@ describe "TextEditorComponent", -> atom.config.set('editor.fontFamily', 'sans-serif') editor.setText('he\u0301y') # e with an accent mark editor.setCursorBufferPosition([0, 3]) - waitsForNextDOMUpdate() runs -> @@ -1167,7 +1118,6 @@ describe "TextEditorComponent", -> it "positions cursors correctly after character widths are changed via a stylesheet change", -> atom.config.set('editor.fontFamily', 'sans-serif') editor.setCursorScreenPosition([0, 16]) - waitsForNextDOMUpdate() runs -> @@ -1176,8 +1126,7 @@ describe "TextEditorComponent", -> font-weight: bold; } """, context: 'atom-text-editor' - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> cursor = componentNode.querySelector('.cursor') @@ -1196,7 +1145,6 @@ describe "TextEditorComponent", -> it "sets the cursor to the default character width at the end of a line", -> editor.setCursorScreenPosition([0, Infinity]) - waitsForNextDOMUpdate() runs -> @@ -1205,7 +1153,6 @@ describe "TextEditorComponent", -> it "gives the cursor a non-zero width even if it's inside atomic tokens", -> editor.setCursorScreenPosition([1, 0]) - waitsForNextDOMUpdate() runs -> @@ -1215,8 +1162,8 @@ describe "TextEditorComponent", -> it "blinks cursors when they aren't moving", -> cursorsNode = componentNode.querySelector('.cursors') wrapperNode.focus() - waitsForNextDOMUpdate() + runs -> expect(cursorsNode.classList.contains('blink-off')).toBe false waitsFor -> cursorsNode.classList.contains('blink-off') @@ -1225,8 +1172,7 @@ describe "TextEditorComponent", -> runs -> # Stop blinking after moving the cursor editor.moveRight() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(cursorsNode.classList.contains('blink-off')).toBe false @@ -1236,7 +1182,6 @@ describe "TextEditorComponent", -> it "does not render cursors that are associated with non-empty selections", -> editor.setSelectedScreenRange([[0, 4], [4, 6]]) editor.addCursorAtScreenPosition([6, 8]) - waitsForNextDOMUpdate() runs -> @@ -1380,7 +1325,6 @@ describe "TextEditorComponent", -> it "updates selections when the line height changes", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setLineHeight(2) - waitsForNextDOMUpdate() runs -> @@ -1390,7 +1334,6 @@ describe "TextEditorComponent", -> it "updates selections when the font size changes", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontSize(10) - waitsForNextDOMUpdate() runs -> @@ -1401,7 +1344,6 @@ describe "TextEditorComponent", -> it "updates selections when the font family changes", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontFamily('sans-serif') - waitsForNextDOMUpdate() runs -> @@ -1411,7 +1353,6 @@ describe "TextEditorComponent", -> it "will flash the selection when flash:true is passed to editor::setSelectedBufferRange", -> editor.setSelectedBufferRange([[1, 6], [1, 10]], flash: true) - waitsForNextDOMUpdate() selectionNode = null @@ -1423,8 +1364,7 @@ describe "TextEditorComponent", -> runs -> editor.setSelectedBufferRange([[1, 5], [1, 7]], flash: true) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(selectionNode.classList.contains('flash')).toBe true @@ -1445,30 +1385,26 @@ describe "TextEditorComponent", -> # Shrink editor vertically wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForNextDOMUpdate() runs -> # Add decorations that are out of range marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]]) editor.decorateMarker(marker2, type: ['line-number', 'line'], class: 'b') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> # Scroll decorations into view verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(9, 'b')).toBe true # Fold a line to move the decorations editor.foldBufferRow(5) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(9, 'b')).toBe false @@ -1479,23 +1415,20 @@ describe "TextEditorComponent", -> editor.setSoftWrapped(true) componentNode.style.width = 16 * charWidth + 'px' component.measureDimensions() - waitsForNextDOMUpdate() runs -> marker.destroy() marker = editor.markBufferRange([[0, 0], [0, 2]]) editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'b') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(0, 'b')).toBe true expect(lineNumberHasClass(1, 'b')).toBe false marker.setBufferRange([[0, 0], [0, Infinity]]) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(0, 'b')).toBe true @@ -1508,7 +1441,6 @@ describe "TextEditorComponent", -> expect(lineAndLineNumberHaveClass(4, 'a')).toBe false editor.getBuffer().insert([0, 0], '\n') - waitsForNextDOMUpdate() runs -> @@ -1518,8 +1450,7 @@ describe "TextEditorComponent", -> expect(lineAndLineNumberHaveClass(5, 'a')).toBe false marker.setBufferRange([[4, 4], [6, 4]]) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(2, 'a')).toBe false @@ -1531,7 +1462,6 @@ describe "TextEditorComponent", -> it "remove decoration classes when decorations are removed", -> decoration.destroy() - waitsForNextDOMUpdate() runs -> @@ -1542,7 +1472,6 @@ describe "TextEditorComponent", -> it "removes decorations when their marker is invalidated", -> editor.getBuffer().insert([3, 2], 'n') - waitsForNextDOMUpdate() runs -> @@ -1553,8 +1482,7 @@ describe "TextEditorComponent", -> expect(lineAndLineNumberHaveClass(4, 'a')).toBe false editor.undo() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(marker.isValid()).toBe true @@ -1587,7 +1515,6 @@ describe "TextEditorComponent", -> describe "when the decoration's 'onlyEmpty' property is true", -> it "only applies the decoration when its marker is empty", -> editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-empty', onlyEmpty: true) - waitsForNextDOMUpdate() runs -> @@ -1595,8 +1522,7 @@ describe "TextEditorComponent", -> expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe false marker.clearTail() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe false @@ -1605,7 +1531,6 @@ describe "TextEditorComponent", -> describe "when the decoration's 'onlyNonEmpty' property is true", -> it "only applies the decoration when its marker is non-empty", -> editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-non-empty', onlyNonEmpty: true) - waitsForNextDOMUpdate() runs -> @@ -1613,8 +1538,7 @@ describe "TextEditorComponent", -> expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe true marker.clearTail() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe false @@ -1632,14 +1556,12 @@ describe "TextEditorComponent", -> it "does not render highlights for off-screen lines until they come on-screen", -> wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForNextDOMUpdate() runs -> marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], invalidate: 'inside') editor.decorateMarker(marker, type: 'highlight', class: 'some-highlight') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> # Should not be rendering range containing the marker @@ -1652,8 +1574,7 @@ describe "TextEditorComponent", -> verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(component.presenter.endRow).toBeGreaterThan(8) @@ -1704,8 +1625,7 @@ describe "TextEditorComponent", -> expect(regions.length).toBe 0 editor.getBuffer().undo() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(marker.isValid()).toBe true @@ -1714,14 +1634,12 @@ describe "TextEditorComponent", -> it "allows multiple space-delimited decoration classes", -> decoration.setProperties(type: 'highlight', class: 'foo bar') - waitsForNextDOMUpdate() runs -> expect(componentNode.querySelectorAll('.foo.bar').length).toBe 2 decoration.setProperties(type: 'highlight', class: 'bar baz') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelectorAll('.bar.baz').length).toBe 2 @@ -1756,8 +1674,9 @@ describe "TextEditorComponent", -> waitsForNextDOMUpdate() runs -> expect(highlightNode.classList.contains('flash-class')).toBe true waits 2 - runs -> decoration.flash('flash-class', 10) - waitsForNextDOMUpdate() + runs -> + decoration.flash('flash-class', 10) + waitsForNextDOMUpdate() runs -> expect(highlightNode.classList.contains('flash-class')).toBe false waitsFor -> highlightNode.classList.contains('flash-class') @@ -1769,7 +1688,6 @@ describe "TextEditorComponent", -> expect(originalTop).toBe(2 * lineHeightInPixels) editor.getBuffer().insert([0, 0], '\n') - waitsForNextDOMUpdate() runs -> @@ -1783,7 +1701,6 @@ describe "TextEditorComponent", -> expect(parseInt(regionStyle.top)).toBe 2 * lineHeightInPixels marker.setBufferRange([[5, 8], [5, 13]]) - waitsForNextDOMUpdate() runs -> @@ -1795,7 +1712,6 @@ describe "TextEditorComponent", -> expect(componentNode.querySelector('.test-highlight')).toBeTruthy() decoration.setProperties(type: 'highlight', class: 'new-test-highlight') - waitsForNextDOMUpdate() runs -> @@ -1821,8 +1737,7 @@ describe "TextEditorComponent", -> expect(overlay).toBe item decoration.destroy() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') @@ -1831,7 +1746,6 @@ describe "TextEditorComponent", -> it "renders the overlay element with the CSS class specified by the decoration", -> marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', class: 'my-overlay', item}) - waitsForNextDOMUpdate() runs -> @@ -1845,7 +1759,6 @@ describe "TextEditorComponent", -> it "renders at the head of the marker by default", -> marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], invalidate: 'never') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - waitsForNextDOMUpdate() runs -> @@ -1897,16 +1810,14 @@ describe "TextEditorComponent", -> expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' editor.insertText('a') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(overlay.style.left).toBe windowWidth - itemWidth + 'px' expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' editor.insertText('b') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(overlay.style.left).toBe windowWidth - itemWidth + 'px' @@ -1921,15 +1832,13 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - waitsForNextDOMUpdate() runs -> expect(editor.getCursorScreenPosition()).toEqual [0, 0] wrapperNode.setScrollTop(3 * lineHeightInPixels) wrapperNode.setScrollLeft(3 * charWidth) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(inputNode.offsetTop).toBe 0 @@ -1937,8 +1846,7 @@ describe "TextEditorComponent", -> # In bounds, not focused editor.setCursorBufferPosition([5, 4], autoscroll: false) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(inputNode.offsetTop).toBe 0 @@ -1946,8 +1854,7 @@ describe "TextEditorComponent", -> # In bounds and focused wrapperNode.focus() # updates via state change - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(inputNode.offsetTop).toBe (5 * lineHeightInPixels) - wrapperNode.getScrollTop() @@ -1955,8 +1862,7 @@ describe "TextEditorComponent", -> # In bounds, not focused inputNode.blur() # updates via state change - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(inputNode.offsetTop).toBe 0 @@ -1964,8 +1870,7 @@ describe "TextEditorComponent", -> # Out of bounds, not focused editor.setCursorBufferPosition([1, 2], autoscroll: false) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(inputNode.offsetTop).toBe 0 @@ -1973,8 +1878,7 @@ describe "TextEditorComponent", -> # Out of bounds, focused inputNode.focus() # updates via state change - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(inputNode.offsetTop).toBe 0 @@ -2000,8 +1904,7 @@ describe "TextEditorComponent", -> coordinates = clientCoordinatesForScreenPosition([0, 2]) coordinates.clientY = -1 linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(editor.getCursorScreenPosition()).toEqual [0, 0] @@ -2037,8 +1940,7 @@ describe "TextEditorComponent", -> runs -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]))) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(editor.getCursorScreenPosition()).toEqual [4, 8] @@ -2168,7 +2070,6 @@ describe "TextEditorComponent", -> wrapperNode.style.height = '100px' wrapperNode.style.width = '100px' component.measureDimensions() - waitsForNextDOMUpdate() runs -> @@ -2643,7 +2544,7 @@ describe "TextEditorComponent", -> runs -> componentNode.style.width = 21 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() describe "when the gutter is clicked", -> it "selects the clicked buffer row", -> @@ -2794,7 +2695,7 @@ describe "TextEditorComponent", -> expect(componentNode.classList.contains('is-focused')).toBe true expect(wrapperNode.classList.contains('is-focused')).toBe true inputNode.blur() - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(componentNode.classList.contains('is-focused')).toBe false expect(wrapperNode.classList.contains('is-focused')).toBe false @@ -2810,14 +2711,12 @@ describe "TextEditorComponent", -> it "adds the 'has-selection' class to the editor when there is a selection", -> expect(componentNode.classList.contains('has-selection')).toBe false editor.selectDown() - waitsForNextDOMUpdate() runs -> expect(componentNode.classList.contains('has-selection')).toBe true editor.moveDown() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(componentNode.classList.contains('has-selection')).toBe false @@ -2826,14 +2725,12 @@ describe "TextEditorComponent", -> it "updates the vertical scrollbar when the scrollTop is changed in the model", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.scrollTop).toBe 0 wrapperNode.setScrollTop(10) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.scrollTop).toBe 10 @@ -2855,8 +2752,7 @@ describe "TextEditorComponent", -> expect(horizontalScrollbarNode.scrollLeft).toBe 0 wrapperNode.setScrollLeft(100) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> top = 0 @@ -2875,8 +2771,7 @@ describe "TextEditorComponent", -> expect(wrapperNode.getScrollLeft()).toBe 0 horizontalScrollbarNode.scrollLeft = 100 horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollLeft()).toBe 100 @@ -2886,7 +2781,6 @@ describe "TextEditorComponent", -> wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) - waitsForNextDOMUpdate() lastLineNode = null @@ -2899,8 +2793,7 @@ describe "TextEditorComponent", -> # Scroll so there's no space below the last line when the horizontal scrollbar disappears wrapperNode.style.width = 100 * charWidth + 'px' component.measureDimensions() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom @@ -2912,7 +2805,6 @@ describe "TextEditorComponent", -> wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() wrapperNode.setScrollLeft(Infinity) - waitsForNextDOMUpdate() runs -> @@ -2927,7 +2819,6 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = '1000px' component.measureDimensions() - waitsForNextDOMUpdate() runs -> @@ -2936,8 +2827,7 @@ describe "TextEditorComponent", -> componentNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.style.display).toBe '' @@ -2945,8 +2835,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 20 * lineHeightInPixels + 'px' component.measureDimensions() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.style.display).toBe 'none' @@ -2988,7 +2877,6 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = '1000px' component.measureDimensions() - waitsForNextDOMUpdate() runs -> @@ -2998,8 +2886,7 @@ describe "TextEditorComponent", -> componentNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' @@ -3008,8 +2895,7 @@ describe "TextEditorComponent", -> wrapperNode.style.height = 20 * lineHeightInPixels + 'px' component.measureDimensions() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' @@ -3545,7 +3431,6 @@ describe "TextEditorComponent", -> newHeight = 4 * editor.getLineHeightInPixels() + "px" expect(parseInt(newHeight)).toBeLessThan wrapperNode.offsetHeight wrapperNode.style.height = newHeight - waitsForNextDOMUpdate() runs -> @@ -3554,8 +3439,7 @@ describe "TextEditorComponent", -> gutterWidth = componentNode.querySelector('.gutter').offsetWidth componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' atom.views.performDocumentPoll() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.line').textContent).toBe "var quicksort " @@ -3582,16 +3466,14 @@ describe "TextEditorComponent", -> expect(lineNumberHasClass(5, 'cursor-line')).toBe false editor.setSelectedScreenRange([[3, 4], [4, 4]]) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(3, 'cursor-line')).toBe true expect(lineNumberHasClass(4, 'cursor-line')).toBe true editor.setSelectedScreenRange([[3, 4], [4, 0]]) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineNumberHasClass(3, 'cursor-line')).toBe true @@ -3614,8 +3496,7 @@ describe "TextEditorComponent", -> expect(lineHasClass(5, 'cursor-line')).toBe false editor.setSelectedScreenRange([[3, 4], [4, 4]]) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(lineHasClass(2, 'cursor-line')).toBe false @@ -3686,8 +3567,7 @@ describe "TextEditorComponent", -> runs -> expect(componentNode.querySelector('.placeholder-text').textContent).toBe "Hello World" editor.setText('hey') - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(componentNode.querySelector('.placeholder-text')).toBeNull() @@ -3820,8 +3700,7 @@ describe "TextEditorComponent", -> runs -> expect(component.lineNodeForScreenRow(0).textContent).toBe "#{newInvisibles.space}a line with tabs#{newInvisibles.tab}and spaces#{newInvisibles.space}#{newInvisibles.eol}" editor.setGrammar(jsGrammar) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(component.lineNodeForScreenRow(0).textContent).toBe "#{jsInvisibles.space}a line with tabs#{jsInvisibles.tab}and spaces#{jsInvisibles.space}#{jsInvisibles.eol}" @@ -3839,7 +3718,6 @@ describe "TextEditorComponent", -> expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false editor.setGrammar(coffeeEditor.getGrammar()) - waitsForNextDOMUpdate() runs -> @@ -3876,14 +3754,12 @@ describe "TextEditorComponent", -> wrapperNode.setWidth(55) wrapperNode.setHeight(55) component.measureDimensions() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> component.presenter.setHorizontalScrollbarHeight(0) component.presenter.setVerticalScrollbarWidth(0) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() describe "when selecting buffer ranges", -> it "autoscrolls the selection if it is last unless the 'autoscroll' option is false", -> @@ -3899,16 +3775,14 @@ describe "TextEditorComponent", -> expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 editor.setSelectedBufferRange([[0, 0], [0, 0]]) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 expect(wrapperNode.getScrollLeft()).toBe 0 editor.setSelectedBufferRange([[6, 6], [6, 8]]) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 @@ -3930,11 +3804,11 @@ describe "TextEditorComponent", -> waitsForNextDOMUpdate() runs -> wrapperNode.scrollToTop() - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.selectLinesContainingCursors() - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 @@ -3945,11 +3819,11 @@ describe "TextEditorComponent", -> waitsForNextDOMUpdate() runs -> editor.addCursorAtScreenPosition([10, 4], autoscroll: false) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.insertText('a') - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 75 @@ -3986,16 +3860,15 @@ describe "TextEditorComponent", -> runs -> expect(wrapperNode.getScrollBottom()).toBe 5.5 * 10 - editor.moveDown() - waitsForNextDOMUpdate() + editor.moveDown() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe 6 * 10 editor.moveDown() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe 7 * 10 @@ -4006,18 +3879,18 @@ describe "TextEditorComponent", -> waitsForNextDOMUpdate() runs -> wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> editor.moveUp() - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe wrapperNode.getScrollHeight() editor.moveUp() - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 7 * 10 editor.moveUp() - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 6 * 10 @@ -4026,14 +3899,13 @@ describe "TextEditorComponent", -> expect(wrapperNode.getScrollRight()).toBe 5.5 * 10 editor.setCursorScreenPosition([0, 2]) - waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollRight()).toBe 5.5 * 10 - editor.moveRight() - waitsForNextDOMUpdate() + editor.moveRight() + waitsForNextDOMUpdate() margin = null runs -> @@ -4056,13 +3928,11 @@ describe "TextEditorComponent", -> runs -> expect(wrapperNode.getScrollRight()).toBe wrapperNode.getScrollWidth() editor.setCursorScreenPosition([6, 62], autoscroll: false) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> editor.moveLeft() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() margin = null runs -> @@ -4070,8 +3940,7 @@ describe "TextEditorComponent", -> left = wrapperNode.pixelPositionForScreenPosition([6, 61]).left - margin expect(wrapperNode.getScrollLeft()).toBeCloseTo left, 0 editor.moveLeft() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> left = wrapperNode.pixelPositionForScreenPosition([6, 60]).left - margin @@ -4085,8 +3954,7 @@ describe "TextEditorComponent", -> runs -> expect(wrapperNode.getScrollBottom()).toBe 14 * 10 editor.insertNewline() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollBottom()).toBe 15 * 10 @@ -4098,8 +3966,7 @@ describe "TextEditorComponent", -> runs -> editor.undo() - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 @@ -4110,13 +3977,11 @@ describe "TextEditorComponent", -> runs -> wrapperNode.setScrollTop(40) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> editor.setCursorBufferPosition([6, 0]) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 40 @@ -4128,54 +3993,54 @@ describe "TextEditorComponent", -> runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.addCursorAtBufferPosition([11, 11], autoscroll: false) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.setCursorScreenPosition([11, 11], autoscroll: false) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.setCursorBufferPosition([11, 11], autoscroll: false) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.addSelectionForBufferRange([[11, 11], [11, 11]], autoscroll: false) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.addSelectionForScreenRange([[11, 11], [11, 12]], autoscroll: false) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.setSelectedBufferRange([[11, 0], [11, 1]], autoscroll: false) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.setSelectedScreenRange([[11, 0], [11, 6]], autoscroll: false) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.clearSelections(autoscroll: false) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.addSelectionForScreenRange([[0, 0], [0, 4]]) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> editor.getCursors()[0].setScreenPosition([11, 11], autoscroll: true) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 editor.getCursors()[0].setBufferPosition([0, 0], autoscroll: true) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], autoscroll: true) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], autoscroll: true) - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(wrapperNode.getScrollTop()).toBe 0 @@ -4199,8 +4064,7 @@ describe "TextEditorComponent", -> runs -> component.setScrollTop(60) - - waitsForNextDOMUpdate() + waitsForNextDOMUpdate() runs -> expect(component.getVisibleRowRange()).toEqual [0, 13] From 2af010e729a11ca3841818ce97d234af2d8aacaa Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 26 Oct 2015 10:29:58 -0600 Subject: [PATCH 07/53] Remove dead test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It tests functionality that no longer matters since we don’t subscribe to decorations being destroyed in DisplayBuffer --- spec/display-buffer-spec.coffee | 5 ----- 1 file changed, 5 deletions(-) diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 68dd9c754..ded3ed455 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -1237,11 +1237,6 @@ describe "DisplayBuffer", -> decoration.destroy() expect(displayBuffer.decorationForId(decoration.id)).not.toBeDefined() - it "does not leak disposables", -> - disposablesSize = displayBuffer.disposables.disposables.size - decoration.destroy() - expect(displayBuffer.disposables.disposables.size).toBe(disposablesSize - 1) - describe "when a decoration is updated via Decoration::update()", -> it "emits an 'updated' event containing the new and old params", -> decoration.onDidChangeProperties updatedSpy = jasmine.createSpy() From 3c98c30b4396c891ae5dc8e195df67053148173c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 26 Oct 2015 10:44:25 -0600 Subject: [PATCH 08/53] Wait for animation frames synchronously after actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This avoids test flakiness, presumably due to an animation frame firing before we have a chance to wait due to jasmine’s queueing. --- spec/text-editor-component-spec.coffee | 100 ++++++++----------------- 1 file changed, 31 insertions(+), 69 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 6264e5ba0..84534acf8 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -2052,16 +2052,14 @@ describe "TextEditorComponent", -> expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), which: 1)) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]] linesNode.dispatchEvent(buildMouseEvent('mouseup')) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([12, 0]), which: 1)) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]] @@ -2078,16 +2076,14 @@ describe "TextEditorComponent", -> linesNode.dispatchEvent(buildMouseEvent('mousedown', {clientX: 0, clientY: 0}, which: 1)) linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 50}, which: 1)) - - waitsForAnimationFrame() for i in [0..5] + waitsForAnimationFrame() for i in [0..5] runs -> expect(wrapperNode.getScrollTop()).toBe(0) expect(wrapperNode.getScrollLeft()).toBeGreaterThan(0) linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 100}, which: 1)) - - waitsForAnimationFrame() for i in [0..5] + waitsForAnimationFrame() for i in [0..5] [previousScrollTop, previousScrollLeft] = [] @@ -2098,16 +2094,14 @@ describe "TextEditorComponent", -> previousScrollLeft = wrapperNode.getScrollLeft() linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 50}, which: 1)) - - waitsForAnimationFrame() for i in [0..5] + waitsForAnimationFrame() for i in [0..5] runs -> expect(wrapperNode.getScrollTop()).toBe(previousScrollTop) expect(wrapperNode.getScrollLeft()).toBeLessThan(previousScrollLeft) linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 10}, which: 1)) - - waitsForAnimationFrame() for i in [0..5] + waitsForAnimationFrame() for i in [0..5] runs -> expect(wrapperNode.getScrollTop()).toBeLessThan(previousScrollTop) @@ -2121,15 +2115,13 @@ describe "TextEditorComponent", -> expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), which: 0)) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1)) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] @@ -2143,8 +2135,7 @@ describe "TextEditorComponent", -> expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] editor.insertText('x') - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[2, 5], [2, 5]] @@ -2154,15 +2145,13 @@ describe "TextEditorComponent", -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([5, 4]), which: 1)) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [5, 4]] editor.delete() - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [2, 4]] @@ -2176,15 +2165,13 @@ describe "TextEditorComponent", -> linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1, metaKey: true)) linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1)) - waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRanges()).toEqual [[[4, 4], [4, 9]], [[2, 4], [6, 8]]] linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([4, 6]), which: 1)) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRanges()).toEqual [[[4, 4], [4, 9]], [[2, 4], [4, 6]]] @@ -2203,8 +2190,7 @@ describe "TextEditorComponent", -> linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 10]), which: 1)) editor.destroy() - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> call.args.pop() for call in window.removeEventListener.calls @@ -2225,8 +2211,7 @@ describe "TextEditorComponent", -> expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [5, 13]] linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1)) - - waitsForAnimationFrame() + waitsForAnimationFrame() maximalScrollTop = null runs -> @@ -2235,8 +2220,7 @@ describe "TextEditorComponent", -> maximalScrollTop = wrapperNode.getScrollTop() linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([9, 3]), which: 1)) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [9, 4]] @@ -2260,8 +2244,7 @@ describe "TextEditorComponent", -> expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [6, 0]] linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1)) - - waitsForAnimationFrame() + waitsForAnimationFrame() maximalScrollTop = null runs -> @@ -2270,8 +2253,7 @@ describe "TextEditorComponent", -> maximalScrollTop = wrapperNode.getScrollTop() linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 4]), which: 1)) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [8, 0]] @@ -2354,7 +2336,6 @@ describe "TextEditorComponent", -> it "selects the rows between the start and end of the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) - waitsForAnimationFrame() runs -> @@ -2369,8 +2350,7 @@ describe "TextEditorComponent", -> runs -> expect(editor.getLastSelection().isReversed()).toBe true gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getLastSelection().isReversed()).toBe false @@ -2385,8 +2365,7 @@ describe "TextEditorComponent", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) - - waitsForAnimationFrame() + waitsForAnimationFrame() maxScrollTop = null runs -> @@ -2394,15 +2373,13 @@ describe "TextEditorComponent", -> maxScrollTop = wrapperNode.getScrollTop() gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(10))) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(wrapperNode.getScrollTop()).toBe maxScrollTop gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(wrapperNode.getScrollTop()).toBeLessThan maxScrollTop @@ -2419,8 +2396,7 @@ describe "TextEditorComponent", -> inputEvent.data = 'x' Object.defineProperty(inputEvent, 'target', get: -> componentNode.querySelector('.hidden-input')) componentNode.dispatchEvent(inputEvent) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[2, 1], [2, 1]] @@ -2436,7 +2412,6 @@ describe "TextEditorComponent", -> it "selects the rows between the start and end of the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - waitsForAnimationFrame() runs -> @@ -2446,7 +2421,6 @@ describe "TextEditorComponent", -> it "merges overlapping selections when the mouse button is released", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - waitsForAnimationFrame() runs -> @@ -2459,7 +2433,6 @@ describe "TextEditorComponent", -> it "selects the rows between the start and end of the drag", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) - waitsForAnimationFrame() runs -> @@ -2469,7 +2442,6 @@ describe "TextEditorComponent", -> it "merges overlapping selections", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) - waitsForAnimationFrame() runs -> @@ -2499,8 +2471,7 @@ describe "TextEditorComponent", -> expect(editor.getSelectedScreenRange()).toEqual [[4, 4], [6, 0]] gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [4, 4]] @@ -2523,15 +2494,13 @@ describe "TextEditorComponent", -> gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), shiftKey: true)) gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) - waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [3, 4]] gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [9, 0]] @@ -2936,8 +2905,7 @@ describe "TextEditorComponent", -> expect(horizontalScrollbarNode.scrollLeft).toBe 0 componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(verticalScrollbarNode.scrollTop).toBe 10 @@ -2952,8 +2920,7 @@ describe "TextEditorComponent", -> expect(horizontalScrollbarNode.scrollLeft).toBe 0 componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(verticalScrollbarNode.scrollTop).toBe 5 @@ -2988,8 +2955,7 @@ describe "TextEditorComponent", -> wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) Object.defineProperty(wheelEvent, 'target', get: -> lineNode) componentNode.dispatchEvent(wheelEvent) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(componentNode.contains(lineNode)).toBe true @@ -3006,8 +2972,7 @@ describe "TextEditorComponent", -> wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 10, wheelDeltaY: 0) Object.defineProperty(wheelEvent, 'target', get: -> lineNode) componentNode.dispatchEvent(wheelEvent) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(component.presenter.mouseWheelScreenRow).toBe null @@ -3054,8 +3019,7 @@ describe "TextEditorComponent", -> wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) Object.defineProperty(wheelEvent, 'target', get: -> lineNumberNode) componentNode.dispatchEvent(wheelEvent) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> expect(componentNode.contains(lineNumberNode)).toBe true @@ -3076,8 +3040,7 @@ describe "TextEditorComponent", -> # scroll to the bottom in one huge event componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -3000)) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> maxScrollTop = wrapperNode.getScrollTop() @@ -3096,8 +3059,7 @@ describe "TextEditorComponent", -> # scroll all the way right componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -3000, wheelDeltaY: 0)) - - waitsForAnimationFrame() + waitsForAnimationFrame() runs -> maxScrollLeft = wrapperNode.getScrollLeft() From 800440d5ca885de4ed4e77c2a2a4988da3c698d7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 26 Oct 2015 15:33:56 -0600 Subject: [PATCH 09/53] Remove logging --- src/text-editor-presenter.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index eabb3ed79..23c9b3be4 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -888,7 +888,6 @@ class TextEditorPresenter @shouldUpdateFocusedState = true @shouldUpdateHiddenInputState = true - console.log 'emitDidUpdateState' @emitDidUpdateState() setScrollTop: (scrollTop, overrideScroll=true) -> From d46091d7e5e4dcf0a737a5cfc08c3028d82b1438 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 26 Oct 2015 15:34:17 -0600 Subject: [PATCH 10/53] Remove dead code --- src/text-editor-presenter.coffee | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 23c9b3be4..c8e6de1e5 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -28,7 +28,6 @@ class TextEditorPresenter @emitter = new Emitter @visibleHighlights = {} @characterWidthsByScope = {} - @rangesByDecorationId = {} @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterNameAndScreenRow = {} @@ -1192,7 +1191,6 @@ class TextEditorPresenter @decorations.push({decoration, range}) updateLineDecorations: -> - @rangesByDecorationId = {} @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterNameAndScreenRow = {} @@ -1216,20 +1214,6 @@ class TextEditorPresenter return - removeFromLineDecorationCaches: (decoration) -> - @removePropertiesFromLineDecorationCaches(decoration.id, decoration.getProperties()) - - removePropertiesFromLineDecorationCaches: (decorationId, decorationProperties) -> - if range = @rangesByDecorationId[decorationId] - delete @rangesByDecorationId[decorationId] - - gutterName = decorationProperties.gutterName - for row in [range.start.row..range.end.row] by 1 - delete @lineDecorationsByScreenRow[row]?[decorationId] - delete @lineNumberDecorationsByScreenRow[row]?[decorationId] - delete @customGutterDecorationsByGutterNameAndScreenRow[gutterName]?[row]?[decorationId] if gutterName - return - addToLineDecorationCaches: (decoration, range) -> marker = decoration.getMarker() properties = decoration.getProperties() @@ -1242,8 +1226,6 @@ class TextEditorPresenter return if properties.onlyEmpty omitLastRow = range.end.column is 0 - @rangesByDecorationId[decoration.id] = range - for row in [range.start.row..range.end.row] by 1 continue if properties.onlyHead and row isnt marker.getHeadScreenPosition().row continue if omitLastRow and row is range.end.row From 92ed7c8b1506893ac4d81ca01e5d9925bb366c3b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 26 Oct 2015 16:29:24 -0600 Subject: [PATCH 11/53] =?UTF-8?q?Don=E2=80=99t=20return=20Decoration=20obj?= =?UTF-8?q?ects=20from=20model=20to=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preparation for LayerDecorations, in which individual decoration objects won’t exist for every marker. --- src/decoration.coffee | 15 +--- src/display-buffer.coffee | 12 +++ src/text-editor-presenter.coffee | 143 +++++++++++++------------------ src/text-editor.coffee | 3 + 4 files changed, 77 insertions(+), 96 deletions(-) diff --git a/src/decoration.coffee b/src/decoration.coffee index a65a417eb..e5d9f5144 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -68,7 +68,6 @@ class Decoration @id = nextId() @setProperties properties @properties.id = @id - @flashQueue = null @destroyed = false @markerDestroyDisposable = @marker.onDidDestroy => @destroy() @@ -167,16 +166,10 @@ class Decoration return false if @properties[key] isnt value true - onDidFlash: (callback) -> - @emitter.on 'did-flash', callback - flash: (klass, duration=500) -> - flashObject = {class: klass, duration} - @flashQueue ?= [] - @flashQueue.push(flashObject) + @properties.flashCount ?= 0 + @properties.flashCount++ + @properties.flashClass = klass + @properties.flashDuration = duration @displayBuffer.scheduleUpdateDecorationsEvent() @emitter.emit 'did-flash' - - consumeNextFlash: -> - return @flashQueue.shift() if @flashQueue?.length > 0 - null diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 68ef0a80b..592601f6a 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -774,6 +774,18 @@ class DisplayBuffer extends Model decorationsByMarkerId[marker.id] = decorations decorationsByMarkerId + decorationStateForScreenRowRange: (startScreenRow, endScreenRow) -> + decorationState = {} + for marker in @findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid() + if decorations = @decorationsByMarkerId[marker.id] + for decoration in decorations + decorationState[decoration.id] = { + properties: decoration.getProperties() + screenRange: marker.getScreenRange() + rangeIsReversed: marker.isReversed() + } + decorationState + decorateMarker: (marker, decorationParams) -> marker = @getMarker(marker.id) decoration = new Decoration(marker, this, decorationParams) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index c8e6de1e5..107594a71 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -623,16 +623,18 @@ class TextEditorPresenter @clearDecorationsForCustomGutterName(gutterName) else @customGutterDecorations[gutterName] = {} - continue if not @gutterIsVisible(gutter) - relevantDecorations = @customGutterDecorationsInRange(gutterName, @startRow, @endRow - 1) - relevantDecorations.forEach (decoration) => - decorationRange = decoration.getMarker().getScreenRange() - @customGutterDecorations[gutterName][decoration.id] = - top: @lineHeight * decorationRange.start.row - height: @lineHeight * decorationRange.getRowCount() - item: decoration.getProperties().item - class: decoration.getProperties().class + continue unless @gutterIsVisible(gutter) + + if customGutterDecorationsByScreenRow = @customGutterDecorationsByGutterNameAndScreenRow[gutterName] + for screenRow in [@startRow..@endRow - 1] + if decorationsById = customGutterDecorationsByScreenRow[screenRow] + for decorationId, {properties, screenRange} of decorationsById + @customGutterDecorations[gutterName][decorationId] ?= + top: @lineHeight * screenRange.start.row + height: @lineHeight * screenRange.getRowCount() + item: properties.item + class: properties.class clearAllCustomGutterDecorations: -> allGutterNames = Object.keys(@customGutterDecorations) @@ -847,32 +849,20 @@ class TextEditorPresenter return null if @model.isMini() decorationClasses = null - for id, decoration of @lineDecorationsByScreenRow[row] + for id, properties of @lineDecorationsByScreenRow[row] decorationClasses ?= [] - decorationClasses.push(decoration.getProperties().class) + decorationClasses.push(properties.class) decorationClasses lineNumberDecorationClassesForRow: (row) -> return null if @model.isMini() decorationClasses = null - for id, decoration of @lineNumberDecorationsByScreenRow[row] + for id, properties of @lineNumberDecorationsByScreenRow[row] decorationClasses ?= [] - decorationClasses.push(decoration.getProperties().class) + decorationClasses.push(properties.class) decorationClasses - # Returns a {Set} of {Decoration}s on the given custom gutter from startRow to endRow (inclusive). - customGutterDecorationsInRange: (gutterName, startRow, endRow) -> - decorations = new Set - - return decorations if @model.isMini() or gutterName is 'line-number' or - not @customGutterDecorationsByGutterNameAndScreenRow[gutterName] - - for screenRow in [@startRow..@endRow - 1] - for id, decoration of @customGutterDecorationsByGutterNameAndScreenRow[gutterName][screenRow] - decorations.add(decoration) - decorations - getCursorBlinkPeriod: -> @cursorBlinkPeriod getCursorBlinkResumeDelay: -> @cursorBlinkResumeDelay @@ -1181,32 +1171,26 @@ class TextEditorPresenter rect fetchDecorations: -> - @decorations = [] - return unless 0 <= @startRow <= @endRow <= Infinity - - for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1) - range = @model.getMarker(markerId).getScreenRange() - for decoration in decorations - @decorations.push({decoration, range}) + @decorations = @model.decorationStateForScreenRowRange(@startRow, @endRow - 1) updateLineDecorations: -> @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterNameAndScreenRow = {} - for {decoration, range} in @decorations - if decoration.isType('line') or decoration.isType('gutter') - @addToLineDecorationCaches(decoration, range) + for decorationId, {properties, screenRange, rangeIsReversed} of @decorations + if Decoration.isType(properties, 'line') or Decoration.isType(properties, 'gutter') + @addToLineDecorationCaches(decorationId, properties, screenRange, rangeIsReversed) return updateHighlightDecorations: -> @visibleHighlights = {} - for {decoration, range} in @decorations - if decoration.isType('highlight') - @updateHighlightState(decoration, range) + for decorationId, {properties, screenRange} of @decorations + if Decoration.isType(properties, 'highlight') + @updateHighlightState(decorationId, properties, screenRange) for tileId, tileState of @state.content.tiles for id, highlight of tileState.highlights @@ -1214,34 +1198,35 @@ class TextEditorPresenter return - addToLineDecorationCaches: (decoration, range) -> - marker = decoration.getMarker() - properties = decoration.getProperties() - - return unless marker.isValid() - - if range.isEmpty() + addToLineDecorationCaches: (decorationId, properties, screenRange, rangeIsReversed) -> + if screenRange.isEmpty() return if properties.onlyNonEmpty else return if properties.onlyEmpty - omitLastRow = range.end.column is 0 + omitLastRow = screenRange.end.column is 0 - for row in [range.start.row..range.end.row] by 1 - continue if properties.onlyHead and row isnt marker.getHeadScreenPosition().row - continue if omitLastRow and row is range.end.row + if rangeIsReversed + headPosition = screenRange.start + else + headPosition = screenRange.end - if decoration.isType('line') + for row in [screenRange.start.row..screenRange.end.row] by 1 + continue if properties.onlyHead and row isnt headPosition.row + continue if omitLastRow and row is screenRange.end.row + + if Decoration.isType(properties, 'line') @lineDecorationsByScreenRow[row] ?= {} - @lineDecorationsByScreenRow[row][decoration.id] = decoration + @lineDecorationsByScreenRow[row][decorationId] = properties - if decoration.isType('line-number') + if Decoration.isType(properties, 'line-number') @lineNumberDecorationsByScreenRow[row] ?= {} - @lineNumberDecorationsByScreenRow[row][decoration.id] = decoration - else if decoration.isType('gutter') - gutterName = decoration.getProperties().gutterName + @lineNumberDecorationsByScreenRow[row][decorationId] = properties + + else if Decoration.isType(properties, 'gutter') + gutterName = properties.gutterName @customGutterDecorationsByGutterNameAndScreenRow[gutterName] ?= {} @customGutterDecorationsByGutterNameAndScreenRow[gutterName][row] ?= {} - @customGutterDecorationsByGutterNameAndScreenRow[gutterName][row][decoration.id] = decoration + @customGutterDecorationsByGutterNameAndScreenRow[gutterName][row][decorationId] = {properties, screenRange} return @@ -1261,46 +1246,34 @@ class TextEditorPresenter intersectingRange - updateHighlightState: (decoration, range) -> + updateHighlightState: (decorationId, properties, screenRange) -> return unless @startRow? and @endRow? and @lineHeight? and @hasPixelPositionRequirements() - properties = decoration.getProperties() - marker = decoration.getMarker() + return if screenRange.isEmpty() - if decoration.isDestroyed() or not marker.isValid() or range.isEmpty() or not range.intersectsRowRange(@startRow, @endRow - 1) - return + if screenRange.start.row < @startRow + screenRange.start.row = @startRow + screenRange.start.column = 0 + if screenRange.end.row >= @endRow + screenRange.end.row = @endRow + screenRange.end.column = 0 - if range.start.row < @startRow - range.start.row = @startRow - range.start.column = 0 - if range.end.row >= @endRow - range.end.row = @endRow - range.end.column = 0 + return if screenRange.isEmpty() - return if range.isEmpty() - - flash = decoration.consumeNextFlash() - - startTile = @tileForRow(range.start.row) - endTile = @tileForRow(range.end.row) + startTile = @tileForRow(screenRange.start.row) + endTile = @tileForRow(screenRange.end.row) for tileStartRow in [startTile..endTile] by @tileSize - rangeWithinTile = @intersectRangeWithTile(range, tileStartRow) + rangeWithinTile = @intersectRangeWithTile(screenRange, tileStartRow) continue if rangeWithinTile.isEmpty() tileState = @state.content.tiles[tileStartRow] ?= {highlights: {}} - highlightState = tileState.highlights[decoration.id] ?= { - flashCount: 0 - flashDuration: null - flashClass: null - } - - if flash? - highlightState.flashCount++ - highlightState.flashClass = flash.class - highlightState.flashDuration = flash.duration + highlightState = tileState.highlights[decorationId] ?= {} + highlightState.flashCount = properties.flashCount + highlightState.flashClass = properties.flashClass + highlightState.flashDuration = properties.flashDuration highlightState.class = properties.class highlightState.deprecatedRegionClass = properties.deprecatedRegionClass highlightState.regions = @buildHighlightRegions(rangeWithinTile) @@ -1309,7 +1282,7 @@ class TextEditorPresenter @repositionRegionWithinTile(region, tileStartRow) @visibleHighlights[tileStartRow] ?= {} - @visibleHighlights[tileStartRow][decoration.id] = true + @visibleHighlights[tileStartRow][decorationId] = true true diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 6fe9c2586..8c3900584 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1478,6 +1478,9 @@ class TextEditor extends Model decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> @displayBuffer.decorationsForScreenRowRange(startScreenRow, endScreenRow) + decorationStateForScreenRowRange: (startScreenRow, endScreenRow) -> + @displayBuffer.decorationStateForScreenRowRange(startScreenRow, endScreenRow) + # Extended: Get all decorations. # # * `propertyFilter` (optional) An {Object} containing key value pairs that From d7b0ab9179aacef3350f74cafb1008e9e1ec22f6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 26 Oct 2015 16:55:57 -0600 Subject: [PATCH 12/53] Simplify updating of custom gutter decoration state --- src/text-editor-presenter.coffee | 35 ++++++++++++++------------------ 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 107594a71..cb3024494 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -30,7 +30,7 @@ class TextEditorPresenter @characterWidthsByScope = {} @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} - @customGutterDecorationsByGutterNameAndScreenRow = {} + @customGutterDecorationsByGutterName = {} @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @@ -625,16 +625,12 @@ class TextEditorPresenter @customGutterDecorations[gutterName] = {} continue unless @gutterIsVisible(gutter) - - if customGutterDecorationsByScreenRow = @customGutterDecorationsByGutterNameAndScreenRow[gutterName] - for screenRow in [@startRow..@endRow - 1] - if decorationsById = customGutterDecorationsByScreenRow[screenRow] - for decorationId, {properties, screenRange} of decorationsById - @customGutterDecorations[gutterName][decorationId] ?= - top: @lineHeight * screenRange.start.row - height: @lineHeight * screenRange.getRowCount() - item: properties.item - class: properties.class + for decorationId, {properties, screenRange} of @customGutterDecorationsByGutterName[gutterName] + @customGutterDecorations[gutterName][decorationId] = + top: @lineHeight * screenRange.start.row + height: @lineHeight * screenRange.getRowCount() + item: properties.item + class: properties.class clearAllCustomGutterDecorations: -> allGutterNames = Object.keys(@customGutterDecorations) @@ -1177,12 +1173,17 @@ class TextEditorPresenter updateLineDecorations: -> @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} - @customGutterDecorationsByGutterNameAndScreenRow = {} + @customGutterDecorationsByGutterName = {} - for decorationId, {properties, screenRange, rangeIsReversed} of @decorations - if Decoration.isType(properties, 'line') or Decoration.isType(properties, 'gutter') + for decorationId, decorationState of @decorations + {properties, screenRange, rangeIsReversed} = decorationState + if Decoration.isType(properties, 'line') or Decoration.isType(properties, 'line-number') @addToLineDecorationCaches(decorationId, properties, screenRange, rangeIsReversed) + else if Decoration.isType(properties, 'gutter') and properties.gutterName? + @customGutterDecorationsByGutterName[properties.gutterName] ?= {} + @customGutterDecorationsByGutterName[properties.gutterName][decorationId] = decorationState + return updateHighlightDecorations: -> @@ -1222,12 +1223,6 @@ class TextEditorPresenter @lineNumberDecorationsByScreenRow[row] ?= {} @lineNumberDecorationsByScreenRow[row][decorationId] = properties - else if Decoration.isType(properties, 'gutter') - gutterName = properties.gutterName - @customGutterDecorationsByGutterNameAndScreenRow[gutterName] ?= {} - @customGutterDecorationsByGutterNameAndScreenRow[gutterName][row] ?= {} - @customGutterDecorationsByGutterNameAndScreenRow[gutterName][row][decorationId] = {properties, screenRange} - return intersectRangeWithTile: (range, tileStartRow) -> From 9d6168aac8d760a2d080e4a4df0726e74909c205 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 26 Oct 2015 19:02:11 -0600 Subject: [PATCH 13/53] Fix gutter spec indentation --- spec/text-editor-spec.coffee | 165 ++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 82 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 552a0ee7c..2bc390043 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5500,101 +5500,102 @@ describe "TextEditor", -> it "does not allow a custom gutter with the 'line-number' name.", -> expect(editor.addGutter.bind(editor, {name: 'line-number'})).toThrow() - describe '::decorateMarker', -> - [marker] = [] + describe '::decorateMarker', -> + [marker] = [] - beforeEach -> - marker = editor.markBufferRange([[1, 0], [1, 0]]) + beforeEach -> + marker = editor.markBufferRange([[1, 0], [1, 0]]) - it 'reflects an added decoration when one of its custom gutters is decorated.', -> - gutter = editor.addGutter {'name': 'custom-gutter'} - decoration = gutter.decorateMarker marker, {class: 'custom-class'} - gutterDecorations = editor.getDecorations - type: 'gutter' - gutterName: 'custom-gutter' - class: 'custom-class' - expect(gutterDecorations.length).toBe 1 - expect(gutterDecorations[0]).toBe decoration + it 'reflects an added decoration when one of its custom gutters is decorated.', -> + gutter = editor.addGutter {'name': 'custom-gutter'} + decoration = gutter.decorateMarker marker, {class: 'custom-class'} + gutterDecorations = editor.getDecorations + type: 'gutter' + gutterName: 'custom-gutter' + class: 'custom-class' + expect(gutterDecorations.length).toBe 1 + expect(gutterDecorations[0]).toBe decoration - it 'reflects an added decoration when its line-number gutter is decorated.', -> - decoration = editor.gutterWithName('line-number').decorateMarker marker, {class: 'test-class'} - gutterDecorations = editor.getDecorations - type: 'line-number' - gutterName: 'line-number' - class: 'test-class' - expect(gutterDecorations.length).toBe 1 - expect(gutterDecorations[0]).toBe decoration + it 'reflects an added decoration when its line-number gutter is decorated.', -> + decoration = editor.gutterWithName('line-number').decorateMarker marker, {class: 'test-class'} + gutterDecorations = editor.getDecorations + type: 'line-number' + gutterName: 'line-number' + class: 'test-class' + expect(gutterDecorations.length).toBe 1 + expect(gutterDecorations[0]).toBe decoration - describe '::observeGutters', -> - [payloads, callback] = [] + describe '::observeGutters', -> + [payloads, callback] = [] - beforeEach -> - payloads = [] - callback = (payload) -> - payloads.push(payload) + beforeEach -> + payloads = [] + callback = (payload) -> + payloads.push(payload) - it 'calls the callback immediately with each existing gutter, and with each added gutter after that.', -> - lineNumberGutter = editor.gutterWithName('line-number') - editor.observeGutters(callback) - expect(payloads).toEqual [lineNumberGutter] - gutter1 = editor.addGutter({name: 'test-gutter-1'}) - expect(payloads).toEqual [lineNumberGutter, gutter1] - gutter2 = editor.addGutter({name: 'test-gutter-2'}) - expect(payloads).toEqual [lineNumberGutter, gutter1, gutter2] + it 'calls the callback immediately with each existing gutter, and with each added gutter after that.', -> + lineNumberGutter = editor.gutterWithName('line-number') + editor.observeGutters(callback) + expect(payloads).toEqual [lineNumberGutter] + gutter1 = editor.addGutter({name: 'test-gutter-1'}) + expect(payloads).toEqual [lineNumberGutter, gutter1] + gutter2 = editor.addGutter({name: 'test-gutter-2'}) + expect(payloads).toEqual [lineNumberGutter, gutter1, gutter2] - it 'does not call the callback when a gutter is removed.', -> - gutter = editor.addGutter({name: 'test-gutter'}) - editor.observeGutters(callback) - payloads = [] - gutter.destroy() - expect(payloads).toEqual [] + it 'does not call the callback when a gutter is removed.', -> + gutter = editor.addGutter({name: 'test-gutter'}) + editor.observeGutters(callback) + payloads = [] + gutter.destroy() + expect(payloads).toEqual [] - it 'does not call the callback after the subscription has been disposed.', -> - subscription = editor.observeGutters(callback) - payloads = [] - subscription.dispose() - editor.addGutter({name: 'test-gutter'}) - expect(payloads).toEqual [] + it 'does not call the callback after the subscription has been disposed.', -> + subscription = editor.observeGutters(callback) + payloads = [] + subscription.dispose() + editor.addGutter({name: 'test-gutter'}) + expect(payloads).toEqual [] - describe '::onDidAddGutter', -> - [payloads, callback] = [] + describe '::onDidAddGutter', -> + [payloads, callback] = [] - beforeEach -> - payloads = [] - callback = (payload) -> - payloads.push(payload) + beforeEach -> + payloads = [] + callback = (payload) -> + payloads.push(payload) - it 'calls the callback with each newly-added gutter, but not with existing gutters.', -> - editor.onDidAddGutter(callback) - expect(payloads).toEqual [] - gutter = editor.addGutter({name: 'test-gutter'}) - expect(payloads).toEqual [gutter] + it 'calls the callback with each newly-added gutter, but not with existing gutters.', -> + editor.onDidAddGutter(callback) + expect(payloads).toEqual [] + gutter = editor.addGutter({name: 'test-gutter'}) + expect(payloads).toEqual [gutter] - it 'does not call the callback after the subscription has been disposed.', -> - subscription = editor.onDidAddGutter(callback) - payloads = [] - subscription.dispose() - editor.addGutter({name: 'test-gutter'}) - expect(payloads).toEqual [] + it 'does not call the callback after the subscription has been disposed.', -> + subscription = editor.onDidAddGutter(callback) + payloads = [] + subscription.dispose() + editor.addGutter({name: 'test-gutter'}) + expect(payloads).toEqual [] - describe '::onDidRemoveGutter', -> - [payloads, callback] = [] + describe '::onDidRemoveGutter', -> + [payloads, callback] = [] - beforeEach -> - payloads = [] - callback = (payload) -> - payloads.push(payload) + beforeEach -> + payloads = [] + callback = (payload) -> + payloads.push(payload) - it 'calls the callback when a gutter is removed.', -> - gutter = editor.addGutter({name: 'test-gutter'}) - editor.onDidRemoveGutter(callback) - expect(payloads).toEqual [] - gutter.destroy() - expect(payloads).toEqual ['test-gutter'] + it 'calls the callback when a gutter is removed.', -> + gutter = editor.addGutter({name: 'test-gutter'}) + editor.onDidRemoveGutter(callback) + expect(payloads).toEqual [] + gutter.destroy() + expect(payloads).toEqual ['test-gutter'] + + it 'does not call the callback after the subscription has been disposed.', -> + gutter = editor.addGutter({name: 'test-gutter'}) + subscription = editor.onDidRemoveGutter(callback) + subscription.dispose() + gutter.destroy() + expect(payloads).toEqual [] - it 'does not call the callback after the subscription has been disposed.', -> - gutter = editor.addGutter({name: 'test-gutter'}) - subscription = editor.onDidRemoveGutter(callback) - subscription.dispose() - gutter.destroy() - expect(payloads).toEqual [] From 7d22ed788c37caa85ba8199f0a42f8bd9964baa6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 26 Oct 2015 19:03:19 -0600 Subject: [PATCH 14/53] Test decorateMarker + decorationStateForScreenRowRange --- spec/text-editor-spec.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 2bc390043..e7dd75618 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5599,3 +5599,13 @@ describe "TextEditor", -> gutter.destroy() expect(payloads).toEqual [] + describe "decorations", -> + describe "::decorateMarker", -> + it "includes the decoration in the object returned from ::decorationStateForScreenRowRange", -> + marker = editor.markBufferRange([[2, 4], [6, 8]]) + decoration = editor.decorateMarker(marker, type: 'highlight', class: 'foo') + expect(editor.decorationStateForScreenRowRange(0, 5)[decoration.id]).toEqual { + properties: {type: 'highlight', class: 'foo', id: decoration.id} + screenRange: marker.getScreenRange(), + rangeIsReversed: false, + } From 3a25fe49752f6d06f6c48ddc267ddf2c1822d1ca Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 26 Oct 2015 19:08:02 -0600 Subject: [PATCH 15/53] Remove id from decoration properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It doesn’t make sense to include this when we’ll reuse the same properties for multiple decoration instances when decorating marker layers. --- spec/display-buffer-spec.coffee | 2 +- spec/text-editor-spec.coffee | 2 +- src/decoration.coffee | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index ded3ed455..accda876a 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -1244,7 +1244,7 @@ describe "DisplayBuffer", -> {oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0] expect(oldProperties).toEqual decorationProperties - expect(newProperties).toEqual type: 'line-number', gutterName: 'line-number', class: 'two', id: decoration.id + expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'} describe "::getDecorations(properties)", -> it "returns decorations matching the given optional properties", -> diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index e7dd75618..d999d76c0 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5605,7 +5605,7 @@ describe "TextEditor", -> marker = editor.markBufferRange([[2, 4], [6, 8]]) decoration = editor.decorateMarker(marker, type: 'highlight', class: 'foo') expect(editor.decorationStateForScreenRowRange(0, 5)[decoration.id]).toEqual { - properties: {type: 'highlight', class: 'foo', id: decoration.id} + properties: {type: 'highlight', class: 'foo'} screenRange: marker.getScreenRange(), rangeIsReversed: false, } diff --git a/src/decoration.coffee b/src/decoration.coffee index e5d9f5144..937909ec7 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -67,7 +67,6 @@ class Decoration @emitter = new Emitter @id = nextId() @setProperties properties - @properties.id = @id @destroyed = false @markerDestroyDisposable = @marker.onDidDestroy => @destroy() @@ -150,7 +149,6 @@ class Decoration return if @destroyed oldProperties = @properties @properties = translateDecorationParamsOldToNew(newProperties) - @properties.id = @id if newProperties.type? @displayBuffer.decorationDidChangeType(this) @displayBuffer.scheduleUpdateDecorationsEvent() From 20f4c613256439962905eaba22c635cad729ffad Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 26 Oct 2015 19:11:20 -0600 Subject: [PATCH 16/53] Drop stray sample.js file --- spec/sample.js | 1 - 1 file changed, 1 deletion(-) delete mode 100644 spec/sample.js diff --git a/spec/sample.js b/spec/sample.js deleted file mode 100644 index 66dc9051d..000000000 --- a/spec/sample.js +++ /dev/null @@ -1 +0,0 @@ -undefined \ No newline at end of file From acf142863c96e8658a0c23b63bbe91a39eb9b63d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 27 Oct 2015 14:05:22 -0600 Subject: [PATCH 17/53] Add TextEditor::decorateMarkerLayer --- spec/text-editor-spec.coffee | 63 +++++++++++++++++++++++++++++++++++- src/display-buffer.coffee | 40 +++++++++++++++++++++-- src/layer-decoration.coffee | 32 ++++++++++++++++++ src/text-editor.coffee | 3 ++ 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/layer-decoration.coffee diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index d999d76c0..0e97121d3 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5607,5 +5607,66 @@ describe "TextEditor", -> expect(editor.decorationStateForScreenRowRange(0, 5)[decoration.id]).toEqual { properties: {type: 'highlight', class: 'foo'} screenRange: marker.getScreenRange(), - rangeIsReversed: false, + rangeIsReversed: false + } + + describe "::decorateMarkerLayer", -> + it "based on the markers in the layer, includes multiple decoration objects with the same properties and different ranges in the object returned from ::decorationStateForScreenRowRange", -> + layer1 = editor.getBuffer().addMarkerLayer() + marker1 = layer1.markRange([[2, 4], [6, 8]]) + marker2 = layer1.markRange([[11, 0], [11, 12]]) + layer2 = editor.getBuffer().addMarkerLayer() + marker3 = layer2.markRange([[8, 0], [9, 0]]) + + layer1Decoration1 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'foo') + layer1Decoration2 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'bar') + layer2Decoration = editor.decorateMarkerLayer(layer2, type: 'highlight', class: 'baz') + + decorationState = editor.decorationStateForScreenRowRange(0, 13) + + expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toEqual { + properties: {type: 'highlight', class: 'foo'}, + screenRange: marker1.getRange(), + rangeIsReversed: false + } + expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toEqual { + properties: {type: 'highlight', class: 'foo'}, + screenRange: marker2.getRange(), + rangeIsReversed: false + } + expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker1.getRange(), + rangeIsReversed: false + } + expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual { + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker2.getRange(), + rangeIsReversed: false + } + expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual { + properties: {type: 'highlight', class: 'baz'}, + screenRange: marker3.getRange(), + rangeIsReversed: false + } + + layer1Decoration1.destroy() + + decorationState = editor.decorationStateForScreenRowRange(0, 12) + expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toBeUndefined() + expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toBeUndefined() + expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker1.getRange(), + rangeIsReversed: false + } + expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual { + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker2.getRange(), + rangeIsReversed: false + } + expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual { + properties: {type: 'highlight', class: 'baz'}, + screenRange: marker3.getRange(), + rangeIsReversed: false } diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 592601f6a..a1609ab62 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -7,6 +7,7 @@ Fold = require './fold' Model = require './model' Token = require './token' Decoration = require './decoration' +LayerDecoration = require './layer-decoration' Marker = require './marker' class BufferToScreenConversionError extends Error @@ -57,6 +58,7 @@ class DisplayBuffer extends Model @decorationsById = {} @decorationsByMarkerId = {} @overlayDecorationsById = {} + @layerDecorationsByMarkerLayerId = {} @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings @disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange @disposables.add @buffer.onDidCreateMarker @handleBufferMarkerCreated @@ -776,14 +778,31 @@ class DisplayBuffer extends Model decorationStateForScreenRowRange: (startScreenRow, endScreenRow) -> decorationState = {} - for marker in @findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid() + + startBufferRow = @bufferRowForScreenRow(startScreenRow) + endBufferRow = @bufferRowForScreenRow(endScreenRow) + + defaultLayer = @buffer.getDefaultMarkerLayer() + for marker in defaultLayer.findMarkers(intersectsRowRange: [startBufferRow, endBufferRow]) when marker.isValid() if decorations = @decorationsByMarkerId[marker.id] for decoration in decorations decorationState[decoration.id] = { properties: decoration.getProperties() - screenRange: marker.getScreenRange() + screenRange: @screenRangeForBufferRange(marker.getRange()) rangeIsReversed: marker.isReversed() } + + for markerLayerId, layerDecorations of @layerDecorationsByMarkerLayerId + markerLayer = @buffer.getMarkerLayer(markerLayerId) + for marker in markerLayer.findMarkers(intersectsRowRange: [startBufferRow, endBufferRow]) when marker.isValid() + screenRange = @screenRangeForBufferRange(marker.getRange()) + rangeIsReversed = marker.isReversed() + for layerDecoration in layerDecorations + decorationState["#{layerDecoration.id}-#{marker.id}"] = { + properties: layerDecoration.getProperties() + screenRange, rangeIsReversed + } + decorationState decorateMarker: (marker, decorationParams) -> @@ -797,6 +816,13 @@ class DisplayBuffer extends Model @emitter.emit 'did-add-decoration', decoration decoration + decorateMarkerLayer: (markerLayer, decorationParams) -> + decoration = new LayerDecoration(markerLayer, this, decorationParams) + @layerDecorationsByMarkerLayerId[markerLayer.id] ?= [] + @layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration) + @scheduleUpdateDecorationsEvent() + decoration + decorationsForMarkerId: (markerId) -> @decorationsByMarkerId[markerId] @@ -1117,6 +1143,16 @@ class DisplayBuffer extends Model delete @overlayDecorationsById[decoration.id] @scheduleUpdateDecorationsEvent() + didDestroyLayerDecoration: (decoration) -> + {markerLayer} = decoration + return unless decorations = @layerDecorationsByMarkerLayerId[markerLayer.id] + index = decorations.indexOf(decoration) + + if index > -1 + decorations.splice(index, 1) + delete @layerDecorationsByMarkerLayerId[markerLayer.id] if decorations.length is 0 + @scheduleUpdateDecorationsEvent() + checkScreenLinesInvariant: -> return if @isSoftWrapped() return if _.size(@foldsByMarkerId) > 0 diff --git a/src/layer-decoration.coffee b/src/layer-decoration.coffee new file mode 100644 index 000000000..2906b7852 --- /dev/null +++ b/src/layer-decoration.coffee @@ -0,0 +1,32 @@ +_ = require 'underscore-plus' + +idCounter = 0 +nextId = -> idCounter++ + +module.exports = +class LayerDecoration + constructor: (@markerLayer, @displayBuffer, @properties) -> + @id = nextId() + @destroyed = false + @markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy() + + destroy: -> + return if @destroyed + @markerLayerDestroyedDisposable.dispose() + @markerLayerDestroyedDisposable = null + @destroyed = true + @displayBuffer.didDestroyLayerDecoration(this) + + isDestroyed: -> @destroyed + + getId: -> @id + + getMarkerLayer: -> @markerLayer + + getProperties: -> + @properties + + setProperties: (newProperties) -> + return if @destroyed + @properties = newProperties + @displayBuffer.scheduleUpdateDecorationsEvent() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 8c3900584..5d4d7bba4 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1465,6 +1465,9 @@ class TextEditor extends Model decorateMarker: (marker, decorationParams) -> @displayBuffer.decorateMarker(marker, decorationParams) + decorateMarkerLayer: (markerLayer, decorationParams) -> + @displayBuffer.decorateMarkerLayer(markerLayer, decorationParams) + # Essential: Get all the decorations within a screen row range. # # * `startScreenRow` the {Number} beginning screen row From 75d0a0820c939a879cc3ae15fc27cf9e491eed98 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 28 Oct 2015 15:07:21 -0600 Subject: [PATCH 18/53] Allow properties to be overridden for a single marker in LayerDecoration --- spec/text-editor-spec.coffee | 16 ++++++++++++++++ src/display-buffer.coffee | 4 ++-- src/layer-decoration.coffee | 9 +++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 0e97121d3..0bd5484d6 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5670,3 +5670,19 @@ describe "TextEditor", -> screenRange: marker3.getRange(), rangeIsReversed: false } + + layer1Decoration2.setPropertiesForMarker(marker1, {type: 'highlight', class: 'quux'}) + decorationState = editor.decorationStateForScreenRowRange(0, 12) + expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { + properties: {type: 'highlight', class: 'quux'}, + screenRange: marker1.getRange(), + rangeIsReversed: false + } + + layer1Decoration2.setPropertiesForMarker(marker1, null) + decorationState = editor.decorationStateForScreenRowRange(0, 12) + expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker1.getRange(), + rangeIsReversed: false + } diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index a1609ab62..e547fd31f 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -787,7 +787,7 @@ class DisplayBuffer extends Model if decorations = @decorationsByMarkerId[marker.id] for decoration in decorations decorationState[decoration.id] = { - properties: decoration.getProperties() + properties: decoration.properties screenRange: @screenRangeForBufferRange(marker.getRange()) rangeIsReversed: marker.isReversed() } @@ -799,7 +799,7 @@ class DisplayBuffer extends Model rangeIsReversed = marker.isReversed() for layerDecoration in layerDecorations decorationState["#{layerDecoration.id}-#{marker.id}"] = { - properties: layerDecoration.getProperties() + properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties screenRange, rangeIsReversed } diff --git a/src/layer-decoration.coffee b/src/layer-decoration.coffee index 2906b7852..a33c9e1a3 100644 --- a/src/layer-decoration.coffee +++ b/src/layer-decoration.coffee @@ -9,6 +9,7 @@ class LayerDecoration @id = nextId() @destroyed = false @markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy() + @overridePropertiesByMarkerId = {} destroy: -> return if @destroyed @@ -30,3 +31,11 @@ class LayerDecoration return if @destroyed @properties = newProperties @displayBuffer.scheduleUpdateDecorationsEvent() + + setPropertiesForMarker: (marker, properties) -> + return if @destroyed + if properties? + @overridePropertiesByMarkerId[marker.id] = properties + else + delete @overridePropertiesByMarkerId[marker.id] + @displayBuffer.scheduleUpdateDecorationsEvent() From a3ff0ad75a60a528726a019931b7c9fa8b2e8018 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 28 Oct 2015 15:08:50 -0600 Subject: [PATCH 19/53] :art: Rename method --- spec/text-editor-spec.coffee | 14 +++++++------- src/display-buffer.coffee | 2 +- src/text-editor-presenter.coffee | 2 +- src/text-editor.coffee | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 0bd5484d6..95f40f3ff 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5601,17 +5601,17 @@ describe "TextEditor", -> describe "decorations", -> describe "::decorateMarker", -> - it "includes the decoration in the object returned from ::decorationStateForScreenRowRange", -> + it "includes the decoration in the object returned from ::decorationsStateForScreenRowRange", -> marker = editor.markBufferRange([[2, 4], [6, 8]]) decoration = editor.decorateMarker(marker, type: 'highlight', class: 'foo') - expect(editor.decorationStateForScreenRowRange(0, 5)[decoration.id]).toEqual { + expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual { properties: {type: 'highlight', class: 'foo'} screenRange: marker.getScreenRange(), rangeIsReversed: false } describe "::decorateMarkerLayer", -> - it "based on the markers in the layer, includes multiple decoration objects with the same properties and different ranges in the object returned from ::decorationStateForScreenRowRange", -> + it "based on the markers in the layer, includes multiple decoration objects with the same properties and different ranges in the object returned from ::decorationsStateForScreenRowRange", -> layer1 = editor.getBuffer().addMarkerLayer() marker1 = layer1.markRange([[2, 4], [6, 8]]) marker2 = layer1.markRange([[11, 0], [11, 12]]) @@ -5622,7 +5622,7 @@ describe "TextEditor", -> layer1Decoration2 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'bar') layer2Decoration = editor.decorateMarkerLayer(layer2, type: 'highlight', class: 'baz') - decorationState = editor.decorationStateForScreenRowRange(0, 13) + decorationState = editor.decorationsStateForScreenRowRange(0, 13) expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toEqual { properties: {type: 'highlight', class: 'foo'}, @@ -5652,7 +5652,7 @@ describe "TextEditor", -> layer1Decoration1.destroy() - decorationState = editor.decorationStateForScreenRowRange(0, 12) + decorationState = editor.decorationsStateForScreenRowRange(0, 12) expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toBeUndefined() expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toBeUndefined() expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { @@ -5672,7 +5672,7 @@ describe "TextEditor", -> } layer1Decoration2.setPropertiesForMarker(marker1, {type: 'highlight', class: 'quux'}) - decorationState = editor.decorationStateForScreenRowRange(0, 12) + decorationState = editor.decorationsStateForScreenRowRange(0, 12) expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { properties: {type: 'highlight', class: 'quux'}, screenRange: marker1.getRange(), @@ -5680,7 +5680,7 @@ describe "TextEditor", -> } layer1Decoration2.setPropertiesForMarker(marker1, null) - decorationState = editor.decorationStateForScreenRowRange(0, 12) + decorationState = editor.decorationsStateForScreenRowRange(0, 12) expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { properties: {type: 'highlight', class: 'bar'}, screenRange: marker1.getRange(), diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index e547fd31f..331f50fa2 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -776,7 +776,7 @@ class DisplayBuffer extends Model decorationsByMarkerId[marker.id] = decorations decorationsByMarkerId - decorationStateForScreenRowRange: (startScreenRow, endScreenRow) -> + decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) -> decorationState = {} startBufferRow = @bufferRowForScreenRow(startScreenRow) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index cb3024494..891252730 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1168,7 +1168,7 @@ class TextEditorPresenter fetchDecorations: -> return unless 0 <= @startRow <= @endRow <= Infinity - @decorations = @model.decorationStateForScreenRowRange(@startRow, @endRow - 1) + @decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1) updateLineDecorations: -> @lineDecorationsByScreenRow = {} diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 5d4d7bba4..81e2f9ecc 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1481,8 +1481,8 @@ class TextEditor extends Model decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> @displayBuffer.decorationsForScreenRowRange(startScreenRow, endScreenRow) - decorationStateForScreenRowRange: (startScreenRow, endScreenRow) -> - @displayBuffer.decorationStateForScreenRowRange(startScreenRow, endScreenRow) + decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) -> + @displayBuffer.decorationsStateForScreenRowRange(startScreenRow, endScreenRow) # Extended: Get all decorations. # From 4139863ceef6d02b4df5d165ebd89bd7ef1c9552 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 28 Oct 2015 16:00:09 -0600 Subject: [PATCH 20/53] :arrow_up: text-buffer (pre-release) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c246678ed..551f91eeb 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "^7.2.0-pre-marker-layers.1", + "text-buffer": "^7.2.0-pre-marker-layers.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From 78513f45289ddd9c9e3f80e8af750e544a60efd7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 29 Oct 2015 07:30:31 -0600 Subject: [PATCH 21/53] Remove duplicated method definition --- src/text-editor.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 81e2f9ecc..9273841eb 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -436,9 +436,6 @@ class TextEditor extends Model onDidRemoveDecoration: (callback) -> @displayBuffer.onDidRemoveDecoration(callback) - onDidUpdateDecorations: (callback) -> - @displayBuffer.onDidUpdateDecorations(callback) - # Extended: Calls your `callback` when the placeholder text is changed. # # * `callback` {Function} From 205b6bf66c366b9ab40e60ac5685140be3a0f845 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 29 Oct 2015 07:46:53 -0600 Subject: [PATCH 22/53] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 551f91eeb..4b406fef9 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "^7.2.0-pre-marker-layers.2", + "text-buffer": "^7.2.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From b508cc6d9f62c837d18dce97a0bb0ae183267ec4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 29 Oct 2015 08:05:21 -0600 Subject: [PATCH 23/53] Fix linter error --- spec/text-editor-component-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 84534acf8..3faf2a468 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -1666,7 +1666,7 @@ describe "TextEditorComponent", -> runs -> expect(highlightNode.classList.contains('flash-class')).toBe true - waitsFor -> !highlightNode.classList.contains('flash-class') + waitsFor -> not highlightNode.classList.contains('flash-class') describe "when ::flash is called again before the first has finished", -> it "removes the class from the decoration highlight before adding it for the second ::flash call", -> From b1a5b58fa283923dc222d64ca3bf968ba1b9576f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 29 Oct 2015 14:32:21 -0600 Subject: [PATCH 24/53] Rename Marker to TextEditorMarker to resolve ambiguity w/ TextBuffer API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We expose both kinds of markers in Atom, and the docs were actually wrong when we refer to Markers from TextBuffer because it linked to the TextEditor layer’s Marker implementation. This will clarify the difference. --- src/cursor.coffee | 4 +- src/decoration.coffee | 6 +-- src/display-buffer.coffee | 22 ++++---- src/gutter.coffee | 4 +- ...arker.coffee => text-editor-marker.coffee} | 26 +++++----- src/text-editor.coffee | 52 +++++++++---------- 6 files changed, 57 insertions(+), 57 deletions(-) rename src/{marker.coffee => text-editor-marker.coffee} (94%) diff --git a/src/cursor.coffee b/src/cursor.coffee index 40cde4aca..0f87c2760 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -7,7 +7,7 @@ Model = require './model' # where text can be inserted. # # Cursors belong to {TextEditor}s and have some metadata attached in the form -# of a {Marker}. +# of a {TextEditorMarker}. module.exports = class Cursor extends Model screenPosition: null @@ -127,7 +127,7 @@ class Cursor extends Model Section: Cursor Position Details ### - # Public: Returns the underlying {Marker} for the cursor. + # Public: Returns the underlying {TextEditorMarker} for the cursor. # Useful with overlay {Decoration}s. getMarker: -> @marker diff --git a/src/decoration.coffee b/src/decoration.coffee index 937909ec7..f57d234d1 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -11,7 +11,7 @@ translateDecorationParamsOldToNew = (decorationParams) -> decorationParams.gutterName = 'line-number' decorationParams -# Essential: Represents a decoration that follows a {Marker}. A decoration is +# Essential: Represents a decoration that follows a {TextEditorMarker}. A decoration is # basically a visual representation of a marker. It allows you to add CSS # classes to line numbers in the gutter, lines, and add selection-line regions # around marked ranges of text. @@ -25,7 +25,7 @@ translateDecorationParamsOldToNew = (decorationParams) -> # decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'}) # ``` # -# Best practice for destroying the decoration is by destroying the {Marker}. +# Best practice for destroying the decoration is by destroying the {TextEditorMarker}. # # ```coffee # marker.destroy() @@ -72,7 +72,7 @@ class Decoration # Essential: Destroy this marker. # - # If you own the marker, you should use {Marker::destroy} which will destroy + # If you own the marker, you should use {TextEditorMarker::destroy} which will destroy # this decoration. destroy: -> return if @destroyed diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 331f50fa2..203d3360d 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -8,7 +8,7 @@ Model = require './model' Token = require './token' Decoration = require './decoration' LayerDecoration = require './layer-decoration' -Marker = require './marker' +TextEditorMarker = require './text-editor-marker' class BufferToScreenConversionError extends Error constructor: (@message, @metadata) -> @@ -826,21 +826,21 @@ class DisplayBuffer extends Model decorationsForMarkerId: (markerId) -> @decorationsByMarkerId[markerId] - # Retrieves a {Marker} based on its id. + # Retrieves a {TextEditorMarker} based on its id. # # id - A {Number} representing a marker id # - # Returns the {Marker} (if it exists). + # Returns the {TextEditorMarker} (if it exists). getMarker: (id) -> unless marker = @markers[id] if bufferMarker = @buffer.getMarker(id) - marker = new Marker({bufferMarker, displayBuffer: this}) + marker = new TextEditorMarker({bufferMarker, displayBuffer: this}) @markers[id] = marker marker # Retrieves the active markers in the buffer. # - # Returns an {Array} of existing {Marker}s. + # Returns an {Array} of existing {TextEditorMarker}s. getMarkers: -> @buffer.getMarkers().map ({id}) => @getMarker(id) @@ -850,7 +850,7 @@ class DisplayBuffer extends Model # Public: Constructs a new marker at the given screen range. # # range - The marker {Range} (representing the distance between the head and tail) - # options - Options to pass to the {Marker} constructor + # options - Options to pass to the {TextEditorMarker} constructor # # Returns a {Number} representing the new marker's ID. markScreenRange: (args...) -> @@ -860,7 +860,7 @@ class DisplayBuffer extends Model # Public: Constructs a new marker at the given buffer range. # # range - The marker {Range} (representing the distance between the head and tail) - # options - Options to pass to the {Marker} constructor + # options - Options to pass to the {TextEditorMarker} constructor # # Returns a {Number} representing the new marker's ID. markBufferRange: (range, options) -> @@ -869,7 +869,7 @@ class DisplayBuffer extends Model # Public: Constructs a new marker at the given screen position. # # range - The marker {Range} (representing the distance between the head and tail) - # options - Options to pass to the {Marker} constructor + # options - Options to pass to the {TextEditorMarker} constructor # # Returns a {Number} representing the new marker's ID. markScreenPosition: (screenPosition, options) -> @@ -878,7 +878,7 @@ class DisplayBuffer extends Model # Public: Constructs a new marker at the given buffer position. # # range - The marker {Range} (representing the distance between the head and tail) - # options - Options to pass to the {Marker} constructor + # options - Options to pass to the {TextEditorMarker} constructor # # Returns a {Number} representing the new marker's ID. markBufferPosition: (bufferPosition, options) -> @@ -895,7 +895,7 @@ class DisplayBuffer extends Model # # Refer to {DisplayBuffer::findMarkers} for details. # - # Returns a {Marker} or null + # Returns a {TextEditorMarker} or null findMarker: (params) -> @findMarkers(params)[0] @@ -916,7 +916,7 @@ class DisplayBuffer extends Model # :containedInBufferRange - A {Range} or range-compatible {Array}. Only # returns markers contained within this range. # - # Returns an {Array} of {Marker}s + # Returns an {Array} of {TextEditorMarker}s findMarkers: (params) -> params = @translateToBufferMarkerParams(params) @buffer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id) diff --git a/src/gutter.coffee b/src/gutter.coffee index 8418823bf..f59fa7b6e 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -71,13 +71,13 @@ class Gutter isVisible: -> @visible - # Essential: Add a decoration that tracks a {Marker}. When the marker moves, + # Essential: Add a decoration that tracks a {TextEditorMarker}. When the marker moves, # is invalidated, or is destroyed, the decoration will be updated to reflect # the marker's state. # # ## Arguments # - # * `marker` A {Marker} you want this decoration to follow. + # * `marker` A {TextEditorMarker} you want this decoration to follow. # * `decorationParams` An {Object} representing the decoration. It is passed # to {TextEditor::decorateMarker} as its `decorationParams` and so supports # all options documented there. diff --git a/src/marker.coffee b/src/text-editor-marker.coffee similarity index 94% rename from src/marker.coffee rename to src/text-editor-marker.coffee index 16f644027..e1ac89fd3 100644 --- a/src/marker.coffee +++ b/src/text-editor-marker.coffee @@ -6,7 +6,7 @@ _ = require 'underscore-plus' # targets, misspelled words, and anything else that needs to track a logical # location in the buffer over time. # -# ### Marker Creation +# ### TextEditorMarker Creation # # Use {TextEditor::markBufferRange} rather than creating Markers directly. # @@ -40,7 +40,7 @@ _ = require 'underscore-plus' # # See {TextEditor::markBufferRange} for usage. module.exports = -class Marker +class TextEditorMarker bufferMarkerSubscription: null oldHeadBufferPosition: null oldHeadScreenPosition: null @@ -66,7 +66,7 @@ class Marker @bufferMarker.destroy() @disposables.dispose() - # Essential: Creates and returns a new {Marker} with the same properties as + # Essential: Creates and returns a new {TextEditorMarker} with the same properties as # this marker. # # {Selection} markers (markers with a custom property `type: "selection"`) @@ -79,7 +79,7 @@ class Marker # marker. The new marker's properties are computed by extending this marker's # properties with `properties`. # - # Returns a {Marker}. + # Returns a {TextEditorMarker}. copy: (properties) -> @displayBuffer.getMarker(@bufferMarker.copy(properties).id) @@ -129,7 +129,7 @@ class Marker @emitter.on 'did-destroy', callback ### - Section: Marker Details + Section: TextEditorMarker Details ### # Essential: Returns a {Boolean} indicating whether the marker is valid. Markers can be @@ -140,7 +140,7 @@ class Marker # Essential: Returns a {Boolean} indicating whether the marker has been destroyed. A marker # can be invalid without being destroyed, in which case undoing the invalidating # operation would restore the marker. Once a marker is destroyed by calling - # {Marker::destroy}, no undo/redo operation can ever bring it back. + # {TextEditorMarker::destroy}, no undo/redo operation can ever bring it back. isDestroyed: -> @bufferMarker.isDestroyed() @@ -179,14 +179,14 @@ class Marker # Essential: Returns a {Boolean} indicating whether this marker is equivalent to # another marker, meaning they have the same range and options. # - # * `other` {Marker} other marker + # * `other` {TextEditorMarker} other marker isEqual: (other) -> return false unless other instanceof @constructor @bufferMarker.isEqual(other.bufferMarker) # Essential: Compares this marker to another based on their ranges. # - # * `other` {Marker} + # * `other` {TextEditorMarker} # # Returns a {Number} compare: (other) -> @@ -225,28 +225,28 @@ class Marker @setBufferRange(@displayBuffer.bufferRangeForScreenRange(screenRange), options) # Essential: Retrieves the buffer position of the marker's start. This will always be - # less than or equal to the result of {Marker::getEndBufferPosition}. + # less than or equal to the result of {TextEditorMarker::getEndBufferPosition}. # # Returns a {Point}. getStartBufferPosition: -> @bufferMarker.getStartPosition() # Essential: Retrieves the screen position of the marker's start. This will always be - # less than or equal to the result of {Marker::getEndScreenPosition}. + # less than or equal to the result of {TextEditorMarker::getEndScreenPosition}. # # Returns a {Point}. getStartScreenPosition: -> @displayBuffer.screenPositionForBufferPosition(@getStartBufferPosition(), wrapAtSoftNewlines: true) # Essential: Retrieves the buffer position of the marker's end. This will always be - # greater than or equal to the result of {Marker::getStartBufferPosition}. + # greater than or equal to the result of {TextEditorMarker::getStartBufferPosition}. # # Returns a {Point}. getEndBufferPosition: -> @bufferMarker.getEndPosition() # Essential: Retrieves the screen position of the marker's end. This will always be - # greater than or equal to the result of {Marker::getStartScreenPosition}. + # greater than or equal to the result of {TextEditorMarker::getStartScreenPosition}. # # Returns a {Point}. getEndScreenPosition: -> @@ -330,7 +330,7 @@ class Marker # Returns a {String} representation of the marker inspect: -> - "Marker(id: #{@id}, bufferRange: #{@getBufferRange()})" + "TextEditorMarker(id: #{@id}, bufferRange: #{@getBufferRange()})" destroyed: -> delete @displayBuffer.markers[@id] diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 9273841eb..a4ad70b6e 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1396,7 +1396,7 @@ class TextEditor extends Model Section: Decorations ### - # Essential: Adds a decoration that tracks a {Marker}. When the marker moves, + # Essential: Adds a decoration that tracks a {TextEditorMarker}. When the marker moves, # is invalidated, or is destroyed, the decoration will be updated to reflect # the marker's state. # @@ -1417,28 +1417,28 @@ class TextEditor extends Model # # ``` # * __overlay__: Positions the view associated with the given item at the head - # or tail of the given `Marker`. - # * __gutter__: A decoration that tracks a {Marker} in a {Gutter}. Gutter + # or tail of the given `TextEditorMarker`. + # * __gutter__: A decoration that tracks a {TextEditorMarker} in a {Gutter}. Gutter # decorations are created by calling {Gutter::decorateMarker} on the # desired `Gutter` instance. # # ## Arguments # - # * `marker` A {Marker} you want this decoration to follow. + # * `marker` A {TextEditorMarker} you want this decoration to follow. # * `decorationParams` An {Object} representing the decoration e.g. # `{type: 'line-number', class: 'linter-error'}` # * `type` There are several supported decoration types. The behavior of the # types are as follows: # * `line` Adds the given `class` to the lines overlapping the rows - # spanned by the `Marker`. + # spanned by the `TextEditorMarker`. # * `line-number` Adds the given `class` to the line numbers overlapping - # the rows spanned by the `Marker`. + # the rows spanned by the `TextEditorMarker`. # * `highlight` Creates a `.highlight` div with the nested class with up - # to 3 nested regions that fill the area spanned by the `Marker`. + # to 3 nested regions that fill the area spanned by the `TextEditorMarker`. # * `overlay` Positions the view associated with the given item at the - # head or tail of the given `Marker`, depending on the `position` + # head or tail of the given `TextEditorMarker`, depending on the `position` # property. - # * `gutter` Tracks a {Marker} in a {Gutter}. Created by calling + # * `gutter` Tracks a {TextEditorMarker} in a {Gutter}. Created by calling # {Gutter::decorateMarker} on the desired `Gutter` instance. # * `class` This CSS class will be applied to the decorated line number, # line, highlight, or overlay. @@ -1446,16 +1446,16 @@ class TextEditor extends Model # corresponding view registered. Only applicable to the `gutter` and # `overlay` types. # * `onlyHead` (optional) If `true`, the decoration will only be applied to - # the head of the `Marker`. Only applicable to the `line` and + # the head of the `TextEditorMarker`. Only applicable to the `line` and # `line-number` types. # * `onlyEmpty` (optional) If `true`, the decoration will only be applied if - # the associated `Marker` is empty. Only applicable to the `gutter`, + # the associated `TextEditorMarker` is empty. Only applicable to the `gutter`, # `line`, and `line-number` types. # * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied - # if the associated `Marker` is non-empty. Only applicable to the + # if the associated `TextEditorMarker` is non-empty. Only applicable to the # `gutter`, `line`, and `line-number` types. # * `position` (optional) Only applicable to decorations of type `overlay`, - # controls where the overlay view is positioned relative to the `Marker`. + # controls where the overlay view is positioned relative to the `TextEditorMarker`. # Values can be `'head'` (the default), or `'tail'`. # # Returns a {Decoration} object @@ -1472,7 +1472,7 @@ class TextEditor extends Model # # Returns an {Object} of decorations in the form # `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}` - # where the keys are {Marker} IDs, and the values are an array of decoration + # where the keys are {TextEditorMarker} IDs, and the values are an array of decoration # params objects attached to the marker. # Returns an empty object when no decorations are found decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> @@ -1567,7 +1567,7 @@ class TextEditor extends Model # region in any way, including changes that end at the marker's # start or start at the marker's end. This is the most fragile strategy. # - # Returns a {Marker}. + # Returns a {TextEditorMarker}. markBufferRange: (args...) -> @displayBuffer.markBufferRange(args...) @@ -1602,7 +1602,7 @@ class TextEditor extends Model # region in any way, including changes that end at the marker's # start or start at the marker's end. This is the most fragile strategy. # - # Returns a {Marker}. + # Returns a {TextEditorMarker}. markScreenRange: (args...) -> @displayBuffer.markScreenRange(args...) @@ -1611,7 +1611,7 @@ class TextEditor extends Model # * `position` A {Point} or {Array} of `[row, column]`. # * `options` (optional) See {TextBuffer::markRange}. # - # Returns a {Marker}. + # Returns a {TextEditorMarker}. markBufferPosition: (args...) -> @displayBuffer.markBufferPosition(args...) @@ -1620,11 +1620,11 @@ class TextEditor extends Model # * `position` A {Point} or {Array} of `[row, column]`. # * `options` (optional) See {TextBuffer::markRange}. # - # Returns a {Marker}. + # Returns a {TextEditorMarker}. markScreenPosition: (args...) -> @displayBuffer.markScreenPosition(args...) - # Essential: Find all {Marker}s that match the given properties. + # Essential: Find all {TextEditorMarker}s that match the given properties. # # This method finds markers based on the given properties. Markers can be # associated with custom properties that will be compared with basic equality. @@ -1649,7 +1649,7 @@ class TextEditor extends Model # Extended: Observe changes in the set of markers that intersect a particular # region of the editor. # - # * `callback` A {Function} to call whenever one or more {Marker}s appears, + # * `callback` A {Function} to call whenever one or more {TextEditorMarker}s appears, # disappears, or moves within the given region. # * `event` An {Object} with the following keys: # * `insert` A {Set} containing the ids of all markers that appeared @@ -1665,13 +1665,13 @@ class TextEditor extends Model observeMarkers: (callback) -> @displayBuffer.observeMarkers(callback) - # Extended: Get the {Marker} for the given marker id. + # Extended: Get the {TextEditorMarker} for the given marker id. # # * `id` {Number} id of the marker getMarker: (id) -> @displayBuffer.getMarker(id) - # Extended: Get all {Marker}s. Consider using {::findMarkers} + # Extended: Get all {TextEditorMarker}s. Consider using {::findMarkers} getMarkers: -> @displayBuffer.getMarkers() @@ -1888,7 +1888,7 @@ class TextEditor extends Model getCursorsOrderedByBufferPosition: -> @getCursors().sort (a, b) -> a.compare(b) - # Add a cursor based on the given {Marker}. + # Add a cursor based on the given {TextEditorMarker}. addCursor: (marker) -> cursor = new Cursor(editor: this, marker: marker, config: @config) @cursors.push(cursor) @@ -2237,7 +2237,7 @@ class TextEditor extends Model # Extended: Select the range of the given marker if it is valid. # - # * `marker` A {Marker} + # * `marker` A {TextEditorMarker} # # Returns the selected {Range} or `undefined` if the marker is invalid. selectMarker: (marker) -> @@ -2363,9 +2363,9 @@ class TextEditor extends Model _.reduce(tail, reducer, [head]) return result if fn? - # Add a {Selection} based on the given {Marker}. + # Add a {Selection} based on the given {TextEditorMarker}. # - # * `marker` The {Marker} to highlight + # * `marker` The {TextEditorMarker} to highlight # * `options` (optional) An {Object} that pertains to the {Selection} constructor. # # Returns the new {Selection}. From 1ee6384332bca27114567942e4c1f9ae01ffc79e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 29 Oct 2015 15:40:32 -0600 Subject: [PATCH 25/53] Add TextEditorMarkerLayer --- src/display-buffer.coffee | 77 +++++---------------------- src/text-editor-marker-layer.coffee | 80 +++++++++++++++++++++++++++++ src/text-editor-marker.coffee | 9 ++-- src/text-editor.coffee | 4 -- 4 files changed, 98 insertions(+), 72 deletions(-) create mode 100644 src/text-editor-marker-layer.coffee diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 203d3360d..f3260a3bb 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -8,7 +8,7 @@ Model = require './model' Token = require './token' Decoration = require './decoration' LayerDecoration = require './layer-decoration' -TextEditorMarker = require './text-editor-marker' +TextEditorMarkerLayer = require './text-editor-marker-layer' class BufferToScreenConversionError extends Error constructor: (@message, @metadata) -> @@ -53,7 +53,7 @@ class DisplayBuffer extends Model }) @buffer = @tokenizedBuffer.buffer @charWidthsByScope = {} - @markers = {} + @defaultMarkerLayer = new TextEditorMarkerLayer(this, @buffer.getDefaultMarkerLayer(), true) @foldsByMarkerId = {} @decorationsById = {} @decorationsByMarkerId = {} @@ -832,17 +832,13 @@ class DisplayBuffer extends Model # # Returns the {TextEditorMarker} (if it exists). getMarker: (id) -> - unless marker = @markers[id] - if bufferMarker = @buffer.getMarker(id) - marker = new TextEditorMarker({bufferMarker, displayBuffer: this}) - @markers[id] = marker - marker + @defaultMarkerLayer.getMarker(id) # Retrieves the active markers in the buffer. # # Returns an {Array} of existing {TextEditorMarker}s. getMarkers: -> - @buffer.getMarkers().map ({id}) => @getMarker(id) + @defaultMarkerLayer.getMarkers() getMarkerCount: -> @buffer.getMarkerCount() @@ -853,9 +849,8 @@ class DisplayBuffer extends Model # options - Options to pass to the {TextEditorMarker} constructor # # Returns a {Number} representing the new marker's ID. - markScreenRange: (args...) -> - bufferRange = @bufferRangeForScreenRange(args.shift()) - @markBufferRange(bufferRange, args...) + markScreenRange: (screenRange, options) -> + @defaultMarkerLayer.markScreenRange(screenRange, options) # Public: Constructs a new marker at the given buffer range. # @@ -863,8 +858,8 @@ class DisplayBuffer extends Model # options - Options to pass to the {TextEditorMarker} constructor # # Returns a {Number} representing the new marker's ID. - markBufferRange: (range, options) -> - @getMarker(@buffer.markRange(range, options).id) + markBufferRange: (bufferRange, options) -> + @defaultMarkerLayer.markBufferRange(bufferRange, options) # Public: Constructs a new marker at the given screen position. # @@ -873,7 +868,7 @@ class DisplayBuffer extends Model # # Returns a {Number} representing the new marker's ID. markScreenPosition: (screenPosition, options) -> - @markBufferPosition(@bufferPositionForScreenPosition(screenPosition), options) + @defaultMarkerLayer.markScreenPosition(screenPosition, options) # Public: Constructs a new marker at the given buffer position. # @@ -882,14 +877,7 @@ class DisplayBuffer extends Model # # Returns a {Number} representing the new marker's ID. markBufferPosition: (bufferPosition, options) -> - @getMarker(@buffer.markPosition(bufferPosition, options).id) - - # Public: Removes the marker with the given id. - # - # id - The {Number} of the ID to remove - destroyMarker: (id) -> - @buffer.destroyMarker(id) - delete @markers[id] + @defaultMarkerLayer.markBufferPosition(bufferPosition, options) # Finds the first marker satisfying the given attributes # @@ -897,7 +885,7 @@ class DisplayBuffer extends Model # # Returns a {TextEditorMarker} or null findMarker: (params) -> - @findMarkers(params)[0] + @defaultMarkerLayer.findMarkers(params)[0] # Public: Find all markers satisfying a set of parameters. # @@ -918,46 +906,7 @@ class DisplayBuffer extends Model # # Returns an {Array} of {TextEditorMarker}s findMarkers: (params) -> - params = @translateToBufferMarkerParams(params) - @buffer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id) - - translateToBufferMarkerParams: (params) -> - bufferMarkerParams = {} - for key, value of params - switch key - when 'startBufferRow' - key = 'startRow' - when 'endBufferRow' - key = 'endRow' - when 'startScreenRow' - key = 'startRow' - value = @bufferRowForScreenRow(value) - when 'endScreenRow' - key = 'endRow' - value = @bufferRowForScreenRow(value) - when 'intersectsBufferRowRange' - key = 'intersectsRowRange' - when 'intersectsScreenRowRange' - key = 'intersectsRowRange' - [startRow, endRow] = value - value = [@bufferRowForScreenRow(startRow), @bufferRowForScreenRow(endRow)] - when 'containsBufferRange' - key = 'containsRange' - when 'containsBufferPosition' - key = 'containsPosition' - when 'containedInBufferRange' - key = 'containedInRange' - when 'containedInScreenRange' - key = 'containedInRange' - value = @bufferRangeForScreenRange(value) - when 'intersectsBufferRange' - key = 'intersectsRange' - when 'intersectsScreenRange' - key = 'intersectsRange' - value = @bufferRangeForScreenRange(value) - bufferMarkerParams[key] = value - - bufferMarkerParams + @defaultMarkerLayer.findMarkers(params) findFoldMarker: (attributes) -> @findFoldMarkers(attributes)[0] @@ -978,7 +927,7 @@ class DisplayBuffer extends Model destroyed: -> fold.destroy() for markerId, fold of @foldsByMarkerId - marker.disposables.dispose() for id, marker of @markers + @defaultMarkerLayer.destroy() @scopedConfigSubscriptions.dispose() @disposables.dispose() @tokenizedBuffer.destroy() diff --git a/src/text-editor-marker-layer.coffee b/src/text-editor-marker-layer.coffee new file mode 100644 index 000000000..b17a19886 --- /dev/null +++ b/src/text-editor-marker-layer.coffee @@ -0,0 +1,80 @@ +TextEditorMarker = require './text-editor-marker' + +module.exports = +class TextEditorMarkerLayer + constructor: (@displayBuffer, @bufferMarkerLayer, @isDefaultLayer) -> + @markersById = {} + + getMarker: (id) -> + if editorMarker = @markersById[id] + editorMarker + else if bufferMarker = @bufferMarkerLayer.getMarker(id) + @markersById[id] = new TextEditorMarker(this, bufferMarker) + + getMarkers: -> + @bufferMarkerLayer.getMarkers().map ({id}) => @getMarker(id) + + markBufferRange: (bufferRange, options) -> + @getMarker(@bufferMarkerLayer.markRange(bufferRange, options).id) + + markScreenRange: (screenRange, options) -> + bufferRange = @displayBuffer.bufferRangeForScreenRange(screenRange) + @markBufferRange(bufferRange, options) + + markBufferPosition: (bufferPosition, options) -> + @getMarker(@bufferMarkerLayer.markPosition(bufferPosition, options).id) + + markScreenPosition: (screenPosition, options) -> + bufferPosition = @displayBuffer.bufferPositionForScreenPosition(screenPosition) + @markBufferPosition(bufferPosition, options) + + findMarkers: (params) -> + params = @translateToBufferMarkerParams(params) + @bufferMarkerLayer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id) + + destroy: -> + if @isDefaultLayer + marker.destroy() for id, marker of @markersById + else + @bufferMarkerLayer.destroy() + + didDestroyMarker: (marker) -> + delete @markersById[marker.id] + + translateToBufferMarkerParams: (params) -> + bufferMarkerParams = {} + for key, value of params + switch key + when 'startBufferRow' + key = 'startRow' + when 'endBufferRow' + key = 'endRow' + when 'startScreenRow' + key = 'startRow' + value = @displayBuffer.bufferRowForScreenRow(value) + when 'endScreenRow' + key = 'endRow' + value = @displayBuffer.bufferRowForScreenRow(value) + when 'intersectsBufferRowRange' + key = 'intersectsRowRange' + when 'intersectsScreenRowRange' + key = 'intersectsRowRange' + [startRow, endRow] = value + value = [@displayBuffer.bufferRowForScreenRow(startRow), @displayBuffer.bufferRowForScreenRow(endRow)] + when 'containsBufferRange' + key = 'containsRange' + when 'containsBufferPosition' + key = 'containsPosition' + when 'containedInBufferRange' + key = 'containedInRange' + when 'containedInScreenRange' + key = 'containedInRange' + value = @displayBuffer.bufferRangeForScreenRange(value) + when 'intersectsBufferRange' + key = 'intersectsRange' + when 'intersectsScreenRange' + key = 'intersectsRange' + value = @displayBuffer.bufferRangeForScreenRange(value) + bufferMarkerParams[key] = value + + bufferMarkerParams diff --git a/src/text-editor-marker.coffee b/src/text-editor-marker.coffee index e1ac89fd3..4c25d6d90 100644 --- a/src/text-editor-marker.coffee +++ b/src/text-editor-marker.coffee @@ -53,7 +53,8 @@ class TextEditorMarker Section: Construction and Destruction ### - constructor: ({@bufferMarker, @displayBuffer}) -> + constructor: (@markerLayer, @bufferMarker) -> + {@displayBuffer} = @markerLayer @emitter = new Emitter @disposables = new CompositeDisposable @id = @bufferMarker.id @@ -81,7 +82,7 @@ class TextEditorMarker # # Returns a {TextEditorMarker}. copy: (properties) -> - @displayBuffer.getMarker(@bufferMarker.copy(properties).id) + @markerLayer.getMarker(@bufferMarker.copy(properties).id) ### Section: Event Subscription @@ -169,7 +170,7 @@ class TextEditorMarker @bufferMarker.setProperties(properties) matchesProperties: (attributes) -> - attributes = @displayBuffer.translateToBufferMarkerParams(attributes) + attributes = @markerLayer.translateToBufferMarkerParams(attributes) @bufferMarker.matchesParams(attributes) ### @@ -333,7 +334,7 @@ class TextEditorMarker "TextEditorMarker(id: #{@id}, bufferRange: #{@getBufferRange()})" destroyed: -> - delete @displayBuffer.markers[@id] + @markerLayer.didDestroyMarker(this) @emitter.emit 'did-destroy' @emitter.dispose() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a4ad70b6e..314d767da 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1681,10 +1681,6 @@ class TextEditor extends Model getMarkerCount: -> @buffer.getMarkerCount() - # {Delegates to: DisplayBuffer.destroyMarker} - destroyMarker: (args...) -> - @displayBuffer.destroyMarker(args...) - ### Section: Cursors ### From 29bb1bb31be10bbd855171a56a93c4c1e12d815b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 2 Nov 2015 16:50:01 -0600 Subject: [PATCH 26/53] Add TextEditor-level marker layers and use them for selections --- spec/atom-environment-spec.coffee | 17 ------ spec/display-buffer-spec.coffee | 5 +- spec/text-editor-component-spec.coffee | 5 +- spec/text-editor-presenter-spec.coffee | 10 ++-- spec/text-editor-spec.coffee | 5 +- src/display-buffer.coffee | 79 +++++++++++++++++--------- src/text-editor-marker-layer.coffee | 13 +++++ src/text-editor-marker.coffee | 10 ++-- src/text-editor.coffee | 50 +++++++++------- 9 files changed, 112 insertions(+), 82 deletions(-) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index e12ac75c1..23f8e0e51 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -243,23 +243,6 @@ describe "AtomEnvironment", -> atomEnvironment.destroy() - describe "::destroy()", -> - it "unsubscribes from all buffers", -> - atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document}) - - waitsForPromise -> - atomEnvironment.workspace.open("sample.js") - - runs -> - buffer = atomEnvironment.workspace.getActivePaneItem().buffer - pane = atomEnvironment.workspace.getActivePane() - pane.splitRight(copyActiveItem: true) - expect(atomEnvironment.workspace.getTextEditors().length).toBe 2 - - atomEnvironment.destroy() - - expect(buffer.getSubscriptionCount()).toBe 0 - describe "::openLocations(locations) (called via IPC from browser process)", -> beforeEach -> spyOn(atom.workspace, 'open') diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index accda876a..21f018641 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -829,7 +829,6 @@ describe "DisplayBuffer", -> it "unsubscribes all display buffer markers from their underlying buffer marker (regression)", -> marker = displayBuffer.markBufferPosition([12, 2]) displayBuffer.destroy() - expect(marker.bufferMarker.getSubscriptionCount()).toBe 0 expect( -> buffer.insert([12, 2], '\n')).not.toThrow() describe "markers", -> @@ -879,7 +878,7 @@ describe "DisplayBuffer", -> [markerChangedHandler, marker] = [] beforeEach -> - marker = displayBuffer.markScreenRange([[5, 4], [5, 10]], maintainHistory: true) + marker = displayBuffer.addMarkerLayer(maintainHistory: true).markScreenRange([[5, 4], [5, 10]]) marker.onDidChange markerChangedHandler = jasmine.createSpy("markerChangedHandler") it "triggers the 'changed' event whenever the markers head's screen position changes in the buffer or on screen", -> @@ -1016,7 +1015,7 @@ describe "DisplayBuffer", -> expect(markerChangedHandler).not.toHaveBeenCalled() it "updates markers before emitting buffer change events, but does not notify their observers until the change event", -> - marker2 = displayBuffer.markBufferRange([[8, 1], [8, 1]], maintainHistory: true) + marker2 = displayBuffer.addMarkerLayer(maintainHistory: true).markBufferRange([[8, 1], [8, 1]]) marker2.onDidChange marker2ChangedHandler = jasmine.createSpy("marker2ChangedHandler") displayBuffer.onDidChange changeHandler = jasmine.createSpy("changeHandler").andCallFake -> onDisplayBufferChange() diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 3faf2a468..13098836d 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -1373,7 +1373,7 @@ describe "TextEditorComponent", -> [marker, decoration, decorationParams] = [] beforeEach -> - marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], invalidate: 'inside', maintainHistory: true) + marker = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[2, 13], [3, 15]], invalidate: 'inside') decorationParams = {type: ['line-number', 'line'], class: 'a'} decoration = editor.decorateMarker(marker, decorationParams) waitsForNextDOMUpdate() @@ -1548,7 +1548,7 @@ describe "TextEditorComponent", -> [marker, decoration, decorationParams, scrollViewClientLeft] = [] beforeEach -> scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left - marker = editor.displayBuffer.markBufferRange([[2, 13], [3, 15]], invalidate: 'inside', maintainHistory: true) + marker = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[2, 13], [3, 15]], invalidate: 'inside') decorationParams = {type: 'highlight', class: 'test-highlight'} decoration = editor.decorateMarker(marker, decorationParams) waitsForNextDOMUpdate() @@ -2673,7 +2673,6 @@ describe "TextEditorComponent", -> cursor = null beforeEach -> - console.log editor.getText() editor.setCursorScreenPosition([0, 0]) waitsForNextDOMUpdate() diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index aa716714f..62477eb16 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1154,10 +1154,10 @@ describe "TextEditorPresenter", -> describe ".decorationClasses", -> it "adds decoration classes to the relevant line state objects, both initially and when decorations change", -> - marker1 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true) + marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') decoration1 = editor.decorateMarker(marker1, type: 'line', class: 'a') presenter = buildPresenter() - marker2 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true) + marker2 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') decoration2 = editor.decorateMarker(marker2, type: 'line', class: 'b') waitsForStateToUpdate presenter @@ -1867,7 +1867,7 @@ describe "TextEditorPresenter", -> presenter.getState().content.overlays[decoration.id] it "contains state for overlay decorations both initially and when their markers move", -> - marker = editor.markBufferPosition([2, 13], invalidate: 'touch', maintainHistory: true) + marker = editor.addMarkerLayer(maintainHistory: true).markBufferPosition([2, 13], invalidate: 'touch') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) presenter = buildPresenter(explicitHeight: 30, scrollTop: 20) @@ -2353,9 +2353,9 @@ describe "TextEditorPresenter", -> describe ".decorationClasses", -> it "adds decoration classes to the relevant line number state objects, both initially and when decorations change", -> - marker1 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true) + marker1 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') decoration1 = editor.decorateMarker(marker1, type: 'line-number', class: 'a') - marker2 = editor.markBufferRange([[4, 0], [6, 2]], invalidate: 'touch', maintainHistory: true) + marker2 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch') decoration2 = editor.decorateMarker(marker2, type: 'line-number', class: 'b') presenter = buildPresenter() diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 95f40f3ff..9147bc21a 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -4589,7 +4589,10 @@ describe "TextEditor", -> expect(buffer.getLineCount()).toBe(count - 1) describe "when the line being deleted preceeds a fold, and the command is undone", -> - it "restores the line and preserves the fold", -> + # TODO: This seemed to have only been passing due to an accident in the text + # buffer implementation. Once we moved selections to a different layer it + # broke. We need to revisit our representation of folds and then reenable it. + xit "restores the line and preserves the fold", -> editor.setCursorBufferPosition([4]) editor.foldCurrentRow() expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index f3260a3bb..6e0468c49 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -54,15 +54,18 @@ class DisplayBuffer extends Model @buffer = @tokenizedBuffer.buffer @charWidthsByScope = {} @defaultMarkerLayer = new TextEditorMarkerLayer(this, @buffer.getDefaultMarkerLayer(), true) + @customMarkerLayersById = {} @foldsByMarkerId = {} @decorationsById = {} @decorationsByMarkerId = {} @overlayDecorationsById = {} @layerDecorationsByMarkerLayerId = {} + @decorationCountsByLayerId = {} + @layerUpdateDisposablesByLayerId = {} + @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings @disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange @disposables.add @buffer.onDidCreateMarker @handleBufferMarkerCreated - @disposables.add @buffer.getDefaultMarkerLayer().onDidUpdate => @scheduleUpdateDecorationsEvent() @foldMarkerAttributes = Object.freeze({class: 'fold', displayBufferId: @id}) folds = (new Fold(this, marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes())) @@ -777,41 +780,39 @@ class DisplayBuffer extends Model decorationsByMarkerId decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) -> - decorationState = {} + decorationsState = {} - startBufferRow = @bufferRowForScreenRow(startScreenRow) - endBufferRow = @bufferRowForScreenRow(endScreenRow) + for layerId of @decorationCountsByLayerId + layer = @getMarkerLayer(layerId) - defaultLayer = @buffer.getDefaultMarkerLayer() - for marker in defaultLayer.findMarkers(intersectsRowRange: [startBufferRow, endBufferRow]) when marker.isValid() - if decorations = @decorationsByMarkerId[marker.id] - for decoration in decorations - decorationState[decoration.id] = { - properties: decoration.properties - screenRange: @screenRangeForBufferRange(marker.getRange()) - rangeIsReversed: marker.isReversed() - } - - for markerLayerId, layerDecorations of @layerDecorationsByMarkerLayerId - markerLayer = @buffer.getMarkerLayer(markerLayerId) - for marker in markerLayer.findMarkers(intersectsRowRange: [startBufferRow, endBufferRow]) when marker.isValid() - screenRange = @screenRangeForBufferRange(marker.getRange()) + for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid() + screenRange = marker.getScreenRange() rangeIsReversed = marker.isReversed() - for layerDecoration in layerDecorations - decorationState["#{layerDecoration.id}-#{marker.id}"] = { - properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties - screenRange, rangeIsReversed - } - decorationState + if decorations = @decorationsByMarkerId[marker.id] + for decoration in decorations + decorationsState[decoration.id] = { + properties: decoration.properties + screenRange, rangeIsReversed + } + + if layerDecorations = @layerDecorationsByMarkerLayerId[layerId] + for layerDecoration in layerDecorations + decorationsState["#{layerDecoration.id}-#{marker.id}"] = { + properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties + screenRange, rangeIsReversed + } + + decorationsState decorateMarker: (marker, decorationParams) -> - marker = @getMarker(marker.id) + marker = @getMarkerLayer(marker.layer.id).getMarker(marker.id) decoration = new Decoration(marker, this, decorationParams) @decorationsByMarkerId[marker.id] ?= [] @decorationsByMarkerId[marker.id].push(decoration) @overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay') @decorationsById[decoration.id] = decoration + @observeDecoratedLayer(marker.layer) @scheduleUpdateDecorationsEvent() @emitter.emit 'did-add-decoration', decoration decoration @@ -820,6 +821,7 @@ class DisplayBuffer extends Model decoration = new LayerDecoration(markerLayer, this, decorationParams) @layerDecorationsByMarkerLayerId[markerLayer.id] ?= [] @layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration) + @observeDecoratedLayer(markerLayer) @scheduleUpdateDecorationsEvent() decoration @@ -908,6 +910,16 @@ class DisplayBuffer extends Model findMarkers: (params) -> @defaultMarkerLayer.findMarkers(params) + addMarkerLayer: (options) -> + bufferLayer = @buffer.addMarkerLayer(options) + @getMarkerLayer(bufferLayer.id) + + getMarkerLayer: (id) -> + if layer = @customMarkerLayersById[id] + layer + else if bufferLayer = @buffer.getMarkerLayer(id) + @customMarkerLayersById[id] = new TextEditorMarkerLayer(this, bufferLayer) + findFoldMarker: (attributes) -> @findFoldMarkers(attributes)[0] @@ -921,8 +933,8 @@ class DisplayBuffer extends Model @foldMarkerAttributes refreshMarkerScreenPositions: -> - for marker in @getMarkers() - marker.notifyObservers(textChanged: false) + @defaultMarkerLayer.refreshMarkerScreenPositions() + layer.refreshMarkerScreenPositions() for id, layer of @customMarkerLayersById return destroyed: -> @@ -1090,6 +1102,7 @@ class DisplayBuffer extends Model @emitter.emit 'did-remove-decoration', decoration delete @decorationsByMarkerId[marker.id] if decorations.length is 0 delete @overlayDecorationsById[decoration.id] + @unobserveDecoratedLayer(marker.layer) @scheduleUpdateDecorationsEvent() didDestroyLayerDecoration: (decoration) -> @@ -1100,8 +1113,20 @@ class DisplayBuffer extends Model if index > -1 decorations.splice(index, 1) delete @layerDecorationsByMarkerLayerId[markerLayer.id] if decorations.length is 0 + @unobserveDecoratedLayer(markerLayer) @scheduleUpdateDecorationsEvent() + observeDecoratedLayer: (layer) -> + @decorationCountsByLayerId[layer.id] ?= 0 + if ++@decorationCountsByLayerId[layer.id] is 1 + @layerUpdateDisposablesByLayerId[layer.id] = layer.onDidUpdate(@scheduleUpdateDecorationsEvent.bind(this)) + + unobserveDecoratedLayer: (layer) -> + if --@decorationCountsByLayerId[layer.id] is 0 + @layerUpdateDisposablesByLayerId[layer.id].dispose() + delete @decorationCountsByLayerId[layer.id] + delete @layerUpdateDisposablesByLayerId[layer.id] + checkScreenLinesInvariant: -> return if @isSoftWrapped() return if _.size(@foldsByMarkerId) > 0 diff --git a/src/text-editor-marker-layer.coffee b/src/text-editor-marker-layer.coffee index b17a19886..9b099637c 100644 --- a/src/text-editor-marker-layer.coffee +++ b/src/text-editor-marker-layer.coffee @@ -3,6 +3,7 @@ TextEditorMarker = require './text-editor-marker' module.exports = class TextEditorMarkerLayer constructor: (@displayBuffer, @bufferMarkerLayer, @isDefaultLayer) -> + @id = @bufferMarkerLayer.id @markersById = {} getMarker: (id) -> @@ -38,6 +39,11 @@ class TextEditorMarkerLayer else @bufferMarkerLayer.destroy() + refreshMarkerScreenPositions: -> + for marker in @getMarkers() + marker.notifyObservers(textChanged: false) + return + didDestroyMarker: (marker) -> delete @markersById[marker.id] @@ -78,3 +84,10 @@ class TextEditorMarkerLayer bufferMarkerParams[key] = value bufferMarkerParams + + onDidCreateMarker: (callback) -> + @bufferMarkerLayer.onDidCreateMarker (bufferMarker) => + callback(@getMarker(bufferMarker.id)) + + onDidUpdate: (callback) -> + @bufferMarkerLayer.onDidUpdate(callback) diff --git a/src/text-editor-marker.coffee b/src/text-editor-marker.coffee index 4c25d6d90..df84700ee 100644 --- a/src/text-editor-marker.coffee +++ b/src/text-editor-marker.coffee @@ -53,8 +53,8 @@ class TextEditorMarker Section: Construction and Destruction ### - constructor: (@markerLayer, @bufferMarker) -> - {@displayBuffer} = @markerLayer + constructor: (@layer, @bufferMarker) -> + {@displayBuffer} = @layer @emitter = new Emitter @disposables = new CompositeDisposable @id = @bufferMarker.id @@ -82,7 +82,7 @@ class TextEditorMarker # # Returns a {TextEditorMarker}. copy: (properties) -> - @markerLayer.getMarker(@bufferMarker.copy(properties).id) + @layer.getMarker(@bufferMarker.copy(properties).id) ### Section: Event Subscription @@ -170,7 +170,7 @@ class TextEditorMarker @bufferMarker.setProperties(properties) matchesProperties: (attributes) -> - attributes = @markerLayer.translateToBufferMarkerParams(attributes) + attributes = @layer.translateToBufferMarkerParams(attributes) @bufferMarker.matchesParams(attributes) ### @@ -334,7 +334,7 @@ class TextEditorMarker "TextEditorMarker(id: #{@id}, bufferRange: #{@getBufferRange()})" destroyed: -> - @markerLayer.didDestroyMarker(this) + @layer.didDestroyMarker(this) @emitter.emit 'did-destroy' @emitter.dispose() diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 314d767da..dc8de4b0a 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -74,6 +74,7 @@ class TextEditor extends Model throw error state.displayBuffer = displayBuffer + state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId) state.config = atomEnvironment.config state.notificationManager = atomEnvironment.notifications state.packageManager = atomEnvironment.packages @@ -90,9 +91,10 @@ class TextEditor extends Model { @softTabs, @scrollRow, @scrollColumn, initialLine, initialColumn, tabLength, - softWrapped, @displayBuffer, buffer, suppressCursorCreation, @mini, @placeholderText, - lineNumberGutterVisible, largeFileMode, @config, @notificationManager, @packageManager, - @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate + softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation, + @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, + @notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry, + @project, @assert, @applicationDelegate } = params throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? @@ -115,8 +117,9 @@ class TextEditor extends Model @config, @assert, @grammarRegistry, @packageManager }) @buffer = @displayBuffer.buffer + @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true) - for marker in @findMarkers(@getSelectionMarkerAttributes()) + for marker in @selectionsMarkerLayer.getMarkers() marker.setProperties(preserveFolds: true) @addSelection(marker) @@ -146,6 +149,7 @@ class TextEditor extends Model scrollRow: @getScrollRow() scrollColumn: @getScrollColumn() displayBuffer: @displayBuffer.serialize() + selectionsMarkerLayerId: @selectionsMarkerLayer.id subscribeToBuffer: -> @buffer.retain() @@ -161,9 +165,9 @@ class TextEditor extends Model @preserveCursorPositionOnBufferReload() subscribeToDisplayBuffer: -> - @disposables.add @displayBuffer.onDidCreateMarker @handleMarkerCreated - @disposables.add @displayBuffer.onDidChangeGrammar => @handleGrammarChange() - @disposables.add @displayBuffer.onDidTokenize => @handleTokenization() + @disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this) + @disposables.add @displayBuffer.onDidChangeGrammar @handleGrammarChange.bind(this) + @disposables.add @displayBuffer.onDidTokenize @handleTokenization.bind(this) @disposables.add @displayBuffer.onDidChange (e) => @mergeIntersectingSelections() @emitter.emit 'did-change', e @@ -480,14 +484,13 @@ class TextEditor extends Model # Create an {TextEditor} with its initial state based on this object copy: -> displayBuffer = @displayBuffer.copy() + selectionsMarkerLayer = displayBuffer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id) softTabs = @getSoftTabs() newEditor = new TextEditor({ - @buffer, displayBuffer, @tabLength, softTabs, suppressCursorCreation: true, - @config, @notificationManager, @packageManager, @clipboard, @viewRegistry, - @grammarRegistry, @project, @assert, @applicationDelegate + @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs, + suppressCursorCreation: true, @config, @notificationManager, @packageManager, + @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate }) - for marker in @findMarkers(editorId: @id) - marker.copy(editorId: newEditor.id, preserveFolds: true) newEditor # Controls visibility based on the given {Boolean}. @@ -1681,6 +1684,15 @@ class TextEditor extends Model getMarkerCount: -> @buffer.getMarkerCount() + destroyMarker: (id) -> + @getMarker(id)?.destroy() + + addMarkerLayer: (options) -> + @displayBuffer.addMarkerLayer(options) + + getMarkerLayer: (id) -> + @displayBuffer.getMarkerLayer(id) + ### Section: Cursors ### @@ -1749,7 +1761,7 @@ class TextEditor extends Model # # Returns a {Cursor}. addCursorAtBufferPosition: (bufferPosition, options) -> - @markBufferPosition(bufferPosition, @getSelectionMarkerAttributes()) + @selectionsMarkerLayer.markBufferPosition(bufferPosition, @getSelectionMarkerAttributes()) @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false @getLastSelection().cursor @@ -1759,7 +1771,7 @@ class TextEditor extends Model # # Returns a {Cursor}. addCursorAtScreenPosition: (screenPosition, options) -> - @markScreenPosition(screenPosition, @getSelectionMarkerAttributes()) + @selectionsMarkerLayer.markScreenPosition(screenPosition, @getSelectionMarkerAttributes()) @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false @getLastSelection().cursor @@ -2037,7 +2049,7 @@ class TextEditor extends Model # # Returns the added {Selection}. addSelectionForBufferRange: (bufferRange, options={}) -> - @markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options)) + @selectionsMarkerLayer.markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options)) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() @@ -2050,7 +2062,7 @@ class TextEditor extends Model # # Returns the added {Selection}. addSelectionForScreenRange: (screenRange, options={}) -> - @markScreenRange(screenRange, _.defaults(@getSelectionMarkerAttributes(), options)) + @selectionsMarkerLayer.markScreenRange(screenRange, _.defaults(@getSelectionMarkerAttributes(), options)) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() @@ -3069,10 +3081,6 @@ class TextEditor extends Model @subscribeToTabTypeConfig() @emitter.emit 'did-change-grammar', @getGrammar() - handleMarkerCreated: (marker) => - if marker.matchesProperties(@getSelectionMarkerAttributes()) - @addSelection(marker) - ### Section: TextEditor Rendering ### @@ -3109,7 +3117,7 @@ class TextEditor extends Model @viewRegistry.getView(this).pixelPositionForScreenPosition(screenPosition) getSelectionMarkerAttributes: -> - {type: 'selection', editorId: @id, invalidate: 'never', maintainHistory: true} + {type: 'selection', invalidate: 'never'} getVerticalScrollMargin: -> @displayBuffer.getVerticalScrollMargin() setVerticalScrollMargin: (verticalScrollMargin) -> @displayBuffer.setVerticalScrollMargin(verticalScrollMargin) From 2f81e5faac0596435291c2287798c0d833820412 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 2 Nov 2015 17:20:55 -0600 Subject: [PATCH 27/53] Store folds in their own marker layer --- spec/display-buffer-spec.coffee | 4 +-- src/display-buffer.coffee | 56 ++++++++++++++------------------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 21f018641..a54c01198 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -418,11 +418,11 @@ describe "DisplayBuffer", -> describe "when creating a fold where one already exists", -> it "returns existing fold and does't create new fold", -> fold = displayBuffer.createFold(0, 10) - expect(displayBuffer.findMarkers(class: 'fold').length).toBe 1 + expect(displayBuffer.foldsMarkerLayer.getMarkers().length).toBe 1 newFold = displayBuffer.createFold(0, 10) expect(newFold).toBe fold - expect(displayBuffer.findMarkers(class: 'fold').length).toBe 1 + expect(displayBuffer.foldsMarkerLayer.getMarkers().length).toBe 1 describe "when a fold is created inside an existing folded region", -> it "creates/destroys the fold, but does not trigger change event", -> diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 6e0468c49..b04fe7b08 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -30,6 +30,7 @@ class DisplayBuffer extends Model @deserialize: (state, atomEnvironment) -> state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) + state.foldsMarkerLayer = state.tokenizedBuffer.buffer.getMarkerLayer(state.foldsMarkerLayerId) state.config = atomEnvironment.config state.assert = atomEnvironment.assert state.grammarRegistry = atomEnvironment.grammars @@ -40,8 +41,8 @@ class DisplayBuffer extends Model super { - tabLength, @editorWidthInChars, @tokenizedBuffer, buffer, ignoreInvisibles, - @largeFileMode, @config, @assert, @grammarRegistry, @packageManager + tabLength, @editorWidthInChars, @tokenizedBuffer, @foldsMarkerLayer, buffer, + ignoreInvisibles, @largeFileMode, @config, @assert, @grammarRegistry, @packageManager } = params @emitter = new Emitter @@ -65,10 +66,10 @@ class DisplayBuffer extends Model @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings @disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange - @disposables.add @buffer.onDidCreateMarker @handleBufferMarkerCreated + @disposables.add @buffer.onDidCreateMarker @didCreateDefaultLayerMarker - @foldMarkerAttributes = Object.freeze({class: 'fold', displayBufferId: @id}) - folds = (new Fold(this, marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes())) + @foldsMarkerLayer ?= @buffer.addMarkerLayer() + folds = (new Fold(this, marker) for marker in @foldsMarkerLayer.getMarkers()) @updateAllScreenLines() @decorateFold(fold) for fold in folds @@ -114,17 +115,15 @@ class DisplayBuffer extends Model editorWidthInChars: @editorWidthInChars tokenizedBuffer: @tokenizedBuffer.serialize() largeFileMode: @largeFileMode + foldsMarkerLayerId: @foldsMarkerLayer.id copy: -> - newDisplayBuffer = new DisplayBuffer({ + foldsMarkerLayer = @foldsMarkerLayer.copy() + new DisplayBuffer({ @buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert, - @grammarRegistry, @packageManager + @grammarRegistry, @packageManager, foldsMarkerLayer }) - for marker in @findMarkers(displayBufferId: @id) - marker.copy(displayBufferId: newDisplayBuffer.id) - newDisplayBuffer - updateAllScreenLines: -> @maxLineLength = 0 @screenLines = [] @@ -396,10 +395,14 @@ class DisplayBuffer extends Model # Returns the new {Fold}. createFold: (startRow, endRow) -> unless @largeFileMode - foldMarker = - @findFoldMarker({startRow, endRow}) ? - @buffer.markRange([[startRow, 0], [endRow, Infinity]], @getFoldMarkerAttributes()) - @foldForMarker(foldMarker) + if foldMarker = @findFoldMarker({startRow, endRow}) + @foldForMarker(foldMarker) + else + foldMarker = @foldsMarkerLayer.markRange([[startRow, 0], [endRow, Infinity]]) + fold = new Fold(this, foldMarker) + fold.updateDisplayBuffer() + @decorateFold(fold) + fold isFoldedAtBufferRow: (bufferRow) -> @largestFoldContainingBufferRow(bufferRow)? @@ -920,17 +923,11 @@ class DisplayBuffer extends Model else if bufferLayer = @buffer.getMarkerLayer(id) @customMarkerLayersById[id] = new TextEditorMarkerLayer(this, bufferLayer) - findFoldMarker: (attributes) -> - @findFoldMarkers(attributes)[0] + findFoldMarker: (params) -> + @findFoldMarkers(params)[0] - findFoldMarkers: (attributes) -> - @buffer.findMarkers(@getFoldMarkerAttributes(attributes)) - - getFoldMarkerAttributes: (attributes) -> - if attributes - _.extend(attributes, @foldMarkerAttributes) - else - @foldMarkerAttributes + findFoldMarkers: (params) -> + @foldsMarkerLayer.findMarkers(params) refreshMarkerScreenPositions: -> @defaultMarkerLayer.refreshMarkerScreenPositions() @@ -938,8 +935,8 @@ class DisplayBuffer extends Model return destroyed: -> - fold.destroy() for markerId, fold of @foldsByMarkerId @defaultMarkerLayer.destroy() + @foldsMarkerLayer.destroy() @scopedConfigSubscriptions.dispose() @disposables.dispose() @tokenizedBuffer.destroy() @@ -1061,12 +1058,7 @@ class DisplayBuffer extends Model @longestScreenRow = screenRow @maxLineLength = length - handleBufferMarkerCreated: (textBufferMarker) => - if textBufferMarker.matchesParams(@getFoldMarkerAttributes()) - fold = new Fold(this, textBufferMarker) - fold.updateDisplayBuffer() - @decorateFold(fold) - + didCreateDefaultLayerMarker: (textBufferMarker) => if marker = @getMarker(textBufferMarker.id) # The marker might have been removed in some other handler called before # this one. Only emit when the marker still exists. From 9ee3d539751ae7677c8d07b0ccb2164b24996ad6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 3 Nov 2015 15:49:11 -0800 Subject: [PATCH 28/53] Destroy the selections marker layer when editors are destroyed --- spec/text-editor-spec.coffee | 9 +++++---- src/text-editor.coffee | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 9147bc21a..0ad43046b 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5060,11 +5060,12 @@ describe "TextEditor", -> expect(coffeeEditor.lineTextForBufferRow(2)).toBe "" describe ".destroy()", -> - it "destroys all markers associated with the edit session", -> - editor.foldAll() - expect(buffer.getMarkerCount()).toBeGreaterThan 0 + it "destroys marker layers associated with the text editor", -> + selectionsMarkerLayerId = editor.selectionsMarkerLayer.id + foldsMarkerLayerId = editor.displayBuffer.foldsMarkerLayer.id editor.destroy() - expect(buffer.getMarkerCount()).toBe 0 + expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined() + expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined() it "notifies ::onDidDestroy observers when the editor is destroyed", -> destroyObserverCalled = false diff --git a/src/text-editor.coffee b/src/text-editor.coffee index dc8de4b0a..da077ae2b 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -181,6 +181,7 @@ class TextEditor extends Model @disposables.dispose() @tabTypeSubscription.dispose() selection.destroy() for selection in @selections.slice() + @selectionsMarkerLayer.destroy() @buffer.release() @displayBuffer.destroy() @languageMode.destroy() From f6859210d0fd78ac11088420cdc2607bcea56d4f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 3 Nov 2015 18:34:40 -0800 Subject: [PATCH 29/53] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4b406fef9..f7fd9d38f 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "^7.2.0", + "text-buffer": "^8.0.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From 5e9cec2ec721c0fb2864da19cbd30c4e9a289cb4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Nov 2015 09:53:00 -0800 Subject: [PATCH 30/53] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f7fd9d38f..f60901e1a 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "^8.0.0", + "text-buffer": "^8.0.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From 9714f2e7294fb9e539da9bd97692913b640589ad Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Nov 2015 11:26:07 -0800 Subject: [PATCH 31/53] Emit decoration events synchronously in legacy spec environment This is needed to keep a bunch of package tests passing that expect synchronous decoration updates. --- src/display-buffer.coffee | 7 +++++++ src/text-editor-element.coffee | 5 ++++- src/text-editor.coffee | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index b04fe7b08..e1bfff70f 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -27,6 +27,7 @@ class DisplayBuffer extends Model height: null width: null didUpdateDecorationsEventScheduled: false + updatedSynchronously: false @deserialize: (state, atomEnvironment) -> state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) @@ -186,6 +187,8 @@ class DisplayBuffer extends Model # visible - A {Boolean} indicating of the tokenized buffer is shown setVisible: (visible) -> @tokenizedBuffer.setVisible(visible) + setUpdatedSynchronously: (@updatedSynchronously) -> + getVerticalScrollMargin: -> maxScrollMargin = Math.floor(((@getHeight() / @getLineHeightInPixels()) - 1) / 2) Math.min(@verticalScrollMargin, maxScrollMargin) @@ -1065,6 +1068,10 @@ class DisplayBuffer extends Model @emitter.emit 'did-create-marker', marker scheduleUpdateDecorationsEvent: -> + if @updatedSynchronously + @emitter.emit 'did-update-decorations' + return + unless @didUpdateDecorationsEventScheduled @didUpdateDecorationsEventScheduled = true process.nextTick => diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 55e23d2da..1a55eb002 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -103,6 +103,7 @@ class TextEditorElement extends HTMLElement return if model.isDestroyed() @model = model + @model.setUpdatedSynchronously(@isUpdatedSynchronously()) @initializeContent() @mountComponent() @addGrammarScopeAttribute() @@ -194,7 +195,9 @@ class TextEditorElement extends HTMLElement hasFocus: -> this is document.activeElement or @contains(document.activeElement) - setUpdatedSynchronously: (@updatedSynchronously) -> @updatedSynchronously + setUpdatedSynchronously: (@updatedSynchronously) -> + @model?.setUpdatedSynchronously(@updatedSynchronously) + @updatedSynchronously isUpdatedSynchronously: -> @updatedSynchronously diff --git a/src/text-editor.coffee b/src/text-editor.coffee index da077ae2b..8cc80f142 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -506,6 +506,9 @@ class TextEditor extends Model isMini: -> @mini + setUpdatedSynchronously: (updatedSynchronously) -> + @displayBuffer.setUpdatedSynchronously(updatedSynchronously) + onDidChangeMini: (callback) -> @emitter.on 'did-change-mini', callback From 3f4d8d0104f92e8b584fe2887216889d2cd1fa39 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Nov 2015 11:26:41 -0800 Subject: [PATCH 32/53] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f60901e1a..69466ed60 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "^8.0.1", + "text-buffer": "^8.0.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From 533146bc6a1a3b75008ff9472e8360cd86e4ca77 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Nov 2015 15:58:00 -0800 Subject: [PATCH 33/53] Document new APIs --- src/display-buffer.coffee | 2 + src/layer-decoration.coffee | 20 ++++ src/text-editor-marker-layer.coffee | 139 +++++++++++++++++++++++----- src/text-editor.coffee | 100 ++++++++++++-------- 4 files changed, 199 insertions(+), 62 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index e1bfff70f..f5a7bd853 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -926,6 +926,8 @@ class DisplayBuffer extends Model else if bufferLayer = @buffer.getMarkerLayer(id) @customMarkerLayersById[id] = new TextEditorMarkerLayer(this, bufferLayer) + getDefaultMarkerLayer: -> @defaultMarkerLayer + findFoldMarker: (params) -> @findFoldMarkers(params)[0] diff --git a/src/layer-decoration.coffee b/src/layer-decoration.coffee index a33c9e1a3..1f76140a3 100644 --- a/src/layer-decoration.coffee +++ b/src/layer-decoration.coffee @@ -3,6 +3,8 @@ _ = require 'underscore-plus' idCounter = 0 nextId = -> idCounter++ +# Essential: Represents a decoration that applies to every marker on a given +# layer. Created via {TextEditor::decorateMarkerLayer}. module.exports = class LayerDecoration constructor: (@markerLayer, @displayBuffer, @properties) -> @@ -11,6 +13,7 @@ class LayerDecoration @markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy() @overridePropertiesByMarkerId = {} + # Essential: Destroys the decoration. destroy: -> return if @destroyed @markerLayerDestroyedDisposable.dispose() @@ -18,20 +21,37 @@ class LayerDecoration @destroyed = true @displayBuffer.didDestroyLayerDecoration(this) + # Essential: Determine whether this decoration is destroyed. + # + # Returns a {Boolean}. isDestroyed: -> @destroyed getId: -> @id getMarkerLayer: -> @markerLayer + # Essential: Get this decoration's properties. + # + # Returns an {Object}. getProperties: -> @properties + # Essential: Set this decoration's properties. + # + # * `newProperties` See {TextEditor::decorateMarker} for more information on + # the properties. The `type` of `gutter` and `overlay` are not supported on + # layer decorations. setProperties: (newProperties) -> return if @destroyed @properties = newProperties @displayBuffer.scheduleUpdateDecorationsEvent() + # Essential: Override the decoration properties for a specific marker. + # + # * `marker` The {TextEditorMarker} or {Marker} for which to override + # properties. + # * `properties` An {Object} containing properties to apply to this marker. + # Pass `null` to clear the override. setPropertiesForMarker: (marker, properties) -> return if @destroyed if properties? diff --git a/src/text-editor-marker-layer.coffee b/src/text-editor-marker-layer.coffee index 9b099637c..29ba3c8ef 100644 --- a/src/text-editor-marker-layer.coffee +++ b/src/text-editor-marker-layer.coffee @@ -1,43 +1,139 @@ TextEditorMarker = require './text-editor-marker' +# Public: *Experimental:* A container for a related set of markers at the +# {TextEditor} level. Wraps an underlying {MarkerLayer} on the editor's +# {TextBuffer}. +# +# This API is experimental and subject to change on any release. module.exports = class TextEditorMarkerLayer constructor: (@displayBuffer, @bufferMarkerLayer, @isDefaultLayer) -> @id = @bufferMarkerLayer.id @markersById = {} + ### + Section: Lifecycle + ### + + # Essential: Destroy this layer. + destroy: -> + if @isDefaultLayer + marker.destroy() for id, marker of @markersById + else + @bufferMarkerLayer.destroy() + + ### + Section: Querying + ### + + # Essential: Get an existing marker by its id. + # + # Returns a {TextEditorMarker}. getMarker: (id) -> if editorMarker = @markersById[id] editorMarker else if bufferMarker = @bufferMarkerLayer.getMarker(id) @markersById[id] = new TextEditorMarker(this, bufferMarker) + # Essential: Get all markers in the layer. + # + # Returns an {Array} of {TextEditorMarker}s. getMarkers: -> @bufferMarkerLayer.getMarkers().map ({id}) => @getMarker(id) - markBufferRange: (bufferRange, options) -> - @getMarker(@bufferMarkerLayer.markRange(bufferRange, options).id) - - markScreenRange: (screenRange, options) -> - bufferRange = @displayBuffer.bufferRangeForScreenRange(screenRange) - @markBufferRange(bufferRange, options) - - markBufferPosition: (bufferPosition, options) -> - @getMarker(@bufferMarkerLayer.markPosition(bufferPosition, options).id) - - markScreenPosition: (screenPosition, options) -> - bufferPosition = @displayBuffer.bufferPositionForScreenPosition(screenPosition) - @markBufferPosition(bufferPosition, options) + # Public: Get the number of markers in the marker layer. + # + # Returns a {Number}. + getMarkerCount: -> + @bufferMarkerLayer.getMarkerCount() + # Public: Find markers in the layer conforming to the given parameters. + # + # See the documentation for {TextEditor::findMarkers}. findMarkers: (params) -> params = @translateToBufferMarkerParams(params) @bufferMarkerLayer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id) - destroy: -> - if @isDefaultLayer - marker.destroy() for id, marker of @markersById - else - @bufferMarkerLayer.destroy() + ### + Section: Marker creation + ### + + # Essential: Create a marker on this layer with the given range in buffer + # coordinates. + # + # See the documentation for {TextEditor::markBufferRange} + markBufferRange: (bufferRange, options) -> + @getMarker(@bufferMarkerLayer.markRange(bufferRange, options).id) + + # Essential: Create a marker on this layer with the given range in screen + # coordinates. + # + # See the documentation for {TextEditor::markScreenRange} + markScreenRange: (screenRange, options) -> + bufferRange = @displayBuffer.bufferRangeForScreenRange(screenRange) + @markBufferRange(bufferRange, options) + + # Public: Create a marker on this layer with the given buffer position and no + # tail. + # + # See the documentation for {TextEditor::markBufferPosition} + markBufferPosition: (bufferPosition, options) -> + @getMarker(@bufferMarkerLayer.markPosition(bufferPosition, options).id) + + # Public: Create a marker on this layer with the given screen position and no + # tail. + # + # See the documentation for {TextEditor::markScreenPosition} + markScreenPosition: (screenPosition, options) -> + bufferPosition = @displayBuffer.bufferPositionForScreenPosition(screenPosition) + @markBufferPosition(bufferPosition, options) + + ### + Section: Event Subscription + ### + + # Public: Subscribe to be notified asynchronously whenever markers are + # created, updated, or destroyed on this layer. *Prefer this method for + # optimal performance when interacting with layers that could contain large + # numbers of markers.* + # + # * `callback` A {Function} that will be called with no arguments when changes + # occur on this layer. + # + # Subscribers are notified once, asynchronously when any number of changes + # occur in a given tick of the event loop. You should re-query the layer + # to determine the state of markers in which you're interested in. It may + # be counter-intuitive, but this is much more efficient than subscribing to + # events on individual markers, which are expensive to deliver. + # + # Returns a {Disposable}. + onDidUpdate: (callback) -> + @bufferMarkerLayer.onDidUpdate(callback) + + # Public: Subscribe to be notified synchronously whenever markers are created + # on this layer. *Avoid this method for optimal performance when interacting + # with layers that could contain large numbers of markers.* + # + # * `callback` A {Function} that will be called with a {TextEditorMarker} + # whenever a new marker is created. + # + # You should prefer {onDidUpdate} when synchronous notifications aren't + # absolutely necessary. + # + # Returns a {Disposable}. + onDidCreateMarker: (callback) -> + @bufferMarkerLayer.onDidCreateMarker (bufferMarker) => + callback(@getMarker(bufferMarker.id)) + + # Public: Subscribe to be notified synchronously when this layer is destroyed. + # + # Returns a {Disposable}. + onDidDestroy: (callback) -> + @bufferMarkerLayer.onDidDestroy(callback) + + ### + Section: Private + ### refreshMarkerScreenPositions: -> for marker in @getMarkers() @@ -84,10 +180,3 @@ class TextEditorMarkerLayer bufferMarkerParams[key] = value bufferMarkerParams - - onDidCreateMarker: (callback) -> - @bufferMarkerLayer.onDidCreateMarker (bufferMarker) => - callback(@getMarker(bufferMarker.id)) - - onDidUpdate: (callback) -> - @bufferMarkerLayer.onDidUpdate(callback) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 8cc80f142..d44791013 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -1403,9 +1403,9 @@ class TextEditor extends Model Section: Decorations ### - # Essential: Adds a decoration that tracks a {TextEditorMarker}. When the marker moves, - # is invalidated, or is destroyed, the decoration will be updated to reflect - # the marker's state. + # Essential: Add a decoration that tracks a {TextEditorMarker}. When the + # marker moves, is invalidated, or is destroyed, the decoration will be + # updated to reflect the marker's state. # # The following are the supported decorations types: # @@ -1469,10 +1469,22 @@ class TextEditor extends Model decorateMarker: (marker, decorationParams) -> @displayBuffer.decorateMarker(marker, decorationParams) + # Essential: *Experimental:* Add a decoration to every marker in the given + # marker layer. Can be used to decorate a large number of markers without + # having to create and manage many individual decorations. + # + # * `markerLayer` A {TextEditorMarkerLayer} or {MarkerLayer} to decorate. + # * `decorationParams` The same parameters that are passed to + # {decorateMarker}, except the `type` cannot be `overlay` or `gutter`. + # + # This API is experimental and subject to change on any release. + # + # Returns a {LayerDecoration}. decorateMarkerLayer: (markerLayer, decorationParams) -> @displayBuffer.decorateMarkerLayer(markerLayer, decorationParams) - # Essential: Get all the decorations within a screen row range. + # Deprecated: Get all the decorations within a screen row range on the default + # layer. # # * `startScreenRow` the {Number} beginning screen row # * `endScreenRow` the {Number} end screen row (inclusive) @@ -1543,10 +1555,10 @@ class TextEditor extends Model Section: Markers ### - # Essential: Create a marker with the given range in buffer coordinates. This - # marker will maintain its logical location as the buffer is changed, so if - # you mark a particular word, the marker will remain over that word even if - # the word's location in the buffer changes. + # Essential: Create a marker on the default marker layer with the given range + # in buffer coordinates. This marker will maintain its logical location as the + # buffer is changed, so if you mark a particular word, the marker will remain + # over that word even if the word's location in the buffer changes. # # * `range` A {Range} or range-compatible {Array} # * `properties` A hash of key-value pairs to associate with the marker. There @@ -1578,10 +1590,10 @@ class TextEditor extends Model markBufferRange: (args...) -> @displayBuffer.markBufferRange(args...) - # Essential: Create a marker with the given range in screen coordinates. This - # marker will maintain its logical location as the buffer is changed, so if - # you mark a particular word, the marker will remain over that word even if - # the word's location in the buffer changes. + # Essential: Create a marker on the default marker layer with the given range + # in screen coordinates. This marker will maintain its logical location as the + # buffer is changed, so if you mark a particular word, the marker will remain + # over that word even if the word's location in the buffer changes. # # * `range` A {Range} or range-compatible {Array} # * `properties` A hash of key-value pairs to associate with the marker. There @@ -1613,7 +1625,8 @@ class TextEditor extends Model markScreenRange: (args...) -> @displayBuffer.markScreenRange(args...) - # Essential: Mark the given position in buffer coordinates. + # Essential: Mark the given position in buffer coordinates on the default + # marker layer. # # * `position` A {Point} or {Array} of `[row, column]`. # * `options` (optional) See {TextBuffer::markRange}. @@ -1622,7 +1635,8 @@ class TextEditor extends Model markBufferPosition: (args...) -> @displayBuffer.markBufferPosition(args...) - # Essential: Mark the given position in screen coordinates. + # Essential: Mark the given position in screen coordinates on the default + # marker layer. # # * `position` A {Point} or {Array} of `[row, column]`. # * `options` (optional) See {TextBuffer::markRange}. @@ -1631,7 +1645,8 @@ class TextEditor extends Model markScreenPosition: (args...) -> @displayBuffer.markScreenPosition(args...) - # Essential: Find all {TextEditorMarker}s that match the given properties. + # Essential: Find all {TextEditorMarker}s on the default marker layer that + # match the given properties. # # This method finds markers based on the given properties. Markers can be # associated with custom properties that will be compared with basic equality. @@ -1653,36 +1668,19 @@ class TextEditor extends Model findMarkers: (properties) -> @displayBuffer.findMarkers(properties) - # Extended: Observe changes in the set of markers that intersect a particular - # region of the editor. - # - # * `callback` A {Function} to call whenever one or more {TextEditorMarker}s appears, - # disappears, or moves within the given region. - # * `event` An {Object} with the following keys: - # * `insert` A {Set} containing the ids of all markers that appeared - # in the range. - # * `update` A {Set} containing the ids of all markers that moved within - # the region. - # * `remove` A {Set} containing the ids of all markers that disappeared - # from the region. - # - # Returns a {MarkerObservationWindow}, which allows you to specify the region - # of interest by calling {MarkerObservationWindow::setBufferRange} or - # {MarkerObservationWindow::setScreenRange}. - observeMarkers: (callback) -> - @displayBuffer.observeMarkers(callback) - - # Extended: Get the {TextEditorMarker} for the given marker id. + # Extended: Get the {TextEditorMarker} on the default layer for the given + # marker id. # # * `id` {Number} id of the marker getMarker: (id) -> @displayBuffer.getMarker(id) - # Extended: Get all {TextEditorMarker}s. Consider using {::findMarkers} + # Extended: Get all {TextEditorMarker}s on the default marker layer. Consider + # using {::findMarkers} getMarkers: -> @displayBuffer.getMarkers() - # Extended: Get the number of markers in this editor's buffer. + # Extended: Get the number of markers in the default marker layer. # # Returns a {Number}. getMarkerCount: -> @@ -1691,12 +1689,40 @@ class TextEditor extends Model destroyMarker: (id) -> @getMarker(id)?.destroy() + # Extended: *Experimental:* Create a marker layer to group related markers. + # + # * `options` An {Object} containing the following keys: + # * `maintainHistory` A {Boolean} indicating whether marker state should be + # restored on undo/redo. Defaults to `false`. + # + # This API is experimental and subject to change on any release. + # + # Returns a {TextEditorMarkerLayer}. addMarkerLayer: (options) -> @displayBuffer.addMarkerLayer(options) + # Public: *Experimental:* Get a {TextEditorMarkerLayer} by id. + # + # * `id` The id of the marker layer to retrieve. + # + # This API is experimental and subject to change on any release. + # + # Returns a {MarkerLayer} or `undefined` if no layer exists with the given + # id. getMarkerLayer: (id) -> @displayBuffer.getMarkerLayer(id) + # Public: *Experimental:* Get the default {TextEditorMarkerLayer}. + # + # All marker APIs not tied to an explicit layer interact with this default + # layer. + # + # This API is experimental and subject to change on any release. + # + # Returns a {TextEditorMarkerLayer}. + getDefaultMarkerLayer: -> + @displayBuffer.getDefaultMarkerLayer() + ### Section: Cursors ### From 3a4199a0cdba4347e36f3840c470028bfe26da00 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Nov 2015 16:17:57 -0800 Subject: [PATCH 34/53] Allow (start/end)(Buffer/Screen)Position in findMarker queries on layers --- src/text-editor-marker-layer.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/text-editor-marker-layer.coffee b/src/text-editor-marker-layer.coffee index 29ba3c8ef..e99ad7323 100644 --- a/src/text-editor-marker-layer.coffee +++ b/src/text-editor-marker-layer.coffee @@ -147,6 +147,16 @@ class TextEditorMarkerLayer bufferMarkerParams = {} for key, value of params switch key + when 'startBufferPosition' + key = 'startPosition' + when 'endBufferPosition' + key = 'endPosition' + when 'startScreenPosition' + key = 'startPosition' + value = @displayBuffer.bufferPositionForScreenPosition(value) + when 'endScreenPosition' + key = 'endPosition' + value = @displayBuffer.bufferPositionForScreenPosition(value) when 'startBufferRow' key = 'startRow' when 'endBufferRow' From 80adbe8562e95fb604dc814449b68d06d6e20b50 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 6 Nov 2015 12:18:10 -0700 Subject: [PATCH 35/53] :arrow_up: find-and-replace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 69466ed60..15c659f13 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "dev-live-reload": "0.47.0", "encoding-selector": "0.21.0", "exception-reporting": "0.37.0", - "find-and-replace": "0.190.0", + "find-and-replace": "0.191.0", "fuzzy-finder": "0.93.0", "git-diff": "0.57.0", "go-to-line": "0.30.0", From 7edb80155a4b4c418d99787cf72b22e27e01db2a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 6 Nov 2015 15:41:32 -0700 Subject: [PATCH 36/53] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 15c659f13..57963d755 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "service-hub": "^0.7.0", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "^8.0.2", + "text-buffer": "^8.0.3", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "yargs": "^3.23.0" From 51a7c9ea6805bf3210f941440c98cbec4aeda427 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 6 Nov 2015 16:01:21 -0700 Subject: [PATCH 37/53] Always set devMode to true for spec windows --- src/browser/atom-application.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index b68ee2c73..8bb44349e 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -496,7 +496,7 @@ class AtomApplication # :specPath - The directory to load specs from. # :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages # and ~/.atom/dev/packages, defaults to false. - runTests: ({headless, devMode, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout}) -> + runTests: ({headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout}) -> if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath) resourcePath = @resourcePath @@ -523,6 +523,7 @@ class AtomApplication legacyTestRunnerPath = @resolveLegacyTestRunnerPath() testRunnerPath = @resolveTestRunnerPath(testPaths[0]) + devMode = true isSpec = true safeMode ?= false new AtomWindow({windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode}) From bddb601b45742e927ad089d34dccb607efa748b5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 6 Nov 2015 16:04:54 -0700 Subject: [PATCH 38/53] :arrow_up: snippets --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 57963d755..25cc899f4 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "package-generator": "0.41.0", "release-notes": "0.53.0", "settings-view": "0.231.0", - "snippets": "0.101.0", + "snippets": "0.101.1", "spell-check": "0.62.0", "status-bar": "0.80.0", "styleguide": "0.45.0", From e9dfc080a30bd9ddaafe1c829780d39b7d627a6b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 6 Nov 2015 17:29:12 -0700 Subject: [PATCH 39/53] Convert text-editor-component-spec to Babel for async/await It's much easier to reason about async/await than Jasmine's built-in queuing system, and using them made it easier to debug flaky async tests. --- spec/async-spec-helpers.coffee | 28 + spec/text-editor-component-spec.coffee | 4110 -------------------- spec/text-editor-component-spec.js | 4735 ++++++++++++++++++++++++ src/display-buffer.coffee | 2 +- src/view-registry.coffee | 4 +- 5 files changed, 4767 insertions(+), 4112 deletions(-) create mode 100644 spec/async-spec-helpers.coffee delete mode 100644 spec/text-editor-component-spec.coffee create mode 100644 spec/text-editor-component-spec.js diff --git a/spec/async-spec-helpers.coffee b/spec/async-spec-helpers.coffee new file mode 100644 index 000000000..9dcff9a69 --- /dev/null +++ b/spec/async-spec-helpers.coffee @@ -0,0 +1,28 @@ +exports.beforeEach = (fn) -> + global.beforeEach -> + result = fn() + if result instanceof Promise + waitsForPromise(-> result) + +exports.afterEach = (fn) -> + global.afterEach -> + result = fn() + if result instanceof Promise + waitsForPromise(-> result) + +['it', 'fit', 'ffit', 'fffit'].forEach (name) -> + exports[name] = (description, fn) -> + global[name] description, -> + result = fn() + if result instanceof Promise + waitsForPromise(-> result) + +waitsForPromise = (fn) -> + promise = fn() + waitsFor 10000, (done) -> + promise.then( + done, + (error) -> + jasmine.getEnv().currentSpec.fail(error) + done() + ) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee deleted file mode 100644 index 13098836d..000000000 --- a/spec/text-editor-component-spec.coffee +++ /dev/null @@ -1,4110 +0,0 @@ -_ = require 'underscore-plus' -{extend, flatten, toArray, last} = _ - -TextEditorElement = require '../src/text-editor-element' -nbsp = String.fromCharCode(160) - -describe "TextEditorComponent", -> - [contentNode, editor, wrapperNode, component, componentNode, verticalScrollbarNode, horizontalScrollbarNode] = [] - [lineHeightInPixels, charWidth, tileSize, tileHeightInPixels] = [] - - beforeEach -> - tileSize = 3 - jasmine.useRealClock() - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - contentNode = document.querySelector('#jasmine-content') - contentNode.style.width = '1000px' - - wrapperNode = new TextEditorElement() - wrapperNode.tileSize = tileSize - wrapperNode.initialize(editor, atom) - wrapperNode.setUpdatedSynchronously(false) - jasmine.attachToDOM(wrapperNode) - - {component} = wrapperNode - component.setFontFamily('monospace') - component.setLineHeight(1.3) - component.setFontSize(20) - - lineHeightInPixels = editor.getLineHeightInPixels() - tileHeightInPixels = tileSize * lineHeightInPixels - charWidth = editor.getDefaultCharWidth() - componentNode = component.getDomNode() - verticalScrollbarNode = componentNode.querySelector('.vertical-scrollbar') - horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') - - component.measureDimensions() - waitsForNextDOMUpdate() - - afterEach -> - contentNode.style.width = '' - - describe "async updates", -> - it "handles corrupted state gracefully", -> - # trigger state updates, e.g. presenter.updateLinesState - editor.insertNewline() - - # simulate state corruption - component.presenter.startRow = -1 - component.presenter.endRow = 9999 - waitsForNextDOMUpdate() - - it "doesn't update when an animation frame was requested but the component got destroyed before its delivery", -> - editor.setText("You shouldn't see this update.") - component.destroy() - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(0).textContent).not.toBe("You shouldn't see this update.") - - describe "line rendering", -> - expectTileContainsRow = (tileNode, screenRow, {top}) -> - lineNode = tileNode.querySelector("[data-screen-row='#{screenRow}']") - tokenizedLine = editor.tokenizedLineForScreenRow(screenRow) - - expect(lineNode.offsetTop).toBe(top) - if tokenizedLine.text is "" - expect(lineNode.innerHTML).toBe(" ") - else - expect(lineNode.textContent).toBe(tokenizedLine.text) - - it "gives the lines container the same height as the wrapper node", -> - linesNode = componentNode.querySelector(".lines") - - wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) - - wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) - - it "renders higher tiles in front of lower ones", -> - wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - tilesNodes = component.tileNodesForLines() - - expect(tilesNodes[0].style.zIndex).toBe("2") - expect(tilesNodes[1].style.zIndex).toBe("1") - expect(tilesNodes[2].style.zIndex).toBe("0") - - verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForNextDOMUpdate() - - runs -> - tilesNodes = component.tileNodesForLines() - - expect(tilesNodes[0].style.zIndex).toBe("3") - expect(tilesNodes[1].style.zIndex).toBe("2") - expect(tilesNodes[2].style.zIndex).toBe("1") - expect(tilesNodes[3].style.zIndex).toBe("0") - - it "renders the currently-visible lines in a tiled fashion", -> - wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - tilesNodes = component.tileNodesForLines() - - expect(tilesNodes.length).toBe(3) - - expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(tilesNodes[0].querySelectorAll(".line").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) - - expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)" - expect(tilesNodes[1].querySelectorAll(".line").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) - - expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels}px, 0px)" - expect(tilesNodes[2].querySelectorAll(".line").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) - - expect(component.lineNodeForScreenRow(9)).toBeUndefined() - - verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5 - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForNextDOMUpdate() - - runs -> - tilesNodes = component.tileNodesForLines() - - expect(component.lineNodeForScreenRow(2)).toBeUndefined() - expect(tilesNodes.length).toBe(3) - - expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{0 * tileHeightInPixels - 5}px, 0px)" - expect(tilesNodes[0].querySelectorAll(".line").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[0], 3, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 4, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 5, top: 2 * lineHeightInPixels) - - expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels - 5}px, 0px)" - expect(tilesNodes[1].querySelectorAll(".line").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[1], 6, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 7, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 8, top: 2 * lineHeightInPixels) - - expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels - 5}px, 0px)" - expect(tilesNodes[2].querySelectorAll(".line").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[2], 9, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[2], 10, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[2], 11, top: 2 * lineHeightInPixels) - - it "updates the top position of subsequent tiles when lines are inserted or removed", -> - wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' - component.measureDimensions() - editor.getBuffer().deleteRows(0, 1) - waitsForNextDOMUpdate() - - runs -> - tilesNodes = component.tileNodesForLines() - - expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) - - expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)" - expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) - - editor.getBuffer().insert([0, 0], '\n\n') - waitsForNextDOMUpdate() - - runs -> - tilesNodes = component.tileNodesForLines() - - expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) - - expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)" - expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) - - expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels}px, 0px)" - expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) - expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) - - it "updates the lines when lines are inserted or removed above the rendered row range", -> - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForNextDOMUpdate() - - buffer = null - runs -> - buffer = editor.getBuffer() - buffer.insert([0, 0], '\n\n') - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(3).textContent).toBe editor.tokenizedLineForScreenRow(3).text - - buffer.delete([[0, 0], [3, 0]]) - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(3).textContent).toBe editor.tokenizedLineForScreenRow(3).text - - it "updates the top position of lines when the line height changes", -> - initialLineHeightInPixels = editor.getLineHeightInPixels() - component.setLineHeight(2) - waitsForNextDOMUpdate() - - runs -> - newLineHeightInPixels = editor.getLineHeightInPixels() - expect(newLineHeightInPixels).not.toBe initialLineHeightInPixels - expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * newLineHeightInPixels - - it "updates the top position of lines when the font size changes", -> - initialLineHeightInPixels = editor.getLineHeightInPixels() - component.setFontSize(10) - waitsForNextDOMUpdate() - - runs -> - newLineHeightInPixels = editor.getLineHeightInPixels() - expect(newLineHeightInPixels).not.toBe initialLineHeightInPixels - expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * newLineHeightInPixels - - it "renders the .lines div at the full height of the editor if there aren't enough lines to scroll vertically", -> - editor.setText('') - wrapperNode.style.height = '300px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - linesNode = componentNode.querySelector('.lines') - expect(linesNode.offsetHeight).toBe 300 - - it "assigns the width of each line so it extends across the full width of the editor", -> - gutterWidth = componentNode.querySelector('.gutter').offsetWidth - scrollViewNode = componentNode.querySelector('.scroll-view') - lineNodes = componentNode.querySelectorAll('.line') - - componentNode.style.width = gutterWidth + (30 * charWidth) + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollWidth()).toBeGreaterThan scrollViewNode.offsetWidth - - # At the time of writing, using width: 100% to achieve the full-width - # lines caused full-screen repaints after switching away from an editor - # and back again Please ensure you don't cause a performance regression if - # you change this behavior. - editorFullWidth = wrapperNode.getScrollWidth() + wrapperNode.getVerticalScrollbarWidth() - - for lineNode in lineNodes - expect(lineNode.getBoundingClientRect().width).toBe(editorFullWidth) - - componentNode.style.width = gutterWidth + wrapperNode.getScrollWidth() + 100 + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - scrollViewWidth = scrollViewNode.offsetWidth - - for lineNode in lineNodes - expect(lineNode.getBoundingClientRect().width).toBe(scrollViewWidth) - - it "renders an nbsp on empty lines when no line-ending character is defined", -> - atom.config.set("editor.showInvisibles", false) - expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp - - it "gives the lines and tiles divs the same background color as the editor to improve GPU performance", -> - linesNode = componentNode.querySelector('.lines') - backgroundColor = getComputedStyle(wrapperNode).backgroundColor - expect(linesNode.style.backgroundColor).toBe backgroundColor - - for tileNode in component.tileNodesForLines() - expect(tileNode.style.backgroundColor).toBe(backgroundColor) - - wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)' - waitsForNextDOMUpdate() - - runs -> - expect(linesNode.style.backgroundColor).toBe 'rgb(255, 0, 0)' - for tileNode in component.tileNodesForLines() - expect(tileNode.style.backgroundColor).toBe("rgb(255, 0, 0)") - - it "applies .leading-whitespace for lines with leading spaces and/or tabs", -> - editor.setText(' a') - waitsForNextDOMUpdate() - - runs -> - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe true - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe false - - editor.setText('\ta') - waitsForNextDOMUpdate() - - runs -> - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe true - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe false - - it "applies .trailing-whitespace for lines with trailing spaces and/or tabs", -> - editor.setText(' ') - waitsForNextDOMUpdate() - - runs -> - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false - - editor.setText('\t') - waitsForNextDOMUpdate() - - runs -> - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false - - editor.setText('a ') - waitsForNextDOMUpdate() - - runs -> - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false - - editor.setText('a\t') - waitsForNextDOMUpdate() - - runs -> - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe true - expect(leafNodes[0].classList.contains('leading-whitespace')).toBe false - - it "keeps rebuilding lines when continuous reflow is on", -> - wrapperNode.setContinuousReflow(true) - - oldLineNodes = componentNode.querySelectorAll(".line") - - waits 300 - - runs -> - newLineNodes = componentNode.querySelectorAll(".line") - expect(oldLineNodes).not.toEqual(newLineNodes) - - wrapperNode.setContinuousReflow(false) - - describe "when showInvisibles is enabled", -> - invisibles = null - - beforeEach -> - invisibles = - eol: 'E' - space: 'S' - tab: 'T' - cr: 'C' - - atom.config.set("editor.showInvisibles", true) - atom.config.set("editor.invisibles", invisibles) - waitsForNextDOMUpdate() - - it "re-renders the lines when the showInvisibles config option changes", -> - editor.setText " a line with tabs\tand spaces \n" - waitsForNextDOMUpdate() - - runs -> - 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) - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces " - - atom.config.set("editor.showInvisibles", true) - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}" - - it "displays leading/trailing spaces, tabs, and newlines as visible characters", -> - editor.setText " a line with tabs\tand spaces \n" - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(0).textContent).toBe "#{invisibles.space}a line with tabs#{invisibles.tab}and spaces#{invisibles.space}#{invisibles.eol}" - - leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('invisible-character')).toBe true - expect(leafNodes[leafNodes.length - 1].classList.contains('invisible-character')).toBe true - - it "displays newlines as their own token outside of the other tokens' scopeDescriptor", -> - editor.setText "var\n" - waitsForNextDOMUpdate() - - runs -> - 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" - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that ends with a carriage return#{invisibles.cr}#{invisibles.eol}" - - it "renders invisible line-ending characters on empty lines", -> - expect(component.lineNodeForScreenRow(10).textContent).toBe invisibles.eol - - it "renders an nbsp on empty lines when the line-ending character is an empty string", -> - atom.config.set("editor.invisibles", eol: '') - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp - - it "renders an nbsp on empty lines when the line-ending character is false", -> - atom.config.set("editor.invisibles", eol: false) - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(10).textContent).toBe nbsp - - it "interleaves invisible line-ending characters with indent guides on empty lines", -> - atom.config.set "editor.showIndentGuide", true - waitsForNextDOMUpdate() - - runs -> - editor.setTextInBufferRange([[10, 0], [11, 0]], "\r\n", normalizeLineEndings: false) - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' - - editor.setTabLength(3) - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE ' - - editor.setTabLength(1) - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' - - editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ') - editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ') - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(10).innerHTML).toBe 'CE' - - describe "when soft wrapping is enabled", -> - beforeEach -> - editor.setText "a line that wraps \n" - editor.setSoftWrapped(true) - waitsForNextDOMUpdate() - runs -> - componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - it "doesn't show end of line invisibles at the end of wrapped lines", -> - expect(component.lineNodeForScreenRow(0).textContent).toBe "a line that " - expect(component.lineNodeForScreenRow(1).textContent).toBe "wraps#{invisibles.space}#{invisibles.eol}" - - describe "when indent guides are enabled", -> - beforeEach -> - atom.config.set "editor.showIndentGuide", true - waitsForNextDOMUpdate() - - it "adds an 'indent-guide' class to spans comprising the leading whitespace", -> - line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) - expect(line1LeafNodes[0].textContent).toBe ' ' - expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false - - line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes[0].textContent).toBe ' ' - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[1].textContent).toBe ' ' - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe false - - it "renders leading whitespace spans with the 'indent-guide' class for empty lines", -> - editor.getBuffer().insert([1, Infinity], '\n') - waitsForNextDOMUpdate() - - runs -> - line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - - expect(line2LeafNodes.length).toBe 2 - expect(line2LeafNodes[0].textContent).toBe ' ' - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[1].textContent).toBe ' ' - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true - - it "renders indent guides correctly on lines containing only whitespace", -> - editor.getBuffer().insert([1, Infinity], '\n ') - waitsForNextDOMUpdate() - - runs -> - line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes.length).toBe 3 - expect(line2LeafNodes[0].textContent).toBe ' ' - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[1].textContent).toBe ' ' - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[2].textContent).toBe ' ' - expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe true - - it "renders indent guides correctly on lines containing only whitespace when invisibles are enabled", -> - atom.config.set 'editor.showInvisibles', true - atom.config.set 'editor.invisibles', space: '-', eol: 'x' - editor.getBuffer().insert([1, Infinity], '\n ') - - waitsForNextDOMUpdate() - - runs -> - line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes.length).toBe 4 - expect(line2LeafNodes[0].textContent).toBe '--' - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[1].textContent).toBe '--' - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[2].textContent).toBe '--' - expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe true - expect(line2LeafNodes[3].textContent).toBe 'x' - - it "does not render indent guides in trailing whitespace for lines containing non whitespace characters", -> - editor.getBuffer().setText " hi " - waitsForNextDOMUpdate() - - runs -> - line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(line0LeafNodes[0].textContent).toBe ' ' - expect(line0LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line0LeafNodes[1].textContent).toBe ' ' - expect(line0LeafNodes[1].classList.contains('indent-guide')).toBe false - - it "updates the indent guides on empty lines preceding an indentation change", -> - editor.getBuffer().insert([12, 0], '\n') - waitsForNextDOMUpdate() - - runs -> - editor.getBuffer().insert([13, 0], ' ') - waitsForNextDOMUpdate() - - runs -> - line12LeafNodes = getLeafNodes(component.lineNodeForScreenRow(12)) - expect(line12LeafNodes[0].textContent).toBe ' ' - expect(line12LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line12LeafNodes[1].textContent).toBe ' ' - expect(line12LeafNodes[1].classList.contains('indent-guide')).toBe true - - it "updates the indent guides on empty lines following an indentation change", -> - editor.getBuffer().insert([12, 2], '\n') - - waitsForNextDOMUpdate() - - runs -> - editor.getBuffer().insert([12, 0], ' ') - waitsForNextDOMUpdate() - - runs -> - line13LeafNodes = getLeafNodes(component.lineNodeForScreenRow(13)) - expect(line13LeafNodes[0].textContent).toBe ' ' - expect(line13LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line13LeafNodes[1].textContent).toBe ' ' - expect(line13LeafNodes[1].classList.contains('indent-guide')).toBe true - - describe "when indent guides are disabled", -> - beforeEach -> - expect(atom.config.get("editor.showIndentGuide")).toBe false - - it "does not render indent guides on lines containing only whitespace", -> - editor.getBuffer().insert([1, Infinity], '\n ') - - waitsForNextDOMUpdate() - - runs -> - line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) - expect(line2LeafNodes.length).toBe 3 - expect(line2LeafNodes[0].textContent).toBe ' ' - expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe false - expect(line2LeafNodes[1].textContent).toBe ' ' - expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe false - expect(line2LeafNodes[2].textContent).toBe ' ' - expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe false - - describe "when the buffer contains null bytes", -> - it "excludes the null byte from character measurement", -> - editor.setText("a\0b") - - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.pixelPositionForScreenPosition([0, Infinity]).left).toEqual 2 * charWidth - - describe "when there is a fold", -> - it "renders a fold marker on the folded line", -> - foldedLineNode = component.lineNodeForScreenRow(4) - expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() - - editor.foldBufferRow(4) - waitsForNextDOMUpdate() - - runs -> - foldedLineNode = component.lineNodeForScreenRow(4) - expect(foldedLineNode.querySelector('.fold-marker')).toBeTruthy() - - editor.unfoldBufferRow(4) - waitsForNextDOMUpdate() - - runs -> - foldedLineNode = component.lineNodeForScreenRow(4) - expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() - - describe "gutter rendering", -> - expectTileContainsRow = (tileNode, screenRow, {top, text}) -> - lineNode = tileNode.querySelector("[data-screen-row='#{screenRow}']") - - expect(lineNode.offsetTop).toBe(top) - expect(lineNode.textContent).toBe(text) - - it "renders higher tiles in front of lower ones", -> - wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - tilesNodes = component.tileNodesForLineNumbers() - - expect(tilesNodes[0].style.zIndex).toBe("2") - expect(tilesNodes[1].style.zIndex).toBe("1") - expect(tilesNodes[2].style.zIndex).toBe("0") - - verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForNextDOMUpdate() - - runs -> - tilesNodes = component.tileNodesForLineNumbers() - - expect(tilesNodes[0].style.zIndex).toBe("3") - expect(tilesNodes[1].style.zIndex).toBe("2") - expect(tilesNodes[2].style.zIndex).toBe("1") - expect(tilesNodes[3].style.zIndex).toBe("0") - - it "gives the line numbers container the same height as the wrapper node", -> - linesNode = componentNode.querySelector(".line-numbers") - - wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) - - wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) - - it "renders the currently-visible line numbers in a tiled fashion", -> - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - tilesNodes = component.tileNodesForLineNumbers() - - expect(tilesNodes.length).toBe(3) - expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - - expect(tilesNodes[0].querySelectorAll('.line-number').length).toBe 3 - expectTileContainsRow(tilesNodes[0], 0, top: lineHeightInPixels * 0, text: "#{nbsp}1") - expectTileContainsRow(tilesNodes[0], 1, top: lineHeightInPixels * 1, text: "#{nbsp}2") - expectTileContainsRow(tilesNodes[0], 2, top: lineHeightInPixels * 2, text: "#{nbsp}3") - - expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels}px, 0px)" - expect(tilesNodes[1].querySelectorAll('.line-number').length).toBe 3 - expectTileContainsRow(tilesNodes[1], 3, top: lineHeightInPixels * 0, text: "#{nbsp}4") - expectTileContainsRow(tilesNodes[1], 4, top: lineHeightInPixels * 1, text: "#{nbsp}5") - expectTileContainsRow(tilesNodes[1], 5, top: lineHeightInPixels * 2, text: "#{nbsp}6") - - expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels}px, 0px)" - expect(tilesNodes[2].querySelectorAll('.line-number').length).toBe 3 - expectTileContainsRow(tilesNodes[2], 6, top: lineHeightInPixels * 0, text: "#{nbsp}7") - expectTileContainsRow(tilesNodes[2], 7, top: lineHeightInPixels * 1, text: "#{nbsp}8") - expectTileContainsRow(tilesNodes[2], 8, top: lineHeightInPixels * 2, text: "#{nbsp}9") - - verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5 - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForNextDOMUpdate() - - runs -> - tilesNodes = component.tileNodesForLineNumbers() - - expect(component.lineNumberNodeForScreenRow(2)).toBeUndefined() - expect(tilesNodes.length).toBe(3) - - expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, #{0 * tileHeightInPixels - 5}px, 0px)" - expect(tilesNodes[0].querySelectorAll(".line-number").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[0], 3, top: lineHeightInPixels * 0, text: "#{nbsp}4") - expectTileContainsRow(tilesNodes[0], 4, top: lineHeightInPixels * 1, text: "#{nbsp}5") - expectTileContainsRow(tilesNodes[0], 5, top: lineHeightInPixels * 2, text: "#{nbsp}6") - - expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeightInPixels - 5}px, 0px)" - expect(tilesNodes[1].querySelectorAll(".line-number").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[1], 6, top: 0 * lineHeightInPixels, text: "#{nbsp}7") - expectTileContainsRow(tilesNodes[1], 7, top: 1 * lineHeightInPixels, text: "#{nbsp}8") - expectTileContainsRow(tilesNodes[1], 8, top: 2 * lineHeightInPixels, text: "#{nbsp}9") - - expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeightInPixels - 5}px, 0px)" - expect(tilesNodes[2].querySelectorAll(".line-number").length).toBe(tileSize) - expectTileContainsRow(tilesNodes[2], 9, top: 0 * lineHeightInPixels, text: "10") - expectTileContainsRow(tilesNodes[2], 10, top: 1 * lineHeightInPixels, text: "11") - expectTileContainsRow(tilesNodes[2], 11, top: 2 * lineHeightInPixels, text: "12") - - it "updates the translation of subsequent line numbers when lines are inserted or removed", -> - editor.getBuffer().insert([0, 0], '\n\n') - waitsForNextDOMUpdate() - - runs -> - lineNumberNodes = componentNode.querySelectorAll('.line-number') - expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 0 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 2 * lineHeightInPixels - - editor.getBuffer().insert([0, 0], '\n\n') - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe 0 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe 2 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(6).offsetTop).toBe 0 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(7).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNumberNodeForScreenRow(8).offsetTop).toBe 2 * lineHeightInPixels - - it "renders • characters for soft-wrapped lines", -> - editor.setSoftWrapped(true) - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 30 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelectorAll('.line-number').length).toBe 9 + 1 # 3 line-numbers tiles + 1 dummy line - expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" - expect(component.lineNumberNodeForScreenRow(1).textContent).toBe "#{nbsp}•" - expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}2" - expect(component.lineNumberNodeForScreenRow(3).textContent).toBe "#{nbsp}•" - expect(component.lineNumberNodeForScreenRow(4).textContent).toBe "#{nbsp}3" - expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}•" - expect(component.lineNumberNodeForScreenRow(6).textContent).toBe "#{nbsp}4" - expect(component.lineNumberNodeForScreenRow(7).textContent).toBe "#{nbsp}•" - expect(component.lineNumberNodeForScreenRow(8).textContent).toBe "#{nbsp}•" - - it "pads line numbers to be right-justified based on the maximum number of line number digits", -> - editor.getBuffer().setText([1..10].join('\n')) - - waitsForNextDOMUpdate() - - [gutterNode, initialGutterWidth] = [] - - runs -> - for screenRow in [0..8] - expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" - expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" - - gutterNode = componentNode.querySelector('.gutter') - initialGutterWidth = gutterNode.offsetWidth - - # Removes padding when the max number of digits goes down - editor.getBuffer().delete([[1, 0], [2, 0]]) - waitsForNextDOMUpdate() - - runs -> - 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') - waitsForNextDOMUpdate() - - runs -> - for screenRow in [0..8] - expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe "#{nbsp}#{screenRow + 1}" - expect(component.lineNumberNodeForScreenRow(9).textContent).toBe "10" - expect(gutterNode.offsetWidth).toBe initialGutterWidth - - it "renders the .line-numbers div at the full height of the editor even if it's taller than its content", -> - wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelector('.line-numbers').offsetHeight).toBe componentNode.offsetHeight - - it "applies the background color of the gutter or the editor to the line numbers to improve GPU performance", -> - gutterNode = componentNode.querySelector('.gutter') - lineNumbersNode = gutterNode.querySelector('.line-numbers') - {backgroundColor} = getComputedStyle(wrapperNode) - expect(lineNumbersNode.style.backgroundColor).toBe backgroundColor - for tileNode in component.tileNodesForLineNumbers() - expect(tileNode.style.backgroundColor).toBe(backgroundColor) - - # favor gutter color if it's assigned - gutterNode.style.backgroundColor = 'rgb(255, 0, 0)' - atom.views.performDocumentPoll() # required due to DOM change not being detected inside shadow DOM - waitsForNextDOMUpdate() - - runs -> - expect(lineNumbersNode.style.backgroundColor).toBe 'rgb(255, 0, 0)' - for tileNode in component.tileNodesForLineNumbers() - expect(tileNode.style.backgroundColor).toBe("rgb(255, 0, 0)") - - it "hides or shows the gutter based on the '::isLineNumberGutterVisible' property on the model and the global 'editor.showLineNumbers' config setting", -> - expect(component.gutterContainerComponent.getLineNumberGutterComponent()?).toBe true - - editor.setLineNumberGutterVisible(false) - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelector('.gutter').style.display).toBe 'none' - - atom.config.set("editor.showLineNumbers", false) - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelector('.gutter').style.display).toBe 'none' - - editor.setLineNumberGutterVisible(true) - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelector('.gutter').style.display).toBe 'none' - - atom.config.set("editor.showLineNumbers", true) - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelector('.gutter').style.display).toBe '' - expect(component.lineNumberNodeForScreenRow(3)?).toBe true - - it "keeps rebuilding line numbers when continuous reflow is on", -> - wrapperNode.setContinuousReflow(true) - - oldLineNodes = componentNode.querySelectorAll(".line-number") - - waits 300 - - runs -> - newLineNodes = componentNode.querySelectorAll(".line-number") - expect(oldLineNodes).not.toEqual(newLineNodes) - - describe "fold decorations", -> - describe "rendering fold decorations", -> - it "adds the foldable class to line numbers when the line is foldable", -> - expect(lineNumberHasClass(0, 'foldable')).toBe true - expect(lineNumberHasClass(1, 'foldable')).toBe true - expect(lineNumberHasClass(2, 'foldable')).toBe false - expect(lineNumberHasClass(3, 'foldable')).toBe false - expect(lineNumberHasClass(4, 'foldable')).toBe true - expect(lineNumberHasClass(5, 'foldable')).toBe false - - it "updates the foldable class on the correct line numbers when the foldable positions change", -> - editor.getBuffer().insert([0, 0], '\n') - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(0, 'foldable')).toBe false - expect(lineNumberHasClass(1, 'foldable')).toBe true - expect(lineNumberHasClass(2, 'foldable')).toBe true - expect(lineNumberHasClass(3, 'foldable')).toBe false - expect(lineNumberHasClass(4, 'foldable')).toBe false - expect(lineNumberHasClass(5, 'foldable')).toBe true - expect(lineNumberHasClass(6, 'foldable')).toBe false - - it "updates the foldable class on a line number that becomes foldable", -> - expect(lineNumberHasClass(11, 'foldable')).toBe false - - editor.getBuffer().insert([11, 44], '\n fold me') - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(11, 'foldable')).toBe true - editor.undo() - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(11, 'foldable')).toBe false - - it "adds, updates and removes the folded class on the correct line number componentNodes", -> - editor.foldBufferRow(4) - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(4, 'folded')).toBe true - editor.getBuffer().insert([0, 0], '\n') - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(4, 'folded')).toBe false - expect(lineNumberHasClass(5, 'folded')).toBe true - - editor.unfoldBufferRow(5) - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(5, 'folded')).toBe false - - describe "when soft wrapping is enabled", -> - beforeEach -> - editor.setSoftWrapped(true) - waitsForNextDOMUpdate() - - runs -> - componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - it "doesn't add the foldable class for soft-wrapped lines", -> - expect(lineNumberHasClass(0, 'foldable')).toBe true - expect(lineNumberHasClass(1, 'foldable')).toBe false - - describe "mouse interactions with fold indicators", -> - [gutterNode] = [] - - buildClickEvent = (target) -> - buildMouseEvent('click', {target}) - - beforeEach -> - gutterNode = componentNode.querySelector('.gutter') - - describe "when the component is destroyed", -> - it "stops listening for folding events", -> - component.destroy() - - lineNumber = component.lineNumberNodeForScreenRow(1) - target = lineNumber.querySelector('.icon-right') - target.dispatchEvent(buildClickEvent(target)) - - it "folds and unfolds the block represented by the fold indicator when clicked", -> - expect(lineNumberHasClass(1, 'folded')).toBe false - - lineNumber = component.lineNumberNodeForScreenRow(1) - target = lineNumber.querySelector('.icon-right') - target.dispatchEvent(buildClickEvent(target)) - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(1, 'folded')).toBe true - - lineNumber = component.lineNumberNodeForScreenRow(1) - target = lineNumber.querySelector('.icon-right') - target.dispatchEvent(buildClickEvent(target)) - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(1, 'folded')).toBe false - - it "does not fold when the line number componentNode is clicked", -> - lineNumber = component.lineNumberNodeForScreenRow(1) - lineNumber.dispatchEvent(buildClickEvent(lineNumber)) - waits 100 - runs -> - expect(lineNumberHasClass(1, 'folded')).toBe false - - describe "cursor rendering", -> - it "renders the currently visible cursors", -> - [cursor1, cursor2, cursor3, cursorNodes] = [] - - cursor1 = editor.getLastCursor() - cursor1.setScreenPosition([0, 5], autoscroll: false) - - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].offsetHeight).toBe lineHeightInPixels - expect(cursorNodes[0].offsetWidth).toBeCloseTo charWidth, 0 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(5 * charWidth)}px, #{0 * lineHeightInPixels}px)" - - cursor2 = editor.addCursorAtScreenPosition([8, 11], autoscroll: false) - cursor3 = editor.addCursorAtScreenPosition([4, 10], autoscroll: false) - waitsForNextDOMUpdate() - - runs -> - cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe 2 - expect(cursorNodes[0].offsetTop).toBe 0 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(5 * charWidth)}px, #{0 * lineHeightInPixels}px)" - expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{Math.round(10 * charWidth)}px, #{4 * lineHeightInPixels}px)" - - verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels - waitsForNextDOMUpdate() - - runs -> - horizontalScrollbarNode.scrollLeft = 3.5 * charWidth - waitsForNextDOMUpdate() - - cursorMovedListener = null - runs -> - cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe 2 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(10 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" - expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" - - editor.onDidChangeCursorPosition cursorMovedListener = jasmine.createSpy('cursorMovedListener') - cursor3.setScreenPosition([4, 11], autoscroll: false) - waitsForNextDOMUpdate() - - runs -> - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" - expect(cursorMovedListener).toHaveBeenCalled() - - cursor3.destroy() - waitsForNextDOMUpdate() - - runs -> - cursorNodes = componentNode.querySelectorAll('.cursor') - - expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" - - it "accounts for character widths when positioning cursors", -> - atom.config.set('editor.fontFamily', 'sans-serif') - editor.setCursorScreenPosition([0, 16]) - waitsForNextDOMUpdate() - - runs -> - cursor = componentNode.querySelector('.cursor') - cursorRect = cursor.getBoundingClientRect() - - cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild - range = document.createRange() - range.setStart(cursorLocationTextNode, 0) - range.setEnd(cursorLocationTextNode, 1) - rangeRect = range.getBoundingClientRect() - - expect(cursorRect.left).toBeCloseTo rangeRect.left, 0 - expect(cursorRect.width).toBeCloseTo rangeRect.width, 0 - - it "accounts for the width of paired characters when positioning cursors", -> - atom.config.set('editor.fontFamily', 'sans-serif') - editor.setText('he\u0301y') # e with an accent mark - editor.setCursorBufferPosition([0, 3]) - waitsForNextDOMUpdate() - - runs -> - cursor = componentNode.querySelector('.cursor') - cursorRect = cursor.getBoundingClientRect() - - cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2] - - range = document.createRange() - range.setStart(cursorLocationTextNode, 0) - range.setEnd(cursorLocationTextNode, 1) - rangeRect = range.getBoundingClientRect() - - expect(cursorRect.left).toBeCloseTo rangeRect.left, 0 - expect(cursorRect.width).toBeCloseTo rangeRect.width, 0 - - it "positions cursors correctly after character widths are changed via a stylesheet change", -> - atom.config.set('editor.fontFamily', 'sans-serif') - editor.setCursorScreenPosition([0, 16]) - waitsForNextDOMUpdate() - - runs -> - atom.styles.addStyleSheet """ - .function.js { - font-weight: bold; - } - """, context: 'atom-text-editor' - waitsForNextDOMUpdate() - - runs -> - cursor = componentNode.querySelector('.cursor') - cursorRect = cursor.getBoundingClientRect() - - cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild - range = document.createRange() - range.setStart(cursorLocationTextNode, 0) - range.setEnd(cursorLocationTextNode, 1) - rangeRect = range.getBoundingClientRect() - - expect(cursorRect.left).toBeCloseTo rangeRect.left, 0 - expect(cursorRect.width).toBeCloseTo rangeRect.width, 0 - - atom.themes.removeStylesheet('test') - - it "sets the cursor to the default character width at the end of a line", -> - editor.setCursorScreenPosition([0, Infinity]) - waitsForNextDOMUpdate() - - runs -> - cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.offsetWidth).toBeCloseTo charWidth, 0 - - it "gives the cursor a non-zero width even if it's inside atomic tokens", -> - editor.setCursorScreenPosition([1, 0]) - waitsForNextDOMUpdate() - - runs -> - cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.offsetWidth).toBeCloseTo charWidth, 0 - - it "blinks cursors when they aren't moving", -> - cursorsNode = componentNode.querySelector('.cursors') - wrapperNode.focus() - waitsForNextDOMUpdate() - - runs -> expect(cursorsNode.classList.contains('blink-off')).toBe false - - waitsFor -> cursorsNode.classList.contains('blink-off') - waitsFor -> not cursorsNode.classList.contains('blink-off') - - runs -> - # Stop blinking after moving the cursor - editor.moveRight() - waitsForNextDOMUpdate() - - runs -> - expect(cursorsNode.classList.contains('blink-off')).toBe false - - waitsFor -> cursorsNode.classList.contains('blink-off') - - it "does not render cursors that are associated with non-empty selections", -> - editor.setSelectedScreenRange([[0, 4], [4, 6]]) - editor.addCursorAtScreenPosition([6, 8]) - waitsForNextDOMUpdate() - - runs -> - cursorNodes = componentNode.querySelectorAll('.cursor') - expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(8 * charWidth)}px, #{6 * lineHeightInPixels}px)" - - it "updates cursor positions when the line height changes", -> - editor.setCursorBufferPosition([1, 10]) - component.setLineHeight(2) - waitsForNextDOMUpdate() - - runs -> - cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(10 * editor.getDefaultCharWidth())}px, #{editor.getLineHeightInPixels()}px)" - - it "updates cursor positions when the font size changes", -> - editor.setCursorBufferPosition([1, 10]) - component.setFontSize(10) - waitsForNextDOMUpdate() - - runs -> - cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(10 * editor.getDefaultCharWidth())}px, #{editor.getLineHeightInPixels()}px)" - - it "updates cursor positions when the font family changes", -> - editor.setCursorBufferPosition([1, 10]) - component.setFontFamily('sans-serif') - waitsForNextDOMUpdate() - - runs -> - cursorNode = componentNode.querySelector('.cursor') - - {left} = wrapperNode.pixelPositionForScreenPosition([1, 10]) - expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(left)}px, #{editor.getLineHeightInPixels()}px)" - - describe "selection rendering", -> - [scrollViewNode, scrollViewClientLeft] = [] - - beforeEach -> - scrollViewNode = componentNode.querySelector('.scroll-view') - scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left - - it "renders 1 region for 1-line selections", -> - # 1-line selection - editor.setSelectedScreenRange([[1, 6], [1, 10]]) - waitsForNextDOMUpdate() - - runs -> - regions = componentNode.querySelectorAll('.selection .region') - - expect(regions.length).toBe 1 - regionRect = regions[0].getBoundingClientRect() - expect(regionRect.top).toBe 1 * lineHeightInPixels - expect(regionRect.height).toBe 1 * lineHeightInPixels - expect(regionRect.left).toBeCloseTo scrollViewClientLeft + 6 * charWidth, 0 - expect(regionRect.width).toBeCloseTo 4 * charWidth, 0 - - it "renders 2 regions for 2-line selections", -> - editor.setSelectedScreenRange([[1, 6], [2, 10]]) - waitsForNextDOMUpdate() - - runs -> - tileNode = component.tileNodesForLines()[0] - regions = tileNode.querySelectorAll('.selection .region') - expect(regions.length).toBe 2 - - region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe 1 * lineHeightInPixels - expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBeCloseTo scrollViewClientLeft + 6 * charWidth, 0 - expect(region1Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 - - region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe 2 * lineHeightInPixels - expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 - expect(region2Rect.width).toBeCloseTo 10 * charWidth, 0 - - it "renders 3 regions per tile for selections with more than 2 lines", -> - editor.setSelectedScreenRange([[0, 6], [5, 10]]) - waitsForNextDOMUpdate() - - runs -> - # Tile 0 - tileNode = component.tileNodesForLines()[0] - regions = tileNode.querySelectorAll('.selection .region') - expect(regions.length).toBe(3) - - region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe 0 - expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBeCloseTo scrollViewClientLeft + 6 * charWidth, 0 - expect(region1Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 - - region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe 1 * lineHeightInPixels - expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 - expect(region2Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 - - region3Rect = regions[2].getBoundingClientRect() - expect(region3Rect.top).toBe 2 * lineHeightInPixels - expect(region3Rect.height).toBe 1 * lineHeightInPixels - expect(region3Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 - expect(region3Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 - - # Tile 3 - tileNode = component.tileNodesForLines()[1] - regions = tileNode.querySelectorAll('.selection .region') - expect(regions.length).toBe(3) - - region1Rect = regions[0].getBoundingClientRect() - expect(region1Rect.top).toBe 3 * lineHeightInPixels - expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 - expect(region1Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 - - region2Rect = regions[1].getBoundingClientRect() - expect(region2Rect.top).toBe 4 * lineHeightInPixels - expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 - expect(region2Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 - - region3Rect = regions[2].getBoundingClientRect() - expect(region3Rect.top).toBe 5 * lineHeightInPixels - expect(region3Rect.height).toBe 1 * lineHeightInPixels - expect(region3Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 - expect(region3Rect.width).toBeCloseTo 10 * charWidth, 0 - - it "does not render empty selections", -> - editor.addSelectionForBufferRange([[2, 2], [2, 2]]) - waitsForNextDOMUpdate() - - runs -> - expect(editor.getSelections()[0].isEmpty()).toBe true - expect(editor.getSelections()[1].isEmpty()).toBe true - - expect(componentNode.querySelectorAll('.selection').length).toBe 0 - - it "updates selections when the line height changes", -> - editor.setSelectedBufferRange([[1, 6], [1, 10]]) - component.setLineHeight(2) - waitsForNextDOMUpdate() - - runs -> - selectionNode = componentNode.querySelector('.region') - expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() - - it "updates selections when the font size changes", -> - editor.setSelectedBufferRange([[1, 6], [1, 10]]) - component.setFontSize(10) - waitsForNextDOMUpdate() - - runs -> - selectionNode = componentNode.querySelector('.region') - expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() - expect(selectionNode.offsetLeft).toBeCloseTo 6 * editor.getDefaultCharWidth(), 0 - - it "updates selections when the font family changes", -> - editor.setSelectedBufferRange([[1, 6], [1, 10]]) - component.setFontFamily('sans-serif') - waitsForNextDOMUpdate() - - runs -> - selectionNode = componentNode.querySelector('.region') - expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() - expect(selectionNode.offsetLeft).toBeCloseTo wrapperNode.pixelPositionForScreenPosition([1, 6]).left, 0 - - it "will flash the selection when flash:true is passed to editor::setSelectedBufferRange", -> - editor.setSelectedBufferRange([[1, 6], [1, 10]], flash: true) - waitsForNextDOMUpdate() - - selectionNode = null - runs -> - selectionNode = componentNode.querySelector('.selection') - expect(selectionNode.classList.contains('flash')).toBe true - - waitsFor -> not selectionNode.classList.contains('flash') - - runs -> - editor.setSelectedBufferRange([[1, 5], [1, 7]], flash: true) - waitsForNextDOMUpdate() - - runs -> - expect(selectionNode.classList.contains('flash')).toBe true - - describe "line decoration rendering", -> - [marker, decoration, decorationParams] = [] - - beforeEach -> - marker = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[2, 13], [3, 15]], invalidate: 'inside') - decorationParams = {type: ['line-number', 'line'], class: 'a'} - decoration = editor.decorateMarker(marker, decorationParams) - waitsForNextDOMUpdate() - - it "applies line decoration classes to lines and line numbers", -> - expect(lineAndLineNumberHaveClass(2, 'a')).toBe true - expect(lineAndLineNumberHaveClass(3, 'a')).toBe true - - # Shrink editor vertically - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - # Add decorations that are out of range - marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]]) - editor.decorateMarker(marker2, type: ['line-number', 'line'], class: 'b') - waitsForNextDOMUpdate() - - runs -> - # Scroll decorations into view - verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForNextDOMUpdate() - - runs -> - expect(lineAndLineNumberHaveClass(9, 'b')).toBe true - - # Fold a line to move the decorations - editor.foldBufferRow(5) - waitsForNextDOMUpdate() - - runs -> - expect(lineAndLineNumberHaveClass(9, 'b')).toBe false - expect(lineAndLineNumberHaveClass(6, 'b')).toBe true - - it "only applies decorations to screen rows that are spanned by their marker when lines are soft-wrapped", -> - editor.setText("a line that wraps, ok") - editor.setSoftWrapped(true) - componentNode.style.width = 16 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - marker.destroy() - marker = editor.markBufferRange([[0, 0], [0, 2]]) - editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'b') - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(0, 'b')).toBe true - expect(lineNumberHasClass(1, 'b')).toBe false - - marker.setBufferRange([[0, 0], [0, Infinity]]) - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(0, 'b')).toBe true - expect(lineNumberHasClass(1, 'b')).toBe true - - it "updates decorations when markers move", -> - expect(lineAndLineNumberHaveClass(1, 'a')).toBe false - expect(lineAndLineNumberHaveClass(2, 'a')).toBe true - expect(lineAndLineNumberHaveClass(3, 'a')).toBe true - expect(lineAndLineNumberHaveClass(4, 'a')).toBe false - - editor.getBuffer().insert([0, 0], '\n') - waitsForNextDOMUpdate() - - runs -> - expect(lineAndLineNumberHaveClass(2, 'a')).toBe false - expect(lineAndLineNumberHaveClass(3, 'a')).toBe true - expect(lineAndLineNumberHaveClass(4, 'a')).toBe true - expect(lineAndLineNumberHaveClass(5, 'a')).toBe false - - marker.setBufferRange([[4, 4], [6, 4]]) - waitsForNextDOMUpdate() - - runs -> - expect(lineAndLineNumberHaveClass(2, 'a')).toBe false - expect(lineAndLineNumberHaveClass(3, 'a')).toBe false - expect(lineAndLineNumberHaveClass(4, 'a')).toBe true - expect(lineAndLineNumberHaveClass(5, 'a')).toBe true - expect(lineAndLineNumberHaveClass(6, 'a')).toBe true - expect(lineAndLineNumberHaveClass(7, 'a')).toBe false - - it "remove decoration classes when decorations are removed", -> - decoration.destroy() - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(1, 'a')).toBe false - expect(lineNumberHasClass(2, 'a')).toBe false - expect(lineNumberHasClass(3, 'a')).toBe false - expect(lineNumberHasClass(4, 'a')).toBe false - - it "removes decorations when their marker is invalidated", -> - editor.getBuffer().insert([3, 2], 'n') - waitsForNextDOMUpdate() - - runs -> - expect(marker.isValid()).toBe false - expect(lineAndLineNumberHaveClass(1, 'a')).toBe false - expect(lineAndLineNumberHaveClass(2, 'a')).toBe false - expect(lineAndLineNumberHaveClass(3, 'a')).toBe false - expect(lineAndLineNumberHaveClass(4, 'a')).toBe false - - editor.undo() - waitsForNextDOMUpdate() - - runs -> - expect(marker.isValid()).toBe true - expect(lineAndLineNumberHaveClass(1, 'a')).toBe false - expect(lineAndLineNumberHaveClass(2, 'a')).toBe true - expect(lineAndLineNumberHaveClass(3, 'a')).toBe true - expect(lineAndLineNumberHaveClass(4, 'a')).toBe false - - it "removes decorations when their marker is destroyed", -> - marker.destroy() - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(1, 'a')).toBe false - expect(lineNumberHasClass(2, 'a')).toBe false - expect(lineNumberHasClass(3, 'a')).toBe false - expect(lineNumberHasClass(4, 'a')).toBe false - - describe "when the decoration's 'onlyHead' property is true", -> - it "only applies the decoration's class to lines containing the marker's head", -> - editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-head', onlyHead: true) - waitsForNextDOMUpdate() - - runs -> - expect(lineAndLineNumberHaveClass(1, 'only-head')).toBe false - expect(lineAndLineNumberHaveClass(2, 'only-head')).toBe false - expect(lineAndLineNumberHaveClass(3, 'only-head')).toBe true - expect(lineAndLineNumberHaveClass(4, 'only-head')).toBe false - - describe "when the decoration's 'onlyEmpty' property is true", -> - it "only applies the decoration when its marker is empty", -> - editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-empty', onlyEmpty: true) - waitsForNextDOMUpdate() - - runs -> - expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe false - expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe false - - marker.clearTail() - waitsForNextDOMUpdate() - - runs -> - expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe false - expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe true - - describe "when the decoration's 'onlyNonEmpty' property is true", -> - it "only applies the decoration when its marker is non-empty", -> - editor.decorateMarker(marker, type: ['line-number', 'line'], class: 'only-non-empty', onlyNonEmpty: true) - waitsForNextDOMUpdate() - - runs -> - expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe true - expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe true - - marker.clearTail() - waitsForNextDOMUpdate() - - runs -> - expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe false - expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe false - - describe "highlight decoration rendering", -> - [marker, decoration, decorationParams, scrollViewClientLeft] = [] - beforeEach -> - scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left - marker = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[2, 13], [3, 15]], invalidate: 'inside') - decorationParams = {type: 'highlight', class: 'test-highlight'} - decoration = editor.decorateMarker(marker, decorationParams) - waitsForNextDOMUpdate() - - it "does not render highlights for off-screen lines until they come on-screen", -> - wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], invalidate: 'inside') - editor.decorateMarker(marker, type: 'highlight', class: 'some-highlight') - waitsForNextDOMUpdate() - - runs -> - # Should not be rendering range containing the marker - expect(component.presenter.endRow).toBeLessThan 9 - - regions = componentNode.querySelectorAll('.some-highlight .region') - - # Nothing when outside the rendered row range - expect(regions.length).toBe 0 - - verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForNextDOMUpdate() - - runs -> - expect(component.presenter.endRow).toBeGreaterThan(8) - - regions = componentNode.querySelectorAll('.some-highlight .region') - - expect(regions.length).toBe 1 - regionRect = regions[0].style - expect(regionRect.top).toBe (0 + 'px') - expect(regionRect.height).toBe 1 * lineHeightInPixels + 'px' - expect(regionRect.left).toBe Math.round(2 * charWidth) + 'px' - expect(regionRect.width).toBe Math.round(2 * charWidth) + 'px' - - it "renders highlights decoration's marker is added", -> - regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe 2 - - it "removes highlights when a decoration is removed", -> - decoration.destroy() - waitsForNextDOMUpdate() - - runs -> - regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe 0 - - it "does not render a highlight that is within a fold", -> - editor.foldBufferRow(1) - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelectorAll('.test-highlight').length).toBe 0 - - it "removes highlights when a decoration's marker is destroyed", -> - marker.destroy() - waitsForNextDOMUpdate() - - runs -> - regions = componentNode.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') - waitsForNextDOMUpdate() - - runs -> - expect(marker.isValid()).toBe false - regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe 0 - - editor.getBuffer().undo() - waitsForNextDOMUpdate() - - runs -> - expect(marker.isValid()).toBe true - regions = componentNode.querySelectorAll('.test-highlight .region') - expect(regions.length).toBe 2 - - it "allows multiple space-delimited decoration classes", -> - decoration.setProperties(type: 'highlight', class: 'foo bar') - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelectorAll('.foo.bar').length).toBe 2 - decoration.setProperties(type: 'highlight', class: 'bar baz') - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelectorAll('.bar.baz').length).toBe 2 - - it "renders classes on the regions directly if 'deprecatedRegionClass' option is defined", -> - decoration = editor.decorateMarker(marker, type: 'highlight', class: 'test-highlight', deprecatedRegionClass: 'test-highlight-region') - waitsForNextDOMUpdate() - - runs -> - regions = componentNode.querySelectorAll('.test-highlight .region.test-highlight-region') - expect(regions.length).toBe 2 - - describe "when flashing a decoration via Decoration::flash()", -> - highlightNode = null - beforeEach -> - highlightNode = componentNode.querySelectorAll('.test-highlight')[1] - - it "adds and removes the flash class specified in ::flash", -> - expect(highlightNode.classList.contains('flash-class')).toBe false - - decoration.flash('flash-class', 10) - waitsForNextDOMUpdate() - - runs -> - expect(highlightNode.classList.contains('flash-class')).toBe true - - waitsFor -> not highlightNode.classList.contains('flash-class') - - describe "when ::flash is called again before the first has finished", -> - it "removes the class from the decoration highlight before adding it for the second ::flash call", -> - decoration.flash('flash-class', 30) - waitsForNextDOMUpdate() - runs -> expect(highlightNode.classList.contains('flash-class')).toBe true - waits 2 - runs -> - decoration.flash('flash-class', 10) - waitsForNextDOMUpdate() - runs -> expect(highlightNode.classList.contains('flash-class')).toBe false - waitsFor -> highlightNode.classList.contains('flash-class') - - describe "when a decoration's marker moves", -> - it "moves rendered highlights when the buffer is changed", -> - regionStyle = componentNode.querySelector('.test-highlight .region').style - originalTop = parseInt(regionStyle.top) - - expect(originalTop).toBe(2 * lineHeightInPixels) - - editor.getBuffer().insert([0, 0], '\n') - waitsForNextDOMUpdate() - - runs -> - regionStyle = componentNode.querySelector('.test-highlight .region').style - newTop = parseInt(regionStyle.top) - - expect(newTop).toBe(0) - - it "moves rendered highlights when the marker is manually moved", -> - regionStyle = componentNode.querySelector('.test-highlight .region').style - expect(parseInt(regionStyle.top)).toBe 2 * lineHeightInPixels - - marker.setBufferRange([[5, 8], [5, 13]]) - waitsForNextDOMUpdate() - - runs -> - regionStyle = componentNode.querySelector('.test-highlight .region').style - expect(parseInt(regionStyle.top)).toBe 2 * lineHeightInPixels - - describe "when a decoration is updated via Decoration::update", -> - it "renders the decoration's new params", -> - expect(componentNode.querySelector('.test-highlight')).toBeTruthy() - - decoration.setProperties(type: 'highlight', class: 'new-test-highlight') - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelector('.test-highlight')).toBeFalsy() - expect(componentNode.querySelector('.new-test-highlight')).toBeTruthy() - - describe "overlay decoration rendering", -> - [item, gutterWidth] = [] - beforeEach -> - item = document.createElement('div') - item.classList.add 'overlay-test' - item.style.background = 'red' - gutterWidth = componentNode.querySelector('.gutter').offsetWidth - - describe "when the marker is empty", -> - it "renders an overlay decoration when added and removes the overlay when the decoration is destroyed", -> - marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - waitsForNextDOMUpdate() - - runs -> - overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') - expect(overlay).toBe item - - decoration.destroy() - waitsForNextDOMUpdate() - - runs -> - overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') - expect(overlay).toBe null - - it "renders the overlay element with the CSS class specified by the decoration", -> - marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', class: 'my-overlay', item}) - waitsForNextDOMUpdate() - - runs -> - overlay = component.getTopmostDOMNode().querySelector('atom-overlay.my-overlay') - expect(overlay).not.toBe null - - child = overlay.querySelector('.overlay-test') - expect(child).toBe item - - describe "when the marker is not empty", -> - it "renders at the head of the marker by default", -> - marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - waitsForNextDOMUpdate() - - runs -> - position = wrapperNode.pixelPositionForBufferPosition([2, 10]) - - overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe Math.round(position.left + gutterWidth) + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - describe "positioning the overlay when near the edge of the editor", -> - [itemWidth, itemHeight, windowWidth, windowHeight] = [] - beforeEach -> - atom.storeWindowDimensions() - - itemWidth = Math.round(4 * editor.getDefaultCharWidth()) - itemHeight = 4 * editor.getLineHeightInPixels() - - windowWidth = Math.round(gutterWidth + 30 * editor.getDefaultCharWidth()) - windowHeight = 10 * editor.getLineHeightInPixels() - - item.style.width = itemWidth + 'px' - item.style.height = itemHeight + 'px' - - wrapperNode.style.width = windowWidth + 'px' - wrapperNode.style.height = windowHeight + 'px' - - atom.setWindowDimensions({width: windowWidth, height: windowHeight}) - - component.measureDimensions() - component.measureWindowSize() - waitsForNextDOMUpdate() - - afterEach -> - atom.restoreWindowDimensions() - - # This spec should actually run on Linux as well, see TextEditorComponent#measureWindowSize for further information. - it "slides horizontally left when near the right edge on #win32 and #darwin", -> - [overlay, position] = [] - - marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], invalidate: 'never') - decoration = editor.decorateMarker(marker, {type: 'overlay', item}) - waitsForNextDOMUpdate() - - runs -> - position = wrapperNode.pixelPositionForBufferPosition([0, 26]) - - overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe Math.round(position.left + gutterWidth) + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - editor.insertText('a') - waitsForNextDOMUpdate() - - runs -> - expect(overlay.style.left).toBe windowWidth - itemWidth + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - editor.insertText('b') - waitsForNextDOMUpdate() - - runs -> - expect(overlay.style.left).toBe windowWidth - itemWidth + 'px' - expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' - - describe "hidden input field", -> - it "renders the hidden input field at the position of the last cursor if the cursor is on screen and the editor is focused", -> - editor.setVerticalScrollMargin(0) - editor.setHorizontalScrollMargin(0) - - inputNode = componentNode.querySelector('.hidden-input') - wrapperNode.style.height = 5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - wrapperNode.setScrollTop(3 * lineHeightInPixels) - wrapperNode.setScrollLeft(3 * charWidth) - waitsForNextDOMUpdate() - - runs -> - expect(inputNode.offsetTop).toBe 0 - expect(inputNode.offsetLeft).toBe 0 - - # In bounds, not focused - editor.setCursorBufferPosition([5, 4], autoscroll: false) - waitsForNextDOMUpdate() - - runs -> - expect(inputNode.offsetTop).toBe 0 - expect(inputNode.offsetLeft).toBe 0 - - # In bounds and focused - wrapperNode.focus() # updates via state change - waitsForNextDOMUpdate() - - runs -> - expect(inputNode.offsetTop).toBe (5 * lineHeightInPixels) - wrapperNode.getScrollTop() - expect(inputNode.offsetLeft).toBeCloseTo (4 * charWidth) - wrapperNode.getScrollLeft(), 0 - - # In bounds, not focused - inputNode.blur() # updates via state change - waitsForNextDOMUpdate() - - runs -> - expect(inputNode.offsetTop).toBe 0 - expect(inputNode.offsetLeft).toBe 0 - - # Out of bounds, not focused - editor.setCursorBufferPosition([1, 2], autoscroll: false) - waitsForNextDOMUpdate() - - runs -> - expect(inputNode.offsetTop).toBe 0 - expect(inputNode.offsetLeft).toBe 0 - - # Out of bounds, focused - inputNode.focus() # updates via state change - waitsForNextDOMUpdate() - - runs -> - expect(inputNode.offsetTop).toBe 0 - expect(inputNode.offsetLeft).toBe 0 - - describe "mouse interactions on the lines", -> - linesNode = null - - beforeEach -> - linesNode = componentNode.querySelector('.lines') - - describe "when the mouse is single-clicked above the first line", -> - it "moves the cursor to the start of file buffer position", -> - editor.setText('foo') - editor.setCursorBufferPosition([0, 3]) - height = 4.5 * lineHeightInPixels - wrapperNode.style.height = height + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - coordinates = clientCoordinatesForScreenPosition([0, 2]) - coordinates.clientY = -1 - linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) - waitsForNextDOMUpdate() - - runs -> - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - - describe "when the mouse is single-clicked below the last line", -> - it "moves the cursor to the end of file buffer position", -> - editor.setText('foo') - editor.setCursorBufferPosition([0, 0]) - height = 4.5 * lineHeightInPixels - wrapperNode.style.height = height + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - coordinates = clientCoordinatesForScreenPosition([0, 2]) - coordinates.clientY = height * 2 - linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) - waitsForNextDOMUpdate() - - runs -> - expect(editor.getCursorScreenPosition()).toEqual [0, 3] - - describe "when a non-folded line is single-clicked", -> - describe "when no modifier keys are held down", -> - it "moves the cursor to the nearest screen position", -> - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - wrapperNode.setScrollTop(3.5 * lineHeightInPixels) - wrapperNode.setScrollLeft(2 * charWidth) - waitsForNextDOMUpdate() - - runs -> - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]))) - waitsForNextDOMUpdate() - - runs -> - 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)) - waitsForNextDOMUpdate() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [5, 6]] - - describe "when the command key is held down", -> - describe "the current cursor position and screen position do not match", -> - it "adds a cursor at the nearest screen position", -> - editor.setCursorScreenPosition([3, 4]) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), metaKey: true)) - waitsForNextDOMUpdate() - - runs -> - expect(editor.getSelectedScreenRanges()).toEqual [[[3, 4], [3, 4]], [[5, 6], [5, 6]]] - - describe "when there are multiple cursors, and one of the cursor's screen position is the same as the mouse click screen position", -> - it "removes a cursor at the mouse screen position", -> - editor.setCursorScreenPosition([3, 4]) - editor.addCursorAtScreenPosition([5, 2]) - editor.addCursorAtScreenPosition([7, 5]) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), metaKey: true)) - waitsForNextDOMUpdate() - - runs -> - expect(editor.getSelectedScreenRanges()).toEqual [[[5, 2], [5, 2]], [[7, 5], [7, 5]]] - - describe "when there is a single cursor and the click occurs at the cursor's screen position", -> - it "neither adds a new cursor nor removes the current cursor", -> - editor.setCursorScreenPosition([3, 4]) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), metaKey: true)) - waitsForNextDOMUpdate() - - runs -> - expect(editor.getSelectedScreenRanges()).toEqual [[[3, 4], [3, 4]]] - - describe "when a non-folded line is double-clicked", -> - describe "when no modifier keys are held down", -> - it "selects the word containing the nearest screen position", -> - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 1)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 2)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [5, 13]] - - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([6, 6]), detail: 1)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRange()).toEqual [[6, 6], [6, 6]] - - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([8, 8]), detail: 1, shiftKey: true)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRange()).toEqual [[6, 6], [8, 8]] - - describe "when the command key is held down", -> - it "selects the word containing the newly-added cursor", -> - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 1, metaKey: true)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 2, metaKey: true)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - - expect(editor.getSelectedScreenRanges()).toEqual [[[0, 0], [0, 0]], [[5, 6], [5, 13]]] - - describe "when a non-folded line is triple-clicked", -> - describe "when no modifier keys are held down", -> - it "selects the line containing the nearest screen position", -> - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 1)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 2)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 3)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [6, 0]] - - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([6, 6]), detail: 1, shiftKey: true)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [7, 0]] - - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([7, 5]), detail: 1)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([8, 8]), detail: 1, shiftKey: true)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRange()).toEqual [[7, 5], [8, 8]] - - describe "when the command key is held down", -> - it "selects the line containing the newly-added cursor", -> - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 1, metaKey: true)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 2, metaKey: true)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 3, metaKey: true)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - expect(editor.getSelectedScreenRanges()).toEqual [[[0, 0], [0, 0]], [[5, 0], [6, 0]]] - - describe "when the mouse is clicked and dragged", -> - it "selects to the nearest screen position until the mouse button is released", -> - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1)) - - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] - - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), which: 1)) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]] - - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([12, 0]), which: 1)) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]] - - it "autoscrolls when the cursor approaches the boundaries of the editor", -> - wrapperNode.style.height = '100px' - wrapperNode.style.width = '100px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollTop()).toBe(0) - expect(wrapperNode.getScrollLeft()).toBe(0) - - linesNode.dispatchEvent(buildMouseEvent('mousedown', {clientX: 0, clientY: 0}, which: 1)) - linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 50}, which: 1)) - waitsForAnimationFrame() for i in [0..5] - - runs -> - expect(wrapperNode.getScrollTop()).toBe(0) - expect(wrapperNode.getScrollLeft()).toBeGreaterThan(0) - - linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 100, clientY: 100}, which: 1)) - waitsForAnimationFrame() for i in [0..5] - - [previousScrollTop, previousScrollLeft] = [] - - runs -> - expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) - - previousScrollTop = wrapperNode.getScrollTop() - previousScrollLeft = wrapperNode.getScrollLeft() - - linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 50}, which: 1)) - waitsForAnimationFrame() for i in [0..5] - - runs -> - expect(wrapperNode.getScrollTop()).toBe(previousScrollTop) - expect(wrapperNode.getScrollLeft()).toBeLessThan(previousScrollLeft) - - linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 10, clientY: 10}, which: 1)) - waitsForAnimationFrame() for i in [0..5] - - runs -> - expect(wrapperNode.getScrollTop()).toBeLessThan(previousScrollTop) - - it "stops selecting if the mouse is dragged into the dev tools", -> - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1)) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] - - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), which: 0)) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] - - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1)) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] - - it "stops selecting before the buffer is modified during the drag", -> - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1)) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]] - - editor.insertText('x') - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 5], [2, 5]] - - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1)) - expect(editor.getSelectedScreenRange()).toEqual [[2, 5], [2, 5]] - - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([5, 4]), which: 1)) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [5, 4]] - - editor.delete() - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [2, 4]] - - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1)) - expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [2, 4]] - - describe "when the command key is held down", -> - it "adds a new selection and selects to the nearest screen position, then merges intersecting selections when the mouse button is released", -> - editor.setSelectedScreenRange([[4, 4], [4, 9]]) - - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1, metaKey: true)) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1)) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRanges()).toEqual [[[4, 4], [4, 9]], [[2, 4], [6, 8]]] - - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([4, 6]), which: 1)) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRanges()).toEqual [[[4, 4], [4, 9]], [[2, 4], [4, 6]]] - - linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([4, 6]), which: 1)) - expect(editor.getSelectedScreenRanges()).toEqual [[[2, 4], [4, 9]]] - - describe "when the editor is destroyed while dragging", -> - it "cleans up the handlers for window.mouseup and window.mousemove", -> - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1)) - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1)) - waitsForAnimationFrame() - - runs -> - spyOn(window, 'removeEventListener').andCallThrough() - - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 10]), which: 1)) - editor.destroy() - waitsForAnimationFrame() - - runs -> - call.args.pop() for call in window.removeEventListener.calls - expect(window.removeEventListener).toHaveBeenCalledWith('mouseup') - expect(window.removeEventListener).toHaveBeenCalledWith('mousemove') - - describe "when the mouse is double-clicked and dragged", -> - it "expands the selection over the nearest word as the cursor moves", -> - jasmine.attachToDOM(wrapperNode) - wrapperNode.style.height = 6 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 1)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 2)) - expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [5, 13]] - - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1)) - waitsForAnimationFrame() - - maximalScrollTop = null - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [12, 2]] - - maximalScrollTop = wrapperNode.getScrollTop() - - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([9, 3]), which: 1)) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [9, 4]] - expect(wrapperNode.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression) - - linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), which: 1)) - - describe "when the mouse is triple-clicked and dragged", -> - it "expands the selection over the nearest line as the cursor moves", -> - jasmine.attachToDOM(wrapperNode) - wrapperNode.style.height = 6 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 1)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 2)) - linesNode.dispatchEvent(buildMouseEvent('mouseup')) - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 3)) - expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [6, 0]] - - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1)) - waitsForAnimationFrame() - - maximalScrollTop = null - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [12, 2]] - - maximalScrollTop = wrapperNode.getScrollTop() - - linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 4]), which: 1)) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [8, 0]] - expect(wrapperNode.getScrollTop()).toBe maximalScrollTop # does not autoscroll upward (regression) - - linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), which: 1)) - - describe "when a line is folded", -> - beforeEach -> - editor.foldBufferRow 4 - waitsForNextDOMUpdate() - - describe "when the folded line's fold-marker is clicked", -> - it "unfolds the buffer row", -> - target = component.lineNodeForScreenRow(4).querySelector '.fold-marker' - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), {target})) - expect(editor.isFoldedAtBufferRow 4).toBe false - - describe "when the horizontal scrollbar is interacted with", -> - it "clicking on the scrollbar does not move the cursor", -> - target = horizontalScrollbarNode - linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), {target})) - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - - describe "mouse interactions on the gutter", -> - gutterNode = null - - beforeEach -> - gutterNode = componentNode.querySelector('.gutter') - - describe "when the component is destroyed", -> - it "stops listening for selection events", -> - component.destroy() - - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) - - expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [0, 0]] - - describe "when the gutter is clicked", -> - it "selects the clicked row", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4))) - expect(editor.getSelectedScreenRange()).toEqual [[4, 0], [5, 0]] - - describe "when the gutter is meta-clicked", -> - it "creates a new selection for the clicked row", -> - editor.setSelectedScreenRange([[3, 0], [3, 2]]) - - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [5, 0]]] - - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [5, 0]], [[6, 0], [7, 0]]] - - describe "when the gutter is shift-clicked", -> - beforeEach -> - editor.setSelectedScreenRange([[3, 4], [4, 5]]) - - describe "when the clicked row is before the current selection's tail", -> - it "selects to the beginning of the clicked row", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), shiftKey: true)) - expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [3, 4]] - - describe "when the clicked row is after the current selection's tail", -> - it "selects to the beginning of the row following the clicked row", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), shiftKey: true)) - expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [7, 0]] - - describe "when the gutter is clicked and dragged", -> - describe "when dragging downward", -> - it "selects the rows between the start and end of the drag", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - waitsForAnimationFrame() - - runs -> - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) - expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]] - - describe "when dragging upward", -> - it "selects the rows between the start and end of the drag", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) - waitsForAnimationFrame() - - runs -> - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2))) - expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]] - - it "orients the selection appropriately when the mouse moves above or below the initially-clicked row", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) - waitsForAnimationFrame() - - runs -> - expect(editor.getLastSelection().isReversed()).toBe true - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - waitsForAnimationFrame() - - runs -> - expect(editor.getLastSelection().isReversed()).toBe false - - it "autoscrolls when the cursor approaches the top or bottom of the editor", -> - wrapperNode.style.height = 6 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) - waitsForAnimationFrame() - - maxScrollTop = null - runs -> - expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 - maxScrollTop = wrapperNode.getScrollTop() - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(10))) - waitsForAnimationFrame() - - runs -> - expect(wrapperNode.getScrollTop()).toBe maxScrollTop - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) - waitsForAnimationFrame() - - runs -> - expect(wrapperNode.getScrollTop()).toBeLessThan maxScrollTop - - it "stops selecting if a textInput event occurs during the drag", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [7, 0]] - - inputEvent = new Event('textInput') - inputEvent.data = 'x' - Object.defineProperty(inputEvent, 'target', get: -> componentNode.querySelector('.hidden-input')) - componentNode.dispatchEvent(inputEvent) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 1], [2, 1]] - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(12))) - expect(editor.getSelectedScreenRange()).toEqual [[2, 1], [2, 1]] - - describe "when the gutter is meta-clicked and dragged", -> - beforeEach -> - editor.setSelectedScreenRange([[3, 0], [3, 2]]) - - describe "when dragging downward", -> - it "selects the rows between the start and end of the drag", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - waitsForAnimationFrame() - - runs -> - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [7, 0]]] - - it "merges overlapping selections when the mouse button is released", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[2, 0], [7, 0]]] - - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[2, 0], [7, 0]]] - - describe "when dragging upward", -> - it "selects the rows between the start and end of the drag", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) - waitsForAnimationFrame() - - runs -> - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(4), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[3, 0], [3, 2]], [[4, 0], [7, 0]]] - - it "merges overlapping selections", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), metaKey: true)) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) - waitsForAnimationFrame() - - runs -> - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[2, 0], [7, 0]]] - - describe "when the gutter is shift-clicked and dragged", -> - describe "when the shift-click is below the existing selection's tail", -> - describe "when dragging downward", -> - it "selects the rows between the existing selection's tail and the end of the drag", -> - editor.setSelectedScreenRange([[3, 4], [4, 5]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), shiftKey: true)) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [9, 0]] - - describe "when dragging upward", -> - it "selects the rows between the end of the drag and the tail of the existing selection", -> - editor.setSelectedScreenRange([[4, 4], [5, 5]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), shiftKey: true)) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5))) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[4, 4], [6, 0]] - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [4, 4]] - - describe "when the shift-click is above the existing selection's tail", -> - describe "when dragging upward", -> - it "selects the rows between the end of the drag and the tail of the existing selection", -> - editor.setSelectedScreenRange([[4, 4], [5, 5]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), shiftKey: true)) - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [4, 4]] - - describe "when dragging downward", -> - it "selects the rows between the existing selection's tail and the end of the drag", -> - editor.setSelectedScreenRange([[3, 4], [4, 5]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), shiftKey: true)) - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[2, 0], [3, 4]] - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) - waitsForAnimationFrame() - - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [9, 0]] - - describe "when soft wrap is enabled", -> - beforeEach -> - gutterNode = componentNode.querySelector('.gutter') - editor.setSoftWrapped(true) - waitsForNextDOMUpdate() - runs -> - componentNode.style.width = 21 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - describe "when the gutter is clicked", -> - it "selects the clicked buffer row", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) - expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [2, 0]] - - describe "when the gutter is meta-clicked", -> - it "creates a new selection for the clicked buffer row", -> - editor.setSelectedScreenRange([[1, 0], [1, 2]]) - - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[1, 0], [1, 2]], [[2, 0], [5, 0]]] - - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[1, 0], [1, 2]], [[2, 0], [5, 0]], [[5, 0], [10, 0]]] - - describe "when the gutter is shift-clicked", -> - beforeEach -> - editor.setSelectedScreenRange([[7, 4], [7, 6]]) - - describe "when the clicked row is before the current selection's tail", -> - it "selects to the beginning of the clicked buffer row", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), shiftKey: true)) - expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [7, 4]] - - describe "when the clicked row is after the current selection's tail", -> - it "selects to the beginning of the screen row following the clicked buffer row", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), shiftKey: true)) - expect(editor.getSelectedScreenRange()).toEqual [[7, 4], [16, 0]] - - describe "when the gutter is clicked and dragged", -> - describe "when dragging downward", -> - it "selects the buffer row containing the click, then screen rows until the end of the drag", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) - waitsForAnimationFrame() - runs -> - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) - expect(editor.getSelectedScreenRange()).toEqual [[0, 0], [6, 14]] - - describe "when dragging upward", -> - it "selects the buffer row containing the click, then screen rows until the end of the drag", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6))) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - waitsForAnimationFrame() - runs -> - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(1))) - expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [10, 0]] - - describe "when the gutter is meta-clicked and dragged", -> - beforeEach -> - editor.setSelectedScreenRange([[7, 4], [7, 6]]) - - describe "when dragging downward", -> - it "adds a selection from the buffer row containing the click to the screen row containing the end of the drag", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), metaKey: true)) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3), metaKey: true)) - waitsForAnimationFrame() - runs -> - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(3), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[0, 0], [3, 14]]] - - it "merges overlapping selections on mouseup", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), metaKey: true)) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7), metaKey: true)) - waitsForAnimationFrame() - runs -> - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(7), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[0, 0], [7, 12]]] - - describe "when dragging upward", -> - it "adds a selection from the buffer row containing the click to the screen row containing the end of the drag", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), metaKey: true)) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11), metaKey: true)) - waitsForAnimationFrame() - runs -> - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[7, 4], [7, 6]], [[11, 4], [19, 0]]] - - it "merges overlapping selections on mouseup", -> - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), metaKey: true)) - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5), metaKey: true)) - waitsForAnimationFrame() - runs -> - gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), metaKey: true)) - expect(editor.getSelectedScreenRanges()).toEqual [[[5, 0], [19, 0]]] - - describe "when the gutter is shift-clicked and dragged", -> - describe "when the shift-click is below the existing selection's tail", -> - describe "when dragging downward", -> - it "selects the screen rows between the existing selection's tail and the end of the drag", -> - editor.setSelectedScreenRange([[1, 4], [1, 7]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), shiftKey: true)) - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11))) - waitsForAnimationFrame() - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[1, 4], [11, 14]] - - describe "when dragging upward", -> - it "selects the screen rows between the end of the drag and the tail of the existing selection", -> - editor.setSelectedScreenRange([[1, 4], [1, 7]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), shiftKey: true)) - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) - waitsForAnimationFrame() - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[1, 4], [7, 12]] - - describe "when the shift-click is above the existing selection's tail", -> - describe "when dragging upward", -> - it "selects the screen rows between the end of the drag and the tail of the existing selection", -> - editor.setSelectedScreenRange([[7, 4], [7, 6]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(3), shiftKey: true)) - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) - waitsForAnimationFrame() - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[1, 0], [7, 4]] - - describe "when dragging downward", -> - it "selects the screen rows between the existing selection's tail and the end of the drag", -> - editor.setSelectedScreenRange([[7, 4], [7, 6]]) - gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), shiftKey: true)) - - gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3))) - waitsForAnimationFrame() - runs -> - expect(editor.getSelectedScreenRange()).toEqual [[3, 2], [7, 4]] - - describe "focus handling", -> - inputNode = null - - beforeEach -> - inputNode = componentNode.querySelector('.hidden-input') - - it "transfers focus to the hidden input", -> - expect(document.activeElement).toBe document.body - wrapperNode.focus() - expect(document.activeElement).toBe wrapperNode - expect(wrapperNode.shadowRoot.activeElement).toBe inputNode - - it "adds the 'is-focused' class to the editor when the hidden input is focused", -> - expect(document.activeElement).toBe document.body - inputNode.focus() - waitsForNextDOMUpdate() - runs -> - expect(componentNode.classList.contains('is-focused')).toBe true - expect(wrapperNode.classList.contains('is-focused')).toBe true - inputNode.blur() - waitsForNextDOMUpdate() - runs -> - expect(componentNode.classList.contains('is-focused')).toBe false - expect(wrapperNode.classList.contains('is-focused')).toBe false - - describe "selection handling", -> - cursor = null - - beforeEach -> - editor.setCursorScreenPosition([0, 0]) - waitsForNextDOMUpdate() - - it "adds the 'has-selection' class to the editor when there is a selection", -> - expect(componentNode.classList.contains('has-selection')).toBe false - editor.selectDown() - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.classList.contains('has-selection')).toBe true - editor.moveDown() - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.classList.contains('has-selection')).toBe false - - describe "scrolling", -> - it "updates the vertical scrollbar when the scrollTop is changed in the model", -> - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(verticalScrollbarNode.scrollTop).toBe 0 - wrapperNode.setScrollTop(10) - waitsForNextDOMUpdate() - - runs -> - expect(verticalScrollbarNode.scrollTop).toBe 10 - - it "updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model", -> - componentNode.style.width = 30 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - tilesNodes = null - runs -> - tilesNodes = component.tileNodesForLines() - - top = 0 - for tileNode in tilesNodes - expect(tileNode.style['-webkit-transform']).toBe "translate3d(0px, #{top}px, 0px)" - top += tileNode.offsetHeight - - expect(horizontalScrollbarNode.scrollLeft).toBe 0 - - wrapperNode.setScrollLeft(100) - waitsForNextDOMUpdate() - - runs -> - top = 0 - for tileNode in tilesNodes - expect(tileNode.style['-webkit-transform']).toBe "translate3d(-100px, #{top}px, 0px)" - top += tileNode.offsetHeight - - expect(horizontalScrollbarNode.scrollLeft).toBe 100 - - it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> - componentNode.style.width = 30 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollLeft()).toBe 0 - horizontalScrollbarNode.scrollLeft = 100 - horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollLeft()).toBe 100 - - it "does not obscure the last line with the horizontal scrollbar", -> - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) - waitsForNextDOMUpdate() - - lastLineNode = null - runs -> - lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) - bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom - topOfHorizontalScrollbar = horizontalScrollbarNode.getBoundingClientRect().top - expect(bottomOfLastLine).toBe topOfHorizontalScrollbar - - # Scroll so there's no space below the last line when the horizontal scrollbar disappears - wrapperNode.style.width = 100 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom - bottomOfEditor = componentNode.getBoundingClientRect().bottom - expect(bottomOfLastLine).toBe bottomOfEditor - - it "does not obscure the last character of the longest line with the vertical scrollbar", -> - wrapperNode.style.height = 7 * lineHeightInPixels + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - wrapperNode.setScrollLeft(Infinity) - waitsForNextDOMUpdate() - - runs -> - rightOfLongestLine = component.lineNodeForScreenRow(6).querySelector('.line > span:last-child').getBoundingClientRect().right - leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left - expect(Math.round(rightOfLongestLine)).toBeCloseTo leftOfVerticalScrollbar - 1, 0 # Leave 1 px so the cursor is visible on the end of the line - - it "only displays dummy scrollbars when scrollable in that direction", -> - expect(verticalScrollbarNode.style.display).toBe 'none' - expect(horizontalScrollbarNode.style.display).toBe 'none' - - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = '1000px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(verticalScrollbarNode.style.display).toBe '' - expect(horizontalScrollbarNode.style.display).toBe 'none' - - componentNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(verticalScrollbarNode.style.display).toBe '' - expect(horizontalScrollbarNode.style.display).toBe '' - - wrapperNode.style.height = 20 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(verticalScrollbarNode.style.display).toBe 'none' - expect(horizontalScrollbarNode.style.display).toBe '' - - it "makes the dummy scrollbar divs only as tall/wide as the actual scrollbars", -> - wrapperNode.style.height = 4 * lineHeightInPixels + 'px' - wrapperNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - - runs -> - atom.styles.addStyleSheet """ - ::-webkit-scrollbar { - width: 8px; - height: 8px; - } - """, context: 'atom-text-editor' - - waitsForAnimationFrame() # handle stylesheet change event - waitsForAnimationFrame() # perform requested update - - runs -> - scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') - expect(verticalScrollbarNode.offsetWidth).toBe 8 - expect(horizontalScrollbarNode.offsetHeight).toBe 8 - expect(scrollbarCornerNode.offsetWidth).toBe 8 - expect(scrollbarCornerNode.offsetHeight).toBe 8 - - atom.themes.removeStylesheet('test') - - it "assigns the bottom/right of the scrollbars to the width of the opposite scrollbar if it is visible", -> - scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') - - expect(verticalScrollbarNode.style.bottom).toBe '0px' - expect(horizontalScrollbarNode.style.right).toBe '0px' - - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = '1000px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(verticalScrollbarNode.style.bottom).toBe '0px' - expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' - expect(scrollbarCornerNode.style.display).toBe 'none' - - componentNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' - expect(horizontalScrollbarNode.style.right).toBe verticalScrollbarNode.offsetWidth + 'px' - expect(scrollbarCornerNode.style.display).toBe '' - - wrapperNode.style.height = 20 * lineHeightInPixels + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(verticalScrollbarNode.style.bottom).toBe horizontalScrollbarNode.offsetHeight + 'px' - expect(horizontalScrollbarNode.style.right).toBe '0px' - expect(scrollbarCornerNode.style.display).toBe 'none' - - it "accounts for the width of the gutter in the scrollWidth of the horizontal scrollbar", -> - gutterNode = componentNode.querySelector('.gutter') - componentNode.style.width = 10 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(horizontalScrollbarNode.scrollWidth).toBe wrapperNode.getScrollWidth() - expect(horizontalScrollbarNode.style.left).toBe '0px' - - describe "mousewheel events", -> - beforeEach -> - atom.config.set('editor.scrollSensitivity', 100) - - describe "updating scrollTop and scrollLeft", -> - beforeEach -> - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - 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 - - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10)) - - waitsForAnimationFrame() - - runs -> - expect(verticalScrollbarNode.scrollTop).toBe 10 - expect(horizontalScrollbarNode.scrollLeft).toBe 0 - - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) - waitsForAnimationFrame() - - runs -> - expect(verticalScrollbarNode.scrollTop).toBe 10 - expect(horizontalScrollbarNode.scrollLeft).toBe 15 - - it "updates the scrollLeft or scrollTop according to the scroll sensitivity", -> - atom.config.set('editor.scrollSensitivity', 50) - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10)) - waitsForAnimationFrame() - - runs -> - expect(horizontalScrollbarNode.scrollLeft).toBe 0 - - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5)) - waitsForAnimationFrame() - - runs -> - expect(verticalScrollbarNode.scrollTop).toBe 5 - expect(horizontalScrollbarNode.scrollLeft).toBe 7 - - it "uses the previous scrollSensitivity when the value is not an int", -> - atom.config.set('editor.scrollSensitivity', 'nope') - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -10)) - waitsForAnimationFrame() - - runs -> - expect(verticalScrollbarNode.scrollTop).toBe 10 - - it "parses negative scrollSensitivity values at the minimum", -> - atom.config.set('editor.scrollSensitivity', -50) - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -10)) - waitsForAnimationFrame() - - runs -> - expect(verticalScrollbarNode.scrollTop).toBe 1 - - describe "when the mousewheel event's target is a line", -> - it "keeps the line on the DOM if it is scrolled off-screen", -> - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - lineNode = null - runs -> - lineNode = componentNode.querySelector('.line') - wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) - Object.defineProperty(wheelEvent, 'target', get: -> lineNode) - componentNode.dispatchEvent(wheelEvent) - waitsForAnimationFrame() - - runs -> - expect(componentNode.contains(lineNode)).toBe true - - it "does not set the mouseWheelScreenRow if scrolling horizontally", -> - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - lineNode = null - runs -> - lineNode = componentNode.querySelector('.line') - wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 10, wheelDeltaY: 0) - Object.defineProperty(wheelEvent, 'target', get: -> lineNode) - componentNode.dispatchEvent(wheelEvent) - waitsForAnimationFrame() - - runs -> - expect(component.presenter.mouseWheelScreenRow).toBe null - - it "clears the mouseWheelScreenRow after a delay even if the event does not cause scrolling", -> - expect(wrapperNode.getScrollTop()).toBe 0 - - lineNode = componentNode.querySelector('.line') - wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 10) - Object.defineProperty(wheelEvent, 'target', get: -> lineNode) - componentNode.dispatchEvent(wheelEvent) - - expect(wrapperNode.getScrollTop()).toBe 0 - - expect(component.presenter.mouseWheelScreenRow).toBe 0 - - waitsFor -> not component.presenter.mouseWheelScreenRow? - - it "does not preserve the line if it is on screen", -> - expect(componentNode.querySelectorAll('.line-number').length).toBe 14 # dummy line - lineNodes = componentNode.querySelectorAll('.line') - expect(lineNodes.length).toBe 13 - lineNode = lineNodes[0] - - wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 100) # goes nowhere, we're already at scrollTop 0 - Object.defineProperty(wheelEvent, 'target', get: -> lineNode) - componentNode.dispatchEvent(wheelEvent) - - expect(component.presenter.mouseWheelScreenRow).toBe 0 - editor.insertText("hello") - expect(componentNode.querySelectorAll('.line-number').length).toBe 14 # dummy line - expect(componentNode.querySelectorAll('.line').length).toBe 13 - - describe "when the mousewheel event's target is a line number", -> - it "keeps the line number on the DOM if it is scrolled off-screen", -> - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - lineNumberNode = null - runs -> - lineNumberNode = componentNode.querySelectorAll('.line-number')[1] - wheelEvent = new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -500) - Object.defineProperty(wheelEvent, 'target', get: -> lineNumberNode) - componentNode.dispatchEvent(wheelEvent) - waitsForAnimationFrame() - - runs -> - expect(componentNode.contains(lineNumberNode)).toBe true - - it "only prevents the default action of the mousewheel event if it actually lead to scrolling", -> - spyOn(WheelEvent::, 'preventDefault').andCallThrough() - - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' - wrapperNode.style.width = 20 * charWidth + 'px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - # try to scroll past the top, which is impossible - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: 50)) - expect(wrapperNode.getScrollTop()).toBe 0 - expect(WheelEvent::preventDefault).not.toHaveBeenCalled() - - # scroll to the bottom in one huge event - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -3000)) - waitsForAnimationFrame() - - runs -> - maxScrollTop = wrapperNode.getScrollTop() - expect(WheelEvent::preventDefault).toHaveBeenCalled() - WheelEvent::preventDefault.reset() - - # try to scroll past the bottom, which is impossible - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 0, wheelDeltaY: -30)) - expect(wrapperNode.getScrollTop()).toBe maxScrollTop - expect(WheelEvent::preventDefault).not.toHaveBeenCalled() - - # try to scroll past the left side, which is impossible - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: 50, wheelDeltaY: 0)) - expect(wrapperNode.getScrollLeft()).toBe 0 - expect(WheelEvent::preventDefault).not.toHaveBeenCalled() - - # scroll all the way right - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -3000, wheelDeltaY: 0)) - waitsForAnimationFrame() - - runs -> - maxScrollLeft = wrapperNode.getScrollLeft() - expect(WheelEvent::preventDefault).toHaveBeenCalled() - WheelEvent::preventDefault.reset() - - # try to scroll past the right side, which is impossible - componentNode.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -30, wheelDeltaY: 0)) - expect(wrapperNode.getScrollLeft()).toBe maxScrollLeft - expect(WheelEvent::preventDefault).not.toHaveBeenCalled() - - describe "input events", -> - inputNode = null - - beforeEach -> - inputNode = componentNode.querySelector('.hidden-input') - - buildTextInputEvent = ({data, target}) -> - event = new Event('textInput') - event.data = data - Object.defineProperty(event, 'target', get: -> target) - event - - it "inserts the newest character in the input's value into the buffer", -> - componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) - waitsForNextDOMUpdate() - runs -> - expect(editor.lineTextForBufferRow(0)).toBe 'xvar quicksort = function () {' - componentNode.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode)) - waitsForNextDOMUpdate() - runs -> - expect(editor.lineTextForBufferRow(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", -> - componentNode.dispatchEvent(buildTextInputEvent(data: 'u', target: inputNode)) - waitsForNextDOMUpdate() - runs -> - expect(editor.lineTextForBufferRow(0)).toBe 'uvar quicksort = function () {' - - # simulate the accented character suggestion's selection of the previous character - inputNode.setSelectionRange(0, 1) - componentNode.dispatchEvent(buildTextInputEvent(data: 'ü', target: inputNode)) - waitsForNextDOMUpdate() - - runs -> - expect(editor.lineTextForBufferRow(0)).toBe 'üvar quicksort = function () {' - - it "does not handle input events when input is disabled", -> - component.setInputEnabled(false) - componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () {' - waitsForAnimationFrame() - runs -> - expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () {' - - it "groups events that occur close together in time into single undo entries", -> - currentTime = 0 - spyOn(Date, 'now').andCallFake -> currentTime - - atom.config.set('editor.undoGroupingInterval', 100) - - editor.setText("") - componentNode.dispatchEvent(buildTextInputEvent(data: 'x', target: inputNode)) - - currentTime += 99 - componentNode.dispatchEvent(buildTextInputEvent(data: 'y', target: inputNode)) - - currentTime += 99 - componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', bubbles: true, cancelable: true)) - - currentTime += 101 - componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', bubbles: true, cancelable: true)) - expect(editor.getText()).toBe "xy\nxy\nxy" - - componentNode.dispatchEvent(new CustomEvent('core:undo', bubbles: true, cancelable: true)) - expect(editor.getText()).toBe "xy\nxy" - - componentNode.dispatchEvent(new CustomEvent('core:undo', bubbles: true, cancelable: true)) - expect(editor.getText()).toBe "" - - describe "when IME composition is used to insert international characters", -> - inputNode = null - - buildIMECompositionEvent = (event, {data, target}={}) -> - event = new Event(event) - event.data = data - Object.defineProperty(event, 'target', get: -> target) - event - - beforeEach -> - inputNode = inputNode = componentNode.querySelector('.hidden-input') - - describe "when nothing is selected", -> - it "inserts the chosen completion", -> - componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe 'svar quicksort = function () {' - - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe 'sdvar quicksort = function () {' - - componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) - componentNode.dispatchEvent(buildTextInputEvent(data: '速度', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe '速度var quicksort = function () {' - - it "reverts back to the original text when the completion helper is dismissed", -> - componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe 'svar quicksort = function () {' - - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe 'sdvar quicksort = function () {' - - componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () {' - - it "allows multiple accented character to be inserted with the ' on a US international layout", -> - inputNode.value = "'" - inputNode.setSelectionRange(0, 1) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: "'", target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe "'var quicksort = function () {" - - componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) - componentNode.dispatchEvent(buildTextInputEvent(data: 'á', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe "ávar quicksort = function () {" - - inputNode.value = "'" - inputNode.setSelectionRange(0, 1) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: "'", target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe "á'var quicksort = function () {" - - componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) - componentNode.dispatchEvent(buildTextInputEvent(data: 'á', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe "áávar quicksort = function () {" - - describe "when a string is selected", -> - beforeEach -> - editor.setSelectedBufferRanges [[[0, 4], [0, 9]], [[0, 16], [0, 19]]] # select 'quick' and 'fun' - - it "inserts the chosen completion", -> - componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe 'var ssort = sction () {' - - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe 'var sdsort = sdction () {' - - componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) - componentNode.dispatchEvent(buildTextInputEvent(data: '速度', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe 'var 速度sort = 速度ction () {' - - it "reverts back to the original text when the completion helper is dismissed", -> - componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', target: inputNode)) - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 's', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe 'var ssort = sction () {' - - componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', data: 'sd', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe 'var sdsort = sdction () {' - - componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', target: inputNode)) - expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () {' - - describe "commands", -> - describe "editor:consolidate-selections", -> - it "consolidates selections on the editor model, aborting the key binding if there is only one selection", -> - spyOn(editor, 'consolidateSelections').andCallThrough() - - event = new CustomEvent('editor:consolidate-selections', bubbles: true, cancelable: true) - event.abortKeyBinding = jasmine.createSpy("event.abortKeyBinding") - componentNode.dispatchEvent(event) - - expect(editor.consolidateSelections).toHaveBeenCalled() - expect(event.abortKeyBinding).toHaveBeenCalled() - - describe "when changing the font", -> - it "measures the default char, the korean char, the double width char and the half width char widths", -> - expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0) - - component.setFontSize(10) - waitsForNextDOMUpdate() - - runs -> - expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0) - expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0) - expect(editor.getDoubleWidthCharWidth()).toBe(10) - expect(editor.getHalfWidthCharWidth()).toBe(5) - - describe "hiding and showing the editor", -> - describe "when the editor is hidden when it is mounted", -> - it "defers measurement and rendering until the editor becomes visible", -> - wrapperNode.remove() - - hiddenParent = document.createElement('div') - hiddenParent.style.display = 'none' - contentNode.appendChild(hiddenParent) - - wrapperNode = new TextEditorElement() - wrapperNode.tileSize = tileSize - wrapperNode.initialize(editor, atom) - hiddenParent.appendChild(wrapperNode) - - {component} = wrapperNode - componentNode = component.getDomNode() - expect(componentNode.querySelectorAll('.line').length).toBe 0 - - hiddenParent.style.display = 'block' - atom.views.performDocumentPoll() - - expect(componentNode.querySelectorAll('.line').length).toBeGreaterThan 0 - - describe "when the lineHeight changes while the editor is hidden", -> - it "does not attempt to measure the lineHeightInPixels until the editor becomes visible again", -> - initialLineHeightInPixels = null - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - - initialLineHeightInPixels = editor.getLineHeightInPixels() - - component.setLineHeight(2) - expect(editor.getLineHeightInPixels()).toBe initialLineHeightInPixels - - wrapperNode.style.display = '' - component.checkForVisibilityChange() - - expect(editor.getLineHeightInPixels()).not.toBe initialLineHeightInPixels - - describe "when the fontSize changes while the editor is hidden", -> - it "does not attempt to measure the lineHeightInPixels or defaultCharWidth until the editor becomes visible again", -> - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - - initialLineHeightInPixels = editor.getLineHeightInPixels() - initialCharWidth = editor.getDefaultCharWidth() - - component.setFontSize(22) - expect(editor.getLineHeightInPixels()).toBe initialLineHeightInPixels - expect(editor.getDefaultCharWidth()).toBe initialCharWidth - - wrapperNode.style.display = '' - component.checkForVisibilityChange() - - expect(editor.getLineHeightInPixels()).not.toBe initialLineHeightInPixels - expect(editor.getDefaultCharWidth()).not.toBe initialCharWidth - - it "does not re-measure character widths until the editor is shown again", -> - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - - component.setFontSize(22) - editor.getBuffer().insert([0, 0], 'a') # regression test against atom/atom#3318 - - wrapperNode.style.display = '' - component.checkForVisibilityChange() - - editor.setCursorBufferPosition([0, Infinity]) - waitsForNextDOMUpdate() - - runs -> - cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left - line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right - expect(cursorLeft).toBeCloseTo line0Right, 0 - - describe "when the fontFamily changes while the editor is hidden", -> - it "does not attempt to measure the defaultCharWidth until the editor becomes visible again", -> - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - - initialLineHeightInPixels = editor.getLineHeightInPixels() - initialCharWidth = editor.getDefaultCharWidth() - - component.setFontFamily('serif') - expect(editor.getDefaultCharWidth()).toBe initialCharWidth - - wrapperNode.style.display = '' - component.checkForVisibilityChange() - - expect(editor.getDefaultCharWidth()).not.toBe initialCharWidth - - it "does not re-measure character widths until the editor is shown again", -> - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - - component.setFontFamily('serif') - - wrapperNode.style.display = '' - component.checkForVisibilityChange() - - editor.setCursorBufferPosition([0, Infinity]) - waitsForNextDOMUpdate() - - runs -> - cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left - line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right - expect(cursorLeft).toBeCloseTo line0Right, 0 - - describe "when stylesheets change while the editor is hidden", -> - afterEach -> - atom.themes.removeStylesheet('test') - - it "does not re-measure character widths until the editor is shown again", -> - atom.config.set('editor.fontFamily', 'sans-serif') - - wrapperNode.style.display = 'none' - component.checkForVisibilityChange() - - atom.themes.applyStylesheet 'test', """ - .function.js { - font-weight: bold; - } - """ - - wrapperNode.style.display = '' - component.checkForVisibilityChange() - - editor.setCursorBufferPosition([0, Infinity]) - waitsForNextDOMUpdate() - - runs -> - cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left - line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right - expect(cursorLeft).toBeCloseTo line0Right, 0 - - describe "soft wrapping", -> - beforeEach -> - editor.setSoftWrapped(true) - waitsForNextDOMUpdate() - - it "updates the wrap location when the editor is resized", -> - newHeight = 4 * editor.getLineHeightInPixels() + "px" - expect(parseInt(newHeight)).toBeLessThan wrapperNode.offsetHeight - wrapperNode.style.height = newHeight - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelectorAll('.line')).toHaveLength(7) # visible rows + model longest screen row - - gutterWidth = componentNode.querySelector('.gutter').offsetWidth - componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - atom.views.performDocumentPoll() - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelector('.line').textContent).toBe "var quicksort " - - it "accounts for the scroll view's padding when determining the wrap location", -> - scrollViewNode = componentNode.querySelector('.scroll-view') - scrollViewNode.style.paddingLeft = 20 + 'px' - componentNode.style.width = 30 * charWidth + 'px' - - atom.views.performDocumentPoll() - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(0).textContent).toBe "var quicksort = " - - describe "default decorations", -> - it "applies .cursor-line decorations for line numbers overlapping selections", -> - editor.setCursorScreenPosition([4, 4]) - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(3, 'cursor-line')).toBe false - expect(lineNumberHasClass(4, 'cursor-line')).toBe true - expect(lineNumberHasClass(5, 'cursor-line')).toBe false - - editor.setSelectedScreenRange([[3, 4], [4, 4]]) - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(3, 'cursor-line')).toBe true - expect(lineNumberHasClass(4, 'cursor-line')).toBe true - - editor.setSelectedScreenRange([[3, 4], [4, 0]]) - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(3, 'cursor-line')).toBe true - expect(lineNumberHasClass(4, 'cursor-line')).toBe false - - it "does not apply .cursor-line to the last line of a selection if it's empty", -> - editor.setSelectedScreenRange([[3, 4], [5, 0]]) - waitsForNextDOMUpdate() - runs -> - expect(lineNumberHasClass(3, 'cursor-line')).toBe true - expect(lineNumberHasClass(4, 'cursor-line')).toBe true - expect(lineNumberHasClass(5, 'cursor-line')).toBe false - - it "applies .cursor-line decorations for lines containing the cursor in non-empty selections", -> - editor.setCursorScreenPosition([4, 4]) - waitsForNextDOMUpdate() - runs -> - expect(lineHasClass(3, 'cursor-line')).toBe false - expect(lineHasClass(4, 'cursor-line')).toBe true - expect(lineHasClass(5, 'cursor-line')).toBe false - - editor.setSelectedScreenRange([[3, 4], [4, 4]]) - waitsForNextDOMUpdate() - - runs -> - expect(lineHasClass(2, 'cursor-line')).toBe false - expect(lineHasClass(3, 'cursor-line')).toBe false - expect(lineHasClass(4, 'cursor-line')).toBe false - expect(lineHasClass(5, 'cursor-line')).toBe false - - it "applies .cursor-line-no-selection to line numbers for rows containing the cursor when the selection is empty", -> - editor.setCursorScreenPosition([4, 4]) - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe true - - editor.setSelectedScreenRange([[3, 4], [4, 4]]) - waitsForNextDOMUpdate() - - runs -> - expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe false - - describe "height", -> - describe "when the wrapper view has an explicit height", -> - it "does not assign a height on the component node", -> - wrapperNode.style.height = '200px' - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.style.height).toBe '' - - describe "when the wrapper view does not have an explicit height", -> - it "assigns a height on the component node based on the editor's content", -> - expect(wrapperNode.style.height).toBe '' - expect(componentNode.style.height).toBe editor.getScreenLineCount() * lineHeightInPixels + 'px' - - describe "when the 'mini' property is true", -> - beforeEach -> - editor.setMini(true) - waitsForNextDOMUpdate() - - it "does not render the gutter", -> - expect(componentNode.querySelector('.gutter')).toBeNull() - - it "adds the 'mini' class to the wrapper view", -> - expect(wrapperNode.classList.contains('mini')).toBe true - - it "does not have an opaque background on lines", -> - expect(component.linesComponent.getDomNode().getAttribute('style')).not.toContain 'background-color' - - it "does not render invisible characters", -> - atom.config.set('editor.invisibles', eol: 'E') - atom.config.set('editor.showInvisibles', true) - expect(component.lineNodeForScreenRow(0).textContent).toBe 'var quicksort = function () {' - - it "does not assign an explicit line-height on the editor contents", -> - expect(componentNode.style.lineHeight).toBe '' - - it "does not apply cursor-line decorations", -> - expect(component.lineNodeForScreenRow(0).classList.contains('cursor-line')).toBe false - - describe "when placholderText is specified", -> - it "renders the placeholder text when the buffer is empty", -> - editor.setPlaceholderText('Hello World') - expect(componentNode.querySelector('.placeholder-text')).toBeNull() - editor.setText('') - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelector('.placeholder-text').textContent).toBe "Hello World" - editor.setText('hey') - waitsForNextDOMUpdate() - - runs -> - expect(componentNode.querySelector('.placeholder-text')).toBeNull() - - describe "grammar data attributes", -> - it "adds and updates the grammar data attribute based on the current grammar", -> - expect(wrapperNode.dataset.grammar).toBe 'source js' - editor.setGrammar(atom.grammars.nullGrammar) - expect(wrapperNode.dataset.grammar).toBe 'text plain null-grammar' - - describe "encoding data attributes", -> - it "adds and updates the encoding data attribute based on the current encoding", -> - expect(wrapperNode.dataset.encoding).toBe 'utf8' - editor.setEncoding('utf16le') - expect(wrapperNode.dataset.encoding).toBe 'utf16le' - - describe "detaching and reattaching the editor (regression)", -> - it "does not throw an exception", -> - wrapperNode.remove() - jasmine.attachToDOM(wrapperNode) - - atom.commands.dispatch(wrapperNode, 'core:move-right') - - expect(editor.getCursorBufferPosition()).toEqual [0, 1] - - describe 'scoped config settings', -> - [coffeeEditor, coffeeComponent] = [] - - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - waitsForPromise -> - atom.workspace.open('coffee.coffee', autoIndent: false).then (o) -> coffeeEditor = o - - afterEach: -> - atom.packages.deactivatePackages() - atom.packages.unloadPackages() - - describe 'soft wrap settings', -> - beforeEach -> - atom.config.set 'editor.softWrap', true, scopeSelector: '.source.coffee' - atom.config.set 'editor.preferredLineLength', 17, scopeSelector: '.source.coffee' - atom.config.set 'editor.softWrapAtPreferredLineLength', true, scopeSelector: '.source.coffee' - - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(20) - coffeeEditor.setDefaultCharWidth(1) - coffeeEditor.setEditorWidthInChars(20) - - it "wraps lines when editor.softWrap is true for a matching scope", -> - expect(editor.lineTextForScreenRow(2)).toEqual ' if (items.length <= 1) return items;' - expect(coffeeEditor.lineTextForScreenRow(3)).toEqual ' return items ' - - it 'updates the wrapped lines when editor.preferredLineLength changes', -> - atom.config.set 'editor.preferredLineLength', 20, scopeSelector: '.source.coffee' - expect(coffeeEditor.lineTextForScreenRow(2)).toEqual ' return items if ' - - it 'updates the wrapped lines when editor.softWrapAtPreferredLineLength changes', -> - atom.config.set 'editor.softWrapAtPreferredLineLength', false, scopeSelector: '.source.coffee' - expect(coffeeEditor.lineTextForScreenRow(2)).toEqual ' return items if ' - - it 'updates the wrapped lines when editor.softWrap changes', -> - atom.config.set 'editor.softWrap', false, scopeSelector: '.source.coffee' - expect(coffeeEditor.lineTextForScreenRow(2)).toEqual ' return items if items.length <= 1' - - atom.config.set 'editor.softWrap', true, scopeSelector: '.source.coffee' - expect(coffeeEditor.lineTextForScreenRow(3)).toEqual ' return items ' - - it 'updates the wrapped lines when the grammar changes', -> - editor.setGrammar(coffeeEditor.getGrammar()) - expect(editor.isSoftWrapped()).toBe true - expect(editor.lineTextForScreenRow(0)).toEqual 'var quicksort = ' - - describe '::isSoftWrapped()', -> - it 'returns the correct value based on the scoped settings', -> - expect(editor.isSoftWrapped()).toBe false - expect(coffeeEditor.isSoftWrapped()).toBe true - - describe 'invisibles settings', -> - [jsInvisibles, coffeeInvisibles] = [] - beforeEach -> - jsInvisibles = - eol: 'J' - space: 'A' - tab: 'V' - cr: 'A' - - coffeeInvisibles = - eol: 'C' - space: 'O' - tab: 'F' - cr: 'E' - - atom.config.set 'editor.showInvisibles', true, scopeSelector: '.source.js' - atom.config.set 'editor.invisibles', jsInvisibles, scopeSelector: '.source.js' - - atom.config.set 'editor.showInvisibles', false, scopeSelector: '.source.coffee' - atom.config.set 'editor.invisibles', coffeeInvisibles, scopeSelector: '.source.coffee' - - editor.setText " a line with tabs\tand spaces \n" - waitsForNextDOMUpdate() - - it "renders the invisibles when editor.showInvisibles is true for a given grammar", -> - expect(component.lineNodeForScreenRow(0).textContent).toBe "#{jsInvisibles.space}a line with tabs#{jsInvisibles.tab}and spaces#{jsInvisibles.space}#{jsInvisibles.eol}" - - it "does not render the invisibles when editor.showInvisibles is false for a given grammar", -> - editor.setGrammar(coffeeEditor.getGrammar()) - waitsForNextDOMUpdate() - runs -> - expect(component.lineNodeForScreenRow(0).textContent).toBe " a line with tabs and spaces " - - it "re-renders the invisibles when the invisible settings change", -> - jsGrammar = editor.getGrammar() - editor.setGrammar(coffeeEditor.getGrammar()) - atom.config.set 'editor.showInvisibles', true, scopeSelector: '.source.coffee' - waitsForNextDOMUpdate() - - newInvisibles = - eol: 'N' - space: 'E' - tab: 'W' - cr: 'I' - - runs -> - expect(component.lineNodeForScreenRow(0).textContent).toBe "#{coffeeInvisibles.space}a line with tabs#{coffeeInvisibles.tab}and spaces#{coffeeInvisibles.space}#{coffeeInvisibles.eol}" - atom.config.set 'editor.invisibles', newInvisibles, scopeSelector: '.source.coffee' - - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(0).textContent).toBe "#{newInvisibles.space}a line with tabs#{newInvisibles.tab}and spaces#{newInvisibles.space}#{newInvisibles.eol}" - editor.setGrammar(jsGrammar) - waitsForNextDOMUpdate() - - runs -> - expect(component.lineNodeForScreenRow(0).textContent).toBe "#{jsInvisibles.space}a line with tabs#{jsInvisibles.tab}and spaces#{jsInvisibles.space}#{jsInvisibles.eol}" - - describe 'editor.showIndentGuide', -> - beforeEach -> - atom.config.set 'editor.showIndentGuide', true, scopeSelector: '.source.js' - atom.config.set 'editor.showIndentGuide', false, scopeSelector: '.source.coffee' - waitsForNextDOMUpdate() - - it "has an 'indent-guide' class when scoped editor.showIndentGuide is true, but not when scoped editor.showIndentGuide is false", -> - line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) - expect(line1LeafNodes[0].textContent).toBe ' ' - expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false - - editor.setGrammar(coffeeEditor.getGrammar()) - waitsForNextDOMUpdate() - - runs -> - line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) - expect(line1LeafNodes[0].textContent).toBe ' ' - expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe false - expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false - - it "removes the 'indent-guide' class when editor.showIndentGuide to false", -> - line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) - expect(line1LeafNodes[0].textContent).toBe ' ' - expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe true - expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false - - atom.config.set 'editor.showIndentGuide', false, scopeSelector: '.source.js' - waitsForNextDOMUpdate() - - runs -> - line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) - expect(line1LeafNodes[0].textContent).toBe ' ' - expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe false - expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false - - describe "autoscroll", -> - beforeEach -> - editor.setVerticalScrollMargin(2) - editor.setHorizontalScrollMargin(2) - component.setLineHeight("10px") - component.setFontSize(17) - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - wrapperNode.setWidth(55) - wrapperNode.setHeight(55) - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - component.presenter.setHorizontalScrollbarHeight(0) - component.presenter.setVerticalScrollbarWidth(0) - waitsForNextDOMUpdate() - - describe "when selecting buffer ranges", -> - it "autoscrolls the selection if it is last unless the 'autoscroll' option is false", -> - expect(wrapperNode.getScrollTop()).toBe 0 - - editor.setSelectedBufferRange([[5, 6], [6, 8]]) - waitsForNextDOMUpdate() - - right = null - runs -> - right = wrapperNode.pixelPositionForBufferPosition([6, 8 + editor.getHorizontalScrollMargin()]).left - expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 - expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 - - editor.setSelectedBufferRange([[0, 0], [0, 0]]) - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - expect(wrapperNode.getScrollLeft()).toBe 0 - - editor.setSelectedBufferRange([[6, 6], [6, 8]]) - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 - expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 - - describe "when adding selections for buffer ranges", -> - it "autoscrolls to the added selection if needed", -> - editor.addSelectionForBufferRange([[8, 10], [8, 15]]) - waitsForNextDOMUpdate() - - runs -> - right = wrapperNode.pixelPositionForBufferPosition([8, 15]).left - expect(wrapperNode.getScrollBottom()).toBe (9 * 10) + (2 * 10) - expect(wrapperNode.getScrollRight()).toBeCloseTo(right + 2 * 10, 0) - - describe "when selecting lines containing cursors", -> - it "autoscrolls to the selection", -> - editor.setCursorScreenPosition([5, 6]) - waitsForNextDOMUpdate() - runs -> - wrapperNode.scrollToTop() - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.selectLinesContainingCursors() - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 - - describe "when inserting text", -> - describe "when there are multiple empty selections on different lines", -> - it "autoscrolls to the last cursor", -> - editor.setCursorScreenPosition([1, 2], autoscroll: false) - waitsForNextDOMUpdate() - runs -> - editor.addCursorAtScreenPosition([10, 4], autoscroll: false) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.insertText('a') - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 75 - - describe "when scrolled to cursor position", -> - it "scrolls the last cursor into view, centering around the cursor if possible and the 'center' option isn't false", -> - editor.setCursorScreenPosition([8, 8], autoscroll: false) - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - expect(wrapperNode.getScrollLeft()).toBe 0 - - editor.scrollToCursorPosition() - waitsForNextDOMUpdate() - - runs -> - right = wrapperNode.pixelPositionForScreenPosition([8, 9 + editor.getHorizontalScrollMargin()]).left - expect(wrapperNode.getScrollTop()).toBe (8.8 * 10) - 30 - expect(wrapperNode.getScrollBottom()).toBe (8.3 * 10) + 30 - expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 - - wrapperNode.setScrollTop(0) - editor.scrollToCursorPosition(center: false) - expect(wrapperNode.getScrollTop()).toBe (7.8 - editor.getVerticalScrollMargin()) * 10 - expect(wrapperNode.getScrollBottom()).toBe (9.3 + editor.getVerticalScrollMargin()) * 10 - - describe "moving cursors", -> - it "scrolls down when the last cursor gets closer than ::verticalScrollMargin to the bottom of the editor", -> - expect(wrapperNode.getScrollTop()).toBe 0 - expect(wrapperNode.getScrollBottom()).toBe 5.5 * 10 - - editor.setCursorScreenPosition([2, 0]) - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollBottom()).toBe 5.5 * 10 - - editor.moveDown() - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollBottom()).toBe 6 * 10 - - editor.moveDown() - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollBottom()).toBe 7 * 10 - - it "scrolls up when the last cursor gets closer than ::verticalScrollMargin to the top of the editor", -> - editor.setCursorScreenPosition([11, 0]) - - waitsForNextDOMUpdate() - runs -> - wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) - waitsForNextDOMUpdate() - runs -> - editor.moveUp() - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollBottom()).toBe wrapperNode.getScrollHeight() - editor.moveUp() - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 7 * 10 - editor.moveUp() - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 6 * 10 - - it "scrolls right when the last cursor gets closer than ::horizontalScrollMargin to the right of the editor", -> - expect(wrapperNode.getScrollLeft()).toBe 0 - expect(wrapperNode.getScrollRight()).toBe 5.5 * 10 - - editor.setCursorScreenPosition([0, 2]) - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollRight()).toBe 5.5 * 10 - - editor.moveRight() - waitsForNextDOMUpdate() - - margin = null - runs -> - margin = component.presenter.getHorizontalScrollMarginInPixels() - right = wrapperNode.pixelPositionForScreenPosition([0, 4]).left + margin - expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 - editor.moveRight() - - waitsForNextDOMUpdate() - - runs -> - right = wrapperNode.pixelPositionForScreenPosition([0, 5]).left + margin - expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 - - it "scrolls left when the last cursor gets closer than ::horizontalScrollMargin to the left of the editor", -> - wrapperNode.setScrollRight(wrapperNode.getScrollWidth()) - - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollRight()).toBe wrapperNode.getScrollWidth() - editor.setCursorScreenPosition([6, 62], autoscroll: false) - waitsForNextDOMUpdate() - - runs -> - editor.moveLeft() - waitsForNextDOMUpdate() - - margin = null - runs -> - margin = component.presenter.getHorizontalScrollMarginInPixels() - left = wrapperNode.pixelPositionForScreenPosition([6, 61]).left - margin - expect(wrapperNode.getScrollLeft()).toBeCloseTo left, 0 - editor.moveLeft() - waitsForNextDOMUpdate() - - runs -> - left = wrapperNode.pixelPositionForScreenPosition([6, 60]).left - margin - expect(wrapperNode.getScrollLeft()).toBeCloseTo left, 0 - - it "scrolls down when inserting lines makes the document longer than the editor's height", -> - editor.setCursorScreenPosition([13, Infinity]) - editor.insertNewline() - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollBottom()).toBe 14 * 10 - editor.insertNewline() - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollBottom()).toBe 15 * 10 - - it "autoscrolls to the cursor when it moves due to undo", -> - editor.insertText('abc') - wrapperNode.setScrollTop(Infinity) - waitsForNextDOMUpdate() - - runs -> - editor.undo() - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - - it "doesn't scroll when the cursor moves into the visible area", -> - editor.setCursorBufferPosition([0, 0]) - waitsForNextDOMUpdate() - - runs -> - wrapperNode.setScrollTop(40) - waitsForNextDOMUpdate() - - runs -> - editor.setCursorBufferPosition([6, 0]) - waitsForNextDOMUpdate() - - runs -> - expect(wrapperNode.getScrollTop()).toBe 40 - - it "honors the autoscroll option on cursor and selection manipulation methods", -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.addCursorAtScreenPosition([11, 11], autoscroll: false) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.addCursorAtBufferPosition([11, 11], autoscroll: false) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.setCursorScreenPosition([11, 11], autoscroll: false) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.setCursorBufferPosition([11, 11], autoscroll: false) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.addSelectionForBufferRange([[11, 11], [11, 11]], autoscroll: false) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.addSelectionForScreenRange([[11, 11], [11, 12]], autoscroll: false) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.setSelectedBufferRange([[11, 0], [11, 1]], autoscroll: false) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.setSelectedScreenRange([[11, 0], [11, 6]], autoscroll: false) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.clearSelections(autoscroll: false) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.addSelectionForScreenRange([[0, 0], [0, 4]]) - waitsForNextDOMUpdate() - runs -> - editor.getCursors()[0].setScreenPosition([11, 11], autoscroll: true) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 - editor.getCursors()[0].setBufferPosition([0, 0], autoscroll: true) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], autoscroll: true) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBeGreaterThan 0 - editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], autoscroll: true) - waitsForNextDOMUpdate() - runs -> - expect(wrapperNode.getScrollTop()).toBe 0 - - describe "::getVisibleRowRange()", -> - beforeEach -> - wrapperNode.style.height = lineHeightInPixels * 8 + "px" - component.measureDimensions() - waitsForNextDOMUpdate() - - it "returns the first and the last visible rows", -> - component.setScrollTop(0) - waitsForNextDOMUpdate() - - runs -> - expect(component.getVisibleRowRange()).toEqual [0, 9] - - it "ends at last buffer row even if there's more space available", -> - wrapperNode.style.height = lineHeightInPixels * 13 + "px" - component.measureDimensions() - waitsForNextDOMUpdate() - - runs -> - component.setScrollTop(60) - waitsForNextDOMUpdate() - - runs -> - expect(component.getVisibleRowRange()).toEqual [0, 13] - - describe "middle mouse paste on Linux", -> - originalPlatform = null - - beforeEach -> - originalPlatform = process.platform - Object.defineProperty process, 'platform', value: 'linux' - - afterEach -> - Object.defineProperty process, 'platform', value: originalPlatform - - it "pastes the previously selected text at the clicked location", -> - clipboardWrittenTo = false - spyOn(require('ipc'), 'send').andCallFake (eventName, selectedText) -> - if eventName is 'write-text-to-selection-clipboard' - require('../src/safe-clipboard').writeText(selectedText, 'selection') - clipboardWrittenTo = true - - atom.clipboard.write('') - component.trackSelectionClipboard() - editor.setSelectedBufferRange([[1, 6], [1, 10]]) - - waitsFor -> - clipboardWrittenTo - - runs -> - componentNode.querySelector('.scroll-view').dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([10, 0]), button: 1)) - componentNode.querySelector('.scroll-view').dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([10, 0]), which: 2)) - expect(atom.clipboard.read()).toBe 'sort' - expect(editor.lineTextForBufferRow(10)).toBe 'sort' - - buildMouseEvent = (type, properties...) -> - properties = extend({bubbles: true, cancelable: true}, properties...) - properties.detail ?= 1 - event = new MouseEvent(type, properties) - Object.defineProperty(event, 'which', get: -> properties.which) if properties.which? - if properties.target? - Object.defineProperty(event, 'target', get: -> properties.target) - Object.defineProperty(event, 'srcObject', get: -> properties.target) - event - - clientCoordinatesForScreenPosition = (screenPosition) -> - positionOffset = wrapperNode.pixelPositionForScreenPosition(screenPosition) - scrollViewClientRect = componentNode.querySelector('.scroll-view').getBoundingClientRect() - clientX = scrollViewClientRect.left + positionOffset.left - wrapperNode.getScrollLeft() - clientY = scrollViewClientRect.top + positionOffset.top - wrapperNode.getScrollTop() - {clientX, clientY} - - clientCoordinatesForScreenRowInGutter = (screenRow) -> - positionOffset = wrapperNode.pixelPositionForScreenPosition([screenRow, Infinity]) - gutterClientRect = componentNode.querySelector('.gutter').getBoundingClientRect() - clientX = gutterClientRect.left + positionOffset.left - wrapperNode.getScrollLeft() - clientY = gutterClientRect.top + positionOffset.top - wrapperNode.getScrollTop() - {clientX, clientY} - - lineAndLineNumberHaveClass = (screenRow, klass) -> - lineHasClass(screenRow, klass) and lineNumberHasClass(screenRow, klass) - - lineNumberHasClass = (screenRow, klass) -> - component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) - - lineNumberForBufferRowHasClass = (bufferRow, klass) -> - screenRow = editor.displayBuffer.screenRowForBufferRow(bufferRow) - component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) - - lineHasClass = (screenRow, klass) -> - component.lineNodeForScreenRow(screenRow).classList.contains(klass) - - getLeafNodes = (node) -> - if node.children.length > 0 - flatten(toArray(node.children).map(getLeafNodes)) - else - [node] - - waitsForNextDOMUpdate = -> - waitsForPromise -> atom.views.getNextUpdatePromise() - - waitsForAnimationFrame = -> - waitsFor 'next animation frame', (done) -> requestAnimationFrame(done) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js new file mode 100644 index 000000000..bfd7646de --- /dev/null +++ b/spec/text-editor-component-spec.js @@ -0,0 +1,4735 @@ +/** @babel */ + +import {it, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' +import TextEditorElement from '../src/text-editor-element' +import _, {extend, flatten, last, toArray} from 'underscore-plus' + +const NBSP = String.fromCharCode(160) +const TILE_SIZE = 3 + +describe('TextEditorComponent', function () { + let charWidth, component, componentNode, contentNode, editor, + horizontalScrollbarNode, lineHeightInPixels, tileHeightInPixels, + verticalScrollbarNode, wrapperNode + + beforeEach(async function () { + jasmine.useRealClock() + + await atom.packages.activatePackage('language-javascript') + editor = await atom.workspace.open('sample.js') + + contentNode = document.querySelector('#jasmine-content') + contentNode.style.width = '1000px' + + wrapperNode = new TextEditorElement() + wrapperNode.tileSize = TILE_SIZE + wrapperNode.initialize(editor, atom) + wrapperNode.setUpdatedSynchronously(false) + jasmine.attachToDOM(wrapperNode) + + component = wrapperNode.component + component.setFontFamily('monospace') + component.setLineHeight(1.3) + component.setFontSize(20) + + lineHeightInPixels = editor.getLineHeightInPixels() + tileHeightInPixels = TILE_SIZE * lineHeightInPixels + charWidth = editor.getDefaultCharWidth() + + componentNode = component.getDomNode() + verticalScrollbarNode = componentNode.querySelector('.vertical-scrollbar') + horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') + + component.measureDimensions() + await atom.views.getNextUpdatePromise() + }) + + afterEach(function () { + contentNode.style.width = '' + }) + + describe('async updates', function () { + it('handles corrupted state gracefully', async function () { + editor.insertNewline() + component.presenter.startRow = -1 + component.presenter.endRow = 9999 + await atom.views.getNextUpdatePromise() // assert an update does occur + }) + + it('does not update when an animation frame was requested but the component got destroyed before its delivery', async function () { + editor.setText('You should not see this update.') + component.destroy() + + await atom.views.getNextUpdatePromise() + + expect(component.lineNodeForScreenRow(0).textContent).not.toBe('You should not see this update.') + }) + }) + + describe('line rendering', async function () { + function expectTileContainsRow (tileNode, screenRow, {top}) { + let lineNode = tileNode.querySelector('[data-screen-row="' + screenRow + '"]') + let tokenizedLine = editor.tokenizedLineForScreenRow(screenRow) + + expect(lineNode.offsetTop).toBe(top) + if (tokenizedLine.text === '') { + expect(lineNode.innerHTML).toBe(' ') + } else { + expect(lineNode.textContent).toBe(tokenizedLine.text) + } + } + + it('gives the lines container the same height as the wrapper node', async function () { + let linesNode = componentNode.querySelector('.lines') + wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) + wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' + component.measureDimensions() + + await atom.views.getNextUpdatePromise() + + expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) + }) + + it('renders higher tiles in front of lower ones', async function () { + wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' + component.measureDimensions() + + await atom.views.getNextUpdatePromise() + + let tilesNodes = component.tileNodesForLines() + expect(tilesNodes[0].style.zIndex).toBe('2') + expect(tilesNodes[1].style.zIndex).toBe('1') + expect(tilesNodes[2].style.zIndex).toBe('0') + verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + + await atom.views.getNextUpdatePromise() + + tilesNodes = component.tileNodesForLines() + expect(tilesNodes[0].style.zIndex).toBe('3') + expect(tilesNodes[1].style.zIndex).toBe('2') + expect(tilesNodes[2].style.zIndex).toBe('1') + expect(tilesNodes[3].style.zIndex).toBe('0') + }) + + it('renders the currently-visible lines in a tiled fashion', async function () { + wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' + component.measureDimensions() + + await atom.views.getNextUpdatePromise() + + let tilesNodes = component.tileNodesForLines() + expect(tilesNodes.length).toBe(3) + + expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') + expect(tilesNodes[0].querySelectorAll('.line').length).toBe(TILE_SIZE) + expectTileContainsRow(tilesNodes[0], 0, { + top: 0 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[0], 1, { + top: 1 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[0], 2, { + top: 2 * lineHeightInPixels + }) + + expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels) + 'px, 0px)') + expect(tilesNodes[1].querySelectorAll('.line').length).toBe(TILE_SIZE) + expectTileContainsRow(tilesNodes[1], 3, { + top: 0 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[1], 4, { + top: 1 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[1], 5, { + top: 2 * lineHeightInPixels + }) + + expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels) + 'px, 0px)') + expect(tilesNodes[2].querySelectorAll('.line').length).toBe(TILE_SIZE) + expectTileContainsRow(tilesNodes[2], 6, { + top: 0 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[2], 7, { + top: 1 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[2], 8, { + top: 2 * lineHeightInPixels + }) + + expect(component.lineNodeForScreenRow(9)).toBeUndefined() + + verticalScrollbarNode.scrollTop = TILE_SIZE * lineHeightInPixels + 5 + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + + await atom.views.getNextUpdatePromise() + + tilesNodes = component.tileNodesForLines() + expect(component.lineNodeForScreenRow(2)).toBeUndefined() + expect(tilesNodes.length).toBe(3) + + expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, ' + (0 * tileHeightInPixels - 5) + 'px, 0px)') + expect(tilesNodes[0].querySelectorAll('.line').length).toBe(TILE_SIZE) + expectTileContainsRow(tilesNodes[0], 3, { + top: 0 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[0], 4, { + top: 1 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[0], 5, { + top: 2 * lineHeightInPixels + }) + + expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels - 5) + 'px, 0px)') + expect(tilesNodes[1].querySelectorAll('.line').length).toBe(TILE_SIZE) + expectTileContainsRow(tilesNodes[1], 6, { + top: 0 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[1], 7, { + top: 1 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[1], 8, { + top: 2 * lineHeightInPixels + }) + + expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels - 5) + 'px, 0px)') + expect(tilesNodes[2].querySelectorAll('.line').length).toBe(TILE_SIZE) + expectTileContainsRow(tilesNodes[2], 9, { + top: 0 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[2], 10, { + top: 1 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[2], 11, { + top: 2 * lineHeightInPixels + }) + }) + + it('updates the top position of subsequent tiles when lines are inserted or removed', async function () { + wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' + component.measureDimensions() + editor.getBuffer().deleteRows(0, 1) + + await atom.views.getNextUpdatePromise() + + let tilesNodes = component.tileNodesForLines() + expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') + expectTileContainsRow(tilesNodes[0], 0, { + top: 0 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[0], 1, { + top: 1 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[0], 2, { + top: 2 * lineHeightInPixels + }) + + expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels) + 'px, 0px)') + expectTileContainsRow(tilesNodes[1], 3, { + top: 0 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[1], 4, { + top: 1 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[1], 5, { + top: 2 * lineHeightInPixels + }) + + editor.getBuffer().insert([0, 0], '\n\n') + + await atom.views.getNextUpdatePromise() + + tilesNodes = component.tileNodesForLines() + expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') + expectTileContainsRow(tilesNodes[0], 0, { + top: 0 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[0], 1, { + top: 1 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[0], 2, { + top: 2 * lineHeightInPixels + }) + + expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels) + 'px, 0px)') + expectTileContainsRow(tilesNodes[1], 3, { + top: 0 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[1], 4, { + top: 1 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[1], 5, { + top: 2 * lineHeightInPixels + }) + + expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels) + 'px, 0px)') + expectTileContainsRow(tilesNodes[2], 6, { + top: 0 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[2], 7, { + top: 1 * lineHeightInPixels + }) + expectTileContainsRow(tilesNodes[2], 8, { + top: 2 * lineHeightInPixels + }) + }) + + it('updates the lines when lines are inserted or removed above the rendered row range', async function () { + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + component.measureDimensions() + + await atom.views.getNextUpdatePromise() + + verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + + await atom.views.getNextUpdatePromise() + + let buffer = editor.getBuffer() + buffer.insert([0, 0], '\n\n') + + await atom.views.getNextUpdatePromise() + + expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text) + buffer.delete([[0, 0], [3, 0]]) + + await atom.views.getNextUpdatePromise() + + expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text) + }) + + it('updates the top position of lines when the line height changes', async function () { + let initialLineHeightInPixels = editor.getLineHeightInPixels() + + component.setLineHeight(2) + + await atom.views.getNextUpdatePromise() + + let newLineHeightInPixels = editor.getLineHeightInPixels() + expect(newLineHeightInPixels).not.toBe(initialLineHeightInPixels) + expect(component.lineNodeForScreenRow(1).offsetTop).toBe(1 * newLineHeightInPixels) + }) + + it('updates the top position of lines when the font size changes', async function () { + let initialLineHeightInPixels = editor.getLineHeightInPixels() + component.setFontSize(10) + + await atom.views.getNextUpdatePromise() + + let newLineHeightInPixels = editor.getLineHeightInPixels() + expect(newLineHeightInPixels).not.toBe(initialLineHeightInPixels) + expect(component.lineNodeForScreenRow(1).offsetTop).toBe(1 * newLineHeightInPixels) + }) + + it('renders the .lines div at the full height of the editor if there are not enough lines to scroll vertically', async function () { + editor.setText('') + wrapperNode.style.height = '300px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + let linesNode = componentNode.querySelector('.lines') + expect(linesNode.offsetHeight).toBe(300) + }) + + it('assigns the width of each line so it extends across the full width of the editor', async function () { + let gutterWidth = componentNode.querySelector('.gutter').offsetWidth + let scrollViewNode = componentNode.querySelector('.scroll-view') + let lineNodes = Array.from(componentNode.querySelectorAll('.line')) + + componentNode.style.width = gutterWidth + (30 * charWidth) + 'px' + component.measureDimensions() + + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollWidth()).toBeGreaterThan(scrollViewNode.offsetWidth) + let editorFullWidth = wrapperNode.getScrollWidth() + wrapperNode.getVerticalScrollbarWidth() + for (let lineNode of lineNodes) { + expect(lineNode.getBoundingClientRect().width).toBe(editorFullWidth) + } + + componentNode.style.width = gutterWidth + wrapperNode.getScrollWidth() + 100 + 'px' + component.measureDimensions() + + await atom.views.getNextUpdatePromise() + + let scrollViewWidth = scrollViewNode.offsetWidth + for (let lineNode of lineNodes) { + expect(lineNode.getBoundingClientRect().width).toBe(scrollViewWidth) + } + }) + + it('renders an nbsp on empty lines when no line-ending character is defined', function () { + atom.config.set('editor.showInvisibles', false) + expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP) + }) + + it('gives the lines and tiles divs the same background color as the editor to improve GPU performance', async function () { + let linesNode = componentNode.querySelector('.lines') + let backgroundColor = getComputedStyle(wrapperNode).backgroundColor + + expect(linesNode.style.backgroundColor).toBe(backgroundColor) + for (let tileNode of component.tileNodesForLines()) { + expect(tileNode.style.backgroundColor).toBe(backgroundColor) + } + + wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)' + await atom.views.getNextUpdatePromise() + + expect(linesNode.style.backgroundColor).toBe('rgb(255, 0, 0)') + for (let tileNode of component.tileNodesForLines()) { + expect(tileNode.style.backgroundColor).toBe('rgb(255, 0, 0)') + } + }) + + it('applies .leading-whitespace for lines with leading spaces and/or tabs', async function () { + editor.setText(' a') + + await atom.views.getNextUpdatePromise() + + let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(true) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(false) + + editor.setText('\ta') + await atom.views.getNextUpdatePromise() + + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(true) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(false) + }) + + it('applies .trailing-whitespace for lines with trailing spaces and/or tabs', async function () { + editor.setText(' ') + await atom.views.getNextUpdatePromise() + + let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) + + editor.setText('\t') + await atom.views.getNextUpdatePromise() + + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) + editor.setText('a ') + await atom.views.getNextUpdatePromise() + + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) + editor.setText('a\t') + await atom.views.getNextUpdatePromise() + + leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) + }) + + it('keeps rebuilding lines when continuous reflow is on', function () { + wrapperNode.setContinuousReflow(true) + let oldLineNode = componentNode.querySelector('.line') + + waitsFor(function () { + return componentNode.querySelector('.line') !== oldLineNode + }) + }) + + describe('when showInvisibles is enabled', function () { + const invisibles = { + eol: 'E', + space: 'S', + tab: 'T', + cr: 'C' + } + + beforeEach(async function () { + atom.config.set('editor.showInvisibles', true) + atom.config.set('editor.invisibles', invisibles) + await atom.views.getNextUpdatePromise() + }) + + it('re-renders the lines when the showInvisibles config option changes', async function () { + editor.setText(' a line with tabs\tand spaces \n') + await atom.views.getNextUpdatePromise() + + expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol) + + atom.config.set('editor.showInvisibles', false) + await atom.views.getNextUpdatePromise() + + expect(component.lineNodeForScreenRow(0).textContent).toBe(' a line with tabs and spaces ') + + atom.config.set('editor.showInvisibles', true) + await atom.views.getNextUpdatePromise() + + expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol) + }) + + it('displays leading/trailing spaces, tabs, and newlines as visible characters', async function () { + editor.setText(' a line with tabs\tand spaces \n') + + await atom.views.getNextUpdatePromise() + + expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol) + + let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(leafNodes[0].classList.contains('invisible-character')).toBe(true) + expect(leafNodes[leafNodes.length - 1].classList.contains('invisible-character')).toBe(true) + }) + + it('displays newlines as their own token outside of the other tokens\' scopeDescriptor', async function () { + editor.setText('let\n') + await atom.views.getNextUpdatePromise() + expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '') + }) + + it('displays trailing carriage returns using a visible, non-empty value', async function () { + editor.setText('a line that ends with a carriage return\r\n') + await atom.views.getNextUpdatePromise() + expect(component.lineNodeForScreenRow(0).textContent).toBe('a line that ends with a carriage return' + invisibles.cr + invisibles.eol) + }) + + it('renders invisible line-ending characters on empty lines', function () { + expect(component.lineNodeForScreenRow(10).textContent).toBe(invisibles.eol) + }) + + it('renders an nbsp on empty lines when the line-ending character is an empty string', async function () { + atom.config.set('editor.invisibles', { + eol: '' + }) + await atom.views.getNextUpdatePromise() + expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP) + }) + + it('renders an nbsp on empty lines when the line-ending character is false', async function () { + atom.config.set('editor.invisibles', { + eol: false + }) + await atom.views.getNextUpdatePromise() + expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP) + }) + + it('interleaves invisible line-ending characters with indent guides on empty lines', async function () { + atom.config.set('editor.showIndentGuide', true) + + await atom.views.getNextUpdatePromise() + + editor.setTextInBufferRange([[10, 0], [11, 0]], '\r\n', { + normalizeLineEndings: false + }) + await atom.views.getNextUpdatePromise() + + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') + editor.setTabLength(3) + await atom.views.getNextUpdatePromise() + + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ') + editor.setTabLength(1) + await atom.views.getNextUpdatePromise() + + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') + editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ') + editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ') + await atom.views.getNextUpdatePromise() + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') + }) + + describe('when soft wrapping is enabled', function () { + beforeEach(async function () { + editor.setText('a line that wraps \n') + editor.setSoftWrapped(true) + await atom.views.getNextUpdatePromise() + + componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + }) + + it('does not show end of line invisibles at the end of wrapped lines', function () { + expect(component.lineNodeForScreenRow(0).textContent).toBe('a line that ') + expect(component.lineNodeForScreenRow(1).textContent).toBe('wraps' + invisibles.space + invisibles.eol) + }) + }) + }) + + describe('when indent guides are enabled', function () { + beforeEach(async function () { + atom.config.set('editor.showIndentGuide', true) + await atom.views.getNextUpdatePromise() + }) + + it('adds an "indent-guide" class to spans comprising the leading whitespace', function () { + let line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + expect(line1LeafNodes[0].textContent).toBe(' ') + expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(true) + expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false) + + let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) + expect(line2LeafNodes[0].textContent).toBe(' ') + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) + expect(line2LeafNodes[1].textContent).toBe(' ') + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) + expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(false) + }) + + it('renders leading whitespace spans with the "indent-guide" class for empty lines', async function () { + editor.getBuffer().insert([1, Infinity], '\n') + await atom.views.getNextUpdatePromise() + + let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) + expect(line2LeafNodes.length).toBe(2) + expect(line2LeafNodes[0].textContent).toBe(' ') + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) + expect(line2LeafNodes[1].textContent).toBe(' ') + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) + }) + + it('renders indent guides correctly on lines containing only whitespace', async function () { + editor.getBuffer().insert([1, Infinity], '\n ') + await atom.views.getNextUpdatePromise() + + let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) + expect(line2LeafNodes.length).toBe(3) + expect(line2LeafNodes[0].textContent).toBe(' ') + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) + expect(line2LeafNodes[1].textContent).toBe(' ') + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) + expect(line2LeafNodes[2].textContent).toBe(' ') + expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(true) + }) + + it('renders indent guides correctly on lines containing only whitespace when invisibles are enabled', async function () { + atom.config.set('editor.showInvisibles', true) + atom.config.set('editor.invisibles', { + space: '-', + eol: 'x' + }) + editor.getBuffer().insert([1, Infinity], '\n ') + + await atom.views.getNextUpdatePromise() + + let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) + expect(line2LeafNodes.length).toBe(4) + expect(line2LeafNodes[0].textContent).toBe('--') + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) + expect(line2LeafNodes[1].textContent).toBe('--') + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) + expect(line2LeafNodes[2].textContent).toBe('--') + expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(true) + expect(line2LeafNodes[3].textContent).toBe('x') + }) + + it('does not render indent guides in trailing whitespace for lines containing non whitespace characters', async function () { + editor.getBuffer().setText(' hi ') + + await atom.views.getNextUpdatePromise() + + let line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) + expect(line0LeafNodes[0].textContent).toBe(' ') + expect(line0LeafNodes[0].classList.contains('indent-guide')).toBe(true) + expect(line0LeafNodes[1].textContent).toBe(' ') + expect(line0LeafNodes[1].classList.contains('indent-guide')).toBe(false) + }) + + it('updates the indent guides on empty lines preceding an indentation change', async function () { + editor.getBuffer().insert([12, 0], '\n') + await atom.views.getNextUpdatePromise() + + editor.getBuffer().insert([13, 0], ' ') + await atom.views.getNextUpdatePromise() + + let line12LeafNodes = getLeafNodes(component.lineNodeForScreenRow(12)) + expect(line12LeafNodes[0].textContent).toBe(' ') + expect(line12LeafNodes[0].classList.contains('indent-guide')).toBe(true) + expect(line12LeafNodes[1].textContent).toBe(' ') + expect(line12LeafNodes[1].classList.contains('indent-guide')).toBe(true) + }) + + it('updates the indent guides on empty lines following an indentation change', async function () { + editor.getBuffer().insert([12, 2], '\n') + + await atom.views.getNextUpdatePromise() + + editor.getBuffer().insert([12, 0], ' ') + await atom.views.getNextUpdatePromise() + + let line13LeafNodes = getLeafNodes(component.lineNodeForScreenRow(13)) + expect(line13LeafNodes[0].textContent).toBe(' ') + expect(line13LeafNodes[0].classList.contains('indent-guide')).toBe(true) + expect(line13LeafNodes[1].textContent).toBe(' ') + expect(line13LeafNodes[1].classList.contains('indent-guide')).toBe(true) + }) + }) + + describe('when indent guides are disabled', function () { + beforeEach(function () { + expect(atom.config.get('editor.showIndentGuide')).toBe(false) + }) + + it('does not render indent guides on lines containing only whitespace', async function () { + editor.getBuffer().insert([1, Infinity], '\n ') + + await atom.views.getNextUpdatePromise() + + let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) + expect(line2LeafNodes.length).toBe(3) + expect(line2LeafNodes[0].textContent).toBe(' ') + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(false) + expect(line2LeafNodes[1].textContent).toBe(' ') + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(false) + expect(line2LeafNodes[2].textContent).toBe(' ') + expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(false) + }) + }) + + describe('when the buffer contains null bytes', function () { + it('excludes the null byte from character measurement', async function () { + editor.setText('a\0b') + await atom.views.getNextUpdatePromise() + expect(wrapperNode.pixelPositionForScreenPosition([0, Infinity]).left).toEqual(2 * charWidth) + }) + }) + + describe('when there is a fold', function () { + it('renders a fold marker on the folded line', async function () { + let foldedLineNode = component.lineNodeForScreenRow(4) + expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() + editor.foldBufferRow(4) + + await atom.views.getNextUpdatePromise() + + foldedLineNode = component.lineNodeForScreenRow(4) + expect(foldedLineNode.querySelector('.fold-marker')).toBeTruthy() + editor.unfoldBufferRow(4) + + await atom.views.getNextUpdatePromise() + + foldedLineNode = component.lineNodeForScreenRow(4) + expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() + }) + }) + }) + + describe('gutter rendering', function () { + function expectTileContainsRow (tileNode, screenRow, {top, text}) { + let lineNode = tileNode.querySelector('[data-screen-row="' + screenRow + '"]') + expect(lineNode.offsetTop).toBe(top) + expect(lineNode.textContent).toBe(text) + } + + it('renders higher tiles in front of lower ones', async function () { + wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + let tilesNodes = component.tileNodesForLineNumbers() + expect(tilesNodes[0].style.zIndex).toBe('2') + expect(tilesNodes[1].style.zIndex).toBe('1') + expect(tilesNodes[2].style.zIndex).toBe('0') + verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + await atom.views.getNextUpdatePromise() + + tilesNodes = component.tileNodesForLineNumbers() + expect(tilesNodes[0].style.zIndex).toBe('3') + expect(tilesNodes[1].style.zIndex).toBe('2') + expect(tilesNodes[2].style.zIndex).toBe('1') + expect(tilesNodes[3].style.zIndex).toBe('0') + }) + + it('gives the line numbers container the same height as the wrapper node', async function () { + let linesNode = componentNode.querySelector('.line-numbers') + wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' + component.measureDimensions() + + await atom.views.getNextUpdatePromise() + + expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) + wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' + component.measureDimensions() + + await atom.views.getNextUpdatePromise() + + expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) + }) + + it('renders the currently-visible line numbers in a tiled fashion', async function () { + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + let tilesNodes = component.tileNodesForLineNumbers() + expect(tilesNodes.length).toBe(3) + + expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') + expect(tilesNodes[0].querySelectorAll('.line-number').length).toBe(3) + expectTileContainsRow(tilesNodes[0], 0, { + top: lineHeightInPixels * 0, + text: '' + NBSP + '1' + }) + expectTileContainsRow(tilesNodes[0], 1, { + top: lineHeightInPixels * 1, + text: '' + NBSP + '2' + }) + expectTileContainsRow(tilesNodes[0], 2, { + top: lineHeightInPixels * 2, + text: '' + NBSP + '3' + }) + + expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels) + 'px, 0px)') + expect(tilesNodes[1].querySelectorAll('.line-number').length).toBe(3) + expectTileContainsRow(tilesNodes[1], 3, { + top: lineHeightInPixels * 0, + text: '' + NBSP + '4' + }) + expectTileContainsRow(tilesNodes[1], 4, { + top: lineHeightInPixels * 1, + text: '' + NBSP + '5' + }) + expectTileContainsRow(tilesNodes[1], 5, { + top: lineHeightInPixels * 2, + text: '' + NBSP + '6' + }) + + expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels) + 'px, 0px)') + expect(tilesNodes[2].querySelectorAll('.line-number').length).toBe(3) + expectTileContainsRow(tilesNodes[2], 6, { + top: lineHeightInPixels * 0, + text: '' + NBSP + '7' + }) + expectTileContainsRow(tilesNodes[2], 7, { + top: lineHeightInPixels * 1, + text: '' + NBSP + '8' + }) + expectTileContainsRow(tilesNodes[2], 8, { + top: lineHeightInPixels * 2, + text: '' + NBSP + '9' + }) + verticalScrollbarNode.scrollTop = TILE_SIZE * lineHeightInPixels + 5 + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + + await atom.views.getNextUpdatePromise() + + tilesNodes = component.tileNodesForLineNumbers() + expect(component.lineNumberNodeForScreenRow(2)).toBeUndefined() + expect(tilesNodes.length).toBe(3) + + expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, ' + (0 * tileHeightInPixels - 5) + 'px, 0px)') + expect(tilesNodes[0].querySelectorAll('.line-number').length).toBe(TILE_SIZE) + expectTileContainsRow(tilesNodes[0], 3, { + top: lineHeightInPixels * 0, + text: '' + NBSP + '4' + }) + expectTileContainsRow(tilesNodes[0], 4, { + top: lineHeightInPixels * 1, + text: '' + NBSP + '5' + }) + expectTileContainsRow(tilesNodes[0], 5, { + top: lineHeightInPixels * 2, + text: '' + NBSP + '6' + }) + + expect(tilesNodes[1].style['-webkit-transform']).toBe('translate3d(0px, ' + (1 * tileHeightInPixels - 5) + 'px, 0px)') + expect(tilesNodes[1].querySelectorAll('.line-number').length).toBe(TILE_SIZE) + expectTileContainsRow(tilesNodes[1], 6, { + top: 0 * lineHeightInPixels, + text: '' + NBSP + '7' + }) + expectTileContainsRow(tilesNodes[1], 7, { + top: 1 * lineHeightInPixels, + text: '' + NBSP + '8' + }) + expectTileContainsRow(tilesNodes[1], 8, { + top: 2 * lineHeightInPixels, + text: '' + NBSP + '9' + }) + + expect(tilesNodes[2].style['-webkit-transform']).toBe('translate3d(0px, ' + (2 * tileHeightInPixels - 5) + 'px, 0px)') + expect(tilesNodes[2].querySelectorAll('.line-number').length).toBe(TILE_SIZE) + expectTileContainsRow(tilesNodes[2], 9, { + top: 0 * lineHeightInPixels, + text: '10' + }) + expectTileContainsRow(tilesNodes[2], 10, { + top: 1 * lineHeightInPixels, + text: '11' + }) + expectTileContainsRow(tilesNodes[2], 11, { + top: 2 * lineHeightInPixels, + text: '12' + }) + }) + + it('updates the translation of subsequent line numbers when lines are inserted or removed', async function () { + editor.getBuffer().insert([0, 0], '\n\n') + await atom.views.getNextUpdatePromise() + + let lineNumberNodes = componentNode.querySelectorAll('.line-number') + expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe(0 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe(1 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe(2 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe(0 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe(1 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe(2 * lineHeightInPixels) + editor.getBuffer().insert([0, 0], '\n\n') + + await atom.views.getNextUpdatePromise() + + expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe(0 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe(1 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe(2 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(3).offsetTop).toBe(0 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(4).offsetTop).toBe(1 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe(2 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(6).offsetTop).toBe(0 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(7).offsetTop).toBe(1 * lineHeightInPixels) + expect(component.lineNumberNodeForScreenRow(8).offsetTop).toBe(2 * lineHeightInPixels) + }) + + it('renders • characters for soft-wrapped lines', async function () { + editor.setSoftWrapped(true) + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 30 * charWidth + 'px' + component.measureDimensions() + + await atom.views.getNextUpdatePromise() + + expect(componentNode.querySelectorAll('.line-number').length).toBe(9 + 1) + expect(component.lineNumberNodeForScreenRow(0).textContent).toBe('' + NBSP + '1') + expect(component.lineNumberNodeForScreenRow(1).textContent).toBe('' + NBSP + '•') + expect(component.lineNumberNodeForScreenRow(2).textContent).toBe('' + NBSP + '2') + expect(component.lineNumberNodeForScreenRow(3).textContent).toBe('' + NBSP + '•') + expect(component.lineNumberNodeForScreenRow(4).textContent).toBe('' + NBSP + '3') + expect(component.lineNumberNodeForScreenRow(5).textContent).toBe('' + NBSP + '•') + expect(component.lineNumberNodeForScreenRow(6).textContent).toBe('' + NBSP + '4') + expect(component.lineNumberNodeForScreenRow(7).textContent).toBe('' + NBSP + '•') + expect(component.lineNumberNodeForScreenRow(8).textContent).toBe('' + NBSP + '•') + }) + + it('pads line numbers to be right-justified based on the maximum number of line number digits', async function () { + editor.getBuffer().setText([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].join('\n')) + await atom.views.getNextUpdatePromise() + + for (let screenRow = 0; screenRow <= 8; ++screenRow) { + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + (screenRow + 1)) + } + expect(component.lineNumberNodeForScreenRow(9).textContent).toBe('10') + let gutterNode = componentNode.querySelector('.gutter') + let initialGutterWidth = gutterNode.offsetWidth + editor.getBuffer().delete([[1, 0], [2, 0]]) + + await atom.views.getNextUpdatePromise() + + for (let screenRow = 0; screenRow <= 8; ++screenRow) { + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + (screenRow + 1)) + } + expect(gutterNode.offsetWidth).toBeLessThan(initialGutterWidth) + editor.getBuffer().insert([0, 0], '\n\n') + + await atom.views.getNextUpdatePromise() + + for (let screenRow = 0; screenRow <= 8; ++screenRow) { + expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + (screenRow + 1)) + } + expect(component.lineNumberNodeForScreenRow(9).textContent).toBe('10') + expect(gutterNode.offsetWidth).toBe(initialGutterWidth) + }) + + it('renders the .line-numbers div at the full height of the editor even if it\'s taller than its content', async function () { + wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + expect(componentNode.querySelector('.line-numbers').offsetHeight).toBe(componentNode.offsetHeight) + }) + + it('applies the background color of the gutter or the editor to the line numbers to improve GPU performance', async function () { + let gutterNode = componentNode.querySelector('.gutter') + let lineNumbersNode = gutterNode.querySelector('.line-numbers') + let backgroundColor = getComputedStyle(wrapperNode).backgroundColor + expect(lineNumbersNode.style.backgroundColor).toBe(backgroundColor) + for (let tileNode of component.tileNodesForLineNumbers()) { + expect(tileNode.style.backgroundColor).toBe(backgroundColor) + } + + gutterNode.style.backgroundColor = 'rgb(255, 0, 0)' + atom.views.performDocumentPoll() + await atom.views.getNextUpdatePromise() + + expect(lineNumbersNode.style.backgroundColor).toBe('rgb(255, 0, 0)') + for (let tileNode of component.tileNodesForLineNumbers()) { + expect(tileNode.style.backgroundColor).toBe('rgb(255, 0, 0)') + } + }) + + it('hides or shows the gutter based on the "::isLineNumberGutterVisible" property on the model and the global "editor.showLineNumbers" config setting', async function () { + expect(component.gutterContainerComponent.getLineNumberGutterComponent() != null).toBe(true) + editor.setLineNumberGutterVisible(false) + await atom.views.getNextUpdatePromise() + + expect(componentNode.querySelector('.gutter').style.display).toBe('none') + atom.config.set('editor.showLineNumbers', false) + await atom.views.getNextUpdatePromise() + + expect(componentNode.querySelector('.gutter').style.display).toBe('none') + editor.setLineNumberGutterVisible(true) + await atom.views.getNextUpdatePromise() + + expect(componentNode.querySelector('.gutter').style.display).toBe('none') + atom.config.set('editor.showLineNumbers', true) + await atom.views.getNextUpdatePromise() + + expect(componentNode.querySelector('.gutter').style.display).toBe('') + expect(component.lineNumberNodeForScreenRow(3) != null).toBe(true) + }) + + it('keeps rebuilding line numbers when continuous reflow is on', function () { + wrapperNode.setContinuousReflow(true) + let oldLineNode = componentNode.querySelectorAll('.line-number')[1] + + waitsFor(function () { + return componentNode.querySelectorAll('.line-number')[1] !== oldLineNode + }) + }) + + describe('fold decorations', function () { + describe('rendering fold decorations', function () { + it('adds the foldable class to line numbers when the line is foldable', function () { + expect(lineNumberHasClass(0, 'foldable')).toBe(true) + expect(lineNumberHasClass(1, 'foldable')).toBe(true) + expect(lineNumberHasClass(2, 'foldable')).toBe(false) + expect(lineNumberHasClass(3, 'foldable')).toBe(false) + expect(lineNumberHasClass(4, 'foldable')).toBe(true) + expect(lineNumberHasClass(5, 'foldable')).toBe(false) + }) + + it('updates the foldable class on the correct line numbers when the foldable positions change', async function () { + editor.getBuffer().insert([0, 0], '\n') + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(0, 'foldable')).toBe(false) + expect(lineNumberHasClass(1, 'foldable')).toBe(true) + expect(lineNumberHasClass(2, 'foldable')).toBe(true) + expect(lineNumberHasClass(3, 'foldable')).toBe(false) + expect(lineNumberHasClass(4, 'foldable')).toBe(false) + expect(lineNumberHasClass(5, 'foldable')).toBe(true) + expect(lineNumberHasClass(6, 'foldable')).toBe(false) + }) + + it('updates the foldable class on a line number that becomes foldable', async function () { + expect(lineNumberHasClass(11, 'foldable')).toBe(false) + editor.getBuffer().insert([11, 44], '\n fold me') + await atom.views.getNextUpdatePromise() + expect(lineNumberHasClass(11, 'foldable')).toBe(true) + editor.undo() + await atom.views.getNextUpdatePromise() + expect(lineNumberHasClass(11, 'foldable')).toBe(false) + }) + + it('adds, updates and removes the folded class on the correct line number componentNodes', async function () { + editor.foldBufferRow(4) + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(4, 'folded')).toBe(true) + + editor.getBuffer().insert([0, 0], '\n') + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(4, 'folded')).toBe(false) + expect(lineNumberHasClass(5, 'folded')).toBe(true) + + editor.unfoldBufferRow(5) + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(5, 'folded')).toBe(false) + }) + + describe('when soft wrapping is enabled', function () { + beforeEach(async function () { + editor.setSoftWrapped(true) + await atom.views.getNextUpdatePromise() + componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + }) + + it('does not add the foldable class for soft-wrapped lines', function () { + expect(lineNumberHasClass(0, 'foldable')).toBe(true) + expect(lineNumberHasClass(1, 'foldable')).toBe(false) + }) + }) + }) + + describe('mouse interactions with fold indicators', function () { + let gutterNode + + function buildClickEvent (target) { + return buildMouseEvent('click', { + target: target + }) + } + + beforeEach(function () { + gutterNode = componentNode.querySelector('.gutter') + }) + + describe('when the component is destroyed', function () { + it('stops listening for folding events', function () { + let lineNumber, target + component.destroy() + lineNumber = component.lineNumberNodeForScreenRow(1) + target = lineNumber.querySelector('.icon-right') + return target.dispatchEvent(buildClickEvent(target)) + }) + }) + + it('folds and unfolds the block represented by the fold indicator when clicked', async function () { + expect(lineNumberHasClass(1, 'folded')).toBe(false) + + let lineNumber = component.lineNumberNodeForScreenRow(1) + let target = lineNumber.querySelector('.icon-right') + + target.dispatchEvent(buildClickEvent(target)) + + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(1, 'folded')).toBe(true) + lineNumber = component.lineNumberNodeForScreenRow(1) + target = lineNumber.querySelector('.icon-right') + target.dispatchEvent(buildClickEvent(target)) + + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(1, 'folded')).toBe(false) + }) + + it('does not fold when the line number componentNode is clicked', function () { + let lineNumber = component.lineNumberNodeForScreenRow(1) + lineNumber.dispatchEvent(buildClickEvent(lineNumber)) + waits(100) + runs(function () { + expect(lineNumberHasClass(1, 'folded')).toBe(false) + }) + }) + }) + }) + }) + + describe('cursor rendering', function () { + it('renders the currently visible cursors', async function () { + let cursor1 = editor.getLastCursor() + cursor1.setScreenPosition([0, 5], { + autoscroll: false + }) + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * lineHeightInPixels + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + let cursorNodes = componentNode.querySelectorAll('.cursor') + expect(cursorNodes.length).toBe(1) + expect(cursorNodes[0].offsetHeight).toBe(lineHeightInPixels) + expect(cursorNodes[0].offsetWidth).toBeCloseTo(charWidth, 0) + expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(5 * charWidth)) + 'px, ' + (0 * lineHeightInPixels) + 'px)') + let cursor2 = editor.addCursorAtScreenPosition([8, 11], { + autoscroll: false + }) + let cursor3 = editor.addCursorAtScreenPosition([4, 10], { + autoscroll: false + }) + await atom.views.getNextUpdatePromise() + + cursorNodes = componentNode.querySelectorAll('.cursor') + expect(cursorNodes.length).toBe(2) + expect(cursorNodes[0].offsetTop).toBe(0) + expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(5 * charWidth)) + 'px, ' + (0 * lineHeightInPixels) + 'px)') + expect(cursorNodes[1].style['-webkit-transform']).toBe('translate(' + (Math.round(10 * charWidth)) + 'px, ' + (4 * lineHeightInPixels) + 'px)') + verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels + horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + await atom.views.getNextUpdatePromise() + + horizontalScrollbarNode.scrollLeft = 3.5 * charWidth + horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + await atom.views.getNextUpdatePromise() + + cursorNodes = componentNode.querySelectorAll('.cursor') + expect(cursorNodes.length).toBe(2) + expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(10 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (4 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') + expect(cursorNodes[1].style['-webkit-transform']).toBe('translate(' + (Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (8 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') + editor.onDidChangeCursorPosition(cursorMovedListener = jasmine.createSpy('cursorMovedListener')) + cursor3.setScreenPosition([4, 11], { + autoscroll: false + }) + await atom.views.getNextUpdatePromise() + + expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (4 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') + expect(cursorMovedListener).toHaveBeenCalled() + cursor3.destroy() + await atom.views.getNextUpdatePromise() + + cursorNodes = componentNode.querySelectorAll('.cursor') + expect(cursorNodes.length).toBe(1) + expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (8 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') + }) + + it('accounts for character widths when positioning cursors', async function () { + atom.config.set('editor.fontFamily', 'sans-serif') + editor.setCursorScreenPosition([0, 16]) + await atom.views.getNextUpdatePromise() + + let cursor = componentNode.querySelector('.cursor') + let cursorRect = cursor.getBoundingClientRect() + let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild + let range = document.createRange() + range.setStart(cursorLocationTextNode, 0) + range.setEnd(cursorLocationTextNode, 1) + let rangeRect = range.getBoundingClientRect() + expect(cursorRect.left).toBeCloseTo(rangeRect.left, 0) + expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) + }) + + it('accounts for the width of paired characters when positioning cursors', async function () { + atom.config.set('editor.fontFamily', 'sans-serif') + editor.setText('he\u0301y') + editor.setCursorBufferPosition([0, 3]) + await atom.views.getNextUpdatePromise() + + let cursor = componentNode.querySelector('.cursor') + let cursorRect = cursor.getBoundingClientRect() + let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2] + let range = document.createRange() + range.setStart(cursorLocationTextNode, 0) + range.setEnd(cursorLocationTextNode, 1) + let rangeRect = range.getBoundingClientRect() + expect(cursorRect.left).toBeCloseTo(rangeRect.left, 0) + expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) + }) + + it('positions cursors correctly after character widths are changed via a stylesheet change', async function () { + atom.config.set('editor.fontFamily', 'sans-serif') + editor.setCursorScreenPosition([0, 16]) + await atom.views.getNextUpdatePromise() + + atom.styles.addStyleSheet('.function.js {\n font-weight: bold;\n}', { + context: 'atom-text-editor' + }) + await atom.views.getNextUpdatePromise() + + let cursor = componentNode.querySelector('.cursor') + let cursorRect = cursor.getBoundingClientRect() + let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.storage.type.function.js').firstChild + let range = document.createRange() + range.setStart(cursorLocationTextNode, 0) + range.setEnd(cursorLocationTextNode, 1) + let rangeRect = range.getBoundingClientRect() + expect(cursorRect.left).toBeCloseTo(rangeRect.left, 0) + expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0) + atom.themes.removeStylesheet('test') + }) + + it('sets the cursor to the default character width at the end of a line', async function () { + editor.setCursorScreenPosition([0, Infinity]) + await atom.views.getNextUpdatePromise() + let cursorNode = componentNode.querySelector('.cursor') + expect(cursorNode.offsetWidth).toBeCloseTo(charWidth, 0) + }) + + it('gives the cursor a non-zero width even if it\'s inside atomic tokens', async function () { + editor.setCursorScreenPosition([1, 0]) + await atom.views.getNextUpdatePromise() + let cursorNode = componentNode.querySelector('.cursor') + expect(cursorNode.offsetWidth).toBeCloseTo(charWidth, 0) + }) + + it('blinks cursors when they are not moving', async function () { + let cursorsNode = componentNode.querySelector('.cursors') + wrapperNode.focus() + await atom.views.getNextUpdatePromise() + expect(cursorsNode.classList.contains('blink-off')).toBe(false) + await conditionPromise(function () { + return cursorsNode.classList.contains('blink-off') + }) + await conditionPromise(function () { + return !cursorsNode.classList.contains('blink-off') + }) + editor.moveRight() + await atom.views.getNextUpdatePromise() + expect(cursorsNode.classList.contains('blink-off')).toBe(false) + await conditionPromise(function () { + return cursorsNode.classList.contains('blink-off') + }) + }) + + it('does not render cursors that are associated with non-empty selections', async function () { + editor.setSelectedScreenRange([[0, 4], [4, 6]]) + editor.addCursorAtScreenPosition([6, 8]) + await atom.views.getNextUpdatePromise() + let cursorNodes = componentNode.querySelectorAll('.cursor') + expect(cursorNodes.length).toBe(1) + expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(8 * charWidth)) + 'px, ' + (6 * lineHeightInPixels) + 'px)') + }) + + it('updates cursor positions when the line height changes', async function () { + editor.setCursorBufferPosition([1, 10]) + component.setLineHeight(2) + await atom.views.getNextUpdatePromise() + let cursorNode = componentNode.querySelector('.cursor') + expect(cursorNode.style['-webkit-transform']).toBe('translate(' + (Math.round(10 * editor.getDefaultCharWidth())) + 'px, ' + (editor.getLineHeightInPixels()) + 'px)') + }) + + it('updates cursor positions when the font size changes', async function () { + editor.setCursorBufferPosition([1, 10]) + component.setFontSize(10) + await atom.views.getNextUpdatePromise() + let cursorNode = componentNode.querySelector('.cursor') + expect(cursorNode.style['-webkit-transform']).toBe('translate(' + (Math.round(10 * editor.getDefaultCharWidth())) + 'px, ' + (editor.getLineHeightInPixels()) + 'px)') + }) + + it('updates cursor positions when the font family changes', async function () { + editor.setCursorBufferPosition([1, 10]) + component.setFontFamily('sans-serif') + await atom.views.getNextUpdatePromise() + let cursorNode = componentNode.querySelector('.cursor') + let left = wrapperNode.pixelPositionForScreenPosition([1, 10]).left + expect(cursorNode.style['-webkit-transform']).toBe('translate(' + (Math.round(left)) + 'px, ' + (editor.getLineHeightInPixels()) + 'px)') + }) + }) + + describe('selection rendering', function () { + let scrollViewClientLeft, scrollViewNode + + beforeEach(function () { + scrollViewNode = componentNode.querySelector('.scroll-view') + scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left + }) + + it('renders 1 region for 1-line selections', async function () { + editor.setSelectedScreenRange([[1, 6], [1, 10]]) + await atom.views.getNextUpdatePromise() + + let regions = componentNode.querySelectorAll('.selection .region') + expect(regions.length).toBe(1) + + let regionRect = regions[0].getBoundingClientRect() + expect(regionRect.top).toBe(1 * lineHeightInPixels) + expect(regionRect.height).toBe(1 * lineHeightInPixels) + expect(regionRect.left).toBeCloseTo(scrollViewClientLeft + 6 * charWidth, 0) + expect(regionRect.width).toBeCloseTo(4 * charWidth, 0) + }) + + it('renders 2 regions for 2-line selections', async function () { + editor.setSelectedScreenRange([[1, 6], [2, 10]]) + await atom.views.getNextUpdatePromise() + + let tileNode = component.tileNodesForLines()[0] + let regions = tileNode.querySelectorAll('.selection .region') + expect(regions.length).toBe(2) + + let region1Rect = regions[0].getBoundingClientRect() + expect(region1Rect.top).toBe(1 * lineHeightInPixels) + expect(region1Rect.height).toBe(1 * lineHeightInPixels) + expect(region1Rect.left).toBeCloseTo(scrollViewClientLeft + 6 * charWidth, 0) + expect(region1Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) + + let region2Rect = regions[1].getBoundingClientRect() + expect(region2Rect.top).toBe(2 * lineHeightInPixels) + expect(region2Rect.height).toBe(1 * lineHeightInPixels) + expect(region2Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) + expect(region2Rect.width).toBeCloseTo(10 * charWidth, 0) + }) + + it('renders 3 regions per tile for selections with more than 2 lines', async function () { + editor.setSelectedScreenRange([[0, 6], [5, 10]]) + await atom.views.getNextUpdatePromise() + + let region1Rect, region2Rect, region3Rect, regions, tileNode + tileNode = component.tileNodesForLines()[0] + regions = tileNode.querySelectorAll('.selection .region') + expect(regions.length).toBe(3) + + region1Rect = regions[0].getBoundingClientRect() + expect(region1Rect.top).toBe(0) + expect(region1Rect.height).toBe(1 * lineHeightInPixels) + expect(region1Rect.left).toBeCloseTo(scrollViewClientLeft + 6 * charWidth, 0) + expect(region1Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) + + region2Rect = regions[1].getBoundingClientRect() + expect(region2Rect.top).toBe(1 * lineHeightInPixels) + expect(region2Rect.height).toBe(1 * lineHeightInPixels) + expect(region2Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) + expect(region2Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) + + region3Rect = regions[2].getBoundingClientRect() + expect(region3Rect.top).toBe(2 * lineHeightInPixels) + expect(region3Rect.height).toBe(1 * lineHeightInPixels) + expect(region3Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) + expect(region3Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) + + tileNode = component.tileNodesForLines()[1] + regions = tileNode.querySelectorAll('.selection .region') + expect(regions.length).toBe(3) + + region1Rect = regions[0].getBoundingClientRect() + expect(region1Rect.top).toBe(3 * lineHeightInPixels) + expect(region1Rect.height).toBe(1 * lineHeightInPixels) + expect(region1Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) + expect(region1Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) + + region2Rect = regions[1].getBoundingClientRect() + expect(region2Rect.top).toBe(4 * lineHeightInPixels) + expect(region2Rect.height).toBe(1 * lineHeightInPixels) + expect(region2Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) + expect(region2Rect.right).toBeCloseTo(tileNode.getBoundingClientRect().right, 0) + + region3Rect = regions[2].getBoundingClientRect() + expect(region3Rect.top).toBe(5 * lineHeightInPixels) + expect(region3Rect.height).toBe(1 * lineHeightInPixels) + expect(region3Rect.left).toBeCloseTo(scrollViewClientLeft + 0, 0) + expect(region3Rect.width).toBeCloseTo(10 * charWidth, 0) + }) + + it('does not render empty selections', async function () { + editor.addSelectionForBufferRange([[2, 2], [2, 2]]) + await atom.views.getNextUpdatePromise() + expect(editor.getSelections()[0].isEmpty()).toBe(true) + expect(editor.getSelections()[1].isEmpty()).toBe(true) + expect(componentNode.querySelectorAll('.selection').length).toBe(0) + }) + + it('updates selections when the line height changes', async function () { + editor.setSelectedBufferRange([[1, 6], [1, 10]]) + component.setLineHeight(2) + await atom.views.getNextUpdatePromise() + let selectionNode = componentNode.querySelector('.region') + expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels()) + }) + + it('updates selections when the font size changes', async function () { + editor.setSelectedBufferRange([[1, 6], [1, 10]]) + component.setFontSize(10) + + await atom.views.getNextUpdatePromise() + + let selectionNode = componentNode.querySelector('.region') + expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels()) + expect(selectionNode.offsetLeft).toBeCloseTo(6 * editor.getDefaultCharWidth(), 0) + }) + + it('updates selections when the font family changes', async function () { + editor.setSelectedBufferRange([[1, 6], [1, 10]]) + component.setFontFamily('sans-serif') + + await atom.views.getNextUpdatePromise() + + let selectionNode = componentNode.querySelector('.region') + expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels()) + expect(selectionNode.offsetLeft).toBeCloseTo(wrapperNode.pixelPositionForScreenPosition([1, 6]).left, 0) + }) + + it('will flash the selection when flash:true is passed to editor::setSelectedBufferRange', async function () { + editor.setSelectedBufferRange([[1, 6], [1, 10]], { + flash: true + }) + await atom.views.getNextUpdatePromise() + + let selectionNode = componentNode.querySelector('.selection') + expect(selectionNode.classList.contains('flash')).toBe(true) + + await conditionPromise(function () { + return !selectionNode.classList.contains('flash') + }) + + editor.setSelectedBufferRange([[1, 5], [1, 7]], { + flash: true + }) + await atom.views.getNextUpdatePromise() + + expect(selectionNode.classList.contains('flash')).toBe(true) + }) + }) + + describe('line decoration rendering', function () { + let decoration, marker + + beforeEach(async function () { + marker = editor.addMarkerLayer({ + maintainHistory: true + }).markBufferRange([[2, 13], [3, 15]], { + invalidate: 'inside' + }) + decoration = editor.decorateMarker(marker, { + type: ['line-number', 'line'], + 'class': 'a' + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + }) + + it('applies line decoration classes to lines and line numbers', async function () { + expect(lineAndLineNumberHaveClass(2, 'a')).toBe(true) + expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + let marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]]) + editor.decorateMarker(marker2, { + type: ['line-number', 'line'], + 'class': 'b' + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + await atom.views.getNextUpdatePromise() + + expect(lineAndLineNumberHaveClass(9, 'b')).toBe(true) + + editor.foldBufferRow(5) + await atom.views.getNextUpdatePromise() + + expect(lineAndLineNumberHaveClass(9, 'b')).toBe(false) + expect(lineAndLineNumberHaveClass(6, 'b')).toBe(true) + }) + + it('only applies decorations to screen rows that are spanned by their marker when lines are soft-wrapped', async function () { + editor.setText('a line that wraps, ok') + editor.setSoftWrapped(true) + componentNode.style.width = 16 * charWidth + 'px' + component.measureDimensions() + + await atom.views.getNextUpdatePromise() + marker.destroy() + marker = editor.markBufferRange([[0, 0], [0, 2]]) + editor.decorateMarker(marker, { + type: ['line-number', 'line'], + 'class': 'b' + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(0, 'b')).toBe(true) + expect(lineNumberHasClass(1, 'b')).toBe(false) + marker.setBufferRange([[0, 0], [0, Infinity]]) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(0, 'b')).toBe(true) + expect(lineNumberHasClass(1, 'b')).toBe(true) + }) + + it('updates decorations when markers move', async function () { + expect(lineAndLineNumberHaveClass(1, 'a')).toBe(false) + expect(lineAndLineNumberHaveClass(2, 'a')).toBe(true) + expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) + expect(lineAndLineNumberHaveClass(4, 'a')).toBe(false) + + editor.getBuffer().insert([0, 0], '\n') + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(lineAndLineNumberHaveClass(2, 'a')).toBe(false) + expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) + expect(lineAndLineNumberHaveClass(4, 'a')).toBe(true) + expect(lineAndLineNumberHaveClass(5, 'a')).toBe(false) + + marker.setBufferRange([[4, 4], [6, 4]]) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(lineAndLineNumberHaveClass(2, 'a')).toBe(false) + expect(lineAndLineNumberHaveClass(3, 'a')).toBe(false) + expect(lineAndLineNumberHaveClass(4, 'a')).toBe(true) + expect(lineAndLineNumberHaveClass(5, 'a')).toBe(true) + expect(lineAndLineNumberHaveClass(6, 'a')).toBe(true) + expect(lineAndLineNumberHaveClass(7, 'a')).toBe(false) + }) + + it('remove decoration classes when decorations are removed', async function () { + decoration.destroy() + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + expect(lineNumberHasClass(1, 'a')).toBe(false) + expect(lineNumberHasClass(2, 'a')).toBe(false) + expect(lineNumberHasClass(3, 'a')).toBe(false) + expect(lineNumberHasClass(4, 'a')).toBe(false) + }) + + it('removes decorations when their marker is invalidated', async function () { + editor.getBuffer().insert([3, 2], 'n') + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(marker.isValid()).toBe(false) + expect(lineAndLineNumberHaveClass(1, 'a')).toBe(false) + expect(lineAndLineNumberHaveClass(2, 'a')).toBe(false) + expect(lineAndLineNumberHaveClass(3, 'a')).toBe(false) + expect(lineAndLineNumberHaveClass(4, 'a')).toBe(false) + editor.undo() + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(marker.isValid()).toBe(true) + expect(lineAndLineNumberHaveClass(1, 'a')).toBe(false) + expect(lineAndLineNumberHaveClass(2, 'a')).toBe(true) + expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) + expect(lineAndLineNumberHaveClass(4, 'a')).toBe(false) + }) + + it('removes decorations when their marker is destroyed', async function () { + marker.destroy() + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + expect(lineNumberHasClass(1, 'a')).toBe(false) + expect(lineNumberHasClass(2, 'a')).toBe(false) + expect(lineNumberHasClass(3, 'a')).toBe(false) + expect(lineNumberHasClass(4, 'a')).toBe(false) + }) + + describe('when the decoration\'s "onlyHead" property is true', function () { + it('only applies the decoration\'s class to lines containing the marker\'s head', async function () { + editor.decorateMarker(marker, { + type: ['line-number', 'line'], + 'class': 'only-head', + onlyHead: true + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + expect(lineAndLineNumberHaveClass(1, 'only-head')).toBe(false) + expect(lineAndLineNumberHaveClass(2, 'only-head')).toBe(false) + expect(lineAndLineNumberHaveClass(3, 'only-head')).toBe(true) + expect(lineAndLineNumberHaveClass(4, 'only-head')).toBe(false) + }) + }) + + describe('when the decoration\'s "onlyEmpty" property is true', function () { + it('only applies the decoration when its marker is empty', async function () { + editor.decorateMarker(marker, { + type: ['line-number', 'line'], + 'class': 'only-empty', + onlyEmpty: true + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe(false) + expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe(false) + + marker.clearTail() + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe(false) + expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe(true) + }) + }) + + describe('when the decoration\'s "onlyNonEmpty" property is true', function () { + it('only applies the decoration when its marker is non-empty', async function () { + editor.decorateMarker(marker, { + type: ['line-number', 'line'], + 'class': 'only-non-empty', + onlyNonEmpty: true + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe(true) + expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe(true) + + marker.clearTail() + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe(false) + expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe(false) + }) + }) + }) + + describe('highlight decoration rendering', function () { + let decoration, marker, scrollViewClientLeft + + beforeEach(async function () { + scrollViewClientLeft = componentNode.querySelector('.scroll-view').getBoundingClientRect().left + marker = editor.addMarkerLayer({ + maintainHistory: true + }).markBufferRange([[2, 13], [3, 15]], { + invalidate: 'inside' + }) + decoration = editor.decorateMarker(marker, { + type: 'highlight', + 'class': 'test-highlight' + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + }) + + it('does not render highlights for off-screen lines until they come on-screen', async function () { + wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], { + invalidate: 'inside' + }) + editor.decorateMarker(marker, { + type: 'highlight', + 'class': 'some-highlight' + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(component.presenter.endRow).toBeLessThan(9) + let regions = componentNode.querySelectorAll('.some-highlight .region') + expect(regions.length).toBe(0) + verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + await atom.views.getNextUpdatePromise() + + expect(component.presenter.endRow).toBeGreaterThan(8) + regions = componentNode.querySelectorAll('.some-highlight .region') + expect(regions.length).toBe(1) + let regionRect = regions[0].style + expect(regionRect.top).toBe(0 + 'px') + expect(regionRect.height).toBe(1 * lineHeightInPixels + 'px') + expect(regionRect.left).toBe(Math.round(2 * charWidth) + 'px') + expect(regionRect.width).toBe(Math.round(2 * charWidth) + 'px') + }) + + it('renders highlights decoration\'s marker is added', async function () { + let regions = componentNode.querySelectorAll('.test-highlight .region') + expect(regions.length).toBe(2) + }) + + it('removes highlights when a decoration is removed', async function () { + decoration.destroy() + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + let regions = componentNode.querySelectorAll('.test-highlight .region') + expect(regions.length).toBe(0) + }) + + it('does not render a highlight that is within a fold', async function () { + editor.foldBufferRow(1) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + expect(componentNode.querySelectorAll('.test-highlight').length).toBe(0) + }) + + it('removes highlights when a decoration\'s marker is destroyed', async function () { + marker.destroy() + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + let regions = componentNode.querySelectorAll('.test-highlight .region') + expect(regions.length).toBe(0) + }) + + it('only renders highlights when a decoration\'s marker is valid', async function () { + editor.getBuffer().insert([3, 2], 'n') + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(marker.isValid()).toBe(false) + let regions = componentNode.querySelectorAll('.test-highlight .region') + expect(regions.length).toBe(0) + editor.getBuffer().undo() + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(marker.isValid()).toBe(true) + regions = componentNode.querySelectorAll('.test-highlight .region') + expect(regions.length).toBe(2) + }) + + it('allows multiple space-delimited decoration classes', async function () { + decoration.setProperties({ + type: 'highlight', + 'class': 'foo bar' + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + expect(componentNode.querySelectorAll('.foo.bar').length).toBe(2) + decoration.setProperties({ + type: 'highlight', + 'class': 'bar baz' + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + expect(componentNode.querySelectorAll('.bar.baz').length).toBe(2) + }) + + it('renders classes on the regions directly if "deprecatedRegionClass" option is defined', async function () { + decoration = editor.decorateMarker(marker, { + type: 'highlight', + 'class': 'test-highlight', + deprecatedRegionClass: 'test-highlight-region' + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + let regions = componentNode.querySelectorAll('.test-highlight .region.test-highlight-region') + expect(regions.length).toBe(2) + }) + + describe('when flashing a decoration via Decoration::flash()', function () { + let highlightNode + + beforeEach(async function () { + highlightNode = componentNode.querySelectorAll('.test-highlight')[1] + }) + + it('adds and removes the flash class specified in ::flash', async function () { + expect(highlightNode.classList.contains('flash-class')).toBe(false) + decoration.flash('flash-class', 10) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(highlightNode.classList.contains('flash-class')).toBe(true) + await conditionPromise(function () { + return !highlightNode.classList.contains('flash-class') + }) + }) + + describe('when ::flash is called again before the first has finished', function () { + it('removes the class from the decoration highlight before adding it for the second ::flash call', async function () { + decoration.flash('flash-class', 100) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + expect(highlightNode.classList.contains('flash-class')).toBe(true) + + await timeoutPromise(2) + + decoration.flash('flash-class', 100) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(highlightNode.classList.contains('flash-class')).toBe(false) + + await conditionPromise(function () { + return highlightNode.classList.contains('flash-class') + }) + }) + }) + }) + + describe('when a decoration\'s marker moves', function () { + it('moves rendered highlights when the buffer is changed', async function () { + let regionStyle = componentNode.querySelector('.test-highlight .region').style + let originalTop = parseInt(regionStyle.top) + expect(originalTop).toBe(2 * lineHeightInPixels) + + editor.getBuffer().insert([0, 0], '\n') + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + regionStyle = componentNode.querySelector('.test-highlight .region').style + let newTop = parseInt(regionStyle.top) + expect(newTop).toBe(0) + }) + + it('moves rendered highlights when the marker is manually moved', async function () { + let regionStyle = componentNode.querySelector('.test-highlight .region').style + expect(parseInt(regionStyle.top)).toBe(2 * lineHeightInPixels) + + marker.setBufferRange([[5, 8], [5, 13]]) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + regionStyle = componentNode.querySelector('.test-highlight .region').style + expect(parseInt(regionStyle.top)).toBe(2 * lineHeightInPixels) + }) + }) + + describe('when a decoration is updated via Decoration::update', function () { + it('renders the decoration\'s new params', async function () { + expect(componentNode.querySelector('.test-highlight')).toBeTruthy() + decoration.setProperties({ + type: 'highlight', + 'class': 'new-test-highlight' + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + expect(componentNode.querySelector('.test-highlight')).toBeFalsy() + expect(componentNode.querySelector('.new-test-highlight')).toBeTruthy() + }) + }) + }) + + describe('overlay decoration rendering', function () { + let gutterWidth, item + + beforeEach(function () { + item = document.createElement('div') + item.classList.add('overlay-test') + item.style.background = 'red' + gutterWidth = componentNode.querySelector('.gutter').offsetWidth + }) + + describe('when the marker is empty', function () { + it('renders an overlay decoration when added and removes the overlay when the decoration is destroyed', async function () { + let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], { + invalidate: 'never' + }) + let decoration = editor.decorateMarker(marker, { + type: 'overlay', + item: item + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + let overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') + expect(overlay).toBe(item) + + decoration.destroy() + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') + expect(overlay).toBe(null) + }) + + it('renders the overlay element with the CSS class specified by the decoration', async function () { + let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], { + invalidate: 'never' + }) + let decoration = editor.decorateMarker(marker, { + type: 'overlay', + 'class': 'my-overlay', + item: item + }) + + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + let overlay = component.getTopmostDOMNode().querySelector('atom-overlay.my-overlay') + expect(overlay).not.toBe(null) + let child = overlay.querySelector('.overlay-test') + expect(child).toBe(item) + }) + }) + + describe('when the marker is not empty', function () { + it('renders at the head of the marker by default', async function () { + let marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], { + invalidate: 'never' + }) + let decoration = editor.decorateMarker(marker, { + type: 'overlay', + item: item + }) + + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + let position = wrapperNode.pixelPositionForBufferPosition([2, 10]) + let overlay = component.getTopmostDOMNode().querySelector('atom-overlay') + expect(overlay.style.left).toBe(Math.round(position.left + gutterWidth) + 'px') + expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') + }) + }) + + describe('positioning the overlay when near the edge of the editor', function () { + let itemHeight, itemWidth, windowHeight, windowWidth + + beforeEach(async function () { + atom.storeWindowDimensions() + itemWidth = Math.round(4 * editor.getDefaultCharWidth()) + itemHeight = 4 * editor.getLineHeightInPixels() + windowWidth = Math.round(gutterWidth + 30 * editor.getDefaultCharWidth()) + windowHeight = 10 * editor.getLineHeightInPixels() + item.style.width = itemWidth + 'px' + item.style.height = itemHeight + 'px' + wrapperNode.style.width = windowWidth + 'px' + wrapperNode.style.height = windowHeight + 'px' + atom.setWindowDimensions({ + width: windowWidth, + height: windowHeight + }) + component.measureDimensions() + component.measureWindowSize() + await atom.views.getNextUpdatePromise() + }) + + afterEach(function () { + atom.restoreWindowDimensions() + }) + + it('slides horizontally left when near the right edge on #win32 and #darwin', async function () { + let marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], { + invalidate: 'never' + }) + let decoration = editor.decorateMarker(marker, { + type: 'overlay', + item: item + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + let position = wrapperNode.pixelPositionForBufferPosition([0, 26]) + let overlay = component.getTopmostDOMNode().querySelector('atom-overlay') + expect(overlay.style.left).toBe(Math.round(position.left + gutterWidth) + 'px') + expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') + + editor.insertText('a') + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(overlay.style.left).toBe(windowWidth - itemWidth + 'px') + expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') + + editor.insertText('b') + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(overlay.style.left).toBe(windowWidth - itemWidth + 'px') + expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') + }) + }) + }) + describe('hidden input field', function () { + it('renders the hidden input field at the position of the last cursor if the cursor is on screen and the editor is focused', async function () { + editor.setVerticalScrollMargin(0) + editor.setHorizontalScrollMargin(0) + let inputNode = componentNode.querySelector('.hidden-input') + wrapperNode.style.height = 5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + wrapperNode.setScrollTop(3 * lineHeightInPixels) + wrapperNode.setScrollLeft(3 * charWidth) + await atom.views.getNextUpdatePromise() + + expect(inputNode.offsetTop).toBe(0) + expect(inputNode.offsetLeft).toBe(0) + + editor.setCursorBufferPosition([5, 4], { + autoscroll: false + }) + await decorationsUpdatedPromise(editor) + await atom.views.getNextUpdatePromise() + + expect(inputNode.offsetTop).toBe(0) + expect(inputNode.offsetLeft).toBe(0) + + wrapperNode.focus() + await atom.views.getNextUpdatePromise() + + expect(inputNode.offsetTop).toBe((5 * lineHeightInPixels) - wrapperNode.getScrollTop()) + expect(inputNode.offsetLeft).toBeCloseTo((4 * charWidth) - wrapperNode.getScrollLeft(), 0) + + inputNode.blur() + await atom.views.getNextUpdatePromise() + + expect(inputNode.offsetTop).toBe(0) + expect(inputNode.offsetLeft).toBe(0) + + editor.setCursorBufferPosition([1, 2], { + autoscroll: false + }) + await atom.views.getNextUpdatePromise() + + expect(inputNode.offsetTop).toBe(0) + expect(inputNode.offsetLeft).toBe(0) + + inputNode.focus() + await atom.views.getNextUpdatePromise() + + expect(inputNode.offsetTop).toBe(0) + expect(inputNode.offsetLeft).toBe(0) + }) + }) + + describe('mouse interactions on the lines', function () { + let linesNode + + beforeEach(function () { + linesNode = componentNode.querySelector('.lines') + }) + + describe('when the mouse is single-clicked above the first line', function () { + it('moves the cursor to the start of file buffer position', async function () { + let height + editor.setText('foo') + editor.setCursorBufferPosition([0, 3]) + height = 4.5 * lineHeightInPixels + wrapperNode.style.height = height + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + let coordinates = clientCoordinatesForScreenPosition([0, 2]) + coordinates.clientY = -1 + linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) + + await atom.views.getNextUpdatePromise() + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + }) + }) + + describe('when the mouse is single-clicked below the last line', function () { + it('moves the cursor to the end of file buffer position', async function () { + editor.setText('foo') + editor.setCursorBufferPosition([0, 0]) + let height = 4.5 * lineHeightInPixels + wrapperNode.style.height = height + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + let coordinates = clientCoordinatesForScreenPosition([0, 2]) + coordinates.clientY = height * 2 + + linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) + await atom.views.getNextUpdatePromise() + + expect(editor.getCursorScreenPosition()).toEqual([0, 3]) + }) + }) + + describe('when a non-folded line is single-clicked', function () { + describe('when no modifier keys are held down', function () { + it('moves the cursor to the nearest screen position', async function () { + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() + wrapperNode.setScrollTop(3.5 * lineHeightInPixels) + wrapperNode.setScrollLeft(2 * charWidth) + await atom.views.getNextUpdatePromise() + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]))) + await atom.views.getNextUpdatePromise() + expect(editor.getCursorScreenPosition()).toEqual([4, 8]) + }) + }) + + describe('when the shift key is held down', function () { + it('selects to the nearest screen position', async function () { + editor.setCursorScreenPosition([3, 4]) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), { + shiftKey: true + })) + await atom.views.getNextUpdatePromise() + expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [5, 6]]) + }) + }) + + describe('when the command key is held down', function () { + describe('the current cursor position and screen position do not match', function () { + it('adds a cursor at the nearest screen position', async function () { + editor.setCursorScreenPosition([3, 4]) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), { + metaKey: true + })) + await atom.views.getNextUpdatePromise() + expect(editor.getSelectedScreenRanges()).toEqual([[[3, 4], [3, 4]], [[5, 6], [5, 6]]]) + }) + }) + + describe('when there are multiple cursors, and one of the cursor\'s screen position is the same as the mouse click screen position', async function () { + it('removes a cursor at the mouse screen position', async function () { + editor.setCursorScreenPosition([3, 4]) + editor.addCursorAtScreenPosition([5, 2]) + editor.addCursorAtScreenPosition([7, 5]) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), { + metaKey: true + })) + await atom.views.getNextUpdatePromise() + expect(editor.getSelectedScreenRanges()).toEqual([[[5, 2], [5, 2]], [[7, 5], [7, 5]]]) + }) + }) + + describe('when there is a single cursor and the click occurs at the cursor\'s screen position', async function () { + it('neither adds a new cursor nor removes the current cursor', async function () { + editor.setCursorScreenPosition([3, 4]) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), { + metaKey: true + })) + await atom.views.getNextUpdatePromise() + expect(editor.getSelectedScreenRanges()).toEqual([[[3, 4], [3, 4]]]) + }) + }) + }) + }) + + describe('when a non-folded line is double-clicked', function () { + describe('when no modifier keys are held down', function () { + it('selects the word containing the nearest screen position', function () { + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 1 + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 2 + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [5, 13]]) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([6, 6]), { + detail: 1 + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + expect(editor.getSelectedScreenRange()).toEqual([[6, 6], [6, 6]]) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([8, 8]), { + detail: 1, + shiftKey: true + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + expect(editor.getSelectedScreenRange()).toEqual([[6, 6], [8, 8]]) + }) + }) + + describe('when the command key is held down', function () { + it('selects the word containing the newly-added cursor', function () { + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 1, + metaKey: true + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 2, + metaKey: true + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + expect(editor.getSelectedScreenRanges()).toEqual([[[0, 0], [0, 0]], [[5, 6], [5, 13]]]) + }) + }) + }) + + describe('when a non-folded line is triple-clicked', function () { + describe('when no modifier keys are held down', function () { + it('selects the line containing the nearest screen position', function () { + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 1 + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 2 + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 3 + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [6, 0]]) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([6, 6]), { + detail: 1, + shiftKey: true + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [7, 0]]) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([7, 5]), { + detail: 1 + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([8, 8]), { + detail: 1, + shiftKey: true + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + expect(editor.getSelectedScreenRange()).toEqual([[7, 5], [8, 8]]) + }) + }) + + describe('when the command key is held down', function () { + it('selects the line containing the newly-added cursor', function () { + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 1, + metaKey: true + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 2, + metaKey: true + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 3, + metaKey: true + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + expect(editor.getSelectedScreenRanges()).toEqual([[[0, 0], [0, 0]], [[5, 0], [6, 0]]]) + }) + }) + }) + + describe('when the mouse is clicked and dragged', function () { + it('selects to the nearest screen position until the mouse button is released', async function () { + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { + which: 1 + })) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { + which: 1 + })) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), { + which: 1 + })) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [10, 0]]) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([12, 0]), { + which: 1 + })) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [10, 0]]) + }) + + it('autoscrolls when the cursor approaches the boundaries of the editor', async function () { + wrapperNode.style.height = '100px' + wrapperNode.style.width = '100px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + expect(wrapperNode.getScrollLeft()).toBe(0) + + linesNode.dispatchEvent(buildMouseEvent('mousedown', { + clientX: 0, + clientY: 0 + }, { + which: 1 + })) + linesNode.dispatchEvent(buildMouseEvent('mousemove', { + clientX: 100, + clientY: 50 + }, { + which: 1 + })) + + for (let i = 0; i <= 5; ++i) { + await nextAnimationFramePromise() + } + + expect(wrapperNode.getScrollTop()).toBe(0) + expect(wrapperNode.getScrollLeft()).toBeGreaterThan(0) + linesNode.dispatchEvent(buildMouseEvent('mousemove', { + clientX: 100, + clientY: 100 + }, { + which: 1 + })) + + for (let i = 0; i <= 5; ++i) { + await nextAnimationFramePromise() + } + + expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) + let previousScrollTop = wrapperNode.getScrollTop() + let previousScrollLeft = wrapperNode.getScrollLeft() + + linesNode.dispatchEvent(buildMouseEvent('mousemove', { + clientX: 10, + clientY: 50 + }, { + which: 1 + })) + + for (let i = 0; i <= 5; ++i) { + await nextAnimationFramePromise() + } + + expect(wrapperNode.getScrollTop()).toBe(previousScrollTop) + expect(wrapperNode.getScrollLeft()).toBeLessThan(previousScrollLeft) + linesNode.dispatchEvent(buildMouseEvent('mousemove', { + clientX: 10, + clientY: 10 + }, { + which: 1 + })) + + for (let i = 0; i <= 5; ++i) { + await nextAnimationFramePromise() + } + + expect(wrapperNode.getScrollTop()).toBeLessThan(previousScrollTop) + }) + + it('stops selecting if the mouse is dragged into the dev tools', async function () { + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { + which: 1 + })) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { + which: 1 + })) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), { + which: 0 + })) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), { + which: 1 + })) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) + }) + + it('stops selecting before the buffer is modified during the drag', async function () { + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { + which: 1 + })) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { + which: 1 + })) + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [6, 8]]) + + editor.insertText('x') + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRange()).toEqual([[2, 5], [2, 5]]) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), { + which: 1 + })) + expect(editor.getSelectedScreenRange()).toEqual([[2, 5], [2, 5]]) + + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { + which: 1 + })) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([5, 4]), { + which: 1 + })) + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [5, 4]]) + + editor.delete() + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [2, 4]]) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), { + which: 1 + })) + expect(editor.getSelectedScreenRange()).toEqual([[2, 4], [2, 4]]) + }) + + describe('when the command key is held down', function () { + it('adds a new selection and selects to the nearest screen position, then merges intersecting selections when the mouse button is released', async function () { + editor.setSelectedScreenRange([[4, 4], [4, 9]]) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { + which: 1, + metaKey: true + })) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { + which: 1 + })) + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRanges()).toEqual([[[4, 4], [4, 9]], [[2, 4], [6, 8]]]) + + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([4, 6]), { + which: 1 + })) + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRanges()).toEqual([[[4, 4], [4, 9]], [[2, 4], [4, 6]]]) + linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([4, 6]), { + which: 1 + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[2, 4], [4, 9]]]) + }) + }) + + describe('when the editor is destroyed while dragging', function () { + it('cleans up the handlers for window.mouseup and window.mousemove', async function () { + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), { + which: 1 + })) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), { + which: 1 + })) + await nextAnimationFramePromise() + + spyOn(window, 'removeEventListener').andCallThrough() + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 10]), { + which: 1 + })) + + editor.destroy() + await nextAnimationFramePromise() + + for (let call of window.removeEventListener.calls) { + call.args.pop() + } + expect(window.removeEventListener).toHaveBeenCalledWith('mouseup') + expect(window.removeEventListener).toHaveBeenCalledWith('mousemove') + }) + }) + }) + + describe('when the mouse is double-clicked and dragged', function () { + it('expands the selection over the nearest word as the cursor moves', async function () { + jasmine.attachToDOM(wrapperNode) + wrapperNode.style.height = 6 * lineHeightInPixels + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 1 + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 2 + })) + expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [5, 13]]) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), { + which: 1 + })) + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [12, 2]]) + let maximalScrollTop = wrapperNode.getScrollTop() + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([9, 3]), { + which: 1 + })) + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRange()).toEqual([[5, 6], [9, 4]]) + expect(wrapperNode.getScrollTop()).toBe(maximalScrollTop) + linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), { + which: 1 + })) + }) + }) + + describe('when the mouse is triple-clicked and dragged', function () { + it('expands the selection over the nearest line as the cursor moves', async function () { + jasmine.attachToDOM(wrapperNode) + wrapperNode.style.height = 6 * lineHeightInPixels + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 1 + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 2 + })) + linesNode.dispatchEvent(buildMouseEvent('mouseup')) + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { + detail: 3 + })) + expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [6, 0]]) + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), { + which: 1 + })) + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [12, 2]]) + let maximalScrollTop = wrapperNode.getScrollTop() + linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 4]), { + which: 1 + })) + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [8, 0]]) + expect(wrapperNode.getScrollTop()).toBe(maximalScrollTop) + linesNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([9, 3]), { + which: 1 + })) + }) + }) + + describe('when a line is folded', function () { + beforeEach(async function () { + editor.foldBufferRow(4) + await atom.views.getNextUpdatePromise() + }) + + describe('when the folded line\'s fold-marker is clicked', function () { + it('unfolds the buffer row', function () { + let target = component.lineNodeForScreenRow(4).querySelector('.fold-marker') + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), { + target: target + })) + expect(editor.isFoldedAtBufferRow(4)).toBe(false) + }) + }) + }) + + describe('when the horizontal scrollbar is interacted with', function () { + it('clicking on the scrollbar does not move the cursor', function () { + let target = horizontalScrollbarNode + linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), { + target: target + })) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + }) + }) + }) + + describe('mouse interactions on the gutter', function () { + let gutterNode + + beforeEach(function () { + gutterNode = componentNode.querySelector('.gutter') + }) + + describe('when the component is destroyed', function () { + it('stops listening for selection events', function () { + component.destroy() + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) + expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [0, 0]]) + }) + }) + + describe('when the gutter is clicked', function () { + it('selects the clicked row', function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4))) + expect(editor.getSelectedScreenRange()).toEqual([[4, 0], [5, 0]]) + }) + }) + + describe('when the gutter is meta-clicked', function () { + it('creates a new selection for the clicked row', function () { + editor.setSelectedScreenRange([[3, 0], [3, 2]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), { + metaKey: true + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [5, 0]]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), { + metaKey: true + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [5, 0]], [[6, 0], [7, 0]]]) + }) + }) + + describe('when the gutter is shift-clicked', function () { + beforeEach(function () { + editor.setSelectedScreenRange([[3, 4], [4, 5]]) + }) + + describe('when the clicked row is before the current selection\'s tail', function () { + it('selects to the beginning of the clicked row', function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { + shiftKey: true + })) + expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [3, 4]]) + }) + }) + + describe('when the clicked row is after the current selection\'s tail', function () { + it('selects to the beginning of the row following the clicked row', function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), { + shiftKey: true + })) + expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [7, 0]]) + }) + }) + }) + + describe('when the gutter is clicked and dragged', function () { + describe('when dragging downward', function () { + it('selects the rows between the start and end of the drag', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) + await nextAnimationFramePromise() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) + expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [7, 0]]) + }) + }) + + describe('when dragging upward', function () { + it('selects the rows between the start and end of the drag', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6))) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) + await nextAnimationFramePromise() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2))) + expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [7, 0]]) + }) + }) + + it('orients the selection appropriately when the mouse moves above or below the initially-clicked row', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4))) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) + await nextAnimationFramePromise() + expect(editor.getLastSelection().isReversed()).toBe(true) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) + await nextAnimationFramePromise() + expect(editor.getLastSelection().isReversed()).toBe(false) + }) + + it('autoscrolls when the cursor approaches the top or bottom of the editor', async function () { + wrapperNode.style.height = 6 * lineHeightInPixels + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) + await nextAnimationFramePromise() + + expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) + let maxScrollTop = wrapperNode.getScrollTop() + + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(10))) + await nextAnimationFramePromise() + + expect(wrapperNode.getScrollTop()).toBe(maxScrollTop) + + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) + await nextAnimationFramePromise() + + expect(wrapperNode.getScrollTop()).toBeLessThan(maxScrollTop) + }) + + it('stops selecting if a textInput event occurs during the drag', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2))) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [7, 0]]) + + let inputEvent = new Event('textInput') + inputEvent.data = 'x' + Object.defineProperty(inputEvent, 'target', { + get: function () { + return componentNode.querySelector('.hidden-input') + } + }) + componentNode.dispatchEvent(inputEvent) + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRange()).toEqual([[2, 1], [2, 1]]) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(12))) + expect(editor.getSelectedScreenRange()).toEqual([[2, 1], [2, 1]]) + }) + }) + + describe('when the gutter is meta-clicked and dragged', function () { + beforeEach(function () { + editor.setSelectedScreenRange([[3, 0], [3, 2]]) + }) + + describe('when dragging downward', function () { + it('selects the rows between the start and end of the drag', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(4), { + metaKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), { + metaKey: true + })) + await nextAnimationFramePromise() + + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), { + metaKey: true + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [7, 0]]]) + }) + + it('merges overlapping selections when the mouse button is released', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), { + metaKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6), { + metaKey: true + })) + await nextAnimationFramePromise() + + expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[2, 0], [7, 0]]]) + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6), { + metaKey: true + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[2, 0], [7, 0]]]) + }) + }) + + describe('when dragging upward', function () { + it('selects the rows between the start and end of the drag', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), { + metaKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(4), { + metaKey: true + })) + await nextAnimationFramePromise() + + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(4), { + metaKey: true + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[3, 0], [3, 2]], [[4, 0], [7, 0]]]) + }) + + it('merges overlapping selections', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6), { + metaKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2), { + metaKey: true + })) + await nextAnimationFramePromise() + + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(2), { + metaKey: true + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[2, 0], [7, 0]]]) + }) + }) + }) + + describe('when the gutter is shift-clicked and dragged', function () { + describe('when the shift-click is below the existing selection\'s tail', function () { + describe('when dragging downward', function () { + it('selects the rows between the existing selection\'s tail and the end of the drag', async function () { + editor.setSelectedScreenRange([[3, 4], [4, 5]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), { + shiftKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [9, 0]]) + }) + }) + + describe('when dragging upward', function () { + it('selects the rows between the end of the drag and the tail of the existing selection', async function () { + editor.setSelectedScreenRange([[4, 4], [5, 5]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), { + shiftKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5))) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[4, 4], [6, 0]]) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [4, 4]]) + }) + }) + }) + + describe('when the shift-click is above the existing selection\'s tail', function () { + describe('when dragging upward', function () { + it('selects the rows between the end of the drag and the tail of the existing selection', async function () { + editor.setSelectedScreenRange([[4, 4], [5, 5]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), { + shiftKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [4, 4]]) + }) + }) + + describe('when dragging downward', function () { + it('selects the rows between the existing selection\'s tail and the end of the drag', async function () { + editor.setSelectedScreenRange([[3, 4], [4, 5]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { + shiftKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2))) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [3, 4]]) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8))) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [9, 0]]) + }) + }) + }) + }) + + describe('when soft wrap is enabled', function () { + beforeEach(async function () { + gutterNode = componentNode.querySelector('.gutter') + editor.setSoftWrapped(true) + await atom.views.getNextUpdatePromise() + componentNode.style.width = 21 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + }) + + describe('when the gutter is clicked', function () { + it('selects the clicked buffer row', function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) + expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [2, 0]]) + }) + }) + + describe('when the gutter is meta-clicked', function () { + it('creates a new selection for the clicked buffer row', function () { + editor.setSelectedScreenRange([[1, 0], [1, 2]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2), { + metaKey: true + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[1, 0], [1, 2]], [[2, 0], [5, 0]]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), { + metaKey: true + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[1, 0], [1, 2]], [[2, 0], [5, 0]], [[5, 0], [10, 0]]]) + }) + }) + + describe('when the gutter is shift-clicked', function () { + beforeEach(function () { + return editor.setSelectedScreenRange([[7, 4], [7, 6]]) + }) + + describe('when the clicked row is before the current selection\'s tail', function () { + it('selects to the beginning of the clicked buffer row', function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { + shiftKey: true + })) + expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [7, 4]]) + }) + }) + + describe('when the clicked row is after the current selection\'s tail', function () { + it('selects to the beginning of the screen row following the clicked buffer row', function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), { + shiftKey: true + })) + expect(editor.getSelectedScreenRange()).toEqual([[7, 4], [16, 0]]) + }) + }) + }) + + describe('when the gutter is clicked and dragged', function () { + describe('when dragging downward', function () { + it('selects the buffer row containing the click, then screen rows until the end of the drag', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1))) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6))) + await nextAnimationFramePromise() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(6))) + expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [6, 14]]) + }) + }) + + describe('when dragging upward', function () { + it('selects the buffer row containing the click, then screen rows until the end of the drag', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(6))) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) + await nextAnimationFramePromise() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(1))) + expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [10, 0]]) + }) + }) + }) + + describe('when the gutter is meta-clicked and dragged', function () { + beforeEach(function () { + editor.setSelectedScreenRange([[7, 4], [7, 6]]) + }) + + describe('when dragging downward', function () { + it('adds a selection from the buffer row containing the click to the screen row containing the end of the drag', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { + metaKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3), { + metaKey: true + })) + await nextAnimationFramePromise() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(3), { + metaKey: true + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[0, 0], [3, 14]]]) + }) + + it('merges overlapping selections on mouseup', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { + metaKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7), { + metaKey: true + })) + await nextAnimationFramePromise() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(7), { + metaKey: true + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[0, 0], [7, 12]]]) + }) + }) + + describe('when dragging upward', function () { + it('adds a selection from the buffer row containing the click to the screen row containing the end of the drag', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), { + metaKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11), { + metaKey: true + })) + await nextAnimationFramePromise() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), { + metaKey: true + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[11, 4], [19, 0]]]) + }) + + it('merges overlapping selections on mouseup', async function () { + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(17), { + metaKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(5), { + metaKey: true + })) + await nextAnimationFramePromise() + gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), { + metaKey: true + })) + expect(editor.getSelectedScreenRanges()).toEqual([[[5, 0], [19, 0]]]) + }) + }) + }) + + describe('when the gutter is shift-clicked and dragged', function () { + describe('when the shift-click is below the existing selection\'s tail', function () { + describe('when dragging downward', function () { + it('selects the screen rows between the existing selection\'s tail and the end of the drag', async function () { + editor.setSelectedScreenRange([[1, 4], [1, 7]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(7), { + shiftKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11))) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [11, 14]]) + }) + }) + + describe('when dragging upward', function () { + it('selects the screen rows between the end of the drag and the tail of the existing selection', async function () { + editor.setSelectedScreenRange([[1, 4], [1, 7]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), { + shiftKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(7))) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [7, 12]]) + }) + }) + }) + + describe('when the shift-click is above the existing selection\'s tail', function () { + describe('when dragging upward', function () { + it('selects the screen rows between the end of the drag and the tail of the existing selection', async function () { + editor.setSelectedScreenRange([[7, 4], [7, 6]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(3), { + shiftKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(1))) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [7, 4]]) + }) + }) + + describe('when dragging downward', function () { + it('selects the screen rows between the existing selection\'s tail and the end of the drag', async function () { + editor.setSelectedScreenRange([[7, 4], [7, 6]]) + gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(1), { + shiftKey: true + })) + gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(3))) + await nextAnimationFramePromise() + expect(editor.getSelectedScreenRange()).toEqual([[3, 2], [7, 4]]) + }) + }) + }) + }) + }) + }) + + describe('focus handling', async function () { + let inputNode + beforeEach(function () { + inputNode = componentNode.querySelector('.hidden-input') + }) + + it('transfers focus to the hidden input', function () { + expect(document.activeElement).toBe(document.body) + wrapperNode.focus() + expect(document.activeElement).toBe(wrapperNode) + expect(wrapperNode.shadowRoot.activeElement).toBe(inputNode) + }) + + it('adds the "is-focused" class to the editor when the hidden input is focused', async function () { + expect(document.activeElement).toBe(document.body) + inputNode.focus() + await atom.views.getNextUpdatePromise() + + expect(componentNode.classList.contains('is-focused')).toBe(true) + expect(wrapperNode.classList.contains('is-focused')).toBe(true) + inputNode.blur() + await atom.views.getNextUpdatePromise() + + expect(componentNode.classList.contains('is-focused')).toBe(false) + expect(wrapperNode.classList.contains('is-focused')).toBe(false) + }) + }) + + describe('selection handling', function () { + let cursor + + beforeEach(async function () { + editor.setCursorScreenPosition([0, 0]) + await atom.views.getNextUpdatePromise() + }) + + it('adds the "has-selection" class to the editor when there is a selection', async function () { + expect(componentNode.classList.contains('has-selection')).toBe(false) + editor.selectDown() + await atom.views.getNextUpdatePromise() + expect(componentNode.classList.contains('has-selection')).toBe(true) + editor.moveDown() + await atom.views.getNextUpdatePromise() + expect(componentNode.classList.contains('has-selection')).toBe(false) + }) + }) + + describe('scrolling', function () { + it('updates the vertical scrollbar when the scrollTop is changed in the model', async function () { + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + expect(verticalScrollbarNode.scrollTop).toBe(0) + wrapperNode.setScrollTop(10) + await atom.views.getNextUpdatePromise() + expect(verticalScrollbarNode.scrollTop).toBe(10) + }) + + it('updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model', async function () { + componentNode.style.width = 30 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + let top = 0 + let tilesNodes = component.tileNodesForLines() + for (let tileNode of tilesNodes) { + expect(tileNode.style['-webkit-transform']).toBe('translate3d(0px, ' + top + 'px, 0px)') + top += tileNode.offsetHeight + } + expect(horizontalScrollbarNode.scrollLeft).toBe(0) + wrapperNode.setScrollLeft(100) + + await atom.views.getNextUpdatePromise() + + top = 0 + for (let tileNode of tilesNodes) { + expect(tileNode.style['-webkit-transform']).toBe('translate3d(-100px, ' + top + 'px, 0px)') + top += tileNode.offsetHeight + } + expect(horizontalScrollbarNode.scrollLeft).toBe(100) + }) + + it('updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes', async function () { + componentNode.style.width = 30 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + expect(wrapperNode.getScrollLeft()).toBe(0) + horizontalScrollbarNode.scrollLeft = 100 + horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + await atom.views.getNextUpdatePromise() + expect(wrapperNode.getScrollLeft()).toBe(100) + }) + + it('does not obscure the last line with the horizontal scrollbar', async function () { + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() + wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) + await atom.views.getNextUpdatePromise() + + let lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) + let bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom + topOfHorizontalScrollbar = horizontalScrollbarNode.getBoundingClientRect().top + expect(bottomOfLastLine).toBe(topOfHorizontalScrollbar) + wrapperNode.style.width = 100 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom + let bottomOfEditor = componentNode.getBoundingClientRect().bottom + expect(bottomOfLastLine).toBe(bottomOfEditor) + }) + + it('does not obscure the last character of the longest line with the vertical scrollbar', async function () { + wrapperNode.style.height = 7 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() + wrapperNode.setScrollLeft(Infinity) + + await atom.views.getNextUpdatePromise() + let rightOfLongestLine = component.lineNodeForScreenRow(6).querySelector('.line > span:last-child').getBoundingClientRect().right + let leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left + expect(Math.round(rightOfLongestLine)).toBeCloseTo(leftOfVerticalScrollbar - 1, 0) + }) + + it('only displays dummy scrollbars when scrollable in that direction', async function () { + expect(verticalScrollbarNode.style.display).toBe('none') + expect(horizontalScrollbarNode.style.display).toBe('none') + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = '1000px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + expect(verticalScrollbarNode.style.display).toBe('') + expect(horizontalScrollbarNode.style.display).toBe('none') + componentNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + expect(verticalScrollbarNode.style.display).toBe('') + expect(horizontalScrollbarNode.style.display).toBe('') + wrapperNode.style.height = 20 * lineHeightInPixels + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + expect(verticalScrollbarNode.style.display).toBe('none') + expect(horizontalScrollbarNode.style.display).toBe('') + }) + + it('makes the dummy scrollbar divs only as tall/wide as the actual scrollbars', async function () { + wrapperNode.style.height = 4 * lineHeightInPixels + 'px' + wrapperNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + atom.styles.addStyleSheet('::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}', { + context: 'atom-text-editor' + }) + + await nextAnimationFramePromise() + await nextAnimationFramePromise() + + let scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') + expect(verticalScrollbarNode.offsetWidth).toBe(8) + expect(horizontalScrollbarNode.offsetHeight).toBe(8) + expect(scrollbarCornerNode.offsetWidth).toBe(8) + expect(scrollbarCornerNode.offsetHeight).toBe(8) + atom.themes.removeStylesheet('test') + }) + + it('assigns the bottom/right of the scrollbars to the width of the opposite scrollbar if it is visible', async function () { + let scrollbarCornerNode = componentNode.querySelector('.scrollbar-corner') + expect(verticalScrollbarNode.style.bottom).toBe('0px') + expect(horizontalScrollbarNode.style.right).toBe('0px') + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = '1000px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + expect(verticalScrollbarNode.style.bottom).toBe('0px') + expect(horizontalScrollbarNode.style.right).toBe(verticalScrollbarNode.offsetWidth + 'px') + expect(scrollbarCornerNode.style.display).toBe('none') + componentNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + expect(verticalScrollbarNode.style.bottom).toBe(horizontalScrollbarNode.offsetHeight + 'px') + expect(horizontalScrollbarNode.style.right).toBe(verticalScrollbarNode.offsetWidth + 'px') + expect(scrollbarCornerNode.style.display).toBe('') + wrapperNode.style.height = 20 * lineHeightInPixels + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + expect(verticalScrollbarNode.style.bottom).toBe(horizontalScrollbarNode.offsetHeight + 'px') + expect(horizontalScrollbarNode.style.right).toBe('0px') + expect(scrollbarCornerNode.style.display).toBe('none') + }) + + it('accounts for the width of the gutter in the scrollWidth of the horizontal scrollbar', async function () { + let gutterNode = componentNode.querySelector('.gutter') + componentNode.style.width = 10 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + expect(horizontalScrollbarNode.scrollWidth).toBe(wrapperNode.getScrollWidth()) + expect(horizontalScrollbarNode.style.left).toBe('0px') + }) + }) + + describe('mousewheel events', function () { + beforeEach(function () { + atom.config.set('editor.scrollSensitivity', 100) + }) + + describe('updating scrollTop and scrollLeft', function () { + beforeEach(async function () { + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + }) + + it('updates the scrollLeft or scrollTop on mousewheel events depending on which delta is greater (x or y)', async function () { + expect(verticalScrollbarNode.scrollTop).toBe(0) + expect(horizontalScrollbarNode.scrollLeft).toBe(0) + componentNode.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaX: -5, + wheelDeltaY: -10 + })) + await nextAnimationFramePromise() + + expect(verticalScrollbarNode.scrollTop).toBe(10) + expect(horizontalScrollbarNode.scrollLeft).toBe(0) + componentNode.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaX: -15, + wheelDeltaY: -5 + })) + await nextAnimationFramePromise() + + expect(verticalScrollbarNode.scrollTop).toBe(10) + expect(horizontalScrollbarNode.scrollLeft).toBe(15) + }) + + it('updates the scrollLeft or scrollTop according to the scroll sensitivity', async function () { + atom.config.set('editor.scrollSensitivity', 50) + componentNode.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaX: -5, + wheelDeltaY: -10 + })) + await nextAnimationFramePromise() + + expect(horizontalScrollbarNode.scrollLeft).toBe(0) + componentNode.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaX: -15, + wheelDeltaY: -5 + })) + await nextAnimationFramePromise() + + expect(verticalScrollbarNode.scrollTop).toBe(5) + expect(horizontalScrollbarNode.scrollLeft).toBe(7) + }) + + it('uses the previous scrollSensitivity when the value is not an int', async function () { + atom.config.set('editor.scrollSensitivity', 'nope') + componentNode.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaX: 0, + wheelDeltaY: -10 + })) + await nextAnimationFramePromise() + expect(verticalScrollbarNode.scrollTop).toBe(10) + }) + + it('parses negative scrollSensitivity values at the minimum', async function () { + atom.config.set('editor.scrollSensitivity', -50) + componentNode.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaX: 0, + wheelDeltaY: -10 + })) + await nextAnimationFramePromise() + expect(verticalScrollbarNode.scrollTop).toBe(1) + }) + }) + + describe('when the mousewheel event\'s target is a line', function () { + it('keeps the line on the DOM if it is scrolled off-screen', async function () { + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + let lineNode = componentNode.querySelector('.line') + let wheelEvent = new WheelEvent('mousewheel', { + wheelDeltaX: 0, + wheelDeltaY: -500 + }) + Object.defineProperty(wheelEvent, 'target', { + get: function () { + return lineNode + } + }) + componentNode.dispatchEvent(wheelEvent) + await nextAnimationFramePromise() + + expect(componentNode.contains(lineNode)).toBe(true) + }) + + it('does not set the mouseWheelScreenRow if scrolling horizontally', async function () { + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + let lineNode = componentNode.querySelector('.line') + let wheelEvent = new WheelEvent('mousewheel', { + wheelDeltaX: 10, + wheelDeltaY: 0 + }) + Object.defineProperty(wheelEvent, 'target', { + get: function () { + return lineNode + } + }) + componentNode.dispatchEvent(wheelEvent) + await nextAnimationFramePromise() + + expect(component.presenter.mouseWheelScreenRow).toBe(null) + }) + + it('clears the mouseWheelScreenRow after a delay even if the event does not cause scrolling', async function () { + expect(wrapperNode.getScrollTop()).toBe(0) + let lineNode = componentNode.querySelector('.line') + let wheelEvent = new WheelEvent('mousewheel', { + wheelDeltaX: 0, + wheelDeltaY: 10 + }) + Object.defineProperty(wheelEvent, 'target', { + get: function () { + return lineNode + } + }) + componentNode.dispatchEvent(wheelEvent) + expect(wrapperNode.getScrollTop()).toBe(0) + expect(component.presenter.mouseWheelScreenRow).toBe(0) + + await conditionPromise(function () { + return component.presenter.mouseWheelScreenRow == null + }) + }) + + it('does not preserve the line if it is on screen', function () { + let lineNode, lineNodes, wheelEvent + expect(componentNode.querySelectorAll('.line-number').length).toBe(14) + lineNodes = componentNode.querySelectorAll('.line') + expect(lineNodes.length).toBe(13) + lineNode = lineNodes[0] + wheelEvent = new WheelEvent('mousewheel', { + wheelDeltaX: 0, + wheelDeltaY: 100 + }) + Object.defineProperty(wheelEvent, 'target', { + get: function () { + return lineNode + } + }) + componentNode.dispatchEvent(wheelEvent) + expect(component.presenter.mouseWheelScreenRow).toBe(0) + editor.insertText('hello') + expect(componentNode.querySelectorAll('.line-number').length).toBe(14) + expect(componentNode.querySelectorAll('.line').length).toBe(13) + }) + }) + + describe('when the mousewheel event\'s target is a line number', function () { + it('keeps the line number on the DOM if it is scrolled off-screen', async function () { + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + let lineNumberNode = componentNode.querySelectorAll('.line-number')[1] + let wheelEvent = new WheelEvent('mousewheel', { + wheelDeltaX: 0, + wheelDeltaY: -500 + }) + Object.defineProperty(wheelEvent, 'target', { + get: function () { + return lineNumberNode + } + }) + componentNode.dispatchEvent(wheelEvent) + await nextAnimationFramePromise() + + expect(componentNode.contains(lineNumberNode)).toBe(true) + }) + }) + + it('only prevents the default action of the mousewheel event if it actually lead to scrolling', async function () { + spyOn(WheelEvent.prototype, 'preventDefault').andCallThrough() + wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + wrapperNode.style.width = 20 * charWidth + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + componentNode.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaX: 0, + wheelDeltaY: 50 + })) + expect(wrapperNode.getScrollTop()).toBe(0) + expect(WheelEvent.prototype.preventDefault).not.toHaveBeenCalled() + componentNode.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaX: 0, + wheelDeltaY: -3000 + })) + await nextAnimationFramePromise() + + let maxScrollTop = wrapperNode.getScrollTop() + expect(WheelEvent.prototype.preventDefault).toHaveBeenCalled() + WheelEvent.prototype.preventDefault.reset() + componentNode.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaX: 0, + wheelDeltaY: -30 + })) + expect(wrapperNode.getScrollTop()).toBe(maxScrollTop) + expect(WheelEvent.prototype.preventDefault).not.toHaveBeenCalled() + componentNode.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaX: 50, + wheelDeltaY: 0 + })) + expect(wrapperNode.getScrollLeft()).toBe(0) + expect(WheelEvent.prototype.preventDefault).not.toHaveBeenCalled() + componentNode.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaX: -3000, + wheelDeltaY: 0 + })) + await nextAnimationFramePromise() + + let maxScrollLeft = wrapperNode.getScrollLeft() + expect(WheelEvent.prototype.preventDefault).toHaveBeenCalled() + WheelEvent.prototype.preventDefault.reset() + componentNode.dispatchEvent(new WheelEvent('mousewheel', { + wheelDeltaX: -30, + wheelDeltaY: 0 + })) + expect(wrapperNode.getScrollLeft()).toBe(maxScrollLeft) + expect(WheelEvent.prototype.preventDefault).not.toHaveBeenCalled() + }) + }) + + describe('input events', function () { + function buildTextInputEvent ({data, target}) { + let event = new Event('textInput') + event.data = data + Object.defineProperty(event, 'target', { + get: function () { + return target + } + }) + return event + } + + let inputNode + + beforeEach(function () { + inputNode = componentNode.querySelector('.hidden-input') + }) + + it('inserts the newest character in the input\'s value into the buffer', async function () { + componentNode.dispatchEvent(buildTextInputEvent({ + data: 'x', + target: inputNode + })) + await atom.views.getNextUpdatePromise() + + expect(editor.lineTextForBufferRow(0)).toBe('xvar quicksort = function () {') + componentNode.dispatchEvent(buildTextInputEvent({ + data: 'y', + target: inputNode + })) + + expect(editor.lineTextForBufferRow(0)).toBe('xyvar quicksort = function () {') + }) + + it('replaces the last character if the length of the input\'s value does not increase, as occurs with the accented character menu', async function () { + componentNode.dispatchEvent(buildTextInputEvent({ + data: 'u', + target: inputNode + })) + await atom.views.getNextUpdatePromise() + + expect(editor.lineTextForBufferRow(0)).toBe('uvar quicksort = function () {') + inputNode.setSelectionRange(0, 1) + componentNode.dispatchEvent(buildTextInputEvent({ + data: 'ü', + target: inputNode + })) + await atom.views.getNextUpdatePromise() + + expect(editor.lineTextForBufferRow(0)).toBe('üvar quicksort = function () {') + }) + + it('does not handle input events when input is disabled', async function () { + component.setInputEnabled(false) + componentNode.dispatchEvent(buildTextInputEvent({ + data: 'x', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') + await nextAnimationFramePromise() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') + }) + + it('groups events that occur close together in time into single undo entries', function () { + let currentTime = 0 + spyOn(Date, 'now').andCallFake(function () { + return currentTime + }) + atom.config.set('editor.undoGroupingInterval', 100) + editor.setText('') + componentNode.dispatchEvent(buildTextInputEvent({ + data: 'x', + target: inputNode + })) + currentTime += 99 + componentNode.dispatchEvent(buildTextInputEvent({ + data: 'y', + target: inputNode + })) + currentTime += 99 + componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', { + bubbles: true, + cancelable: true + })) + currentTime += 101 + componentNode.dispatchEvent(new CustomEvent('editor:duplicate-lines', { + bubbles: true, + cancelable: true + })) + expect(editor.getText()).toBe('xy\nxy\nxy') + componentNode.dispatchEvent(new CustomEvent('core:undo', { + bubbles: true, + cancelable: true + })) + expect(editor.getText()).toBe('xy\nxy') + componentNode.dispatchEvent(new CustomEvent('core:undo', { + bubbles: true, + cancelable: true + })) + expect(editor.getText()).toBe('') + }) + + describe('when IME composition is used to insert international characters', function () { + function buildIMECompositionEvent (event, {data, target} = {}) { + event = new Event(event) + event.data = data + Object.defineProperty(event, 'target', { + get: function () { + return target + } + }) + return event + } + + let inputNode + + beforeEach(function () { + inputNode = componentNode.querySelector('.hidden-input') + }) + + describe('when nothing is selected', function () { + it('inserts the chosen completion', function () { + componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { + target: inputNode + })) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { + data: 's', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('svar quicksort = function () {') + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { + data: 'sd', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('sdvar quicksort = function () {') + componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { + target: inputNode + })) + componentNode.dispatchEvent(buildTextInputEvent({ + data: '速度', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('速度var quicksort = function () {') + }) + + it('reverts back to the original text when the completion helper is dismissed', function () { + componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { + target: inputNode + })) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { + data: 's', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('svar quicksort = function () {') + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { + data: 'sd', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('sdvar quicksort = function () {') + componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') + }) + + it('allows multiple accented character to be inserted with the \' on a US international layout', function () { + inputNode.value = '\'' + inputNode.setSelectionRange(0, 1) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { + target: inputNode + })) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { + data: '\'', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('\'var quicksort = function () {') + componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { + target: inputNode + })) + componentNode.dispatchEvent(buildTextInputEvent({ + data: 'á', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('ávar quicksort = function () {') + inputNode.value = '\'' + inputNode.setSelectionRange(0, 1) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { + target: inputNode + })) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { + data: '\'', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('á\'var quicksort = function () {') + componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { + target: inputNode + })) + componentNode.dispatchEvent(buildTextInputEvent({ + data: 'á', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('áávar quicksort = function () {') + }) + }) + + describe('when a string is selected', function () { + beforeEach(function () { + editor.setSelectedBufferRanges([[[0, 4], [0, 9]], [[0, 16], [0, 19]]]) + }) + + it('inserts the chosen completion', function () { + componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { + target: inputNode + })) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { + data: 's', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('var ssort = sction () {') + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { + data: 'sd', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('var sdsort = sdction () {') + componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { + target: inputNode + })) + componentNode.dispatchEvent(buildTextInputEvent({ + data: '速度', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('var 速度sort = 速度ction () {') + }) + + it('reverts back to the original text when the completion helper is dismissed', function () { + componentNode.dispatchEvent(buildIMECompositionEvent('compositionstart', { + target: inputNode + })) + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { + data: 's', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('var ssort = sction () {') + componentNode.dispatchEvent(buildIMECompositionEvent('compositionupdate', { + data: 'sd', + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('var sdsort = sdction () {') + componentNode.dispatchEvent(buildIMECompositionEvent('compositionend', { + target: inputNode + })) + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {') + }) + }) + }) + }) + + describe('commands', function () { + describe('editor:consolidate-selections', function () { + it('consolidates selections on the editor model, aborting the key binding if there is only one selection', function () { + spyOn(editor, 'consolidateSelections').andCallThrough() + let event = new CustomEvent('editor:consolidate-selections', { + bubbles: true, + cancelable: true + }) + event.abortKeyBinding = jasmine.createSpy('event.abortKeyBinding') + componentNode.dispatchEvent(event) + expect(editor.consolidateSelections).toHaveBeenCalled() + expect(event.abortKeyBinding).toHaveBeenCalled() + }) + }) + }) + + describe('when changing the font', async function () { + it('measures the default char, the korean char, the double width char and the half width char widths', async function () { + expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0) + component.setFontSize(10) + await atom.views.getNextUpdatePromise() + expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0) + expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0) + expect(editor.getDoubleWidthCharWidth()).toBe(10) + expect(editor.getHalfWidthCharWidth()).toBe(5) + }) + }) + + describe('hiding and showing the editor', function () { + describe('when the editor is hidden when it is mounted', function () { + it('defers measurement and rendering until the editor becomes visible', function () { + wrapperNode.remove() + let hiddenParent = document.createElement('div') + hiddenParent.style.display = 'none' + contentNode.appendChild(hiddenParent) + wrapperNode = new TextEditorElement() + wrapperNode.tileSize = TILE_SIZE + wrapperNode.initialize(editor, atom) + hiddenParent.appendChild(wrapperNode) + component = wrapperNode.component + componentNode = component.getDomNode() + expect(componentNode.querySelectorAll('.line').length).toBe(0) + hiddenParent.style.display = 'block' + atom.views.performDocumentPoll() + expect(componentNode.querySelectorAll('.line').length).toBeGreaterThan(0) + }) + }) + + describe('when the lineHeight changes while the editor is hidden', function () { + it('does not attempt to measure the lineHeightInPixels until the editor becomes visible again', function () { + wrapperNode.style.display = 'none' + component.checkForVisibilityChange() + let initialLineHeightInPixels = editor.getLineHeightInPixels() + component.setLineHeight(2) + expect(editor.getLineHeightInPixels()).toBe(initialLineHeightInPixels) + wrapperNode.style.display = '' + component.checkForVisibilityChange() + expect(editor.getLineHeightInPixels()).not.toBe(initialLineHeightInPixels) + }) + }) + + describe('when the fontSize changes while the editor is hidden', function () { + it('does not attempt to measure the lineHeightInPixels or defaultCharWidth until the editor becomes visible again', function () { + wrapperNode.style.display = 'none' + component.checkForVisibilityChange() + let initialLineHeightInPixels = editor.getLineHeightInPixels() + let initialCharWidth = editor.getDefaultCharWidth() + component.setFontSize(22) + expect(editor.getLineHeightInPixels()).toBe(initialLineHeightInPixels) + expect(editor.getDefaultCharWidth()).toBe(initialCharWidth) + wrapperNode.style.display = '' + component.checkForVisibilityChange() + expect(editor.getLineHeightInPixels()).not.toBe(initialLineHeightInPixels) + expect(editor.getDefaultCharWidth()).not.toBe(initialCharWidth) + }) + + it('does not re-measure character widths until the editor is shown again', async function () { + wrapperNode.style.display = 'none' + component.checkForVisibilityChange() + component.setFontSize(22) + editor.getBuffer().insert([0, 0], 'a') + wrapperNode.style.display = '' + component.checkForVisibilityChange() + editor.setCursorBufferPosition([0, Infinity]) + await atom.views.getNextUpdatePromise() + let cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left + let line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right + expect(cursorLeft).toBeCloseTo(line0Right, 0) + }) + }) + + describe('when the fontFamily changes while the editor is hidden', function () { + it('does not attempt to measure the defaultCharWidth until the editor becomes visible again', function () { + wrapperNode.style.display = 'none' + component.checkForVisibilityChange() + let initialLineHeightInPixels = editor.getLineHeightInPixels() + let initialCharWidth = editor.getDefaultCharWidth() + component.setFontFamily('serif') + expect(editor.getDefaultCharWidth()).toBe(initialCharWidth) + wrapperNode.style.display = '' + component.checkForVisibilityChange() + expect(editor.getDefaultCharWidth()).not.toBe(initialCharWidth) + }) + + it('does not re-measure character widths until the editor is shown again', async function () { + wrapperNode.style.display = 'none' + component.checkForVisibilityChange() + component.setFontFamily('serif') + wrapperNode.style.display = '' + component.checkForVisibilityChange() + editor.setCursorBufferPosition([0, Infinity]) + await atom.views.getNextUpdatePromise() + let cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left + let line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right + expect(cursorLeft).toBeCloseTo(line0Right, 0) + }) + }) + + describe('when stylesheets change while the editor is hidden', function () { + afterEach(function () { + atom.themes.removeStylesheet('test') + }) + + it('does not re-measure character widths until the editor is shown again', async function () { + atom.config.set('editor.fontFamily', 'sans-serif') + wrapperNode.style.display = 'none' + component.checkForVisibilityChange() + atom.themes.applyStylesheet('test', '.function.js {\n font-weight: bold;\n}') + wrapperNode.style.display = '' + component.checkForVisibilityChange() + editor.setCursorBufferPosition([0, Infinity]) + await atom.views.getNextUpdatePromise() + let cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left + let line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right + expect(cursorLeft).toBeCloseTo(line0Right, 0) + }) + }) + }) + + describe('soft wrapping', function () { + beforeEach(async function () { + editor.setSoftWrapped(true) + await atom.views.getNextUpdatePromise() + }) + + it('updates the wrap location when the editor is resized', async function () { + let newHeight = 4 * editor.getLineHeightInPixels() + 'px' + expect(parseInt(newHeight)).toBeLessThan(wrapperNode.offsetHeight) + wrapperNode.style.height = newHeight + await atom.views.getNextUpdatePromise() + + expect(componentNode.querySelectorAll('.line')).toHaveLength(7) + let gutterWidth = componentNode.querySelector('.gutter').offsetWidth + componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' + atom.views.performDocumentPoll() + await atom.views.getNextUpdatePromise() + expect(componentNode.querySelector('.line').textContent).toBe('var quicksort ') + }) + + it('accounts for the scroll view\'s padding when determining the wrap location', async function () { + let scrollViewNode = componentNode.querySelector('.scroll-view') + scrollViewNode.style.paddingLeft = 20 + 'px' + componentNode.style.width = 30 * charWidth + 'px' + atom.views.performDocumentPoll() + await atom.views.getNextUpdatePromise() + expect(component.lineNodeForScreenRow(0).textContent).toBe('var quicksort = ') + }) + }) + + describe('default decorations', function () { + it('applies .cursor-line decorations for line numbers overlapping selections', async function () { + editor.setCursorScreenPosition([4, 4]) + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(3, 'cursor-line')).toBe(false) + expect(lineNumberHasClass(4, 'cursor-line')).toBe(true) + expect(lineNumberHasClass(5, 'cursor-line')).toBe(false) + editor.setSelectedScreenRange([[3, 4], [4, 4]]) + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(3, 'cursor-line')).toBe(true) + expect(lineNumberHasClass(4, 'cursor-line')).toBe(true) + editor.setSelectedScreenRange([[3, 4], [4, 0]]) + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(3, 'cursor-line')).toBe(true) + expect(lineNumberHasClass(4, 'cursor-line')).toBe(false) + }) + + it('does not apply .cursor-line to the last line of a selection if it\'s empty', async function () { + editor.setSelectedScreenRange([[3, 4], [5, 0]]) + await atom.views.getNextUpdatePromise() + expect(lineNumberHasClass(3, 'cursor-line')).toBe(true) + expect(lineNumberHasClass(4, 'cursor-line')).toBe(true) + expect(lineNumberHasClass(5, 'cursor-line')).toBe(false) + }) + + it('applies .cursor-line decorations for lines containing the cursor in non-empty selections', async function () { + editor.setCursorScreenPosition([4, 4]) + await atom.views.getNextUpdatePromise() + + expect(lineHasClass(3, 'cursor-line')).toBe(false) + expect(lineHasClass(4, 'cursor-line')).toBe(true) + expect(lineHasClass(5, 'cursor-line')).toBe(false) + editor.setSelectedScreenRange([[3, 4], [4, 4]]) + await atom.views.getNextUpdatePromise() + + expect(lineHasClass(2, 'cursor-line')).toBe(false) + expect(lineHasClass(3, 'cursor-line')).toBe(false) + expect(lineHasClass(4, 'cursor-line')).toBe(false) + expect(lineHasClass(5, 'cursor-line')).toBe(false) + }) + + it('applies .cursor-line-no-selection to line numbers for rows containing the cursor when the selection is empty', async function () { + editor.setCursorScreenPosition([4, 4]) + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe(true) + editor.setSelectedScreenRange([[3, 4], [4, 4]]) + await atom.views.getNextUpdatePromise() + + expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe(false) + }) + }) + + describe('height', function () { + describe('when the wrapper view has an explicit height', function () { + it('does not assign a height on the component node', async function () { + wrapperNode.style.height = '200px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + expect(componentNode.style.height).toBe('') + }) + }) + + describe('when the wrapper view does not have an explicit height', function () { + it('assigns a height on the component node based on the editor\'s content', function () { + expect(wrapperNode.style.height).toBe('') + expect(componentNode.style.height).toBe(editor.getScreenLineCount() * lineHeightInPixels + 'px') + }) + }) + }) + + describe('when the "mini" property is true', function () { + beforeEach(async function () { + editor.setMini(true) + await atom.views.getNextUpdatePromise() + }) + + it('does not render the gutter', function () { + expect(componentNode.querySelector('.gutter')).toBeNull() + }) + + it('adds the "mini" class to the wrapper view', function () { + expect(wrapperNode.classList.contains('mini')).toBe(true) + }) + + it('does not have an opaque background on lines', function () { + expect(component.linesComponent.getDomNode().getAttribute('style')).not.toContain('background-color') + }) + + it('does not render invisible characters', function () { + atom.config.set('editor.invisibles', { + eol: 'E' + }) + atom.config.set('editor.showInvisibles', true) + expect(component.lineNodeForScreenRow(0).textContent).toBe('var quicksort = function () {') + }) + + it('does not assign an explicit line-height on the editor contents', function () { + expect(componentNode.style.lineHeight).toBe('') + }) + + it('does not apply cursor-line decorations', function () { + expect(component.lineNodeForScreenRow(0).classList.contains('cursor-line')).toBe(false) + }) + }) + + describe('when placholderText is specified', function () { + it('renders the placeholder text when the buffer is empty', async function () { + editor.setPlaceholderText('Hello World') + expect(componentNode.querySelector('.placeholder-text')).toBeNull() + editor.setText('') + await atom.views.getNextUpdatePromise() + + expect(componentNode.querySelector('.placeholder-text').textContent).toBe('Hello World') + editor.setText('hey') + await atom.views.getNextUpdatePromise() + + expect(componentNode.querySelector('.placeholder-text')).toBeNull() + }) + }) + + describe('grammar data attributes', function () { + it('adds and updates the grammar data attribute based on the current grammar', function () { + expect(wrapperNode.dataset.grammar).toBe('source js') + editor.setGrammar(atom.grammars.nullGrammar) + expect(wrapperNode.dataset.grammar).toBe('text plain null-grammar') + }) + }) + + describe('encoding data attributes', function () { + it('adds and updates the encoding data attribute based on the current encoding', function () { + expect(wrapperNode.dataset.encoding).toBe('utf8') + editor.setEncoding('utf16le') + expect(wrapperNode.dataset.encoding).toBe('utf16le') + }) + }) + + describe('detaching and reattaching the editor (regression)', function () { + it('does not throw an exception', function () { + wrapperNode.remove() + jasmine.attachToDOM(wrapperNode) + atom.commands.dispatch(wrapperNode, 'core:move-right') + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + }) + + describe('scoped config settings', function () { + let coffeeComponent, coffeeEditor + + beforeEach(async function () { + await atom.packages.activatePackage('language-coffee-script') + coffeeEditor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) + }) + + afterEach(function () { + atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('soft wrap settings', function () { + beforeEach(function () { + atom.config.set('editor.softWrap', true, { + scopeSelector: '.source.coffee' + }) + atom.config.set('editor.preferredLineLength', 17, { + scopeSelector: '.source.coffee' + }) + atom.config.set('editor.softWrapAtPreferredLineLength', true, { + scopeSelector: '.source.coffee' + }) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(20) + coffeeEditor.setDefaultCharWidth(1) + coffeeEditor.setEditorWidthInChars(20) + }) + + it('wraps lines when editor.softWrap is true for a matching scope', function () { + expect(editor.lineTextForScreenRow(2)).toEqual(' if (items.length <= 1) return items;') + expect(coffeeEditor.lineTextForScreenRow(3)).toEqual(' return items ') + }) + + it('updates the wrapped lines when editor.preferredLineLength changes', function () { + atom.config.set('editor.preferredLineLength', 20, { + scopeSelector: '.source.coffee' + }) + expect(coffeeEditor.lineTextForScreenRow(2)).toEqual(' return items if ') + }) + + it('updates the wrapped lines when editor.softWrapAtPreferredLineLength changes', function () { + atom.config.set('editor.softWrapAtPreferredLineLength', false, { + scopeSelector: '.source.coffee' + }) + expect(coffeeEditor.lineTextForScreenRow(2)).toEqual(' return items if ') + }) + + it('updates the wrapped lines when editor.softWrap changes', function () { + atom.config.set('editor.softWrap', false, { + scopeSelector: '.source.coffee' + }) + expect(coffeeEditor.lineTextForScreenRow(2)).toEqual(' return items if items.length <= 1') + atom.config.set('editor.softWrap', true, { + scopeSelector: '.source.coffee' + }) + expect(coffeeEditor.lineTextForScreenRow(3)).toEqual(' return items ') + }) + + it('updates the wrapped lines when the grammar changes', function () { + editor.setGrammar(coffeeEditor.getGrammar()) + expect(editor.isSoftWrapped()).toBe(true) + expect(editor.lineTextForScreenRow(0)).toEqual('var quicksort = ') + }) + + describe('::isSoftWrapped()', function () { + it('returns the correct value based on the scoped settings', function () { + expect(editor.isSoftWrapped()).toBe(false) + expect(coffeeEditor.isSoftWrapped()).toBe(true) + }) + }) + }) + + describe('invisibles settings', function () { + const jsInvisibles = { + eol: 'J', + space: 'A', + tab: 'V', + cr: 'A' + } + const coffeeInvisibles = { + eol: 'C', + space: 'O', + tab: 'F', + cr: 'E' + } + + beforeEach(async function () { + atom.config.set('editor.showInvisibles', true, { + scopeSelector: '.source.js' + }) + atom.config.set('editor.invisibles', jsInvisibles, { + scopeSelector: '.source.js' + }) + atom.config.set('editor.showInvisibles', false, { + scopeSelector: '.source.coffee' + }) + atom.config.set('editor.invisibles', coffeeInvisibles, { + scopeSelector: '.source.coffee' + }) + editor.setText(' a line with tabs\tand spaces \n') + await atom.views.getNextUpdatePromise() + }) + + it('renders the invisibles when editor.showInvisibles is true for a given grammar', function () { + expect(component.lineNodeForScreenRow(0).textContent).toBe('' + jsInvisibles.space + 'a line with tabs' + jsInvisibles.tab + 'and spaces' + jsInvisibles.space + jsInvisibles.eol) + }) + + it('does not render the invisibles when editor.showInvisibles is false for a given grammar', async function () { + editor.setGrammar(coffeeEditor.getGrammar()) + await atom.views.getNextUpdatePromise() + expect(component.lineNodeForScreenRow(0).textContent).toBe(' a line with tabs and spaces ') + }) + + it('re-renders the invisibles when the invisible settings change', async function () { + let jsGrammar = editor.getGrammar() + editor.setGrammar(coffeeEditor.getGrammar()) + atom.config.set('editor.showInvisibles', true, { + scopeSelector: '.source.coffee' + }) + await atom.views.getNextUpdatePromise() + + let newInvisibles = { + eol: 'N', + space: 'E', + tab: 'W', + cr: 'I' + } + + expect(component.lineNodeForScreenRow(0).textContent).toBe('' + coffeeInvisibles.space + 'a line with tabs' + coffeeInvisibles.tab + 'and spaces' + coffeeInvisibles.space + coffeeInvisibles.eol) + atom.config.set('editor.invisibles', newInvisibles, { + scopeSelector: '.source.coffee' + }) + await atom.views.getNextUpdatePromise() + + expect(component.lineNodeForScreenRow(0).textContent).toBe('' + newInvisibles.space + 'a line with tabs' + newInvisibles.tab + 'and spaces' + newInvisibles.space + newInvisibles.eol) + editor.setGrammar(jsGrammar) + await atom.views.getNextUpdatePromise() + + expect(component.lineNodeForScreenRow(0).textContent).toBe('' + jsInvisibles.space + 'a line with tabs' + jsInvisibles.tab + 'and spaces' + jsInvisibles.space + jsInvisibles.eol) + }) + }) + + describe('editor.showIndentGuide', function () { + beforeEach(async function () { + atom.config.set('editor.showIndentGuide', true, { + scopeSelector: '.source.js' + }) + atom.config.set('editor.showIndentGuide', false, { + scopeSelector: '.source.coffee' + }) + await atom.views.getNextUpdatePromise() + }) + + it('has an "indent-guide" class when scoped editor.showIndentGuide is true, but not when scoped editor.showIndentGuide is false', async function () { + let line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + expect(line1LeafNodes[0].textContent).toBe(' ') + expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(true) + expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false) + editor.setGrammar(coffeeEditor.getGrammar()) + await atom.views.getNextUpdatePromise() + + line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + expect(line1LeafNodes[0].textContent).toBe(' ') + expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(false) + expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false) + }) + + it('removes the "indent-guide" class when editor.showIndentGuide to false', async function () { + let line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + + expect(line1LeafNodes[0].textContent).toBe(' ') + expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(true) + expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false) + atom.config.set('editor.showIndentGuide', false, { + scopeSelector: '.source.js' + }) + await atom.views.getNextUpdatePromise() + + line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) + expect(line1LeafNodes[0].textContent).toBe(' ') + expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(false) + expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false) + }) + }) + }) + + describe('autoscroll', function () { + beforeEach(async function () { + editor.setVerticalScrollMargin(2) + editor.setHorizontalScrollMargin(2) + component.setLineHeight('10px') + component.setFontSize(17) + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + wrapperNode.setWidth(55) + wrapperNode.setHeight(55) + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + component.presenter.setHorizontalScrollbarHeight(0) + component.presenter.setVerticalScrollbarWidth(0) + await atom.views.getNextUpdatePromise() + }) + + describe('when selecting buffer ranges', function () { + it('autoscrolls the selection if it is last unless the "autoscroll" option is false', async function () { + expect(wrapperNode.getScrollTop()).toBe(0) + editor.setSelectedBufferRange([[5, 6], [6, 8]]) + await atom.views.getNextUpdatePromise() + + let right = wrapperNode.pixelPositionForBufferPosition([6, 8 + editor.getHorizontalScrollMargin()]).left + expect(wrapperNode.getScrollBottom()).toBe((7 + editor.getVerticalScrollMargin()) * 10) + expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) + editor.setSelectedBufferRange([[0, 0], [0, 0]]) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + expect(wrapperNode.getScrollLeft()).toBe(0) + editor.setSelectedBufferRange([[6, 6], [6, 8]]) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollBottom()).toBe((7 + editor.getVerticalScrollMargin()) * 10) + expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) + }) + }) + + describe('when adding selections for buffer ranges', function () { + it('autoscrolls to the added selection if needed', async function () { + editor.addSelectionForBufferRange([[8, 10], [8, 15]]) + await atom.views.getNextUpdatePromise() + + let right = wrapperNode.pixelPositionForBufferPosition([8, 15]).left + expect(wrapperNode.getScrollBottom()).toBe((9 * 10) + (2 * 10)) + expect(wrapperNode.getScrollRight()).toBeCloseTo(right + 2 * 10, 0) + }) + }) + + describe('when selecting lines containing cursors', function () { + it('autoscrolls to the selection', async function () { + editor.setCursorScreenPosition([5, 6]) + await atom.views.getNextUpdatePromise() + + wrapperNode.scrollToTop() + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + editor.selectLinesContainingCursors() + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollBottom()).toBe((7 + editor.getVerticalScrollMargin()) * 10) + }) + }) + + describe('when inserting text', function () { + describe('when there are multiple empty selections on different lines', function () { + it('autoscrolls to the last cursor', async function () { + editor.setCursorScreenPosition([1, 2], { + autoscroll: false + }) + await atom.views.getNextUpdatePromise() + + editor.addCursorAtScreenPosition([10, 4], { + autoscroll: false + }) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + editor.insertText('a') + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(75) + }) + }) + }) + + describe('when scrolled to cursor position', function () { + it('scrolls the last cursor into view, centering around the cursor if possible and the "center" option is not false', async function () { + editor.setCursorScreenPosition([8, 8], { + autoscroll: false + }) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + expect(wrapperNode.getScrollLeft()).toBe(0) + editor.scrollToCursorPosition() + await atom.views.getNextUpdatePromise() + + let right = wrapperNode.pixelPositionForScreenPosition([8, 9 + editor.getHorizontalScrollMargin()]).left + expect(wrapperNode.getScrollTop()).toBe((8.8 * 10) - 30) + expect(wrapperNode.getScrollBottom()).toBe((8.3 * 10) + 30) + expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) + wrapperNode.setScrollTop(0) + editor.scrollToCursorPosition({ + center: false + }) + expect(wrapperNode.getScrollTop()).toBe((7.8 - editor.getVerticalScrollMargin()) * 10) + expect(wrapperNode.getScrollBottom()).toBe((9.3 + editor.getVerticalScrollMargin()) * 10) + }) + }) + + describe('moving cursors', function () { + it('scrolls down when the last cursor gets closer than ::verticalScrollMargin to the bottom of the editor', async function () { + expect(wrapperNode.getScrollTop()).toBe(0) + expect(wrapperNode.getScrollBottom()).toBe(5.5 * 10) + editor.setCursorScreenPosition([2, 0]) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollBottom()).toBe(5.5 * 10) + editor.moveDown() + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollBottom()).toBe(6 * 10) + editor.moveDown() + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollBottom()).toBe(7 * 10) + }) + + it('scrolls up when the last cursor gets closer than ::verticalScrollMargin to the top of the editor', async function () { + editor.setCursorScreenPosition([11, 0]) + await atom.views.getNextUpdatePromise() + + wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) + await atom.views.getNextUpdatePromise() + + editor.moveUp() + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollBottom()).toBe(wrapperNode.getScrollHeight()) + editor.moveUp() + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(7 * 10) + editor.moveUp() + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(6 * 10) + }) + + it('scrolls right when the last cursor gets closer than ::horizontalScrollMargin to the right of the editor', async function () { + expect(wrapperNode.getScrollLeft()).toBe(0) + expect(wrapperNode.getScrollRight()).toBe(5.5 * 10) + editor.setCursorScreenPosition([0, 2]) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollRight()).toBe(5.5 * 10) + editor.moveRight() + await atom.views.getNextUpdatePromise() + + let margin = component.presenter.getHorizontalScrollMarginInPixels() + let right = wrapperNode.pixelPositionForScreenPosition([0, 4]).left + margin + expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) + editor.moveRight() + await atom.views.getNextUpdatePromise() + + right = wrapperNode.pixelPositionForScreenPosition([0, 5]).left + margin + expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) + }) + + it('scrolls left when the last cursor gets closer than ::horizontalScrollMargin to the left of the editor', async function () { + wrapperNode.setScrollRight(wrapperNode.getScrollWidth()) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollRight()).toBe(wrapperNode.getScrollWidth()) + editor.setCursorScreenPosition([6, 62], { + autoscroll: false + }) + await atom.views.getNextUpdatePromise() + + editor.moveLeft() + await atom.views.getNextUpdatePromise() + + let margin = component.presenter.getHorizontalScrollMarginInPixels() + let left = wrapperNode.pixelPositionForScreenPosition([6, 61]).left - margin + expect(wrapperNode.getScrollLeft()).toBeCloseTo(left, 0) + editor.moveLeft() + await atom.views.getNextUpdatePromise() + + left = wrapperNode.pixelPositionForScreenPosition([6, 60]).left - margin + expect(wrapperNode.getScrollLeft()).toBeCloseTo(left, 0) + }) + + it('scrolls down when inserting lines makes the document longer than the editor\'s height', async function () { + editor.setCursorScreenPosition([13, Infinity]) + editor.insertNewline() + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollBottom()).toBe(14 * 10) + editor.insertNewline() + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollBottom()).toBe(15 * 10) + }) + + it('autoscrolls to the cursor when it moves due to undo', async function () { + editor.insertText('abc') + wrapperNode.setScrollTop(Infinity) + await atom.views.getNextUpdatePromise() + + editor.undo() + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + }) + + it('does not scroll when the cursor moves into the visible area', async function () { + editor.setCursorBufferPosition([0, 0]) + await atom.views.getNextUpdatePromise() + + wrapperNode.setScrollTop(40) + await atom.views.getNextUpdatePromise() + + editor.setCursorBufferPosition([6, 0]) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(40) + }) + + it('honors the autoscroll option on cursor and selection manipulation methods', async function () { + expect(wrapperNode.getScrollTop()).toBe(0) + editor.addCursorAtScreenPosition([11, 11], {autoscroll: false}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + editor.addCursorAtBufferPosition([11, 11], {autoscroll: false}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + editor.setCursorScreenPosition([11, 11], {autoscroll: false}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + editor.setCursorBufferPosition([11, 11], {autoscroll: false}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + editor.addSelectionForBufferRange([[11, 11], [11, 11]], {autoscroll: false}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + editor.addSelectionForScreenRange([[11, 11], [11, 12]], {autoscroll: false}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + editor.setSelectedBufferRange([[11, 0], [11, 1]], {autoscroll: false}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + editor.setSelectedScreenRange([[11, 0], [11, 6]], {autoscroll: false}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + editor.clearSelections({autoscroll: false}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + editor.addSelectionForScreenRange([[0, 0], [0, 4]]) + await atom.views.getNextUpdatePromise() + + editor.getCursors()[0].setScreenPosition([11, 11], {autoscroll: true}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) + editor.getCursors()[0].setBufferPosition([0, 0], {autoscroll: true}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], {autoscroll: true}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) + editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], {autoscroll: true}) + await atom.views.getNextUpdatePromise() + + expect(wrapperNode.getScrollTop()).toBe(0) + }) + }) + }) + + describe('::getVisibleRowRange()', function () { + beforeEach(async function () { + wrapperNode.style.height = lineHeightInPixels * 8 + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + }) + + it('returns the first and the last visible rows', async function () { + component.setScrollTop(0) + await atom.views.getNextUpdatePromise() + expect(component.getVisibleRowRange()).toEqual([0, 9]) + }) + + it('ends at last buffer row even if there\'s more space available', async function () { + wrapperNode.style.height = lineHeightInPixels * 13 + 'px' + component.measureDimensions() + await atom.views.getNextUpdatePromise() + + component.setScrollTop(60) + await atom.views.getNextUpdatePromise() + + expect(component.getVisibleRowRange()).toEqual([0, 13]) + }) + }) + + describe('middle mouse paste on Linux', function () { + let originalPlatform + + beforeEach(function () { + originalPlatform = process.platform + Object.defineProperty(process, 'platform', { + value: 'linux' + }) + }) + + afterEach(function () { + Object.defineProperty(process, 'platform', { + value: originalPlatform + }) + }) + + it('pastes the previously selected text at the clicked location', async function () { + let clipboardWrittenTo = false + spyOn(require('ipc'), 'send').andCallFake(function (eventName, selectedText) { + if (eventName === 'write-text-to-selection-clipboard') { + require('../src/safe-clipboard').writeText(selectedText, 'selection') + clipboardWrittenTo = true + } + }) + atom.clipboard.write('') + component.trackSelectionClipboard() + editor.setSelectedBufferRange([[1, 6], [1, 10]]) + + await conditionPromise(function () { + return clipboardWrittenTo + }) + + componentNode.querySelector('.scroll-view').dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([10, 0]), { + button: 1 + })) + componentNode.querySelector('.scroll-view').dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenPosition([10, 0]), { + which: 2 + })) + expect(atom.clipboard.read()).toBe('sort') + expect(editor.lineTextForBufferRow(10)).toBe('sort') + }) + }) + + function buildMouseEvent (type, ...propertiesObjects) { + let properties = extend({ + bubbles: true, + cancelable: true + }, ...propertiesObjects) + + if (properties.detail == null) { + properties.detail = 1 + } + + let event = new MouseEvent(type, properties) + if (properties.which != null) { + Object.defineProperty(event, 'which', { + get: function () { + return properties.which + } + }) + } + if (properties.target != null) { + Object.defineProperty(event, 'target', { + get: function () { + return properties.target + } + }) + Object.defineProperty(event, 'srcObject', { + get: function () { + return properties.target + } + }) + } + return event + } + + function clientCoordinatesForScreenPosition (screenPosition) { + let clientX, clientY, positionOffset, scrollViewClientRect + positionOffset = wrapperNode.pixelPositionForScreenPosition(screenPosition) + scrollViewClientRect = componentNode.querySelector('.scroll-view').getBoundingClientRect() + clientX = scrollViewClientRect.left + positionOffset.left - wrapperNode.getScrollLeft() + clientY = scrollViewClientRect.top + positionOffset.top - wrapperNode.getScrollTop() + return { + clientX: clientX, + clientY: clientY + } + } + + function clientCoordinatesForScreenRowInGutter (screenRow) { + let clientX, clientY, gutterClientRect, positionOffset + positionOffset = wrapperNode.pixelPositionForScreenPosition([screenRow, Infinity]) + gutterClientRect = componentNode.querySelector('.gutter').getBoundingClientRect() + clientX = gutterClientRect.left + positionOffset.left - wrapperNode.getScrollLeft() + clientY = gutterClientRect.top + positionOffset.top - wrapperNode.getScrollTop() + return { + clientX: clientX, + clientY: clientY + } + } + + function lineAndLineNumberHaveClass (screenRow, klass) { + return lineHasClass(screenRow, klass) && lineNumberHasClass(screenRow, klass) + } + + function lineNumberHasClass (screenRow, klass) { + return component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) + } + + function lineNumberForBufferRowHasClass (bufferRow, klass) { + let screenRow + screenRow = editor.displayBuffer.screenRowForBufferRow(bufferRow) + return component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass) + } + + function lineHasClass (screenRow, klass) { + return component.lineNodeForScreenRow(screenRow).classList.contains(klass) + } + + function getLeafNodes (node) { + if (node.children.length > 0) { + return flatten(toArray(node.children).map(getLeafNodes)) + } else { + return [node] + } + } + + function conditionPromise (condition) { + let timeoutError = new Error("Timed out waiting on condition") + Error.captureStackTrace(timeoutError, conditionPromise) + + return new Promise(function (resolve, reject) { + let interval = window.setInterval(function () { + if (condition()) { + window.clearInterval(interval) + window.clearTimeout(timeout) + resolve() + } + }, 100) + let timeout = window.setTimeout(function () { + window.clearInterval(interval) + reject(timeoutError) + }, 3000) + }) + } + + function timeoutPromise (timeout) { + return new Promise(function (resolve) { + window.setTimeout(resolve, timeout) + }) + } + + function nextAnimationFramePromise () { + return new Promise(function (resolve) { + window.requestAnimationFrame(resolve) + }) + } + + function decorationsUpdatedPromise(editor) { + return new Promise(function (resolve) { + let disposable = editor.onDidUpdateDecorations(function () { + disposable.dispose() + resolve() + }) + }) + } +}) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index f5a7bd853..ceea3d4e2 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -1076,7 +1076,7 @@ class DisplayBuffer extends Model unless @didUpdateDecorationsEventScheduled @didUpdateDecorationsEventScheduled = true - process.nextTick => + global.setImmediate => @didUpdateDecorationsEventScheduled = false @emitter.emit 'did-update-decorations' diff --git a/src/view-registry.coffee b/src/view-registry.coffee index c21622c04..49ec29247 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -224,8 +224,10 @@ class ViewRegistry # process updates requested as a result of reads writer() while writer = @documentWriters.shift() + resolveNextUpdatePromise = @resolveNextUpdatePromise @nextUpdatePromise = null - @resolveNextUpdatePromise?() + @resolveNextUpdatePromise = null + resolveNextUpdatePromise?() startPollingDocument: -> window.addEventListener('resize', @requestDocumentPoll) From ff681752f7aeda2955f3f851c09c19574fb0fbd8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 7 Nov 2015 13:24:36 -0700 Subject: [PATCH 40/53] Add a nextViewUpdatePromise helper so we get stack traces from timeouts --- spec/text-editor-component-spec.js | 607 +++++++++++++++-------------- 1 file changed, 310 insertions(+), 297 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index bfd7646de..d22ca255b 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -41,7 +41,7 @@ describe('TextEditorComponent', function () { horizontalScrollbarNode = componentNode.querySelector('.horizontal-scrollbar') component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) afterEach(function () { @@ -53,14 +53,14 @@ describe('TextEditorComponent', function () { editor.insertNewline() component.presenter.startRow = -1 component.presenter.endRow = 9999 - await atom.views.getNextUpdatePromise() // assert an update does occur + await nextViewUpdatePromise() // assert an update does occur }) it('does not update when an animation frame was requested but the component got destroyed before its delivery', async function () { editor.setText('You should not see this update.') component.destroy() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).not.toBe('You should not see this update.') }) @@ -83,13 +83,13 @@ describe('TextEditorComponent', function () { let linesNode = componentNode.querySelector('.lines') wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) }) @@ -98,7 +98,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let tilesNodes = component.tileNodesForLines() expect(tilesNodes[0].style.zIndex).toBe('2') @@ -107,7 +107,7 @@ describe('TextEditorComponent', function () { verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() tilesNodes = component.tileNodesForLines() expect(tilesNodes[0].style.zIndex).toBe('3') @@ -120,7 +120,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let tilesNodes = component.tileNodesForLines() expect(tilesNodes.length).toBe(3) @@ -166,7 +166,7 @@ describe('TextEditorComponent', function () { verticalScrollbarNode.scrollTop = TILE_SIZE * lineHeightInPixels + 5 verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() tilesNodes = component.tileNodesForLines() expect(component.lineNodeForScreenRow(2)).toBeUndefined() @@ -214,7 +214,7 @@ describe('TextEditorComponent', function () { component.measureDimensions() editor.getBuffer().deleteRows(0, 1) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let tilesNodes = component.tileNodesForLines() expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') @@ -241,7 +241,7 @@ describe('TextEditorComponent', function () { editor.getBuffer().insert([0, 0], '\n\n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() tilesNodes = component.tileNodesForLines() expect(tilesNodes[0].style['-webkit-transform']).toBe('translate3d(0px, 0px, 0px)') @@ -282,22 +282,22 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() verticalScrollbarNode.scrollTop = 5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let buffer = editor.getBuffer() buffer.insert([0, 0], '\n\n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text) buffer.delete([[0, 0], [3, 0]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text) }) @@ -307,7 +307,7 @@ describe('TextEditorComponent', function () { component.setLineHeight(2) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let newLineHeightInPixels = editor.getLineHeightInPixels() expect(newLineHeightInPixels).not.toBe(initialLineHeightInPixels) @@ -318,7 +318,7 @@ describe('TextEditorComponent', function () { let initialLineHeightInPixels = editor.getLineHeightInPixels() component.setFontSize(10) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let newLineHeightInPixels = editor.getLineHeightInPixels() expect(newLineHeightInPixels).not.toBe(initialLineHeightInPixels) @@ -329,7 +329,7 @@ describe('TextEditorComponent', function () { editor.setText('') wrapperNode.style.height = '300px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let linesNode = componentNode.querySelector('.lines') expect(linesNode.offsetHeight).toBe(300) }) @@ -342,7 +342,7 @@ describe('TextEditorComponent', function () { componentNode.style.width = gutterWidth + (30 * charWidth) + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollWidth()).toBeGreaterThan(scrollViewNode.offsetWidth) let editorFullWidth = wrapperNode.getScrollWidth() + wrapperNode.getVerticalScrollbarWidth() @@ -353,7 +353,7 @@ describe('TextEditorComponent', function () { componentNode.style.width = gutterWidth + wrapperNode.getScrollWidth() + 100 + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let scrollViewWidth = scrollViewNode.offsetWidth for (let lineNode of lineNodes) { @@ -376,7 +376,7 @@ describe('TextEditorComponent', function () { } wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)' - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(linesNode.style.backgroundColor).toBe('rgb(255, 0, 0)') for (let tileNode of component.tileNodesForLines()) { @@ -387,14 +387,14 @@ describe('TextEditorComponent', function () { it('applies .leading-whitespace for lines with leading spaces and/or tabs', async function () { editor.setText(' a') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(true) expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(false) editor.setText('\ta') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(true) @@ -403,26 +403,26 @@ describe('TextEditorComponent', function () { it('applies .trailing-whitespace for lines with trailing spaces and/or tabs', async function () { editor.setText(' ') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) editor.setText('\t') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) editor.setText('a ') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) editor.setText('a\t') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) @@ -449,22 +449,22 @@ describe('TextEditorComponent', function () { beforeEach(async function () { atom.config.set('editor.showInvisibles', true) atom.config.set('editor.invisibles', invisibles) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('re-renders the lines when the showInvisibles config option changes', async function () { editor.setText(' a line with tabs\tand spaces \n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol) atom.config.set('editor.showInvisibles', false) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe(' a line with tabs and spaces ') atom.config.set('editor.showInvisibles', true) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol) }) @@ -472,7 +472,7 @@ describe('TextEditorComponent', function () { it('displays leading/trailing spaces, tabs, and newlines as visible characters', async function () { editor.setText(' a line with tabs\tand spaces \n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('' + invisibles.space + 'a line with tabs' + invisibles.tab + 'and spaces' + invisibles.space + invisibles.eol) @@ -483,13 +483,13 @@ describe('TextEditorComponent', function () { it('displays newlines as their own token outside of the other tokens\' scopeDescriptor', async function () { editor.setText('let\n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '') }) it('displays trailing carriage returns using a visible, non-empty value', async function () { editor.setText('a line that ends with a carriage return\r\n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('a line that ends with a carriage return' + invisibles.cr + invisibles.eol) }) @@ -501,7 +501,7 @@ describe('TextEditorComponent', function () { atom.config.set('editor.invisibles', { eol: '' }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP) }) @@ -509,32 +509,32 @@ describe('TextEditorComponent', function () { atom.config.set('editor.invisibles', { eol: false }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP) }) it('interleaves invisible line-ending characters with indent guides on empty lines', async function () { atom.config.set('editor.showIndentGuide', true) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() editor.setTextInBufferRange([[10, 0], [11, 0]], '\r\n', { normalizeLineEndings: false }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') editor.setTabLength(3) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ') editor.setTabLength(1) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ') editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') }) @@ -542,11 +542,11 @@ describe('TextEditorComponent', function () { beforeEach(async function () { editor.setText('a line that wraps \n') editor.setSoftWrapped(true) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('does not show end of line invisibles at the end of wrapped lines', function () { @@ -559,7 +559,7 @@ describe('TextEditorComponent', function () { describe('when indent guides are enabled', function () { beforeEach(async function () { atom.config.set('editor.showIndentGuide', true) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('adds an "indent-guide" class to spans comprising the leading whitespace', function () { @@ -578,7 +578,7 @@ describe('TextEditorComponent', function () { it('renders leading whitespace spans with the "indent-guide" class for empty lines', async function () { editor.getBuffer().insert([1, Infinity], '\n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe(2) @@ -590,7 +590,7 @@ describe('TextEditorComponent', function () { it('renders indent guides correctly on lines containing only whitespace', async function () { editor.getBuffer().insert([1, Infinity], '\n ') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe(3) @@ -610,7 +610,7 @@ describe('TextEditorComponent', function () { }) editor.getBuffer().insert([1, Infinity], '\n ') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe(4) @@ -626,7 +626,7 @@ describe('TextEditorComponent', function () { it('does not render indent guides in trailing whitespace for lines containing non whitespace characters', async function () { editor.getBuffer().setText(' hi ') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(line0LeafNodes[0].textContent).toBe(' ') @@ -637,10 +637,10 @@ describe('TextEditorComponent', function () { it('updates the indent guides on empty lines preceding an indentation change', async function () { editor.getBuffer().insert([12, 0], '\n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() editor.getBuffer().insert([13, 0], ' ') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let line12LeafNodes = getLeafNodes(component.lineNodeForScreenRow(12)) expect(line12LeafNodes[0].textContent).toBe(' ') @@ -652,10 +652,10 @@ describe('TextEditorComponent', function () { it('updates the indent guides on empty lines following an indentation change', async function () { editor.getBuffer().insert([12, 2], '\n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() editor.getBuffer().insert([12, 0], ' ') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let line13LeafNodes = getLeafNodes(component.lineNodeForScreenRow(13)) expect(line13LeafNodes[0].textContent).toBe(' ') @@ -673,7 +673,7 @@ describe('TextEditorComponent', function () { it('does not render indent guides on lines containing only whitespace', async function () { editor.getBuffer().insert([1, Infinity], '\n ') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe(3) @@ -689,7 +689,7 @@ describe('TextEditorComponent', function () { describe('when the buffer contains null bytes', function () { it('excludes the null byte from character measurement', async function () { editor.setText('a\0b') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.pixelPositionForScreenPosition([0, Infinity]).left).toEqual(2 * charWidth) }) }) @@ -700,13 +700,13 @@ describe('TextEditorComponent', function () { expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() editor.foldBufferRow(4) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() foldedLineNode = component.lineNodeForScreenRow(4) expect(foldedLineNode.querySelector('.fold-marker')).toBeTruthy() editor.unfoldBufferRow(4) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() foldedLineNode = component.lineNodeForScreenRow(4) expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() @@ -724,7 +724,7 @@ describe('TextEditorComponent', function () { it('renders higher tiles in front of lower ones', async function () { wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let tilesNodes = component.tileNodesForLineNumbers() expect(tilesNodes[0].style.zIndex).toBe('2') @@ -732,7 +732,7 @@ describe('TextEditorComponent', function () { expect(tilesNodes[2].style.zIndex).toBe('0') verticalScrollbarNode.scrollTop = 1 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() tilesNodes = component.tileNodesForLineNumbers() expect(tilesNodes[0].style.zIndex).toBe('3') @@ -746,13 +746,13 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(linesNode.getBoundingClientRect().height).toBe(6.5 * lineHeightInPixels) wrapperNode.style.height = 3.5 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(linesNode.getBoundingClientRect().height).toBe(3.5 * lineHeightInPixels) }) @@ -760,7 +760,7 @@ describe('TextEditorComponent', function () { it('renders the currently-visible line numbers in a tiled fashion', async function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let tilesNodes = component.tileNodesForLineNumbers() expect(tilesNodes.length).toBe(3) @@ -812,7 +812,7 @@ describe('TextEditorComponent', function () { verticalScrollbarNode.scrollTop = TILE_SIZE * lineHeightInPixels + 5 verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() tilesNodes = component.tileNodesForLineNumbers() expect(component.lineNumberNodeForScreenRow(2)).toBeUndefined() @@ -866,7 +866,7 @@ describe('TextEditorComponent', function () { it('updates the translation of subsequent line numbers when lines are inserted or removed', async function () { editor.getBuffer().insert([0, 0], '\n\n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let lineNumberNodes = componentNode.querySelectorAll('.line-number') expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe(0 * lineHeightInPixels) @@ -877,7 +877,7 @@ describe('TextEditorComponent', function () { expect(component.lineNumberNodeForScreenRow(5).offsetTop).toBe(2 * lineHeightInPixels) editor.getBuffer().insert([0, 0], '\n\n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNumberNodeForScreenRow(0).offsetTop).toBe(0 * lineHeightInPixels) expect(component.lineNumberNodeForScreenRow(1).offsetTop).toBe(1 * lineHeightInPixels) @@ -896,7 +896,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.width = 30 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelectorAll('.line-number').length).toBe(9 + 1) expect(component.lineNumberNodeForScreenRow(0).textContent).toBe('' + NBSP + '1') @@ -912,7 +912,7 @@ describe('TextEditorComponent', function () { it('pads line numbers to be right-justified based on the maximum number of line number digits', async function () { editor.getBuffer().setText([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].join('\n')) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() for (let screenRow = 0; screenRow <= 8; ++screenRow) { expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + (screenRow + 1)) @@ -922,7 +922,7 @@ describe('TextEditorComponent', function () { let initialGutterWidth = gutterNode.offsetWidth editor.getBuffer().delete([[1, 0], [2, 0]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() for (let screenRow = 0; screenRow <= 8; ++screenRow) { expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + (screenRow + 1)) @@ -930,7 +930,7 @@ describe('TextEditorComponent', function () { expect(gutterNode.offsetWidth).toBeLessThan(initialGutterWidth) editor.getBuffer().insert([0, 0], '\n\n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() for (let screenRow = 0; screenRow <= 8; ++screenRow) { expect(component.lineNumberNodeForScreenRow(screenRow).textContent).toBe('' + NBSP + (screenRow + 1)) @@ -942,7 +942,7 @@ describe('TextEditorComponent', function () { it('renders the .line-numbers div at the full height of the editor even if it\'s taller than its content', async function () { wrapperNode.style.height = componentNode.offsetHeight + 100 + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelector('.line-numbers').offsetHeight).toBe(componentNode.offsetHeight) }) @@ -957,7 +957,7 @@ describe('TextEditorComponent', function () { gutterNode.style.backgroundColor = 'rgb(255, 0, 0)' atom.views.performDocumentPoll() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumbersNode.style.backgroundColor).toBe('rgb(255, 0, 0)') for (let tileNode of component.tileNodesForLineNumbers()) { @@ -968,19 +968,19 @@ describe('TextEditorComponent', function () { it('hides or shows the gutter based on the "::isLineNumberGutterVisible" property on the model and the global "editor.showLineNumbers" config setting', async function () { expect(component.gutterContainerComponent.getLineNumberGutterComponent() != null).toBe(true) editor.setLineNumberGutterVisible(false) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelector('.gutter').style.display).toBe('none') atom.config.set('editor.showLineNumbers', false) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelector('.gutter').style.display).toBe('none') editor.setLineNumberGutterVisible(true) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelector('.gutter').style.display).toBe('none') atom.config.set('editor.showLineNumbers', true) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelector('.gutter').style.display).toBe('') expect(component.lineNumberNodeForScreenRow(3) != null).toBe(true) @@ -1008,7 +1008,7 @@ describe('TextEditorComponent', function () { it('updates the foldable class on the correct line numbers when the foldable positions change', async function () { editor.getBuffer().insert([0, 0], '\n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(0, 'foldable')).toBe(false) expect(lineNumberHasClass(1, 'foldable')).toBe(true) @@ -1022,27 +1022,27 @@ describe('TextEditorComponent', function () { it('updates the foldable class on a line number that becomes foldable', async function () { expect(lineNumberHasClass(11, 'foldable')).toBe(false) editor.getBuffer().insert([11, 44], '\n fold me') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(11, 'foldable')).toBe(true) editor.undo() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(11, 'foldable')).toBe(false) }) it('adds, updates and removes the folded class on the correct line number componentNodes', async function () { editor.foldBufferRow(4) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(4, 'folded')).toBe(true) editor.getBuffer().insert([0, 0], '\n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(4, 'folded')).toBe(false) expect(lineNumberHasClass(5, 'folded')).toBe(true) editor.unfoldBufferRow(5) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(5, 'folded')).toBe(false) }) @@ -1050,10 +1050,10 @@ describe('TextEditorComponent', function () { describe('when soft wrapping is enabled', function () { beforeEach(async function () { editor.setSoftWrapped(true) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('does not add the foldable class for soft-wrapped lines', function () { @@ -1094,14 +1094,14 @@ describe('TextEditorComponent', function () { target.dispatchEvent(buildClickEvent(target)) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(1, 'folded')).toBe(true) lineNumber = component.lineNumberNodeForScreenRow(1) target = lineNumber.querySelector('.icon-right') target.dispatchEvent(buildClickEvent(target)) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(1, 'folded')).toBe(false) }) @@ -1127,7 +1127,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe(1) @@ -1140,7 +1140,7 @@ describe('TextEditorComponent', function () { let cursor3 = editor.addCursorAtScreenPosition([4, 10], { autoscroll: false }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe(2) @@ -1149,11 +1149,11 @@ describe('TextEditorComponent', function () { expect(cursorNodes[1].style['-webkit-transform']).toBe('translate(' + (Math.round(10 * charWidth)) + 'px, ' + (4 * lineHeightInPixels) + 'px)') verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() horizontalScrollbarNode.scrollLeft = 3.5 * charWidth horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe(2) @@ -1163,12 +1163,12 @@ describe('TextEditorComponent', function () { cursor3.setScreenPosition([4, 11], { autoscroll: false }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)) + 'px, ' + (4 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px)') expect(cursorMovedListener).toHaveBeenCalled() cursor3.destroy() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe(1) @@ -1178,7 +1178,7 @@ describe('TextEditorComponent', function () { it('accounts for character widths when positioning cursors', async function () { atom.config.set('editor.fontFamily', 'sans-serif') editor.setCursorScreenPosition([0, 16]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursor = componentNode.querySelector('.cursor') let cursorRect = cursor.getBoundingClientRect() @@ -1195,7 +1195,7 @@ describe('TextEditorComponent', function () { atom.config.set('editor.fontFamily', 'sans-serif') editor.setText('he\u0301y') editor.setCursorBufferPosition([0, 3]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursor = componentNode.querySelector('.cursor') let cursorRect = cursor.getBoundingClientRect() @@ -1211,12 +1211,12 @@ describe('TextEditorComponent', function () { it('positions cursors correctly after character widths are changed via a stylesheet change', async function () { atom.config.set('editor.fontFamily', 'sans-serif') editor.setCursorScreenPosition([0, 16]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() atom.styles.addStyleSheet('.function.js {\n font-weight: bold;\n}', { context: 'atom-text-editor' }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursor = componentNode.querySelector('.cursor') let cursorRect = cursor.getBoundingClientRect() @@ -1232,14 +1232,14 @@ describe('TextEditorComponent', function () { it('sets the cursor to the default character width at the end of a line', async function () { editor.setCursorScreenPosition([0, Infinity]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursorNode = componentNode.querySelector('.cursor') expect(cursorNode.offsetWidth).toBeCloseTo(charWidth, 0) }) it('gives the cursor a non-zero width even if it\'s inside atomic tokens', async function () { editor.setCursorScreenPosition([1, 0]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursorNode = componentNode.querySelector('.cursor') expect(cursorNode.offsetWidth).toBeCloseTo(charWidth, 0) }) @@ -1247,7 +1247,7 @@ describe('TextEditorComponent', function () { it('blinks cursors when they are not moving', async function () { let cursorsNode = componentNode.querySelector('.cursors') wrapperNode.focus() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(cursorsNode.classList.contains('blink-off')).toBe(false) await conditionPromise(function () { return cursorsNode.classList.contains('blink-off') @@ -1256,7 +1256,7 @@ describe('TextEditorComponent', function () { return !cursorsNode.classList.contains('blink-off') }) editor.moveRight() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(cursorsNode.classList.contains('blink-off')).toBe(false) await conditionPromise(function () { return cursorsNode.classList.contains('blink-off') @@ -1266,7 +1266,7 @@ describe('TextEditorComponent', function () { it('does not render cursors that are associated with non-empty selections', async function () { editor.setSelectedScreenRange([[0, 4], [4, 6]]) editor.addCursorAtScreenPosition([6, 8]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe(1) expect(cursorNodes[0].style['-webkit-transform']).toBe('translate(' + (Math.round(8 * charWidth)) + 'px, ' + (6 * lineHeightInPixels) + 'px)') @@ -1275,7 +1275,7 @@ describe('TextEditorComponent', function () { it('updates cursor positions when the line height changes', async function () { editor.setCursorBufferPosition([1, 10]) component.setLineHeight(2) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursorNode = componentNode.querySelector('.cursor') expect(cursorNode.style['-webkit-transform']).toBe('translate(' + (Math.round(10 * editor.getDefaultCharWidth())) + 'px, ' + (editor.getLineHeightInPixels()) + 'px)') }) @@ -1283,7 +1283,7 @@ describe('TextEditorComponent', function () { it('updates cursor positions when the font size changes', async function () { editor.setCursorBufferPosition([1, 10]) component.setFontSize(10) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursorNode = componentNode.querySelector('.cursor') expect(cursorNode.style['-webkit-transform']).toBe('translate(' + (Math.round(10 * editor.getDefaultCharWidth())) + 'px, ' + (editor.getLineHeightInPixels()) + 'px)') }) @@ -1291,7 +1291,7 @@ describe('TextEditorComponent', function () { it('updates cursor positions when the font family changes', async function () { editor.setCursorBufferPosition([1, 10]) component.setFontFamily('sans-serif') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursorNode = componentNode.querySelector('.cursor') let left = wrapperNode.pixelPositionForScreenPosition([1, 10]).left expect(cursorNode.style['-webkit-transform']).toBe('translate(' + (Math.round(left)) + 'px, ' + (editor.getLineHeightInPixels()) + 'px)') @@ -1308,7 +1308,7 @@ describe('TextEditorComponent', function () { it('renders 1 region for 1-line selections', async function () { editor.setSelectedScreenRange([[1, 6], [1, 10]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let regions = componentNode.querySelectorAll('.selection .region') expect(regions.length).toBe(1) @@ -1322,7 +1322,7 @@ describe('TextEditorComponent', function () { it('renders 2 regions for 2-line selections', async function () { editor.setSelectedScreenRange([[1, 6], [2, 10]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let tileNode = component.tileNodesForLines()[0] let regions = tileNode.querySelectorAll('.selection .region') @@ -1343,7 +1343,7 @@ describe('TextEditorComponent', function () { it('renders 3 regions per tile for selections with more than 2 lines', async function () { editor.setSelectedScreenRange([[0, 6], [5, 10]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let region1Rect, region2Rect, region3Rect, regions, tileNode tileNode = component.tileNodesForLines()[0] @@ -1393,7 +1393,7 @@ describe('TextEditorComponent', function () { it('does not render empty selections', async function () { editor.addSelectionForBufferRange([[2, 2], [2, 2]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.getSelections()[0].isEmpty()).toBe(true) expect(editor.getSelections()[1].isEmpty()).toBe(true) expect(componentNode.querySelectorAll('.selection').length).toBe(0) @@ -1402,7 +1402,7 @@ describe('TextEditorComponent', function () { it('updates selections when the line height changes', async function () { editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setLineHeight(2) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let selectionNode = componentNode.querySelector('.region') expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels()) }) @@ -1411,7 +1411,7 @@ describe('TextEditorComponent', function () { editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontSize(10) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let selectionNode = componentNode.querySelector('.region') expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels()) @@ -1422,7 +1422,7 @@ describe('TextEditorComponent', function () { editor.setSelectedBufferRange([[1, 6], [1, 10]]) component.setFontFamily('sans-serif') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let selectionNode = componentNode.querySelector('.region') expect(selectionNode.offsetTop).toBe(editor.getLineHeightInPixels()) @@ -1433,7 +1433,7 @@ describe('TextEditorComponent', function () { editor.setSelectedBufferRange([[1, 6], [1, 10]], { flash: true }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let selectionNode = componentNode.querySelector('.selection') expect(selectionNode.classList.contains('flash')).toBe(true) @@ -1445,7 +1445,7 @@ describe('TextEditorComponent', function () { editor.setSelectedBufferRange([[1, 5], [1, 7]], { flash: true }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(selectionNode.classList.contains('flash')).toBe(true) }) @@ -1465,7 +1465,7 @@ describe('TextEditorComponent', function () { 'class': 'a' }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('applies line decoration classes to lines and line numbers', async function () { @@ -1473,7 +1473,7 @@ describe('TextEditorComponent', function () { expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]]) editor.decorateMarker(marker2, { @@ -1481,16 +1481,16 @@ describe('TextEditorComponent', function () { 'class': 'b' }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(9, 'b')).toBe(true) editor.foldBufferRow(5) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(9, 'b')).toBe(false) expect(lineAndLineNumberHaveClass(6, 'b')).toBe(true) @@ -1502,7 +1502,7 @@ describe('TextEditorComponent', function () { componentNode.style.width = 16 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() marker.destroy() marker = editor.markBufferRange([[0, 0], [0, 2]]) editor.decorateMarker(marker, { @@ -1510,13 +1510,13 @@ describe('TextEditorComponent', function () { 'class': 'b' }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(0, 'b')).toBe(true) expect(lineNumberHasClass(1, 'b')).toBe(false) marker.setBufferRange([[0, 0], [0, Infinity]]) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(0, 'b')).toBe(true) expect(lineNumberHasClass(1, 'b')).toBe(true) @@ -1530,7 +1530,7 @@ describe('TextEditorComponent', function () { editor.getBuffer().insert([0, 0], '\n') await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(2, 'a')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'a')).toBe(true) @@ -1539,7 +1539,7 @@ describe('TextEditorComponent', function () { marker.setBufferRange([[4, 4], [6, 4]]) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(2, 'a')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'a')).toBe(false) @@ -1552,7 +1552,7 @@ describe('TextEditorComponent', function () { it('remove decoration classes when decorations are removed', async function () { decoration.destroy() await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(1, 'a')).toBe(false) expect(lineNumberHasClass(2, 'a')).toBe(false) expect(lineNumberHasClass(3, 'a')).toBe(false) @@ -1562,7 +1562,7 @@ describe('TextEditorComponent', function () { it('removes decorations when their marker is invalidated', async function () { editor.getBuffer().insert([3, 2], 'n') await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(marker.isValid()).toBe(false) expect(lineAndLineNumberHaveClass(1, 'a')).toBe(false) @@ -1571,7 +1571,7 @@ describe('TextEditorComponent', function () { expect(lineAndLineNumberHaveClass(4, 'a')).toBe(false) editor.undo() await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(marker.isValid()).toBe(true) expect(lineAndLineNumberHaveClass(1, 'a')).toBe(false) @@ -1583,7 +1583,7 @@ describe('TextEditorComponent', function () { it('removes decorations when their marker is destroyed', async function () { marker.destroy() await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(1, 'a')).toBe(false) expect(lineNumberHasClass(2, 'a')).toBe(false) expect(lineNumberHasClass(3, 'a')).toBe(false) @@ -1598,7 +1598,7 @@ describe('TextEditorComponent', function () { onlyHead: true }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(1, 'only-head')).toBe(false) expect(lineAndLineNumberHaveClass(2, 'only-head')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'only-head')).toBe(true) @@ -1614,14 +1614,14 @@ describe('TextEditorComponent', function () { onlyEmpty: true }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe(false) marker.clearTail() await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(2, 'only-empty')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'only-empty')).toBe(true) @@ -1636,14 +1636,14 @@ describe('TextEditorComponent', function () { onlyNonEmpty: true }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe(true) expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe(true) marker.clearTail() await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineAndLineNumberHaveClass(2, 'only-non-empty')).toBe(false) expect(lineAndLineNumberHaveClass(3, 'only-non-empty')).toBe(false) @@ -1666,13 +1666,13 @@ describe('TextEditorComponent', function () { 'class': 'test-highlight' }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('does not render highlights for off-screen lines until they come on-screen', async function () { wrapperNode.style.height = 2.5 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], { invalidate: 'inside' @@ -1682,14 +1682,14 @@ describe('TextEditorComponent', function () { 'class': 'some-highlight' }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.presenter.endRow).toBeLessThan(9) let regions = componentNode.querySelectorAll('.some-highlight .region') expect(regions.length).toBe(0) verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.presenter.endRow).toBeGreaterThan(8) regions = componentNode.querySelectorAll('.some-highlight .region') @@ -1709,7 +1709,7 @@ describe('TextEditorComponent', function () { it('removes highlights when a decoration is removed', async function () { decoration.destroy() await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe(0) }) @@ -1717,14 +1717,14 @@ describe('TextEditorComponent', function () { it('does not render a highlight that is within a fold', async function () { editor.foldBufferRow(1) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelectorAll('.test-highlight').length).toBe(0) }) it('removes highlights when a decoration\'s marker is destroyed', async function () { marker.destroy() await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe(0) }) @@ -1732,14 +1732,14 @@ describe('TextEditorComponent', function () { it('only renders highlights when a decoration\'s marker is valid', async function () { editor.getBuffer().insert([3, 2], 'n') await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(marker.isValid()).toBe(false) let regions = componentNode.querySelectorAll('.test-highlight .region') expect(regions.length).toBe(0) editor.getBuffer().undo() await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(marker.isValid()).toBe(true) regions = componentNode.querySelectorAll('.test-highlight .region') @@ -1752,14 +1752,14 @@ describe('TextEditorComponent', function () { 'class': 'foo bar' }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelectorAll('.foo.bar').length).toBe(2) decoration.setProperties({ type: 'highlight', 'class': 'bar baz' }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelectorAll('.bar.baz').length).toBe(2) }) @@ -1770,7 +1770,7 @@ describe('TextEditorComponent', function () { deprecatedRegionClass: 'test-highlight-region' }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let regions = componentNode.querySelectorAll('.test-highlight .region.test-highlight-region') expect(regions.length).toBe(2) }) @@ -1786,7 +1786,7 @@ describe('TextEditorComponent', function () { expect(highlightNode.classList.contains('flash-class')).toBe(false) decoration.flash('flash-class', 10) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(highlightNode.classList.contains('flash-class')).toBe(true) await conditionPromise(function () { @@ -1796,16 +1796,14 @@ describe('TextEditorComponent', function () { describe('when ::flash is called again before the first has finished', function () { it('removes the class from the decoration highlight before adding it for the second ::flash call', async function () { - decoration.flash('flash-class', 100) + decoration.flash('flash-class', 500) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(highlightNode.classList.contains('flash-class')).toBe(true) - await timeoutPromise(2) - - decoration.flash('flash-class', 100) + decoration.flash('flash-class', 500) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(highlightNode.classList.contains('flash-class')).toBe(false) @@ -1824,7 +1822,7 @@ describe('TextEditorComponent', function () { editor.getBuffer().insert([0, 0], '\n') await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() regionStyle = componentNode.querySelector('.test-highlight .region').style let newTop = parseInt(regionStyle.top) @@ -1837,7 +1835,7 @@ describe('TextEditorComponent', function () { marker.setBufferRange([[5, 8], [5, 13]]) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() regionStyle = componentNode.querySelector('.test-highlight .region').style expect(parseInt(regionStyle.top)).toBe(2 * lineHeightInPixels) @@ -1852,7 +1850,7 @@ describe('TextEditorComponent', function () { 'class': 'new-test-highlight' }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelector('.test-highlight')).toBeFalsy() expect(componentNode.querySelector('.new-test-highlight')).toBeTruthy() }) @@ -1879,14 +1877,14 @@ describe('TextEditorComponent', function () { item: item }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') expect(overlay).toBe(item) decoration.destroy() await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test') expect(overlay).toBe(null) @@ -1903,7 +1901,7 @@ describe('TextEditorComponent', function () { }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let overlay = component.getTopmostDOMNode().querySelector('atom-overlay.my-overlay') expect(overlay).not.toBe(null) @@ -1923,7 +1921,7 @@ describe('TextEditorComponent', function () { }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let position = wrapperNode.pixelPositionForBufferPosition([2, 10]) let overlay = component.getTopmostDOMNode().querySelector('atom-overlay') @@ -1951,7 +1949,7 @@ describe('TextEditorComponent', function () { }) component.measureDimensions() component.measureWindowSize() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) afterEach(function () { @@ -1967,7 +1965,7 @@ describe('TextEditorComponent', function () { item: item }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let position = wrapperNode.pixelPositionForBufferPosition([0, 26]) let overlay = component.getTopmostDOMNode().querySelector('atom-overlay') @@ -1976,14 +1974,14 @@ describe('TextEditorComponent', function () { editor.insertText('a') await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(overlay.style.left).toBe(windowWidth - itemWidth + 'px') expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') editor.insertText('b') await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(overlay.style.left).toBe(windowWidth - itemWidth + 'px') expect(overlay.style.top).toBe(position.top + editor.getLineHeightInPixels() + 'px') @@ -1998,13 +1996,13 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 5 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.getCursorScreenPosition()).toEqual([0, 0]) wrapperNode.setScrollTop(3 * lineHeightInPixels) wrapperNode.setScrollLeft(3 * charWidth) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(inputNode.offsetTop).toBe(0) expect(inputNode.offsetLeft).toBe(0) @@ -2013,19 +2011,19 @@ describe('TextEditorComponent', function () { autoscroll: false }) await decorationsUpdatedPromise(editor) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(inputNode.offsetTop).toBe(0) expect(inputNode.offsetLeft).toBe(0) wrapperNode.focus() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(inputNode.offsetTop).toBe((5 * lineHeightInPixels) - wrapperNode.getScrollTop()) expect(inputNode.offsetLeft).toBeCloseTo((4 * charWidth) - wrapperNode.getScrollLeft(), 0) inputNode.blur() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(inputNode.offsetTop).toBe(0) expect(inputNode.offsetLeft).toBe(0) @@ -2033,13 +2031,13 @@ describe('TextEditorComponent', function () { editor.setCursorBufferPosition([1, 2], { autoscroll: false }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(inputNode.offsetTop).toBe(0) expect(inputNode.offsetLeft).toBe(0) inputNode.focus() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(inputNode.offsetTop).toBe(0) expect(inputNode.offsetLeft).toBe(0) @@ -2062,13 +2060,13 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = height + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let coordinates = clientCoordinatesForScreenPosition([0, 2]) coordinates.clientY = -1 linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.getCursorScreenPosition()).toEqual([0, 0]) }) }) @@ -2081,13 +2079,13 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = height + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let coordinates = clientCoordinatesForScreenPosition([0, 2]) coordinates.clientY = height * 2 linesNode.dispatchEvent(buildMouseEvent('mousedown', coordinates)) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.getCursorScreenPosition()).toEqual([0, 3]) }) @@ -2101,9 +2099,9 @@ describe('TextEditorComponent', function () { component.measureDimensions() wrapperNode.setScrollTop(3.5 * lineHeightInPixels) wrapperNode.setScrollLeft(2 * charWidth) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]))) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.getCursorScreenPosition()).toEqual([4, 8]) }) }) @@ -2114,7 +2112,7 @@ describe('TextEditorComponent', function () { linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), { shiftKey: true })) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.getSelectedScreenRange()).toEqual([[3, 4], [5, 6]]) }) }) @@ -2126,7 +2124,7 @@ describe('TextEditorComponent', function () { linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), { metaKey: true })) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.getSelectedScreenRanges()).toEqual([[[3, 4], [3, 4]], [[5, 6], [5, 6]]]) }) }) @@ -2139,7 +2137,7 @@ describe('TextEditorComponent', function () { linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), { metaKey: true })) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.getSelectedScreenRanges()).toEqual([[[5, 2], [5, 2]], [[7, 5], [7, 5]]]) }) }) @@ -2150,7 +2148,7 @@ describe('TextEditorComponent', function () { linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([3, 4]), { metaKey: true })) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.getSelectedScreenRanges()).toEqual([[[3, 4], [3, 4]]]) }) }) @@ -2284,7 +2282,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = '100px' wrapperNode.style.width = '100px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) expect(wrapperNode.getScrollLeft()).toBe(0) @@ -2470,7 +2468,7 @@ describe('TextEditorComponent', function () { jasmine.attachToDOM(wrapperNode) wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 1 @@ -2506,7 +2504,7 @@ describe('TextEditorComponent', function () { jasmine.attachToDOM(wrapperNode) wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), { detail: 1 @@ -2543,7 +2541,7 @@ describe('TextEditorComponent', function () { describe('when a line is folded', function () { beforeEach(async function () { editor.foldBufferRow(4) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) describe('when the folded line\'s fold-marker is clicked', function () { @@ -2662,7 +2660,7 @@ describe('TextEditorComponent', function () { it('autoscrolls when the cursor approaches the top or bottom of the editor', async function () { wrapperNode.style.height = 6 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) @@ -2842,10 +2840,10 @@ describe('TextEditorComponent', function () { beforeEach(async function () { gutterNode = componentNode.querySelector('.gutter') editor.setSoftWrapped(true) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() componentNode.style.width = 21 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) describe('when the gutter is clicked', function () { @@ -3053,12 +3051,12 @@ describe('TextEditorComponent', function () { it('adds the "is-focused" class to the editor when the hidden input is focused', async function () { expect(document.activeElement).toBe(document.body) inputNode.focus() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.classList.contains('is-focused')).toBe(true) expect(wrapperNode.classList.contains('is-focused')).toBe(true) inputNode.blur() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.classList.contains('is-focused')).toBe(false) expect(wrapperNode.classList.contains('is-focused')).toBe(false) @@ -3070,16 +3068,16 @@ describe('TextEditorComponent', function () { beforeEach(async function () { editor.setCursorScreenPosition([0, 0]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('adds the "has-selection" class to the editor when there is a selection', async function () { expect(componentNode.classList.contains('has-selection')).toBe(false) editor.selectDown() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.classList.contains('has-selection')).toBe(true) editor.moveDown() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.classList.contains('has-selection')).toBe(false) }) }) @@ -3088,17 +3086,17 @@ describe('TextEditorComponent', function () { it('updates the vertical scrollbar when the scrollTop is changed in the model', async function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(verticalScrollbarNode.scrollTop).toBe(0) wrapperNode.setScrollTop(10) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(verticalScrollbarNode.scrollTop).toBe(10) }) it('updates the horizontal scrollbar and the x transform of the lines based on the scrollLeft of the model', async function () { componentNode.style.width = 30 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let top = 0 let tilesNodes = component.tileNodesForLines() @@ -3109,7 +3107,7 @@ describe('TextEditorComponent', function () { expect(horizontalScrollbarNode.scrollLeft).toBe(0) wrapperNode.setScrollLeft(100) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() top = 0 for (let tileNode of tilesNodes) { @@ -3122,11 +3120,11 @@ describe('TextEditorComponent', function () { it('updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes', async function () { componentNode.style.width = 30 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollLeft()).toBe(0) horizontalScrollbarNode.scrollLeft = 100 horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollLeft()).toBe(100) }) @@ -3135,7 +3133,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let lastLineNode = component.lineNodeForScreenRow(editor.getLastScreenRow()) let bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom @@ -3143,7 +3141,7 @@ describe('TextEditorComponent', function () { expect(bottomOfLastLine).toBe(topOfHorizontalScrollbar) wrapperNode.style.width = 100 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() bottomOfLastLine = lastLineNode.getBoundingClientRect().bottom let bottomOfEditor = componentNode.getBoundingClientRect().bottom @@ -3156,7 +3154,7 @@ describe('TextEditorComponent', function () { component.measureDimensions() wrapperNode.setScrollLeft(Infinity) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let rightOfLongestLine = component.lineNodeForScreenRow(6).querySelector('.line > span:last-child').getBoundingClientRect().right let leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left expect(Math.round(rightOfLongestLine)).toBeCloseTo(leftOfVerticalScrollbar - 1, 0) @@ -3168,19 +3166,19 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = '1000px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(verticalScrollbarNode.style.display).toBe('') expect(horizontalScrollbarNode.style.display).toBe('none') componentNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(verticalScrollbarNode.style.display).toBe('') expect(horizontalScrollbarNode.style.display).toBe('') wrapperNode.style.height = 20 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(verticalScrollbarNode.style.display).toBe('none') expect(horizontalScrollbarNode.style.display).toBe('') @@ -3190,7 +3188,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 4 * lineHeightInPixels + 'px' wrapperNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() atom.styles.addStyleSheet('::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}', { context: 'atom-text-editor' @@ -3214,21 +3212,21 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = '1000px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(verticalScrollbarNode.style.bottom).toBe('0px') expect(horizontalScrollbarNode.style.right).toBe(verticalScrollbarNode.offsetWidth + 'px') expect(scrollbarCornerNode.style.display).toBe('none') componentNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(verticalScrollbarNode.style.bottom).toBe(horizontalScrollbarNode.offsetHeight + 'px') expect(horizontalScrollbarNode.style.right).toBe(verticalScrollbarNode.offsetWidth + 'px') expect(scrollbarCornerNode.style.display).toBe('') wrapperNode.style.height = 20 * lineHeightInPixels + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(verticalScrollbarNode.style.bottom).toBe(horizontalScrollbarNode.offsetHeight + 'px') expect(horizontalScrollbarNode.style.right).toBe('0px') @@ -3239,7 +3237,7 @@ describe('TextEditorComponent', function () { let gutterNode = componentNode.querySelector('.gutter') componentNode.style.width = 10 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(horizontalScrollbarNode.scrollWidth).toBe(wrapperNode.getScrollWidth()) expect(horizontalScrollbarNode.style.left).toBe('0px') @@ -3256,7 +3254,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('updates the scrollLeft or scrollTop on mousewheel events depending on which delta is greater (x or y)', async function () { @@ -3325,7 +3323,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let lineNode = componentNode.querySelector('.line') let wheelEvent = new WheelEvent('mousewheel', { @@ -3347,7 +3345,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let lineNode = componentNode.querySelector('.line') let wheelEvent = new WheelEvent('mousewheel', { @@ -3414,7 +3412,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let lineNumberNode = componentNode.querySelectorAll('.line-number')[1] let wheelEvent = new WheelEvent('mousewheel', { @@ -3438,7 +3436,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() componentNode.dispatchEvent(new WheelEvent('mousewheel', { wheelDeltaX: 0, @@ -3508,7 +3506,7 @@ describe('TextEditorComponent', function () { data: 'x', target: inputNode })) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.lineTextForBufferRow(0)).toBe('xvar quicksort = function () {') componentNode.dispatchEvent(buildTextInputEvent({ @@ -3524,7 +3522,7 @@ describe('TextEditorComponent', function () { data: 'u', target: inputNode })) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.lineTextForBufferRow(0)).toBe('uvar quicksort = function () {') inputNode.setSelectionRange(0, 1) @@ -3532,7 +3530,7 @@ describe('TextEditorComponent', function () { data: 'ü', target: inputNode })) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.lineTextForBufferRow(0)).toBe('üvar quicksort = function () {') }) @@ -3762,7 +3760,7 @@ describe('TextEditorComponent', function () { it('measures the default char, the korean char, the double width char and the half width char widths', async function () { expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0) component.setFontSize(10) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0) expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0) expect(editor.getDoubleWidthCharWidth()).toBe(10) @@ -3826,7 +3824,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.display = '' component.checkForVisibilityChange() editor.setCursorBufferPosition([0, Infinity]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left let line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right expect(cursorLeft).toBeCloseTo(line0Right, 0) @@ -3853,7 +3851,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.display = '' component.checkForVisibilityChange() editor.setCursorBufferPosition([0, Infinity]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left let line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right expect(cursorLeft).toBeCloseTo(line0Right, 0) @@ -3873,7 +3871,7 @@ describe('TextEditorComponent', function () { wrapperNode.style.display = '' component.checkForVisibilityChange() editor.setCursorBufferPosition([0, Infinity]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left let line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right expect(cursorLeft).toBeCloseTo(line0Right, 0) @@ -3884,20 +3882,20 @@ describe('TextEditorComponent', function () { describe('soft wrapping', function () { beforeEach(async function () { editor.setSoftWrapped(true) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('updates the wrap location when the editor is resized', async function () { let newHeight = 4 * editor.getLineHeightInPixels() + 'px' expect(parseInt(newHeight)).toBeLessThan(wrapperNode.offsetHeight) wrapperNode.style.height = newHeight - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelectorAll('.line')).toHaveLength(7) let gutterWidth = componentNode.querySelector('.gutter').offsetWidth componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' atom.views.performDocumentPoll() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelector('.line').textContent).toBe('var quicksort ') }) @@ -3906,7 +3904,7 @@ describe('TextEditorComponent', function () { scrollViewNode.style.paddingLeft = 20 + 'px' componentNode.style.width = 30 * charWidth + 'px' atom.views.performDocumentPoll() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('var quicksort = ') }) }) @@ -3914,18 +3912,18 @@ describe('TextEditorComponent', function () { describe('default decorations', function () { it('applies .cursor-line decorations for line numbers overlapping selections', async function () { editor.setCursorScreenPosition([4, 4]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(3, 'cursor-line')).toBe(false) expect(lineNumberHasClass(4, 'cursor-line')).toBe(true) expect(lineNumberHasClass(5, 'cursor-line')).toBe(false) editor.setSelectedScreenRange([[3, 4], [4, 4]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(3, 'cursor-line')).toBe(true) expect(lineNumberHasClass(4, 'cursor-line')).toBe(true) editor.setSelectedScreenRange([[3, 4], [4, 0]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(3, 'cursor-line')).toBe(true) expect(lineNumberHasClass(4, 'cursor-line')).toBe(false) @@ -3933,7 +3931,7 @@ describe('TextEditorComponent', function () { it('does not apply .cursor-line to the last line of a selection if it\'s empty', async function () { editor.setSelectedScreenRange([[3, 4], [5, 0]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(3, 'cursor-line')).toBe(true) expect(lineNumberHasClass(4, 'cursor-line')).toBe(true) expect(lineNumberHasClass(5, 'cursor-line')).toBe(false) @@ -3941,13 +3939,13 @@ describe('TextEditorComponent', function () { it('applies .cursor-line decorations for lines containing the cursor in non-empty selections', async function () { editor.setCursorScreenPosition([4, 4]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineHasClass(3, 'cursor-line')).toBe(false) expect(lineHasClass(4, 'cursor-line')).toBe(true) expect(lineHasClass(5, 'cursor-line')).toBe(false) editor.setSelectedScreenRange([[3, 4], [4, 4]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineHasClass(2, 'cursor-line')).toBe(false) expect(lineHasClass(3, 'cursor-line')).toBe(false) @@ -3957,11 +3955,11 @@ describe('TextEditorComponent', function () { it('applies .cursor-line-no-selection to line numbers for rows containing the cursor when the selection is empty', async function () { editor.setCursorScreenPosition([4, 4]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe(true) editor.setSelectedScreenRange([[3, 4], [4, 4]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(lineNumberHasClass(4, 'cursor-line-no-selection')).toBe(false) }) @@ -3972,7 +3970,7 @@ describe('TextEditorComponent', function () { it('does not assign a height on the component node', async function () { wrapperNode.style.height = '200px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.style.height).toBe('') }) }) @@ -3988,7 +3986,7 @@ describe('TextEditorComponent', function () { describe('when the "mini" property is true', function () { beforeEach(async function () { editor.setMini(true) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('does not render the gutter', function () { @@ -4025,11 +4023,11 @@ describe('TextEditorComponent', function () { editor.setPlaceholderText('Hello World') expect(componentNode.querySelector('.placeholder-text')).toBeNull() editor.setText('') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelector('.placeholder-text').textContent).toBe('Hello World') editor.setText('hey') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(componentNode.querySelector('.placeholder-text')).toBeNull() }) @@ -4162,7 +4160,7 @@ describe('TextEditorComponent', function () { scopeSelector: '.source.coffee' }) editor.setText(' a line with tabs\tand spaces \n') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('renders the invisibles when editor.showInvisibles is true for a given grammar', function () { @@ -4171,7 +4169,7 @@ describe('TextEditorComponent', function () { it('does not render the invisibles when editor.showInvisibles is false for a given grammar', async function () { editor.setGrammar(coffeeEditor.getGrammar()) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe(' a line with tabs and spaces ') }) @@ -4181,7 +4179,7 @@ describe('TextEditorComponent', function () { atom.config.set('editor.showInvisibles', true, { scopeSelector: '.source.coffee' }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let newInvisibles = { eol: 'N', @@ -4194,11 +4192,11 @@ describe('TextEditorComponent', function () { atom.config.set('editor.invisibles', newInvisibles, { scopeSelector: '.source.coffee' }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('' + newInvisibles.space + 'a line with tabs' + newInvisibles.tab + 'and spaces' + newInvisibles.space + newInvisibles.eol) editor.setGrammar(jsGrammar) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.lineNodeForScreenRow(0).textContent).toBe('' + jsInvisibles.space + 'a line with tabs' + jsInvisibles.tab + 'and spaces' + jsInvisibles.space + jsInvisibles.eol) }) @@ -4212,7 +4210,7 @@ describe('TextEditorComponent', function () { atom.config.set('editor.showIndentGuide', false, { scopeSelector: '.source.coffee' }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('has an "indent-guide" class when scoped editor.showIndentGuide is true, but not when scoped editor.showIndentGuide is false', async function () { @@ -4221,7 +4219,7 @@ describe('TextEditorComponent', function () { expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe(false) editor.setGrammar(coffeeEditor.getGrammar()) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) expect(line1LeafNodes[0].textContent).toBe(' ') @@ -4238,7 +4236,7 @@ describe('TextEditorComponent', function () { atom.config.set('editor.showIndentGuide', false, { scopeSelector: '.source.js' }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() line1LeafNodes = getLeafNodes(component.lineNodeForScreenRow(1)) expect(line1LeafNodes[0].textContent).toBe(' ') @@ -4255,34 +4253,34 @@ describe('TextEditorComponent', function () { component.setLineHeight('10px') component.setFontSize(17) component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() wrapperNode.setWidth(55) wrapperNode.setHeight(55) component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() component.presenter.setHorizontalScrollbarHeight(0) component.presenter.setVerticalScrollbarWidth(0) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) describe('when selecting buffer ranges', function () { it('autoscrolls the selection if it is last unless the "autoscroll" option is false', async function () { expect(wrapperNode.getScrollTop()).toBe(0) editor.setSelectedBufferRange([[5, 6], [6, 8]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let right = wrapperNode.pixelPositionForBufferPosition([6, 8 + editor.getHorizontalScrollMargin()]).left expect(wrapperNode.getScrollBottom()).toBe((7 + editor.getVerticalScrollMargin()) * 10) expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) editor.setSelectedBufferRange([[0, 0], [0, 0]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) expect(wrapperNode.getScrollLeft()).toBe(0) editor.setSelectedBufferRange([[6, 6], [6, 8]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe((7 + editor.getVerticalScrollMargin()) * 10) expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) @@ -4292,7 +4290,7 @@ describe('TextEditorComponent', function () { describe('when adding selections for buffer ranges', function () { it('autoscrolls to the added selection if needed', async function () { editor.addSelectionForBufferRange([[8, 10], [8, 15]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let right = wrapperNode.pixelPositionForBufferPosition([8, 15]).left expect(wrapperNode.getScrollBottom()).toBe((9 * 10) + (2 * 10)) @@ -4303,14 +4301,14 @@ describe('TextEditorComponent', function () { describe('when selecting lines containing cursors', function () { it('autoscrolls to the selection', async function () { editor.setCursorScreenPosition([5, 6]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() wrapperNode.scrollToTop() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.selectLinesContainingCursors() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe((7 + editor.getVerticalScrollMargin()) * 10) }) @@ -4322,16 +4320,16 @@ describe('TextEditorComponent', function () { editor.setCursorScreenPosition([1, 2], { autoscroll: false }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() editor.addCursorAtScreenPosition([10, 4], { autoscroll: false }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.insertText('a') - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(75) }) @@ -4343,12 +4341,12 @@ describe('TextEditorComponent', function () { editor.setCursorScreenPosition([8, 8], { autoscroll: false }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) expect(wrapperNode.getScrollLeft()).toBe(0) editor.scrollToCursorPosition() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let right = wrapperNode.pixelPositionForScreenPosition([8, 9 + editor.getHorizontalScrollMargin()]).left expect(wrapperNode.getScrollTop()).toBe((8.8 * 10) - 30) @@ -4368,36 +4366,36 @@ describe('TextEditorComponent', function () { expect(wrapperNode.getScrollTop()).toBe(0) expect(wrapperNode.getScrollBottom()).toBe(5.5 * 10) editor.setCursorScreenPosition([2, 0]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe(5.5 * 10) editor.moveDown() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe(6 * 10) editor.moveDown() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe(7 * 10) }) it('scrolls up when the last cursor gets closer than ::verticalScrollMargin to the top of the editor', async function () { editor.setCursorScreenPosition([11, 0]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() wrapperNode.setScrollBottom(wrapperNode.getScrollHeight()) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() editor.moveUp() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe(wrapperNode.getScrollHeight()) editor.moveUp() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(7 * 10) editor.moveUp() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(6 * 10) }) @@ -4406,17 +4404,17 @@ describe('TextEditorComponent', function () { expect(wrapperNode.getScrollLeft()).toBe(0) expect(wrapperNode.getScrollRight()).toBe(5.5 * 10) editor.setCursorScreenPosition([0, 2]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollRight()).toBe(5.5 * 10) editor.moveRight() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let margin = component.presenter.getHorizontalScrollMarginInPixels() let right = wrapperNode.pixelPositionForScreenPosition([0, 4]).left + margin expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) editor.moveRight() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() right = wrapperNode.pixelPositionForScreenPosition([0, 5]).left + margin expect(wrapperNode.getScrollRight()).toBeCloseTo(right, 0) @@ -4424,22 +4422,22 @@ describe('TextEditorComponent', function () { it('scrolls left when the last cursor gets closer than ::horizontalScrollMargin to the left of the editor', async function () { wrapperNode.setScrollRight(wrapperNode.getScrollWidth()) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollRight()).toBe(wrapperNode.getScrollWidth()) editor.setCursorScreenPosition([6, 62], { autoscroll: false }) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() editor.moveLeft() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() let margin = component.presenter.getHorizontalScrollMarginInPixels() let left = wrapperNode.pixelPositionForScreenPosition([6, 61]).left - margin expect(wrapperNode.getScrollLeft()).toBeCloseTo(left, 0) editor.moveLeft() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() left = wrapperNode.pixelPositionForScreenPosition([6, 60]).left - margin expect(wrapperNode.getScrollLeft()).toBeCloseTo(left, 0) @@ -4448,11 +4446,11 @@ describe('TextEditorComponent', function () { it('scrolls down when inserting lines makes the document longer than the editor\'s height', async function () { editor.setCursorScreenPosition([13, Infinity]) editor.insertNewline() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe(14 * 10) editor.insertNewline() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollBottom()).toBe(15 * 10) }) @@ -4460,23 +4458,23 @@ describe('TextEditorComponent', function () { it('autoscrolls to the cursor when it moves due to undo', async function () { editor.insertText('abc') wrapperNode.setScrollTop(Infinity) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() editor.undo() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) }) it('does not scroll when the cursor moves into the visible area', async function () { editor.setCursorBufferPosition([0, 0]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() wrapperNode.setScrollTop(40) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() editor.setCursorBufferPosition([6, 0]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(40) }) @@ -4484,58 +4482,58 @@ describe('TextEditorComponent', function () { it('honors the autoscroll option on cursor and selection manipulation methods', async function () { expect(wrapperNode.getScrollTop()).toBe(0) editor.addCursorAtScreenPosition([11, 11], {autoscroll: false}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.addCursorAtBufferPosition([11, 11], {autoscroll: false}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.setCursorScreenPosition([11, 11], {autoscroll: false}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.setCursorBufferPosition([11, 11], {autoscroll: false}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.addSelectionForBufferRange([[11, 11], [11, 11]], {autoscroll: false}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.addSelectionForScreenRange([[11, 11], [11, 12]], {autoscroll: false}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.setSelectedBufferRange([[11, 0], [11, 1]], {autoscroll: false}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.setSelectedScreenRange([[11, 0], [11, 6]], {autoscroll: false}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.clearSelections({autoscroll: false}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.addSelectionForScreenRange([[0, 0], [0, 4]]) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() editor.getCursors()[0].setScreenPosition([11, 11], {autoscroll: true}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) editor.getCursors()[0].setBufferPosition([0, 0], {autoscroll: true}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) editor.getSelections()[0].setScreenRange([[11, 0], [11, 4]], {autoscroll: true}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBeGreaterThan(0) editor.getSelections()[0].setBufferRange([[0, 0], [0, 4]], {autoscroll: true}) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(wrapperNode.getScrollTop()).toBe(0) }) @@ -4546,22 +4544,22 @@ describe('TextEditorComponent', function () { beforeEach(async function () { wrapperNode.style.height = lineHeightInPixels * 8 + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() }) it('returns the first and the last visible rows', async function () { component.setScrollTop(0) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.getVisibleRowRange()).toEqual([0, 9]) }) it('ends at last buffer row even if there\'s more space available', async function () { wrapperNode.style.height = lineHeightInPixels * 13 + 'px' component.measureDimensions() - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() component.setScrollTop(60) - await atom.views.getNextUpdatePromise() + await nextViewUpdatePromise() expect(component.getVisibleRowRange()).toEqual([0, 13]) }) @@ -4724,6 +4722,21 @@ describe('TextEditorComponent', function () { }) } + function nextViewUpdatePromise () { + let timeoutError = new Error("Timed out waiting on a view update") + Error.captureStackTrace(timeoutError, nextViewUpdatePromise) + + return new Promise(function (resolve, reject) { + atom.views.getNextUpdatePromise().then(function (ts) { + window.clearTimeout(timeout) + resolve(ts) + }) + let timeout = window.setTimeout(function () { + reject(timeoutError) + }, 3000) + }) + } + function decorationsUpdatedPromise(editor) { return new Promise(function (resolve) { let disposable = editor.onDidUpdateDecorations(function () { From b9528dcb397e25ad62acaec8e2062708dfbc96f0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 7 Nov 2015 13:25:38 -0700 Subject: [PATCH 41/53] Cancel ViewRegistry animation frames between specs --- src/view-registry.coffee | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/view-registry.coffee b/src/view-registry.coffee index 49ec29247..ba8657614 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -43,7 +43,7 @@ _ = require 'underscore-plus' # ``` module.exports = class ViewRegistry - documentUpdateRequested: false + animationFrameRequest: null documentReadInProgress: false performDocumentPollAfterUpdate: false debouncedPerformDocumentPoll: null @@ -203,16 +203,16 @@ class ViewRegistry @documentReaders = [] @documentWriters = [] @documentPollers = [] - @documentUpdateRequested = false + if @animationFrameRequest? + cancelAnimationFrame(@animationFrameRequest) + @animationFrameRequest = null @stopPollingDocument() requestDocumentUpdate: -> - unless @documentUpdateRequested - @documentUpdateRequested = true - requestAnimationFrame(@performDocumentUpdate) + @animationFrameRequest ?= requestAnimationFrame(@performDocumentUpdate) performDocumentUpdate: => - @documentUpdateRequested = false + @animationFrameRequest = null writer() while writer = @documentWriters.shift() @documentReadInProgress = true @@ -238,7 +238,7 @@ class ViewRegistry @observer.disconnect() requestDocumentPoll: => - if @documentUpdateRequested + if @animationFrameRequest? @performDocumentPollAfterUpdate = true else @debouncedPerformDocumentPoll() From fe88611e79dc8bf34cf6e3e95b8492c045855f31 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 7 Nov 2015 14:33:59 -0700 Subject: [PATCH 42/53] Tear down timers when destroying TextEditorPresenter --- src/text-editor-presenter.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 891252730..018ef72e2 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -48,6 +48,9 @@ class TextEditorPresenter destroy: -> @disposables.dispose() + clearTimeout(@stoppedScrollingTimeoutId) if @stoppedScrollingTimeoutId? + clearInterval(@reflowingInterval) if @reflowingInterval? + @stopBlinkingCursors() # Calls your `callback` when some changes in the model occurred and the current state has been updated. onDidUpdateState: (callback) -> From 1a8adbb4e145a3fee597b13ff9a0513c597e4501 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 7 Nov 2015 15:45:41 -0700 Subject: [PATCH 43/53] :art: --- spec/text-editor-component-spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index d22ca255b..ec0cbc744 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1988,6 +1988,7 @@ describe('TextEditorComponent', function () { }) }) }) + describe('hidden input field', function () { it('renders the hidden input field at the position of the last cursor if the cursor is on screen and the editor is focused', async function () { editor.setVerticalScrollMargin(0) From 54a9012796ff07b26c452a403eebd99d82970598 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 7 Nov 2015 17:25:52 -0700 Subject: [PATCH 44/53] Clear next update promise when resetting ViewRegistry --- src/view-registry.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/view-registry.coffee b/src/view-registry.coffee index ba8657614..56849b962 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -203,6 +203,8 @@ class ViewRegistry @documentReaders = [] @documentWriters = [] @documentPollers = [] + @nextUpdatePromise = null + @resolveNextUpdatePromise = null if @animationFrameRequest? cancelAnimationFrame(@animationFrameRequest) @animationFrameRequest = null From 1aa4b7e06a34c1d6f1f42bb359df938980934bca Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 8 Nov 2015 22:43:59 -0700 Subject: [PATCH 45/53] Clear nextUpdatePromise immediately when an animation frame fires --- src/view-registry.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/view-registry.coffee b/src/view-registry.coffee index 56849b962..0f07600ae 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -214,7 +214,11 @@ class ViewRegistry @animationFrameRequest ?= requestAnimationFrame(@performDocumentUpdate) performDocumentUpdate: => + resolveNextUpdatePromise = @resolveNextUpdatePromise @animationFrameRequest = null + @nextUpdatePromise = null + @resolveNextUpdatePromise = null + writer() while writer = @documentWriters.shift() @documentReadInProgress = true @@ -226,9 +230,6 @@ class ViewRegistry # process updates requested as a result of reads writer() while writer = @documentWriters.shift() - resolveNextUpdatePromise = @resolveNextUpdatePromise - @nextUpdatePromise = null - @resolveNextUpdatePromise = null resolveNextUpdatePromise?() startPollingDocument: -> From 213e7d0b35980e45f52c895c50ccef7c55153dc7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 8 Nov 2015 22:45:22 -0700 Subject: [PATCH 46/53] Schedule marker updates with nextTick instead of setImmediate From what I understand, nextTick callbacks actually happen at the end of the current event loop cycle rather than the next one. The naming is confusing. --- src/display-buffer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index ceea3d4e2..f5a7bd853 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -1076,7 +1076,7 @@ class DisplayBuffer extends Model unless @didUpdateDecorationsEventScheduled @didUpdateDecorationsEventScheduled = true - global.setImmediate => + process.nextTick => @didUpdateDecorationsEventScheduled = false @emitter.emit 'did-update-decorations' From 21f8ad69583b659b57b87976afcac9b42cbec9cf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 8 Nov 2015 22:46:24 -0700 Subject: [PATCH 47/53] Bump timeouts --- spec/text-editor-component-spec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index ec0cbc744..4e9d04941 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3321,6 +3321,7 @@ describe('TextEditorComponent', function () { describe('when the mousewheel event\'s target is a line', function () { it('keeps the line on the DOM if it is scrolled off-screen', async function () { + component.presenter.stoppedScrollingDelay = 3000 // account for slower build machines wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' wrapperNode.style.width = 20 * charWidth + 'px' component.measureDimensions() @@ -3337,7 +3338,7 @@ describe('TextEditorComponent', function () { } }) componentNode.dispatchEvent(wheelEvent) - await nextAnimationFramePromise() + await nextViewUpdatePromise() expect(componentNode.contains(lineNode)).toBe(true) }) @@ -4707,7 +4708,7 @@ describe('TextEditorComponent', function () { let timeout = window.setTimeout(function () { window.clearInterval(interval) reject(timeoutError) - }, 3000) + }, 5000) }) } From 98c420a408750b45fd024bfc0f9d48393fbd45bc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 8 Nov 2015 22:47:14 -0700 Subject: [PATCH 48/53] Wait for nextTick after presenter updates This gives any nextTick callbacks in the model a chance to complete before proceeding with tests. --- spec/text-editor-presenter-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 62477eb16..7376b5823 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -67,7 +67,7 @@ describe "TextEditorPresenter", -> fn?() disposable = presenter.onDidUpdateState -> disposable.dispose() - done() + process.nextTick(done) tiledContentContract = (stateFn) -> it "contains states for tiles that are visible on screen", -> From 6dd18b348bb837d59d0c4dd90bf7d32bee381bbf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 8 Nov 2015 22:48:15 -0700 Subject: [PATCH 49/53] Include more debug info when nextViewUpdatePromise times out --- spec/text-editor-component-spec.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 4e9d04941..a8ee824ae 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -4725,17 +4725,19 @@ describe('TextEditorComponent', function () { } function nextViewUpdatePromise () { - let timeoutError = new Error("Timed out waiting on a view update") + let timeoutError = new Error('Timed out waiting on a view update.') Error.captureStackTrace(timeoutError, nextViewUpdatePromise) return new Promise(function (resolve, reject) { - atom.views.getNextUpdatePromise().then(function (ts) { + let nextUpdatePromise = atom.views.getNextUpdatePromise() + nextUpdatePromise.then(function (ts) { window.clearTimeout(timeout) resolve(ts) }) let timeout = window.setTimeout(function () { + timeoutError.message += ' Frame pending? ' + atom.views.animationFrameRequest + ' Same next update promise pending? ' + (nextUpdatePromise === atom.views.nextUpdatePromise) reject(timeoutError) - }, 3000) + }, 5000) }) } From fc5788c43b07d6ea59de3326fd9f51ec8d66275b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 8 Nov 2015 23:07:28 -0700 Subject: [PATCH 50/53] Disable renderer backgrounding --- src/browser/atom-application.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 8bb44349e..79062cca7 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -158,6 +158,7 @@ class AtomApplication # Configures required javascript environment flags. setupJavaScriptArguments: -> app.commandLine.appendSwitch 'js-flags', '--harmony' + app.commandLine.appendSwitch 'disable-renderer-backgrounding' # Registers basic application commands, non-idempotent. handleEvents: -> From a35e24658d2466c08f185e330badc24b90294dcd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 8 Nov 2015 23:09:19 -0700 Subject: [PATCH 51/53] Increase timeouts --- spec/async-spec-helpers.coffee | 2 +- spec/text-editor-component-spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/async-spec-helpers.coffee b/spec/async-spec-helpers.coffee index 9dcff9a69..fd82c308f 100644 --- a/spec/async-spec-helpers.coffee +++ b/spec/async-spec-helpers.coffee @@ -19,7 +19,7 @@ exports.afterEach = (fn) -> waitsForPromise = (fn) -> promise = fn() - waitsFor 10000, (done) -> + waitsFor 30000, (done) -> promise.then( done, (error) -> diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index a8ee824ae..609d20291 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -4737,7 +4737,7 @@ describe('TextEditorComponent', function () { let timeout = window.setTimeout(function () { timeoutError.message += ' Frame pending? ' + atom.views.animationFrameRequest + ' Same next update promise pending? ' + (nextUpdatePromise === atom.views.nextUpdatePromise) reject(timeoutError) - }, 5000) + }, 30000) }) } From 5587bad75897bbc8fe3b8181bee83e91dc2ab6a9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 8 Nov 2015 23:35:29 -0700 Subject: [PATCH 52/53] Add description to waitsFor --- spec/async-spec-helpers.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/async-spec-helpers.coffee b/spec/async-spec-helpers.coffee index fd82c308f..5f8e03ca3 100644 --- a/spec/async-spec-helpers.coffee +++ b/spec/async-spec-helpers.coffee @@ -19,7 +19,7 @@ exports.afterEach = (fn) -> waitsForPromise = (fn) -> promise = fn() - waitsFor 30000, (done) -> + waitsFor 'spec promise to resolve', 30000, (done) -> promise.then( done, (error) -> From c97ecf9da2afa6eabfc267111a01e1bebd7bd5c2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 9 Nov 2015 12:19:55 -0700 Subject: [PATCH 53/53] =?UTF-8?q?Don=E2=80=99t=20disable=20render=20backgr?= =?UTF-8?q?ounding.=20Doesn=E2=80=99t=20seem=20to=20make=20a=20difference.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/browser/atom-application.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index 79062cca7..8bb44349e 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -158,7 +158,6 @@ class AtomApplication # Configures required javascript environment flags. setupJavaScriptArguments: -> app.commandLine.appendSwitch 'js-flags', '--harmony' - app.commandLine.appendSwitch 'disable-renderer-backgrounding' # Registers basic application commands, non-idempotent. handleEvents: ->