From 4c5101a7e15356d32649fbef4db6ddb1c5fb2416 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 15 Sep 2015 15:44:06 +0200 Subject: [PATCH 01/80] :art: Refactor ::updateTilesState to a composed method --- src/text-editor-presenter.coffee | 76 +++++++++++++++++++------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index cf31186a9..79cbe5b51 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -331,48 +331,64 @@ class TextEditorPresenter (@getEndTileRow() - @getStartTileRow() + 1) / @tileSize ) - updateTilesState: -> - return unless @startRow? and @endRow? and @lineHeight? + updateLinesTileState: (startRow, zIndex) -> + endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize) - visibleTiles = {} + tile = @state.content.tiles[startRow] ?= {} + tile.top = startRow * @lineHeight - @scrollTop + tile.left = -@scrollLeft + tile.height = @tileSize * @lineHeight + tile.display = "block" + tile.zIndex = zIndex + tile.highlights ?= {} + + @updateLinesState(tile, startRow, endRow) if @shouldUpdateLinesState + + updateLineNumbersTileState: (startRow, zIndex) -> + endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize) + + gutterTile = @lineNumberGutter.tiles[startRow] ?= {} + gutterTile.top = startRow * @lineHeight - @scrollTop + gutterTile.height = @tileSize * @lineHeight + gutterTile.display = "block" + gutterTile.zIndex = zIndex + + @updateLineNumbersState(gutterTile, startRow, endRow) if @shouldUpdateLineNumbersState + + updateVisibleTilesState: -> zIndex = @getTilesCount() - 1 for startRow in [@getStartTileRow()..@getEndTileRow()] by @tileSize - endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize) - - tile = @state.content.tiles[startRow] ?= {} - tile.top = startRow * @lineHeight - @scrollTop - tile.left = -@scrollLeft - tile.height = @tileSize * @lineHeight - tile.display = "block" - tile.zIndex = zIndex - tile.highlights ?= {} - - gutterTile = @lineNumberGutter.tiles[startRow] ?= {} - gutterTile.top = startRow * @lineHeight - @scrollTop - gutterTile.height = @tileSize * @lineHeight - gutterTile.display = "block" - gutterTile.zIndex = zIndex - - @updateLinesState(tile, startRow, endRow) if @shouldUpdateLinesState - @updateLineNumbersState(gutterTile, startRow, endRow) if @shouldUpdateLineNumbersState - - visibleTiles[startRow] = true + @updateLinesTileState(startRow, zIndex) + @updateLineNumbersTileState(startRow, zIndex) + @visibleTiles[startRow] = true zIndex-- - if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)? - mouseWheelTile = @tileForRow(@mouseWheelScreenRow) + updateMouseWheelTileState: -> + return unless @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)? - unless visibleTiles[mouseWheelTile]? - @lineNumberGutter.tiles[mouseWheelTile].display = "none" - @state.content.tiles[mouseWheelTile].display = "none" - visibleTiles[mouseWheelTile] = true + mouseWheelTile = @tileForRow(@mouseWheelScreenRow) + unless @visibleTiles[mouseWheelTile]? + @lineNumberGutter.tiles[mouseWheelTile].display = "none" + @state.content.tiles[mouseWheelTile].display = "none" + @visibleTiles[mouseWheelTile] = true + + deleteHiddenTilesState: -> for id, tile of @state.content.tiles - continue if visibleTiles.hasOwnProperty(id) + continue if @visibleTiles.hasOwnProperty(id) delete @state.content.tiles[id] delete @lineNumberGutter.tiles[id] + updateTilesState: -> + return unless @startRow? and @endRow? and @lineHeight? + + @visibleTiles = {} + + @updateVisibleTilesState() + @updateMouseWheelTileState() + @deleteHiddenTilesState() + updateLinesState: (tileState, startRow, endRow) -> tileState.lines ?= {} visibleLineIds = {} From 8e06e06899d3c0a88277d22120d89842f6f02597 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 15 Sep 2015 16:32:42 +0200 Subject: [PATCH 02/80] Compute the state for the longest line on screen --- spec/text-editor-presenter-spec.coffee | 37 +++++++++++++++- src/text-editor-presenter.coffee | 60 +++++++++++++++----------- 2 files changed, 70 insertions(+), 27 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 76589416e..d1602ae93 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -832,11 +832,46 @@ describe "TextEditorPresenter", -> lineStateForScreenRow = (presenter, row) -> lineId = presenter.model.tokenizedLineForScreenRow(row).id tileRow = presenter.tileForRow(row) - presenter.getState().content.tiles[tileRow]?.lines[lineId] + presenter.getState().content.tiles[tileRow]?.lines?[lineId] tiledContentContract (presenter) -> presenter.getState().content + it "contains the state for the longest tile on screen", -> + presenter = buildPresenter(explicitHeight: 4, scrollTop: 0, lineHeight: 1, tileSize: 2) + + expectValues presenter.getState().content.tiles[6], { + visibility: "hidden" + } + + expectStateUpdate presenter, -> presenter.setScrollTop(2) + + expectValues presenter.getState().content.tiles[6], { + visibility: "initial" + } + + expectStateUpdate presenter, -> presenter.setScrollTop(0) + + expectValues presenter.getState().content.tiles[6], { + visibility: "hidden" + } + describe "[tileId].lines[lineId]", -> # line state objects + it "includes the state for the longest line on screen", -> + presenter = buildPresenter(explicitHeight: 4, scrollTop: 0, lineHeight: 1, tileSize: 2) + + expect(lineStateForScreenRow(presenter, 6)).toBeDefined() + expect(lineStateForScreenRow(presenter, 7)).toBeUndefined() + + expectStateUpdate presenter, -> presenter.setScrollTop(2) + + expect(lineStateForScreenRow(presenter, 6)).toBeDefined() + expect(lineStateForScreenRow(presenter, 7)).toBeDefined() + + expectStateUpdate presenter, -> presenter.setScrollTop(0) + + expect(lineStateForScreenRow(presenter, 6)).toBeDefined() + expect(lineStateForScreenRow(presenter, 7)).toBeUndefined() + it "includes the state for visible lines in a tile", -> presenter = buildPresenter(explicitHeight: 3, scrollTop: 4, lineHeight: 1, tileSize: 3, stoppedScrollingDelay: 200) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 79cbe5b51..b6c812123 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -331,35 +331,29 @@ class TextEditorPresenter (@getEndTileRow() - @getStartTileRow() + 1) / @tileSize ) - updateLinesTileState: (startRow, zIndex) -> - endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize) - - tile = @state.content.tiles[startRow] ?= {} - tile.top = startRow * @lineHeight - @scrollTop - tile.left = -@scrollLeft - tile.height = @tileSize * @lineHeight - tile.display = "block" - tile.zIndex = zIndex - tile.highlights ?= {} - - @updateLinesState(tile, startRow, endRow) if @shouldUpdateLinesState - - updateLineNumbersTileState: (startRow, zIndex) -> - endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize) - - gutterTile = @lineNumberGutter.tiles[startRow] ?= {} - gutterTile.top = startRow * @lineHeight - @scrollTop - gutterTile.height = @tileSize * @lineHeight - gutterTile.display = "block" - gutterTile.zIndex = zIndex - - @updateLineNumbersState(gutterTile, startRow, endRow) if @shouldUpdateLineNumbersState - updateVisibleTilesState: -> zIndex = @getTilesCount() - 1 for startRow in [@getStartTileRow()..@getEndTileRow()] by @tileSize - @updateLinesTileState(startRow, zIndex) - @updateLineNumbersTileState(startRow, zIndex) + endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize) + + tile = @state.content.tiles[startRow] ?= {} + tile.top = startRow * @lineHeight - @scrollTop + tile.left = -@scrollLeft + tile.height = @tileSize * @lineHeight + tile.display = "block" + tile.visibility = "initial" + tile.zIndex = zIndex + tile.highlights ?= {} + + gutterTile = @lineNumberGutter.tiles[startRow] ?= {} + gutterTile.top = startRow * @lineHeight - @scrollTop + gutterTile.height = @tileSize * @lineHeight + gutterTile.display = "block" + gutterTile.zIndex = zIndex + + @updateLinesState(tile, startRow, endRow) if @shouldUpdateLinesState + @updateLineNumbersState(gutterTile, startRow, endRow) if @shouldUpdateLineNumbersState + @visibleTiles[startRow] = true zIndex-- @@ -373,6 +367,19 @@ class TextEditorPresenter @state.content.tiles[mouseWheelTile].display = "none" @visibleTiles[mouseWheelTile] = true + updateLongestTileState: -> + longestScreenRow = @model.getLongestScreenRow() + longestScreenRowTile = @tileForRow(longestScreenRow) + + return if @getStartTileRow() <= longestScreenRowTile <= @getEndTileRow() + + tile = @state.content.tiles[longestScreenRowTile] ?= {} + tile.visibility = "hidden" + + @updateLinesState(tile, longestScreenRow, longestScreenRow + 1) + + @visibleTiles[longestScreenRowTile] = true + deleteHiddenTilesState: -> for id, tile of @state.content.tiles continue if @visibleTiles.hasOwnProperty(id) @@ -386,6 +393,7 @@ class TextEditorPresenter @visibleTiles = {} @updateVisibleTilesState() + @updateLongestTileState() @updateMouseWheelTileState() @deleteHiddenTilesState() From a6c13d097a7dd0b4ef970630d3c9051499b8111a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 15 Sep 2015 16:53:55 +0200 Subject: [PATCH 03/80] Render the longest screen row without painting it --- spec/text-editor-component-spec.coffee | 50 +++++++++++++++++++++++++- src/lines-tile-component.coffee | 4 +++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 2baa90ac7..b19116bd2 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -219,6 +219,54 @@ describe "TextEditorComponent", -> expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) + fffit "renders the longest screen row even when it's not visible", -> + wrapperNode.style.height = 2 * lineHeightInPixels + 'px' + component.measureDimensions() + nextAnimationFrame() + + tileNodes = component.tileNodesForLines() + expect(tileNodes.length).toBe(3) + + expect(getComputedStyle(tileNodes[0]).visibility).toBe("visible") + expect(tileNodes[0].querySelectorAll(".line").length).toBe(3) + + expect(getComputedStyle(tileNodes[1]).visibility).toBe("visible") + expect(tileNodes[1].querySelectorAll(".line").length).toBe(3) + + # tile with the longest screen row + expect(getComputedStyle(tileNodes[2]).visibility).toBe("hidden") + expect(tileNodes[2].querySelectorAll(".line").length).toBe(1) + + verticalScrollbarNode.scrollTop = 4 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + nextAnimationFrame() + + tileNodes = component.tileNodesForLines() + expect(tileNodes.length).toBe(2) # don't render an extra tile if the longest screen row is already visible + + expect(getComputedStyle(tileNodes[0]).visibility).toBe("visible") + expect(tileNodes[0].querySelectorAll(".line").length).toBe(3) + + expect(getComputedStyle(tileNodes[1]).visibility).toBe("visible") + expect(tileNodes[1].querySelectorAll(".line").length).toBe(3) + + verticalScrollbarNode.scrollTop = 0 * lineHeightInPixels + verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) + nextAnimationFrame() + + tileNodes = component.tileNodesForLines() + expect(tileNodes.length).toBe(3) + + expect(getComputedStyle(tileNodes[0]).visibility).toBe("visible") + expect(tileNodes[0].querySelectorAll(".line").length).toBe(3) + + expect(getComputedStyle(tileNodes[1]).visibility).toBe("visible") + expect(tileNodes[1].querySelectorAll(".line").length).toBe(3) + + # tile with the longest screen row + expect(getComputedStyle(tileNodes[2]).visibility).toBe("hidden") + expect(tileNodes[2].querySelectorAll(".line").length).toBe(1) + it "updates the lines when lines are inserted or removed above the rendered row range", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() @@ -2986,7 +3034,7 @@ describe "TextEditorComponent", -> atom.views.performDocumentPoll() nextAnimationFrame() - expect(componentNode.querySelectorAll('.line')).toHaveLength(6) + expect(componentNode.querySelectorAll('.line')).toHaveLength(7) # visible rows + the longest screen row gutterWidth = componentNode.querySelector('.gutter').offsetWidth componentNode.style.width = gutterWidth + 14 * charWidth + editor.getVerticalScrollbarWidth() + 'px' diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 7f6de6397..51d11f814 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -47,6 +47,10 @@ class LinesTileComponent @domNode.style.zIndex = @newTileState.zIndex @oldTileState.zIndex = @newTileState.zIndex + if @newTileState.visibility isnt @oldTileState.visibility + @domNode.style.visibility = @newTileState.visibility + @oldTileState.visibility = @newTileState.visibility + if @newTileState.display isnt @oldTileState.display @domNode.style.display = @newTileState.display @oldTileState.display = @newTileState.display From 578b157da0e39ac394b67014acfcdef6e1266edd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 15 Sep 2015 17:22:22 +0200 Subject: [PATCH 04/80] Differentiate between gutter and lines tiles ...and fix specs as well. :green_heart: --- spec/text-editor-component-spec.coffee | 2 +- spec/text-editor-presenter-spec.coffee | 9 ++++++--- src/text-editor-presenter.coffee | 20 +++++++++++--------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index b19116bd2..0da967d1c 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -219,7 +219,7 @@ describe "TextEditorComponent", -> expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) - fffit "renders the longest screen row even when it's not visible", -> + it "renders the longest screen row even when it's not visible", -> wrapperNode.style.height = 2 * lineHeightInPixels + 'px' component.measureDimensions() nextAnimationFrame() diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index d1602ae93..7b65bb00e 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -154,20 +154,23 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[10]).toBeUndefined() it "updates when ::lineHeight changes", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) + presenter = buildPresenter(explicitHeight: 10, scrollTop: 0, lineHeight: 1, tileSize: 2) expect(stateFn(presenter).tiles[0]).toBeDefined() expect(stateFn(presenter).tiles[2]).toBeDefined() expect(stateFn(presenter).tiles[4]).toBeDefined() expect(stateFn(presenter).tiles[6]).toBeDefined() - expect(stateFn(presenter).tiles[8]).toBeUndefined() + expect(stateFn(presenter).tiles[8]).toBeDefined() + expect(stateFn(presenter).tiles[10]).toBeDefined() + expect(stateFn(presenter).tiles[12]).toBeUndefined() expectStateUpdate presenter, -> presenter.setLineHeight(2) expect(stateFn(presenter).tiles[0]).toBeDefined() expect(stateFn(presenter).tiles[2]).toBeDefined() expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeUndefined() + expect(stateFn(presenter).tiles[6]).toBeDefined() + expect(stateFn(presenter).tiles[8]).toBeUndefined() it "does not remove out-of-view tiles corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index b6c812123..8331e6605 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -354,7 +354,8 @@ class TextEditorPresenter @updateLinesState(tile, startRow, endRow) if @shouldUpdateLinesState @updateLineNumbersState(gutterTile, startRow, endRow) if @shouldUpdateLineNumbersState - @visibleTiles[startRow] = true + @visibleLinesTiles[startRow] = true + @visibleGutterTiles[startRow] = true zIndex-- updateMouseWheelTileState: -> @@ -362,10 +363,11 @@ class TextEditorPresenter mouseWheelTile = @tileForRow(@mouseWheelScreenRow) - unless @visibleTiles[mouseWheelTile]? + unless @visibleGutterTiles[mouseWheelTile]? and @visibleLinesTiles[mouseWheelTile] @lineNumberGutter.tiles[mouseWheelTile].display = "none" @state.content.tiles[mouseWheelTile].display = "none" - @visibleTiles[mouseWheelTile] = true + @visibleGutterTiles[mouseWheelTile] = true + @visibleLinesTiles[mouseWheelTile] = true updateLongestTileState: -> longestScreenRow = @model.getLongestScreenRow() @@ -375,22 +377,22 @@ class TextEditorPresenter tile = @state.content.tiles[longestScreenRowTile] ?= {} tile.visibility = "hidden" + tile.highlights = {} @updateLinesState(tile, longestScreenRow, longestScreenRow + 1) - @visibleTiles[longestScreenRowTile] = true + @visibleLinesTiles[longestScreenRowTile] = true deleteHiddenTilesState: -> for id, tile of @state.content.tiles - continue if @visibleTiles.hasOwnProperty(id) - - delete @state.content.tiles[id] - delete @lineNumberGutter.tiles[id] + delete @state.content.tiles[id] unless @visibleLinesTiles[id] + delete @lineNumberGutter.tiles[id] unless @visibleGutterTiles[id] updateTilesState: -> return unless @startRow? and @endRow? and @lineHeight? - @visibleTiles = {} + @visibleGutterTiles = {} + @visibleLinesTiles = {} @updateVisibleTilesState() @updateLongestTileState() From 1cdc7ae5a0498a9011d95df9e9a9e651d9a91584 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 15 Sep 2015 17:30:42 +0200 Subject: [PATCH 05/80] Decouple horizontal dimensions computation from vertical ones The former needs measurement and, therefore, depends on the latter. --- src/text-editor-presenter.coffee | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 8331e6605..ca7aadf3b 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -67,12 +67,14 @@ class TextEditorPresenter getState: -> @updating = true - @updateContentDimensions() + @updateVerticalDimensions() @updateScrollbarDimensions() @updateStartRow() @updateEndRow() @updateCommonGutterState() + @updateHorizontalDimensions() + @updateFocusedState() if @shouldUpdateFocusedState @updateHeightState() if @shouldUpdateHeightState @updateVerticalScrollState() if @shouldUpdateVerticalScrollState @@ -112,7 +114,7 @@ class TextEditorPresenter observeModel: -> @disposables.add @model.onDidChange => - @updateContentDimensions() + @updateVerticalDimensions() @shouldUpdateHeightState = true @shouldUpdateVerticalScrollState = true @@ -671,11 +673,17 @@ class TextEditorPresenter @scrollHeight = scrollHeight @updateScrollTop() - updateContentDimensions: -> + updateVerticalDimensions: -> if @lineHeight? oldContentHeight = @contentHeight @contentHeight = @lineHeight * @model.getScreenLineCount() + if @contentHeight isnt oldContentHeight + @updateHeight() + @updateScrollbarDimensions() + @updateScrollHeight() + + updateHorizontalDimensions: -> if @baseCharacterWidth? oldContentWidth = @contentWidth clip = @model.tokenizedLineForScreenRow(@model.getLongestScreenRow())?.isSoftWrapped() @@ -683,15 +691,14 @@ class TextEditorPresenter @contentWidth += @scrollLeft @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width - if @contentHeight isnt oldContentHeight - @updateHeight() - @updateScrollbarDimensions() - @updateScrollHeight() - if @contentWidth isnt oldContentWidth @updateScrollbarDimensions() @updateScrollWidth() + updateContentDimensions: -> + @updateVerticalDimensions() + @updateHorizontalDimensions() + updateClientHeight: -> return unless @height? and @horizontalScrollbarHeight? From 346c7d9b372d79f0b29c493c800b90d091bbbb7e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 15 Sep 2015 17:44:31 +0200 Subject: [PATCH 06/80] Defer state building in TextEditorPresenter --- src/text-editor-presenter.coffee | 47 ++++++++++++-------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index ca7aadf3b..05b8a636b 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -34,6 +34,7 @@ class TextEditorPresenter @observeModel() @observeConfig() @buildState() + @invalidate() @startBlinkingCursors() if @focused @updating = false @@ -112,6 +113,23 @@ class TextEditorPresenter @shouldUpdateGutterOrderState = false @shouldUpdateCustomGutterDecorationState = false + invalidate: -> + @shouldUpdateFocusedState = true + @shouldUpdateHeightState = true + @shouldUpdateVerticalScrollState = true + @shouldUpdateHorizontalScrollState = true + @shouldUpdateScrollbarsState = true + @shouldUpdateHiddenInputState = true + @shouldUpdateContentState = true + @shouldUpdateDecorations = true + @shouldUpdateLinesState = true + @shouldUpdateCursorsState = true + @shouldUpdateOverlaysState = true + @shouldUpdateLineNumberGutterState = true + @shouldUpdateLineNumbersState = true + @shouldUpdateGutterOrderState = true + @shouldUpdateCustomGutterDecorationState = true + observeModel: -> @disposables.add @model.onDidChange => @updateVerticalDimensions() @@ -227,35 +245,6 @@ class TextEditorPresenter @lineNumberGutter = tiles: {} - @updateState() - - updateState: -> - @shouldUpdateLinesState = true - @shouldUpdateLineNumbersState = true - - @updateContentDimensions() - @updateScrollbarDimensions() - @updateStartRow() - @updateEndRow() - - @updateFocusedState() - @updateHeightState() - @updateVerticalScrollState() - @updateHorizontalScrollState() - @updateScrollbarsState() - @updateHiddenInputState() - @updateContentState() - @updateDecorations() - @updateTilesState() - @updateCursorsState() - @updateOverlaysState() - @updateLineNumberGutterState() - @updateCommonGutterState() - @updateGutterOrderState() - @updateCustomGutterDecorationState() - - @resetTrackedUpdates() - updateFocusedState: -> @state.focused = @focused From 398fb1f62d673c6b28324fbef9a92b36af9aaecc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 15 Sep 2015 17:50:21 +0200 Subject: [PATCH 07/80] :fire: Avoid mounting the container in ctor ...as it will be mounted anyways when updateSync gets called --- src/text-editor-component.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 73b03ce88..2c3d6d823 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -70,8 +70,6 @@ class TextEditorComponent @scrollViewNode.classList.add('scroll-view') @domNode.appendChild(@scrollViewNode) - @mountGutterContainerComponent() if @presenter.getState().gutters.length - @hiddenInputComponent = new InputComponent @scrollViewNode.appendChild(@hiddenInputComponent.getDomNode()) From 29846d0a51ef3c0c8c83a485ed5f263bdeaf9eef Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 15 Sep 2015 18:41:41 +0200 Subject: [PATCH 08/80] Add LinesYardstick ...and create a MockLineNodesProvider for testing purposes --- spec/lines-yardstick-spec.coffee | 58 ++++++++++++++++++++ spec/mock-line-nodes-provider.coffee | 34 ++++++++++++ src/lines-yardstick.coffee | 82 ++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 spec/lines-yardstick-spec.coffee create mode 100644 spec/mock-line-nodes-provider.coffee create mode 100644 src/lines-yardstick.coffee diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee new file mode 100644 index 000000000..6ec5ddd39 --- /dev/null +++ b/spec/lines-yardstick-spec.coffee @@ -0,0 +1,58 @@ +LinesYardstick = require '../src/lines-yardstick' +MockLineNodesProvider = require './mock-line-nodes-provider' + +describe "LinesYardstick", -> + [editor, mockLineNodesProvider, builtLineNodes, linesYardstick] = [] + + beforeEach -> + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + + waitsForPromise -> + atom.project.open('sample.js').then (o) -> editor = o + + runs -> + mockLineNodesProvider = new MockLineNodesProvider(editor) + linesYardstick = new LinesYardstick(editor, mockLineNodesProvider) + + afterEach -> + mockLineNodesProvider.dispose() + + it "converts screen positions to pixel positions", -> + mockLineNodesProvider.setDefaultFont("14px monospace") + + conversionTable = [ + [[0, 0], {left: 0, top: editor.getLineHeightInPixels() * 0}] + [[0, 3], {left: 24, top: editor.getLineHeightInPixels() * 0}] + [[0, 4], {left: 32, top: editor.getLineHeightInPixels() * 0}] + [[0, 5], {left: 40, top: editor.getLineHeightInPixels() * 0}] + [[1, 0], {left: 0, top: editor.getLineHeightInPixels() * 1}] + [[1, 1], {left: 0, top: editor.getLineHeightInPixels() * 1}] + [[1, 6], {left: 48, top: editor.getLineHeightInPixels() * 1}] + [[1, Infinity], {left: 240, top: editor.getLineHeightInPixels() * 1}] + ] + + for [point, position] in conversionTable + expect( + linesYardstick.pixelPositionForScreenPosition(point) + ).toEqual(position) + + mockLineNodesProvider.setFontForScopes( + ["source.js", "storage.modifier.js"], "16px monospace" + ) + + conversionTable = [ + [[0, 0], {left: 0, top: editor.getLineHeightInPixels() * 0}] + [[0, 3], {left: 30, top: editor.getLineHeightInPixels() * 0}] + [[0, 4], {left: 38, top: editor.getLineHeightInPixels() * 0}] + [[0, 5], {left: 46, top: editor.getLineHeightInPixels() * 0}] + [[1, 0], {left: 0, top: editor.getLineHeightInPixels() * 1}] + [[1, 1], {left: 0, top: editor.getLineHeightInPixels() * 1}] + [[1, 6], {left: 54, top: editor.getLineHeightInPixels() * 1}] + [[1, Infinity], {left: 246, top: editor.getLineHeightInPixels() * 1}] + ] + + for [point, position] in conversionTable + expect( + linesYardstick.pixelPositionForScreenPosition(point) + ).toEqual(position) diff --git a/spec/mock-line-nodes-provider.coffee b/spec/mock-line-nodes-provider.coffee new file mode 100644 index 000000000..81ed5a154 --- /dev/null +++ b/spec/mock-line-nodes-provider.coffee @@ -0,0 +1,34 @@ +TokenIterator = require '../src/token-iterator' + +module.exports = +class MockLineNodesProvider + constructor: (@editor) -> + @defaultFont = "" + @fontsByScopes = {} + @tokenIterator = new TokenIterator + @builtLineNodes = [] + + dispose: -> + node.remove() for node in @builtLineNodes + + setFontForScopes: (scopes, font) -> @fontsByScopes[scopes] = font + + setDefaultFont: (font) -> @defaultFont = font + + lineNodeForScreenRow: (screenRow) -> + lineNode = document.createElement("div") + lineNode.style.whiteSpace = "pre" + lineState = @editor.tokenizedLineForScreenRow(screenRow) + + @tokenIterator.reset(lineState) + while @tokenIterator.next() + font = @fontsByScopes[@tokenIterator.getScopes()] or @defaultFont + span = document.createElement("span") + span.style.font = font + span.textContent = @tokenIterator.getText() + lineNode.innerHTML += span.outerHTML + + @builtLineNodes.push(lineNode) + document.body.appendChild(lineNode) + + lineNode diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee new file mode 100644 index 000000000..f361f403a --- /dev/null +++ b/src/lines-yardstick.coffee @@ -0,0 +1,82 @@ +TokenIterator = require './token-iterator' +AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} +{Point} = require 'text-buffer' + +module.exports = +class LinesYardstick + constructor: (@model, @lineNodesProvider) -> + @tokenIterator = new TokenIterator + @rangeForMeasurement = document.createRange() + + pixelPositionForScreenPosition: (screenPosition, clip=true) -> + screenPosition = Point.fromObject(screenPosition) + screenPosition = @model.clipScreenPosition(screenPosition) if clip + + targetRow = screenPosition.row + targetColumn = screenPosition.column + baseCharacterWidth = @baseCharacterWidth + + top = targetRow * @model.getLineHeightInPixels() + left = @leftPixelPositionForScreenPosition(targetRow, targetColumn) + + {top, left} + + leftPixelPositionForScreenPosition: (row, column) -> + lineNode = @lineNodesProvider.lineNodeForScreenRow(row) + + tokenizedLine = @model.tokenizedLineForScreenRow(row) + iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) + charIndex = 0 + + @tokenIterator.reset(tokenizedLine) + while @tokenIterator.next() + text = @tokenIterator.getText() + + textIndex = 0 + while textIndex < text.length + if @tokenIterator.isPairedCharacter() + char = text + charLength = 2 + textIndex += 2 + else + char = text[textIndex] + charLength = 1 + textIndex++ + + continue if char is '\0' + + unless textNode? + textNode = iterator.nextNode() + textNodeLength = textNode.textContent.length + textNodeIndex = 0 + nextTextNodeIndex = textNodeLength + + while nextTextNodeIndex <= charIndex + textNode = iterator.nextNode() + textNodeLength = textNode.textContent.length + textNodeIndex = nextTextNodeIndex + nextTextNodeIndex = textNodeIndex + textNodeLength + + if charIndex is column + indexWithinToken = charIndex - textNodeIndex + return @leftPixelPositionForCharInTextNode(textNode, indexWithinToken) + + charIndex += charLength + + if textNode? + @leftPixelPositionForCharInTextNode(textNode, textNode.textContent.length) + else + 0 + + leftPixelPositionForCharInTextNode: (textNode, charIndex) -> + @rangeForMeasurement.setEnd(textNode, textNode.textContent.length) + + if charIndex is 0 + @rangeForMeasurement.setStart(textNode, 0) + @rangeForMeasurement.getBoundingClientRect().left + else if charIndex is textNode.textContent.length + @rangeForMeasurement.setStart(textNode, 0) + @rangeForMeasurement.getBoundingClientRect().right + else + @rangeForMeasurement.setStart(textNode, charIndex) + @rangeForMeasurement.getBoundingClientRect().left From 2750a384acce31c0806eda778700749af31d13f1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 16 Sep 2015 11:33:51 +0200 Subject: [PATCH 09/80] :green_heart: Fix TextEditorPresenter specs ...so that they play nicely with LinesYardstick --- spec/text-editor-presenter-spec.coffee | 83 ++++++++++++++++++++------ src/text-editor-presenter.coffee | 44 +++----------- 2 files changed, 72 insertions(+), 55 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 7b65bb00e..883749aab 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -4,12 +4,14 @@ TextBuffer = require 'text-buffer' {Point, Range} = TextBuffer TextEditor = require '../src/text-editor' TextEditorPresenter = require '../src/text-editor-presenter' +LinesYardstick = require '../src/lines-yardstick' +MockLineNodesProvider = require './mock-line-nodes-provider' describe "TextEditorPresenter", -> # These `describe` and `it` blocks mirror the structure of the ::state object. # Please maintain this structure when adding specs for new state fields. describe "::getState()", -> - [buffer, editor] = [] + [buffer, editor, linesYardstick, lineNodesProvider] = [] beforeEach -> # These *should* be mocked in the spec helper, but changing that now would break packages :-( @@ -18,9 +20,13 @@ describe "TextEditorPresenter", -> buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js')) editor = new TextEditor({buffer}) + lineNodesProvider = new MockLineNodesProvider(editor) + lineNodesProvider.setDefaultFont("16px monospace") + linesYardstick = new LinesYardstick(editor, lineNodesProvider) waitsForPromise -> buffer.load() afterEach -> + lineNodesProvider.dispose() editor.destroy() buffer.destroy() @@ -40,7 +46,9 @@ describe "TextEditorPresenter", -> scrollTop: 0 scrollLeft: 0 - new TextEditorPresenter(params) + presenter = new TextEditorPresenter(params) + presenter.setLinesYardstick(linesYardstick) + presenter expectValues = (actual, expected) -> for key, value of expected @@ -297,7 +305,9 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) + expectStateUpdate presenter, -> + lineNodesProvider.setDefaultFont("25px monospace") + presenter.setBaseCharacterWidth(15) expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 15 * maxLineLength + 1 it "updates when the scoped character widths change", -> @@ -308,8 +318,13 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide + expectStateUpdate presenter, -> + lineNodesProvider.setFontForScopes(['source.js', 'support.function.js'], "34px monospace") + presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) + presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'u', 20) + presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 's', 20) + presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'h', 20) + expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 8)) + (20 * 8) + 1 # 8 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10) @@ -536,10 +551,16 @@ describe "TextEditorPresenter", -> presenter = buildPresenter() expect(presenter.getState().hiddenInput.width).toBe 10 - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) + expectStateUpdate presenter, -> + lineNodesProvider.setDefaultFont("25px monospace") + presenter.setBaseCharacterWidth(15) expect(presenter.getState().hiddenInput.width).toBe 15 - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + expectStateUpdate presenter, -> + lineNodesProvider.setFontForScopes(['source.js', 'storage.modifier.js'], "33px monospace") + presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20) + presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'a', 20) + presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) expect(presenter.getState().hiddenInput.width).toBe 20 it "is 2px at the end of lines", -> @@ -631,7 +652,9 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) + expectStateUpdate presenter, -> + lineNodesProvider.setDefaultFont("25px monospace") + presenter.setBaseCharacterWidth(15) expect(presenter.getState().content.scrollWidth).toBe 15 * maxLineLength + 1 it "updates when the scoped character widths change", -> @@ -642,8 +665,13 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) - expect(presenter.getState().content.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide + expectStateUpdate presenter, -> + lineNodesProvider.setFontForScopes(['source.js', 'support.function.js'], "33px monospace") + presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) + presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'u', 20) + presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 's', 20) + presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'h', 20) + expect(presenter.getState().content.scrollWidth).toBe (10 * (maxLineLength - 8)) + (20 * 8) + 1 # 8 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10) @@ -1221,7 +1249,9 @@ describe "TextEditorPresenter", -> editor.setCursorBufferPosition([2, 4]) presenter = buildPresenter(explicitHeight: 20, scrollTop: 20) - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20) + expectStateUpdate presenter, -> + lineNodesProvider.setDefaultFont("33px monospace") + presenter.setBaseCharacterWidth(20) expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 20, width: 20, height: 10} it "updates when scoped character widths change", -> @@ -1232,11 +1262,19 @@ describe "TextEditorPresenter", -> editor.setCursorBufferPosition([1, 4]) presenter = buildPresenter(explicitHeight: 20) - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20) - expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 10, height: 10} + expectStateUpdate presenter, -> + lineNodesProvider.setFontForScopes(['source.js', 'storage.modifier.js'], "33px monospace") + presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20) + presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'a', 20) + presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (2 * 10) + (2 * 20), width: 20, height: 10} - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) - expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 20, height: 10} + expectStateUpdate presenter, -> + lineNodesProvider.setFontForScopes(['source.js', 'storage.modifier.js'], "36px monospace") + presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 22) + presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'a', 22) + presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 22) + expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (2 * 10) + (2 * 22), width: 22, height: 10} it "updates when cursors are added, moved, hidden, shown, or destroyed", -> editor.setSelectedBufferRanges([ @@ -1561,7 +1599,9 @@ describe "TextEditorPresenter", -> expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 2 * 10, width: 2 * 10, height: 10}] } - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20) + expectStateUpdate presenter, -> + lineNodesProvider.setDefaultFont("33px monospace") + presenter.setBaseCharacterWidth(20) expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 2 * 20, width: 2 * 20, height: 10}] } @@ -1580,9 +1620,12 @@ describe "TextEditorPresenter", -> expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] } - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'i', 20) + expectStateUpdate presenter, -> + lineNodesProvider.setFontForScopes(['source.js', 'keyword.control.js'], "25px monospace") + presenter.setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'i', 20) + presenter.setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'f', 20) expectValues stateForSelectionInTile(presenter, 0, 2), { - regions: [{top: 0, left: 4 * 10, width: 20 + 10, height: 10}] + regions: [{top: 0, left: 4 * 10, width: 15 * 2, height: 10}] } it "updates when highlight decorations are added, moved, hidden, shown, or destroyed", -> @@ -1744,7 +1787,9 @@ describe "TextEditorPresenter", -> pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 10} } - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(5) + expectStateUpdate presenter, -> + lineNodesProvider.setDefaultFont("9px monospace") + presenter.setBaseCharacterWidth(5) expectValues stateForOverlay(presenter, decoration), { item: item diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 05b8a636b..a82c88825 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -38,6 +38,8 @@ class TextEditorPresenter @startBlinkingCursors() if @focused @updating = false + setLinesYardstick: (@linesYardstick) -> + destroy: -> @disposables.dispose() @@ -1057,42 +1059,12 @@ class TextEditorPresenter hasPixelPositionRequirements: -> @lineHeight? and @baseCharacterWidth? - pixelPositionForScreenPosition: (screenPosition, clip=true) -> - screenPosition = Point.fromObject(screenPosition) - screenPosition = @model.clipScreenPosition(screenPosition) if clip - - targetRow = screenPosition.row - targetColumn = screenPosition.column - baseCharacterWidth = @baseCharacterWidth - - top = targetRow * @lineHeight - left = 0 - column = 0 - - iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator() - while iterator.next() - characterWidths = @getScopedCharacterWidths(iterator.getScopes()) - - valueIndex = 0 - text = iterator.getText() - while valueIndex < text.length - if iterator.isPairedCharacter() - char = text - charLength = 2 - valueIndex += 2 - else - char = text[valueIndex] - charLength = 1 - valueIndex++ - - break if column is targetColumn - - left += characterWidths[char] ? baseCharacterWidth unless char is '\0' - column += charLength - - top -= @scrollTop - left -= @scrollLeft - {top, left} + pixelPositionForScreenPosition: (screenPosition, clip) -> + pixelPosition = + @linesYardstick.pixelPositionForScreenPosition(screenPosition, clip) + pixelPosition.top -= @scrollTop + pixelPosition.left -= @scrollLeft + pixelPosition hasPixelRectRequirements: -> @hasPixelPositionRequirements() and @scrollWidth? From 2dd944f3ee869ad516b1b59f2888e80745327043 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 16 Sep 2015 12:07:57 +0200 Subject: [PATCH 10/80] Trigger ::onWillMeasure before measuring anything --- spec/text-editor-presenter-spec.coffee | 28 +++++++++++++++++++++- src/text-editor-presenter.coffee | 33 +++++++++++++++++++------- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 883749aab..5b2e6d78d 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -220,10 +220,36 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[0]).toBeDefined() describe "during state retrieval", -> - it "does not trigger onDidUpdateState events", -> + it "does not trigger ::onDidUpdateState events", -> presenter = buildPresenter() expectNoStateUpdate presenter, -> presenter.getState() + it "triggers ::onWillMeasure events before computing any state that needs measurement", -> + editor.setCursorBufferPosition([0, 0]) + cursorLine = editor.tokenizedLineForScreenRow(0) + called = false + + onWillMeasureSpy = (state) -> + called = true + expect(Object.keys(state.content.tiles).length).toBeGreaterThan(0) + for tile, tileState of state.content.tiles + expect(tileState.highlights).toEqual({}) + expect(state.content.tiles[0].lines[cursorLine.id].decorationClasses).not.toBeNull() + + expect(state.gutters).toEqual([]) + expect(state.hiddenInput).toEqual({}) + expect(state.content.overlays).toEqual({}) + expect(state.content.cursors).toEqual({}) + expect(state.content.width).toBeUndefined() + expect(state.content.scrollWidth).toBeUndefined() + + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) + + presenter.onWillMeasure(onWillMeasureSpy) + presenter.getState() + + expect(called).toBe(true) + describe ".horizontalScrollbar", -> describe ".visible", -> it "is true if the scrollWidth exceeds the computed client width", -> diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index a82c88825..87a030217 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -43,6 +43,10 @@ class TextEditorPresenter destroy: -> @disposables.dispose() + # Calls your `callback` while performing ::getState(), before computing any state that needs measurements. + onWillMeasure: (callback) -> + @emitter.on "will-measure", callback + # Calls your `callback` when some changes in the model occurred and the current state has been updated. onDidUpdateState: (callback) -> @emitter.on 'did-update-state', callback @@ -74,8 +78,13 @@ class TextEditorPresenter @updateScrollbarDimensions() @updateStartRow() @updateEndRow() - @updateCommonGutterState() + @updateLinesDecorations() if @shouldUpdateDecorations + @updateTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState + + @emitter.emit "will-measure", @state + + @updateCommonGutterState() @updateHorizontalDimensions() @updateFocusedState() if @shouldUpdateFocusedState @@ -85,8 +94,7 @@ class TextEditorPresenter @updateScrollbarsState() if @shouldUpdateScrollbarsState @updateHiddenInputState() if @shouldUpdateHiddenInputState @updateContentState() if @shouldUpdateContentState - @updateDecorations() if @shouldUpdateDecorations - @updateTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState + @updateHighlightDecorations() if @shouldUpdateDecorations @updateCursorsState() if @shouldUpdateCursorsState @updateOverlaysState() if @shouldUpdateOverlaysState @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState @@ -240,6 +248,7 @@ class TextEditorPresenter tiles: {} highlights: {} overlays: {} + cursors: {} gutters: [] # Shared state that is copied into ``@state.gutters`. @sharedGutterStyles = {} @@ -1144,22 +1153,28 @@ class TextEditorPresenter @emitDidUpdateState() - updateDecorations: -> + updateLinesDecorations: -> @rangesByDecorationId = {} @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterNameAndScreenRow = {} + + return unless 0 <= @startRow <= @endRow <= Infinity + + for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1) + range = @model.getMarker(markerId).getScreenRange() + for decoration in decorations when decoration.isType('line') or decoration.isType('gutter') + @addToLineDecorationCaches(decoration, range) + + updateHighlightDecorations: -> @visibleHighlights = {} return unless 0 <= @startRow <= @endRow <= Infinity for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1) range = @model.getMarker(markerId).getScreenRange() - for decoration in decorations - if decoration.isType('line') or decoration.isType('gutter') - @addToLineDecorationCaches(decoration, range) - else if decoration.isType('highlight') - @updateHighlightState(decoration, range) + for decoration in decorations when decoration.isType('highlight') + @updateHighlightState(decoration, range) for tileId, tileState of @state.content.tiles for id, highlight of tileState.highlights From 2542a8d6037ce9dc1dc779184884432af9e9567c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 16 Sep 2015 12:32:26 +0200 Subject: [PATCH 11/80] Integrate LinesYardstick with TextEditorComponent --- spec/text-editor-component-spec.coffee | 2 +- src/lines-yardstick.coffee | 28 +++++++++++++++----------- src/text-editor-component.coffee | 9 +++++++++ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 0da967d1c..31419cdfb 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -3013,7 +3013,7 @@ describe "TextEditorComponent", -> expect(cursorLeft).toBe line0Right describe "when lines are changed while the editor is hidden", -> - it "does not measure new characters until the editor is shown again", -> + xit "does not measure new characters until the editor is shown again", -> editor.setText('') wrapperView.hide() editor.setText('var z = 1') diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index f361f403a..724467938 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -24,6 +24,7 @@ class LinesYardstick leftPixelPositionForScreenPosition: (row, column) -> lineNode = @lineNodesProvider.lineNodeForScreenRow(row) + return 0 unless lineNode? tokenizedLine = @model.tokenizedLineForScreenRow(row) iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) charIndex = 0 @@ -59,24 +60,27 @@ class LinesYardstick if charIndex is column indexWithinToken = charIndex - textNodeIndex - return @leftPixelPositionForCharInTextNode(textNode, indexWithinToken) + return @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinToken) charIndex += charLength if textNode? - @leftPixelPositionForCharInTextNode(textNode, textNode.textContent.length) + @leftPixelPositionForCharInTextNode(lineNode, textNode, textNode.textContent.length) else 0 - leftPixelPositionForCharInTextNode: (textNode, charIndex) -> + leftPixelPositionForCharInTextNode: (lineNode, textNode, charIndex) -> @rangeForMeasurement.setEnd(textNode, textNode.textContent.length) - if charIndex is 0 - @rangeForMeasurement.setStart(textNode, 0) - @rangeForMeasurement.getBoundingClientRect().left - else if charIndex is textNode.textContent.length - @rangeForMeasurement.setStart(textNode, 0) - @rangeForMeasurement.getBoundingClientRect().right - else - @rangeForMeasurement.setStart(textNode, charIndex) - @rangeForMeasurement.getBoundingClientRect().left + position = + if charIndex is 0 + @rangeForMeasurement.setStart(textNode, 0) + @rangeForMeasurement.getBoundingClientRect().left + else if charIndex is textNode.textContent.length + @rangeForMeasurement.setStart(textNode, 0) + @rangeForMeasurement.getBoundingClientRect().right + else + @rangeForMeasurement.setStart(textNode, charIndex) + @rangeForMeasurement.getBoundingClientRect().left + + position - lineNode.getBoundingClientRect().left diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 2c3d6d823..deed59bf9 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -13,6 +13,8 @@ ScrollbarComponent = require './scrollbar-component' ScrollbarCornerComponent = require './scrollbar-corner-component' OverlayManager = require './overlay-manager' +LinesYardstick = require './lines-yardstick' + module.exports = class TextEditorComponent scrollSensitivity: 0.4 @@ -76,6 +78,10 @@ class TextEditorComponent @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) + @linesYardstick = new LinesYardstick(@editor, this) + @presenter.setLinesYardstick(@linesYardstick) + @presenter.onWillMeasure(@updateLinesComponentSync) + @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll}) @scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode()) @@ -109,6 +115,9 @@ class TextEditorComponent getDomNode: -> @domNode + updateLinesComponentSync: (state) => + @linesComponent.updateSync(state) + updateSync: -> @oldState ?= {} @newState = @presenter.getState() From bae4d7d336aebef60d0e06041e38a2eff08cabae Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 16 Sep 2015 17:59:04 +0200 Subject: [PATCH 12/80] :fire: --- src/lines-tile-component.coffee | 1 + src/text-editor-presenter.coffee | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 51d11f814..3f1167238 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -315,6 +315,7 @@ class LinesTileComponent lineNode.dataset.screenRow = newLineState.screenRow oldLineState.screenRow = newLineState.screenRow @lineIdsByScreenRow[newLineState.screenRow] = id + @screenRowsByLineId[id] = newLineState.screenRow lineNodeForScreenRow: (screenRow) -> @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 87a030217..27bbd0de3 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -695,10 +695,6 @@ class TextEditorPresenter @updateScrollbarDimensions() @updateScrollWidth() - updateContentDimensions: -> - @updateVerticalDimensions() - @updateHorizontalDimensions() - updateClientHeight: -> return unless @height? and @horizontalScrollbarHeight? From 751f5920b4643b9a26f904961a9ff22d88d50a2c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Sep 2015 10:50:18 +0200 Subject: [PATCH 13/80] :art: --- src/text-editor-presenter.coffee | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 27bbd0de3..1c041a735 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -333,6 +333,17 @@ class TextEditorPresenter (@getEndTileRow() - @getStartTileRow() + 1) / @tileSize ) + updateTilesState: -> + return unless @startRow? and @endRow? and @lineHeight? + + @visibleGutterTiles = {} + @visibleLinesTiles = {} + + @updateVisibleTilesState() + @updateLongestTileState() + @updateMouseWheelTileState() + @deleteHiddenTilesState() + updateVisibleTilesState: -> zIndex = @getTilesCount() - 1 for startRow in [@getStartTileRow()..@getEndTileRow()] by @tileSize @@ -375,6 +386,7 @@ class TextEditorPresenter longestScreenRow = @model.getLongestScreenRow() longestScreenRowTile = @tileForRow(longestScreenRow) + return unless longestScreenRow? return if @getStartTileRow() <= longestScreenRowTile <= @getEndTileRow() tile = @state.content.tiles[longestScreenRowTile] ?= {} @@ -390,17 +402,6 @@ class TextEditorPresenter delete @state.content.tiles[id] unless @visibleLinesTiles[id] delete @lineNumberGutter.tiles[id] unless @visibleGutterTiles[id] - updateTilesState: -> - return unless @startRow? and @endRow? and @lineHeight? - - @visibleGutterTiles = {} - @visibleLinesTiles = {} - - @updateVisibleTilesState() - @updateLongestTileState() - @updateMouseWheelTileState() - @deleteHiddenTilesState() - updateLinesState: (tileState, startRow, endRow) -> tileState.lines ?= {} visibleLineIds = {} From 42e58f1dd39c4826dfdd26fa2b90e2100f9adaaf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Sep 2015 11:10:32 +0200 Subject: [PATCH 14/80] :green_heart: Use tokenizedLine.id to refer to lines --- spec/mock-line-nodes-provider.coffee | 2 +- src/lines-tile-component.coffee | 3 +++ src/lines-yardstick.coffee | 8 ++++++-- src/text-editor-component.coffee | 6 ++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/spec/mock-line-nodes-provider.coffee b/spec/mock-line-nodes-provider.coffee index 81ed5a154..c0944a771 100644 --- a/spec/mock-line-nodes-provider.coffee +++ b/spec/mock-line-nodes-provider.coffee @@ -15,7 +15,7 @@ class MockLineNodesProvider setDefaultFont: (font) -> @defaultFont = font - lineNodeForScreenRow: (screenRow) -> + lineNodeForLineIdAndScreenRow: (id, screenRow) -> lineNode = document.createElement("div") lineNode.style.whiteSpace = "pre" lineState = @editor.tokenizedLineForScreenRow(screenRow) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 3f1167238..734d57e59 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -320,6 +320,9 @@ class LinesTileComponent lineNodeForScreenRow: (screenRow) -> @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] + lineNodeForLineId: (lineId) -> + @lineNodesByLineId[lineId] + measureCharactersInNewLines: -> for id, lineState of @oldTileState.lines unless @measuredLines.has(id) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 724467938..0ab61a5fb 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -22,10 +22,14 @@ class LinesYardstick {top, left} leftPixelPositionForScreenPosition: (row, column) -> - lineNode = @lineNodesProvider.lineNodeForScreenRow(row) + tokenizedLine = @model.tokenizedLineForScreenRow(row) + return 0 unless tokenizedLine? + + lineNode = + @lineNodesProvider.lineNodeForLineIdAndScreenRow(tokenizedLine.id, row) return 0 unless lineNode? - tokenizedLine = @model.tokenizedLineForScreenRow(row) + iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) charIndex = 0 diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index deed59bf9..7f23ab408 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -743,6 +743,12 @@ class TextEditorComponent consolidateSelections: (e) -> e.abortKeyBinding() unless @editor.consolidateSelections() + lineNodeForLineIdAndScreenRow: (lineId, screenRow) -> + tileRow = @presenter.tileForRow(screenRow) + tileComponent = @linesComponent.getComponentForTile(tileRow) + + tileComponent?.lineNodeForLineId(lineId) + lineNodeForScreenRow: (screenRow) -> tileRow = @presenter.tileForRow(screenRow) tileComponent = @linesComponent.getComponentForTile(tileRow) From 80d872c4ac0d3f2342de62a66be216522f3141b6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Sep 2015 11:51:03 +0200 Subject: [PATCH 15/80] Avoid calling getBoundingClientRect twice for each line --- src/lines-yardstick.coffee | 11 +++++++---- src/text-editor-component.coffee | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 749ae7ad9..c241aa0ab 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -7,6 +7,9 @@ class LinesYardstick constructor: (@model, @lineNodesProvider) -> @tokenIterator = new TokenIterator @rangeForMeasurement = document.createRange() + @leftMargin = 0 + + setLeftMargin: (@leftMargin) -> pixelPositionForScreenPosition: (screenPosition, clip=true) -> screenPosition = Point.fromObject(screenPosition) @@ -62,16 +65,16 @@ class LinesYardstick if charIndex is column indexWithinToken = charIndex - textNodeIndex - return @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinToken) + return @leftPixelPositionForCharInTextNode(textNode, indexWithinToken) charIndex += charLength if textNode? - @leftPixelPositionForCharInTextNode(lineNode, textNode, textNode.textContent.length) + @leftPixelPositionForCharInTextNode(textNode, textNode.textContent.length) else 0 - leftPixelPositionForCharInTextNode: (lineNode, textNode, charIndex) -> + leftPixelPositionForCharInTextNode: (textNode, charIndex) -> @rangeForMeasurement.setEnd(textNode, textNode.textContent.length) position = @@ -85,4 +88,4 @@ class LinesYardstick @rangeForMeasurement.setStart(textNode, charIndex) @rangeForMeasurement.getBoundingClientRect().left - position - lineNode.getBoundingClientRect().left + position - @leftMargin diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 7dc3468cb..dcb436975 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -121,6 +121,9 @@ class TextEditorComponent updateLinesComponentSync: (state) => @linesComponent.updateSync(state) + @linesYardstick.setLeftMargin( + @linesComponent.getDomNode().getBoundingClientRect().left + ) updateSync: -> @oldState ?= {} From be843cc4df1394eeb4e3b99fa874ba18e2a99230 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Sep 2015 14:16:21 +0200 Subject: [PATCH 16/80] :racehorse: Cache positions --- src/lines-yardstick.coffee | 23 +++++++++++++++-------- src/text-editor-component.coffee | 3 --- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index c241aa0ab..c2db69ad9 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -7,9 +7,7 @@ class LinesYardstick constructor: (@model, @lineNodesProvider) -> @tokenIterator = new TokenIterator @rangeForMeasurement = document.createRange() - @leftMargin = 0 - - setLeftMargin: (@leftMargin) -> + @cachedPositionsByLineId = {} pixelPositionForScreenPosition: (screenPosition, clip=true) -> screenPosition = Point.fromObject(screenPosition) @@ -28,16 +26,22 @@ class LinesYardstick tokenizedLine = @model.tokenizedLineForScreenRow(row) return 0 unless tokenizedLine? + if cachedPosition = @cachedPositionsByLineId[tokenizedLine.id]?[column] + return cachedPosition + lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(tokenizedLine.id, row) return 0 unless lineNode? + indexWithinTextNode = null iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) charIndex = 0 @tokenIterator.reset(tokenizedLine) while @tokenIterator.next() + break if indexWithinTextNode? + text = @tokenIterator.getText() textIndex = 0 @@ -64,17 +68,20 @@ class LinesYardstick nextTextNodeIndex = textNodeIndex + textNodeLength if charIndex is column - indexWithinToken = charIndex - textNodeIndex - return @leftPixelPositionForCharInTextNode(textNode, indexWithinToken) + indexWithinTextNode = charIndex - textNodeIndex + break charIndex += charLength if textNode? - @leftPixelPositionForCharInTextNode(textNode, textNode.textContent.length) + indexWithinTextNode ?= textNode.textContent.length + @cachedPositionsByLineId[tokenizedLine.id] ?= {} + @cachedPositionsByLineId[tokenizedLine.id][column] = + @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinTextNode) else 0 - leftPixelPositionForCharInTextNode: (textNode, charIndex) -> + leftPixelPositionForCharInTextNode: (lineNode, textNode, charIndex) -> @rangeForMeasurement.setEnd(textNode, textNode.textContent.length) position = @@ -88,4 +95,4 @@ class LinesYardstick @rangeForMeasurement.setStart(textNode, charIndex) @rangeForMeasurement.getBoundingClientRect().left - position - @leftMargin + position - lineNode.getBoundingClientRect().left diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index dcb436975..7dc3468cb 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -121,9 +121,6 @@ class TextEditorComponent updateLinesComponentSync: (state) => @linesComponent.updateSync(state) - @linesYardstick.setLeftMargin( - @linesComponent.getDomNode().getBoundingClientRect().left - ) updateSync: -> @oldState ?= {} From 2ad336c649b3d150d67ab74b9922c168c5c03258 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Sep 2015 16:26:55 +0200 Subject: [PATCH 17/80] :green_heart: --- spec/lines-yardstick-spec.coffee | 17 +++++++++++++++++ src/lines-yardstick.coffee | 3 +++ src/text-editor-presenter.coffee | 2 ++ 3 files changed, 22 insertions(+) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 6ec5ddd39..23842e6ba 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -40,6 +40,7 @@ describe "LinesYardstick", -> mockLineNodesProvider.setFontForScopes( ["source.js", "storage.modifier.js"], "16px monospace" ) + linesYardstick.clearCache() conversionTable = [ [[0, 0], {left: 0, top: editor.getLineHeightInPixels() * 0}] @@ -56,3 +57,19 @@ describe "LinesYardstick", -> expect( linesYardstick.pixelPositionForScreenPosition(point) ).toEqual(position) + + it "does not compute the same position twice unless the cache gets cleared", -> + mockLineNodesProvider.setDefaultFont("14px monospace") + + oldPosition1 = linesYardstick.pixelPositionForScreenPosition([0, 5]) + oldPosition2 = linesYardstick.pixelPositionForScreenPosition([1, 4]) + + mockLineNodesProvider.setDefaultFont("16px monospace") + + expect(linesYardstick.pixelPositionForScreenPosition([0, 5])).toEqual(oldPosition1) + expect(linesYardstick.pixelPositionForScreenPosition([1, 4])).toEqual(oldPosition2) + + linesYardstick.clearCache() + + expect(linesYardstick.pixelPositionForScreenPosition([0, 5])).not.toEqual(oldPosition1) + expect(linesYardstick.pixelPositionForScreenPosition([1, 4])).not.toEqual(oldPosition2) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index c2db69ad9..0e86e24ca 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -9,6 +9,9 @@ class LinesYardstick @rangeForMeasurement = document.createRange() @cachedPositionsByLineId = {} + clearCache: -> + @cachedPositionsByLineId = {} + pixelPositionForScreenPosition: (screenPosition, clip=true) -> screenPosition = Point.fromObject(screenPosition) screenPosition = @model.clipScreenPosition(screenPosition) if clip diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 1c041a735..1aed3c342 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1046,6 +1046,8 @@ class TextEditorPresenter @characterWidthsChanged() unless @batchingCharacterMeasurement characterWidthsChanged: -> + @linesYardstick.clearCache() + @shouldUpdateHorizontalScrollState = true @shouldUpdateVerticalScrollState = true @shouldUpdateScrollbarsState = true From 8a0d029ad1036286481c281f3fc09d9aa975c21c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Sep 2015 16:34:57 +0200 Subject: [PATCH 18/80] :art: Rename to ::onWillNeedMeasurements --- spec/text-editor-presenter-spec.coffee | 6 +++--- src/text-editor-component.coffee | 2 +- src/text-editor-presenter.coffee | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 5b2e6d78d..4f662294e 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -224,12 +224,12 @@ describe "TextEditorPresenter", -> presenter = buildPresenter() expectNoStateUpdate presenter, -> presenter.getState() - it "triggers ::onWillMeasure events before computing any state that needs measurement", -> + it "triggers ::onWillNeedMeasurements events before computing any state that needs measurement", -> editor.setCursorBufferPosition([0, 0]) cursorLine = editor.tokenizedLineForScreenRow(0) called = false - onWillMeasureSpy = (state) -> + onWillNeedMeasurementsSpy = (state) -> called = true expect(Object.keys(state.content.tiles).length).toBeGreaterThan(0) for tile, tileState of state.content.tiles @@ -245,7 +245,7 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - presenter.onWillMeasure(onWillMeasureSpy) + presenter.onWillNeedMeasurements(onWillNeedMeasurementsSpy) presenter.getState() expect(called).toBe(true) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 7dc3468cb..b2f8e0b92 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -83,7 +83,7 @@ class TextEditorComponent @linesYardstick = new LinesYardstick(@editor, this) @presenter.setLinesYardstick(@linesYardstick) - @presenter.onWillMeasure(@updateLinesComponentSync) + @presenter.onWillNeedMeasurements(@updateLinesComponentSync) @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll}) @scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode()) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 1aed3c342..f4986fe6a 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -44,8 +44,8 @@ class TextEditorPresenter @disposables.dispose() # Calls your `callback` while performing ::getState(), before computing any state that needs measurements. - onWillMeasure: (callback) -> - @emitter.on "will-measure", callback + onWillNeedMeasurements: (callback) -> + @emitter.on "will-need-measurements", callback # Calls your `callback` when some changes in the model occurred and the current state has been updated. onDidUpdateState: (callback) -> @@ -82,7 +82,7 @@ class TextEditorPresenter @updateLinesDecorations() if @shouldUpdateDecorations @updateTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState - @emitter.emit "will-measure", @state + @emitter.emit "will-need-measurements", @state @updateCommonGutterState() @updateHorizontalDimensions() From bf7d7e0d2ad714e121741d503d5801660b284f61 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Sep 2015 10:17:53 +0200 Subject: [PATCH 19/80] Improve LinesYardstick design We have shifted the responsibility of orchestrating state updates and measurements to the yardstick. The presenter still needs to be updated to make use of these new capabilities. --- spec/lines-yardstick-spec.coffee | 39 ++++++++----------- ...der.coffee => mock-lines-component.coffee} | 14 ++++--- spec/text-editor-presenter-spec.coffee | 2 +- src/lines-yardstick.coffee | 8 +++- 4 files changed, 31 insertions(+), 32 deletions(-) rename spec/{mock-line-nodes-provider.coffee => mock-lines-component.coffee} (85%) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 23842e6ba..e68ac15e5 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -1,8 +1,8 @@ LinesYardstick = require '../src/lines-yardstick' -MockLineNodesProvider = require './mock-line-nodes-provider' +MockLinesComponent = require './mock-lines-component' describe "LinesYardstick", -> - [editor, mockLineNodesProvider, builtLineNodes, linesYardstick] = [] + [editor, mockPresenter, mockLinesComponent, linesYardstick] = [] beforeEach -> waitsForPromise -> @@ -12,14 +12,23 @@ describe "LinesYardstick", -> atom.project.open('sample.js').then (o) -> editor = o runs -> - mockLineNodesProvider = new MockLineNodesProvider(editor) - linesYardstick = new LinesYardstick(editor, mockLineNodesProvider) + mockPresenter = {getStateForMeasurements: jasmine.createSpy()} + mockLinesComponent = new MockLinesComponent(editor) + linesYardstick = new LinesYardstick(editor, mockPresenter, mockLinesComponent) + + mockLinesComponent.setDefaultFont("14px monospace") afterEach -> - mockLineNodesProvider.dispose() + doSomething = true it "converts screen positions to pixel positions", -> - mockLineNodesProvider.setDefaultFont("14px monospace") + stubState = {anything: {}} + mockPresenter.getStateForMeasurements.andReturn(stubState) + + linesYardstick.prepareScreenRowsForMeasurement([0, 1, 2]) + + expect(mockPresenter.getStateForMeasurements).toHaveBeenCalledWith([0, 1, 2]) + expect(mockLinesComponent.updateSync).toHaveBeenCalledWith(stubState) conversionTable = [ [[0, 0], {left: 0, top: editor.getLineHeightInPixels() * 0}] @@ -37,7 +46,7 @@ describe "LinesYardstick", -> linesYardstick.pixelPositionForScreenPosition(point) ).toEqual(position) - mockLineNodesProvider.setFontForScopes( + mockLinesComponent.setFontForScopes( ["source.js", "storage.modifier.js"], "16px monospace" ) linesYardstick.clearCache() @@ -57,19 +66,3 @@ describe "LinesYardstick", -> expect( linesYardstick.pixelPositionForScreenPosition(point) ).toEqual(position) - - it "does not compute the same position twice unless the cache gets cleared", -> - mockLineNodesProvider.setDefaultFont("14px monospace") - - oldPosition1 = linesYardstick.pixelPositionForScreenPosition([0, 5]) - oldPosition2 = linesYardstick.pixelPositionForScreenPosition([1, 4]) - - mockLineNodesProvider.setDefaultFont("16px monospace") - - expect(linesYardstick.pixelPositionForScreenPosition([0, 5])).toEqual(oldPosition1) - expect(linesYardstick.pixelPositionForScreenPosition([1, 4])).toEqual(oldPosition2) - - linesYardstick.clearCache() - - expect(linesYardstick.pixelPositionForScreenPosition([0, 5])).not.toEqual(oldPosition1) - expect(linesYardstick.pixelPositionForScreenPosition([1, 4])).not.toEqual(oldPosition2) diff --git a/spec/mock-line-nodes-provider.coffee b/spec/mock-lines-component.coffee similarity index 85% rename from spec/mock-line-nodes-provider.coffee rename to spec/mock-lines-component.coffee index c0944a771..fd8d04cbc 100644 --- a/spec/mock-line-nodes-provider.coffee +++ b/spec/mock-lines-component.coffee @@ -1,8 +1,8 @@ TokenIterator = require '../src/token-iterator' module.exports = -class MockLineNodesProvider - constructor: (@editor) -> +class MockLinesComponent + constructor: (@model) -> @defaultFont = "" @fontsByScopes = {} @tokenIterator = new TokenIterator @@ -11,14 +11,12 @@ class MockLineNodesProvider dispose: -> node.remove() for node in @builtLineNodes - setFontForScopes: (scopes, font) -> @fontsByScopes[scopes] = font - - setDefaultFont: (font) -> @defaultFont = font + updateSync: jasmine.createSpy() lineNodeForLineIdAndScreenRow: (id, screenRow) -> lineNode = document.createElement("div") lineNode.style.whiteSpace = "pre" - lineState = @editor.tokenizedLineForScreenRow(screenRow) + lineState = @model.tokenizedLineForScreenRow(screenRow) @tokenIterator.reset(lineState) while @tokenIterator.next() @@ -32,3 +30,7 @@ class MockLineNodesProvider document.body.appendChild(lineNode) lineNode + + setFontForScopes: (scopes, font) -> @fontsByScopes[scopes] = font + + setDefaultFont: (font) -> @defaultFont = font diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 4f662294e..60de8293b 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -5,7 +5,7 @@ TextBuffer = require 'text-buffer' TextEditor = require '../src/text-editor' TextEditorPresenter = require '../src/text-editor-presenter' LinesYardstick = require '../src/lines-yardstick' -MockLineNodesProvider = require './mock-line-nodes-provider' +MockLineNodesProvider = require './mock-lines-component' describe "TextEditorPresenter", -> # These `describe` and `it` blocks mirror the structure of the ::state object. diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 0e86e24ca..ab1177071 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -4,14 +4,18 @@ AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} module.exports = class LinesYardstick - constructor: (@model, @lineNodesProvider) -> + constructor: (@model, @presenter, @lineNodesProvider) -> + @cachedPositionsByLineId = {} @tokenIterator = new TokenIterator @rangeForMeasurement = document.createRange() - @cachedPositionsByLineId = {} clearCache: -> @cachedPositionsByLineId = {} + prepareScreenRowsForMeasurement: (screenRows) -> + state = @presenter.getStateForMeasurements(screenRows) + @lineNodesProvider.updateSync(state) + pixelPositionForScreenPosition: (screenPosition, clip=true) -> screenPosition = Point.fromObject(screenPosition) screenPosition = @model.clipScreenPosition(screenPosition) if clip From 96d4bdb173d11153678a6f28415c716da62283ba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Sep 2015 10:26:06 +0200 Subject: [PATCH 20/80] Revert changes to the presenter and the component --- spec/text-editor-component-spec.coffee | 52 +------- spec/text-editor-presenter-spec.coffee | 167 +++++-------------------- src/text-editor-component.coffee | 15 --- src/text-editor-presenter.coffee | 127 +++++++++---------- 4 files changed, 88 insertions(+), 273 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 4b69382b5..e0230fbb8 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -219,54 +219,6 @@ describe "TextEditorComponent", -> expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) - it "renders the longest screen row even when it's not visible", -> - wrapperNode.style.height = 2 * lineHeightInPixels + 'px' - component.measureDimensions() - nextAnimationFrame() - - tileNodes = component.tileNodesForLines() - expect(tileNodes.length).toBe(3) - - expect(getComputedStyle(tileNodes[0]).visibility).toBe("visible") - expect(tileNodes[0].querySelectorAll(".line").length).toBe(3) - - expect(getComputedStyle(tileNodes[1]).visibility).toBe("visible") - expect(tileNodes[1].querySelectorAll(".line").length).toBe(3) - - # tile with the longest screen row - expect(getComputedStyle(tileNodes[2]).visibility).toBe("hidden") - expect(tileNodes[2].querySelectorAll(".line").length).toBe(1) - - verticalScrollbarNode.scrollTop = 4 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - nextAnimationFrame() - - tileNodes = component.tileNodesForLines() - expect(tileNodes.length).toBe(2) # don't render an extra tile if the longest screen row is already visible - - expect(getComputedStyle(tileNodes[0]).visibility).toBe("visible") - expect(tileNodes[0].querySelectorAll(".line").length).toBe(3) - - expect(getComputedStyle(tileNodes[1]).visibility).toBe("visible") - expect(tileNodes[1].querySelectorAll(".line").length).toBe(3) - - verticalScrollbarNode.scrollTop = 0 * lineHeightInPixels - verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) - nextAnimationFrame() - - tileNodes = component.tileNodesForLines() - expect(tileNodes.length).toBe(3) - - expect(getComputedStyle(tileNodes[0]).visibility).toBe("visible") - expect(tileNodes[0].querySelectorAll(".line").length).toBe(3) - - expect(getComputedStyle(tileNodes[1]).visibility).toBe("visible") - expect(tileNodes[1].querySelectorAll(".line").length).toBe(3) - - # tile with the longest screen row - expect(getComputedStyle(tileNodes[2]).visibility).toBe("hidden") - expect(tileNodes[2].querySelectorAll(".line").length).toBe(1) - it "updates the lines when lines are inserted or removed above the rendered row range", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' component.measureDimensions() @@ -3050,7 +3002,7 @@ describe "TextEditorComponent", -> expect(cursorLeft).toBe line0Right describe "when lines are changed while the editor is hidden", -> - xit "does not measure new characters until the editor is shown again", -> + it "does not measure new characters until the editor is shown again", -> editor.setText('') wrapperView.hide() editor.setText('var z = 1') @@ -3071,7 +3023,7 @@ describe "TextEditorComponent", -> atom.views.performDocumentPoll() nextAnimationFrame() - expect(componentNode.querySelectorAll('.line')).toHaveLength(7) # visible rows + the longest screen row + expect(componentNode.querySelectorAll('.line')).toHaveLength(6) gutterWidth = componentNode.querySelector('.gutter').offsetWidth componentNode.style.width = gutterWidth + 14 * charWidth + editor.getVerticalScrollbarWidth() + 'px' diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 60de8293b..76589416e 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -4,14 +4,12 @@ TextBuffer = require 'text-buffer' {Point, Range} = TextBuffer TextEditor = require '../src/text-editor' TextEditorPresenter = require '../src/text-editor-presenter' -LinesYardstick = require '../src/lines-yardstick' -MockLineNodesProvider = require './mock-lines-component' describe "TextEditorPresenter", -> # These `describe` and `it` blocks mirror the structure of the ::state object. # Please maintain this structure when adding specs for new state fields. describe "::getState()", -> - [buffer, editor, linesYardstick, lineNodesProvider] = [] + [buffer, editor] = [] beforeEach -> # These *should* be mocked in the spec helper, but changing that now would break packages :-( @@ -20,13 +18,9 @@ describe "TextEditorPresenter", -> buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js')) editor = new TextEditor({buffer}) - lineNodesProvider = new MockLineNodesProvider(editor) - lineNodesProvider.setDefaultFont("16px monospace") - linesYardstick = new LinesYardstick(editor, lineNodesProvider) waitsForPromise -> buffer.load() afterEach -> - lineNodesProvider.dispose() editor.destroy() buffer.destroy() @@ -46,9 +40,7 @@ describe "TextEditorPresenter", -> scrollTop: 0 scrollLeft: 0 - presenter = new TextEditorPresenter(params) - presenter.setLinesYardstick(linesYardstick) - presenter + new TextEditorPresenter(params) expectValues = (actual, expected) -> for key, value of expected @@ -162,17 +154,7 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[10]).toBeUndefined() it "updates when ::lineHeight changes", -> - presenter = buildPresenter(explicitHeight: 10, scrollTop: 0, lineHeight: 1, tileSize: 2) - - expect(stateFn(presenter).tiles[0]).toBeDefined() - expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeDefined() - expect(stateFn(presenter).tiles[8]).toBeDefined() - expect(stateFn(presenter).tiles[10]).toBeDefined() - expect(stateFn(presenter).tiles[12]).toBeUndefined() - - expectStateUpdate presenter, -> presenter.setLineHeight(2) + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) expect(stateFn(presenter).tiles[0]).toBeDefined() expect(stateFn(presenter).tiles[2]).toBeDefined() @@ -180,6 +162,13 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[6]).toBeDefined() expect(stateFn(presenter).tiles[8]).toBeUndefined() + expectStateUpdate presenter, -> presenter.setLineHeight(2) + + expect(stateFn(presenter).tiles[0]).toBeDefined() + expect(stateFn(presenter).tiles[2]).toBeDefined() + expect(stateFn(presenter).tiles[4]).toBeDefined() + expect(stateFn(presenter).tiles[6]).toBeUndefined() + it "does not remove out-of-view tiles corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) @@ -220,36 +209,10 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[0]).toBeDefined() describe "during state retrieval", -> - it "does not trigger ::onDidUpdateState events", -> + it "does not trigger onDidUpdateState events", -> presenter = buildPresenter() expectNoStateUpdate presenter, -> presenter.getState() - it "triggers ::onWillNeedMeasurements events before computing any state that needs measurement", -> - editor.setCursorBufferPosition([0, 0]) - cursorLine = editor.tokenizedLineForScreenRow(0) - called = false - - onWillNeedMeasurementsSpy = (state) -> - called = true - expect(Object.keys(state.content.tiles).length).toBeGreaterThan(0) - for tile, tileState of state.content.tiles - expect(tileState.highlights).toEqual({}) - expect(state.content.tiles[0].lines[cursorLine.id].decorationClasses).not.toBeNull() - - expect(state.gutters).toEqual([]) - expect(state.hiddenInput).toEqual({}) - expect(state.content.overlays).toEqual({}) - expect(state.content.cursors).toEqual({}) - expect(state.content.width).toBeUndefined() - expect(state.content.scrollWidth).toBeUndefined() - - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - - presenter.onWillNeedMeasurements(onWillNeedMeasurementsSpy) - presenter.getState() - - expect(called).toBe(true) - describe ".horizontalScrollbar", -> describe ".visible", -> it "is true if the scrollWidth exceeds the computed client width", -> @@ -331,9 +294,7 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> - lineNodesProvider.setDefaultFont("25px monospace") - presenter.setBaseCharacterWidth(15) + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 15 * maxLineLength + 1 it "updates when the scoped character widths change", -> @@ -344,13 +305,8 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> - lineNodesProvider.setFontForScopes(['source.js', 'support.function.js'], "34px monospace") - presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) - presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'u', 20) - presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 's', 20) - presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'h', 20) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 8)) + (20 * 8) + 1 # 8 of the characters are 20px wide now instead of 10px wide + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) + expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10) @@ -577,16 +533,10 @@ describe "TextEditorPresenter", -> presenter = buildPresenter() expect(presenter.getState().hiddenInput.width).toBe 10 - expectStateUpdate presenter, -> - lineNodesProvider.setDefaultFont("25px monospace") - presenter.setBaseCharacterWidth(15) + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) expect(presenter.getState().hiddenInput.width).toBe 15 - expectStateUpdate presenter, -> - lineNodesProvider.setFontForScopes(['source.js', 'storage.modifier.js'], "33px monospace") - presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20) - presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'a', 20) - presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) expect(presenter.getState().hiddenInput.width).toBe 20 it "is 2px at the end of lines", -> @@ -678,9 +628,7 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> - lineNodesProvider.setDefaultFont("25px monospace") - presenter.setBaseCharacterWidth(15) + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) expect(presenter.getState().content.scrollWidth).toBe 15 * maxLineLength + 1 it "updates when the scoped character widths change", -> @@ -691,13 +639,8 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> - lineNodesProvider.setFontForScopes(['source.js', 'support.function.js'], "33px monospace") - presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) - presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'u', 20) - presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 's', 20) - presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'h', 20) - expect(presenter.getState().content.scrollWidth).toBe (10 * (maxLineLength - 8)) + (20 * 8) + 1 # 8 of the characters are 20px wide now instead of 10px wide + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) + expect(presenter.getState().content.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10) @@ -889,46 +832,11 @@ describe "TextEditorPresenter", -> lineStateForScreenRow = (presenter, row) -> lineId = presenter.model.tokenizedLineForScreenRow(row).id tileRow = presenter.tileForRow(row) - presenter.getState().content.tiles[tileRow]?.lines?[lineId] + presenter.getState().content.tiles[tileRow]?.lines[lineId] tiledContentContract (presenter) -> presenter.getState().content - it "contains the state for the longest tile on screen", -> - presenter = buildPresenter(explicitHeight: 4, scrollTop: 0, lineHeight: 1, tileSize: 2) - - expectValues presenter.getState().content.tiles[6], { - visibility: "hidden" - } - - expectStateUpdate presenter, -> presenter.setScrollTop(2) - - expectValues presenter.getState().content.tiles[6], { - visibility: "initial" - } - - expectStateUpdate presenter, -> presenter.setScrollTop(0) - - expectValues presenter.getState().content.tiles[6], { - visibility: "hidden" - } - describe "[tileId].lines[lineId]", -> # line state objects - it "includes the state for the longest line on screen", -> - presenter = buildPresenter(explicitHeight: 4, scrollTop: 0, lineHeight: 1, tileSize: 2) - - expect(lineStateForScreenRow(presenter, 6)).toBeDefined() - expect(lineStateForScreenRow(presenter, 7)).toBeUndefined() - - expectStateUpdate presenter, -> presenter.setScrollTop(2) - - expect(lineStateForScreenRow(presenter, 6)).toBeDefined() - expect(lineStateForScreenRow(presenter, 7)).toBeDefined() - - expectStateUpdate presenter, -> presenter.setScrollTop(0) - - expect(lineStateForScreenRow(presenter, 6)).toBeDefined() - expect(lineStateForScreenRow(presenter, 7)).toBeUndefined() - it "includes the state for visible lines in a tile", -> presenter = buildPresenter(explicitHeight: 3, scrollTop: 4, lineHeight: 1, tileSize: 3, stoppedScrollingDelay: 200) @@ -1275,9 +1183,7 @@ describe "TextEditorPresenter", -> editor.setCursorBufferPosition([2, 4]) presenter = buildPresenter(explicitHeight: 20, scrollTop: 20) - expectStateUpdate presenter, -> - lineNodesProvider.setDefaultFont("33px monospace") - presenter.setBaseCharacterWidth(20) + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20) expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 20, width: 20, height: 10} it "updates when scoped character widths change", -> @@ -1288,19 +1194,11 @@ describe "TextEditorPresenter", -> editor.setCursorBufferPosition([1, 4]) presenter = buildPresenter(explicitHeight: 20) - expectStateUpdate presenter, -> - lineNodesProvider.setFontForScopes(['source.js', 'storage.modifier.js'], "33px monospace") - presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20) - presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'a', 20) - presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) - expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (2 * 10) + (2 * 20), width: 20, height: 10} + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20) + expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 10, height: 10} - expectStateUpdate presenter, -> - lineNodesProvider.setFontForScopes(['source.js', 'storage.modifier.js'], "36px monospace") - presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 22) - presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'a', 22) - presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 22) - expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (2 * 10) + (2 * 22), width: 22, height: 10} + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 20, height: 10} it "updates when cursors are added, moved, hidden, shown, or destroyed", -> editor.setSelectedBufferRanges([ @@ -1625,9 +1523,7 @@ describe "TextEditorPresenter", -> expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 2 * 10, width: 2 * 10, height: 10}] } - expectStateUpdate presenter, -> - lineNodesProvider.setDefaultFont("33px monospace") - presenter.setBaseCharacterWidth(20) + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20) expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 2 * 20, width: 2 * 20, height: 10}] } @@ -1646,12 +1542,9 @@ describe "TextEditorPresenter", -> expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] } - expectStateUpdate presenter, -> - lineNodesProvider.setFontForScopes(['source.js', 'keyword.control.js'], "25px monospace") - presenter.setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'i', 20) - presenter.setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'f', 20) + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'i', 20) expectValues stateForSelectionInTile(presenter, 0, 2), { - regions: [{top: 0, left: 4 * 10, width: 15 * 2, height: 10}] + regions: [{top: 0, left: 4 * 10, width: 20 + 10, height: 10}] } it "updates when highlight decorations are added, moved, hidden, shown, or destroyed", -> @@ -1813,9 +1706,7 @@ describe "TextEditorPresenter", -> pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 10} } - expectStateUpdate presenter, -> - lineNodesProvider.setDefaultFont("9px monospace") - presenter.setBaseCharacterWidth(5) + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(5) expectValues stateForOverlay(presenter, decoration), { item: item diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 34052af11..aa198bb3c 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -14,8 +14,6 @@ ScrollbarCornerComponent = require './scrollbar-corner-component' OverlayManager = require './overlay-manager' DOMElementPool = require './dom-element-pool' -LinesYardstick = require './lines-yardstick' - module.exports = class TextEditorComponent scrollSensitivity: 0.4 @@ -81,10 +79,6 @@ class TextEditorComponent @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) - @linesYardstick = new LinesYardstick(@editor, this) - @presenter.setLinesYardstick(@linesYardstick) - @presenter.onWillNeedMeasurements(@updateLinesComponentSync) - @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll}) @scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode()) @@ -119,9 +113,6 @@ class TextEditorComponent getDomNode: -> @domNode - updateLinesComponentSync: (state) => - @linesComponent.updateSync(state) - updateSync: -> @oldState ?= {} @newState = @presenter.getState() @@ -752,12 +743,6 @@ class TextEditorComponent consolidateSelections: (e) -> e.abortKeyBinding() unless @editor.consolidateSelections() - lineNodeForLineIdAndScreenRow: (lineId, screenRow) -> - tileRow = @presenter.tileForRow(screenRow) - tileComponent = @linesComponent.getComponentForTile(tileRow) - - tileComponent?.lineNodeForLineId(lineId) - lineNodeForScreenRow: (screenRow) -> tileRow = @presenter.tileForRow(screenRow) tileComponent = @linesComponent.getComponentForTile(tileRow) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 4699486bf..3f60aa133 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -45,10 +45,6 @@ class TextEditorPresenter destroy: -> @disposables.dispose() - # Calls your `callback` while performing ::getState(), before computing any state that needs measurements. - onWillNeedMeasurements: (callback) -> - @emitter.on "will-need-measurements", callback - # Calls your `callback` when some changes in the model occurred and the current state has been updated. onDidUpdateState: (callback) -> @emitter.on 'did-update-state', callback @@ -80,12 +76,6 @@ class TextEditorPresenter @updateScrollbarDimensions() @updateStartRow() @updateEndRow() - - @updateLinesDecorations() if @shouldUpdateDecorations - @updateTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState - - @emitter.emit "will-need-measurements", @state - @updateCommonGutterState() @updateHorizontalDimensions() @updateReflowState() @@ -97,7 +87,8 @@ class TextEditorPresenter @updateScrollbarsState() if @shouldUpdateScrollbarsState @updateHiddenInputState() if @shouldUpdateHiddenInputState @updateContentState() if @shouldUpdateContentState - @updateHighlightDecorations() if @shouldUpdateDecorations + @updateDecorations() if @shouldUpdateDecorations + @updateTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState @updateCursorsState() if @shouldUpdateCursorsState @updateOverlaysState() if @shouldUpdateOverlaysState @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState @@ -146,6 +137,7 @@ class TextEditorPresenter observeModel: -> @disposables.add @model.onDidChange => @updateVerticalDimensions() + @updateHorizontalDimensions() @shouldUpdateHeightState = true @shouldUpdateVerticalScrollState = true @@ -356,15 +348,7 @@ class TextEditorPresenter updateTilesState: -> return unless @startRow? and @endRow? and @lineHeight? - @visibleGutterTiles = {} - @visibleLinesTiles = {} - - @updateVisibleTilesState() - @updateLongestTileState() - @updateMouseWheelTileState() - @deleteHiddenTilesState() - - updateVisibleTilesState: -> + visibleTiles = {} zIndex = @getTilesCount() - 1 for startRow in [@getStartTileRow()..@getEndTileRow()] by @tileSize endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize) @@ -374,7 +358,6 @@ class TextEditorPresenter tile.left = -@scrollLeft tile.height = @tileSize * @lineHeight tile.display = "block" - tile.visibility = "initial" tile.zIndex = zIndex tile.highlights ?= {} @@ -387,40 +370,22 @@ class TextEditorPresenter @updateLinesState(tile, startRow, endRow) if @shouldUpdateLinesState @updateLineNumbersState(gutterTile, startRow, endRow) if @shouldUpdateLineNumbersState - @visibleLinesTiles[startRow] = true - @visibleGutterTiles[startRow] = true + visibleTiles[startRow] = true zIndex-- - updateMouseWheelTileState: -> - return unless @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)? + if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)? + mouseWheelTile = @tileForRow(@mouseWheelScreenRow) - mouseWheelTile = @tileForRow(@mouseWheelScreenRow) + unless visibleTiles[mouseWheelTile]? + @lineNumberGutter.tiles[mouseWheelTile].display = "none" + @state.content.tiles[mouseWheelTile].display = "none" + visibleTiles[mouseWheelTile] = true - unless @visibleGutterTiles[mouseWheelTile]? and @visibleLinesTiles[mouseWheelTile] - @lineNumberGutter.tiles[mouseWheelTile].display = "none" - @state.content.tiles[mouseWheelTile].display = "none" - @visibleGutterTiles[mouseWheelTile] = true - @visibleLinesTiles[mouseWheelTile] = true - - updateLongestTileState: -> - longestScreenRow = @model.getLongestScreenRow() - longestScreenRowTile = @tileForRow(longestScreenRow) - - return unless longestScreenRow? - return if @getStartTileRow() <= longestScreenRowTile <= @getEndTileRow() - - tile = @state.content.tiles[longestScreenRowTile] ?= {} - tile.visibility = "hidden" - tile.highlights = {} - - @updateLinesState(tile, longestScreenRow, longestScreenRow + 1) - - @visibleLinesTiles[longestScreenRowTile] = true - - deleteHiddenTilesState: -> for id, tile of @state.content.tiles - delete @state.content.tiles[id] unless @visibleLinesTiles[id] - delete @lineNumberGutter.tiles[id] unless @visibleGutterTiles[id] + continue if visibleTiles.hasOwnProperty(id) + + delete @state.content.tiles[id] + delete @lineNumberGutter.tiles[id] updateLinesState: (tileState, startRow, endRow) -> tileState.lines ?= {} @@ -1066,8 +1031,6 @@ class TextEditorPresenter @characterWidthsChanged() unless @batchingCharacterMeasurement characterWidthsChanged: -> - @linesYardstick.clearCache() - @shouldUpdateHorizontalScrollState = true @shouldUpdateVerticalScrollState = true @shouldUpdateScrollbarsState = true @@ -1087,12 +1050,42 @@ class TextEditorPresenter hasPixelPositionRequirements: -> @lineHeight? and @baseCharacterWidth? - pixelPositionForScreenPosition: (screenPosition, clip) -> - pixelPosition = - @linesYardstick.pixelPositionForScreenPosition(screenPosition, clip) - pixelPosition.top -= @scrollTop - pixelPosition.left -= @scrollLeft - pixelPosition + pixelPositionForScreenPosition: (screenPosition, clip=true) -> + screenPosition = Point.fromObject(screenPosition) + screenPosition = @model.clipScreenPosition(screenPosition) if clip + + targetRow = screenPosition.row + targetColumn = screenPosition.column + baseCharacterWidth = @baseCharacterWidth + + top = targetRow * @lineHeight + left = 0 + column = 0 + + iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator() + while iterator.next() + characterWidths = @getScopedCharacterWidths(iterator.getScopes()) + + valueIndex = 0 + text = iterator.getText() + while valueIndex < text.length + if iterator.isPairedCharacter() + char = text + charLength = 2 + valueIndex += 2 + else + char = text[valueIndex] + charLength = 1 + valueIndex++ + + break if column is targetColumn + + left += characterWidths[char] ? baseCharacterWidth unless char is '\0' + column += charLength + + top -= @scrollTop + left -= @scrollLeft + {top, left} hasPixelRectRequirements: -> @hasPixelPositionRequirements() and @scrollWidth? @@ -1172,28 +1165,22 @@ class TextEditorPresenter @emitDidUpdateState() - updateLinesDecorations: -> + updateDecorations: -> @rangesByDecorationId = {} @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterNameAndScreenRow = {} - - return unless 0 <= @startRow <= @endRow <= Infinity - - for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1) - range = @model.getMarker(markerId).getScreenRange() - for decoration in decorations when decoration.isType('line') or decoration.isType('gutter') - @addToLineDecorationCaches(decoration, range) - - updateHighlightDecorations: -> @visibleHighlights = {} return unless 0 <= @startRow <= @endRow <= Infinity for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1) range = @model.getMarker(markerId).getScreenRange() - for decoration in decorations when decoration.isType('highlight') - @updateHighlightState(decoration, range) + for decoration in decorations + if decoration.isType('line') or decoration.isType('gutter') + @addToLineDecorationCaches(decoration, range) + else if decoration.isType('highlight') + @updateHighlightState(decoration, range) for tileId, tileState of @state.content.tiles for id, highlight of tileState.highlights From 63db59e9ab4bc893041be6d34a963141ca27c7cc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Sep 2015 11:29:08 +0200 Subject: [PATCH 21/80] Start refactoring the presenter to accommodate LinesYardstick --- src/text-editor-presenter.coffee | 50 ++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 3f60aa133..b89824638 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -88,7 +88,7 @@ class TextEditorPresenter @updateHiddenInputState() if @shouldUpdateHiddenInputState @updateContentState() if @shouldUpdateContentState @updateDecorations() if @shouldUpdateDecorations - @updateTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState + @updateVisibleTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState @updateCursorsState() if @shouldUpdateCursorsState @updateOverlaysState() if @shouldUpdateOverlaysState @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState @@ -340,38 +340,50 @@ class TextEditorPresenter @tileForRow(@model.getScreenLineCount()), @tileForRow(@endRow) ) - getTilesCount: -> - Math.ceil( - (@getEndTileRow() - @getStartTileRow() + 1) / @tileSize - ) - - updateTilesState: -> + updateVisibleTilesState: -> return unless @startRow? and @endRow? and @lineHeight? - visibleTiles = {} - zIndex = @getTilesCount() - 1 - for startRow in [@getStartTileRow()..@getEndTileRow()] by @tileSize - endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize) + @updateTilesState([@startRow..@endRow]) - tile = @state.content.tiles[startRow] ?= {} - tile.top = startRow * @lineHeight - @scrollTop + updateTilesState: (screenRows) -> + visibleTiles = {} + + startRow = screenRows[0] + endRow = screenRows[screenRows.length - 1] + screenRowIndex = screenRows.length - 1 + zIndex = 0 + + for tileStartRow in [@tileForRow(endRow)..@tileForRow(startRow)] by -@tileSize + tileEndRow = Math.min(@model.getScreenLineCount(), tileStartRow + @tileSize) + rowsWithinTile = [] + + while screenRowIndex >= 0 + currentScreenRow = screenRows[screenRowIndex] + break if currentScreenRow < tileStartRow + rowsWithinTile.push(currentScreenRow) + screenRowIndex-- + + continue if rowsWithinTile.length is 0 + + tile = @state.content.tiles[tileStartRow] ?= {} + tile.top = tileStartRow * @lineHeight - @scrollTop tile.left = -@scrollLeft tile.height = @tileSize * @lineHeight tile.display = "block" tile.zIndex = zIndex tile.highlights ?= {} - gutterTile = @lineNumberGutter.tiles[startRow] ?= {} - gutterTile.top = startRow * @lineHeight - @scrollTop + gutterTile = @lineNumberGutter.tiles[tileStartRow] ?= {} + gutterTile.top = tileStartRow * @lineHeight - @scrollTop gutterTile.height = @tileSize * @lineHeight gutterTile.display = "block" gutterTile.zIndex = zIndex - @updateLinesState(tile, startRow, endRow) if @shouldUpdateLinesState - @updateLineNumbersState(gutterTile, startRow, endRow) if @shouldUpdateLineNumbersState + @updateLinesState(tile, tileStartRow, tileEndRow) if @shouldUpdateLinesState + @updateLineNumbersState(gutterTile, tileStartRow, tileEndRow) if @shouldUpdateLineNumbersState - visibleTiles[startRow] = true - zIndex-- + visibleTiles[tileStartRow] = true + zIndex++ if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)? mouseWheelTile = @tileForRow(@mouseWheelScreenRow) From 96de78f264b94b3e15d4c4e7219e3499829ee499 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Sep 2015 11:36:18 +0200 Subject: [PATCH 22/80] :fire: Avoid computing top for line and line numbers --- spec/text-editor-presenter-spec.coffee | 18 ++++++------------ src/text-editor-presenter.coffee | 3 --- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 76589416e..35ce570ae 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -851,7 +851,6 @@ describe "TextEditorPresenter", -> firstNonWhitespaceIndex: line3.firstNonWhitespaceIndex firstTrailingWhitespaceIndex: line3.firstTrailingWhitespaceIndex invisibles: line3.invisibles - top: 0 } line4 = editor.tokenizedLineForScreenRow(4) @@ -863,7 +862,6 @@ describe "TextEditorPresenter", -> firstNonWhitespaceIndex: line4.firstNonWhitespaceIndex firstTrailingWhitespaceIndex: line4.firstTrailingWhitespaceIndex invisibles: line4.invisibles - top: 1 } line5 = editor.tokenizedLineForScreenRow(5) @@ -875,7 +873,6 @@ describe "TextEditorPresenter", -> firstNonWhitespaceIndex: line5.firstNonWhitespaceIndex firstTrailingWhitespaceIndex: line5.firstTrailingWhitespaceIndex invisibles: line5.invisibles - top: 2 } line6 = editor.tokenizedLineForScreenRow(6) @@ -887,7 +884,6 @@ describe "TextEditorPresenter", -> firstNonWhitespaceIndex: line6.firstNonWhitespaceIndex firstTrailingWhitespaceIndex: line6.firstTrailingWhitespaceIndex invisibles: line6.invisibles - top: 0 } line7 = editor.tokenizedLineForScreenRow(7) @@ -899,7 +895,6 @@ describe "TextEditorPresenter", -> firstNonWhitespaceIndex: line7.firstNonWhitespaceIndex firstTrailingWhitespaceIndex: line7.firstTrailingWhitespaceIndex invisibles: line7.invisibles - top: 1 } line8 = editor.tokenizedLineForScreenRow(8) @@ -911,7 +906,6 @@ describe "TextEditorPresenter", -> firstNonWhitespaceIndex: line8.firstNonWhitespaceIndex firstTrailingWhitespaceIndex: line8.firstTrailingWhitespaceIndex invisibles: line8.invisibles - top: 2 } expect(lineStateForScreenRow(presenter, 9)).toBeUndefined() @@ -2050,12 +2044,12 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, tileSize: 2) expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {screenRow: 2, bufferRow: 2, softWrapped: false, top: 0 * 10} - expectValues lineNumberStateForScreenRow(presenter, 3), {screenRow: 3, bufferRow: 3, softWrapped: false, top: 1 * 10} - expectValues lineNumberStateForScreenRow(presenter, 4), {screenRow: 4, bufferRow: 3, softWrapped: true, top: 0 * 10} - expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false, top: 1 * 10} - expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 7, softWrapped: false, top: 0 * 10} - expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 8, softWrapped: false, top: 1 * 10} + expectValues lineNumberStateForScreenRow(presenter, 2), {screenRow: 2, bufferRow: 2, softWrapped: false} + expectValues lineNumberStateForScreenRow(presenter, 3), {screenRow: 3, bufferRow: 3, softWrapped: false} + expectValues lineNumberStateForScreenRow(presenter, 4), {screenRow: 4, bufferRow: 3, softWrapped: true} + expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false} + expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 7, softWrapped: false} + expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 8, softWrapped: false} expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() it "updates when the editor's content changes", -> diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index b89824638..39c3b9c25 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -412,7 +412,6 @@ class TextEditorPresenter if tileState.lines.hasOwnProperty(line.id) lineState = tileState.lines[line.id] lineState.screenRow = row - lineState.top = (row - startRow) * @lineHeight lineState.decorationClasses = @lineDecorationClassesForRow(row) else tileState.lines[line.id] = @@ -429,7 +428,6 @@ class TextEditorPresenter indentLevel: line.indentLevel tabLength: line.tabLength fold: line.fold - top: (row - startRow) * @lineHeight decorationClasses: @lineDecorationClassesForRow(row) row++ @@ -623,7 +621,6 @@ class TextEditorPresenter softWrapped = false screenRow = startRow + i - top = (screenRow - startRow) * @lineHeight decorationClasses = @lineNumberDecorationClassesForRow(screenRow) foldable = @model.isFoldableAtScreenRow(screenRow) id = @model.tokenizedLineForScreenRow(screenRow).id From b773b60c7b2ab92ea8095d1845b179c605125d6d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Sep 2015 12:03:00 +0200 Subject: [PATCH 23/80] :bug: Avoid creating a useless extra tile --- spec/text-editor-presenter-spec.coffee | 19 +++++-------------- src/text-editor-presenter.coffee | 4 ++-- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 35ce570ae..b72d64986 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -93,11 +93,7 @@ describe "TextEditorPresenter", -> expectValues stateFn(presenter).tiles[8], { top: 5 } - expectValues stateFn(presenter).tiles[10], { - top: 7 - } - - expect(stateFn(presenter).tiles[12]).toBeUndefined() + expect(stateFn(presenter).tiles[10]).toBeUndefined() it "includes state for all tiles if no external ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null, tileSize: 2) @@ -166,8 +162,7 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[0]).toBeDefined() expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeUndefined() + expect(stateFn(presenter).tiles[4]).toBeUndefined() it "does not remove out-of-view tiles corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) @@ -1436,7 +1431,7 @@ describe "TextEditorPresenter", -> presenter.setBaseCharacterWidth(8) assignedAnyHighlight = false for tileId, tileState of presenter.getState().content.tiles - assignedAnyHighlight ||= _.isEqual(tileState.highlights, {}) + assignedAnyHighlight ||= not _.isEqual(tileState.highlights, {}) expect(assignedAnyHighlight).toBe(true) @@ -2065,9 +2060,7 @@ describe "TextEditorPresenter", -> expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4} expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} - expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8} - expectValues lineNumberStateForScreenRow(presenter, 9), {bufferRow: 9} - expect(lineNumberStateForScreenRow(presenter, 10)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() expectStateUpdate presenter, -> editor.getBuffer().insert([3, Infinity], new Array(25).join("x ")) @@ -2079,9 +2072,7 @@ describe "TextEditorPresenter", -> expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 3} expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 4} expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 7} - expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8} - expectValues lineNumberStateForScreenRow(presenter, 9), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 10)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() it "correctly handles the first screen line being soft-wrapped", -> editor.setSoftWrapped(true) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 39c3b9c25..e8c99ca25 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -343,7 +343,7 @@ class TextEditorPresenter updateVisibleTilesState: -> return unless @startRow? and @endRow? and @lineHeight? - @updateTilesState([@startRow..@endRow]) + @updateTilesState([@startRow...@endRow]) updateTilesState: (screenRows) -> visibleTiles = {} @@ -360,7 +360,7 @@ class TextEditorPresenter while screenRowIndex >= 0 currentScreenRow = screenRows[screenRowIndex] break if currentScreenRow < tileStartRow - rowsWithinTile.push(currentScreenRow) + rowsWithinTile.unshift(currentScreenRow) screenRowIndex-- continue if rowsWithinTile.length is 0 From 03507f7be386a80d443effcfa0dede80abc4bc10 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Sep 2015 12:24:53 +0200 Subject: [PATCH 24/80] Revert previous commit --- spec/text-editor-presenter-spec.coffee | 19 ++++++++++++++----- src/text-editor-presenter.coffee | 6 ++++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index b72d64986..35ce570ae 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -93,7 +93,11 @@ describe "TextEditorPresenter", -> expectValues stateFn(presenter).tiles[8], { top: 5 } - expect(stateFn(presenter).tiles[10]).toBeUndefined() + expectValues stateFn(presenter).tiles[10], { + top: 7 + } + + expect(stateFn(presenter).tiles[12]).toBeUndefined() it "includes state for all tiles if no external ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null, tileSize: 2) @@ -162,7 +166,8 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[0]).toBeDefined() expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeUndefined() + expect(stateFn(presenter).tiles[4]).toBeDefined() + expect(stateFn(presenter).tiles[6]).toBeUndefined() it "does not remove out-of-view tiles corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) @@ -1431,7 +1436,7 @@ describe "TextEditorPresenter", -> presenter.setBaseCharacterWidth(8) assignedAnyHighlight = false for tileId, tileState of presenter.getState().content.tiles - assignedAnyHighlight ||= not _.isEqual(tileState.highlights, {}) + assignedAnyHighlight ||= _.isEqual(tileState.highlights, {}) expect(assignedAnyHighlight).toBe(true) @@ -2060,7 +2065,9 @@ describe "TextEditorPresenter", -> expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4} expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8} + expectValues lineNumberStateForScreenRow(presenter, 9), {bufferRow: 9} + expect(lineNumberStateForScreenRow(presenter, 10)).toBeUndefined() expectStateUpdate presenter, -> editor.getBuffer().insert([3, Infinity], new Array(25).join("x ")) @@ -2072,7 +2079,9 @@ describe "TextEditorPresenter", -> expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 3} expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 4} expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 7} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8} + expectValues lineNumberStateForScreenRow(presenter, 9), {bufferRow: 8} + expect(lineNumberStateForScreenRow(presenter, 10)).toBeUndefined() it "correctly handles the first screen line being soft-wrapped", -> editor.setSoftWrapped(true) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index e8c99ca25..ec239ef46 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -343,7 +343,9 @@ class TextEditorPresenter updateVisibleTilesState: -> return unless @startRow? and @endRow? and @lineHeight? - @updateTilesState([@startRow...@endRow]) + startRow = @getStartTileRow() + endRow = Math.min(@model.getScreenLineCount(), @getEndTileRow() + @tileSize) + @updateTilesState([startRow...endRow]) updateTilesState: (screenRows) -> visibleTiles = {} @@ -360,7 +362,7 @@ class TextEditorPresenter while screenRowIndex >= 0 currentScreenRow = screenRows[screenRowIndex] break if currentScreenRow < tileStartRow - rowsWithinTile.unshift(currentScreenRow) + rowsWithinTile.push(currentScreenRow) screenRowIndex-- continue if rowsWithinTile.length is 0 From be94b4da07fc660331e335e62b1def06c7760e0a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Sep 2015 12:42:37 +0200 Subject: [PATCH 25/80] Use arrays instead of while loops --- src/text-editor-presenter.coffee | 56 +++++++++++++------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index ec239ef46..45ee3d7cc 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -381,8 +381,8 @@ class TextEditorPresenter gutterTile.display = "block" gutterTile.zIndex = zIndex - @updateLinesState(tile, tileStartRow, tileEndRow) if @shouldUpdateLinesState - @updateLineNumbersState(gutterTile, tileStartRow, tileEndRow) if @shouldUpdateLineNumbersState + @updateLinesState(tile, rowsWithinTile) if @shouldUpdateLinesState + @updateLineNumbersState(gutterTile, rowsWithinTile) if @shouldUpdateLineNumbersState visibleTiles[tileStartRow] = true zIndex++ @@ -401,23 +401,22 @@ class TextEditorPresenter delete @state.content.tiles[id] delete @lineNumberGutter.tiles[id] - updateLinesState: (tileState, startRow, endRow) -> + updateLinesState: (tileState, screenRows) -> tileState.lines ?= {} visibleLineIds = {} - row = startRow - while row < endRow - line = @model.tokenizedLineForScreenRow(row) + for screenRow in screenRows + line = @model.tokenizedLineForScreenRow(screenRow) unless line? throw new Error("No line exists for row #{row}. Last screen row: #{@model.getLastScreenRow()}") visibleLineIds[line.id] = true if tileState.lines.hasOwnProperty(line.id) lineState = tileState.lines[line.id] - lineState.screenRow = row - lineState.decorationClasses = @lineDecorationClassesForRow(row) + lineState.screenRow = screenRow + lineState.decorationClasses = @lineDecorationClassesForRow(screenRow) else tileState.lines[line.id] = - screenRow: row + screenRow: screenRow text: line.text openScopes: line.openScopes tags: line.tags @@ -430,8 +429,7 @@ class TextEditorPresenter indentLevel: line.indentLevel tabLength: line.tabLength fold: line.fold - decorationClasses: @lineDecorationClassesForRow(row) - row++ + decorationClasses: @lineDecorationClassesForRow(screenRow) for id, line of tileState.lines delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) @@ -603,32 +601,24 @@ class TextEditorPresenter isVisible = isVisible and @showLineNumbers isVisible - updateLineNumbersState: (tileState, startRow, endRow) -> + isSoftWrappedRow: (bufferRow, screenRow) -> + return false if screenRow is 0 + + @model.bufferRowForScreenRow(screenRow - 1) is bufferRow + + updateLineNumbersState: (tileState, screenRows) -> tileState.lineNumbers ?= {} visibleLineNumberIds = {} - if startRow > 0 - rowBeforeStartRow = startRow - 1 - lastBufferRow = @model.bufferRowForScreenRow(rowBeforeStartRow) - else - lastBufferRow = null + for screenRow in screenRows + bufferRow = @model.bufferRowForScreenRow(screenRow) + softWrapped = @isSoftWrappedRow(bufferRow, screenRow) + decorationClasses = @lineNumberDecorationClassesForRow(screenRow) + foldable = @model.isFoldableAtScreenRow(screenRow) + id = @model.tokenizedLineForScreenRow(screenRow).id - if endRow > startRow - bufferRows = @model.bufferRowsForScreenRows(startRow, endRow - 1) - for bufferRow, i in bufferRows - if bufferRow is lastBufferRow - softWrapped = true - else - lastBufferRow = bufferRow - softWrapped = false - - screenRow = startRow + i - decorationClasses = @lineNumberDecorationClassesForRow(screenRow) - foldable = @model.isFoldableAtScreenRow(screenRow) - id = @model.tokenizedLineForScreenRow(screenRow).id - - tileState.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable} - visibleLineNumberIds[id] = true + tileState.lineNumbers[id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable} + visibleLineNumberIds[id] = true for id of tileState.lineNumbers delete tileState.lineNumbers[id] unless visibleLineNumberIds[id] From 7fafdbb6abc510999b5593e5a83561c7051b17df Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Sep 2015 14:00:04 +0200 Subject: [PATCH 26/80] Implement ::getStateForMeasurements(screenRows) --- spec/text-editor-presenter-spec.coffee | 113 ++++++++++++++----------- src/text-editor-presenter.coffee | 25 +++++- 2 files changed, 84 insertions(+), 54 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 35ce570ae..98b12930e 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -5,59 +5,72 @@ TextBuffer = require 'text-buffer' TextEditor = require '../src/text-editor' TextEditorPresenter = require '../src/text-editor-presenter' -describe "TextEditorPresenter", -> +fdescribe "TextEditorPresenter", -> + [buffer, editor] = [] + + beforeEach -> + # These *should* be mocked in the spec helper, but changing that now would break packages :-( + spyOn(window, "setInterval").andCallFake window.fakeSetInterval + spyOn(window, "clearInterval").andCallFake window.fakeClearInterval + + buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js')) + editor = new TextEditor({buffer}) + waitsForPromise -> buffer.load() + + afterEach -> + editor.destroy() + buffer.destroy() + + buildPresenter = (params={}) -> + _.defaults params, + model: editor + explicitHeight: 130 + contentFrameWidth: 500 + windowWidth: 500 + windowHeight: 130 + boundingClientRect: {left: 0, top: 0, width: 500, height: 130} + gutterWidth: 0 + lineHeight: 10 + baseCharacterWidth: 10 + horizontalScrollbarHeight: 10 + verticalScrollbarWidth: 10 + scrollTop: 0 + scrollLeft: 0 + + new TextEditorPresenter(params) + + expectValues = (actual, expected) -> + for key, value of expected + expect(actual[key]).toEqual value + + expectStateUpdatedToBe = (value, presenter, fn) -> + updatedState = false + disposable = presenter.onDidUpdateState -> + updatedState = true + disposable.dispose() + fn() + expect(updatedState).toBe(value) + + expectStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(true, presenter, fn) + + expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn) + + describe "::getStateForMeasurements(screenRows)", -> + it "contains states for tiles of the visible rows + the supplied ones", -> + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) + state = presenter.getStateForMeasurements([10, 11]) + + expect(state.content.tiles[0]).toBeDefined() + expect(state.content.tiles[2]).toBeDefined() + expect(state.content.tiles[4]).toBeDefined() + expect(state.content.tiles[6]).toBeDefined() + expect(state.content.tiles[8]).toBeUndefined() + expect(state.content.tiles[10]).toBeDefined() + expect(state.content.tiles[12]).toBeUndefined() + # These `describe` and `it` blocks mirror the structure of the ::state object. # Please maintain this structure when adding specs for new state fields. describe "::getState()", -> - [buffer, editor] = [] - - beforeEach -> - # These *should* be mocked in the spec helper, but changing that now would break packages :-( - spyOn(window, "setInterval").andCallFake window.fakeSetInterval - spyOn(window, "clearInterval").andCallFake window.fakeClearInterval - - buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js')) - editor = new TextEditor({buffer}) - waitsForPromise -> buffer.load() - - afterEach -> - editor.destroy() - buffer.destroy() - - buildPresenter = (params={}) -> - _.defaults params, - model: editor - explicitHeight: 130 - contentFrameWidth: 500 - windowWidth: 500 - windowHeight: 130 - boundingClientRect: {left: 0, top: 0, width: 500, height: 130} - gutterWidth: 0 - lineHeight: 10 - baseCharacterWidth: 10 - horizontalScrollbarHeight: 10 - verticalScrollbarWidth: 10 - scrollTop: 0 - scrollLeft: 0 - - new TextEditorPresenter(params) - - expectValues = (actual, expected) -> - for key, value of expected - expect(actual[key]).toEqual value - - expectStateUpdatedToBe = (value, presenter, fn) -> - updatedState = false - disposable = presenter.onDidUpdateState -> - updatedState = true - disposable.dispose() - fn() - expect(updatedState).toBe(value) - - expectStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(true, presenter, fn) - - expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn) - tiledContentContract = (stateFn) -> it "contains states for tiles that are visible on screen", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 45ee3d7cc..a8deeb4fe 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -67,6 +67,18 @@ class TextEditorPresenter isBatching: -> @updating is false + getStateForMeasurements: (screenRows) -> + screenRows = _.uniq(screenRows.concat(@getVisibleRows())) + + @updateVerticalDimensions() + @updateScrollbarDimensions() + @updateStartRow() + @updateEndRow() + + @updateTilesState(screenRows) + + @state + # Public: Gets this presenter's state, updating it just in time before returning from this function. # Returns a state {Object}, useful for rendering to screen. getState: -> @@ -340,16 +352,21 @@ class TextEditorPresenter @tileForRow(@model.getScreenLineCount()), @tileForRow(@endRow) ) + getVisibleRows: -> + startRow = @getStartTileRow() + endRow = Math.min(@model.getScreenLineCount(), @getEndTileRow() + @tileSize) + + [startRow...endRow] + updateVisibleTilesState: -> return unless @startRow? and @endRow? and @lineHeight? - startRow = @getStartTileRow() - endRow = Math.min(@model.getScreenLineCount(), @getEndTileRow() + @tileSize) - @updateTilesState([startRow...endRow]) + @updateTilesState(@getVisibleRows()) updateTilesState: (screenRows) -> - visibleTiles = {} + screenRows.sort (a, b) -> a - b + visibleTiles = {} startRow = screenRows[0] endRow = screenRows[screenRows.length - 1] screenRowIndex = screenRows.length - 1 From 7b95b9923a1b7ff442055d6142d48f207c645ecb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Sep 2015 16:05:14 +0200 Subject: [PATCH 27/80] Start integrating the yardstick --- src/text-editor-presenter.coffee | 33 +++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index a8deeb4fe..9663aaaf1 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -68,14 +68,15 @@ class TextEditorPresenter @updating is false getStateForMeasurements: (screenRows) -> - screenRows = _.uniq(screenRows.concat(@getVisibleRows())) - @updateVerticalDimensions() @updateScrollbarDimensions() @updateStartRow() @updateEndRow() - @updateTilesState(screenRows) + screenRows = _.without(screenRows, null, undefined, @getVisibleRows()...) + + @updateVisibleTilesState() if @shouldUpdateLineNumbersState or @shouldUpdateLinesState + @updateTilesState(screenRows, true) @state @@ -84,10 +85,11 @@ class TextEditorPresenter getState: -> @updating = true - @updateVerticalDimensions() - @updateScrollbarDimensions() - @updateStartRow() - @updateEndRow() + @linesYardstick.prepareScreenRowsForMeasurement([ + @model.getLongestScreenRow() + ]) + @deleteTemporaryTiles() + @updateCommonGutterState() @updateHorizontalDimensions() @updateReflowState() @@ -359,11 +361,16 @@ class TextEditorPresenter [startRow...endRow] updateVisibleTilesState: -> - return unless @startRow? and @endRow? and @lineHeight? - @updateTilesState(@getVisibleRows()) - updateTilesState: (screenRows) -> + deleteTemporaryTiles: -> + for tileId, tileState of @state.content.tiles when tileState.temporary + delete @state.content.tiles[tileId] + delete @lineNumberGutter.tiles[tileId] + + updateTilesState: (screenRows, temporary = false) -> + return unless @startRow? and @endRow? and @lineHeight? + screenRows.sort (a, b) -> a - b visibleTiles = {} @@ -391,12 +398,14 @@ class TextEditorPresenter tile.display = "block" tile.zIndex = zIndex tile.highlights ?= {} + tile.temporary = temporary gutterTile = @lineNumberGutter.tiles[tileStartRow] ?= {} gutterTile.top = tileStartRow * @lineHeight - @scrollTop gutterTile.height = @tileSize * @lineHeight gutterTile.display = "block" gutterTile.zIndex = zIndex + gutterTile.temporary = temporary @updateLinesState(tile, rowsWithinTile) if @shouldUpdateLinesState @updateLineNumbersState(gutterTile, rowsWithinTile) if @shouldUpdateLineNumbersState @@ -404,6 +413,8 @@ class TextEditorPresenter visibleTiles[tileStartRow] = true zIndex++ + return if temporary + if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)? mouseWheelTile = @tileForRow(@mouseWheelScreenRow) @@ -424,7 +435,7 @@ class TextEditorPresenter for screenRow in screenRows line = @model.tokenizedLineForScreenRow(screenRow) unless line? - throw new Error("No line exists for row #{row}. Last screen row: #{@model.getLastScreenRow()}") + throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}") visibleLineIds[line.id] = true if tileState.lines.hasOwnProperty(line.id) From af41b71cd8f245fe45c3fae411466194c15a85a4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 20 Sep 2015 12:10:09 +0200 Subject: [PATCH 28/80] Redesign LinesYardstick --- spec/lines-yardstick-spec.coffee | 68 ------------------ spec/text-editor-presenter-spec.coffee | 24 +++---- src/lines-component.coffee | 4 ++ src/lines-tile-component.coffee | 2 + src/lines-yardstick.coffee | 21 ++++-- src/text-editor-component.coffee | 4 ++ src/text-editor-presenter.coffee | 97 ++++++++++++++------------ 7 files changed, 85 insertions(+), 135 deletions(-) delete mode 100644 spec/lines-yardstick-spec.coffee diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee deleted file mode 100644 index e68ac15e5..000000000 --- a/spec/lines-yardstick-spec.coffee +++ /dev/null @@ -1,68 +0,0 @@ -LinesYardstick = require '../src/lines-yardstick' -MockLinesComponent = require './mock-lines-component' - -describe "LinesYardstick", -> - [editor, mockPresenter, mockLinesComponent, linesYardstick] = [] - - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - waitsForPromise -> - atom.project.open('sample.js').then (o) -> editor = o - - runs -> - mockPresenter = {getStateForMeasurements: jasmine.createSpy()} - mockLinesComponent = new MockLinesComponent(editor) - linesYardstick = new LinesYardstick(editor, mockPresenter, mockLinesComponent) - - mockLinesComponent.setDefaultFont("14px monospace") - - afterEach -> - doSomething = true - - it "converts screen positions to pixel positions", -> - stubState = {anything: {}} - mockPresenter.getStateForMeasurements.andReturn(stubState) - - linesYardstick.prepareScreenRowsForMeasurement([0, 1, 2]) - - expect(mockPresenter.getStateForMeasurements).toHaveBeenCalledWith([0, 1, 2]) - expect(mockLinesComponent.updateSync).toHaveBeenCalledWith(stubState) - - conversionTable = [ - [[0, 0], {left: 0, top: editor.getLineHeightInPixels() * 0}] - [[0, 3], {left: 24, top: editor.getLineHeightInPixels() * 0}] - [[0, 4], {left: 32, top: editor.getLineHeightInPixels() * 0}] - [[0, 5], {left: 40, top: editor.getLineHeightInPixels() * 0}] - [[1, 0], {left: 0, top: editor.getLineHeightInPixels() * 1}] - [[1, 1], {left: 0, top: editor.getLineHeightInPixels() * 1}] - [[1, 6], {left: 48, top: editor.getLineHeightInPixels() * 1}] - [[1, Infinity], {left: 240, top: editor.getLineHeightInPixels() * 1}] - ] - - for [point, position] in conversionTable - expect( - linesYardstick.pixelPositionForScreenPosition(point) - ).toEqual(position) - - mockLinesComponent.setFontForScopes( - ["source.js", "storage.modifier.js"], "16px monospace" - ) - linesYardstick.clearCache() - - conversionTable = [ - [[0, 0], {left: 0, top: editor.getLineHeightInPixels() * 0}] - [[0, 3], {left: 30, top: editor.getLineHeightInPixels() * 0}] - [[0, 4], {left: 38, top: editor.getLineHeightInPixels() * 0}] - [[0, 5], {left: 46, top: editor.getLineHeightInPixels() * 0}] - [[1, 0], {left: 0, top: editor.getLineHeightInPixels() * 1}] - [[1, 1], {left: 0, top: editor.getLineHeightInPixels() * 1}] - [[1, 6], {left: 54, top: editor.getLineHeightInPixels() * 1}] - [[1, Infinity], {left: 246, top: editor.getLineHeightInPixels() * 1}] - ] - - for [point, position] in conversionTable - expect( - linesYardstick.pixelPositionForScreenPosition(point) - ).toEqual(position) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 98b12930e..e497bf2e9 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -4,15 +4,17 @@ TextBuffer = require 'text-buffer' {Point, Range} = TextBuffer TextEditor = require '../src/text-editor' TextEditorPresenter = require '../src/text-editor-presenter' +LinesYardstick = require '../src/lines-yardstick' -fdescribe "TextEditorPresenter", -> - [buffer, editor] = [] +describe "TextEditorPresenter", -> + [buffer, editor, linesYardstick, mockLineNodesProvider] = [] beforeEach -> # These *should* be mocked in the spec helper, but changing that now would break packages :-( spyOn(window, "setInterval").andCallFake window.fakeSetInterval spyOn(window, "clearInterval").andCallFake window.fakeClearInterval + mockLineNodesProvider = {updateSync: ->} buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js')) editor = new TextEditor({buffer}) waitsForPromise -> buffer.load() @@ -37,7 +39,10 @@ fdescribe "TextEditorPresenter", -> scrollTop: 0 scrollLeft: 0 - new TextEditorPresenter(params) + presenter = new TextEditorPresenter(params) + linesYardstick = new LinesYardstick(editor, presenter, mockLineNodesProvider) + presenter.setLinesYardstick(linesYardstick) + presenter expectValues = (actual, expected) -> for key, value of expected @@ -55,19 +60,6 @@ fdescribe "TextEditorPresenter", -> expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn) - describe "::getStateForMeasurements(screenRows)", -> - it "contains states for tiles of the visible rows + the supplied ones", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - state = presenter.getStateForMeasurements([10, 11]) - - expect(state.content.tiles[0]).toBeDefined() - expect(state.content.tiles[2]).toBeDefined() - expect(state.content.tiles[4]).toBeDefined() - expect(state.content.tiles[6]).toBeDefined() - expect(state.content.tiles[8]).toBeUndefined() - expect(state.content.tiles[10]).toBeDefined() - expect(state.content.tiles[12]).toBeUndefined() - # These `describe` and `it` blocks mirror the structure of the ::state object. # Please maintain this structure when adding specs for new state fields. describe "::getState()", -> diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 237f24958..b3e83fc40 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -97,3 +97,7 @@ class LinesComponent extends TiledComponent component.clearMeasurements() @presenter.clearScopedCharacterWidths() + + lineNodeForLineIdAndScreenRow: (lineId, screenRow) -> + tile = @presenter.tileForRow(screenRow) + @getComponentForTile(screenRow)?.lineNodeForLineId(lineId) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 3c70c1199..28afe7d5c 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -399,3 +399,5 @@ class LinesTileComponent clearMeasurements: -> @measuredLines.clear() + + lineNodeForLineId: (id) -> @lineNodesByLineId[id] diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index ab1177071..0ec946413 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -13,8 +13,17 @@ class LinesYardstick @cachedPositionsByLineId = {} prepareScreenRowsForMeasurement: (screenRows) -> - state = @presenter.getStateForMeasurements(screenRows) - @lineNodesProvider.updateSync(state) + @presenter.setScreenRowsToMeasure(screenRows) + @lineNodesProvider.updateSync(@presenter.getStateForMeasurements()) + + cleanup: -> + @presenter.clearScreenRowsToMeasure() + @lineNodesProvider.updateSync(@presenter.getStateForMeasurements()) + + measure: (screenRows, fn) -> + @prepareScreenRowsForMeasurement(screenRows) + fn() + @cleanup() pixelPositionForScreenPosition: (screenPosition, clip=true) -> screenPosition = Point.fromObject(screenPosition) @@ -47,7 +56,7 @@ class LinesYardstick @tokenIterator.reset(tokenizedLine) while @tokenIterator.next() - break if indexWithinTextNode? + break if foundIndexWithinTextNode? text = @tokenIterator.getText() @@ -75,16 +84,16 @@ class LinesYardstick nextTextNodeIndex = textNodeIndex + textNodeLength if charIndex is column - indexWithinTextNode = charIndex - textNodeIndex + foundIndexWithinTextNode = charIndex - textNodeIndex break charIndex += charLength if textNode? - indexWithinTextNode ?= textNode.textContent.length + foundIndexWithinTextNode ?= textNode.textContent.length @cachedPositionsByLineId[tokenizedLine.id] ?= {} @cachedPositionsByLineId[tokenizedLine.id][column] = - @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinTextNode) + @leftPixelPositionForCharInTextNode(lineNode, textNode, foundIndexWithinTextNode) else 0 diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index aa198bb3c..39b0e1ee4 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -13,6 +13,7 @@ ScrollbarComponent = require './scrollbar-component' ScrollbarCornerComponent = require './scrollbar-corner-component' OverlayManager = require './overlay-manager' DOMElementPool = require './dom-element-pool' +LinesYardstick = require './lines-yardstick' module.exports = class TextEditorComponent @@ -79,6 +80,9 @@ class TextEditorComponent @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) + @linesYardstick = new LinesYardstick(@editor, @presenter, @linesComponent) + @presenter.setLinesYardstick(@linesYardstick) + @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll}) @scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode()) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 9663aaaf1..27ff26190 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -67,16 +67,17 @@ class TextEditorPresenter isBatching: -> @updating is false - getStateForMeasurements: (screenRows) -> + getStateForMeasurements: -> @updateVerticalDimensions() @updateScrollbarDimensions() @updateStartRow() @updateEndRow() - screenRows = _.without(screenRows, null, undefined, @getVisibleRows()...) + @updateLineDecorations() if @shouldUpdateDecorations + @updateTilesState() if @shouldUpdateLineNumbersState or @shouldUpdateLinesState - @updateVisibleTilesState() if @shouldUpdateLineNumbersState or @shouldUpdateLinesState - @updateTilesState(screenRows, true) + @shouldUpdateLinesState = false + @shouldUpdateLineNumbersState = false @state @@ -85,29 +86,25 @@ class TextEditorPresenter getState: -> @updating = true - @linesYardstick.prepareScreenRowsForMeasurement([ - @model.getLongestScreenRow() - ]) - @deleteTemporaryTiles() + @linesYardstick.measure [@model.getLongestScreenRow()], => + @updateCommonGutterState() + @updateHorizontalDimensions() + @updateReflowState() - @updateCommonGutterState() - @updateHorizontalDimensions() - @updateReflowState() + @updateFocusedState() if @shouldUpdateFocusedState + @updateHeightState() if @shouldUpdateHeightState + @updateVerticalScrollState() if @shouldUpdateVerticalScrollState + @updateHorizontalScrollState() if @shouldUpdateHorizontalScrollState + @updateScrollbarsState() if @shouldUpdateScrollbarsState + @updateHiddenInputState() if @shouldUpdateHiddenInputState + @updateContentState() if @shouldUpdateContentState + @updateHighlightDecorations() if @shouldUpdateDecorations + @updateCursorsState() if @shouldUpdateCursorsState + @updateOverlaysState() if @shouldUpdateOverlaysState + @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState + @updateGutterOrderState() if @shouldUpdateGutterOrderState + @updateCustomGutterDecorationState() if @shouldUpdateCustomGutterDecorationState - @updateFocusedState() if @shouldUpdateFocusedState - @updateHeightState() if @shouldUpdateHeightState - @updateVerticalScrollState() if @shouldUpdateVerticalScrollState - @updateHorizontalScrollState() if @shouldUpdateHorizontalScrollState - @updateScrollbarsState() if @shouldUpdateScrollbarsState - @updateHiddenInputState() if @shouldUpdateHiddenInputState - @updateContentState() if @shouldUpdateContentState - @updateDecorations() if @shouldUpdateDecorations - @updateVisibleTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState - @updateCursorsState() if @shouldUpdateCursorsState - @updateOverlaysState() if @shouldUpdateOverlaysState - @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState - @updateGutterOrderState() if @shouldUpdateGutterOrderState - @updateCustomGutterDecorationState() if @shouldUpdateCustomGutterDecorationState @updating = false @resetTrackedUpdates() @@ -354,25 +351,31 @@ class TextEditorPresenter @tileForRow(@model.getScreenLineCount()), @tileForRow(@endRow) ) - getVisibleRows: -> + getVisibleScreenRows: -> startRow = @getStartTileRow() endRow = Math.min(@model.getScreenLineCount(), @getEndTileRow() + @tileSize) [startRow...endRow] - updateVisibleTilesState: -> - @updateTilesState(@getVisibleRows()) + getScreenRows: -> + screenRows = @getVisibleScreenRows().concat(@screenRowsToMeasure) + screenRows.sort (a, b) -> a - b + _.uniq(screenRows, true) - deleteTemporaryTiles: -> - for tileId, tileState of @state.content.tiles when tileState.temporary - delete @state.content.tiles[tileId] - delete @lineNumberGutter.tiles[tileId] + setScreenRowsToMeasure: (screenRows) -> + @screenRowsToMeasure = screenRows + @shouldUpdateLinesState = true + @shouldUpdateDecorations = true - updateTilesState: (screenRows, temporary = false) -> + clearScreenRowsToMeasure: -> + @screenRowsToMeasure = [] + @shouldUpdateLinesState = true + @shouldUpdateDecorations = true + + updateTilesState: -> return unless @startRow? and @endRow? and @lineHeight? - screenRows.sort (a, b) -> a - b - + screenRows = @getScreenRows() visibleTiles = {} startRow = screenRows[0] endRow = screenRows[screenRows.length - 1] @@ -398,14 +401,12 @@ class TextEditorPresenter tile.display = "block" tile.zIndex = zIndex tile.highlights ?= {} - tile.temporary = temporary gutterTile = @lineNumberGutter.tiles[tileStartRow] ?= {} gutterTile.top = tileStartRow * @lineHeight - @scrollTop gutterTile.height = @tileSize * @lineHeight gutterTile.display = "block" gutterTile.zIndex = zIndex - gutterTile.temporary = temporary @updateLinesState(tile, rowsWithinTile) if @shouldUpdateLinesState @updateLineNumbersState(gutterTile, rowsWithinTile) if @shouldUpdateLineNumbersState @@ -413,8 +414,6 @@ class TextEditorPresenter visibleTiles[tileStartRow] = true zIndex++ - return if temporary - if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)? mouseWheelTile = @tileForRow(@mouseWheelScreenRow) @@ -1194,22 +1193,30 @@ class TextEditorPresenter @emitDidUpdateState() - updateDecorations: -> + updateLineDecorations: -> @rangesByDecorationId = {} @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterNameAndScreenRow = {} + + return unless 0 <= @startRow <= @endRow <= Infinity + + for row in @getScreenRows() + for markerId, decorations of @model.decorationsForScreenRowRange(row, row) + range = @model.getMarker(markerId).getScreenRange() + for decoration in decorations + if decoration.isType('line') or decoration.isType('gutter') + @addToLineDecorationCaches(decoration, range) + + updateHighlightDecorations: -> @visibleHighlights = {} return unless 0 <= @startRow <= @endRow <= Infinity for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1) range = @model.getMarker(markerId).getScreenRange() - for decoration in decorations - if decoration.isType('line') or decoration.isType('gutter') - @addToLineDecorationCaches(decoration, range) - else if decoration.isType('highlight') - @updateHighlightState(decoration, range) + for decoration in decorations when decoration.isType('highlight') + @updateHighlightState(decoration, range) for tileId, tileState of @state.content.tiles for id, highlight of tileState.highlights From 4318de43c95b3a510d4287fd1c20376f7425b8a3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sun, 20 Sep 2015 21:02:25 +0200 Subject: [PATCH 29/80] wip --- spec/text-editor-presenter-spec.coffee | 26 ++++++++++++++ src/lines-component.coffee | 2 +- src/text-editor-component.coffee | 1 + src/text-editor-presenter.coffee | 47 +++++--------------------- 4 files changed, 37 insertions(+), 39 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index e497bf2e9..9560d5794 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -60,6 +60,32 @@ describe "TextEditorPresenter", -> expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn) + describe "::getStateForMeasurements()", -> + it "contains states for tiles that need measurement, in addition to visible ones", -> + presenter = buildPresenter(explicitHeight: 4, scrollTop: 6, lineHeight: 1, tileSize: 2) + + presenter.setScreenRowsToMeasure([4, 0]) + state = presenter.getStateForMeasurements() + + expect(state.content.tiles[0]).toBeDefined() + expect(state.content.tiles[2]).toBeUndefined() + expect(state.content.tiles[4]).toBeDefined() + expect(state.content.tiles[6]).toBeDefined() + expect(state.content.tiles[8]).toBeDefined() + expect(state.content.tiles[10]).toBeDefined() + expect(state.content.tiles[12]).toBeUndefined() + + presenter.clearScreenRowsToMeasure() + state = presenter.getStateForMeasurements() + + expect(state.content.tiles[0]).toBeUndefined() + expect(state.content.tiles[2]).toBeUndefined() + expect(state.content.tiles[4]).toBeUndefined() + expect(state.content.tiles[6]).toBeDefined() + expect(state.content.tiles[8]).toBeDefined() + expect(state.content.tiles[10]).toBeDefined() + expect(state.content.tiles[12]).toBeUndefined() + # These `describe` and `it` blocks mirror the structure of the ::state object. # Please maintain this structure when adding specs for new state fields. describe "::getState()", -> diff --git a/src/lines-component.coffee b/src/lines-component.coffee index b3e83fc40..2861e55b4 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -100,4 +100,4 @@ class LinesComponent extends TiledComponent lineNodeForLineIdAndScreenRow: (lineId, screenRow) -> tile = @presenter.tileForRow(screenRow) - @getComponentForTile(screenRow)?.lineNodeForLineId(lineId) + @getComponentForTile(tile)?.lineNodeForLineId(lineId) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 39b0e1ee4..420861cf9 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -660,6 +660,7 @@ class TextEditorComponent if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight @clearPoolAfterUpdate = true + @linesYardstick.clearCache() @measureLineHeightAndDefaultCharWidth() if (@fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily) and @performedInitialMeasurement diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 27ff26190..6d8ae7a93 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -147,8 +147,9 @@ class TextEditorPresenter observeModel: -> @disposables.add @model.onDidChange => - @updateVerticalDimensions() - @updateHorizontalDimensions() + @linesYardstick.measure [@model.getLongestScreenRow()], => + @updateVerticalDimensions() + @updateHorizontalDimensions() @shouldUpdateHeightState = true @shouldUpdateVerticalScrollState = true @@ -1078,42 +1079,12 @@ class TextEditorPresenter hasPixelPositionRequirements: -> @lineHeight? and @baseCharacterWidth? - pixelPositionForScreenPosition: (screenPosition, clip=true) -> - screenPosition = Point.fromObject(screenPosition) - screenPosition = @model.clipScreenPosition(screenPosition) if clip - - targetRow = screenPosition.row - targetColumn = screenPosition.column - baseCharacterWidth = @baseCharacterWidth - - top = targetRow * @lineHeight - left = 0 - column = 0 - - iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator() - while iterator.next() - characterWidths = @getScopedCharacterWidths(iterator.getScopes()) - - valueIndex = 0 - text = iterator.getText() - while valueIndex < text.length - if iterator.isPairedCharacter() - char = text - charLength = 2 - valueIndex += 2 - else - char = text[valueIndex] - charLength = 1 - valueIndex++ - - break if column is targetColumn - - left += characterWidths[char] ? baseCharacterWidth unless char is '\0' - column += charLength - - top -= @scrollTop - left -= @scrollLeft - {top, left} + pixelPositionForScreenPosition: (screenPosition, clip) -> + position = + @linesYardstick.pixelPositionForScreenPosition(screenPosition, clip) + position.left -= @scrollLeft + position.top -= @scrollTop + position hasPixelRectRequirements: -> @hasPixelPositionRequirements() and @scrollWidth? From b367b799bf6f247534bfbaecff20e0fda29d0284 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Sep 2015 10:28:44 +0200 Subject: [PATCH 30/80] Rename to TextEditorPresenter::invalidateState --- src/text-editor-presenter.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 6d8ae7a93..e02609105 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -35,7 +35,7 @@ class TextEditorPresenter @observeModel() @observeConfig() @buildState() - @invalidate() + @invalidateState() @startBlinkingCursors() if @focused @startReflowing() if @continuousReflow @updating = false @@ -128,7 +128,7 @@ class TextEditorPresenter @shouldUpdateGutterOrderState = false @shouldUpdateCustomGutterDecorationState = false - invalidate: -> + invalidateState: -> @shouldUpdateFocusedState = true @shouldUpdateHeightState = true @shouldUpdateVerticalScrollState = true From 13f82280a0a11e7e8cc38ad75adea2f3800c284d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Sep 2015 11:05:35 +0200 Subject: [PATCH 31/80] :green_heart: Fix presenter specs --- spec/mock-lines-component.coffee | 2 +- spec/text-editor-presenter-spec.coffee | 111 ++++++++----------------- src/lines-component.coffee | 2 +- src/text-editor-presenter.coffee | 2 + 4 files changed, 40 insertions(+), 77 deletions(-) diff --git a/spec/mock-lines-component.coffee b/spec/mock-lines-component.coffee index fd8d04cbc..64c37ad4f 100644 --- a/spec/mock-lines-component.coffee +++ b/spec/mock-lines-component.coffee @@ -8,7 +8,7 @@ class MockLinesComponent @tokenIterator = new TokenIterator @builtLineNodes = [] - dispose: -> + destroy: -> node.remove() for node in @builtLineNodes updateSync: jasmine.createSpy() diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 9560d5794..7a7709e7f 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -5,23 +5,30 @@ TextBuffer = require 'text-buffer' TextEditor = require '../src/text-editor' TextEditorPresenter = require '../src/text-editor-presenter' LinesYardstick = require '../src/lines-yardstick' +MockLinesComponent = require './mock-lines-component' describe "TextEditorPresenter", -> - [buffer, editor, linesYardstick, mockLineNodesProvider] = [] + [buffer, editor, linesYardstick, mockLinesComponent] = [] beforeEach -> # These *should* be mocked in the spec helper, but changing that now would break packages :-( spyOn(window, "setInterval").andCallFake window.fakeSetInterval spyOn(window, "clearInterval").andCallFake window.fakeClearInterval - mockLineNodesProvider = {updateSync: ->} buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js')) editor = new TextEditor({buffer}) - waitsForPromise -> buffer.load() + waitsForPromise -> + buffer.load() afterEach -> editor.destroy() buffer.destroy() + mockLinesComponent.destroy() + + buildLinesYardstick = (presenter) -> + mockLinesComponent = new MockLinesComponent(editor) + mockLinesComponent.setDefaultFont("16px monospace") + new LinesYardstick(editor, presenter, mockLinesComponent) buildPresenter = (params={}) -> _.defaults params, @@ -40,7 +47,7 @@ describe "TextEditorPresenter", -> scrollLeft: 0 presenter = new TextEditorPresenter(params) - linesYardstick = new LinesYardstick(editor, presenter, mockLineNodesProvider) + linesYardstick = buildLinesYardstick(presenter) presenter.setLinesYardstick(linesYardstick) presenter @@ -320,24 +327,15 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setContentFrameWidth(10 * maxLineLength + 20) expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 20 - it "updates when the ::baseCharacterWidth changes", -> + it "updates when character widths change", -> maxLineLength = editor.getMaxScreenLineLength() presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 15 * maxLineLength + 1 - - it "updates when the scoped character widths change", -> - waitsForPromise -> atom.packages.activatePackage('language-javascript') - - runs -> - maxLineLength = editor.getMaxScreenLineLength() - presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide + expectStateUpdate presenter, -> + mockLinesComponent.setDefaultFont("33px monospace") + presenter.characterWidthsChanged() + expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 20 * maxLineLength + 1 it "updates when ::softWrapped changes on the editor", -> presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10) @@ -564,10 +562,9 @@ describe "TextEditorPresenter", -> presenter = buildPresenter() expect(presenter.getState().hiddenInput.width).toBe 10 - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) - expect(presenter.getState().hiddenInput.width).toBe 15 - - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + expectStateUpdate presenter, -> + mockLinesComponent.setDefaultFont("33px monospace") + presenter.characterWidthsChanged() expect(presenter.getState().hiddenInput.width).toBe 20 it "is 2px at the end of lines", -> @@ -654,24 +651,15 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setContentFrameWidth(10 * maxLineLength + 20) expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 20 - it "updates when the ::baseCharacterWidth changes", -> + it "updates when character widths change", -> maxLineLength = editor.getMaxScreenLineLength() presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) - expect(presenter.getState().content.scrollWidth).toBe 15 * maxLineLength + 1 - - it "updates when the scoped character widths change", -> - waitsForPromise -> atom.packages.activatePackage('language-javascript') - - runs -> - maxLineLength = editor.getMaxScreenLineLength() - presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - - expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) - expect(presenter.getState().content.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide + expectStateUpdate presenter, -> + mockLinesComponent.setDefaultFont("33px monospace") + presenter.characterWidthsChanged() + expect(presenter.getState().content.scrollWidth).toBe 20 * maxLineLength + 1 it "updates when ::softWrapped changes on the editor", -> presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10) @@ -1204,27 +1192,15 @@ describe "TextEditorPresenter", -> expect(stateForCursor(presenter, 3)).toEqual {top: 5, left: 12 * 10, width: 10, height: 5} expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 5 - 20, left: 4 * 10, width: 10, height: 5} - it "updates when ::baseCharacterWidth changes", -> + it "updates when character widths change", -> editor.setCursorBufferPosition([2, 4]) presenter = buildPresenter(explicitHeight: 20, scrollTop: 20) - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20) + expectStateUpdate presenter, -> + mockLinesComponent.setDefaultFont("33px monospace") + presenter.characterWidthsChanged() expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 20, width: 20, height: 10} - it "updates when scoped character widths change", -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - runs -> - editor.setCursorBufferPosition([1, 4]) - presenter = buildPresenter(explicitHeight: 20) - - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20) - expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 10, height: 10} - - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) - expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 20, height: 10} - it "updates when cursors are added, moved, hidden, shown, or destroyed", -> editor.setSelectedBufferRanges([ [[1, 2], [1, 2]], @@ -1538,7 +1514,7 @@ describe "TextEditorPresenter", -> ] } - it "updates when ::baseCharacterWidth changes", -> + it "updates when character widths change", -> editor.setSelectedBufferRanges([ [[2, 2], [2, 4]], ]) @@ -1548,30 +1524,13 @@ describe "TextEditorPresenter", -> expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 2 * 10, width: 2 * 10, height: 10}] } - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20) + expectStateUpdate presenter, -> + mockLinesComponent.setDefaultFont("33px monospace") + presenter.characterWidthsChanged() expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 2 * 20, width: 2 * 20, height: 10}] } - it "updates when scoped character widths change", -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - runs -> - editor.setSelectedBufferRanges([ - [[2, 4], [2, 6]], - ]) - - presenter = buildPresenter(explicitHeight: 20, scrollTop: 0, tileSize: 2) - - expectValues stateForSelectionInTile(presenter, 0, 2), { - regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] - } - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'i', 20) - expectValues stateForSelectionInTile(presenter, 0, 2), { - regions: [{top: 0, left: 4 * 10, width: 20 + 10, height: 10}] - } - it "updates when highlight decorations are added, moved, hidden, shown, or destroyed", -> editor.setSelectedBufferRanges([ [[1, 2], [1, 4]], @@ -1720,7 +1679,7 @@ describe "TextEditorPresenter", -> pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} } - it "updates when ::baseCharacterWidth changes", -> + it "updates when character widths change", -> scrollTop = 20 marker = editor.markBufferPosition([2, 13], invalidate: 'touch') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) @@ -1731,7 +1690,9 @@ describe "TextEditorPresenter", -> pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 10} } - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(5) + expectStateUpdate presenter, -> + mockLinesComponent.setDefaultFont("8px monospace") + presenter.characterWidthsChanged() expectValues stateForOverlay(presenter, decoration), { item: item diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 2861e55b4..4bddb302e 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -10,7 +10,7 @@ module.exports = class LinesComponent extends TiledComponent placeholderTextDiv: null - constructor: ({@presenter, @hostElement, @useShadowDOM, visible, @domElementPool}) -> + constructor: ({@presenter, @useShadowDOM, @domElementPool}) -> @domNode = document.createElement('div') @domNode.classList.add('lines') @tilesNode = document.createElement("div") diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index e02609105..00b532213 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1060,6 +1060,8 @@ class TextEditorPresenter @characterWidthsChanged() unless @batchingCharacterMeasurement characterWidthsChanged: -> + @linesYardstick.clearCache() + @shouldUpdateHorizontalScrollState = true @shouldUpdateVerticalScrollState = true @shouldUpdateScrollbarsState = true From 39e8920c72232f74a37c60e26b08b37c12964def Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Sep 2015 11:31:34 +0200 Subject: [PATCH 32/80] Reset only lines state --- src/text-editor-presenter.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 48b685dc5..13cc911e4 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -74,10 +74,9 @@ class TextEditorPresenter @updateEndRow() @updateLineDecorations() if @shouldUpdateDecorations - @updateTilesState() if @shouldUpdateLineNumbersState or @shouldUpdateLinesState + @updateTilesState() if @shouldUpdateLinesState @shouldUpdateLinesState = false - @shouldUpdateLineNumbersState = false @state From 58c219d95c8292b820a310db22108bb2fe46c080 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Sep 2015 11:37:07 +0200 Subject: [PATCH 33/80] Revert "Reset only lines state" This reverts commit 39e8920c72232f74a37c60e26b08b37c12964def. --- src/text-editor-presenter.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 13cc911e4..48b685dc5 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -74,9 +74,10 @@ class TextEditorPresenter @updateEndRow() @updateLineDecorations() if @shouldUpdateDecorations - @updateTilesState() if @shouldUpdateLinesState + @updateTilesState() if @shouldUpdateLineNumbersState or @shouldUpdateLinesState @shouldUpdateLinesState = false + @shouldUpdateLineNumbersState = false @state From 1e4a1723c96de2e7ff88e9f1409f503493c1f975 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Sep 2015 11:05:43 +0200 Subject: [PATCH 34/80] :art: Some refactoring before updating from master --- src/text-editor-presenter.coffee | 66 ++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 48b685dc5..62e1da167 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -86,24 +86,25 @@ class TextEditorPresenter getState: -> @updating = true - @linesYardstick.measure [@model.getLongestScreenRow()], => - @updateCommonGutterState() - @updateHorizontalDimensions() - @updateReflowState() + @linesYardstick.prepareScreenRowsForMeasurement() - @updateFocusedState() if @shouldUpdateFocusedState - @updateHeightState() if @shouldUpdateHeightState - @updateVerticalScrollState() if @shouldUpdateVerticalScrollState - @updateHorizontalScrollState() if @shouldUpdateHorizontalScrollState - @updateScrollbarsState() if @shouldUpdateScrollbarsState - @updateHiddenInputState() if @shouldUpdateHiddenInputState - @updateContentState() if @shouldUpdateContentState - @updateHighlightDecorations() if @shouldUpdateDecorations - @updateCursorsState() if @shouldUpdateCursorsState - @updateOverlaysState() if @shouldUpdateOverlaysState - @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState - @updateGutterOrderState() if @shouldUpdateGutterOrderState - @updateCustomGutterDecorationState() if @shouldUpdateCustomGutterDecorationState + @updateCommonGutterState() + @updateHorizontalDimensions() + @updateReflowState() + + @updateFocusedState() if @shouldUpdateFocusedState + @updateHeightState() if @shouldUpdateHeightState + @updateVerticalScrollState() if @shouldUpdateVerticalScrollState + @updateHorizontalScrollState() if @shouldUpdateHorizontalScrollState + @updateScrollbarsState() if @shouldUpdateScrollbarsState + @updateHiddenInputState() if @shouldUpdateHiddenInputState + @updateContentState() if @shouldUpdateContentState + @updateHighlightDecorations() if @shouldUpdateDecorations + @updateCursorsState() if @shouldUpdateCursorsState + @updateOverlaysState() if @shouldUpdateOverlaysState + @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState + @updateGutterOrderState() if @shouldUpdateGutterOrderState + @updateCustomGutterDecorationState() if @shouldUpdateCustomGutterDecorationState @updating = false @@ -344,33 +345,42 @@ class TextEditorPresenter tileForRow: (row) -> row - (row % @tileSize) + constrainRow: (row) -> + Math.min(0, Math.max(0, @model.getScreenLineCount())) + getStartTileRow: -> - Math.max(0, @tileForRow(@startRow)) + @constrainRow(@tileForRow(@startRow)) getEndTileRow: -> - Math.min( - @tileForRow(@model.getScreenLineCount()), @tileForRow(@endRow) - ) - - getVisibleScreenRows: -> - startRow = @getStartTileRow() - endRow = Math.min(@model.getScreenLineCount(), @getEndTileRow() + @tileSize) - - [startRow...endRow] + @constrainRow(@tileForRow(@endRow)) getScreenRows: -> - screenRows = @getVisibleScreenRows().concat(@screenRowsToMeasure) + startRow = @getStartTileRow() + endRow = @constrainRow(@getEndTileRow() + @tileSize) + + screenRows = [startRow...endRow] + if longestScreenRow = @model.getLongestScreenRow() + screenRows.push(longestScreenRow) + if @screenRowsToMeasure? + screenRows.push(@screenRowsToMeasure...) + screenRows.sort (a, b) -> a - b _.uniq(screenRows, true) setScreenRowsToMeasure: (screenRows) -> + return if not screenRows? or screenRows.length is 0 + @screenRowsToMeasure = screenRows @shouldUpdateLinesState = true + @shouldUpdateLineNumbersState = true @shouldUpdateDecorations = true clearScreenRowsToMeasure: -> + return if not screenRows? or screenRows.length is 0 + @screenRowsToMeasure = [] @shouldUpdateLinesState = true + @shouldUpdateLineNumbersState = true @shouldUpdateDecorations = true updateTilesState: -> From 6f7b98178c4035344ec17fde0ae14771e729d8fe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Sep 2015 15:40:43 +0200 Subject: [PATCH 35/80] Fix linting errors --- src/lines-tile-component.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 28afe7d5c..3c70c1199 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -399,5 +399,3 @@ class LinesTileComponent clearMeasurements: -> @measuredLines.clear() - - lineNodeForLineId: (id) -> @lineNodesByLineId[id] From aec72e5ed63b6c047be6ca32000c2642ddbe6f86 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Sep 2015 15:43:13 +0200 Subject: [PATCH 36/80] Fix soft-wrapping spec --- spec/text-editor-component-spec.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 27e9864aa..8e003d942 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -473,8 +473,7 @@ describe "TextEditorComponent", -> editor.setText "a line that wraps \n" editor.setSoftWrapped(true) nextAnimationFrame() - componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' - component.measureDimensions() + wrapperNode.setWidth(16 * charWidth) nextAnimationFrame() it "doesn't show end of line invisibles at the end of wrapped lines", -> From 49577313e47285282ff64ca3a8783abf29b010b3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Sep 2015 10:25:54 +0200 Subject: [PATCH 37/80] Remove yardstick to start with a clean slate --- spec/text-editor-component-spec.coffee | 3 +- spec/text-editor-presenter-spec.coffee | 232 +++++++++++++------------ src/text-editor-component.coffee | 5 - src/text-editor-presenter.coffee | 121 ++++++------- 4 files changed, 183 insertions(+), 178 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 8e003d942..27e9864aa 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -473,7 +473,8 @@ describe "TextEditorComponent", -> editor.setText "a line that wraps \n" editor.setSoftWrapped(true) nextAnimationFrame() - wrapperNode.setWidth(16 * charWidth) + componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' + component.measureDimensions() nextAnimationFrame() it "doesn't show end of line invisibles at the end of wrapped lines", -> diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index a57356604..fbe74fb96 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -4,98 +4,60 @@ TextBuffer = require 'text-buffer' {Point, Range} = TextBuffer TextEditor = require '../src/text-editor' TextEditorPresenter = require '../src/text-editor-presenter' -LinesYardstick = require '../src/lines-yardstick' -MockLinesComponent = require './mock-lines-component' describe "TextEditorPresenter", -> - [buffer, editor, linesYardstick, mockLinesComponent] = [] - - beforeEach -> - # These *should* be mocked in the spec helper, but changing that now would break packages :-( - spyOn(window, "setInterval").andCallFake window.fakeSetInterval - spyOn(window, "clearInterval").andCallFake window.fakeClearInterval - - buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js')) - editor = new TextEditor({buffer}) - waitsForPromise -> - buffer.load() - - afterEach -> - editor.destroy() - buffer.destroy() - mockLinesComponent.destroy() - - buildLinesYardstick = (presenter) -> - mockLinesComponent = new MockLinesComponent(editor) - mockLinesComponent.setDefaultFont("16px monospace") - new LinesYardstick(editor, presenter, mockLinesComponent) - - buildPresenter = (params={}) -> - _.defaults params, - model: editor - explicitHeight: 130 - contentFrameWidth: 500 - windowWidth: 500 - windowHeight: 130 - boundingClientRect: {left: 0, top: 0, width: 500, height: 130} - gutterWidth: 0 - lineHeight: 10 - baseCharacterWidth: 10 - horizontalScrollbarHeight: 10 - verticalScrollbarWidth: 10 - scrollTop: 0 - scrollLeft: 0 - - presenter = new TextEditorPresenter(params) - linesYardstick = buildLinesYardstick(presenter) - presenter.setLinesYardstick(linesYardstick) - presenter - - expectValues = (actual, expected) -> - for key, value of expected - expect(actual[key]).toEqual value - - expectStateUpdatedToBe = (value, presenter, fn) -> - updatedState = false - disposable = presenter.onDidUpdateState -> - updatedState = true - disposable.dispose() - fn() - expect(updatedState).toBe(value) - - expectStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(true, presenter, fn) - - expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn) - - describe "::getStateForMeasurements()", -> - it "contains states for tiles that need measurement, in addition to visible ones", -> - presenter = buildPresenter(explicitHeight: 4, scrollTop: 6, lineHeight: 1, tileSize: 2) - - presenter.setScreenRowsToMeasure([4, 0]) - state = presenter.getStateForMeasurements() - - expect(state.content.tiles[0]).toBeDefined() - expect(state.content.tiles[2]).toBeUndefined() - expect(state.content.tiles[4]).toBeDefined() - expect(state.content.tiles[6]).toBeDefined() - expect(state.content.tiles[8]).toBeDefined() - expect(state.content.tiles[10]).toBeDefined() - expect(state.content.tiles[12]).toBeUndefined() - - presenter.clearScreenRowsToMeasure() - state = presenter.getStateForMeasurements() - - expect(state.content.tiles[0]).toBeUndefined() - expect(state.content.tiles[2]).toBeUndefined() - expect(state.content.tiles[4]).toBeUndefined() - expect(state.content.tiles[6]).toBeDefined() - expect(state.content.tiles[8]).toBeDefined() - expect(state.content.tiles[10]).toBeDefined() - expect(state.content.tiles[12]).toBeUndefined() - # These `describe` and `it` blocks mirror the structure of the ::state object. # Please maintain this structure when adding specs for new state fields. describe "::getState()", -> + [buffer, editor] = [] + + beforeEach -> + # These *should* be mocked in the spec helper, but changing that now would break packages :-( + spyOn(window, "setInterval").andCallFake window.fakeSetInterval + spyOn(window, "clearInterval").andCallFake window.fakeClearInterval + + buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js')) + editor = new TextEditor({buffer}) + waitsForPromise -> buffer.load() + + afterEach -> + editor.destroy() + buffer.destroy() + + buildPresenter = (params={}) -> + _.defaults params, + model: editor + explicitHeight: 130 + contentFrameWidth: 500 + windowWidth: 500 + windowHeight: 130 + boundingClientRect: {left: 0, top: 0, width: 500, height: 130} + gutterWidth: 0 + lineHeight: 10 + baseCharacterWidth: 10 + horizontalScrollbarHeight: 10 + verticalScrollbarWidth: 10 + scrollTop: 0 + scrollLeft: 0 + + new TextEditorPresenter(params) + + expectValues = (actual, expected) -> + for key, value of expected + expect(actual[key]).toEqual value + + expectStateUpdatedToBe = (value, presenter, fn) -> + updatedState = false + disposable = presenter.onDidUpdateState -> + updatedState = true + disposable.dispose() + fn() + expect(updatedState).toBe(value) + + expectStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(true, presenter, fn) + + expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn) + tiledContentContract = (stateFn) -> it "contains states for tiles that are visible on screen", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) @@ -327,15 +289,24 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setContentFrameWidth(10 * maxLineLength + 20) expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 20 - it "updates when character widths change", -> + it "updates when the ::baseCharacterWidth changes", -> maxLineLength = editor.getMaxScreenLineLength() presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> - mockLinesComponent.setDefaultFont("33px monospace") - presenter.characterWidthsChanged() - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 20 * maxLineLength + 1 + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) + expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 15 * maxLineLength + 1 + + it "updates when the scoped character widths change", -> + waitsForPromise -> atom.packages.activatePackage('language-javascript') + + runs -> + maxLineLength = editor.getMaxScreenLineLength() + presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) + + expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) + expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10) @@ -574,9 +545,10 @@ describe "TextEditorPresenter", -> presenter = buildPresenter() expect(presenter.getState().hiddenInput.width).toBe 10 - expectStateUpdate presenter, -> - mockLinesComponent.setDefaultFont("33px monospace") - presenter.characterWidthsChanged() + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) + expect(presenter.getState().hiddenInput.width).toBe 15 + + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) expect(presenter.getState().hiddenInput.width).toBe 20 it "is 2px at the end of lines", -> @@ -664,15 +636,24 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setContentFrameWidth(10 * maxLineLength + 20) expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 20 - it "updates when character widths change", -> + it "updates when the ::baseCharacterWidth changes", -> maxLineLength = editor.getMaxScreenLineLength() presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> - mockLinesComponent.setDefaultFont("33px monospace") - presenter.characterWidthsChanged() - expect(presenter.getState().content.scrollWidth).toBe 20 * maxLineLength + 1 + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) + expect(presenter.getState().content.scrollWidth).toBe 15 * maxLineLength + 1 + + it "updates when the scoped character widths change", -> + waitsForPromise -> atom.packages.activatePackage('language-javascript') + + runs -> + maxLineLength = editor.getMaxScreenLineLength() + presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) + + expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) + expect(presenter.getState().content.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10) @@ -1294,15 +1275,27 @@ describe "TextEditorPresenter", -> expect(stateForCursor(presenter, 3)).toEqual {top: 5, left: 12 * 10, width: 10, height: 5} expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 5 - 20, left: 4 * 10, width: 10, height: 5} - it "updates when character widths change", -> + it "updates when ::baseCharacterWidth changes", -> editor.setCursorBufferPosition([2, 4]) presenter = buildPresenter(explicitHeight: 20, scrollTop: 20) - expectStateUpdate presenter, -> - mockLinesComponent.setDefaultFont("33px monospace") - presenter.characterWidthsChanged() + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20) expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 20, width: 20, height: 10} + it "updates when scoped character widths change", -> + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + + runs -> + editor.setCursorBufferPosition([1, 4]) + presenter = buildPresenter(explicitHeight: 20) + + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20) + expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 10, height: 10} + + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 20, height: 10} + it "updates when cursors are added, moved, hidden, shown, or destroyed", -> editor.setSelectedBufferRanges([ [[1, 2], [1, 2]], @@ -1616,7 +1609,7 @@ describe "TextEditorPresenter", -> ] } - it "updates when character widths change", -> + it "updates when ::baseCharacterWidth changes", -> editor.setSelectedBufferRanges([ [[2, 2], [2, 4]], ]) @@ -1626,13 +1619,30 @@ describe "TextEditorPresenter", -> expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 2 * 10, width: 2 * 10, height: 10}] } - expectStateUpdate presenter, -> - mockLinesComponent.setDefaultFont("33px monospace") - presenter.characterWidthsChanged() + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20) expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 2 * 20, width: 2 * 20, height: 10}] } + it "updates when scoped character widths change", -> + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + + runs -> + editor.setSelectedBufferRanges([ + [[2, 4], [2, 6]], + ]) + + presenter = buildPresenter(explicitHeight: 20, scrollTop: 0, tileSize: 2) + + expectValues stateForSelectionInTile(presenter, 0, 2), { + regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] + } + expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'i', 20) + expectValues stateForSelectionInTile(presenter, 0, 2), { + regions: [{top: 0, left: 4 * 10, width: 20 + 10, height: 10}] + } + it "updates when highlight decorations are added, moved, hidden, shown, or destroyed", -> editor.setSelectedBufferRanges([ [[1, 2], [1, 4]], @@ -1781,7 +1791,7 @@ describe "TextEditorPresenter", -> pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} } - it "updates when character widths change", -> + it "updates when ::baseCharacterWidth changes", -> scrollTop = 20 marker = editor.markBufferPosition([2, 13], invalidate: 'touch') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) @@ -1792,9 +1802,7 @@ describe "TextEditorPresenter", -> pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 10} } - expectStateUpdate presenter, -> - mockLinesComponent.setDefaultFont("8px monospace") - presenter.characterWidthsChanged() + expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(5) expectValues stateForOverlay(presenter, decoration), { item: item diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index fcd443151..49b80e1b6 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -12,7 +12,6 @@ ScrollbarComponent = require './scrollbar-component' ScrollbarCornerComponent = require './scrollbar-corner-component' OverlayManager = require './overlay-manager' DOMElementPool = require './dom-element-pool' -LinesYardstick = require './lines-yardstick' module.exports = class TextEditorComponent @@ -86,9 +85,6 @@ class TextEditorComponent @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) - @linesYardstick = new LinesYardstick(@editor, @presenter, @linesComponent) - @presenter.setLinesYardstick(@linesYardstick) - @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll}) @scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode()) @@ -716,7 +712,6 @@ class TextEditorComponent if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight @clearPoolAfterUpdate = true - @linesYardstick.clearCache() @measureLineHeightAndDefaultCharWidth() if (@fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily) and @performedInitialMeasurement diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 7554487ad..525fa82f7 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -41,8 +41,6 @@ class TextEditorPresenter @startReflowing() if @continuousReflow @updating = false - setLinesYardstick: (@linesYardstick) -> - destroy: -> @disposables.dispose() @@ -65,32 +63,17 @@ class TextEditorPresenter isBatching: -> @updating is false - getStateForMeasurements: -> - @updateVerticalDimensions() - @updateScrollbarDimensions() - @updateScrollPosition() - @updateStartRow() - @updateEndRow() - - @fetchVisibleDecorations() if @shouldUpdateDecorations - - @updateLineDecorations() if @shouldUpdateDecorations - @updateTilesState() if @shouldUpdateLineNumbersState or @shouldUpdateLinesState - - @shouldUpdateLinesState = false - @shouldUpdateLineNumbersState = false - - @state - # Public: Gets this presenter's state, updating it just in time before returning from this function. # Returns a state {Object}, useful for rendering to screen. getState: -> @updating = true - @linesYardstick.prepareScreenRowsForMeasurement() - + @updateContentDimensions() + @updateScrollbarDimensions() + @updateScrollPosition() + @updateStartRow() + @updateEndRow() @updateCommonGutterState() - @updateHorizontalDimensions() @updateReflowState() @updateFocusedState() if @shouldUpdateFocusedState @@ -100,13 +83,13 @@ class TextEditorPresenter @updateScrollbarsState() if @shouldUpdateScrollbarsState @updateHiddenInputState() if @shouldUpdateHiddenInputState @updateContentState() if @shouldUpdateContentState - @updateHighlightDecorations() if @shouldUpdateDecorations + @updateDecorations() if @shouldUpdateDecorations + @updateTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState @updateCursorsState() if @shouldUpdateCursorsState @updateOverlaysState() if @shouldUpdateOverlaysState @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState @updateGutterOrderState() if @shouldUpdateGutterOrderState @updateCustomGutterDecorationState() if @shouldUpdateCustomGutterDecorationState - @updating = false @resetTrackedUpdates() @@ -355,8 +338,8 @@ class TextEditorPresenter endRow = @constrainRow(@getEndTileRow() + @tileSize) screenRows = [startRow...endRow] - if longestScreenRow = @model.getLongestScreenRow() - screenRows.push(longestScreenRow) + # if longestScreenRow = @model.getLongestScreenRow() + # screenRows.push(longestScreenRow) if @screenRowsToMeasure? screenRows.push(@screenRowsToMeasure...) @@ -694,17 +677,11 @@ class TextEditorPresenter @scrollHeight = scrollHeight @updateScrollTop() - updateVerticalDimensions: -> + updateContentDimensions: -> if @lineHeight? oldContentHeight = @contentHeight @contentHeight = @lineHeight * @model.getScreenLineCount() - if @contentHeight isnt oldContentHeight - @updateHeight() - @updateScrollbarDimensions() - @updateScrollHeight() - - updateHorizontalDimensions: -> if @baseCharacterWidth? oldContentWidth = @contentWidth clip = @model.tokenizedLineForScreenRow(@model.getLongestScreenRow())?.isSoftWrapped() @@ -712,6 +689,11 @@ class TextEditorPresenter @contentWidth += @scrollLeft @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width + if @contentHeight isnt oldContentHeight + @updateHeight() + @updateScrollbarDimensions() + @updateScrollHeight() + if @contentWidth isnt oldContentWidth @updateScrollbarDimensions() @updateScrollWidth() @@ -1105,8 +1087,6 @@ class TextEditorPresenter @characterWidthsChanged() unless @batchingCharacterMeasurement characterWidthsChanged: -> - @linesYardstick.clearCache() - @shouldUpdateHorizontalScrollState = true @shouldUpdateVerticalScrollState = true @shouldUpdateScrollbarsState = true @@ -1126,12 +1106,42 @@ class TextEditorPresenter hasPixelPositionRequirements: -> @lineHeight? and @baseCharacterWidth? - pixelPositionForScreenPosition: (screenPosition, clip) -> - position = - @linesYardstick.pixelPositionForScreenPosition(screenPosition, clip) - position.left -= @scrollLeft - position.top -= @scrollTop - position + pixelPositionForScreenPosition: (screenPosition, clip=true) -> + screenPosition = Point.fromObject(screenPosition) + screenPosition = @model.clipScreenPosition(screenPosition) if clip + + targetRow = screenPosition.row + targetColumn = screenPosition.column + baseCharacterWidth = @baseCharacterWidth + + top = targetRow * @lineHeight + left = 0 + column = 0 + + iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator() + while iterator.next() + characterWidths = @getScopedCharacterWidths(iterator.getScopes()) + + valueIndex = 0 + text = iterator.getText() + while valueIndex < text.length + if iterator.isPairedCharacter() + char = text + charLength = 2 + valueIndex += 2 + else + char = text[valueIndex] + charLength = 1 + valueIndex++ + + break if column is targetColumn + + left += characterWidths[char] ? baseCharacterWidth unless char is '\0' + column += charLength + + top -= @scrollTop + left -= @scrollLeft + {top, left} hasPixelRectRequirements: -> @hasPixelPositionRequirements() and @scrollWidth? @@ -1211,31 +1221,22 @@ class TextEditorPresenter @emitDidUpdateState() - fetchVisibleDecorations: -> - @visibleDecorations = [] - - for row in @getScreenRows() - for markerId, decorations of @model.decorationsForScreenRowRange(row, row) - range = @model.getMarker(markerId).getScreenRange() - for decoration in decorations - @visibleDecorations.push({decoration, range}) - - updateLineDecorations: -> + updateDecorations: -> @rangesByDecorationId = {} @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterNameAndScreenRow = {} - - for {decoration, range} in @visibleDecorations - if decoration.isType('line') or decoration.isType('gutter') - @addToLineDecorationCaches(decoration, range) - - updateHighlightDecorations: -> @visibleHighlights = {} - for {decoration, range} in @visibleDecorations - if decoration.isType('highlight') - @updateHighlightState(decoration, range) + return unless 0 <= @startRow <= @endRow <= Infinity + + for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1) + range = @model.getMarker(markerId).getScreenRange() + for decoration in decorations + if decoration.isType('line') or decoration.isType('gutter') + @addToLineDecorationCaches(decoration, range) + else if decoration.isType('highlight') + @updateHighlightState(decoration, range) for tileId, tileState of @state.content.tiles for id, highlight of tileState.highlights From 4386910e59d1ba73081228f1d9daaf101fe000ae Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Sep 2015 10:49:28 +0200 Subject: [PATCH 38/80] Decouple pending logical scroll positions --- src/text-editor-presenter.coffee | 96 +++++++++++++++++--------------- 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 525fa82f7..4dd016326 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -70,7 +70,14 @@ class TextEditorPresenter @updateContentDimensions() @updateScrollbarDimensions() - @updateScrollPosition() + + @restoreScrollPosition() + @commitPendingLogicalScrollTopPosition() + @commitPendingScrollTopPosition() + @commitPendingLogicalScrollLeftPosition() + @commitPendingScrollLeftPosition() + @clearPendingScrollPosition() + @updateStartRow() @updateEndRow() @updateCommonGutterState() @@ -826,10 +833,10 @@ class TextEditorPresenter @emitDidUpdateState() - setScrollTop: (scrollTop) -> + setScrollTop: (scrollTop, overrideScroll=true) -> return unless scrollTop? - @pendingScrollLogicalPosition = null + @pendingScrollLogicalPosition = null if overrideScroll @pendingScrollTop = scrollTop @shouldUpdateVerticalScrollState = true @@ -867,10 +874,10 @@ class TextEditorPresenter @emitDidUpdateState() - setScrollLeft: (scrollLeft) -> + setScrollLeft: (scrollLeft, overrideScroll=true) -> return unless scrollLeft? - @pendingScrollLogicalPosition = null + @pendingScrollLogicalPosition = null if overrideScroll @pendingScrollLeft = scrollLeft @shouldUpdateHorizontalScrollState = true @@ -901,13 +908,13 @@ class TextEditorPresenter @contentFrameWidth - @verticalScrollbarWidth getScrollBottom: -> @getScrollTop() + @getClientHeight() - setScrollBottom: (scrollBottom) -> - @setScrollTop(scrollBottom - @getClientHeight()) + setScrollBottom: (scrollBottom, overrideScroll) -> + @setScrollTop(scrollBottom - @getClientHeight(), overrideScroll) @getScrollBottom() getScrollRight: -> @getScrollLeft() + @getClientWidth() - setScrollRight: (scrollRight) -> - @setScrollLeft(scrollRight - @getClientWidth()) + setScrollRight: (scrollRight, overrideScroll) -> + @setScrollLeft(scrollRight - @getClientWidth(), overrideScroll) @getScrollRight() getScrollHeight: -> @@ -1520,23 +1527,15 @@ class TextEditorPresenter getHorizontalScrollbarHeight: -> @horizontalScrollbarHeight - commitPendingLogicalScrollPosition: -> + commitPendingLogicalScrollTopPosition: -> return unless @pendingScrollLogicalPosition? {screenRange, options} = @pendingScrollLogicalPosition verticalScrollMarginInPixels = @getVerticalScrollMarginInPixels() - horizontalScrollMarginInPixels = @getHorizontalScrollMarginInPixels() - {top, left} = @pixelRectForScreenRange(new Range(screenRange.start, screenRange.start)) - {top: endTop, left: endLeft, height: endHeight} = @pixelRectForScreenRange(new Range(screenRange.end, screenRange.end)) - bottom = endTop + endHeight - right = endLeft - - top += @scrollTop - bottom += @scrollTop - left += @scrollLeft - right += @scrollLeft + top = screenRange.start.row * @lineHeight + bottom = (screenRange.end.row + 1) * @lineHeight if options?.center desiredScrollCenter = (top + bottom) / 2 @@ -1547,31 +1546,43 @@ class TextEditorPresenter desiredScrollTop = top - verticalScrollMarginInPixels desiredScrollBottom = bottom + verticalScrollMarginInPixels + if options?.reversed ? true + if desiredScrollBottom > @getScrollBottom() + @setScrollBottom(desiredScrollBottom, false) + if desiredScrollTop < @getScrollTop() + @setScrollTop(desiredScrollTop, false) + else + if desiredScrollTop < @getScrollTop() + @setScrollTop(desiredScrollTop, false) + if desiredScrollBottom > @getScrollBottom() + @setScrollBottom(desiredScrollBottom, false) + + commitPendingLogicalScrollLeftPosition: -> + return unless @pendingScrollLogicalPosition? + + {screenRange, options} = @pendingScrollLogicalPosition + + horizontalScrollMarginInPixels = @getHorizontalScrollMarginInPixels() + + {left} = @pixelRectForScreenRange(new Range(screenRange.start, screenRange.start)) + {left: right} = @pixelRectForScreenRange(new Range(screenRange.end, screenRange.end)) + + left += @scrollLeft + right += @scrollLeft + desiredScrollLeft = left - horizontalScrollMarginInPixels desiredScrollRight = right + horizontalScrollMarginInPixels if options?.reversed ? true - if desiredScrollBottom > @getScrollBottom() - @setScrollBottom(desiredScrollBottom) - if desiredScrollTop < @getScrollTop() - @setScrollTop(desiredScrollTop) - if desiredScrollRight > @getScrollRight() - @setScrollRight(desiredScrollRight) + @setScrollRight(desiredScrollRight, false) if desiredScrollLeft < @getScrollLeft() - @setScrollLeft(desiredScrollLeft) + @setScrollLeft(desiredScrollLeft, false) else - if desiredScrollTop < @getScrollTop() - @setScrollTop(desiredScrollTop) - if desiredScrollBottom > @getScrollBottom() - @setScrollBottom(desiredScrollBottom) - if desiredScrollLeft < @getScrollLeft() - @setScrollLeft(desiredScrollLeft) + @setScrollLeft(desiredScrollLeft, false) if desiredScrollRight > @getScrollRight() - @setScrollRight(desiredScrollRight) - - @pendingScrollLogicalPosition = null + @setScrollRight(desiredScrollRight, false) commitPendingScrollLeftPosition: -> return unless @pendingScrollLeft? @@ -1585,8 +1596,6 @@ class TextEditorPresenter @emitter.emit 'did-change-scroll-left', @scrollLeft - @pendingScrollLeft = null - commitPendingScrollTopPosition: -> return unless @pendingScrollTop? @@ -1600,8 +1609,6 @@ class TextEditorPresenter @didStartScrolling() @emitter.emit 'did-change-scroll-top', @scrollTop - @pendingScrollTop = null - restoreScrollPosition: -> return if @hasRestoredScrollPosition or not @hasPixelPositionRequirements() @@ -1610,11 +1617,10 @@ class TextEditorPresenter @hasRestoredScrollPosition = true - updateScrollPosition: -> - @restoreScrollPosition() - @commitPendingLogicalScrollPosition() - @commitPendingScrollLeftPosition() - @commitPendingScrollTopPosition() + clearPendingScrollPosition: -> + @pendingScrollLogicalPosition = null + @pendingScrollTop = null + @pendingScrollLeft = null canScrollLeftTo: (scrollLeft) -> @scrollLeft isnt @constrainScrollLeft(scrollLeft) From a8bb011f8941b9a16e584d3b332d63963db6569e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Sep 2015 11:25:58 +0200 Subject: [PATCH 39/80] Split presenter state in pre/post measurement --- src/text-editor-presenter.coffee | 41 ++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 4dd016326..6028c864b 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -63,26 +63,32 @@ class TextEditorPresenter isBatching: -> @updating is false - # Public: Gets this presenter's state, updating it just in time before returning from this function. - # Returns a state {Object}, useful for rendering to screen. - getState: -> + getPreMeasurementState: -> @updating = true - @updateContentDimensions() + @updateVerticalDimensions() @updateScrollbarDimensions() @restoreScrollPosition() @commitPendingLogicalScrollTopPosition() @commitPendingScrollTopPosition() - @commitPendingLogicalScrollLeftPosition() - @commitPendingScrollLeftPosition() - @clearPendingScrollPosition() @updateStartRow() @updateEndRow() @updateCommonGutterState() @updateReflowState() + @updateTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState + + @updating = false + @state + + getPostMeasurementState: -> + @updateHorizontalDimensions() + @commitPendingLogicalScrollLeftPosition() + @commitPendingScrollLeftPosition() + @clearPendingScrollPosition() + @updateFocusedState() if @shouldUpdateFocusedState @updateHeightState() if @shouldUpdateHeightState @updateVerticalScrollState() if @shouldUpdateVerticalScrollState @@ -101,6 +107,14 @@ class TextEditorPresenter @resetTrackedUpdates() + # Public: Gets this presenter's state, updating it just in time before returning from this function. + # Returns a state {Object}, useful for rendering to screen. + getState: -> + @updating = true + + @getPreMeasurementState() + @getPostMeasurementState() + @state resetTrackedUpdates: -> @@ -684,11 +698,17 @@ class TextEditorPresenter @scrollHeight = scrollHeight @updateScrollTop() - updateContentDimensions: -> + updateVerticalDimensions: -> if @lineHeight? oldContentHeight = @contentHeight @contentHeight = @lineHeight * @model.getScreenLineCount() + if @contentHeight isnt oldContentHeight + @updateHeight() + @updateScrollbarDimensions() + @updateScrollHeight() + + updateHorizontalDimensions: -> if @baseCharacterWidth? oldContentWidth = @contentWidth clip = @model.tokenizedLineForScreenRow(@model.getLongestScreenRow())?.isSoftWrapped() @@ -696,11 +716,6 @@ class TextEditorPresenter @contentWidth += @scrollLeft @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width - if @contentHeight isnt oldContentHeight - @updateHeight() - @updateScrollbarDimensions() - @updateScrollHeight() - if @contentWidth isnt oldContentWidth @updateScrollbarDimensions() @updateScrollWidth() From b481250757e186153065e63ecdef6c566c5d48fe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Sep 2015 11:41:44 +0200 Subject: [PATCH 40/80] Always render the longest screen row --- spec/text-editor-component-spec.coffee | 2 +- spec/text-editor-presenter-spec.coffee | 7 ++++--- src/text-editor-presenter.coffee | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 27e9864aa..43c47281e 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -3051,7 +3051,7 @@ describe "TextEditorComponent", -> atom.views.performDocumentPoll() nextAnimationFrame() - expect(componentNode.querySelectorAll('.line')).toHaveLength(6) + expect(componentNode.querySelectorAll('.line')).toHaveLength(7) # visible rows + model longest screen row gutterWidth = componentNode.querySelector('.gutter').offsetWidth componentNode.style.width = gutterWidth + 14 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px' diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index fbe74fb96..1bd0f580c 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -162,12 +162,13 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[6]).toBeDefined() expect(stateFn(presenter).tiles[8]).toBeUndefined() - expectStateUpdate presenter, -> presenter.setLineHeight(2) + expectStateUpdate presenter, -> presenter.setLineHeight(4) expect(stateFn(presenter).tiles[0]).toBeDefined() expect(stateFn(presenter).tiles[2]).toBeDefined() - expect(stateFn(presenter).tiles[4]).toBeDefined() - expect(stateFn(presenter).tiles[6]).toBeUndefined() + expect(stateFn(presenter).tiles[4]).toBeUndefined() + expect(stateFn(presenter).tiles[6]).toBeDefined() + expect(stateFn(presenter).tiles[8]).toBeUndefined() it "does not remove out-of-view tiles corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 6028c864b..c5372f75f 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -359,8 +359,8 @@ class TextEditorPresenter endRow = @constrainRow(@getEndTileRow() + @tileSize) screenRows = [startRow...endRow] - # if longestScreenRow = @model.getLongestScreenRow() - # screenRows.push(longestScreenRow) + if longestScreenRow = @model.getLongestScreenRow() + screenRows.push(longestScreenRow) if @screenRowsToMeasure? screenRows.push(@screenRowsToMeasure...) From 40e5f264c53c43024d5e89aac77dd604acec9ef2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Sep 2015 11:47:46 +0200 Subject: [PATCH 41/80] Pass LinesYardstick to TextEditorPresenter --- src/text-editor-component.coffee | 4 ++++ src/text-editor-presenter.coffee | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 49b80e1b6..a290239ee 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -12,6 +12,7 @@ ScrollbarComponent = require './scrollbar-component' ScrollbarCornerComponent = require './scrollbar-corner-component' OverlayManager = require './overlay-manager' DOMElementPool = require './dom-element-pool' +LinesYardstick = require './lines-yardstick' module.exports = class TextEditorComponent @@ -85,6 +86,9 @@ class TextEditorComponent @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool}) @scrollViewNode.appendChild(@linesComponent.getDomNode()) + @linesYardstick = new LinesYardstick(@editor, @presenter, @linesComponent) + @presenter.setLinesYardstick(@linesYardstick) + @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll}) @scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode()) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index c5372f75f..0d68bc360 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -41,6 +41,8 @@ class TextEditorPresenter @startReflowing() if @continuousReflow @updating = false + setLinesYardstick: (@linesYardstick) -> + destroy: -> @disposables.dispose() From e6a72b794c8bcb458f4132cec279b19f75e2cc3b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Sep 2015 15:16:49 +0200 Subject: [PATCH 42/80] Temporarily disable caching from yardstick This will enable us to avoid worrying about cache invalidation while integrating `LinesYardstick` into `TextEditorPresenter`. --- src/lines-yardstick.coffee | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 0ec946413..d35e5bfa6 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -5,25 +5,12 @@ AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} module.exports = class LinesYardstick constructor: (@model, @presenter, @lineNodesProvider) -> - @cachedPositionsByLineId = {} @tokenIterator = new TokenIterator @rangeForMeasurement = document.createRange() - clearCache: -> - @cachedPositionsByLineId = {} - prepareScreenRowsForMeasurement: (screenRows) -> @presenter.setScreenRowsToMeasure(screenRows) - @lineNodesProvider.updateSync(@presenter.getStateForMeasurements()) - - cleanup: -> - @presenter.clearScreenRowsToMeasure() - @lineNodesProvider.updateSync(@presenter.getStateForMeasurements()) - - measure: (screenRows, fn) -> - @prepareScreenRowsForMeasurement(screenRows) - fn() - @cleanup() + @lineNodesProvider.updateSync(@presenter.getPreMeasurementState()) pixelPositionForScreenPosition: (screenPosition, clip=true) -> screenPosition = Point.fromObject(screenPosition) @@ -42,9 +29,6 @@ class LinesYardstick tokenizedLine = @model.tokenizedLineForScreenRow(row) return 0 unless tokenizedLine? - if cachedPosition = @cachedPositionsByLineId[tokenizedLine.id]?[column] - return cachedPosition - lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(tokenizedLine.id, row) @@ -91,9 +75,7 @@ class LinesYardstick if textNode? foundIndexWithinTextNode ?= textNode.textContent.length - @cachedPositionsByLineId[tokenizedLine.id] ?= {} - @cachedPositionsByLineId[tokenizedLine.id][column] = - @leftPixelPositionForCharInTextNode(lineNode, textNode, foundIndexWithinTextNode) + @leftPixelPositionForCharInTextNode(lineNode, textNode, foundIndexWithinTextNode) else 0 From 5270a1da1c7431b6a1412558e2cbcc5527e1350f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Sep 2015 15:44:05 +0200 Subject: [PATCH 43/80] Use LinesYardstick in pixelPositionForScreenPosition --- spec/text-editor-component-spec.coffee | 7 ++++- src/text-editor-presenter.coffee | 43 +++++--------------------- 2 files changed, 13 insertions(+), 37 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 43c47281e..3a672a882 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -3024,7 +3024,9 @@ describe "TextEditorComponent", -> expect(cursorLeft).toBe line0Right describe "when lines are changed while the editor is hidden", -> - it "does not measure new characters until the editor is shown again", -> + xit "does not measure new characters until the editor is shown again", -> + # TODO: This spec fails. Check if we need to keep it or not. + editor.setText('') wrapperNode.style.display = 'none' @@ -3633,6 +3635,9 @@ describe "TextEditorComponent", -> event clientCoordinatesForScreenPosition = (screenPosition) -> + # TODO: Remove this line here when `pixelPositionForScreenPosition` will + # handle automatically screen row preparation for measurement. + wrapperNode.component.linesYardstick.prepareScreenRowsForMeasurement() positionOffset = wrapperNode.pixelPositionForScreenPosition(screenPosition) scrollViewClientRect = componentNode.querySelector('.scroll-view').getBoundingClientRect() clientX = scrollViewClientRect.left + positionOffset.left - wrapperNode.getScrollLeft() diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 0d68bc360..800a81f4b 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -114,7 +114,8 @@ class TextEditorPresenter getState: -> @updating = true - @getPreMeasurementState() + @linesYardstick.prepareScreenRowsForMeasurement() + @getPostMeasurementState() @state @@ -1131,41 +1132,11 @@ class TextEditorPresenter @lineHeight? and @baseCharacterWidth? pixelPositionForScreenPosition: (screenPosition, clip=true) -> - screenPosition = Point.fromObject(screenPosition) - screenPosition = @model.clipScreenPosition(screenPosition) if clip - - targetRow = screenPosition.row - targetColumn = screenPosition.column - baseCharacterWidth = @baseCharacterWidth - - top = targetRow * @lineHeight - left = 0 - column = 0 - - iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator() - while iterator.next() - characterWidths = @getScopedCharacterWidths(iterator.getScopes()) - - valueIndex = 0 - text = iterator.getText() - while valueIndex < text.length - if iterator.isPairedCharacter() - char = text - charLength = 2 - valueIndex += 2 - else - char = text[valueIndex] - charLength = 1 - valueIndex++ - - break if column is targetColumn - - left += characterWidths[char] ? baseCharacterWidth unless char is '\0' - column += charLength - - top -= @scrollTop - left -= @scrollLeft - {top, left} + position = + @linesYardstick.pixelPositionForScreenPosition(screenPosition, clip) + position.top -= @getScrollTop() + position.left -= @getScrollLeft() + position hasPixelRectRequirements: -> @hasPixelPositionRequirements() and @scrollWidth? From 5dcfea0a82b59dbcfb241dea1b566dde4d53e589 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Sep 2015 16:11:59 +0200 Subject: [PATCH 44/80] :green_heart: Fix TextEditorPresenter specs --- spec/fake-lines-yardstick.coffee | 58 ++++++++++++++++++++ spec/mock-lines-component.coffee | 36 ------------- spec/text-editor-presenter-spec.coffee | 73 +++++++++----------------- src/text-editor-presenter.coffee | 2 + 4 files changed, 85 insertions(+), 84 deletions(-) create mode 100644 spec/fake-lines-yardstick.coffee delete mode 100644 spec/mock-lines-component.coffee diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee new file mode 100644 index 000000000..821489e61 --- /dev/null +++ b/spec/fake-lines-yardstick.coffee @@ -0,0 +1,58 @@ +{Point} = require 'text-buffer' + +module.exports = +class FakeLinesYardstick + constructor: (@model, @presenter) -> + @characterWidthsByScope = {} + + prepareScreenRowsForMeasurement: -> + @presenter.getPreMeasurementState() + + getScopedCharacterWidth: (scopeNames, char) -> + @getScopedCharacterWidths(scopeNames)[char] + + getScopedCharacterWidths: (scopeNames) -> + scope = @characterWidthsByScope + for scopeName in scopeNames + scope[scopeName] ?= {} + scope = scope[scopeName] + scope.characterWidths ?= {} + scope.characterWidths + + setScopedCharacterWidth: (scopeNames, character, width) -> + @getScopedCharacterWidths(scopeNames)[character] = width + + pixelPositionForScreenPosition: (screenPosition, clip=true) -> + screenPosition = Point.fromObject(screenPosition) + screenPosition = @model.clipScreenPosition(screenPosition) if clip + + targetRow = screenPosition.row + targetColumn = screenPosition.column + baseCharacterWidth = @model.getDefaultCharWidth() + + top = targetRow * @model.getLineHeightInPixels() + left = 0 + column = 0 + + iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator() + while iterator.next() + characterWidths = @getScopedCharacterWidths(iterator.getScopes()) + + valueIndex = 0 + text = iterator.getText() + while valueIndex < text.length + if iterator.isPairedCharacter() + char = text + charLength = 2 + valueIndex += 2 + else + char = text[valueIndex] + charLength = 1 + valueIndex++ + + break if column is targetColumn + + left += characterWidths[char] ? baseCharacterWidth unless char is '\0' + column += charLength + + {top, left} diff --git a/spec/mock-lines-component.coffee b/spec/mock-lines-component.coffee deleted file mode 100644 index 64c37ad4f..000000000 --- a/spec/mock-lines-component.coffee +++ /dev/null @@ -1,36 +0,0 @@ -TokenIterator = require '../src/token-iterator' - -module.exports = -class MockLinesComponent - constructor: (@model) -> - @defaultFont = "" - @fontsByScopes = {} - @tokenIterator = new TokenIterator - @builtLineNodes = [] - - destroy: -> - node.remove() for node in @builtLineNodes - - updateSync: jasmine.createSpy() - - lineNodeForLineIdAndScreenRow: (id, screenRow) -> - lineNode = document.createElement("div") - lineNode.style.whiteSpace = "pre" - lineState = @model.tokenizedLineForScreenRow(screenRow) - - @tokenIterator.reset(lineState) - while @tokenIterator.next() - font = @fontsByScopes[@tokenIterator.getScopes()] or @defaultFont - span = document.createElement("span") - span.style.font = font - span.textContent = @tokenIterator.getText() - lineNode.innerHTML += span.outerHTML - - @builtLineNodes.push(lineNode) - document.body.appendChild(lineNode) - - lineNode - - setFontForScopes: (scopes, font) -> @fontsByScopes[scopes] = font - - setDefaultFont: (font) -> @defaultFont = font diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 1bd0f580c..3f7405539 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -4,6 +4,7 @@ TextBuffer = require 'text-buffer' {Point, Range} = TextBuffer TextEditor = require '../src/text-editor' TextEditorPresenter = require '../src/text-editor-presenter' +FakeLinesYardstick = require './fake-lines-yardstick' describe "TextEditorPresenter", -> # These `describe` and `it` blocks mirror the structure of the ::state object. @@ -40,7 +41,9 @@ describe "TextEditorPresenter", -> scrollTop: 0 scrollLeft: 0 - new TextEditorPresenter(params) + presenter = new TextEditorPresenter(params) + presenter.setLinesYardstick(new FakeLinesYardstick(editor, presenter)) + presenter expectValues = (actual, expected) -> for key, value of expected @@ -290,15 +293,7 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setContentFrameWidth(10 * maxLineLength + 20) expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 20 - it "updates when the ::baseCharacterWidth changes", -> - maxLineLength = editor.getMaxScreenLineLength() - presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) - expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 15 * maxLineLength + 1 - - it "updates when the scoped character widths change", -> + it "updates when character widths change", -> waitsForPromise -> atom.packages.activatePackage('language-javascript') runs -> @@ -306,7 +301,9 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) + expectStateUpdate presenter, -> + presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) + presenter.characterWidthsChanged() expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> @@ -549,7 +546,9 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) expect(presenter.getState().hiddenInput.width).toBe 15 - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + expectStateUpdate presenter, -> + presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + presenter.characterWidthsChanged() expect(presenter.getState().hiddenInput.width).toBe 20 it "is 2px at the end of lines", -> @@ -637,15 +636,7 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setContentFrameWidth(10 * maxLineLength + 20) expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 20 - it "updates when the ::baseCharacterWidth changes", -> - maxLineLength = editor.getMaxScreenLineLength() - presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) - - expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15) - expect(presenter.getState().content.scrollWidth).toBe 15 * maxLineLength + 1 - - it "updates when the scoped character widths change", -> + it "updates when character widths change", -> waitsForPromise -> atom.packages.activatePackage('language-javascript') runs -> @@ -653,7 +644,9 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10) expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1 - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) + expectStateUpdate presenter, -> + presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20) + presenter.characterWidthsChanged() expect(presenter.getState().content.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide it "updates when ::softWrapped changes on the editor", -> @@ -1276,13 +1269,6 @@ describe "TextEditorPresenter", -> expect(stateForCursor(presenter, 3)).toEqual {top: 5, left: 12 * 10, width: 10, height: 5} expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 5 - 20, left: 4 * 10, width: 10, height: 5} - it "updates when ::baseCharacterWidth changes", -> - editor.setCursorBufferPosition([2, 4]) - presenter = buildPresenter(explicitHeight: 20, scrollTop: 20) - - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20) - expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 20, width: 20, height: 10} - it "updates when scoped character widths change", -> waitsForPromise -> atom.packages.activatePackage('language-javascript') @@ -1291,10 +1277,14 @@ describe "TextEditorPresenter", -> editor.setCursorBufferPosition([1, 4]) presenter = buildPresenter(explicitHeight: 20) - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20) + expectStateUpdate presenter, -> + presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20) + presenter.characterWidthsChanged() expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 10, height: 10} - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + expectStateUpdate presenter, -> + presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20) + presenter.characterWidthsChanged() expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 20, height: 10} it "updates when cursors are added, moved, hidden, shown, or destroyed", -> @@ -1610,21 +1600,6 @@ describe "TextEditorPresenter", -> ] } - it "updates when ::baseCharacterWidth changes", -> - editor.setSelectedBufferRanges([ - [[2, 2], [2, 4]], - ]) - - presenter = buildPresenter(explicitHeight: 20, scrollTop: 0, tileSize: 2) - - expectValues stateForSelectionInTile(presenter, 0, 2), { - regions: [{top: 0, left: 2 * 10, width: 2 * 10, height: 10}] - } - expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20) - expectValues stateForSelectionInTile(presenter, 0, 2), { - regions: [{top: 0, left: 2 * 20, width: 2 * 20, height: 10}] - } - it "updates when scoped character widths change", -> waitsForPromise -> atom.packages.activatePackage('language-javascript') @@ -1639,7 +1614,9 @@ describe "TextEditorPresenter", -> expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}] } - expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'i', 20) + expectStateUpdate presenter, -> + presenter.getLinesYardstick().setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'i', 20) + presenter.characterWidthsChanged() expectValues stateForSelectionInTile(presenter, 0, 2), { regions: [{top: 0, left: 4 * 10, width: 20 + 10, height: 10}] } @@ -1792,7 +1769,7 @@ describe "TextEditorPresenter", -> pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10} } - it "updates when ::baseCharacterWidth changes", -> + it "updates when character widths changes", -> scrollTop = 20 marker = editor.markBufferPosition([2, 13], invalidate: 'touch') decoration = editor.decorateMarker(marker, {type: 'overlay', item}) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 800a81f4b..41a8a450c 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -43,6 +43,8 @@ class TextEditorPresenter setLinesYardstick: (@linesYardstick) -> + getLinesYardstick: -> @linesYardstick + destroy: -> @disposables.dispose() From 123594dbbf410b902d2dc065390a069e0b7877cb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Sep 2015 17:13:49 +0200 Subject: [PATCH 45/80] Implement ::screenPositionForPixelPosition in LinesYardstick --- src/lines-yardstick.coffee | 52 ++++++++++++++++++++++++++++++++ src/text-editor-component.coffee | 4 +-- src/text-editor-presenter.coffee | 35 --------------------- 3 files changed, 54 insertions(+), 37 deletions(-) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index d35e5bfa6..aa86f9586 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -12,6 +12,58 @@ class LinesYardstick @presenter.setScreenRowsToMeasure(screenRows) @lineNodesProvider.updateSync(@presenter.getPreMeasurementState()) + screenPositionForPixelPosition: (pixelPosition) -> + targetTop = pixelPosition.top + targetLeft = pixelPosition.left + defaultCharWidth = @model.getDefaultCharWidth() + row = Math.floor(targetTop / @model.getLineHeightInPixels()) + targetLeft = 0 if row < 0 + targetLeft = Infinity if row > @model.getLastScreenRow() + row = Math.min(row, @model.getLastScreenRow()) + row = Math.max(0, row) + + tokenizedLine = @model.tokenizedLineForScreenRow(row) + lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(tokenizedLine.id, row) + + return new Point(row, 0) unless lineNode? and tokenizedLine? + + iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) + charIndex = 0 + + @tokenIterator.reset(tokenizedLine) + while @tokenIterator.next() + text = @tokenIterator.getText() + textIndex = 0 + while textIndex < text.length + if @tokenIterator.isPairedCharacter() + char = text + charLength = 2 + textIndex += 2 + else + char = text[textIndex] + charLength = 1 + textIndex++ + + unless textNode? + textNode = iterator.nextNode() + textNodeLength = textNode.textContent.length + textNodeIndex = 0 + nextTextNodeIndex = textNodeLength + + while nextTextNodeIndex <= charIndex + textNode = iterator.nextNode() + textNodeLength = textNode.textContent.length + textNodeIndex = nextTextNodeIndex + nextTextNodeIndex = textNodeIndex + textNodeLength + + indexWithinTextNode = charIndex - textNodeIndex + left = @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinTextNode) + break if left >= targetLeft + + charIndex += charLength + + new Point(row, charIndex) + pixelPositionForScreenPosition: (screenPosition, clip=true) -> screenPosition = Point.fromObject(screenPosition) screenPosition = @model.clipScreenPosition(screenPosition) if clip diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index a290239ee..8ed17fb1b 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -427,7 +427,7 @@ class TextEditorComponent position screenPositionForPixelPosition: (pixelPosition) -> - @presenter.screenPositionForPixelPosition(pixelPosition) + @linesYardstick.screenPositionForPixelPosition(pixelPosition) pixelRectForScreenRange: (screenRange) -> rect = @presenter.pixelRectForScreenRange(screenRange) @@ -857,7 +857,7 @@ class TextEditorComponent screenPositionForMouseEvent: (event, linesClientRect) -> pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect) - @presenter.screenPositionForPixelPosition(pixelPosition) + @screenPositionForPixelPosition(pixelPosition) pixelPositionForMouseEvent: (event, linesClientRect) -> {clientX, clientY} = event diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 41a8a450c..39b51063c 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1626,38 +1626,3 @@ class TextEditorPresenter getVisibleRowRange: -> [@startRow, @endRow] - - screenPositionForPixelPosition: (pixelPosition) -> - targetTop = pixelPosition.top - targetLeft = pixelPosition.left - defaultCharWidth = @baseCharacterWidth - row = Math.floor(targetTop / @lineHeight) - targetLeft = 0 if row < 0 - targetLeft = Infinity if row > @model.getLastScreenRow() - row = Math.min(row, @model.getLastScreenRow()) - row = Math.max(0, row) - - left = 0 - column = 0 - - iterator = @model.tokenizedLineForScreenRow(row).getTokenIterator() - while iterator.next() - charWidths = @getScopedCharacterWidths(iterator.getScopes()) - value = iterator.getText() - valueIndex = 0 - while valueIndex < value.length - if iterator.isPairedCharacter() - char = value - charLength = 2 - valueIndex += 2 - else - char = value[valueIndex] - charLength = 1 - valueIndex++ - - charWidth = charWidths[char] ? defaultCharWidth - break if targetLeft <= left + (charWidth / 2) - left += charWidth - column += charLength - - new Point(row, column) From 72093da1a7a6513bacff388df4488a28627e9e77 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Sep 2015 17:32:20 +0200 Subject: [PATCH 46/80] :fire: :tada: Remove scoped character width --- src/display-buffer.coffee | 30 ------------ src/lines-component.coffee | 19 -------- src/lines-tile-component.coffee | 82 -------------------------------- src/text-editor-component.coffee | 16 +------ src/text-editor-presenter.coffee | 29 ----------- 5 files changed, 2 insertions(+), 174 deletions(-) diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 27d1b44cf..339fa11c1 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -18,7 +18,6 @@ module.exports = class DisplayBuffer extends Model verticalScrollMargin: 2 horizontalScrollMargin: 6 - scopedCharacterWidthsChangeCount: 0 changeCount: 0 softWrapped: null editorWidthInChars: null @@ -198,35 +197,6 @@ class DisplayBuffer extends Model getCursorWidth: -> 1 - getScopedCharWidth: (scopeNames, char) -> - @getScopedCharWidths(scopeNames)[char] - - getScopedCharWidths: (scopeNames) -> - scope = @charWidthsByScope - for scopeName in scopeNames - scope[scopeName] ?= {} - scope = scope[scopeName] - scope.charWidths ?= {} - scope.charWidths - - batchCharacterMeasurement: (fn) -> - oldChangeCount = @scopedCharacterWidthsChangeCount - @batchingCharacterMeasurement = true - fn() - @batchingCharacterMeasurement = false - @characterWidthsChanged() if oldChangeCount isnt @scopedCharacterWidthsChangeCount - - setScopedCharWidth: (scopeNames, char, width) -> - @getScopedCharWidths(scopeNames)[char] = width - @scopedCharacterWidthsChangeCount++ - @characterWidthsChanged() unless @batchingCharacterMeasurement - - characterWidthsChanged: -> - @emitter.emit 'did-change-character-widths', @scopedCharacterWidthsChangeCount - - clearScopedCharWidths: -> - @charWidthsByScope = {} - scrollToScreenRange: (screenRange, options = {}) -> scrollEvent = {screenRange, options} @emitter.emit "did-request-autoscroll", scrollEvent diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 422729135..3cd9215a6 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -82,25 +82,6 @@ class LinesComponent extends TiledComponent @presenter.setLineHeight(lineHeightInPixels) @presenter.setBaseCharacterWidth(charWidth) - remeasureCharacterWidths: -> - return unless @presenter.baseCharacterWidth - - @clearScopedCharWidths() - @measureCharactersInNewLines() - - measureCharactersInNewLines: -> - @presenter.batchCharacterMeasurement => - for id, component of @componentsByTileId - component.measureCharactersInNewLines() - - return - - clearScopedCharWidths: -> - for id, component of @componentsByTileId - component.clearMeasurements() - - @presenter.clearScopedCharacterWidths() - lineNodeForLineIdAndScreenRow: (lineId, screenRow) -> tile = @presenter.tileForRow(screenRow) @getComponentForTile(tile)?.lineNodeForLineId(lineId) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 3c70c1199..4b18af3bb 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -317,85 +317,3 @@ class LinesTileComponent lineNodeForLineId: (lineId) -> @lineNodesByLineId[lineId] - - measureCharactersInNewLines: -> - for id, lineState of @oldTileState.lines - unless @measuredLines.has(id) - lineNode = @lineNodesByLineId[id] - @measureCharactersInLine(id, lineState, lineNode) - return - - measureCharactersInLine: (lineId, tokenizedLine, lineNode) -> - rangeForMeasurement = null - iterator = null - charIndex = 0 - - @tokenIterator.reset(tokenizedLine) - while @tokenIterator.next() - scopes = @tokenIterator.getScopes() - text = @tokenIterator.getText() - charWidths = @presenter.getScopedCharacterWidths(scopes) - - textIndex = 0 - while textIndex < text.length - if @tokenIterator.isPairedCharacter() - char = text - charLength = 2 - textIndex += 2 - else - char = text[textIndex] - charLength = 1 - textIndex++ - - unless charWidths[char]? - unless textNode? - rangeForMeasurement ?= document.createRange() - iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) - textNode = iterator.nextNode() - textNodeLength = textNode.textContent.length - textNodeIndex = 0 - nextTextNodeIndex = textNodeLength - - while nextTextNodeIndex <= charIndex - textNode = iterator.nextNode() - textNodeLength = textNode.textContent.length - textNodeIndex = nextTextNodeIndex - nextTextNodeIndex = textNodeIndex + textNodeLength - - i = charIndex - textNodeIndex - rangeForMeasurement.setStart(textNode, i) - - if i + charLength <= textNodeLength - rangeForMeasurement.setEnd(textNode, i + charLength) - else - rangeForMeasurement.setEnd(textNode, textNodeLength) - atom.assert false, "Expected index to be less than the length of text node while measuring", (error) => - editor = @presenter.model - screenRow = tokenizedLine.screenRow - bufferRow = editor.bufferRowForScreenRow(screenRow) - - error.metadata = { - grammarScopeName: editor.getGrammar().scopeName - screenRow: screenRow - bufferRow: bufferRow - softWrapped: editor.isSoftWrapped() - softTabs: editor.getSoftTabs() - i: i - charLength: charLength - textNodeLength: textNode.length - } - error.privateMetadataDescription = "The contents of line #{bufferRow + 1}." - error.privateMetadata = { - lineText: editor.lineTextForBufferRow(bufferRow) - } - error.privateMetadataRequestName = "measured-line-text" - - charWidth = rangeForMeasurement.getBoundingClientRect().width - @presenter.setScopedCharacterWidth(scopes, char, charWidth) - - charIndex += charLength - - @measuredLines.add(lineId) - - clearMeasurements: -> - @measuredLines.clear() diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 8ed17fb1b..b9535b2aa 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -30,7 +30,6 @@ class TextEditorComponent inputEnabled: true measureScrollbarsWhenShown: true measureLineHeightAndDefaultCharWidthWhenShown: true - remeasureCharacterWidthsWhenShown: false stylingChangeAnimationFrameRequested: false gutterComponent: null mounted: true @@ -169,7 +168,6 @@ class TextEditorComponent @updateParentViewMiniClass() readAfterUpdateSync: => - @linesComponent.measureCharactersInNewLines() if @isVisible() and not @newState.content.scrollingVertically @overlayManager?.measureOverlays() mountGutterContainerComponent: -> @@ -184,7 +182,6 @@ class TextEditorComponent @measureWindowSize() @measureDimensions() @measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown - @remeasureCharacterWidths() if @remeasureCharacterWidthsWhenShown @editor.setVisible(true) @performedInitialMeasurement = true @updatesPaused = false @@ -563,7 +560,6 @@ class TextEditorComponent handleStylingChange: => @sampleFontStyling() @sampleBackgroundColors() - @remeasureCharacterWidths() handleDragUntilMouseUp: (dragHandler) => dragging = false @@ -712,15 +708,14 @@ class TextEditorComponent oldFontFamily = @fontFamily oldLineHeight = @lineHeight + @presenter.characterWidthsChanged() + {@fontSize, @fontFamily, @lineHeight} = getComputedStyle(@getTopmostDOMNode()) if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight @clearPoolAfterUpdate = true @measureLineHeightAndDefaultCharWidth() - if (@fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily) and @performedInitialMeasurement - @remeasureCharacterWidths() - sampleBackgroundColors: (suppressUpdate) -> {backgroundColor} = getComputedStyle(@hostElement) @@ -738,13 +733,6 @@ class TextEditorComponent else @measureLineHeightAndDefaultCharWidthWhenShown = true - remeasureCharacterWidths: -> - if @isVisible() - @remeasureCharacterWidthsWhenShown = false - @linesComponent.remeasureCharacterWidths() - else - @remeasureCharacterWidthsWhenShown = true - measureScrollbars: -> @measureScrollbarsWhenShown = false diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 39b51063c..f9166ace9 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -9,7 +9,6 @@ class TextEditorPresenter startBlinkingCursorsAfterDelay: null stoppedScrollingTimeoutId: null mouseWheelScreenRow: null - scopedCharacterWidthsChangeCount: 0 overlayDimensions: {} minimumReflowInterval: 200 @@ -1089,30 +1088,6 @@ class TextEditorPresenter @model.setDefaultCharWidth(baseCharacterWidth) @characterWidthsChanged() - getScopedCharacterWidth: (scopeNames, char) -> - @getScopedCharacterWidths(scopeNames)[char] - - getScopedCharacterWidths: (scopeNames) -> - scope = @characterWidthsByScope - for scopeName in scopeNames - scope[scopeName] ?= {} - scope = scope[scopeName] - scope.characterWidths ?= {} - scope.characterWidths - - batchCharacterMeasurement: (fn) -> - oldChangeCount = @scopedCharacterWidthsChangeCount - @batchingCharacterMeasurement = true - @model.batchCharacterMeasurement(fn) - @batchingCharacterMeasurement = false - @characterWidthsChanged() if oldChangeCount isnt @scopedCharacterWidthsChangeCount - - setScopedCharacterWidth: (scopeNames, character, width) -> - @getScopedCharacterWidths(scopeNames)[character] = width - @model.setScopedCharWidth(scopeNames, character, width) - @scopedCharacterWidthsChangeCount++ - @characterWidthsChanged() unless @batchingCharacterMeasurement - characterWidthsChanged: -> @shouldUpdateHorizontalScrollState = true @shouldUpdateVerticalScrollState = true @@ -1126,10 +1101,6 @@ class TextEditorPresenter @emitDidUpdateState() - clearScopedCharacterWidths: -> - @characterWidthsByScope = {} - @model.clearScopedCharWidths() - hasPixelPositionRequirements: -> @lineHeight? and @baseCharacterWidth? From 698a5ac421c58203db50707e703e814bf8ff82f1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Sep 2015 17:37:43 +0200 Subject: [PATCH 47/80] :green_heart: --- src/text-editor-component.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index b9535b2aa..0ed7df25e 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -560,6 +560,7 @@ class TextEditorComponent handleStylingChange: => @sampleFontStyling() @sampleBackgroundColors() + @presenter.characterWidthsChanged() handleDragUntilMouseUp: (dragHandler) => dragging = false @@ -708,8 +709,6 @@ class TextEditorComponent oldFontFamily = @fontFamily oldLineHeight = @lineHeight - @presenter.characterWidthsChanged() - {@fontSize, @fontFamily, @lineHeight} = getComputedStyle(@getTopmostDOMNode()) if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight @@ -824,6 +823,7 @@ class TextEditorComponent setFontSize: (fontSize) -> @getTopmostDOMNode().style.fontSize = fontSize + 'px' @sampleFontStyling() + @presenter.characterWidthsChanged() getFontFamily: -> getComputedStyle(@getTopmostDOMNode()).fontFamily @@ -831,10 +831,12 @@ class TextEditorComponent setFontFamily: (fontFamily) -> @getTopmostDOMNode().style.fontFamily = fontFamily @sampleFontStyling() + @presenter.characterWidthsChanged() setLineHeight: (lineHeight) -> @getTopmostDOMNode().style.lineHeight = lineHeight @sampleFontStyling() + @presenter.characterWidthsChanged() setShowIndentGuide: (showIndentGuide) -> atom.config.set("editor.showIndentGuide", showIndentGuide) From 496120a6848ce214d92dcf17002d93a711074cbe Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Sep 2015 15:33:09 +0200 Subject: [PATCH 48/80] :racehorse: Compute lines state only once --- src/text-editor-presenter.coffee | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 60b213a0f..d14be108d 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -81,7 +81,9 @@ class TextEditorPresenter @updateCommonGutterState() @updateReflowState() - @updateTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState + if @shouldUpdateLinesState or @shouldUpdateLineNumbersState + @updateTilesState() + @shouldUpdateTilesState = true @updating = false @state @@ -101,7 +103,7 @@ class TextEditorPresenter @updateHiddenInputState() if @shouldUpdateHiddenInputState @updateContentState() if @shouldUpdateContentState @updateDecorations() if @shouldUpdateDecorations - @updateTilesState() if @shouldUpdateLinesState or @shouldUpdateLineNumbersState + @updateTilesState() if @shouldUpdateTilesState @updateCursorsState() if @shouldUpdateCursorsState @updateOverlaysState() if @shouldUpdateOverlaysState @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState @@ -132,6 +134,7 @@ class TextEditorPresenter @shouldUpdateContentState = false @shouldUpdateDecorations = false @shouldUpdateLinesState = false + @shouldUpdateTilesState = false @shouldUpdateCursorsState = false @shouldUpdateOverlaysState = false @shouldUpdateLineNumberGutterState = false @@ -149,6 +152,7 @@ class TextEditorPresenter @shouldUpdateContentState = true @shouldUpdateDecorations = true @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateCursorsState = true @shouldUpdateOverlaysState = true @shouldUpdateLineNumberGutterState = true From 0edb69047b9f1646ce6ac90096361b935bb51bd2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Sep 2015 15:39:37 +0200 Subject: [PATCH 49/80] :bug: Move accidentally leftover statement --- src/text-editor-presenter.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index d14be108d..92eadb6db 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -89,6 +89,8 @@ class TextEditorPresenter @state getPostMeasurementState: -> + @updating = true + @updateHorizontalDimensions() @commitPendingLogicalScrollLeftPosition() @commitPendingScrollLeftPosition() @@ -116,8 +118,6 @@ class TextEditorPresenter # Public: Gets this presenter's state, updating it just in time before returning from this function. # Returns a state {Object}, useful for rendering to screen. getState: -> - @updating = true - @linesYardstick.prepareScreenRowsForMeasurement() @getPostMeasurementState() From 59d6974f897f847e3b0bd1ecf980a6210527cf85 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Sep 2015 16:03:31 +0200 Subject: [PATCH 50/80] :racehorse: Remove AcceptFilter ...so that Chrome doesn't need to switch to user space when iterating over line nodes. --- src/lines-yardstick.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index aa86f9586..b445ab643 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -1,5 +1,4 @@ TokenIterator = require './token-iterator' -AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} {Point} = require 'text-buffer' module.exports = @@ -27,7 +26,7 @@ class LinesYardstick return new Point(row, 0) unless lineNode? and tokenizedLine? - iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) + iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT) charIndex = 0 @tokenIterator.reset(tokenizedLine) @@ -87,7 +86,7 @@ class LinesYardstick return 0 unless lineNode? indexWithinTextNode = null - iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) + iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT) charIndex = 0 @tokenIterator.reset(tokenizedLine) From bb709f58d91ce08133ed963be7283eb79c7465c8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Sep 2015 16:15:49 +0200 Subject: [PATCH 51/80] :racehorse: Cache pixel positions --- src/lines-yardstick.coffee | 31 ++++++++++++++++++++----------- src/text-editor-component.coffee | 10 +++++++--- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index b445ab643..07fff489a 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -6,6 +6,10 @@ class LinesYardstick constructor: (@model, @presenter, @lineNodesProvider) -> @tokenIterator = new TokenIterator @rangeForMeasurement = document.createRange() + @invalidateCache() + + invalidateCache: -> + @pixelPositionsByLineIdAndColumn = {} prepareScreenRowsForMeasurement: (screenRows) -> @presenter.setScreenRowsToMeasure(screenRows) @@ -21,15 +25,15 @@ class LinesYardstick row = Math.min(row, @model.getLastScreenRow()) row = Math.max(0, row) - tokenizedLine = @model.tokenizedLineForScreenRow(row) - lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(tokenizedLine.id, row) + line = @model.tokenizedLineForScreenRow(row) + lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row) - return new Point(row, 0) unless lineNode? and tokenizedLine? + return new Point(row, 0) unless lineNode? and line? iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT) charIndex = 0 - @tokenIterator.reset(tokenizedLine) + @tokenIterator.reset(line) while @tokenIterator.next() text = @tokenIterator.getText() textIndex = 0 @@ -77,19 +81,19 @@ class LinesYardstick {top, left} leftPixelPositionForScreenPosition: (row, column) -> - tokenizedLine = @model.tokenizedLineForScreenRow(row) - return 0 unless tokenizedLine? + line = @model.tokenizedLineForScreenRow(row) + lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row) - lineNode = - @lineNodesProvider.lineNodeForLineIdAndScreenRow(tokenizedLine.id, row) + return 0 unless line? and lineNode? - return 0 unless lineNode? + if cachedPosition = @pixelPositionsByLineIdAndColumn[line.id]?[column] + return cachedPosition indexWithinTextNode = null iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT) charIndex = 0 - @tokenIterator.reset(tokenizedLine) + @tokenIterator.reset(line) while @tokenIterator.next() break if foundIndexWithinTextNode? @@ -126,7 +130,12 @@ class LinesYardstick if textNode? foundIndexWithinTextNode ?= textNode.textContent.length - @leftPixelPositionForCharInTextNode(lineNode, textNode, foundIndexWithinTextNode) + position = @leftPixelPositionForCharInTextNode( + lineNode, textNode, foundIndexWithinTextNode + ) + @pixelPositionsByLineIdAndColumn[line.id] ?= {} + @pixelPositionsByLineIdAndColumn[line.id][column] = position + position else 0 diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 0ed7df25e..235ab4a0e 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -560,7 +560,7 @@ class TextEditorComponent handleStylingChange: => @sampleFontStyling() @sampleBackgroundColors() - @presenter.characterWidthsChanged() + @invalidateCharacterWidths() handleDragUntilMouseUp: (dragHandler) => dragging = false @@ -823,7 +823,7 @@ class TextEditorComponent setFontSize: (fontSize) -> @getTopmostDOMNode().style.fontSize = fontSize + 'px' @sampleFontStyling() - @presenter.characterWidthsChanged() + @invalidateCharacterWidths() getFontFamily: -> getComputedStyle(@getTopmostDOMNode()).fontFamily @@ -831,11 +831,15 @@ class TextEditorComponent setFontFamily: (fontFamily) -> @getTopmostDOMNode().style.fontFamily = fontFamily @sampleFontStyling() - @presenter.characterWidthsChanged() + @invalidateCharacterWidths() setLineHeight: (lineHeight) -> @getTopmostDOMNode().style.lineHeight = lineHeight @sampleFontStyling() + @invalidateCharacterWidths() + + invalidateCharacterWidths: -> + @linesYardstick.invalidateCache() @presenter.characterWidthsChanged() setShowIndentGuide: (showIndentGuide) -> From bca3be32e6b5ba8a8f3b2600533bf4f9b092b974 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Sep 2015 16:23:49 +0200 Subject: [PATCH 52/80] Avoid to call ::prepareScreenRows explicitly --- spec/text-editor-component-spec.coffee | 3 --- src/lines-yardstick.coffee | 7 ++++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 3a672a882..caddb0ee5 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -3635,9 +3635,6 @@ describe "TextEditorComponent", -> event clientCoordinatesForScreenPosition = (screenPosition) -> - # TODO: Remove this line here when `pixelPositionForScreenPosition` will - # handle automatically screen row preparation for measurement. - wrapperNode.component.linesYardstick.prepareScreenRowsForMeasurement() positionOffset = wrapperNode.pixelPositionForScreenPosition(screenPosition) scrollViewClientRect = componentNode.querySelector('.scroll-view').getBoundingClientRect() clientX = scrollViewClientRect.left + positionOffset.left - wrapperNode.getScrollLeft() diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 07fff489a..200f204d8 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -12,6 +12,8 @@ class LinesYardstick @pixelPositionsByLineIdAndColumn = {} prepareScreenRowsForMeasurement: (screenRows) -> + return unless @presenter.isBatching() + @presenter.setScreenRowsToMeasure(screenRows) @lineNodesProvider.updateSync(@presenter.getPreMeasurementState()) @@ -25,6 +27,8 @@ class LinesYardstick row = Math.min(row, @model.getLastScreenRow()) row = Math.max(0, row) + @prepareScreenRowsForMeasurement([row]) + line = @model.tokenizedLineForScreenRow(row) lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row) @@ -73,7 +77,8 @@ class LinesYardstick targetRow = screenPosition.row targetColumn = screenPosition.column - baseCharacterWidth = @baseCharacterWidth + + @prepareScreenRowsForMeasurement([targetRow]) top = targetRow * @model.getLineHeightInPixels() left = @leftPixelPositionForScreenPosition(targetRow, targetColumn) From 2b6973d4b1cd25e6fdf82b0c32e7fb2616b8d7dc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Sep 2015 17:44:26 +0200 Subject: [PATCH 53/80] Fix a :bug: where some invalid lines were being measured --- spec/text-editor-presenter-spec.coffee | 24 ++++++++++++++++++++++++ src/text-editor-presenter.coffee | 15 ++++----------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index edec7eff2..88b26d62d 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -102,6 +102,30 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[12]).toBeUndefined() + it "includes state for tiles containing screen rows to measure", -> + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) + presenter.setScreenRowsToMeasure([10, 12]) + + expect(stateFn(presenter).tiles[0]).toBeDefined() + expect(stateFn(presenter).tiles[2]).toBeDefined() + expect(stateFn(presenter).tiles[4]).toBeDefined() + expect(stateFn(presenter).tiles[6]).toBeDefined() + expect(stateFn(presenter).tiles[8]).toBeUndefined() + expect(stateFn(presenter).tiles[10]).toBeDefined() + expect(stateFn(presenter).tiles[12]).toBeDefined() + + it "excludes invalid tiles for screen rows to measure", -> + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) + presenter.setScreenRowsToMeasure([20, 30]) # unexisting rows + + expect(stateFn(presenter).tiles[0]).toBeDefined() + expect(stateFn(presenter).tiles[2]).toBeDefined() + expect(stateFn(presenter).tiles[4]).toBeDefined() + expect(stateFn(presenter).tiles[6]).toBeDefined() + expect(stateFn(presenter).tiles[8]).toBeUndefined() + expect(stateFn(presenter).tiles[10]).toBeUndefined() + expect(stateFn(presenter).tiles[12]).toBeUndefined() + it "includes state for all tiles if no external ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null, tileSize: 2) expect(stateFn(presenter).tiles[0]).toBeDefined() diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 92eadb6db..3d7298bcf 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -30,6 +30,7 @@ class TextEditorPresenter @lineDecorationsByScreenRow = {} @lineNumberDecorationsByScreenRow = {} @customGutterDecorationsByGutterNameAndScreenRow = {} + @screenRowsToMeasure = [] @transferMeasurementsToModel() @transferMeasurementsFromModel() @observeModel() @@ -370,8 +371,8 @@ class TextEditorPresenter screenRows = [startRow...endRow] if longestScreenRow = @model.getLongestScreenRow() screenRows.push(longestScreenRow) - if @screenRowsToMeasure? - screenRows.push(@screenRowsToMeasure...) + for row in @screenRowsToMeasure when @constrainRow(row) is row + screenRows.push(row) screenRows.sort (a, b) -> a - b _.uniq(screenRows, true) @@ -384,14 +385,6 @@ class TextEditorPresenter @shouldUpdateLineNumbersState = true @shouldUpdateDecorations = true - clearScreenRowsToMeasure: -> - return if not screenRows? or screenRows.length is 0 - - @screenRowsToMeasure = [] - @shouldUpdateLinesState = true - @shouldUpdateLineNumbersState = true - @shouldUpdateDecorations = true - updateTilesState: -> return unless @startRow? and @endRow? and @lineHeight? @@ -403,7 +396,7 @@ class TextEditorPresenter zIndex = 0 for tileStartRow in [@tileForRow(endRow)..@tileForRow(startRow)] by -@tileSize - tileEndRow = Math.min(@model.getScreenLineCount(), tileStartRow + @tileSize) + tileEndRow = @constrainRow(tileStartRow + @tileSize) rowsWithinTile = [] while screenRowIndex >= 0 From 58219a243e33950278af554f006b13ba759b6698 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 30 Sep 2015 18:20:48 +0200 Subject: [PATCH 54/80] Slightly more precise conversion to screen positions --- src/lines-yardstick.coffee | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 200f204d8..ba0f66ae2 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -35,7 +35,9 @@ class LinesYardstick return new Point(row, 0) unless lineNode? and line? iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT) - charIndex = 0 + column = 0 + previousColumn = 0 + previousDistance = Infinity @tokenIterator.reset(line) while @tokenIterator.next() @@ -57,19 +59,23 @@ class LinesYardstick textNodeIndex = 0 nextTextNodeIndex = textNodeLength - while nextTextNodeIndex <= charIndex + while nextTextNodeIndex <= column textNode = iterator.nextNode() textNodeLength = textNode.textContent.length textNodeIndex = nextTextNodeIndex nextTextNodeIndex = textNodeIndex + textNodeLength - indexWithinTextNode = charIndex - textNodeIndex + indexWithinTextNode = column - textNodeIndex left = @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinTextNode) - break if left >= targetLeft + distance = Math.abs(targetLeft - left) - charIndex += charLength + return new Point(row, previousColumn) if distance > previousDistance - new Point(row, charIndex) + previousDistance = distance + previousColumn = column + column += charLength + + new Point(row, column) pixelPositionForScreenPosition: (screenPosition, clip=true) -> screenPosition = Point.fromObject(screenPosition) From 243dea1a1cc55e2b4cae447e217689bc43bda529 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 1 Oct 2015 11:03:25 +0200 Subject: [PATCH 55/80] :art: Move pixel-related code to LinesYardstick * :fire: Remove useless 'scoped char width' methods --- spec/fake-lines-yardstick.coffee | 15 +++++++++++++++ src/lines-yardstick.coffee | 15 +++++++++++++++ src/text-editor-component.coffee | 12 ++---------- src/text-editor-presenter.coffee | 15 ++++----------- src/text-editor.coffee | 9 --------- 5 files changed, 36 insertions(+), 30 deletions(-) diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee index 821489e61..9934b1917 100644 --- a/spec/fake-lines-yardstick.coffee +++ b/spec/fake-lines-yardstick.coffee @@ -56,3 +56,18 @@ class FakeLinesYardstick column += charLength {top, left} + + pixelRectForScreenRange: (screenRange) -> + lineHeight = @model.getLineHeightInPixels() + + if screenRange.end.row > screenRange.start.row + top = @pixelPositionForScreenPosition(screenRange.start).top + left = 0 + height = (screenRange.end.row - screenRange.start.row + 1) * lineHeight + width = @presenter.getScrollWidth() + else + {top, left} = @pixelPositionForScreenPosition(screenRange.start, false) + height = lineHeight + width = @pixelPositionForScreenPosition(screenRange.end, false).left - left + + {top, left, width, height} diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index ba0f66ae2..2925c652d 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -165,3 +165,18 @@ class LinesYardstick @rangeForMeasurement.getBoundingClientRect().left position - lineNode.getBoundingClientRect().left + + pixelRectForScreenRange: (screenRange) -> + lineHeight = @model.getLineHeightInPixels() + + if screenRange.end.row > screenRange.start.row + top = @pixelPositionForScreenPosition(screenRange.start).top + left = 0 + height = (screenRange.end.row - screenRange.start.row + 1) * lineHeight + width = @presenter.getScrollWidth() + else + {top, left} = @pixelPositionForScreenPosition(screenRange.start, false) + height = lineHeight + width = @pixelPositionForScreenPosition(screenRange.end, false).left - left + + {top, left, width, height} diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 235ab4a0e..e684805ec 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -418,21 +418,13 @@ class TextEditorComponent @presenter.getVisibleRowRange() pixelPositionForScreenPosition: (screenPosition) -> - position = @presenter.pixelPositionForScreenPosition(screenPosition) - position.top += @presenter.getScrollTop() - position.left += @presenter.getScrollLeft() - position + @linesYardstick.pixelPositionForScreenPosition(screenPosition) screenPositionForPixelPosition: (pixelPosition) -> @linesYardstick.screenPositionForPixelPosition(pixelPosition) pixelRectForScreenRange: (screenRange) -> - rect = @presenter.pixelRectForScreenRange(screenRange) - rect.top += @presenter.getScrollTop() - rect.bottom += @presenter.getScrollTop() - rect.left += @presenter.getScrollLeft() - rect.right += @presenter.getScrollLeft() - rect + @linesYardstick.pixelRectForScreenRange(screenRange) pixelRangeForScreenRange: (screenRange, clip=true) -> {start, end} = Range.fromObject(screenRange) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 3d7298bcf..6d3cee2f1 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1133,17 +1133,10 @@ class TextEditorPresenter @hasPixelRectRequirements() and @boundingClientRect? and @windowWidth and @windowHeight pixelRectForScreenRange: (screenRange) -> - if screenRange.end.row > screenRange.start.row - top = @pixelPositionForScreenPosition(screenRange.start).top - left = 0 - height = (screenRange.end.row - screenRange.start.row + 1) * @lineHeight - width = @scrollWidth - else - {top, left} = @pixelPositionForScreenPosition(screenRange.start, false) - height = @lineHeight - width = @pixelPositionForScreenPosition(screenRange.end, false).left - left - - {top, left, width, height} + rect = @linesYardstick.pixelRectForScreenRange(screenRange) + rect.top -= @getScrollTop() + rect.left -= @getScrollLeft() + rect observeDecoration: (decoration) -> decorationDisposables = new CompositeDisposable diff --git a/src/text-editor.coffee b/src/text-editor.coffee index eb47484df..f2763f4e2 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -2966,15 +2966,6 @@ class TextEditor extends Model getLineHeightInPixels: -> @displayBuffer.getLineHeightInPixels() setLineHeightInPixels: (lineHeightInPixels) -> @displayBuffer.setLineHeightInPixels(lineHeightInPixels) - batchCharacterMeasurement: (fn) -> @displayBuffer.batchCharacterMeasurement(fn) - - getScopedCharWidth: (scopeNames, char) -> @displayBuffer.getScopedCharWidth(scopeNames, char) - setScopedCharWidth: (scopeNames, char, width) -> @displayBuffer.setScopedCharWidth(scopeNames, char, width) - - getScopedCharWidths: (scopeNames) -> @displayBuffer.getScopedCharWidths(scopeNames) - - clearScopedCharWidths: -> @displayBuffer.clearScopedCharWidths() - getDefaultCharWidth: -> @displayBuffer.getDefaultCharWidth() setDefaultCharWidth: (defaultCharWidth) -> @displayBuffer.setDefaultCharWidth(defaultCharWidth) From 116f92d8161d8cea386f5fee1ef70127f3cf2e9a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 1 Oct 2015 11:15:55 +0200 Subject: [PATCH 56/80] :racehorse: Mark soft-wrapped lines in TokenizedLine --- spec/display-buffer-spec.coffee | 10 ++++++++++ src/text-editor-presenter.coffee | 17 ++++++----------- src/tokenized-line.coffee | 3 +++ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 7ed97f0d0..c0c7b608a 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -143,6 +143,16 @@ describe "DisplayBuffer", -> expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[1].isHardTab).toBeTruthy() describe "when a line is wrapped", -> + it "marks it as soft-wrapped", -> + displayBuffer.setEditorWidthInChars(7) + + expect(displayBuffer.tokenizedLineForScreenRow(0).softWrapped).toBeFalsy() + expect(displayBuffer.tokenizedLineForScreenRow(1).softWrapped).toBeTruthy() + expect(displayBuffer.tokenizedLineForScreenRow(2).softWrapped).toBeTruthy() + expect(displayBuffer.tokenizedLineForScreenRow(3).softWrapped).toBeTruthy() + expect(displayBuffer.tokenizedLineForScreenRow(4).softWrapped).toBeTruthy() + expect(displayBuffer.tokenizedLineForScreenRow(5).softWrapped).toBeFalsy() + it "breaks soft-wrap indentation into a token for each indentation level to support indent guides", -> tokenizedLine = displayBuffer.tokenizedLineForScreenRow(4) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 6d3cee2f1..64e73b770 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -641,27 +641,22 @@ class TextEditorPresenter isVisible = isVisible and @showLineNumbers isVisible - isSoftWrappedRow: (bufferRow, screenRow) -> - return false if screenRow is 0 - - @model.bufferRowForScreenRow(screenRow - 1) is bufferRow - updateLineNumbersState: (tileState, screenRows) -> tileState.lineNumbers ?= {} visibleLineNumberIds = {} for screenRow in screenRows + line = @model.tokenizedLineForScreenRow(screenRow) bufferRow = @model.bufferRowForScreenRow(screenRow) - softWrapped = @isSoftWrappedRow(bufferRow, screenRow) + softWrapped = line.softWrapped decorationClasses = @lineNumberDecorationClassesForRow(screenRow) foldable = @model.isFoldableAtScreenRow(screenRow) - id = @model.tokenizedLineForScreenRow(screenRow).id - tileState.lineNumbers[id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable} - visibleLineNumberIds[id] = true + tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable} + visibleLineNumberIds[line.id] = true - for id of tileState.lineNumbers - delete tileState.lineNumbers[id] unless visibleLineNumberIds[id] + for lineId of tileState.lineNumbers + delete tileState.lineNumbers[lineId] unless visibleLineNumberIds[lineId] return diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index bf234b6d3..320703a9b 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -34,6 +34,7 @@ class TokenizedLine lineIsWhitespaceOnly: false firstNonWhitespaceIndex: 0 foldable: false + softWrapped: false constructor: (properties) -> @id = idCounter++ @@ -420,6 +421,7 @@ class TokenizedLine leftFragment.tabLength = @tabLength leftFragment.firstNonWhitespaceIndex = Math.min(column, @firstNonWhitespaceIndex) leftFragment.firstTrailingWhitespaceIndex = Math.min(column, @firstTrailingWhitespaceIndex) + leftFragment.softWrapped = @softWrapped rightFragment = new TokenizedLine rightFragment.tokenIterator = @tokenIterator @@ -437,6 +439,7 @@ class TokenizedLine rightFragment.endOfLineInvisibles = @endOfLineInvisibles rightFragment.firstNonWhitespaceIndex = Math.max(softWrapIndent, @firstNonWhitespaceIndex - column + softWrapIndent) rightFragment.firstTrailingWhitespaceIndex = Math.max(softWrapIndent, @firstTrailingWhitespaceIndex - column + softWrapIndent) + rightFragment.softWrapped = true [leftFragment, rightFragment] From e94ff33d831d6318adc77fb5106b40f6f58f5d05 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 1 Oct 2015 12:58:02 +0200 Subject: [PATCH 57/80] :bug: Exclude invalid tiles --- spec/text-editor-presenter-spec.coffee | 5 +++++ src/text-editor-presenter.coffee | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 88b26d62d..2c8b173e3 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -126,6 +126,11 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[10]).toBeUndefined() expect(stateFn(presenter).tiles[12]).toBeUndefined() + presenter.setScreenRowsToMeasure([12]) + buffer.deleteRows(12, 13) + + expect(stateFn(presenter).tiles[12]).toBeUndefined() + it "includes state for all tiles if no external ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null, tileSize: 2) expect(stateFn(presenter).tiles[0]).toBeDefined() diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 64e73b770..7a63c4c85 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -371,9 +371,10 @@ class TextEditorPresenter screenRows = [startRow...endRow] if longestScreenRow = @model.getLongestScreenRow() screenRows.push(longestScreenRow) - for row in @screenRowsToMeasure when @constrainRow(row) is row - screenRows.push(row) + if @screenRows? + screenRows.push(@screenRows...) + screenRows = screenRows.filter (row) => @constrainRow(row) is row screenRows.sort (a, b) -> a - b _.uniq(screenRows, true) From b6a0db916f257c99679b776768f07d6fe9504826 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 1 Oct 2015 13:05:50 +0200 Subject: [PATCH 58/80] :bug: Fix wrong pixel to screen position conversion --- src/lines-yardstick.coffee | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 2925c652d..94a08e81d 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -37,7 +37,7 @@ class LinesYardstick iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT) column = 0 previousColumn = 0 - previousDistance = Infinity + previousLeft = 0 @tokenIterator.reset(line) while @tokenIterator.next() @@ -67,15 +67,18 @@ class LinesYardstick indexWithinTextNode = column - textNodeIndex left = @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinTextNode) - distance = Math.abs(targetLeft - left) + charWidth = left - previousLeft - return new Point(row, previousColumn) if distance > previousDistance + return new Point(row, previousColumn) if targetLeft <= previousLeft + (charWidth / 2) - previousDistance = distance + previousLeft = left previousColumn = column column += charLength - new Point(row, column) + if targetLeft <= previousLeft + (charWidth / 2) + new Point(row, previousColumn) + else + new Point(row, column) pixelPositionForScreenPosition: (screenPosition, clip=true) -> screenPosition = Point.fromObject(screenPosition) From b16d2a59f22afd237ea92e824bb4fbc98e911725 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 1 Oct 2015 13:58:13 +0200 Subject: [PATCH 59/80] :green_heart: --- src/text-editor-presenter.coffee | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 7a63c4c85..75fd7f9a0 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -364,6 +364,9 @@ class TextEditorPresenter getEndTileRow: -> @constrainRow(@tileForRow(@endRow)) + isValidScreenRow: (screenRow) -> + screenRow >= 0 and screenRow < @model.getScreenLineCount() + getScreenRows: -> startRow = @getStartTileRow() endRow = @constrainRow(@getEndTileRow() + @tileSize) @@ -371,10 +374,10 @@ class TextEditorPresenter screenRows = [startRow...endRow] if longestScreenRow = @model.getLongestScreenRow() screenRows.push(longestScreenRow) - if @screenRows? - screenRows.push(@screenRows...) + if @screenRowsToMeasure? + screenRows.push(@screenRowsToMeasure...) - screenRows = screenRows.filter (row) => @constrainRow(row) is row + screenRows = screenRows.filter @isValidScreenRow.bind(this) screenRows.sort (a, b) -> a - b _.uniq(screenRows, true) @@ -397,7 +400,6 @@ class TextEditorPresenter zIndex = 0 for tileStartRow in [@tileForRow(endRow)..@tileForRow(startRow)] by -@tileSize - tileEndRow = @constrainRow(tileStartRow + @tileSize) rowsWithinTile = [] while screenRowIndex >= 0 From 63ce6cae030bac43ee919eeefb1fddc8043f683e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 1 Oct 2015 14:20:53 +0200 Subject: [PATCH 60/80] :bug: Fix an issue where yardstick cache wasn't being emptied --- src/text-editor-component.coffee | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index e684805ec..170c90bbf 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -269,9 +269,15 @@ class TextEditorComponent timeoutId = setTimeout(writeSelectedTextToSelectionClipboard) observeConfig: -> - @disposables.add atom.config.onDidChange 'editor.fontSize', @sampleFontStyling - @disposables.add atom.config.onDidChange 'editor.fontFamily', @sampleFontStyling - @disposables.add atom.config.onDidChange 'editor.lineHeight', @sampleFontStyling + @disposables.add atom.config.onDidChange 'editor.fontSize', => + @sampleFontStyling() + @invalidateCharacterWidths() + @disposables.add atom.config.onDidChange 'editor.fontFamily', => + @sampleFontStyling() + @invalidateCharacterWidths() + @disposables.add atom.config.onDidChange 'editor.lineHeight', => + @sampleFontStyling() + @invalidateCharacterWidths() onGrammarChanged: => if @scopedConfigDisposables? @@ -706,6 +712,7 @@ class TextEditorComponent if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight @clearPoolAfterUpdate = true @measureLineHeightAndDefaultCharWidth() + @invalidateCharacterWidths() sampleBackgroundColors: (suppressUpdate) -> {backgroundColor} = getComputedStyle(@hostElement) From e1c3d2ef571139f8bdf03ef509d91f3702d63680 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 1 Oct 2015 17:42:44 +0200 Subject: [PATCH 61/80] :racehorse: Avoid recomputing state for lines twice --- src/text-editor-presenter.coffee | 40 +++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 75fd7f9a0..e6707b7fe 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -82,8 +82,14 @@ class TextEditorPresenter @updateCommonGutterState() @updateReflowState() + if @shouldUpdateDecorations + @fetchDecorations() + @updateLineDecorations() + if @shouldUpdateLinesState or @shouldUpdateLineNumbersState @updateTilesState() + @shouldUpdateLinesState = false + @shouldUpdateLineNumbersState = false @shouldUpdateTilesState = true @updating = false @@ -105,7 +111,7 @@ class TextEditorPresenter @updateScrollbarsState() if @shouldUpdateScrollbarsState @updateHiddenInputState() if @shouldUpdateHiddenInputState @updateContentState() if @shouldUpdateContentState - @updateDecorations() if @shouldUpdateDecorations + @updateHighlightDecorations() if @shouldUpdateDecorations @updateTilesState() if @shouldUpdateTilesState @updateCursorsState() if @shouldUpdateCursorsState @updateOverlaysState() if @shouldUpdateOverlaysState @@ -1195,22 +1201,34 @@ class TextEditorPresenter @emitDidUpdateState() - updateDecorations: -> - @rangesByDecorationId = {} - @lineDecorationsByScreenRow = {} - @lineNumberDecorationsByScreenRow = {} - @customGutterDecorationsByGutterNameAndScreenRow = {} - @visibleHighlights = {} + fetchDecorations: -> + @decorations = [] return unless 0 <= @startRow <= @endRow <= Infinity for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1) range = @model.getMarker(markerId).getScreenRange() for decoration in decorations - if decoration.isType('line') or decoration.isType('gutter') - @addToLineDecorationCaches(decoration, range) - else if decoration.isType('highlight') - @updateHighlightState(decoration, range) + @decorations.push({decoration, range}) + + updateLineDecorations: -> + @rangesByDecorationId = {} + @lineDecorationsByScreenRow = {} + @lineNumberDecorationsByScreenRow = {} + @customGutterDecorationsByGutterNameAndScreenRow = {} + + for {decoration, range} in @decorations + if decoration.isType('line') or decoration.isType('gutter') + @addToLineDecorationCaches(decoration, range) + + return + + updateHighlightDecorations: -> + @visibleHighlights = {} + + for {decoration, range} in @decorations + if decoration.isType('highlight') + @updateHighlightState(decoration, range) for tileId, tileState of @state.content.tiles for id, highlight of tileState.highlights From 33523751eace74612db968b06f4143b7325899bb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 1 Oct 2015 18:34:41 +0200 Subject: [PATCH 62/80] :white_check_mark: Thoroughly test LinesYardstick interactions --- spec/lines-yardstick-spec.coffee | 136 +++++++++++++++++++++++++ spec/text-editor-component-spec.coffee | 13 --- 2 files changed, 136 insertions(+), 13 deletions(-) create mode 100644 spec/lines-yardstick-spec.coffee diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee new file mode 100644 index 000000000..68af04da4 --- /dev/null +++ b/spec/lines-yardstick-spec.coffee @@ -0,0 +1,136 @@ +LinesYardstick = require "../src/lines-yardstick" + +describe "LinesYardstick", -> + [editor, mockPresenter, mockLineNodesProvider, createdLineNodes, linesYardstick] = [] + + beforeEach -> + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + + waitsForPromise -> + atom.project.open('sample.js').then (o) -> editor = o + + runs -> + createdLineNodes = [] + availableScreenRows = {} + screenRowsToMeasure = [] + + buildLineNode = (screenRow) -> + tokenizedLine = editor.tokenizedLineForScreenRow(screenRow) + iterator = tokenizedLine.getTokenIterator() + lineNode = document.createElement("div") + lineNode.style.whiteSpace = "pre" + while iterator.next() + span = document.createElement("span") + span.className = iterator.getScopes().join(' ').replace(/\.+/g, ' ') + span.textContent = iterator.getText() + lineNode.appendChild(span) + + jasmine.attachToDOM(lineNode) + createdLineNodes.push(lineNode) + lineNode + + mockPresenter = + isBatching: -> true + setScreenRowsToMeasure: (screenRows) -> screenRowsToMeasure = screenRows + getPreMeasurementState: -> + state = {} + for screenRow in screenRowsToMeasure + tokenizedLine = editor.tokenizedLineForScreenRow(screenRow) + state[tokenizedLine.id] = screenRow + state + + mockLineNodesProvider = + updateSync: (state) -> availableScreenRows = state + lineNodeForLineIdAndScreenRow: (lineId, screenRow) -> + if availableScreenRows[lineId] isnt screenRow + throw new Error("No line node found!") + + buildLineNode(screenRow) + + editor.setLineHeightInPixels(14) + linesYardstick = new LinesYardstick(editor, mockPresenter, mockLineNodesProvider) + + afterEach -> + lineNode.remove() for lineNode in createdLineNodes + atom.themes.removeStylesheet('test') + + describe "::pixelPositionForScreenPosition(screenPosition)", -> + it "converts screen positions to pixel positions", -> + atom.styles.addStyleSheet """ + * { + font-size: 12px; + font-family: monospace; + } + .function { + font-size: 16px + } + """ + + expect(linesYardstick.pixelPositionForScreenPosition([0, 0])).toEqual({left: 0, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition([0, 1])).toEqual({left: 7, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition([0, 5])).toEqual({left: 38, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition([1, 6])).toEqual({left: 42, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition([1, 9])).toEqual({left: 72, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition([2, Infinity])).toEqual({left: 280, top: 28}) + + it "reuses already computed pixel positions unless it is invalidated", -> + atom.styles.addStyleSheet """ + * { + font-size: 16px; + font-family: monospace; + } + """ + + expect(linesYardstick.pixelPositionForScreenPosition([1, 2])).toEqual({left: 20, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 60, top: 28}) + expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 100, top: 70}) + + atom.styles.addStyleSheet """ + * { + font-size: 20px; + } + """ + + expect(linesYardstick.pixelPositionForScreenPosition([1, 2])).toEqual({left: 20, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 60, top: 28}) + expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 100, top: 70}) + + linesYardstick.invalidateCache() + + expect(linesYardstick.pixelPositionForScreenPosition([1, 2])).toEqual({left: 24, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 72, top: 28}) + expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 120, top: 70}) + + describe "::screenPositionForPixelPosition(pixelPosition)", -> + it "converts pixel positions to screen positions", -> + atom.styles.addStyleSheet """ + * { + font-size: 12px; + font-family: monospace; + } + .function { + font-size: 16px + } + """ + + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 12.5})).toEqual([0, 2]) + expect(linesYardstick.screenPositionForPixelPosition({top: 14, left: 17.8})).toEqual([1, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 28, left: 100})).toEqual([2, 14]) + expect(linesYardstick.screenPositionForPixelPosition({top: 32, left: 24.3})).toEqual([2, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 46, left: 66.5})).toEqual([3, 9]) + expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 99.9})).toEqual([5, 14]) + expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 221.5})).toEqual([5, 29]) + expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 222})).toEqual([5, 30]) + + it "clips pixel positions above buffer start", -> + expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0] + expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: Infinity)).toEqual [0, 0] + expect(linesYardstick.screenPositionForPixelPosition(top: -1, left: Infinity)).toEqual [0, 0] + expect(linesYardstick.screenPositionForPixelPosition(top: 0, left: Infinity)).toEqual [0, 29] + + it "clips pixel positions below buffer end", -> + expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: -Infinity)).toEqual [12, 2] + expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2] + expect(linesYardstick.screenPositionForPixelPosition(top: (editor.getLastScreenRow() + 1) * 14, left: 0)).toEqual [12, 2] + expect(linesYardstick.screenPositionForPixelPosition(top: editor.getLastScreenRow() * 14, left: 0)).toEqual [12, 0] diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index caddb0ee5..6f53ee4e2 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -3558,19 +3558,6 @@ describe "TextEditorComponent", -> nextAnimationFrame() expect(wrapperNode.getScrollTop()).toBe 0 - describe "::screenPositionForPixelPosition(pixelPosition)", -> - it "clips pixel positions above buffer start", -> - expect(component.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0] - expect(component.screenPositionForPixelPosition(top: -Infinity, left: Infinity)).toEqual [0, 0] - expect(component.screenPositionForPixelPosition(top: -1, left: Infinity)).toEqual [0, 0] - expect(component.screenPositionForPixelPosition(top: 0, left: Infinity)).toEqual [0, 29] - - it "clips pixel positions below buffer end", -> - expect(component.screenPositionForPixelPosition(top: Infinity, left: -Infinity)).toEqual [12, 2] - expect(component.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2] - expect(component.screenPositionForPixelPosition(top: component.getScrollHeight() + 1, left: 0)).toEqual [12, 2] - expect(component.screenPositionForPixelPosition(top: component.getScrollHeight() - 1, left: 0)).toEqual [12, 0] - describe "::getVisibleRowRange()", -> beforeEach -> wrapperNode.style.height = lineHeightInPixels * 8 + "px" From 2ffa7da59e79a6a2ee1d39814fd39696beea0a8c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 2 Oct 2015 12:32:59 +0200 Subject: [PATCH 63/80] :racehorse: Remove rows to measure in the next frame --- spec/lines-yardstick-spec.coffee | 1 + spec/text-editor-presenter-spec.coffee | 10 ++++++++++ src/lines-yardstick.coffee | 7 +++++++ src/text-editor-presenter.coffee | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 68af04da4..88e56f2df 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -33,6 +33,7 @@ describe "LinesYardstick", -> mockPresenter = isBatching: -> true setScreenRowsToMeasure: (screenRows) -> screenRowsToMeasure = screenRows + clearScreenRowsToMeasure: -> setScreenRowsToMeasure = [] getPreMeasurementState: -> state = {} for screenRow in screenRowsToMeasure diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 2c8b173e3..7b8f50f13 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -114,6 +114,16 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[10]).toBeDefined() expect(stateFn(presenter).tiles[12]).toBeDefined() + presenter.clearScreenRowsToMeasure() + + expect(stateFn(presenter).tiles[0]).toBeDefined() + expect(stateFn(presenter).tiles[2]).toBeDefined() + expect(stateFn(presenter).tiles[4]).toBeDefined() + expect(stateFn(presenter).tiles[6]).toBeDefined() + expect(stateFn(presenter).tiles[8]).toBeUndefined() + expect(stateFn(presenter).tiles[10]).toBeUndefined() + expect(stateFn(presenter).tiles[12]).toBeUndefined() + it "excludes invalid tiles for screen rows to measure", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) presenter.setScreenRowsToMeasure([20, 30]) # unexisting rows diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 94a08e81d..7432bc82e 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -17,6 +17,9 @@ class LinesYardstick @presenter.setScreenRowsToMeasure(screenRows) @lineNodesProvider.updateSync(@presenter.getPreMeasurementState()) + clearScreenRowsForMeasurement: -> + @presenter.clearScreenRowsToMeasure() + screenPositionForPixelPosition: (pixelPosition) -> targetTop = pixelPosition.top targetLeft = pixelPosition.left @@ -75,6 +78,8 @@ class LinesYardstick previousColumn = column column += charLength + @clearScreenRowsForMeasurement() + if targetLeft <= previousLeft + (charWidth / 2) new Point(row, previousColumn) else @@ -92,6 +97,8 @@ class LinesYardstick top = targetRow * @model.getLineHeightInPixels() left = @leftPixelPositionForScreenPosition(targetRow, targetColumn) + @clearScreenRowsForMeasurement() + {top, left} leftPixelPositionForScreenPosition: (row, column) -> diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index e6707b7fe..b0591b10f 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -395,6 +395,12 @@ class TextEditorPresenter @shouldUpdateLineNumbersState = true @shouldUpdateDecorations = true + clearScreenRowsToMeasure: -> + @screenRowsToMeasure = [] + @shouldUpdateLinesState = true + @shouldUpdateLineNumbersState = true + @shouldUpdateDecorations = true + updateTilesState: -> return unless @startRow? and @endRow? and @lineHeight? From b7e373fdca174d5bd48fbc5f66d48c3285b56d82 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 2 Oct 2015 12:48:07 +0200 Subject: [PATCH 64/80] :bug: Correctly measure RTL characters This will fix also the "hidden cursor" issue we were experiencing. /cc: @izuzak --- spec/lines-yardstick-spec.coffee | 17 +++++++++++++++++ src/lines-yardstick.coffee | 19 ++++++++----------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 88e56f2df..2eea19da7 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -103,6 +103,23 @@ describe "LinesYardstick", -> expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 72, top: 28}) expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 120, top: 70}) + it "correctly handles RTL characters", -> + atom.styles.addStyleSheet """ + * { + font-size: 14px; + font-family: monospace; + } + """ + + editor.setText("السلام عليكم") + expect(linesYardstick.pixelPositionForScreenPosition([0, 0]).left).toBe 0 + expect(linesYardstick.pixelPositionForScreenPosition([0, 1]).left).toBe 8 + expect(linesYardstick.pixelPositionForScreenPosition([0, 2]).left).toBe 17 + expect(linesYardstick.pixelPositionForScreenPosition([0, 5]).left).toBe 34 + expect(linesYardstick.pixelPositionForScreenPosition([0, 7]).left).toBe 50 + expect(linesYardstick.pixelPositionForScreenPosition([0, 9]).left).toBe 67 + expect(linesYardstick.pixelPositionForScreenPosition([0, 11]).left).toBe 84 + describe "::screenPositionForPixelPosition(pixelPosition)", -> it "converts pixel positions to screen positions", -> atom.styles.addStyleSheet """ diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 7432bc82e..10ae82b16 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -161,20 +161,17 @@ class LinesYardstick 0 leftPixelPositionForCharInTextNode: (lineNode, textNode, charIndex) -> + @rangeForMeasurement.setStart(textNode, 0) + @rangeForMeasurement.setEnd(textNode, charIndex) + width = @rangeForMeasurement.getBoundingClientRect().width + + @rangeForMeasurement.setStart(textNode, 0) @rangeForMeasurement.setEnd(textNode, textNode.textContent.length) + left = @rangeForMeasurement.getBoundingClientRect().left - position = - if charIndex is 0 - @rangeForMeasurement.setStart(textNode, 0) - @rangeForMeasurement.getBoundingClientRect().left - else if charIndex is textNode.textContent.length - @rangeForMeasurement.setStart(textNode, 0) - @rangeForMeasurement.getBoundingClientRect().right - else - @rangeForMeasurement.setStart(textNode, charIndex) - @rangeForMeasurement.getBoundingClientRect().left + offset = lineNode.getBoundingClientRect().left - position - lineNode.getBoundingClientRect().left + left + width - offset pixelRectForScreenRange: (screenRange) -> lineHeight = @model.getLineHeightInPixels() From beb7896234c10873424c00cce43ae51651dd88ce Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 2 Oct 2015 15:47:27 +0200 Subject: [PATCH 65/80] Enable subpixel font scaling For certain font sizes, enabling `textRendering: optimizeLegibility` caused a bunch of measurement-related issues. You can reproduce it by setting the following in your stylesheet: ``` atom-text-editor { font-size: 14px; text-rendering: optimizeLegibility; } ``` Although I wanted to defer subpixel font scaling to a later moment, it seems like Chrome needs to have it enabled in order to properly support the "legibility" path for text rendering. (I guess this is part of the reason why the Chromium team enabled it by default at some point in the past.) --- spec/lines-yardstick-spec.coffee | 36 +++---- spec/text-editor-component-spec.coffee | 126 +++++++++++++------------ src/browser/atom-window.coffee | 2 +- src/text-editor-presenter.coffee | 16 +++- 4 files changed, 99 insertions(+), 81 deletions(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 2eea19da7..35a13c1fd 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -70,10 +70,10 @@ describe "LinesYardstick", -> expect(linesYardstick.pixelPositionForScreenPosition([0, 0])).toEqual({left: 0, top: 0}) expect(linesYardstick.pixelPositionForScreenPosition([0, 1])).toEqual({left: 7, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition([0, 5])).toEqual({left: 38, top: 0}) - expect(linesYardstick.pixelPositionForScreenPosition([1, 6])).toEqual({left: 42, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition([1, 9])).toEqual({left: 72, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition([2, Infinity])).toEqual({left: 280, top: 28}) + expect(linesYardstick.pixelPositionForScreenPosition([0, 5])).toEqual({left: 37.8046875, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition([1, 6])).toEqual({left: 43.20703125, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition([1, 9])).toEqual({left: 72.20703125, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition([2, Infinity])).toEqual({left: 288.046875, top: 28}) it "reuses already computed pixel positions unless it is invalidated", -> atom.styles.addStyleSheet """ @@ -83,9 +83,9 @@ describe "LinesYardstick", -> } """ - expect(linesYardstick.pixelPositionForScreenPosition([1, 2])).toEqual({left: 20, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 60, top: 28}) - expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 100, top: 70}) + expect(linesYardstick.pixelPositionForScreenPosition([1, 2])).toEqual({left: 19.203125, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 57.609375, top: 28}) + expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 95.609375, top: 70}) atom.styles.addStyleSheet """ * { @@ -93,15 +93,15 @@ describe "LinesYardstick", -> } """ - expect(linesYardstick.pixelPositionForScreenPosition([1, 2])).toEqual({left: 20, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 60, top: 28}) - expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 100, top: 70}) + expect(linesYardstick.pixelPositionForScreenPosition([1, 2])).toEqual({left: 19.203125, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 57.609375, top: 28}) + expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 95.609375, top: 70}) linesYardstick.invalidateCache() - expect(linesYardstick.pixelPositionForScreenPosition([1, 2])).toEqual({left: 24, top: 14}) - expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 72, top: 28}) - expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 120, top: 70}) + expect(linesYardstick.pixelPositionForScreenPosition([1, 2])).toEqual({left: 24.00390625, top: 14}) + expect(linesYardstick.pixelPositionForScreenPosition([2, 6])).toEqual({left: 72.01171875, top: 28}) + expect(linesYardstick.pixelPositionForScreenPosition([5, 10])).toEqual({left: 120.01171875, top: 70}) it "correctly handles RTL characters", -> atom.styles.addStyleSheet """ @@ -114,8 +114,8 @@ describe "LinesYardstick", -> editor.setText("السلام عليكم") expect(linesYardstick.pixelPositionForScreenPosition([0, 0]).left).toBe 0 expect(linesYardstick.pixelPositionForScreenPosition([0, 1]).left).toBe 8 - expect(linesYardstick.pixelPositionForScreenPosition([0, 2]).left).toBe 17 - expect(linesYardstick.pixelPositionForScreenPosition([0, 5]).left).toBe 34 + expect(linesYardstick.pixelPositionForScreenPosition([0, 2]).left).toBe 16 + expect(linesYardstick.pixelPositionForScreenPosition([0, 5]).left).toBe 33 expect(linesYardstick.pixelPositionForScreenPosition([0, 7]).left).toBe 50 expect(linesYardstick.pixelPositionForScreenPosition([0, 9]).left).toBe 67 expect(linesYardstick.pixelPositionForScreenPosition([0, 11]).left).toBe 84 @@ -133,13 +133,13 @@ describe "LinesYardstick", -> """ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 12.5})).toEqual([0, 2]) - expect(linesYardstick.screenPositionForPixelPosition({top: 14, left: 17.8})).toEqual([1, 3]) + expect(linesYardstick.screenPositionForPixelPosition({top: 14, left: 18.8})).toEqual([1, 3]) expect(linesYardstick.screenPositionForPixelPosition({top: 28, left: 100})).toEqual([2, 14]) expect(linesYardstick.screenPositionForPixelPosition({top: 32, left: 24.3})).toEqual([2, 3]) expect(linesYardstick.screenPositionForPixelPosition({top: 46, left: 66.5})).toEqual([3, 9]) expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 99.9})).toEqual([5, 14]) - expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 221.5})).toEqual([5, 29]) - expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 222})).toEqual([5, 30]) + expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 224.4365234375})).toEqual([5, 29]) + expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 225})).toEqual([5, 30]) it "clips pixel positions above buffer start", -> expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0] diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 6f53ee4e2..f6936e4c4 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -958,8 +958,8 @@ describe "TextEditorComponent", -> cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 expect(cursorNodes[0].offsetHeight).toBe lineHeightInPixels - expect(cursorNodes[0].offsetWidth).toBe charWidth - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{5 * charWidth}px, #{0 * lineHeightInPixels}px)" + expect(cursorNodes[0].offsetWidth).toBeCloseTo charWidth, 0 + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(5 * charWidth)}px, #{0 * lineHeightInPixels}px)" cursor2 = editor.addCursorAtScreenPosition([8, 11], autoscroll: false) cursor3 = editor.addCursorAtScreenPosition([4, 10], autoscroll: false) @@ -968,8 +968,8 @@ describe "TextEditorComponent", -> cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 expect(cursorNodes[0].offsetTop).toBe 0 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{5 * charWidth}px, #{0 * lineHeightInPixels}px)" - expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{10 * charWidth}px, #{4 * lineHeightInPixels}px)" + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(5 * charWidth)}px, #{0 * lineHeightInPixels}px)" + expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{Math.round(10 * charWidth)}px, #{4 * lineHeightInPixels}px)" verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) @@ -980,13 +980,13 @@ describe "TextEditorComponent", -> cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{10 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" - expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{11 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(10 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" + expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" editor.onDidChangeCursorPosition cursorMovedListener = jasmine.createSpy('cursorMovedListener') cursor3.setScreenPosition([4, 11], autoscroll: false) nextAnimationFrame() - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" expect(cursorMovedListener).toHaveBeenCalled() cursor3.destroy() @@ -994,7 +994,7 @@ describe "TextEditorComponent", -> cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(11 * charWidth - horizontalScrollbarNode.scrollLeft)}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" it "accounts for character widths when positioning cursors", -> atom.config.set('editor.fontFamily', 'sans-serif') @@ -1010,8 +1010,8 @@ describe "TextEditorComponent", -> range.setEnd(cursorLocationTextNode, 1) rangeRect = range.getBoundingClientRect() - expect(cursorRect.left).toBe rangeRect.left - expect(cursorRect.width).toBe rangeRect.width + expect(cursorRect.left).toBeCloseTo rangeRect.left, 0 + expect(cursorRect.width).toBeCloseTo rangeRect.width, 0 it "accounts for the width of paired characters when positioning cursors", -> atom.config.set('editor.fontFamily', 'sans-serif') @@ -1029,8 +1029,8 @@ describe "TextEditorComponent", -> range.setEnd(cursorLocationTextNode, 1) rangeRect = range.getBoundingClientRect() - expect(cursorRect.left).toBe rangeRect.left - expect(cursorRect.width).toBe rangeRect.width + expect(cursorRect.left).toBeCloseTo rangeRect.left, 0 + expect(cursorRect.width).toBeCloseTo rangeRect.width, 0 it "positions cursors correctly after character widths are changed via a stylesheet change", -> atom.config.set('editor.fontFamily', 'sans-serif') @@ -1053,8 +1053,8 @@ describe "TextEditorComponent", -> range.setEnd(cursorLocationTextNode, 1) rangeRect = range.getBoundingClientRect() - expect(cursorRect.left).toBe rangeRect.left - expect(cursorRect.width).toBe rangeRect.width + expect(cursorRect.left).toBeCloseTo rangeRect.left, 0 + expect(cursorRect.width).toBeCloseTo rangeRect.width, 0 atom.themes.removeStylesheet('test') @@ -1062,13 +1062,13 @@ describe "TextEditorComponent", -> editor.setCursorScreenPosition([0, Infinity]) nextAnimationFrame() cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.offsetWidth).toBe charWidth + expect(cursorNode.offsetWidth).toBeCloseTo charWidth, 0 it "gives the cursor a non-zero width even if it's inside atomic tokens", -> editor.setCursorScreenPosition([1, 0]) nextAnimationFrame() cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.offsetWidth).toBe charWidth + expect(cursorNode.offsetWidth).toBeCloseTo charWidth, 0 it "blinks cursors when they aren't moving", -> cursorsNode = componentNode.querySelector('.cursors') @@ -1102,21 +1102,21 @@ describe "TextEditorComponent", -> cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{8 * charWidth}px, #{6 * lineHeightInPixels}px)" + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{Math.round(8 * charWidth)}px, #{6 * lineHeightInPixels}px)" it "updates cursor positions when the line height changes", -> editor.setCursorBufferPosition([1, 10]) component.setLineHeight(2) nextAnimationFrame() cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.style['-webkit-transform']).toBe "translate(#{10 * editor.getDefaultCharWidth()}px, #{editor.getLineHeightInPixels()}px)" + expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(10 * editor.getDefaultCharWidth())}px, #{editor.getLineHeightInPixels()}px)" it "updates cursor positions when the font size changes", -> editor.setCursorBufferPosition([1, 10]) component.setFontSize(10) nextAnimationFrame() cursorNode = componentNode.querySelector('.cursor') - expect(cursorNode.style['-webkit-transform']).toBe "translate(#{10 * editor.getDefaultCharWidth()}px, #{editor.getLineHeightInPixels()}px)" + expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(10 * editor.getDefaultCharWidth())}px, #{editor.getLineHeightInPixels()}px)" it "updates cursor positions when the font family changes", -> editor.setCursorBufferPosition([1, 10]) @@ -1125,7 +1125,7 @@ describe "TextEditorComponent", -> cursorNode = componentNode.querySelector('.cursor') {left} = wrapperNode.pixelPositionForScreenPosition([1, 10]) - expect(cursorNode.style['-webkit-transform']).toBe "translate(#{left}px, #{editor.getLineHeightInPixels()}px)" + expect(cursorNode.style['-webkit-transform']).toBe "translate(#{Math.round(left)}px, #{editor.getLineHeightInPixels()}px)" describe "selection rendering", -> [scrollViewNode, scrollViewClientLeft] = [] @@ -1144,8 +1144,8 @@ describe "TextEditorComponent", -> regionRect = regions[0].getBoundingClientRect() expect(regionRect.top).toBe 1 * lineHeightInPixels expect(regionRect.height).toBe 1 * lineHeightInPixels - expect(regionRect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(regionRect.width).toBe 4 * charWidth + expect(regionRect.left).toBeCloseTo scrollViewClientLeft + 6 * charWidth, 0 + expect(regionRect.width).toBeCloseTo 4 * charWidth, 0 it "renders 2 regions for 2-line selections", -> editor.setSelectedScreenRange([[1, 6], [2, 10]]) @@ -1157,14 +1157,14 @@ describe "TextEditorComponent", -> region1Rect = regions[0].getBoundingClientRect() expect(region1Rect.top).toBe 1 * lineHeightInPixels expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(region1Rect.right).toBe tileNode.getBoundingClientRect().right + expect(region1Rect.left).toBeCloseTo scrollViewClientLeft + 6 * charWidth, 0 + expect(region1Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 region2Rect = regions[1].getBoundingClientRect() expect(region2Rect.top).toBe 2 * lineHeightInPixels expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBe scrollViewClientLeft + 0 - expect(region2Rect.width).toBe 10 * charWidth + expect(region2Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 + expect(region2Rect.width).toBeCloseTo 10 * charWidth, 0 it "renders 3 regions per tile for selections with more than 2 lines", -> editor.setSelectedScreenRange([[0, 6], [5, 10]]) @@ -1178,20 +1178,20 @@ describe "TextEditorComponent", -> region1Rect = regions[0].getBoundingClientRect() expect(region1Rect.top).toBe 0 expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth - expect(region1Rect.right).toBe tileNode.getBoundingClientRect().right + expect(region1Rect.left).toBeCloseTo scrollViewClientLeft + 6 * charWidth, 0 + expect(region1Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 region2Rect = regions[1].getBoundingClientRect() expect(region2Rect.top).toBe 1 * lineHeightInPixels expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBe scrollViewClientLeft + 0 - expect(region2Rect.right).toBe tileNode.getBoundingClientRect().right + expect(region2Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 + expect(region2Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 region3Rect = regions[2].getBoundingClientRect() expect(region3Rect.top).toBe 2 * lineHeightInPixels expect(region3Rect.height).toBe 1 * lineHeightInPixels - expect(region3Rect.left).toBe scrollViewClientLeft + 0 - expect(region3Rect.right).toBe tileNode.getBoundingClientRect().right + expect(region3Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 + expect(region3Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 # Tile 3 tileNode = component.tileNodesForLines()[1] @@ -1201,20 +1201,20 @@ describe "TextEditorComponent", -> region1Rect = regions[0].getBoundingClientRect() expect(region1Rect.top).toBe 3 * lineHeightInPixels expect(region1Rect.height).toBe 1 * lineHeightInPixels - expect(region1Rect.left).toBe scrollViewClientLeft + 0 - expect(region1Rect.right).toBe tileNode.getBoundingClientRect().right + expect(region1Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 + expect(region1Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 region2Rect = regions[1].getBoundingClientRect() expect(region2Rect.top).toBe 4 * lineHeightInPixels expect(region2Rect.height).toBe 1 * lineHeightInPixels - expect(region2Rect.left).toBe scrollViewClientLeft + 0 - expect(region2Rect.right).toBe tileNode.getBoundingClientRect().right + expect(region2Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 + expect(region2Rect.right).toBeCloseTo tileNode.getBoundingClientRect().right, 0 region3Rect = regions[2].getBoundingClientRect() expect(region3Rect.top).toBe 5 * lineHeightInPixels expect(region3Rect.height).toBe 1 * lineHeightInPixels - expect(region3Rect.left).toBe scrollViewClientLeft + 0 - expect(region3Rect.width).toBe 10 * charWidth + expect(region3Rect.left).toBeCloseTo scrollViewClientLeft + 0, 0 + expect(region3Rect.width).toBeCloseTo 10 * charWidth, 0 it "does not render empty selections", -> editor.addSelectionForBufferRange([[2, 2], [2, 2]]) @@ -1237,7 +1237,7 @@ describe "TextEditorComponent", -> nextAnimationFrame() selectionNode = componentNode.querySelector('.region') expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() - expect(selectionNode.offsetLeft).toBe 6 * editor.getDefaultCharWidth() + expect(selectionNode.offsetLeft).toBeCloseTo 6 * editor.getDefaultCharWidth(), 0 it "updates selections when the font family changes", -> editor.setSelectedBufferRange([[1, 6], [1, 10]]) @@ -1245,7 +1245,7 @@ describe "TextEditorComponent", -> nextAnimationFrame() selectionNode = componentNode.querySelector('.region') expect(selectionNode.offsetTop).toBe editor.getLineHeightInPixels() - expect(selectionNode.offsetLeft).toBe wrapperNode.pixelPositionForScreenPosition([1, 6]).left + expect(selectionNode.offsetLeft).toBeCloseTo wrapperNode.pixelPositionForScreenPosition([1, 6]).left, 0 it "will flash the selection when flash:true is passed to editor::setSelectedBufferRange", -> editor.setSelectedBufferRange([[1, 6], [1, 10]], flash: true) @@ -1439,8 +1439,8 @@ describe "TextEditorComponent", -> regionRect = regions[0].style expect(regionRect.top).toBe (0 + 'px') expect(regionRect.height).toBe 1 * lineHeightInPixels + 'px' - expect(regionRect.left).toBe 2 * charWidth + 'px' - expect(regionRect.width).toBe 2 * charWidth + 'px' + expect(regionRect.left).toBe Math.round(2 * charWidth) + 'px' + expect(regionRect.width).toBe Math.round(2 * charWidth) + 'px' it "renders highlights decoration's marker is added", -> regions = componentNode.querySelectorAll('.test-highlight .region') @@ -1606,7 +1606,7 @@ describe "TextEditorComponent", -> position = wrapperNode.pixelPositionForBufferPosition([2, 10]) overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe position.left + gutterWidth + 'px' + expect(overlay.style.left).toBe Math.round(position.left + gutterWidth) + 'px' expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' describe "positioning the overlay when near the edge of the editor", -> @@ -1614,10 +1614,10 @@ describe "TextEditorComponent", -> beforeEach -> atom.storeWindowDimensions() - itemWidth = 4 * editor.getDefaultCharWidth() + itemWidth = Math.round(4 * editor.getDefaultCharWidth()) itemHeight = 4 * editor.getLineHeightInPixels() - windowWidth = gutterWidth + 30 * editor.getDefaultCharWidth() + windowWidth = Math.round(gutterWidth + 30 * editor.getDefaultCharWidth()) windowHeight = 10 * editor.getLineHeightInPixels() item.style.width = itemWidth + 'px' @@ -1645,7 +1645,7 @@ describe "TextEditorComponent", -> position = wrapperNode.pixelPositionForBufferPosition([0, 26]) overlay = component.getTopmostDOMNode().querySelector('atom-overlay') - expect(overlay.style.left).toBe position.left + gutterWidth + 'px' + expect(overlay.style.left).toBe Math.round(position.left + gutterWidth) + 'px' expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px' editor.insertText('a') @@ -1689,7 +1689,7 @@ describe "TextEditorComponent", -> wrapperNode.focus() # updates via state change nextAnimationFrame() expect(inputNode.offsetTop).toBe (5 * lineHeightInPixels) - wrapperNode.getScrollTop() - expect(inputNode.offsetLeft).toBe (4 * charWidth) - wrapperNode.getScrollLeft() + expect(inputNode.offsetLeft).toBeCloseTo (4 * charWidth) - wrapperNode.getScrollLeft(), 0 # In bounds, not focused inputNode.blur() # updates via state change @@ -2482,7 +2482,7 @@ describe "TextEditorComponent", -> rightOfLongestLine = component.lineNodeForScreenRow(6).querySelector('.line > span:last-child').getBoundingClientRect().right leftOfVerticalScrollbar = verticalScrollbarNode.getBoundingClientRect().left - expect(Math.round(rightOfLongestLine)).toBe leftOfVerticalScrollbar - 1 # Leave 1 px so the cursor is visible on the end of the line + expect(Math.round(rightOfLongestLine)).toBeCloseTo leftOfVerticalScrollbar - 1, 0 # Leave 1 px so the cursor is visible on the end of the line it "only displays dummy scrollbars when scrollable in that direction", -> expect(verticalScrollbarNode.style.display).toBe 'none' @@ -2963,7 +2963,7 @@ describe "TextEditorComponent", -> cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right - expect(cursorLeft).toBe line0Right + expect(cursorLeft).toBeCloseTo line0Right, 0 describe "when the fontFamily changes while the editor is hidden", -> it "does not attempt to measure the defaultCharWidth until the editor becomes visible again", -> @@ -2995,7 +2995,7 @@ describe "TextEditorComponent", -> cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right - expect(cursorLeft).toBe line0Right + expect(cursorLeft).toBeCloseTo line0Right, 0 describe "when stylesheets change while the editor is hidden", -> afterEach -> @@ -3021,7 +3021,7 @@ describe "TextEditorComponent", -> cursorLeft = componentNode.querySelector('.cursor').getBoundingClientRect().left line0Right = componentNode.querySelector('.line > span:last-child').getBoundingClientRect().right - expect(cursorLeft).toBe line0Right + expect(cursorLeft).toBeCloseTo line0Right, 0 describe "when lines are changed while the editor is hidden", -> xit "does not measure new characters until the editor is shown again", -> @@ -3350,8 +3350,9 @@ describe "TextEditorComponent", -> editor.setSelectedBufferRange([[5, 6], [6, 8]]) nextAnimationFrame() + right = wrapperNode.pixelPositionForBufferPosition([6, 8 + editor.getHorizontalScrollMargin()]).left expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 - expect(wrapperNode.getScrollRight()).toBe (8 + editor.getHorizontalScrollMargin()) * 10 + expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 editor.setSelectedBufferRange([[0, 0], [0, 0]]) nextAnimationFrame() @@ -3361,14 +3362,16 @@ describe "TextEditorComponent", -> editor.setSelectedBufferRange([[6, 6], [6, 8]]) nextAnimationFrame() expect(wrapperNode.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10 - expect(wrapperNode.getScrollRight()).toBe (8 + editor.getHorizontalScrollMargin()) * 10 + expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 describe "when adding selections for buffer ranges", -> it "autoscrolls to the added selection if needed", -> editor.addSelectionForBufferRange([[8, 10], [8, 15]]) nextAnimationFrame() + + right = wrapperNode.pixelPositionForBufferPosition([8, 15]).left expect(wrapperNode.getScrollBottom()).toBe (9 * 10) + (2 * 10) - expect(wrapperNode.getScrollRight()).toBe (15 * 10) + (2 * 10) + expect(wrapperNode.getScrollRight()).toBeCloseTo(right + 2 * 10, 0) describe "when selecting lines containing cursors", -> it "autoscrolls to the selection", -> @@ -3406,9 +3409,10 @@ describe "TextEditorComponent", -> editor.scrollToCursorPosition() nextAnimationFrame() + right = wrapperNode.pixelPositionForScreenPosition([8, 9 + editor.getHorizontalScrollMargin()]).left expect(wrapperNode.getScrollTop()).toBe (8.8 * 10) - 30 expect(wrapperNode.getScrollBottom()).toBe (8.3 * 10) + 30 - expect(wrapperNode.getScrollRight()).toBe (9 + editor.getHorizontalScrollMargin()) * 10 + expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 wrapperNode.setScrollTop(0) editor.scrollToCursorPosition(center: false) @@ -3460,11 +3464,13 @@ describe "TextEditorComponent", -> editor.moveRight() nextAnimationFrame() - expect(wrapperNode.getScrollRight()).toBe 6 * 10 + right = wrapperNode.pixelPositionForScreenPosition([0, 6]).left + expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 editor.moveRight() nextAnimationFrame() - expect(wrapperNode.getScrollRight()).toBe 7 * 10 + right = wrapperNode.pixelPositionForScreenPosition([0, 7]).left + expect(wrapperNode.getScrollRight()).toBeCloseTo right, 0 it "scrolls left when the last cursor gets closer than ::horizontalScrollMargin to the left of the editor", -> wrapperNode.setScrollRight(wrapperNode.getScrollWidth()) @@ -3475,11 +3481,13 @@ describe "TextEditorComponent", -> editor.moveLeft() nextAnimationFrame() - expect(wrapperNode.getScrollLeft()).toBe 59 * 10 + left = wrapperNode.pixelPositionForScreenPosition([6, 59]).left + expect(wrapperNode.getScrollLeft()).toBeCloseTo left, 0 editor.moveLeft() nextAnimationFrame() - expect(wrapperNode.getScrollLeft()).toBe 58 * 10 + left = wrapperNode.pixelPositionForScreenPosition([6, 58]).left + expect(wrapperNode.getScrollLeft()).toBeCloseTo left, 0 it "scrolls down when inserting lines makes the document longer than the editor's height", -> editor.setCursorScreenPosition([13, Infinity]) diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee index 99b28444f..1372d46ec 100644 --- a/src/browser/atom-window.coffee +++ b/src/browser/atom-window.coffee @@ -28,7 +28,7 @@ class AtomWindow title: 'Atom' 'web-preferences': 'direct-write': true - 'subpixel-font-scaling': false + 'subpixel-font-scaling': true # Don't set icon on Windows so the exe's ico will be used as window and # taskbar's icon. See https://github.com/atom/atom/issues/4811 for more. if process.platform is 'linux' diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index b0591b10f..e647aa40c 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -501,7 +501,7 @@ class TextEditorPresenter return unless cursor.isVisible() and @startRow <= screenRange.start.row < @endRow pixelRect = @pixelRectForScreenRange(screenRange) - pixelRect.width = @baseCharacterWidth if pixelRect.width is 0 + pixelRect.width = Math.round(@baseCharacterWidth) if pixelRect.width is 0 @state.content.cursors[cursor.id] = pixelRect updateOverlaysState: -> @@ -1134,6 +1134,10 @@ class TextEditorPresenter @linesYardstick.pixelPositionForScreenPosition(screenPosition, clip) position.top -= @getScrollTop() position.left -= @getScrollLeft() + + position.top = Math.round(position.top) + position.left = Math.round(position.left) + position hasPixelRectRequirements: -> @@ -1146,6 +1150,12 @@ class TextEditorPresenter rect = @linesYardstick.pixelRectForScreenRange(screenRange) rect.top -= @getScrollTop() rect.left -= @getScrollLeft() + + rect.top = Math.round(rect.top) + rect.left = Math.round(rect.left) + rect.width = Math.round(rect.width) + rect.height = Math.round(rect.height) + rect observeDecoration: (decoration) -> @@ -1507,10 +1517,10 @@ class TextEditorPresenter @emitDidUpdateState() getVerticalScrollMarginInPixels: -> - @model.getVerticalScrollMargin() * @lineHeight + Math.round(@model.getVerticalScrollMargin() * @lineHeight) getHorizontalScrollMarginInPixels: -> - @model.getHorizontalScrollMargin() * @baseCharacterWidth + Math.round(@model.getHorizontalScrollMargin() * @baseCharacterWidth) getVerticalScrollbarWidth: -> @verticalScrollbarWidth From 56488748db5bedd7077545f43f217abfe34ea0d6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 5 Oct 2015 14:37:22 +0200 Subject: [PATCH 66/80] :fire: Remove dead code --- src/lines-component.coffee | 1 + src/lines-tile-component.coffee | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 3cd9215a6..b4bbed602 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -54,6 +54,7 @@ class LinesComponent extends TiledComponent @placeholderTextDiv.classList.add('placeholder-text') @placeholderTextDiv.textContent = @newState.placeholderText @domNode.appendChild(@placeholderTextDiv) + @oldState.placeholderText = @newState.placeholderText if @newState.width isnt @oldState.width @domNode.style.width = @newState.width + 'px' diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 4b18af3bb..5b4fc55c5 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -49,10 +49,6 @@ class LinesTileComponent @domNode.style.zIndex = @newTileState.zIndex @oldTileState.zIndex = @newTileState.zIndex - if @newTileState.visibility isnt @oldTileState.visibility - @domNode.style.visibility = @newTileState.visibility - @oldTileState.visibility = @newTileState.visibility - if @newTileState.display isnt @oldTileState.display @domNode.style.display = @newTileState.display @oldTileState.display = @newTileState.display From 581ffb136078b4b499ba1fd393839f14c3362bcd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 5 Oct 2015 16:05:17 +0200 Subject: [PATCH 67/80] :racehorse: Avoid to measure invisible lines when dragging --- spec/lines-yardstick-spec.coffee | 27 +++++++++++++++++++++++++-- src/lines-yardstick.coffee | 12 ++++++------ src/text-editor-component.coffee | 14 +++++++------- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 35a13c1fd..3bc14ba3b 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -44,8 +44,7 @@ describe "LinesYardstick", -> mockLineNodesProvider = updateSync: (state) -> availableScreenRows = state lineNodeForLineIdAndScreenRow: (lineId, screenRow) -> - if availableScreenRows[lineId] isnt screenRow - throw new Error("No line node found!") + return if availableScreenRows[lineId] isnt screenRow buildLineNode(screenRow) @@ -120,6 +119,18 @@ describe "LinesYardstick", -> expect(linesYardstick.pixelPositionForScreenPosition([0, 9]).left).toBe 67 expect(linesYardstick.pixelPositionForScreenPosition([0, 11]).left).toBe 84 + it "doesn't measure invisible lines if it is explicitly told so", -> + atom.styles.addStyleSheet """ + * { + font-size: 12px; + font-family: monospace; + } + """ + + expect(linesYardstick.pixelPositionForScreenPosition([0, 0], true, true)).toEqual({left: 0, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition([0, 1], true, true)).toEqual({left: 0, top: 0}) + expect(linesYardstick.pixelPositionForScreenPosition([0, 5], true, true)).toEqual({left: 0, top: 0}) + describe "::screenPositionForPixelPosition(pixelPosition)", -> it "converts pixel positions to screen positions", -> atom.styles.addStyleSheet """ @@ -152,3 +163,15 @@ describe "LinesYardstick", -> expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2] expect(linesYardstick.screenPositionForPixelPosition(top: (editor.getLastScreenRow() + 1) * 14, left: 0)).toEqual [12, 2] expect(linesYardstick.screenPositionForPixelPosition(top: editor.getLastScreenRow() * 14, left: 0)).toEqual [12, 0] + + it "doesn't measure invisible lines if it is explicitly told so", -> + atom.styles.addStyleSheet """ + * { + font-size: 12px; + font-family: monospace; + } + """ + + expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 13}, true)).toEqual([0, 0]) + expect(linesYardstick.screenPositionForPixelPosition({top: 14, left: 20}, true)).toEqual([1, 0]) + expect(linesYardstick.screenPositionForPixelPosition({top: 28, left: 100}, true)).toEqual([2, 0]) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index 10ae82b16..d20d7f018 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -20,7 +20,7 @@ class LinesYardstick clearScreenRowsForMeasurement: -> @presenter.clearScreenRowsToMeasure() - screenPositionForPixelPosition: (pixelPosition) -> + screenPositionForPixelPosition: (pixelPosition, measureVisibleLinesOnly) -> targetTop = pixelPosition.top targetLeft = pixelPosition.left defaultCharWidth = @model.getDefaultCharWidth() @@ -30,7 +30,7 @@ class LinesYardstick row = Math.min(row, @model.getLastScreenRow()) row = Math.max(0, row) - @prepareScreenRowsForMeasurement([row]) + @prepareScreenRowsForMeasurement([row]) unless measureVisibleLinesOnly line = @model.tokenizedLineForScreenRow(row) lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row) @@ -78,26 +78,26 @@ class LinesYardstick previousColumn = column column += charLength - @clearScreenRowsForMeasurement() + @clearScreenRowsForMeasurement() unless measureVisibleLinesOnly if targetLeft <= previousLeft + (charWidth / 2) new Point(row, previousColumn) else new Point(row, column) - pixelPositionForScreenPosition: (screenPosition, clip=true) -> + pixelPositionForScreenPosition: (screenPosition, clip=true, measureVisibleLinesOnly) -> screenPosition = Point.fromObject(screenPosition) screenPosition = @model.clipScreenPosition(screenPosition) if clip targetRow = screenPosition.row targetColumn = screenPosition.column - @prepareScreenRowsForMeasurement([targetRow]) + @prepareScreenRowsForMeasurement([targetRow]) unless measureVisibleLinesOnly top = targetRow * @model.getLineHeightInPixels() left = @leftPixelPositionForScreenPosition(targetRow, targetColumn) - @clearScreenRowsForMeasurement() + @clearScreenRowsForMeasurement() unless measureVisibleLinesOnly {top, left} diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 170c90bbf..b7a85161a 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -423,14 +423,14 @@ class TextEditorComponent getVisibleRowRange: -> @presenter.getVisibleRowRange() - pixelPositionForScreenPosition: (screenPosition) -> - @linesYardstick.pixelPositionForScreenPosition(screenPosition) + pixelPositionForScreenPosition: -> + @linesYardstick.pixelPositionForScreenPosition(arguments...) - screenPositionForPixelPosition: (pixelPosition) -> - @linesYardstick.screenPositionForPixelPosition(pixelPosition) + screenPositionForPixelPosition: -> + @linesYardstick.screenPositionForPixelPosition(arguments...) - pixelRectForScreenRange: (screenRange) -> - @linesYardstick.pixelRectForScreenRange(screenRange) + pixelRectForScreenRange: -> + @linesYardstick.pixelRectForScreenRange(arguments...) pixelRangeForScreenRange: (screenRange, clip=true) -> {start, end} = Range.fromObject(screenRange) @@ -850,7 +850,7 @@ class TextEditorComponent screenPositionForMouseEvent: (event, linesClientRect) -> pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect) - @screenPositionForPixelPosition(pixelPosition) + @screenPositionForPixelPosition(pixelPosition, true) pixelPositionForMouseEvent: (event, linesClientRect) -> {clientX, clientY} = event From 0bee6a0cc184341189520c8c919136020cb53656 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 5 Oct 2015 16:10:58 +0200 Subject: [PATCH 68/80] Avoid invalidating state when clearing screen rows to measure --- spec/text-editor-presenter-spec.coffee | 14 +++++++++++++- src/text-editor-presenter.coffee | 3 --- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 7b8f50f13..69fa7bd9d 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -114,13 +114,25 @@ describe "TextEditorPresenter", -> expect(stateFn(presenter).tiles[10]).toBeDefined() expect(stateFn(presenter).tiles[12]).toBeDefined() - presenter.clearScreenRowsToMeasure() + # clearing additional rows won't trigger a state update + expectNoStateUpdate presenter, -> presenter.clearScreenRowsToMeasure() expect(stateFn(presenter).tiles[0]).toBeDefined() expect(stateFn(presenter).tiles[2]).toBeDefined() expect(stateFn(presenter).tiles[4]).toBeDefined() expect(stateFn(presenter).tiles[6]).toBeDefined() expect(stateFn(presenter).tiles[8]).toBeUndefined() + expect(stateFn(presenter).tiles[10]).toBeDefined() + expect(stateFn(presenter).tiles[12]).toBeDefined() + + # when another change triggers a state update we remove useless lines + expectStateUpdate presenter, -> presenter.setScrollTop(1) + + expect(stateFn(presenter).tiles[0]).toBeDefined() + expect(stateFn(presenter).tiles[2]).toBeDefined() + expect(stateFn(presenter).tiles[4]).toBeDefined() + expect(stateFn(presenter).tiles[6]).toBeDefined() + expect(stateFn(presenter).tiles[8]).toBeDefined() expect(stateFn(presenter).tiles[10]).toBeUndefined() expect(stateFn(presenter).tiles[12]).toBeUndefined() diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index e647aa40c..4b75dc0ae 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -397,9 +397,6 @@ class TextEditorPresenter clearScreenRowsToMeasure: -> @screenRowsToMeasure = [] - @shouldUpdateLinesState = true - @shouldUpdateLineNumbersState = true - @shouldUpdateDecorations = true updateTilesState: -> return unless @startRow? and @endRow? and @lineHeight? From 1c56c3f951771c1a6eee5bc6019034a245a348d6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 5 Oct 2015 16:28:02 +0200 Subject: [PATCH 69/80] :art: --- spec/lines-yardstick-spec.coffee | 1 - src/lines-yardstick.coffee | 10 ++++------ src/text-editor-presenter.coffee | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index 3bc14ba3b..c87f3f9df 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -31,7 +31,6 @@ describe "LinesYardstick", -> lineNode mockPresenter = - isBatching: -> true setScreenRowsToMeasure: (screenRows) -> screenRowsToMeasure = screenRows clearScreenRowsToMeasure: -> setScreenRowsToMeasure = [] getPreMeasurementState: -> diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index d20d7f018..d0d5f7cfc 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -12,8 +12,6 @@ class LinesYardstick @pixelPositionsByLineIdAndColumn = {} prepareScreenRowsForMeasurement: (screenRows) -> - return unless @presenter.isBatching() - @presenter.setScreenRowsToMeasure(screenRows) @lineNodesProvider.updateSync(@presenter.getPreMeasurementState()) @@ -173,17 +171,17 @@ class LinesYardstick left + width - offset - pixelRectForScreenRange: (screenRange) -> + pixelRectForScreenRange: (screenRange, measureVisibleLinesOnly) -> lineHeight = @model.getLineHeightInPixels() if screenRange.end.row > screenRange.start.row - top = @pixelPositionForScreenPosition(screenRange.start).top + top = @pixelPositionForScreenPosition(screenRange.start, true, measureVisibleLinesOnly).top left = 0 height = (screenRange.end.row - screenRange.start.row + 1) * lineHeight width = @presenter.getScrollWidth() else - {top, left} = @pixelPositionForScreenPosition(screenRange.start, false) + {top, left} = @pixelPositionForScreenPosition(screenRange.start, false, measureVisibleLinesOnly) height = lineHeight - width = @pixelPositionForScreenPosition(screenRange.end, false).left - left + width = @pixelPositionForScreenPosition(screenRange.end, false, measureVisibleLinesOnly).left - left {top, left, width, height} diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 4b75dc0ae..a3ce1b370 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1128,7 +1128,7 @@ class TextEditorPresenter pixelPositionForScreenPosition: (screenPosition, clip=true) -> position = - @linesYardstick.pixelPositionForScreenPosition(screenPosition, clip) + @linesYardstick.pixelPositionForScreenPosition(screenPosition, clip, true) position.top -= @getScrollTop() position.left -= @getScrollLeft() @@ -1144,7 +1144,7 @@ class TextEditorPresenter @hasPixelRectRequirements() and @boundingClientRect? and @windowWidth and @windowHeight pixelRectForScreenRange: (screenRange) -> - rect = @linesYardstick.pixelRectForScreenRange(screenRange) + rect = @linesYardstick.pixelRectForScreenRange(screenRange, true) rect.top -= @getScrollTop() rect.left -= @getScrollLeft() From e04aef0af3a707c49bf1c48ab01b78d4c79e446f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 5 Oct 2015 17:36:28 +0200 Subject: [PATCH 70/80] :racehorse: Faster line number calculation --- spec/display-buffer-spec.coffee | 10 ---------- src/text-editor-presenter.coffee | 34 ++++++++++++++++++++++++-------- src/tokenized-line.coffee | 3 --- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index c0c7b608a..7ed97f0d0 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -143,16 +143,6 @@ describe "DisplayBuffer", -> expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[1].isHardTab).toBeTruthy() describe "when a line is wrapped", -> - it "marks it as soft-wrapped", -> - displayBuffer.setEditorWidthInChars(7) - - expect(displayBuffer.tokenizedLineForScreenRow(0).softWrapped).toBeFalsy() - expect(displayBuffer.tokenizedLineForScreenRow(1).softWrapped).toBeTruthy() - expect(displayBuffer.tokenizedLineForScreenRow(2).softWrapped).toBeTruthy() - expect(displayBuffer.tokenizedLineForScreenRow(3).softWrapped).toBeTruthy() - expect(displayBuffer.tokenizedLineForScreenRow(4).softWrapped).toBeTruthy() - expect(displayBuffer.tokenizedLineForScreenRow(5).softWrapped).toBeFalsy() - it "breaks soft-wrap indentation into a token for each indentation level to support indent guides", -> tokenizedLine = displayBuffer.tokenizedLineForScreenRow(4) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index a3ce1b370..6b2c893c7 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -657,15 +657,33 @@ class TextEditorPresenter tileState.lineNumbers ?= {} visibleLineNumberIds = {} - for screenRow in screenRows - line = @model.tokenizedLineForScreenRow(screenRow) - bufferRow = @model.bufferRowForScreenRow(screenRow) - softWrapped = line.softWrapped - decorationClasses = @lineNumberDecorationClassesForRow(screenRow) - foldable = @model.isFoldableAtScreenRow(screenRow) + # rows are reversed + startRow = screenRows[screenRows.length - 1] + endRow = screenRows[0] - tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable} - visibleLineNumberIds[line.id] = true + if startRow > 0 + rowBeforeStartRow = startRow - 1 + lastBufferRow = @model.bufferRowForScreenRow(rowBeforeStartRow) + else + lastBufferRow = null + + if endRow > startRow + bufferRows = @model.bufferRowsForScreenRows(startRow, endRow) + for bufferRow, i in bufferRows + if bufferRow is lastBufferRow + softWrapped = true + else + lastBufferRow = bufferRow + softWrapped = false + + screenRow = startRow + i + top = (screenRow - startRow) * @lineHeight + decorationClasses = @lineNumberDecorationClassesForRow(screenRow) + foldable = @model.isFoldableAtScreenRow(screenRow) + id = @model.tokenizedLineForScreenRow(screenRow).id + + tileState.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable} + visibleLineNumberIds[id] = true for lineId of tileState.lineNumbers delete tileState.lineNumbers[lineId] unless visibleLineNumberIds[lineId] diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index 320703a9b..bf234b6d3 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -34,7 +34,6 @@ class TokenizedLine lineIsWhitespaceOnly: false firstNonWhitespaceIndex: 0 foldable: false - softWrapped: false constructor: (properties) -> @id = idCounter++ @@ -421,7 +420,6 @@ class TokenizedLine leftFragment.tabLength = @tabLength leftFragment.firstNonWhitespaceIndex = Math.min(column, @firstNonWhitespaceIndex) leftFragment.firstTrailingWhitespaceIndex = Math.min(column, @firstTrailingWhitespaceIndex) - leftFragment.softWrapped = @softWrapped rightFragment = new TokenizedLine rightFragment.tokenIterator = @tokenIterator @@ -439,7 +437,6 @@ class TokenizedLine rightFragment.endOfLineInvisibles = @endOfLineInvisibles rightFragment.firstNonWhitespaceIndex = Math.max(softWrapIndent, @firstNonWhitespaceIndex - column + softWrapIndent) rightFragment.firstTrailingWhitespaceIndex = Math.max(softWrapIndent, @firstTrailingWhitespaceIndex - column + softWrapIndent) - rightFragment.softWrapped = true [leftFragment, rightFragment] From b31d3d1a3f6699602000b516b9447cf1d7e0c000 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Oct 2015 09:16:29 +0200 Subject: [PATCH 71/80] Revert ":racehorse: Faster line number calculation" This reverts commit e04aef0af3a707c49bf1c48ab01b78d4c79e446f. --- spec/display-buffer-spec.coffee | 10 ++++++++++ src/text-editor-presenter.coffee | 34 ++++++++------------------------ src/tokenized-line.coffee | 3 +++ 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index 7ed97f0d0..c0c7b608a 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -143,6 +143,16 @@ describe "DisplayBuffer", -> expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[1].isHardTab).toBeTruthy() describe "when a line is wrapped", -> + it "marks it as soft-wrapped", -> + displayBuffer.setEditorWidthInChars(7) + + expect(displayBuffer.tokenizedLineForScreenRow(0).softWrapped).toBeFalsy() + expect(displayBuffer.tokenizedLineForScreenRow(1).softWrapped).toBeTruthy() + expect(displayBuffer.tokenizedLineForScreenRow(2).softWrapped).toBeTruthy() + expect(displayBuffer.tokenizedLineForScreenRow(3).softWrapped).toBeTruthy() + expect(displayBuffer.tokenizedLineForScreenRow(4).softWrapped).toBeTruthy() + expect(displayBuffer.tokenizedLineForScreenRow(5).softWrapped).toBeFalsy() + it "breaks soft-wrap indentation into a token for each indentation level to support indent guides", -> tokenizedLine = displayBuffer.tokenizedLineForScreenRow(4) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 6b2c893c7..a3ce1b370 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -657,33 +657,15 @@ class TextEditorPresenter tileState.lineNumbers ?= {} visibleLineNumberIds = {} - # rows are reversed - startRow = screenRows[screenRows.length - 1] - endRow = screenRows[0] + for screenRow in screenRows + line = @model.tokenizedLineForScreenRow(screenRow) + bufferRow = @model.bufferRowForScreenRow(screenRow) + softWrapped = line.softWrapped + decorationClasses = @lineNumberDecorationClassesForRow(screenRow) + foldable = @model.isFoldableAtScreenRow(screenRow) - if startRow > 0 - rowBeforeStartRow = startRow - 1 - lastBufferRow = @model.bufferRowForScreenRow(rowBeforeStartRow) - else - lastBufferRow = null - - if endRow > startRow - bufferRows = @model.bufferRowsForScreenRows(startRow, endRow) - for bufferRow, i in bufferRows - if bufferRow is lastBufferRow - softWrapped = true - else - lastBufferRow = bufferRow - softWrapped = false - - screenRow = startRow + i - top = (screenRow - startRow) * @lineHeight - decorationClasses = @lineNumberDecorationClassesForRow(screenRow) - foldable = @model.isFoldableAtScreenRow(screenRow) - id = @model.tokenizedLineForScreenRow(screenRow).id - - tileState.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable} - visibleLineNumberIds[id] = true + tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable} + visibleLineNumberIds[line.id] = true for lineId of tileState.lineNumbers delete tileState.lineNumbers[lineId] unless visibleLineNumberIds[lineId] diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index bf234b6d3..320703a9b 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -34,6 +34,7 @@ class TokenizedLine lineIsWhitespaceOnly: false firstNonWhitespaceIndex: 0 foldable: false + softWrapped: false constructor: (properties) -> @id = idCounter++ @@ -420,6 +421,7 @@ class TokenizedLine leftFragment.tabLength = @tabLength leftFragment.firstNonWhitespaceIndex = Math.min(column, @firstNonWhitespaceIndex) leftFragment.firstTrailingWhitespaceIndex = Math.min(column, @firstTrailingWhitespaceIndex) + leftFragment.softWrapped = @softWrapped rightFragment = new TokenizedLine rightFragment.tokenIterator = @tokenIterator @@ -437,6 +439,7 @@ class TokenizedLine rightFragment.endOfLineInvisibles = @endOfLineInvisibles rightFragment.firstNonWhitespaceIndex = Math.max(softWrapIndent, @firstNonWhitespaceIndex - column + softWrapIndent) rightFragment.firstTrailingWhitespaceIndex = Math.max(softWrapIndent, @firstTrailingWhitespaceIndex - column + softWrapIndent) + rightFragment.softWrapped = true [leftFragment, rightFragment] From c79cc87172ca9e2321f1744668265701a67f708d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Oct 2015 09:54:05 +0200 Subject: [PATCH 72/80] Prepare DOMElementPool to account for text nodes --- spec/dom-element-pool-spec.coffee | 45 ++++++++++++++------------ src/dom-element-pool.coffee | 33 ++++++++++++------- src/highlights-component.coffee | 6 ++-- src/line-numbers-tile-component.coffee | 6 ++-- src/lines-tile-component.coffee | 24 +++++++------- 5 files changed, 64 insertions(+), 50 deletions(-) diff --git a/spec/dom-element-pool-spec.coffee b/spec/dom-element-pool-spec.coffee index 1399b17fc..2efe80beb 100644 --- a/spec/dom-element-pool-spec.coffee +++ b/spec/dom-element-pool-spec.coffee @@ -1,4 +1,5 @@ DOMElementPool = require '../src/dom-element-pool' +{contains} = require 'underscore-plus' describe "DOMElementPool", -> domElementPool = null @@ -7,46 +8,50 @@ describe "DOMElementPool", -> domElementPool = new DOMElementPool it "builds DOM nodes, recycling them when they are freed", -> - [div, span1, span2, span3, span4, span5] = elements = [ - domElementPool.build("div") - domElementPool.build("span") - domElementPool.build("span") - domElementPool.build("span") - domElementPool.build("span") - domElementPool.build("span") + [div, span1, span2, span3, span4, span5, textNode] = elements = [ + domElementPool.buildElement("div") + domElementPool.buildElement("span") + domElementPool.buildElement("span") + domElementPool.buildElement("span") + domElementPool.buildElement("span") + domElementPool.buildElement("span") + domElementPool.buildText("Hello world!") ] div.appendChild(span1) span1.appendChild(span2) div.appendChild(span3) span3.appendChild(span4) + span4.appendChild(textNode) domElementPool.freeElementAndDescendants(div) domElementPool.freeElementAndDescendants(span5) - expect(elements).toContain(domElementPool.build("div")) - expect(elements).toContain(domElementPool.build("span")) - expect(elements).toContain(domElementPool.build("span")) - expect(elements).toContain(domElementPool.build("span")) - expect(elements).toContain(domElementPool.build("span")) - expect(elements).toContain(domElementPool.build("span")) + expect(contains(elements, domElementPool.buildElement("div"))).toBe(true) + expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) + expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) + expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) + expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) + expect(contains(elements, domElementPool.buildElement("span"))).toBe(true) + expect(contains(elements, domElementPool.buildText("another text"))).toBe(true) - expect(elements).not.toContain(domElementPool.build("div")) - expect(elements).not.toContain(domElementPool.build("span")) + expect(contains(elements, domElementPool.buildElement("div"))).toBe(false) + expect(contains(elements, domElementPool.buildElement("span"))).toBe(false) + expect(contains(elements, domElementPool.buildText("unexisting"))).toBe(false) it "forgets free nodes after being cleared", -> - span = domElementPool.build("span") - div = domElementPool.build("div") + span = domElementPool.buildElement("span") + div = domElementPool.buildElement("div") domElementPool.freeElementAndDescendants(span) domElementPool.freeElementAndDescendants(div) domElementPool.clear() - expect(domElementPool.build("span")).not.toBe(span) - expect(domElementPool.build("div")).not.toBe(div) + expect(domElementPool.buildElement("span")).not.toBe(span) + expect(domElementPool.buildElement("div")).not.toBe(div) it "throws an error when trying to free the same node twice", -> - div = domElementPool.build("div") + div = domElementPool.buildElement("div") domElementPool.freeElementAndDescendants(div) expect(-> domElementPool.freeElementAndDescendants(div)).toThrow() diff --git a/src/dom-element-pool.coffee b/src/dom-element-pool.coffee index 257f9a180..9cdf1f7f6 100644 --- a/src/dom-element-pool.coffee +++ b/src/dom-element-pool.coffee @@ -10,23 +10,32 @@ class DOMElementPool freeElements.length = 0 return - build: (tagName, className, textContent = "") -> + build: (tagName, factory, reset) -> element = @freeElementsByTagName[tagName]?.pop() - element ?= document.createElement(tagName) - delete element.dataset[dataId] for dataId of element.dataset - element.removeAttribute("class") - element.removeAttribute("style") - element.className = className if className? - element.textContent = textContent - + element ?= factory() + reset(element) @freedElements.delete(element) - element + buildElement: (tagName, className, textContent = "") -> + factory = -> document.createElement(tagName) + reset = (element) -> + delete element.dataset[dataId] for dataId of element.dataset + element.removeAttribute("class") + element.removeAttribute("style") + element.className = className if className? + element.textContent = textContent + @build(tagName, factory, reset) + + buildText: (textContent) -> + factory = -> document.createTextNode(textContent) + reset = (element) -> element.textContent = textContent + @build("#text", factory, reset) + freeElementAndDescendants: (element) -> @free(element) - for index in [element.children.length - 1..0] by -1 - child = element.children[index] + for index in [element.childNodes.length - 1..0] by -1 + child = element.childNodes[index] @freeElementAndDescendants(child) return @@ -34,7 +43,7 @@ class DOMElementPool throw new Error("The element cannot be null or undefined.") unless element? throw new Error("The element has already been freed!") if @freedElements.has(element) - tagName = element.tagName.toLowerCase() + tagName = element.nodeName.toLowerCase() @freeElementsByTagName[tagName] ?= [] @freeElementsByTagName[tagName].push(element) @freedElements.add(element) diff --git a/src/highlights-component.coffee b/src/highlights-component.coffee index 4c5e138d8..70e8d4acc 100644 --- a/src/highlights-component.coffee +++ b/src/highlights-component.coffee @@ -9,7 +9,7 @@ class HighlightsComponent @highlightNodesById = {} @regionNodesByHighlightId = {} - @domNode = @domElementPool.build("div", "highlights") + @domNode = @domElementPool.buildElement("div", "highlights") getDomNode: -> @domNode @@ -29,7 +29,7 @@ class HighlightsComponent # add or update highlights for id, highlightState of newState unless @oldState[id]? - highlightNode = @domElementPool.build("div", "highlight") + highlightNode = @domElementPool.buildElement("div", "highlight") @highlightNodesById[id] = highlightNode @regionNodesByHighlightId[id] = {} @domNode.appendChild(highlightNode) @@ -73,7 +73,7 @@ class HighlightsComponent for newRegionState, i in newHighlightState.regions unless oldHighlightState.regions[i]? oldHighlightState.regions[i] = {} - regionNode = @domElementPool.build("div", "region") + regionNode = @domElementPool.buildElement("div", "region") # This prevents highlights at the tiles boundaries to be hidden by the # subsequent tile. When this happens, subpixel anti-aliasing gets # disabled. diff --git a/src/line-numbers-tile-component.coffee b/src/line-numbers-tile-component.coffee index 19a9868ba..1393d48bf 100644 --- a/src/line-numbers-tile-component.coffee +++ b/src/line-numbers-tile-component.coffee @@ -7,7 +7,7 @@ class LineNumbersTileComponent constructor: ({@id, @domElementPool}) -> @lineNumberNodesById = {} - @domNode = @domElementPool.build("div") + @domNode = @domElementPool.buildElement("div") @domNode.style.position = "absolute" @domNode.style.display = "block" @domNode.style.top = 0 # Cover the space occupied by a dummy lineNumber @@ -99,7 +99,7 @@ class LineNumbersTileComponent {screenRow, bufferRow, softWrapped, top, decorationClasses, zIndex} = lineNumberState className = @buildLineNumberClassName(lineNumberState) - lineNumberNode = @domElementPool.build("div", className) + lineNumberNode = @domElementPool.buildElement("div", className) lineNumberNode.dataset.screenRow = screenRow lineNumberNode.dataset.bufferRow = bufferRow @@ -115,7 +115,7 @@ class LineNumbersTileComponent lineNumber = (bufferRow + 1).toString() padding = _.multiplyString("\u00a0", maxLineNumberDigits - lineNumber.length) - iconRight = @domElementPool.build("div", "icon-right") + iconRight = @domElementPool.buildElement("div", "icon-right") lineNumberNode.textContent = padding + lineNumber lineNumberNode.appendChild(iconRight) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 5b4fc55c5..a4dd40ca1 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -19,7 +19,7 @@ class LinesTileComponent @lineNodesByLineId = {} @screenRowsByLineId = {} @lineIdsByScreenRow = {} - @domNode = @domElementPool.build("div") + @domNode = @domElementPool.buildElement("div") @domNode.style.position = "absolute" @domNode.style.display = "block" @@ -126,7 +126,7 @@ class LinesTileComponent {width} = @newState {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id] - lineNode = @domElementPool.build("div", "line") + lineNode = @domElementPool.buildElement("div", "line") lineNode.dataset.screenRow = screenRow if decorationClasses? @@ -138,7 +138,7 @@ class LinesTileComponent else @setLineInnerNodes(id, lineNode) - lineNode.appendChild(@domElementPool.build("span", "fold-marker")) if fold + lineNode.appendChild(@domElementPool.buildElement("span", "fold-marker")) if fold lineNode setEmptyLineInnerNodes: (id, lineNode) -> @@ -148,11 +148,11 @@ class LinesTileComponent if indentGuidesVisible and indentLevel > 0 invisibleIndex = 0 for i in [0...indentLevel] - indentGuide = @domElementPool.build("span", "indent-guide") + indentGuide = @domElementPool.buildElement("span", "indent-guide") for j in [0...tabLength] if invisible = endOfLineInvisibles?[invisibleIndex++] indentGuide.appendChild( - @domElementPool.build("span", "invisible-character", invisible) + @domElementPool.buildElement("span", "invisible-character", invisible) ) else indentGuide.insertAdjacentText("beforeend", " ") @@ -161,7 +161,7 @@ class LinesTileComponent while invisibleIndex < endOfLineInvisibles?.length invisible = endOfLineInvisibles[invisibleIndex++] lineNode.appendChild( - @domElementPool.build("span", "invisible-character", invisible) + @domElementPool.buildElement("span", "invisible-character", invisible) ) else unless @appendEndOfLineNodes(id, lineNode) @@ -180,7 +180,7 @@ class LinesTileComponent openScopeNode = openScopeNode.parentElement for scope in @tokenIterator.getScopeStarts() - newScopeNode = @domElementPool.build("span", scope.replace(/\.+/g, ' ')) + newScopeNode = @domElementPool.buildElement("span", scope.replace(/\.+/g, ' ')) openScopeNode.appendChild(newScopeNode) openScopeNode = newScopeNode @@ -213,7 +213,7 @@ class LinesTileComponent appendTokenNodes: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, scopeNode) -> if isHardTab - hardTabNode = @domElementPool.build("span", "hard-tab", tokenText) + hardTabNode = @domElementPool.buildElement("span", "hard-tab", tokenText) hardTabNode.classList.add("leading-whitespace") if firstNonWhitespaceIndex? hardTabNode.classList.add("trailing-whitespace") if firstTrailingWhitespaceIndex? hardTabNode.classList.add("indent-guide") if hasIndentGuide @@ -228,7 +228,7 @@ class LinesTileComponent trailingWhitespaceNode = null if firstNonWhitespaceIndex? - leadingWhitespaceNode = @domElementPool.build( + leadingWhitespaceNode = @domElementPool.buildElement( "span", "leading-whitespace", tokenText.substring(0, firstNonWhitespaceIndex) @@ -241,7 +241,7 @@ class LinesTileComponent if firstTrailingWhitespaceIndex? tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0 - trailingWhitespaceNode = @domElementPool.build( + trailingWhitespaceNode = @domElementPool.buildElement( "span", "trailing-whitespace", tokenText.substring(firstTrailingWhitespaceIndex) @@ -256,7 +256,7 @@ class LinesTileComponent if tokenText.length > MaxTokenLength while startIndex < endIndex text = @sliceText(tokenText, startIndex, startIndex + MaxTokenLength) - scopeNode.appendChild(@domElementPool.build("span", null, text)) + scopeNode.appendChild(@domElementPool.buildElement("span", null, text)) startIndex += MaxTokenLength else scopeNode.insertAdjacentText("beforeend", @sliceText(tokenText, startIndex, endIndex)) @@ -276,7 +276,7 @@ class LinesTileComponent for invisible in endOfLineInvisibles hasInvisibles = true lineNode.appendChild( - @domElementPool.build("span", "invisible-character", invisible) + @domElementPool.buildElement("span", "invisible-character", invisible) ) hasInvisibles From c1e56322aa57d9a1c11a685a842ac653db08765a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Oct 2015 10:08:06 +0200 Subject: [PATCH 73/80] Recycle text nodes in line numbers --- src/dom-element-pool.coffee | 9 ++++++--- src/line-numbers-tile-component.coffee | 7 +++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/dom-element-pool.coffee b/src/dom-element-pool.coffee index 9cdf1f7f6..46a2d3fa3 100644 --- a/src/dom-element-pool.coffee +++ b/src/dom-element-pool.coffee @@ -34,9 +34,12 @@ class DOMElementPool freeElementAndDescendants: (element) -> @free(element) - for index in [element.childNodes.length - 1..0] by -1 - child = element.childNodes[index] - @freeElementAndDescendants(child) + @freeDescendants(element) + + freeDescendants: (element) -> + for descendant in element.childNodes by -1 + @free(descendant) + @freeDescendants(descendant) return free: (element) -> diff --git a/src/line-numbers-tile-component.coffee b/src/line-numbers-tile-component.coffee index 1393d48bf..32dbca0a2 100644 --- a/src/line-numbers-tile-component.coffee +++ b/src/line-numbers-tile-component.coffee @@ -107,17 +107,20 @@ class LineNumbersTileComponent lineNumberNode setLineNumberInnerNodes: (bufferRow, softWrapped, lineNumberNode) -> + @domElementPool.freeDescendants(lineNumberNode) + {maxLineNumberDigits} = @newState if softWrapped lineNumber = "•" else lineNumber = (bufferRow + 1).toString() - padding = _.multiplyString("\u00a0", maxLineNumberDigits - lineNumber.length) + + textNode = @domElementPool.buildText(padding + lineNumber) iconRight = @domElementPool.buildElement("div", "icon-right") - lineNumberNode.textContent = padding + lineNumber + lineNumberNode.appendChild(textNode) lineNumberNode.appendChild(iconRight) updateLineNumberNode: (lineNumberId, newLineNumberState) -> From dede68011f0cda7a57390c92087546f810e32586 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Oct 2015 10:18:05 +0200 Subject: [PATCH 74/80] Recycle text nodes in lines --- src/lines-tile-component.coffee | 56 ++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index a4dd40ca1..30700825c 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -151,21 +151,21 @@ class LinesTileComponent indentGuide = @domElementPool.buildElement("span", "indent-guide") for j in [0...tabLength] if invisible = endOfLineInvisibles?[invisibleIndex++] - indentGuide.appendChild( - @domElementPool.buildElement("span", "invisible-character", invisible) - ) + invisibleSpan = @domElementPool.buildElement("span", "invisible-character") + invisibleSpan.appendChild(@domElementPool.buildText(invisible)) + indentGuide.appendChild(invisibleSpan) else - indentGuide.insertAdjacentText("beforeend", " ") + indentGuide.appendChild(@domElementPool.buildText(" ")) lineNode.appendChild(indentGuide) while invisibleIndex < endOfLineInvisibles?.length invisible = endOfLineInvisibles[invisibleIndex++] - lineNode.appendChild( - @domElementPool.buildElement("span", "invisible-character", invisible) - ) + invisibleSpan = @domElementPool.buildElement("span", "invisible-character") + invisibleSpan.appendChild(@domElementPool.buildText(invisible)) + lineNode.appendChild(invisibleSpan) else unless @appendEndOfLineNodes(id, lineNode) - lineNode.textContent = "\u00a0" + lineNode.appendChild(@domElementPool.buildText("\u00a0")) setLineInnerNodes: (id, lineNode) -> lineState = @newTileState.lines[id] @@ -213,11 +213,12 @@ class LinesTileComponent appendTokenNodes: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, scopeNode) -> if isHardTab - hardTabNode = @domElementPool.buildElement("span", "hard-tab", tokenText) + hardTabNode = @domElementPool.buildElement("span", "hard-tab") hardTabNode.classList.add("leading-whitespace") if firstNonWhitespaceIndex? hardTabNode.classList.add("trailing-whitespace") if firstTrailingWhitespaceIndex? hardTabNode.classList.add("indent-guide") if hasIndentGuide hardTabNode.classList.add("invisible-character") if hasInvisibleCharacters + hardTabNode.appendChild(@domElementPool.buildText(tokenText)) scopeNode.appendChild(hardTabNode) else @@ -228,26 +229,24 @@ class LinesTileComponent trailingWhitespaceNode = null if firstNonWhitespaceIndex? - leadingWhitespaceNode = @domElementPool.buildElement( - "span", - "leading-whitespace", - tokenText.substring(0, firstNonWhitespaceIndex) - ) + leadingWhitespaceNode = @domElementPool.buildElement("span", "leading-whitespace") leadingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide leadingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters + leadingWhitespaceNode.appendChild( + @domElementPool.buildText(tokenText.substring(0, firstNonWhitespaceIndex)) + ) startIndex = firstNonWhitespaceIndex if firstTrailingWhitespaceIndex? tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0 - trailingWhitespaceNode = @domElementPool.buildElement( - "span", - "trailing-whitespace", - tokenText.substring(firstTrailingWhitespaceIndex) - ) + trailingWhitespaceNode = @domElementPool.buildElement("span", "trailing-whitespace") trailingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace trailingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters + trailingWhitespaceNode.appendChild( + @domElementPool.buildText(tokenText.substring(firstTrailingWhitespaceIndex)) + ) endIndex = firstTrailingWhitespaceIndex @@ -255,11 +254,18 @@ class LinesTileComponent if tokenText.length > MaxTokenLength while startIndex < endIndex - text = @sliceText(tokenText, startIndex, startIndex + MaxTokenLength) - scopeNode.appendChild(@domElementPool.buildElement("span", null, text)) + textNode = @domElementPool.buildText( + @sliceText(tokenText, startIndex, startIndex + MaxTokenLength) + ) + textSpan = @domElementPool.buildElement("span") + + textSpan.appendChild(textNode) + scopeNode.appendChild(textSpan) startIndex += MaxTokenLength else - scopeNode.insertAdjacentText("beforeend", @sliceText(tokenText, startIndex, endIndex)) + scopeNode.appendChild( + @domElementPool.buildText(@sliceText(tokenText, startIndex, endIndex)) + ) scopeNode.appendChild(trailingWhitespaceNode) if trailingWhitespaceNode? @@ -275,9 +281,9 @@ class LinesTileComponent if endOfLineInvisibles? for invisible in endOfLineInvisibles hasInvisibles = true - lineNode.appendChild( - @domElementPool.buildElement("span", "invisible-character", invisible) - ) + invisibleSpan = @domElementPool.buildElement("span", "invisible-character") + invisibleSpan.appendChild(@domElementPool.buildText(invisible)) + lineNode.appendChild(invisibleSpan) hasInvisibles From cc1b42b27906007de669d6a65c6f0e154e84d52e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Oct 2015 10:21:13 +0200 Subject: [PATCH 75/80] Faster DOM removal --- src/dom-element-pool.coffee | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/dom-element-pool.coffee b/src/dom-element-pool.coffee index 46a2d3fa3..f81a537f3 100644 --- a/src/dom-element-pool.coffee +++ b/src/dom-element-pool.coffee @@ -17,14 +17,15 @@ class DOMElementPool @freedElements.delete(element) element - buildElement: (tagName, className, textContent = "") -> + buildElement: (tagName, className) -> factory = -> document.createElement(tagName) reset = (element) -> delete element.dataset[dataId] for dataId of element.dataset - element.removeAttribute("class") element.removeAttribute("style") - element.className = className if className? - element.textContent = textContent + if className? + element.className = className + else + element.removeAttribute("class") @build(tagName, factory, reset) buildText: (textContent) -> From 5529645ff3677885ce85f6fb17ce1cd02b3baca0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Oct 2015 10:26:49 +0200 Subject: [PATCH 76/80] Recycle highlights --- src/highlights-component.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/highlights-component.coffee b/src/highlights-component.coffee index 70e8d4acc..a6e85b7e5 100644 --- a/src/highlights-component.coffee +++ b/src/highlights-component.coffee @@ -21,7 +21,7 @@ class HighlightsComponent # remove highlights for id of @oldState unless newState[id]? - @highlightNodesById[id].remove() + @domElementPool.freeElementAndDescendants(@highlightNodesById[id]) delete @highlightNodesById[id] delete @regionNodesByHighlightId[id] delete @oldState[id] @@ -66,7 +66,7 @@ class HighlightsComponent # remove regions while oldHighlightState.regions.length > newHighlightState.regions.length oldHighlightState.regions.pop() - @regionNodesByHighlightId[id][oldHighlightState.regions.length].remove() + @domElementPool.freeElementAndDescendants(@regionNodesByHighlightId[id][oldHighlightState.regions.length]) delete @regionNodesByHighlightId[id][oldHighlightState.regions.length] # add or update regions From 175c21f47e94bb4605b27b6567180a13a5ad40d4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Oct 2015 10:35:01 +0200 Subject: [PATCH 77/80] Cache built text nodes --- src/lines-tile-component.coffee | 65 ++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee index 30700825c..619acc56a 100644 --- a/src/lines-tile-component.coffee +++ b/src/lines-tile-component.coffee @@ -19,6 +19,7 @@ class LinesTileComponent @lineNodesByLineId = {} @screenRowsByLineId = {} @lineIdsByScreenRow = {} + @textNodesByLineId = {} @domNode = @domElementPool.buildElement("div") @domNode.style.position = "absolute" @domNode.style.display = "block" @@ -80,6 +81,7 @@ class LinesTileComponent removeLineNode: (id) -> @domElementPool.freeElementAndDescendants(@lineNodesByLineId[id]) delete @lineNodesByLineId[id] + delete @textNodesByLineId[id] delete @lineIdsByScreenRow[@screenRowsByLineId[id]] delete @screenRowsByLineId[id] delete @oldTileState.lines[id] @@ -133,10 +135,12 @@ class LinesTileComponent for decorationClass in decorationClasses lineNode.classList.add(decorationClass) + @currentLineTextNodes = [] if text is "" @setEmptyLineInnerNodes(id, lineNode) else @setLineInnerNodes(id, lineNode) + @textNodesByLineId[id] = @currentLineTextNodes lineNode.appendChild(@domElementPool.buildElement("span", "fold-marker")) if fold lineNode @@ -152,20 +156,32 @@ class LinesTileComponent for j in [0...tabLength] if invisible = endOfLineInvisibles?[invisibleIndex++] invisibleSpan = @domElementPool.buildElement("span", "invisible-character") - invisibleSpan.appendChild(@domElementPool.buildText(invisible)) + textNode = @domElementPool.buildText(invisible) + invisibleSpan.appendChild(textNode) indentGuide.appendChild(invisibleSpan) + + @currentLineTextNodes.push(textNode) else - indentGuide.appendChild(@domElementPool.buildText(" ")) + textNode = @domElementPool.buildText(" ") + indentGuide.appendChild(textNode) + + @currentLineTextNodes.push(textNode) lineNode.appendChild(indentGuide) while invisibleIndex < endOfLineInvisibles?.length invisible = endOfLineInvisibles[invisibleIndex++] invisibleSpan = @domElementPool.buildElement("span", "invisible-character") - invisibleSpan.appendChild(@domElementPool.buildText(invisible)) + textNode = @domElementPool.buildText(invisible) + invisibleSpan.appendChild(textNode) lineNode.appendChild(invisibleSpan) + + @currentLineTextNodes.push(textNode) else unless @appendEndOfLineNodes(id, lineNode) - lineNode.appendChild(@domElementPool.buildText("\u00a0")) + textNode = @domElementPool.buildText("\u00a0") + lineNode.appendChild(textNode) + + @currentLineTextNodes.push(textNode) setLineInnerNodes: (id, lineNode) -> lineState = @newTileState.lines[id] @@ -213,44 +229,50 @@ class LinesTileComponent appendTokenNodes: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, scopeNode) -> if isHardTab + textNode = @domElementPool.buildText(tokenText) hardTabNode = @domElementPool.buildElement("span", "hard-tab") hardTabNode.classList.add("leading-whitespace") if firstNonWhitespaceIndex? hardTabNode.classList.add("trailing-whitespace") if firstTrailingWhitespaceIndex? hardTabNode.classList.add("indent-guide") if hasIndentGuide hardTabNode.classList.add("invisible-character") if hasInvisibleCharacters - hardTabNode.appendChild(@domElementPool.buildText(tokenText)) + hardTabNode.appendChild(textNode) scopeNode.appendChild(hardTabNode) + @currentLineTextNodes.push(textNode) else startIndex = 0 endIndex = tokenText.length leadingWhitespaceNode = null + leadingWhitespaceTextNode = null trailingWhitespaceNode = null + trailingWhitespaceTextNode = null if firstNonWhitespaceIndex? + leadingWhitespaceTextNode = + @domElementPool.buildText(tokenText.substring(0, firstNonWhitespaceIndex)) leadingWhitespaceNode = @domElementPool.buildElement("span", "leading-whitespace") leadingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide leadingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters - leadingWhitespaceNode.appendChild( - @domElementPool.buildText(tokenText.substring(0, firstNonWhitespaceIndex)) - ) + leadingWhitespaceNode.appendChild(leadingWhitespaceTextNode) startIndex = firstNonWhitespaceIndex if firstTrailingWhitespaceIndex? tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0 + trailingWhitespaceTextNode = + @domElementPool.buildText(tokenText.substring(firstTrailingWhitespaceIndex)) trailingWhitespaceNode = @domElementPool.buildElement("span", "trailing-whitespace") trailingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace trailingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters - trailingWhitespaceNode.appendChild( - @domElementPool.buildText(tokenText.substring(firstTrailingWhitespaceIndex)) - ) + trailingWhitespaceNode.appendChild(trailingWhitespaceTextNode) endIndex = firstTrailingWhitespaceIndex - scopeNode.appendChild(leadingWhitespaceNode) if leadingWhitespaceNode? + if leadingWhitespaceNode? + scopeNode.appendChild(leadingWhitespaceNode) + @currentLineTextNodes.push(leadingWhitespaceTextNode) if tokenText.length > MaxTokenLength while startIndex < endIndex @@ -262,12 +284,15 @@ class LinesTileComponent textSpan.appendChild(textNode) scopeNode.appendChild(textSpan) startIndex += MaxTokenLength + @currentLineTextNodes.push(textNode) else - scopeNode.appendChild( - @domElementPool.buildText(@sliceText(tokenText, startIndex, endIndex)) - ) + textNode = @domElementPool.buildText(@sliceText(tokenText, startIndex, endIndex)) + scopeNode.appendChild(textNode) + @currentLineTextNodes.push(textNode) - scopeNode.appendChild(trailingWhitespaceNode) if trailingWhitespaceNode? + if trailingWhitespaceNode? + scopeNode.appendChild(trailingWhitespaceNode) + @currentLineTextNodes.push(trailingWhitespaceTextNode) sliceText: (tokenText, startIndex, endIndex) -> if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length @@ -282,9 +307,12 @@ class LinesTileComponent for invisible in endOfLineInvisibles hasInvisibles = true invisibleSpan = @domElementPool.buildElement("span", "invisible-character") - invisibleSpan.appendChild(@domElementPool.buildText(invisible)) + textNode = @domElementPool.buildText(invisible) + invisibleSpan.appendChild(textNode) lineNode.appendChild(invisibleSpan) + @currentLineTextNodes.push(textNode) + hasInvisibles updateLineNode: (id) -> @@ -319,3 +347,6 @@ class LinesTileComponent lineNodeForLineId: (lineId) -> @lineNodesByLineId[lineId] + + textNodesForLineId: (lineId) -> + @textNodesByLineId[lineId].slice() From 61892f932b2d8d7235f2f348e365395a33e29a2c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Oct 2015 10:43:32 +0200 Subject: [PATCH 78/80] Use cached text nodes instead of NodeIterator --- spec/lines-yardstick-spec.coffee | 8 ++++++++ src/lines-component.coffee | 4 ++++ src/lines-yardstick.coffee | 12 ++++++------ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee index c87f3f9df..6320cc99f 100644 --- a/spec/lines-yardstick-spec.coffee +++ b/spec/lines-yardstick-spec.coffee @@ -1,4 +1,5 @@ LinesYardstick = require "../src/lines-yardstick" +{toArray} = require 'underscore-plus' describe "LinesYardstick", -> [editor, mockPresenter, mockLineNodesProvider, createdLineNodes, linesYardstick] = [] @@ -46,6 +47,13 @@ describe "LinesYardstick", -> return if availableScreenRows[lineId] isnt screenRow buildLineNode(screenRow) + textNodesForLineIdAndScreenRow: (lineId, screenRow) -> + lineNode = @lineNodeForLineIdAndScreenRow(lineId, screenRow) + textNodes = [] + for span in lineNode.children + for textNode in span.childNodes + textNodes.push(textNode) + textNodes editor.setLineHeightInPixels(14) linesYardstick = new LinesYardstick(editor, mockPresenter, mockLineNodesProvider) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index b4bbed602..2a721a0aa 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -86,3 +86,7 @@ class LinesComponent extends TiledComponent lineNodeForLineIdAndScreenRow: (lineId, screenRow) -> tile = @presenter.tileForRow(screenRow) @getComponentForTile(tile)?.lineNodeForLineId(lineId) + + textNodesForLineIdAndScreenRow: (lineId, screenRow) -> + tile = @presenter.tileForRow(screenRow) + @getComponentForTile(tile)?.textNodesForLineId(lineId) diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee index d0d5f7cfc..0dc3e7602 100644 --- a/src/lines-yardstick.coffee +++ b/src/lines-yardstick.coffee @@ -35,7 +35,7 @@ class LinesYardstick return new Point(row, 0) unless lineNode? and line? - iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT) + textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row) column = 0 previousColumn = 0 previousLeft = 0 @@ -55,13 +55,13 @@ class LinesYardstick textIndex++ unless textNode? - textNode = iterator.nextNode() + textNode = textNodes.shift() textNodeLength = textNode.textContent.length textNodeIndex = 0 nextTextNodeIndex = textNodeLength while nextTextNodeIndex <= column - textNode = iterator.nextNode() + textNode = textNodes.shift() textNodeLength = textNode.textContent.length textNodeIndex = nextTextNodeIndex nextTextNodeIndex = textNodeIndex + textNodeLength @@ -108,8 +108,8 @@ class LinesYardstick if cachedPosition = @pixelPositionsByLineIdAndColumn[line.id]?[column] return cachedPosition + textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row) indexWithinTextNode = null - iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT) charIndex = 0 @tokenIterator.reset(line) @@ -130,13 +130,13 @@ class LinesYardstick textIndex++ unless textNode? - textNode = iterator.nextNode() + textNode = textNodes.shift() textNodeLength = textNode.textContent.length textNodeIndex = 0 nextTextNodeIndex = textNodeLength while nextTextNodeIndex <= charIndex - textNode = iterator.nextNode() + textNode = textNodes.shift() textNodeLength = textNode.textContent.length textNodeIndex = nextTextNodeIndex nextTextNodeIndex = textNodeIndex + textNodeLength From e8387e0095ff1a45875caf0c87e3a730a54602de Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Oct 2015 11:00:50 +0200 Subject: [PATCH 79/80] :racehorse: --- src/text-editor-presenter.coffee | 37 +++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index a3ce1b370..e7287fd7e 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -657,18 +657,35 @@ class TextEditorPresenter tileState.lineNumbers ?= {} visibleLineNumberIds = {} - for screenRow in screenRows - line = @model.tokenizedLineForScreenRow(screenRow) - bufferRow = @model.bufferRowForScreenRow(screenRow) - softWrapped = line.softWrapped - decorationClasses = @lineNumberDecorationClassesForRow(screenRow) - foldable = @model.isFoldableAtScreenRow(screenRow) + startRow = screenRows[screenRows.length - 1] + endRow = Math.min(screenRows[0] + 1, @model.getScreenLineCount()) - tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable} - visibleLineNumberIds[line.id] = true + if startRow > 0 + rowBeforeStartRow = startRow - 1 + lastBufferRow = @model.bufferRowForScreenRow(rowBeforeStartRow) + else + lastBufferRow = null - for lineId of tileState.lineNumbers - delete tileState.lineNumbers[lineId] unless visibleLineNumberIds[lineId] + if endRow > startRow + bufferRows = @model.bufferRowsForScreenRows(startRow, endRow - 1) + for bufferRow, i in bufferRows + if bufferRow is lastBufferRow + softWrapped = true + else + lastBufferRow = bufferRow + softWrapped = false + + screenRow = startRow + i + line = @model.tokenizedLineForScreenRow(screenRow) + softWrapped = line.softWrapped + decorationClasses = @lineNumberDecorationClassesForRow(screenRow) + foldable = @model.isFoldableAtScreenRow(screenRow) + + tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable} + visibleLineNumberIds[line.id] = true + + for id of tileState.lineNumbers + delete tileState.lineNumbers[id] unless visibleLineNumberIds[id] return From 428f0db75b4c0134efe2d553c09a0ee5d393c3d9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 6 Oct 2015 11:02:36 +0200 Subject: [PATCH 80/80] :fire: --- spec/display-buffer-spec.coffee | 10 ---------- src/text-editor-presenter.coffee | 1 - src/tokenized-line.coffee | 3 --- 3 files changed, 14 deletions(-) diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee index c0c7b608a..7ed97f0d0 100644 --- a/spec/display-buffer-spec.coffee +++ b/spec/display-buffer-spec.coffee @@ -143,16 +143,6 @@ describe "DisplayBuffer", -> expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[1].isHardTab).toBeTruthy() describe "when a line is wrapped", -> - it "marks it as soft-wrapped", -> - displayBuffer.setEditorWidthInChars(7) - - expect(displayBuffer.tokenizedLineForScreenRow(0).softWrapped).toBeFalsy() - expect(displayBuffer.tokenizedLineForScreenRow(1).softWrapped).toBeTruthy() - expect(displayBuffer.tokenizedLineForScreenRow(2).softWrapped).toBeTruthy() - expect(displayBuffer.tokenizedLineForScreenRow(3).softWrapped).toBeTruthy() - expect(displayBuffer.tokenizedLineForScreenRow(4).softWrapped).toBeTruthy() - expect(displayBuffer.tokenizedLineForScreenRow(5).softWrapped).toBeFalsy() - it "breaks soft-wrap indentation into a token for each indentation level to support indent guides", -> tokenizedLine = displayBuffer.tokenizedLineForScreenRow(4) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index e7287fd7e..4f572a362 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -677,7 +677,6 @@ class TextEditorPresenter screenRow = startRow + i line = @model.tokenizedLineForScreenRow(screenRow) - softWrapped = line.softWrapped decorationClasses = @lineNumberDecorationClassesForRow(screenRow) foldable = @model.isFoldableAtScreenRow(screenRow) diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index 320703a9b..bf234b6d3 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -34,7 +34,6 @@ class TokenizedLine lineIsWhitespaceOnly: false firstNonWhitespaceIndex: 0 foldable: false - softWrapped: false constructor: (properties) -> @id = idCounter++ @@ -421,7 +420,6 @@ class TokenizedLine leftFragment.tabLength = @tabLength leftFragment.firstNonWhitespaceIndex = Math.min(column, @firstNonWhitespaceIndex) leftFragment.firstTrailingWhitespaceIndex = Math.min(column, @firstTrailingWhitespaceIndex) - leftFragment.softWrapped = @softWrapped rightFragment = new TokenizedLine rightFragment.tokenIterator = @tokenIterator @@ -439,7 +437,6 @@ class TokenizedLine rightFragment.endOfLineInvisibles = @endOfLineInvisibles rightFragment.firstNonWhitespaceIndex = Math.max(softWrapIndent, @firstNonWhitespaceIndex - column + softWrapIndent) rightFragment.firstTrailingWhitespaceIndex = Math.max(softWrapIndent, @firstTrailingWhitespaceIndex - column + softWrapIndent) - rightFragment.softWrapped = true [leftFragment, rightFragment]