From 49577313e47285282ff64ca3a8783abf29b010b3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 29 Sep 2015 10:25:54 +0200 Subject: [PATCH] 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