From 9c5bddaa69d4ef60ac1dcfc2d5ec5f5b99aaaf65 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 6 Oct 2016 10:26:09 +0200 Subject: [PATCH] Reimplement block decorations without the shadow DOM --- spec/text-editor-component-spec.js | 235 ++++---- spec/text-editor-presenter-spec.coffee | 527 ++++++++---------- src/block-decorations-component.coffee | 86 --- src/lines-component.coffee | 16 +- src/lines-tile-component.js | 273 +++++---- src/off-screen-block-decorations-component.js | 61 ++ src/text-editor-component.coffee | 15 +- src/text-editor-presenter.coffee | 75 ++- src/tiled-component.coffee | 15 +- 9 files changed, 637 insertions(+), 666 deletions(-) delete mode 100644 src/block-decorations-component.coffee create mode 100644 src/off-screen-block-decorations-component.js diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 5feb5761c..323ecf69e 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -405,49 +405,49 @@ describe('TextEditorComponent', function () { } }) - it('applies .syntax--leading-whitespace for lines with leading spaces and/or tabs', function () { + it('applies .leading-whitespace for lines with leading spaces and/or tabs', function () { editor.setText(' a') runAnimationFrames() let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('syntax--leading-whitespace')).toBe(true) - expect(leafNodes[0].classList.contains('syntax--trailing-whitespace')).toBe(false) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(true) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(false) editor.setText('\ta') runAnimationFrames() leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('syntax--leading-whitespace')).toBe(true) - expect(leafNodes[0].classList.contains('syntax--trailing-whitespace')).toBe(false) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(true) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(false) }) - it('applies .syntax--trailing-whitespace for lines with trailing spaces and/or tabs', function () { + it('applies .trailing-whitespace for lines with trailing spaces and/or tabs', function () { editor.setText(' ') runAnimationFrames() let leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('syntax--trailing-whitespace')).toBe(true) - expect(leafNodes[0].classList.contains('syntax--leading-whitespace')).toBe(false) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) editor.setText('\t') runAnimationFrames() leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('syntax--trailing-whitespace')).toBe(true) - expect(leafNodes[0].classList.contains('syntax--leading-whitespace')).toBe(false) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) editor.setText('a ') runAnimationFrames() leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('syntax--trailing-whitespace')).toBe(true) - expect(leafNodes[0].classList.contains('syntax--leading-whitespace')).toBe(false) + expect(leafNodes[0].classList.contains('trailing-whitespace')).toBe(true) + expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false) editor.setText('a\t') runAnimationFrames() leafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) - expect(leafNodes[0].classList.contains('syntax--trailing-whitespace')).toBe(true) - expect(leafNodes[0].classList.contains('syntax--leading-whitespace')).toBe(false) + 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 () { @@ -501,14 +501,14 @@ describe('TextEditorComponent', function () { 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('syntax--invisible-character')).toBe(true) - expect(leafNodes[leafNodes.length - 1].classList.contains('syntax--invisible-character')).toBe(true) + 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', function () { editor.setText('let\n') runAnimationFrames() - expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '') + expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '') }) it('displays trailing carriage returns using a visible, non-empty value', function () { @@ -543,20 +543,20 @@ describe('TextEditorComponent', function () { normalizeLineEndings: false }) runAnimationFrames() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') editor.setTabLength(3) runAnimationFrames() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') editor.setTabLength(1) runAnimationFrames() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ') editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ') runAnimationFrames() - expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') + expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE') }) describe('when soft wrapping is enabled', function () { @@ -583,30 +583,30 @@ describe('TextEditorComponent', function () { runAnimationFrames() }) - it('adds an "syntax--indent-guide" class to spans comprising the leading whitespace', function () { + 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('syntax--indent-guide')).toBe(true) - expect(line1LeafNodes[1].classList.contains('syntax--indent-guide')).toBe(false) + 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('syntax--indent-guide')).toBe(true) + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[1].textContent).toBe(' ') - expect(line2LeafNodes[1].classList.contains('syntax--indent-guide')).toBe(true) - expect(line2LeafNodes[2].classList.contains('syntax--indent-guide')).toBe(false) + 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 "syntax--indent-guide" class for empty lines', function () { + it('renders leading whitespace spans with the "indent-guide" class for empty lines', function () { editor.getBuffer().insert([1, Infinity], '\n') runAnimationFrames() let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe(2) expect(line2LeafNodes[0].textContent).toBe(' ') - expect(line2LeafNodes[0].classList.contains('syntax--indent-guide')).toBe(true) + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[1].textContent).toBe(' ') - expect(line2LeafNodes[1].classList.contains('syntax--indent-guide')).toBe(true) + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) }) it('renders indent guides correctly on lines containing only whitespace', function () { @@ -616,11 +616,11 @@ describe('TextEditorComponent', function () { let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe(3) expect(line2LeafNodes[0].textContent).toBe(' ') - expect(line2LeafNodes[0].classList.contains('syntax--indent-guide')).toBe(true) + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[1].textContent).toBe(' ') - expect(line2LeafNodes[1].classList.contains('syntax--indent-guide')).toBe(true) + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[2].textContent).toBe(' ') - expect(line2LeafNodes[2].classList.contains('syntax--indent-guide')).toBe(true) + expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(true) }) it('renders indent guides correctly on lines containing only whitespace when invisibles are enabled', function () { @@ -638,11 +638,11 @@ describe('TextEditorComponent', function () { let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe(4) expect(line2LeafNodes[0].textContent).toBe('--') - expect(line2LeafNodes[0].classList.contains('syntax--indent-guide')).toBe(true) + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[1].textContent).toBe('--') - expect(line2LeafNodes[1].classList.contains('syntax--indent-guide')).toBe(true) + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[2].textContent).toBe('--') - expect(line2LeafNodes[2].classList.contains('syntax--indent-guide')).toBe(true) + expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(true) expect(line2LeafNodes[3].textContent).toBe('x') }) @@ -653,9 +653,9 @@ describe('TextEditorComponent', function () { let line0LeafNodes = getLeafNodes(component.lineNodeForScreenRow(0)) expect(line0LeafNodes[0].textContent).toBe(' ') - expect(line0LeafNodes[0].classList.contains('syntax--indent-guide')).toBe(true) + expect(line0LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line0LeafNodes[1].textContent).toBe(' ') - expect(line0LeafNodes[1].classList.contains('syntax--indent-guide')).toBe(false) + expect(line0LeafNodes[1].classList.contains('indent-guide')).toBe(false) }) it('updates the indent guides on empty lines preceding an indentation change', function () { @@ -667,9 +667,9 @@ describe('TextEditorComponent', function () { let line12LeafNodes = getLeafNodes(component.lineNodeForScreenRow(12)) expect(line12LeafNodes[0].textContent).toBe(' ') - expect(line12LeafNodes[0].classList.contains('syntax--indent-guide')).toBe(true) + expect(line12LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line12LeafNodes[1].textContent).toBe(' ') - expect(line12LeafNodes[1].classList.contains('syntax--indent-guide')).toBe(true) + expect(line12LeafNodes[1].classList.contains('indent-guide')).toBe(true) }) it('updates the indent guides on empty lines following an indentation change', function () { @@ -682,9 +682,9 @@ describe('TextEditorComponent', function () { let line13LeafNodes = getLeafNodes(component.lineNodeForScreenRow(13)) expect(line13LeafNodes[0].textContent).toBe(' ') - expect(line13LeafNodes[0].classList.contains('syntax--indent-guide')).toBe(true) + expect(line13LeafNodes[0].classList.contains('indent-guide')).toBe(true) expect(line13LeafNodes[1].textContent).toBe(' ') - expect(line13LeafNodes[1].classList.contains('syntax--indent-guide')).toBe(true) + expect(line13LeafNodes[1].classList.contains('indent-guide')).toBe(true) }) }) @@ -701,11 +701,11 @@ describe('TextEditorComponent', function () { let line2LeafNodes = getLeafNodes(component.lineNodeForScreenRow(2)) expect(line2LeafNodes.length).toBe(3) expect(line2LeafNodes[0].textContent).toBe(' ') - expect(line2LeafNodes[0].classList.contains('syntax--indent-guide')).toBe(false) + expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe(false) expect(line2LeafNodes[1].textContent).toBe(' ') - expect(line2LeafNodes[1].classList.contains('syntax--indent-guide')).toBe(false) + expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe(false) expect(line2LeafNodes[2].textContent).toBe(' ') - expect(line2LeafNodes[2].classList.contains('syntax--indent-guide')).toBe(false) + expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe(false) }) }) @@ -720,19 +720,19 @@ describe('TextEditorComponent', function () { describe('when there is a fold', function () { it('renders a fold marker on the folded line', function () { let foldedLineNode = component.lineNodeForScreenRow(4) - expect(foldedLineNode.querySelector('.syntax--fold-marker')).toBeFalsy() + expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() editor.foldBufferRow(4) runAnimationFrames() foldedLineNode = component.lineNodeForScreenRow(4) - expect(foldedLineNode.querySelector('.syntax--fold-marker')).toBeTruthy() + expect(foldedLineNode.querySelector('.fold-marker')).toBeTruthy() editor.unfoldBufferRow(4) runAnimationFrames() foldedLineNode = component.lineNodeForScreenRow(4) - expect(foldedLineNode.querySelector('.syntax--fold-marker')).toBeFalsy() + expect(foldedLineNode.querySelector('.fold-marker')).toBeFalsy() }) }) }) @@ -1284,7 +1284,7 @@ describe('TextEditorComponent', function () { runAnimationFrames() let cursorRect = componentNode.querySelector('.cursor').getBoundingClientRect() - let foldMarkerRect = componentNode.querySelector('.syntax--fold-marker').getBoundingClientRect() + let foldMarkerRect = componentNode.querySelector('.fold-marker').getBoundingClientRect() expect(cursorRect.left).toBeCloseTo(foldMarkerRect.right, 0) }) @@ -1771,32 +1771,32 @@ describe('TextEditorComponent', function () { let [item3, blockDecoration3] = createBlockDecorationBeforeScreenRow(4, {className: "decoration-3"}) let [item4, blockDecoration4] = createBlockDecorationBeforeScreenRow(7, {className: "decoration-4"}) let [item5, blockDecoration5] = createBlockDecorationAfterScreenRow(7, {className: "decoration-5"}) + let [item6, blockDecoration6] = createBlockDecorationAfterScreenRow(12, {className: "decoration-6"}) atom.styles.addStyleSheet( `atom-text-editor .decoration-1 { width: 30px; height: 80px; } atom-text-editor .decoration-2 { width: 30px; height: 40px; } atom-text-editor .decoration-3 { width: 30px; height: 100px; } atom-text-editor .decoration-4 { width: 30px; height: 120px; } - atom-text-editor .decoration-5 { width: 30px; height: 42px; }`, + atom-text-editor .decoration-5 { width: 30px; height: 42px; } + atom-text-editor .decoration-6 { width: 30px; height: 22px; }`, {context: 'atom-text-editor'} ) runAnimationFrames() - expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) - + expect(verticalScrollbarNode.scrollHeight).toBe(editor.getScreenLineCount() * editor.getLineHeightInPixels() + 80 + 40 + 100 + 120 + 42 + 22) expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 80 + 40 + "px") expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + "px") expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) - expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBe(item1) expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBeNull() - + expect(component.getTopmostDOMNode().querySelector(".decoration-6")).toBeNull() expect(item1.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 0) expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 2 + 80) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 4 + 80 + 40) @@ -1804,24 +1804,21 @@ describe('TextEditorComponent', function () { editor.setCursorScreenPosition([0, 0]) editor.insertNewline() blockDecoration1.destroy() - runAnimationFrames() - expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) - + expect(verticalScrollbarNode.scrollHeight).toBe(editor.getScreenLineCount() * editor.getLineHeightInPixels() + 40 + 100 + 120 + 42 + 22) expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 40 + "px") expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) - expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBeNull() - + expect(component.getTopmostDOMNode().querySelector(".decoration-6")).toBeNull() expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 40) @@ -1832,52 +1829,71 @@ describe('TextEditorComponent', function () { runAnimationFrames() // causes the DOM to update and to retrieve new styles runAnimationFrames() // applies the changes - expect(component.getDomNode().querySelectorAll(".line").length).toBe(7) - + expect(verticalScrollbarNode.scrollHeight).toBe(editor.getScreenLineCount() * editor.getLineHeightInPixels() + 60 + 100 + 120 + 42 + 22) expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 60 + "px") expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) - expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBeNull() expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBeNull() - + expect(component.getTopmostDOMNode().querySelector(".decoration-6")).toBeNull() expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 60) item2.style.height = "20px" wrapperNode.invalidateBlockDecorationDimensions(blockDecoration2) - runAnimationFrames() - runAnimationFrames() - + runAnimationFrames() // causes the DOM to update and to retrieve new styles + runAnimationFrames() // applies the changes expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) - + expect(verticalScrollbarNode.scrollHeight).toBe(editor.getScreenLineCount() * editor.getLineHeightInPixels() + 20 + 100 + 120 + 42 + 22) expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 20 + "px") expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) - expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBe(item5) + expect(component.getTopmostDOMNode().querySelector(".decoration-6")).toBeNull() + expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) + expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 20) + expect(item4.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 20 + 100) + expect(item5.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 20 + 100 + 120 + lineHeightInPixels) + item6.style.height = "33px" + wrapperNode.invalidateBlockDecorationDimensions(blockDecoration6) + runAnimationFrames() // causes the DOM to update and to retrieve new styles + runAnimationFrames() // applies the changes + expect(component.getDomNode().querySelectorAll(".line").length).toBe(9) + expect(verticalScrollbarNode.scrollHeight).toBe(editor.getScreenLineCount() * editor.getLineHeightInPixels() + 20 + 100 + 120 + 42 + 33) + expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") + expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)") + expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 100 + 20 + "px") + expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`) + expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 120 + 42 + "px") + expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) + expect(component.getTopmostDOMNode().querySelector(".decoration-1")).toBeNull() + expect(component.getTopmostDOMNode().querySelector(".decoration-2")).toBe(item2) + expect(component.getTopmostDOMNode().querySelector(".decoration-3")).toBe(item3) + expect(component.getTopmostDOMNode().querySelector(".decoration-4")).toBe(item4) + expect(component.getTopmostDOMNode().querySelector(".decoration-5")).toBe(item5) + expect(component.getTopmostDOMNode().querySelector(".decoration-6")).toBeNull() expect(item2.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 3) expect(item3.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 5 + 20) expect(item4.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 20 + 100) expect(item5.getBoundingClientRect().top).toBe(editor.getLineHeightInPixels() * 8 + 20 + 100 + 120 + lineHeightInPixels) }) - it("correctly sets screen rows on elements, both initially and when decorations move", function () { + it("correctly sets screen rows on block decoration and ruler nodes, both initially and when decorations move", function () { let [item, blockDecoration] = createBlockDecorationBeforeScreenRow(0, {className: "decoration-1"}) atom.styles.addStyleSheet( 'atom-text-editor .decoration-1 { width: 30px; height: 80px; }', @@ -1885,42 +1901,37 @@ describe('TextEditorComponent', function () { ) runAnimationFrames() - - let tileNode, contentElements - - tileNode = component.tileNodesForLines()[0] - contentElements = tileNode.querySelectorAll("content") - - expect(contentElements.length).toBe(1) - expect(contentElements[0].dataset.screenRow).toBe("0") - expect(component.lineNodeForScreenRow(0).dataset.screenRow).toBe("0") - expect(component.lineNodeForScreenRow(1).dataset.screenRow).toBe("1") - expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2") + const line0 = component.lineNodeForScreenRow(0) + expect(item.previousSibling.dataset.screenRow).toBe("0") + expect(item.dataset.screenRow).toBe("0") + expect(item.nextSibling.dataset.screenRow).toBe("0") + expect(line0.previousSibling).toBe(item.nextSibling) editor.setCursorBufferPosition([0, 0]) editor.insertNewline() runAnimationFrames() + const line1 = component.lineNodeForScreenRow(1) + expect(item.previousSibling.dataset.screenRow).toBe("1") + expect(item.dataset.screenRow).toBe("1") + expect(item.nextSibling.dataset.screenRow).toBe("1") + expect(line1.previousSibling).toBe(item.nextSibling) - tileNode = component.tileNodesForLines()[0] - contentElements = tileNode.querySelectorAll("content") - - expect(contentElements.length).toBe(1) - expect(contentElements[0].dataset.screenRow).toBe("1") - expect(component.lineNodeForScreenRow(0).dataset.screenRow).toBe("0") - expect(component.lineNodeForScreenRow(1).dataset.screenRow).toBe("1") - expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2") - - blockDecoration.getMarker().setHeadBufferPosition([2, 0]) + editor.setCursorBufferPosition([0, 0]) + editor.insertNewline() runAnimationFrames() + const line2 = component.lineNodeForScreenRow(2) + expect(item.previousSibling.dataset.screenRow).toBe("2") + expect(item.dataset.screenRow).toBe("2") + expect(item.nextSibling.dataset.screenRow).toBe("2") + expect(line2.previousSibling).toBe(item.nextSibling) - tileNode = component.tileNodesForLines()[0] - contentElements = tileNode.querySelectorAll("content") - - expect(contentElements.length).toBe(1) - expect(contentElements[0].dataset.screenRow).toBe("2") - expect(component.lineNodeForScreenRow(0).dataset.screenRow).toBe("0") - expect(component.lineNodeForScreenRow(1).dataset.screenRow).toBe("1") - expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2") + blockDecoration.getMarker().setHeadBufferPosition([4, 0]) + runAnimationFrames() + const line4 = component.lineNodeForScreenRow(4) + expect(item.previousSibling.dataset.screenRow).toBe("4") + expect(item.dataset.screenRow).toBe("4") + expect(item.nextSibling.dataset.screenRow).toBe("4") + expect(line4.previousSibling).toBe(item.nextSibling) }) it('measures block decorations taking into account both top and bottom margins of the element and its children', function () { @@ -1945,6 +1956,18 @@ describe('TextEditorComponent', function () { expect(component.tileNodesForLines()[2].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px") expect(component.tileNodesForLines()[2].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight + component.tileNodesForLines()[1].offsetHeight}px, 0px)`) }) + + it('allows the same block decoration item to be moved from one tile to another in the same animation frame', function () { + let [item, blockDecoration] = createBlockDecorationBeforeScreenRow(5, {className: "decoration-1"}) + runAnimationFrames() + expect(component.tileNodesForLines()[0].querySelector('.decoration-1')).toBeNull() + expect(component.tileNodesForLines()[1].querySelector('.decoration-1')).toBe(item) + + blockDecoration.getMarker().setHeadBufferPosition([0, 0]) + runAnimationFrames() + expect(component.tileNodesForLines()[0].querySelector('.decoration-1')).toBe(item) + expect(component.tileNodesForLines()[1].querySelector('.decoration-1')).toBeNull() + }) }) describe('highlight decoration rendering', function () { @@ -2872,20 +2895,20 @@ describe('TextEditorComponent', function () { editor.foldBufferRange([[4, 6], [4, 10]]) editor.foldBufferRange([[4, 15], [4, 20]]) runAnimationFrames() - debugger - let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.syntax--fold-marker') + + let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') expect(foldMarkers.length).toBe(2) expect(editor.isFoldedAtBufferRow(4)).toBe(true) clickElementAtPosition(foldMarkers[0], [4, 6]) runAnimationFrames() - foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.syntax--fold-marker') + foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') expect(foldMarkers.length).toBe(1) expect(editor.isFoldedAtBufferRow(4)).toBe(true) clickElementAtPosition(foldMarkers[0], [4, 15]) runAnimationFrames() - foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.syntax--fold-marker') + foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') expect(foldMarkers.length).toBe(0) expect(editor.isFoldedAtBufferRow(4)).toBe(false) }) @@ -2895,25 +2918,25 @@ describe('TextEditorComponent', function () { editor.foldBufferRange([[4, 4], [4, 5]]) editor.foldBufferRange([[4, 4], [4, 20]]) runAnimationFrames() - let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.syntax--fold-marker') + let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') expect(foldMarkers.length).toBe(1) expect(editor.isFoldedAtBufferRow(4)).toBe(true) clickElementAtPosition(foldMarkers[0], [4, 4]) runAnimationFrames() - foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.syntax--fold-marker') + foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') expect(foldMarkers.length).toBe(1) expect(editor.isFoldedAtBufferRow(4)).toBe(true) clickElementAtPosition(foldMarkers[0], [4, 4]) runAnimationFrames() - foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.syntax--fold-marker') + foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') expect(foldMarkers.length).toBe(1) expect(editor.isFoldedAtBufferRow(4)).toBe(true) clickElementAtPosition(foldMarkers[0], [4, 10]) runAnimationFrames() - foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.syntax--fold-marker') + foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker') expect(foldMarkers.length).toBe(0) expect(editor.isFoldedAtBufferRow(4)).toBe(false) }) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 8cc5e5086..cbcebee25 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -479,7 +479,7 @@ describe "TextEditorPresenter", -> expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'meta.method-call.js', 'support.function.js'], 'p', 20) + presenter.getLinesYardstick().setScopedCharacterWidth(['syntax--source.syntax--js', 'syntax--meta.syntax--method-call.syntax--js', 'syntax--support.syntax--function.syntax--js'], 'p', 20) presenter.measurementsChanged() expect(getState(presenter).horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide @@ -766,7 +766,7 @@ describe "TextEditorPresenter", -> expect(getState(presenter).hiddenInput.width).toBe 15 expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.type.var.js'], 'r', 20) + presenter.getLinesYardstick().setScopedCharacterWidth(['syntax--source.syntax--js', 'syntax--storage.syntax--type.syntax--var.syntax--js'], 'r', 20) presenter.measurementsChanged() expect(getState(presenter).hiddenInput.width).toBe 20 @@ -922,7 +922,7 @@ describe "TextEditorPresenter", -> expect(getState(presenter).content.scrollWidth).toBe 10 * maxLineLength + 1 expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'meta.method-call.js', 'support.function.js'], 'p', 20) + presenter.getLinesYardstick().setScopedCharacterWidth(['syntax--source.syntax--js', 'syntax--meta.syntax--method-call.syntax--js', 'syntax--support.syntax--function.syntax--js'], 'p', 20) presenter.measurementsChanged() expect(getState(presenter).content.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide @@ -1274,7 +1274,16 @@ describe "TextEditorPresenter", -> expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 0).tagCodes).openTags).toContain('invisible-character eol') expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 1).tagCodes).openTags).toContain('invisible-character eol') - describe ".blockDecorations", -> + describe ".{preceding,following}BlockDecorations", -> + stateForBlockDecorations = (blockDecorations) -> + state = {} + for blockDecoration in blockDecorations + state[blockDecoration.id] = { + decoration: blockDecoration, + screenRow: blockDecoration.getMarker().getHeadScreenPosition().row + } + state + it "contains all block decorations that are present before/after a line, both initially and when decorations change", -> blockDecoration1 = addBlockDecorationBeforeScreenRow(0) presenter = buildPresenter() @@ -1286,32 +1295,32 @@ describe "TextEditorPresenter", -> blockDecoration4 = addBlockDecorationAfterScreenRow(7) runs -> - expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual([blockDecoration1]) - expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual([blockDecoration2]) - expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual([blockDecoration3]) - expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual([blockDecoration4]) - expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration1])) + expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration2])) + expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration3])) + expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration4])) + expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual({}) waitsForStateToUpdate presenter, -> blockDecoration1.getMarker().setHeadBufferPosition([1, 0]) @@ -1320,32 +1329,32 @@ describe "TextEditorPresenter", -> blockDecoration4.getMarker().setHeadBufferPosition([8, 0]) runs -> - expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual([blockDecoration1]) - expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual([blockDecoration4]) - expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual([blockDecoration2, blockDecoration3]) - expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration1])) + expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration4])) + expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration2, blockDecoration3])) + expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual({}) waitsForStateToUpdate presenter, -> blockDecoration4.destroy() @@ -1353,71 +1362,85 @@ describe "TextEditorPresenter", -> blockDecoration1.getMarker().setHeadBufferPosition([0, 0]) runs -> - expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual([blockDecoration1]) - expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual([blockDecoration2]) - expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration1])) + expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration2])) + expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual({}) waitsForStateToUpdate presenter, -> editor.setCursorBufferPosition([0, 0]) editor.insertNewline() runs -> - expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual([blockDecoration1]) - expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual([blockDecoration2]) - expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual([]) - expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual([]) + expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 1).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration1])) + expect(lineStateForScreenRow(presenter, 1).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 2).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 2).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 3).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 3).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 5).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 5).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 6).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 6).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 7).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 7).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 8).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 8).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 9).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 9).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 10).precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration2])) + expect(lineStateForScreenRow(presenter, 10).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 11).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 11).followingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 12).precedingBlockDecorations).toEqual({}) + expect(lineStateForScreenRow(presenter, 12).followingBlockDecorations).toEqual({}) - it "inserts block decorations before the line if not specified otherwise", -> + it "contains block decorations located in ::mouseWheelScreenRow even if they are off screen", -> + blockDecoration = addBlockDecorationBeforeScreenRow(0) + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) + lineId = presenter.displayLayer.getScreenLines(0, 1)[0].id + + expect(getState(presenter).content.tiles[0].lines[lineId].precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration])) + + presenter.setMouseWheelScreenRow(0) + expectStateUpdate presenter, -> presenter.setScrollTop(4) + expect(getState(presenter).content.tiles[0].lines[lineId].precedingBlockDecorations).toEqual(stateForBlockDecorations([blockDecoration])) + + advanceClock(presenter.stoppedScrollingDelay) + expect(getState(presenter).content.tiles[0]).toBeUndefined() + + it "inserts block decorations before the line unless otherwise specified", -> blockDecoration = editor.decorateMarker(editor.markScreenPosition([4, 0]), {type: "block"}) presenter = buildPresenter() - expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual [blockDecoration] - expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual [] + expect(lineStateForScreenRow(presenter, 4).precedingBlockDecorations).toEqual stateForBlockDecorations([blockDecoration]) + expect(lineStateForScreenRow(presenter, 4).followingBlockDecorations).toEqual {} describe ".decorationClasses", -> it "adds decoration classes to the relevant line state objects, both initially and when decorations change", -> @@ -1734,12 +1757,12 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(explicitHeight: 20) expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.type.var.js'], 'v', 20) + presenter.getLinesYardstick().setScopedCharacterWidth(['syntax--source.syntax--js', 'syntax--storage.syntax--type.syntax--var.syntax--js'], 'v', 20) presenter.measurementsChanged() expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 10, height: 10} expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.type.var.js'], 'r', 20) + presenter.getLinesYardstick().setScopedCharacterWidth(['syntax--source.syntax--js', 'syntax--storage.syntax--type.syntax--var.syntax--js'], 'r', 20) presenter.measurementsChanged() expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 20, height: 10} @@ -2085,7 +2108,7 @@ describe "TextEditorPresenter", -> regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] } expectStateUpdate presenter, -> - presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'i', 20) + presenter.getLinesYardstick().setScopedCharacterWidth(['syntax--source.syntax--js', 'syntax--keyword.syntax--control.syntax--js'], 'i', 20) presenter.measurementsChanged() expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 4 * 10, width: 20 + 10, height: 10}] @@ -2208,222 +2231,112 @@ describe "TextEditorPresenter", -> flashCount: 2 } - describe ".blockDecorations", -> - stateForBlockDecoration = (presenter, decoration) -> - getState(presenter).content.blockDecorations[decoration.id] + describe ".offScreenBlockDecorations", -> + stateForOffScreenBlockDecoration = (presenter, decoration) -> + getState(presenter).content.offScreenBlockDecorations[decoration.id] - it "contains state for measured block decorations that are not visible when they are on ::mouseWheelScreenRow", -> - blockDecoration1 = addBlockDecorationBeforeScreenRow(0) - presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0, stoppedScrollingDelay: 200) - getState(presenter) # flush pending state - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 0) - - presenter.setScrollTop(100) - presenter.setMouseWheelScreenRow(0) - - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 0 - isVisible: true - } - - advanceClock(presenter.stoppedScrollingDelay) - - expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - - it "invalidates block decorations that intersect a change in the buffer", -> - blockDecoration1 = addBlockDecorationBeforeScreenRow(9) - blockDecoration2 = addBlockDecorationBeforeScreenRow(10) - blockDecoration3 = addBlockDecorationBeforeScreenRow(11) - presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) - - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 9 - isVisible: false - } - expectValues stateForBlockDecoration(presenter, blockDecoration2), { - decoration: blockDecoration2 - screenRow: 10 - isVisible: false - } - expectValues stateForBlockDecoration(presenter, blockDecoration3), { - decoration: blockDecoration3 - screenRow: 11 - isVisible: false - } - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) - presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) - presenter.setBlockDecorationDimensions(blockDecoration3, 0, 10) - expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - expect(stateForBlockDecoration(presenter, blockDecoration3)).toBeUndefined() - - editor.setSelectedScreenRange([[10, 0], [12, 0]]) - editor.delete() - presenter.setScrollTop(0) # deleting the buffer causes the editor to autoscroll - - expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expectValues stateForBlockDecoration(presenter, blockDecoration2), { - decoration: blockDecoration2 - screenRow: 10 - isVisible: false - } - expectValues stateForBlockDecoration(presenter, blockDecoration3), { - decoration: blockDecoration3 - screenRow: 10 - isVisible: false - } - - it "invalidates all block decorations when content frame width, window size or bounding client rect change", -> - blockDecoration1 = addBlockDecorationBeforeScreenRow(11) - presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) - - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 11 - isVisible: false - } - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) - expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - - presenter.setBoundingClientRect({top: 0, left: 0, width: 50, height: 30}) - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 11 - isVisible: false - } - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) - expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - - presenter.setContentFrameWidth(100) - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 11 - isVisible: false - } - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) - expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - - presenter.setWindowSize(100, 200) - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 11 - isVisible: false - } - - presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) - expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - - it "contains state for on-screen and unmeasured block decorations, both initially and when they are updated or destroyed", -> + it "contains state for off-screen unmeasured block decorations, both initially and when they are updated or destroyed", -> item = {} blockDecoration1 = addBlockDecorationBeforeScreenRow(0, item) blockDecoration2 = addBlockDecorationBeforeScreenRow(4, item) blockDecoration3 = addBlockDecorationBeforeScreenRow(4, item) blockDecoration4 = addBlockDecorationBeforeScreenRow(10, item) presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) - - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 0 - isVisible: true - } - expectValues stateForBlockDecoration(presenter, blockDecoration2), { - decoration: blockDecoration2 - screenRow: 4 - isVisible: true - } - expectValues stateForBlockDecoration(presenter, blockDecoration3), { - decoration: blockDecoration3 - screenRow: 4 - isVisible: true - } - expectValues stateForBlockDecoration(presenter, blockDecoration4), { - decoration: blockDecoration4 - screenRow: 10 - isVisible: false - } + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration4)).toBe(blockDecoration4) presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) presenter.setBlockDecorationDimensions(blockDecoration4, 0, 20) - - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 0 - isVisible: true - } - expectValues stateForBlockDecoration(presenter, blockDecoration2), { - decoration: blockDecoration2 - screenRow: 4 - isVisible: false - } - expectValues stateForBlockDecoration(presenter, blockDecoration3), { - decoration: blockDecoration3 - screenRow: 4 - isVisible: false - } - expect(stateForBlockDecoration(presenter, blockDecoration4)).toBeUndefined() - - blockDecoration3.getMarker().setHeadScreenPosition([5, 0]) - presenter.setScrollTop(90) - - expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expectValues stateForBlockDecoration(presenter, blockDecoration2), { - decoration: blockDecoration2 - screenRow: 4 - isVisible: false - } - expectValues stateForBlockDecoration(presenter, blockDecoration3), { - decoration: blockDecoration3 - screenRow: 5 - isVisible: false - } - expectValues stateForBlockDecoration(presenter, blockDecoration4), { - decoration: blockDecoration4 - screenRow: 10 - isVisible: true - } + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBe(blockDecoration3) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration4)).toBeUndefined() presenter.invalidateBlockDecorationDimensions(blockDecoration1) + presenter.invalidateBlockDecorationDimensions(blockDecoration4) presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) presenter.setBlockDecorationDimensions(blockDecoration3, 0, 10) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration4)).toBe(blockDecoration4) - expectValues stateForBlockDecoration(presenter, blockDecoration1), { - decoration: blockDecoration1 - screenRow: 0 - isVisible: false - } - expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - expect(stateForBlockDecoration(presenter, blockDecoration3)).toBeUndefined() - expectValues stateForBlockDecoration(presenter, blockDecoration4), { - decoration: blockDecoration4 - screenRow: 10 - isVisible: true - } + blockDecoration4.destroy() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration4)).toBeUndefined() - blockDecoration1.destroy() + it "contains state for off-screen block decorations that intersect a buffer change", -> + blockDecoration1 = addBlockDecorationBeforeScreenRow(9) + blockDecoration2 = addBlockDecorationBeforeScreenRow(10) + blockDecoration3 = addBlockDecorationBeforeScreenRow(11) + presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBe(blockDecoration1) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBe(blockDecoration3) - expect(stateForBlockDecoration(presenter, blockDecoration1)).toBeUndefined() - expect(stateForBlockDecoration(presenter, blockDecoration2)).toBeUndefined() - expect(stateForBlockDecoration(presenter, blockDecoration3)).toBeUndefined() - expectValues stateForBlockDecoration(presenter, blockDecoration4), { - decoration: blockDecoration4 - screenRow: 10 - isVisible: true - } + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration3, 0, 10) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBeUndefined() + + editor.setSelectedScreenRange([[10, 0], [12, 0]]) + editor.delete() + presenter.setScrollTop(0) # deleting the buffer causes the editor to autoscroll + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration3)).toBe(blockDecoration3) + + it "contains state for all off-screen block decorations when content frame width, window size or bounding client rect change", -> + blockDecoration1 = addBlockDecorationBeforeScreenRow(10) + blockDecoration2 = addBlockDecorationBeforeScreenRow(11) + presenter = buildPresenter(explicitHeight: 30, lineHeight: 10, tileSize: 2, scrollTop: 0) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBe(blockDecoration1) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 10) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 10) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + + presenter.setBoundingClientRect({top: 0, left: 0, width: 50, height: 30}) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBe(blockDecoration1) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + + presenter.setContentFrameWidth(100) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBe(blockDecoration1) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() + + presenter.setWindowSize(100, 200) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBe(blockDecoration1) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBe(blockDecoration2) + + presenter.setBlockDecorationDimensions(blockDecoration1, 0, 20) + presenter.setBlockDecorationDimensions(blockDecoration2, 0, 20) + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration1)).toBeUndefined() + expect(stateForOffScreenBlockDecoration(presenter, blockDecoration2)).toBeUndefined() it "doesn't throw an error when setting the dimensions for a destroyed decoration", -> blockDecoration = addBlockDecorationBeforeScreenRow(0) presenter = buildPresenter() - blockDecoration.destroy() presenter.setBlockDecorationDimensions(blockDecoration, 30, 30) - - expect(getState(presenter).content.blockDecorations).toEqual({}) + expect(getState(presenter).content.offScreenBlockDecorations).toEqual({}) describe ".overlays", -> [item] = [] diff --git a/src/block-decorations-component.coffee b/src/block-decorations-component.coffee deleted file mode 100644 index 48bbf77f3..000000000 --- a/src/block-decorations-component.coffee +++ /dev/null @@ -1,86 +0,0 @@ -cloneObject = (object) -> - clone = {} - clone[key] = value for key, value of object - clone - -module.exports = -class BlockDecorationsComponent - constructor: (@container, @views, @presenter, @domElementPool) -> - @newState = null - @oldState = null - @blockDecorationNodesById = {} - @domNode = @domElementPool.buildElement("content") - @domNode.setAttribute("select", ".atom--invisible-block-decoration") - @domNode.style.visibility = "hidden" - - getDomNode: -> - @domNode - - updateSync: (state) -> - @newState = state.content - @oldState ?= {blockDecorations: {}, width: 0} - - if @newState.width isnt @oldState.width - @domNode.style.width = @newState.width + "px" - @oldState.width = @newState.width - - for id of @oldState.blockDecorations - unless @newState.blockDecorations.hasOwnProperty(id) - blockDecorationNode = @blockDecorationNodesById[id] - blockDecorationNode.previousSibling.remove() - blockDecorationNode.nextSibling.remove() - blockDecorationNode.remove() - delete @blockDecorationNodesById[id] - delete @oldState.blockDecorations[id] - - for id of @newState.blockDecorations - if @oldState.blockDecorations.hasOwnProperty(id) - @updateBlockDecorationNode(id) - else - @oldState.blockDecorations[id] = {} - @createAndAppendBlockDecorationNode(id) - - measureBlockDecorations: -> - for decorationId, blockDecorationNode of @blockDecorationNodesById - decoration = @newState.blockDecorations[decorationId].decoration - topRuler = blockDecorationNode.previousSibling - bottomRuler = blockDecorationNode.nextSibling - - width = blockDecorationNode.offsetWidth - height = bottomRuler.offsetTop - topRuler.offsetTop - @presenter.setBlockDecorationDimensions(decoration, width, height) - - createAndAppendBlockDecorationNode: (id) -> - blockDecorationState = @newState.blockDecorations[id] - blockDecorationClass = "atom--block-decoration-#{id}" - topRuler = document.createElement("div") - blockDecorationNode = @views.getView(blockDecorationState.decoration.getProperties().item) - bottomRuler = document.createElement("div") - topRuler.classList.add(blockDecorationClass) - blockDecorationNode.classList.add(blockDecorationClass) - bottomRuler.classList.add(blockDecorationClass) - - @container.appendChild(topRuler) - @container.appendChild(blockDecorationNode) - @container.appendChild(bottomRuler) - - @blockDecorationNodesById[id] = blockDecorationNode - @updateBlockDecorationNode(id) - - updateBlockDecorationNode: (id) -> - newBlockDecorationState = @newState.blockDecorations[id] - oldBlockDecorationState = @oldState.blockDecorations[id] - blockDecorationNode = @blockDecorationNodesById[id] - - if newBlockDecorationState.isVisible - blockDecorationNode.previousSibling.classList.remove("atom--invisible-block-decoration") - blockDecorationNode.classList.remove("atom--invisible-block-decoration") - blockDecorationNode.nextSibling.classList.remove("atom--invisible-block-decoration") - else - blockDecorationNode.previousSibling.classList.add("atom--invisible-block-decoration") - blockDecorationNode.classList.add("atom--invisible-block-decoration") - blockDecorationNode.nextSibling.classList.add("atom--invisible-block-decoration") - - if oldBlockDecorationState.screenRow isnt newBlockDecorationState.screenRow - blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow - oldBlockDecorationState.screenRow = newBlockDecorationState.screenRow diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 0b19a0d43..d25b87b6b 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -19,7 +19,7 @@ module.exports = class LinesComponent extends TiledComponent placeholderTextDiv: null - constructor: ({@presenter, @domElementPool, @assert}) -> + constructor: ({@views, @presenter, @domElementPool, @assert}) -> @domNode = document.createElement('div') @domNode.classList.add('lines') @tilesNode = document.createElement("div") @@ -57,9 +57,17 @@ class LinesComponent extends TiledComponent @domNode.appendChild(@placeholderTextDiv) @oldState.placeholderText = @newState.placeholderText + # Removing and updating block decorations needs to be done in two different + # steps, so that the same decoration node can be moved from one tile to + # another in the same animation frame. + for component in @getComponents() + component.removeDeletedBlockDecorations() + for component in @getComponents() + component.updateBlockDecorations() + @cursorsComponent.updateSync(state) - buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert}) + buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @views}) buildEmptyState: -> {tiles: {}} @@ -83,6 +91,10 @@ class LinesComponent extends TiledComponent @presenter.setLineHeight(lineHeightInPixels) @presenter.setBaseCharacterWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) + measureBlockDecorations: -> + for component in @getComponents() + component.measureBlockDecorations() + lineIdForScreenRow: (screenRow) -> tile = @presenter.tileForRow(screenRow) @getComponentForTile(tile)?.lineIdForScreenRow(screenRow) diff --git a/src/lines-tile-component.js b/src/lines-tile-component.js index 1ac94bc42..a05762fc5 100644 --- a/src/lines-tile-component.js +++ b/src/lines-tile-component.js @@ -2,18 +2,17 @@ const HighlightsComponent = require('./highlights-component') const ZERO_WIDTH_NBSP = '\ufeff' module.exports = class LinesTileComponent { - constructor ({presenter, id, domElementPool, assert}) { - this.presenter = presenter + constructor ({presenter, id, domElementPool, assert, views}) { this.id = id + this.presenter = presenter + this.views = views this.domElementPool = domElementPool this.assert = assert - this.measuredLines = new Set() this.lineNodesByLineId = {} this.screenRowsByLineId = {} this.lineIdsByScreenRow = {} this.textNodesByLineId = {} - this.insertionPointsBeforeLineById = {} - this.insertionPointsAfterLineById = {} + this.blockDecorationNodesByLineIdAndDecorationId = {} this.domNode = this.domElementPool.buildElement('div') this.domNode.style.position = 'absolute' this.domNode.style.display = 'block' @@ -22,6 +21,7 @@ module.exports = class LinesTileComponent { } destroy () { + this.removeLineNodes() this.domElementPool.freeElementAndDescendants(this.domNode) } @@ -80,15 +80,29 @@ module.exports = class LinesTileComponent { } } - removeLineNode (id) { - this.domElementPool.freeElementAndDescendants(this.lineNodesByLineId[id]) - this.removeBlockDecorationInsertionPointBeforeLine(id) - this.removeBlockDecorationInsertionPointAfterLine(id) - delete this.lineNodesByLineId[id] - delete this.textNodesByLineId[id] - delete this.lineIdsByScreenRow[this.screenRowsByLineId[id]] - delete this.screenRowsByLineId[id] - delete this.oldTileState.lines[id] + removeLineNode (lineId) { + this.domElementPool.freeElementAndDescendants(this.lineNodesByLineId[lineId]) + for (const decorationId of Object.keys(this.oldTileState.lines[lineId].precedingBlockDecorations)) { + const {topRulerNode, blockDecorationNode, bottomRulerNode} = + this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] + topRulerNode.remove() + blockDecorationNode.remove() + bottomRulerNode.remove() + } + for (const decorationId of Object.keys(this.oldTileState.lines[lineId].followingBlockDecorations)) { + const {topRulerNode, blockDecorationNode, bottomRulerNode} = + this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] + topRulerNode.remove() + blockDecorationNode.remove() + bottomRulerNode.remove() + } + + delete this.blockDecorationNodesByLineIdAndDecorationId[lineId] + delete this.lineNodesByLineId[lineId] + delete this.textNodesByLineId[lineId] + delete this.lineIdsByScreenRow[this.screenRowsByLineId[lineId]] + delete this.screenRowsByLineId[lineId] + delete this.oldTileState.lines[lineId] } updateLineNodes () { @@ -110,6 +124,10 @@ module.exports = class LinesTileComponent { this.screenRowsByLineId[id] = lineState.screenRow this.lineIdsByScreenRow[lineState.screenRow] = id this.oldTileState.lines[id] = Object.assign({}, lineState) + // Avoid assigning state for block decorations, because we need to + // process it later when updating the DOM. + this.oldTileState.lines[id].precedingBlockDecorations = {} + this.oldTileState.lines[id].followingBlockDecorations = {} } } @@ -123,87 +141,6 @@ module.exports = class LinesTileComponent { } else { this.domNode.insertBefore(lineNode, nextNode) } - this.insertBlockDecorationInsertionPointBeforeLine(id) - this.insertBlockDecorationInsertionPointAfterLine(id) - } - } - - removeBlockDecorationInsertionPointBeforeLine (id) { - const insertionPoint = this.insertionPointsBeforeLineById[id] - if (insertionPoint != null) { - this.domElementPool.freeElementAndDescendants(insertionPoint) - delete this.insertionPointsBeforeLineById[id] - } - } - - insertBlockDecorationInsertionPointBeforeLine (id) { - const {hasPrecedingBlockDecorations, screenRow} = this.newTileState.lines[id] - if (hasPrecedingBlockDecorations) { - const lineNode = this.lineNodesByLineId[id] - const insertionPoint = this.domElementPool.buildElement('content') - this.domNode.insertBefore(insertionPoint, lineNode) - this.insertionPointsBeforeLineById[id] = insertionPoint - insertionPoint.dataset.screenRow = screenRow - this.updateBlockDecorationInsertionPointBeforeLine(id) - } - } - - updateBlockDecorationInsertionPointBeforeLine (id) { - const oldLineState = this.oldTileState.lines[id] - const newLineState = this.newTileState.lines[id] - const insertionPoint = this.insertionPointsBeforeLineById[id] - if (insertionPoint != null) { - if (newLineState.screenRow !== oldLineState.screenRow) { - insertionPoint.dataset.screenRow = newLineState.screenRow - } - - const precedingBlockDecorationsSelector = newLineState.precedingBlockDecorations - .map((d) => `.atom--block-decoration-${d.id}`) - .join(',') - if (precedingBlockDecorationsSelector !== oldLineState.precedingBlockDecorationsSelector) { - insertionPoint.setAttribute('select', precedingBlockDecorationsSelector) - oldLineState.precedingBlockDecorationsSelector = precedingBlockDecorationsSelector - } - } - } - - removeBlockDecorationInsertionPointAfterLine (id) { - const insertionPoint = this.insertionPointsAfterLineById[id] - if (insertionPoint != null) { - this.domElementPool.freeElementAndDescendants(insertionPoint) - delete this.insertionPointsAfterLineById[id] - } - } - - insertBlockDecorationInsertionPointAfterLine (id) { - const {hasFollowingBlockDecorations, screenRow} = this.newTileState.lines[id] - if (hasFollowingBlockDecorations) { - const lineNode = this.lineNodesByLineId[id] - const insertionPoint = this.domElementPool.buildElement('content') - this.domNode.insertBefore(insertionPoint, lineNode.nextSibling) - this.insertionPointsAfterLineById[id] = insertionPoint - insertionPoint.dataset.screenRow = screenRow - this.updateBlockDecorationInsertionPointAfterLine(id) - } - } - - updateBlockDecorationInsertionPointAfterLine (id) { - const oldLineState = this.oldTileState.lines[id] - const newLineState = this.newTileState.lines[id] - const insertionPoint = this.insertionPointsAfterLineById[id] - - if (insertionPoint != null) { - if (newLineState.screenRow !== oldLineState.screenRow) { - insertionPoint.dataset.screenRow = newLineState.screenRow - } - - const followingBlockDecorationsSelector = newLineState.followingBlockDecorations - .map((d) => `.atom--block-decoration-${d.id}`) - .join(',') - if (followingBlockDecorationsSelector !== oldLineState.followingBlockDecorationsSelector) { - insertionPoint.setAttribute('select', followingBlockDecorationsSelector) - oldLineState.followingBlockDecorationsSelector = followingBlockDecorationsSelector - } } } @@ -296,29 +233,143 @@ module.exports = class LinesTileComponent { oldLineState.decorationClasses = newLineState.decorationClasses - if (!oldLineState.hasPrecedingBlockDecorations && newLineState.hasPrecedingBlockDecorations) { - this.insertBlockDecorationInsertionPointBeforeLine(id) - } else if (oldLineState.hasPrecedingBlockDecorations && !newLineState.hasPrecedingBlockDecorations) { - this.removeBlockDecorationInsertionPointBeforeLine(id) - } - - if (!oldLineState.hasFollowingBlockDecorations && newLineState.hasFollowingBlockDecorations) { - this.insertBlockDecorationInsertionPointAfterLine(id) - } else if (oldLineState.hasFollowingBlockDecorations && !newLineState.hasFollowingBlockDecorations) { - this.removeBlockDecorationInsertionPointAfterLine(id) - } - if (newLineState.screenRow !== oldLineState.screenRow) { lineNode.dataset.screenRow = newLineState.screenRow this.lineIdsByScreenRow[newLineState.screenRow] = id this.screenRowsByLineId[id] = newLineState.screenRow } - this.updateBlockDecorationInsertionPointBeforeLine(id) - this.updateBlockDecorationInsertionPointAfterLine(id) oldLineState.screenRow = newLineState.screenRow - oldLineState.hasPrecedingBlockDecorations = newLineState.hasPrecedingBlockDecorations - oldLineState.hasFollowingBlockDecorations = newLineState.hasFollowingBlockDecorations + } + + removeDeletedBlockDecorations () { + for (const lineId of Object.keys(this.newTileState.lines)) { + const oldLineState = this.oldTileState.lines[lineId] + const newLineState = this.newTileState.lines[lineId] + const lineNode = this.lineNodesByLineId[lineId] + for (const decorationId of Object.keys(oldLineState.precedingBlockDecorations)) { + if (!newLineState.precedingBlockDecorations.hasOwnProperty(decorationId)) { + const {topRulerNode, blockDecorationNode, bottomRulerNode} = + this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] + topRulerNode.remove() + blockDecorationNode.remove() + bottomRulerNode.remove() + delete this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] + delete oldLineState.precedingBlockDecorations[decorationId] + } + } + for (const decorationId of Object.keys(oldLineState.followingBlockDecorations)) { + if (!newLineState.followingBlockDecorations.hasOwnProperty(decorationId)) { + const {topRulerNode, blockDecorationNode, bottomRulerNode} = + this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] + topRulerNode.remove() + blockDecorationNode.remove() + bottomRulerNode.remove() + delete this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] + delete oldLineState.followingBlockDecorations[decorationId] + } + } + } + } + + updateBlockDecorations () { + for (const lineId of Object.keys(this.newTileState.lines)) { + const oldLineState = this.oldTileState.lines[lineId] + const newLineState = this.newTileState.lines[lineId] + const lineNode = this.lineNodesByLineId[lineId] + if (!this.blockDecorationNodesByLineIdAndDecorationId.hasOwnProperty(lineId)) { + this.blockDecorationNodesByLineIdAndDecorationId[lineId] = {} + } + for (const decorationId of Object.keys(newLineState.precedingBlockDecorations)) { + const oldBlockDecorationState = oldLineState.precedingBlockDecorations[decorationId] + const newBlockDecorationState = newLineState.precedingBlockDecorations[decorationId] + if (oldBlockDecorationState != null) { + const {topRulerNode, blockDecorationNode, bottomRulerNode} = + this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] + if (oldBlockDecorationState.screenRow !== newBlockDecorationState.screenRow) { + topRulerNode.remove() + blockDecorationNode.remove() + bottomRulerNode.remove() + topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow + this.domNode.insertBefore(topRulerNode, lineNode) + blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow + this.domNode.insertBefore(blockDecorationNode, lineNode) + bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow + this.domNode.insertBefore(bottomRulerNode, lineNode) + } + } else { + const topRulerNode = document.createElement('div') + topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow + this.domNode.insertBefore(topRulerNode, lineNode) + const blockDecorationNode = this.views.getView(newBlockDecorationState.decoration.getProperties().item) + blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow + this.domNode.insertBefore(blockDecorationNode, lineNode) + const bottomRulerNode = document.createElement('div') + bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow + this.domNode.insertBefore(bottomRulerNode, lineNode) + + this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] = + {topRulerNode, blockDecorationNode, bottomRulerNode} + } + oldLineState.precedingBlockDecorations[decorationId] = Object.assign({}, newBlockDecorationState) + } + for (const decorationId of Object.keys(newLineState.followingBlockDecorations)) { + const oldBlockDecorationState = oldLineState.followingBlockDecorations[decorationId] + const newBlockDecorationState = newLineState.followingBlockDecorations[decorationId] + if (oldBlockDecorationState != null) { + const {topRulerNode, blockDecorationNode, bottomRulerNode} = + this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] + if (oldBlockDecorationState.screenRow !== newBlockDecorationState.screenRow) { + topRulerNode.remove() + blockDecorationNode.remove() + bottomRulerNode.remove() + bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow + this.domNode.insertBefore(bottomRulerNode, lineNode.nextSibling) + blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow + this.domNode.insertBefore(blockDecorationNode, lineNode.nextSibling) + topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow + this.domNode.insertBefore(topRulerNode, lineNode.nextSibling) + } + } else { + const bottomRulerNode = document.createElement('div') + bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow + this.domNode.insertBefore(bottomRulerNode, lineNode.nextSibling) + const blockDecorationNode = this.views.getView(newBlockDecorationState.decoration.getProperties().item) + blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow + this.domNode.insertBefore(blockDecorationNode, lineNode.nextSibling) + const topRulerNode = document.createElement('div') + topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow + this.domNode.insertBefore(topRulerNode, lineNode.nextSibling) + + this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] = + {topRulerNode, blockDecorationNode, bottomRulerNode} + } + oldLineState.followingBlockDecorations[decorationId] = Object.assign({}, newBlockDecorationState) + } + } + } + + measureBlockDecorations () { + for (const lineId of Object.keys(this.newTileState.lines)) { + const newLineState = this.newTileState.lines[lineId] + + for (const decorationId of Object.keys(newLineState.precedingBlockDecorations)) { + const {topRulerNode, blockDecorationNode, bottomRulerNode} = + this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] + const width = blockDecorationNode.offsetWidth + const height = bottomRulerNode.offsetTop - topRulerNode.offsetTop + const {decoration} = newLineState.precedingBlockDecorations[decorationId] + this.presenter.setBlockDecorationDimensions(decoration, width, height) + } + for (const decorationId of Object.keys(newLineState.followingBlockDecorations)) { + const {topRulerNode, blockDecorationNode, bottomRulerNode} = + this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] + const width = blockDecorationNode.offsetWidth + const height = bottomRulerNode.offsetTop - topRulerNode.offsetTop + const {decoration} = newLineState.followingBlockDecorations[decorationId] + this.presenter.setBlockDecorationDimensions(decoration, width, height) + } + } } lineNodeForScreenRow (screenRow) { diff --git a/src/off-screen-block-decorations-component.js b/src/off-screen-block-decorations-component.js new file mode 100644 index 000000000..d651211be --- /dev/null +++ b/src/off-screen-block-decorations-component.js @@ -0,0 +1,61 @@ +module.exports = class OffScreenBlockDecorationsComponent { + constructor ({presenter, views}) { + this.presenter = presenter + this.views = views + this.newState = {offScreenBlockDecorations: {}, width: 0} + this.oldState = {offScreenBlockDecorations: {}, width: 0} + this.domNode = document.createElement('div') + this.domNode.style.visibility = 'hidden' + this.blockDecorationNodesById = {} + } + + getDomNode () { + return this.domNode + } + + updateSync (state) { + this.newState = state.content + + if (this.newState.width !== this.oldState.width) { + this.domNode.style.width = `${this.newState.width}px` + this.oldState.width = this.newState.width + } + + for (const id of Object.keys(this.oldState.offScreenBlockDecorations)) { + if (!this.newState.offScreenBlockDecorations.hasOwnProperty(id)) { + const {topRuler, blockDecoration, bottomRuler} = this.blockDecorationNodesById[id] + topRuler.remove() + blockDecoration.remove() + bottomRuler.remove() + delete this.blockDecorationNodesById[id] + delete this.oldState.offScreenBlockDecorations[id] + } + } + + for (const id of Object.keys(this.newState.offScreenBlockDecorations)) { + const decoration = this.newState.offScreenBlockDecorations[id] + if (!this.oldState.offScreenBlockDecorations.hasOwnProperty(id)) { + const topRuler = document.createElement('div') + this.domNode.appendChild(topRuler) + const blockDecoration = this.views.getView(decoration.getProperties().item) + this.domNode.appendChild(blockDecoration) + const bottomRuler = document.createElement('div') + this.domNode.appendChild(bottomRuler) + + this.blockDecorationNodesById[id] = {topRuler, blockDecoration, bottomRuler} + } + + this.oldState.offScreenBlockDecorations[id] = decoration + } + } + + measureBlockDecorations () { + for (const id of Object.keys(this.blockDecorationNodesById)) { + const {topRuler, blockDecoration, bottomRuler} = this.blockDecorationNodesById[id] + const width = blockDecoration.offsetWidth + const height = bottomRuler.offsetTop - topRuler.offsetTop + const decoration = this.newState.offScreenBlockDecorations[id] + this.presenter.setBlockDecorationDimensions(decoration, width, height) + } + } +} diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 60033dc88..a3fac1e81 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -8,12 +8,12 @@ TextEditorPresenter = require './text-editor-presenter' GutterContainerComponent = require './gutter-container-component' InputComponent = require './input-component' LinesComponent = require './lines-component' +OffScreenBlockDecorationsComponent = require './off-screen-block-decorations-component' ScrollbarComponent = require './scrollbar-component' ScrollbarCornerComponent = require './scrollbar-corner-component' OverlayManager = require './overlay-manager' DOMElementPool = require './dom-element-pool' LinesYardstick = require './lines-yardstick' -BlockDecorationsComponent = require './block-decorations-component' LineTopIndex = require 'line-top-index' module.exports = @@ -69,7 +69,6 @@ class TextEditorComponent @domNode.classList.add('editor--private') @overlayManager = new OverlayManager(@presenter, @domNode, @views) - @blockDecorationsComponent = new BlockDecorationsComponent(@hostElement, @views, @presenter, @domElementPool) @scrollViewNode = document.createElement('div') @scrollViewNode.classList.add('scroll-view') @@ -78,10 +77,11 @@ class TextEditorComponent @hiddenInputComponent = new InputComponent @scrollViewNode.appendChild(@hiddenInputComponent.getDomNode()) - @linesComponent = new LinesComponent({@presenter, @hostElement, @domElementPool, @assert, @grammars}) + @linesComponent = new LinesComponent({@presenter, @hostElement, @domElementPool, @assert, @grammars, @views}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) - @linesComponent.getDomNode().appendChild(@blockDecorationsComponent.getDomNode()) + @offScreenBlockDecorationsComponent = new OffScreenBlockDecorationsComponent({@presenter, @views}) + @scrollViewNode.appendChild(@offScreenBlockDecorationsComponent.getDomNode()) @linesYardstick = new LinesYardstick(@editor, @linesComponent, lineTopIndex) @presenter.setLinesYardstick(@linesYardstick) @@ -165,8 +165,8 @@ class TextEditorComponent @gutterContainerComponent = null @hiddenInputComponent.updateSync(@newState) + @offScreenBlockDecorationsComponent.updateSync(@newState) @linesComponent.updateSync(@newState) - @blockDecorationsComponent?.updateSync(@newState) @horizontalScrollbarComponent.updateSync(@newState) @verticalScrollbarComponent.updateSync(@newState) @scrollbarCornerComponent.updateSync(@newState) @@ -186,7 +186,8 @@ class TextEditorComponent readAfterUpdateSync: => @overlayManager?.measureOverlays() - @blockDecorationsComponent?.measureBlockDecorations() if @isVisible() + @linesComponent.measureBlockDecorations() + @offScreenBlockDecorationsComponent.measureBlockDecorations() mountGutterContainerComponent: -> @gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool, @views}) @@ -543,7 +544,7 @@ class TextEditorComponent screenPosition = @screenPositionForMouseEvent(event) - if event.target?.classList.contains('syntax--fold-marker') + if event.target?.classList.contains('fold-marker') bufferPosition = @editor.bufferPositionForScreenPosition(screenPosition) @editor.destroyFoldsIntersectingBufferRange([bufferPosition, bufferPosition]) return diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 90de3fd91..3a5078f26 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -34,6 +34,8 @@ class TextEditorPresenter @observedBlockDecorations = new Set() @invalidatedDimensionsByBlockDecoration = new Set() @invalidateAllBlockDecorationsDimensions = false + @precedingBlockDecorationsByScreenRowAndId = {} + @followingBlockDecorationsByScreenRowAndId = {} @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @@ -191,7 +193,7 @@ class TextEditorPresenter highlights: {} overlays: {} cursors: {} - blockDecorations: {} + offScreenBlockDecorations: {} gutters: [] # Shared state that is copied into ``@state.gutters`. @sharedGutterStyles = {} @@ -412,16 +414,14 @@ class TextEditorPresenter throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}") visibleLineIds[line.id] = true - precedingBlockDecorations = @precedingBlockDecorationsByScreenRow[screenRow] ? [] - followingBlockDecorations = @followingBlockDecorationsByScreenRow[screenRow] ? [] + precedingBlockDecorations = @precedingBlockDecorationsByScreenRowAndId[screenRow] ? {} + followingBlockDecorations = @followingBlockDecorationsByScreenRowAndId[screenRow] ? {} if tileState.lines.hasOwnProperty(line.id) lineState = tileState.lines[line.id] lineState.screenRow = screenRow lineState.decorationClasses = @lineDecorationClassesForRow(screenRow) lineState.precedingBlockDecorations = precedingBlockDecorations lineState.followingBlockDecorations = followingBlockDecorations - lineState.hasPrecedingBlockDecorations = precedingBlockDecorations.length > 0 - lineState.hasFollowingBlockDecorations = followingBlockDecorations.length > 0 else tileState.lines[line.id] = screenRow: screenRow @@ -430,8 +430,6 @@ class TextEditorPresenter decorationClasses: @lineDecorationClassesForRow(screenRow) precedingBlockDecorations: precedingBlockDecorations followingBlockDecorations: followingBlockDecorations - hasPrecedingBlockDecorations: precedingBlockDecorations.length > 0 - hasFollowingBlockDecorations: followingBlockDecorations.length > 0 for id, line of tileState.lines delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) @@ -1059,41 +1057,42 @@ class TextEditorPresenter @decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1) updateBlockDecorations: -> - @blockDecorationsToRenderById = {} - @precedingBlockDecorationsByScreenRow = {} - @followingBlockDecorationsByScreenRow = {} - visibleDecorationsByMarkerId = @model.decorationsForScreenRowRange(@getStartTileRow(), @getEndTileRow() + @tileSize - 1) - if @invalidateAllBlockDecorationsDimensions for decoration in @model.getDecorations(type: 'block') @invalidatedDimensionsByBlockDecoration.add(decoration) @invalidateAllBlockDecorationsDimensions = false - for markerId, decorations of visibleDecorationsByMarkerId + visibleDecorationsById = {} + visibleDecorationsByScreenRowAndId = {} + for markerId, decorations of @model.decorationsForScreenRowRange(@getStartTileRow(), @getEndTileRow() + @tileSize - 1) for decoration in decorations when decoration.isType('block') - @updateBlockDecorationState(decoration, true) + screenRow = decoration.getMarker().getHeadScreenPosition().row + if decoration.getProperties().position is "after" + @followingBlockDecorationsByScreenRowAndId[screenRow] ?= {} + @followingBlockDecorationsByScreenRowAndId[screenRow][decoration.id] = {screenRow, decoration} + else + @precedingBlockDecorationsByScreenRowAndId[screenRow] ?= {} + @precedingBlockDecorationsByScreenRowAndId[screenRow][decoration.id] = {screenRow, decoration} + visibleDecorationsById[decoration.id] = true + visibleDecorationsByScreenRowAndId[screenRow] ?= {} + visibleDecorationsByScreenRowAndId[screenRow][decoration.id] = true + for screenRow, blockDecorations of @precedingBlockDecorationsByScreenRowAndId + if Number(screenRow) isnt @mouseWheelScreenRow + for id, blockDecoration of blockDecorations + unless visibleDecorationsByScreenRowAndId[screenRow]?[id] + delete @precedingBlockDecorationsByScreenRowAndId[screenRow][id] + + for screenRow, blockDecorations of @followingBlockDecorationsByScreenRowAndId + if Number(screenRow) isnt @mouseWheelScreenRow + for id, blockDecoration of blockDecorations + unless visibleDecorationsByScreenRowAndId[screenRow]?[id] + delete @followingBlockDecorationsByScreenRowAndId[screenRow][id] + + @state.content.offScreenBlockDecorations = {} @invalidatedDimensionsByBlockDecoration.forEach (decoration) => - @updateBlockDecorationState(decoration, false) - - for decorationId, decorationState of @state.content.blockDecorations - continue if @blockDecorationsToRenderById[decorationId] - continue if decorationState.screenRow is @mouseWheelScreenRow - - delete @state.content.blockDecorations[decorationId] - - updateBlockDecorationState: (decoration, isVisible) -> - return if @blockDecorationsToRenderById[decoration.getId()] - - screenRow = decoration.getMarker().getHeadScreenPosition().row - if decoration.getProperties().position is "after" - @followingBlockDecorationsByScreenRow[screenRow] ?= [] - @followingBlockDecorationsByScreenRow[screenRow].push(decoration) - else - @precedingBlockDecorationsByScreenRow[screenRow] ?= [] - @precedingBlockDecorationsByScreenRow[screenRow].push(decoration) - @state.content.blockDecorations[decoration.getId()] = {decoration, screenRow, isVisible} - @blockDecorationsToRenderById[decoration.getId()] = true + unless visibleDecorationsById[decoration.id] + @state.content.offScreenBlockDecorations[decoration.id] = decoration updateLineDecorations: -> @lineDecorationsByScreenRow = {} @@ -1295,7 +1294,7 @@ class TextEditorPresenter setBlockDecorationDimensions: (decoration, width, height) -> return unless @observedBlockDecorations.has(decoration) - @lineTopIndex.resizeBlock(decoration.getId(), height) + @lineTopIndex.resizeBlock(decoration.id, height) @invalidatedDimensionsByBlockDecoration.delete(decoration) @shouldUpdateDecorations = true @@ -1332,7 +1331,7 @@ class TextEditorPresenter @didDestroyBlockDecoration(decoration) isAfter = decoration.getProperties().position is "after" - @lineTopIndex.insertBlock(decoration.getId(), decoration.getMarker().getHeadScreenPosition().row, 0, isAfter) + @lineTopIndex.insertBlock(decoration.id, decoration.getMarker().getHeadScreenPosition().row, 0, isAfter) @observedBlockDecorations.add(decoration) @invalidateBlockDecorationDimensions(decoration) @@ -1346,14 +1345,14 @@ class TextEditorPresenter # change. return if markerEvent.textChanged - @lineTopIndex.moveBlock(decoration.getId(), decoration.getMarker().getHeadScreenPosition().row) + @lineTopIndex.moveBlock(decoration.id, decoration.getMarker().getHeadScreenPosition().row) @shouldUpdateDecorations = true @emitDidUpdateState() didDestroyBlockDecoration: (decoration) -> return unless @observedBlockDecorations.has(decoration) - @lineTopIndex.removeBlock(decoration.getId()) + @lineTopIndex.removeBlock(decoration.id) @observedBlockDecorations.delete(decoration) @invalidatedDimensionsByBlockDecoration.delete(decoration) @shouldUpdateDecorations = true diff --git a/src/tiled-component.coffee b/src/tiled-component.coffee index 2e8dc7149..37de27e9b 100644 --- a/src/tiled-component.coffee +++ b/src/tiled-component.coffee @@ -1,10 +1,3 @@ -{values} = require 'underscore-plus' - -cloneObject = (object) -> - clone = {} - clone[key] = value for key, value of object - clone - module.exports = class TiledComponent updateSync: (state) -> @@ -41,7 +34,7 @@ class TiledComponent component = @componentsByTileId[tileRow] = @buildComponentForTile(tileRow) @getTilesNode().appendChild(component.getDomNode()) - @oldState.tiles[tileRow] = cloneObject(tileState) + @oldState.tiles[tileRow] = Object.assign({}, tileState) component.updateSync(@newState) @@ -50,5 +43,9 @@ class TiledComponent getComponentForTile: (tileRow) -> @componentsByTileId[tileRow] + getComponents: -> + for _, component of @componentsByTileId + component + getTiles: -> - values(@componentsByTileId).map (component) -> component.getDomNode() + @getComponents().map((component) -> component.getDomNode())