From 55a7508287407671ec8350cecc7f06554e52a216 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 May 2015 11:38:54 +0200 Subject: [PATCH 01/58] wip --- src/lines-presenter.coffee | 41 ++++++++++++++++++++++++++++++++ src/text-editor-presenter.coffee | 30 +++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/lines-presenter.coffee diff --git a/src/lines-presenter.coffee b/src/lines-presenter.coffee new file mode 100644 index 000000000..c1e3676ba --- /dev/null +++ b/src/lines-presenter.coffee @@ -0,0 +1,41 @@ +module.exports = +class LinesPresenter + startRow: null + endRow: null + lineHeight: null + + constructor: (@presenter) -> + @lines = {} + + getState: -> + visibleLineIds = {} + row = @startRow + while row < @endRow + line = @presenter.model.tokenizedLineForScreenRow(row) + unless line? + throw new Error("No line exists for row #{row}. Last screen row: #{@model.getLastScreenRow()}") + + visibleLineIds[line.id] = true + if @lines.hasOwnProperty(line.id) + lineState = @lines[line.id] + lineState.screenRow = row + lineState.top = (row - @startRow) * @lineHeight + lineState.decorationClasses = @presenter.lineDecorationClassesForRow(row) + else + @lines[line.id] = + screenRow: row + text: line.text + tokens: line.tokens + isOnlyWhitespace: line.isOnlyWhitespace() + endOfLineInvisibles: line.endOfLineInvisibles + indentLevel: line.indentLevel + tabLength: line.tabLength + fold: line.fold + top: (row - @startRow) * @lineHeight + decorationClasses: @presenter.lineDecorationClassesForRow(row) + row++ + + for id, line of @lines + delete @lines[id] unless visibleLineIds.hasOwnProperty(id) + + @lines diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index ac6d0c363..e9cba9dec 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -2,6 +2,7 @@ {Point, Range} = require 'text-buffer' _ = require 'underscore-plus' Decoration = require './decoration' +LinesPresenter = require './lines-presenter' module.exports = class TextEditorPresenter @@ -20,6 +21,8 @@ class TextEditorPresenter @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @measuredVerticalScrollbarWidth = verticalScrollbarWidth @gutterWidth ?= 0 + @tileCount ?= 3 + @linesPresentersByTileIndex = {} @disposables = new CompositeDisposable @emitter = new Emitter @@ -74,6 +77,7 @@ class TextEditorPresenter @updateHiddenInputState() if @shouldUpdateHiddenInputState @updateContentState() if @shouldUpdateContentState @updateDecorations() if @shouldUpdateDecorations + @updateTilesState() if @shouldUpdateLinesState @updateLinesState() if @shouldUpdateLinesState @updateCursorsState() if @shouldUpdateCursorsState @updateOverlaysState() if @shouldUpdateOverlaysState @@ -205,6 +209,7 @@ class TextEditorPresenter content: scrollingVertically: false cursorsVisible: false + tiles: {} lines: {} highlights: {} overlays: {} @@ -229,6 +234,7 @@ class TextEditorPresenter @updateHiddenInputState() @updateContentState() @updateDecorations() + @updateTilesState() @updateLinesState() @updateCursorsState() @updateOverlaysState() @@ -298,6 +304,30 @@ class TextEditorPresenter @state.content.backgroundColor = if @model.isMini() then null else @backgroundColor @state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null + updateTilesState: -> + return unless @startRow? and @endRow? and @lineHeight? + + linesPerTile = @height / @lineHeight / @tileCount + + startIndex = Math.floor(@startRow / linesPerTile) + endIndex = Math.ceil(@endRow / linesPerTile) + visibleIndexesRange = [startIndex..endIndex] + + for index, tile of @state.content.tiles + unless index in visibleIndexesRange + delete @state.content.tiles[index] + delete @linesPresentersByTileIndex[index] + + for index in visibleIndexesRange + presenter = @linesPresentersByTileIndex[index] ?= new LinesPresenter(@) + presenter.startRow = Math.floor(index * linesPerTile) + presenter.endRow = Math.ceil(Math.min(@endRow, (index + 1) * linesPerTile)) + presenter.lineHeight = @lineHeight + + tile = @state.content.tiles[index] ?= {} + tile.top = (index * linesPerTile * @lineHeight) - @scrollTop + tile.lines = presenter.getState() + updateLinesState: -> return unless @startRow? and @endRow? and @lineHeight? From ee85abc2ebc21bf46f730d0fdd278099b974405b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 May 2015 11:41:09 +0200 Subject: [PATCH 02/58] :fire: Delete line handling from TextEditorPresenter --- src/text-editor-presenter.coffee | 76 ++++++-------------------------- 1 file changed, 14 insertions(+), 62 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index e9cba9dec..835a89182 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -77,8 +77,7 @@ class TextEditorPresenter @updateHiddenInputState() if @shouldUpdateHiddenInputState @updateContentState() if @shouldUpdateContentState @updateDecorations() if @shouldUpdateDecorations - @updateTilesState() if @shouldUpdateLinesState - @updateLinesState() if @shouldUpdateLinesState + @updateTilesState() if @shouldUpdateTilesState @updateCursorsState() if @shouldUpdateCursorsState @updateOverlaysState() if @shouldUpdateOverlaysState @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState @@ -100,7 +99,7 @@ class TextEditorPresenter @shouldUpdateHiddenInputState = false @shouldUpdateContentState = false @shouldUpdateDecorations = false - @shouldUpdateLinesState = false + @shouldUpdateTilesState = false @shouldUpdateCursorsState = false @shouldUpdateOverlaysState = false @shouldUpdateLineNumberGutterState = false @@ -118,7 +117,7 @@ class TextEditorPresenter @shouldUpdateScrollbarsState = true @shouldUpdateContentState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateLineNumberGutterState = true @shouldUpdateLineNumbersState = true @shouldUpdateGutterOrderState = true @@ -134,7 +133,7 @@ class TextEditorPresenter @shouldUpdateScrollbarsState = true @shouldUpdateContentState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateLineNumberGutterState = true @shouldUpdateLineNumbersState = true @shouldUpdateGutterOrderState = true @@ -210,7 +209,6 @@ class TextEditorPresenter scrollingVertically: false cursorsVisible: false tiles: {} - lines: {} highlights: {} overlays: {} gutters: @@ -235,7 +233,6 @@ class TextEditorPresenter @updateContentState() @updateDecorations() @updateTilesState() - @updateLinesState() @updateCursorsState() @updateOverlaysState() @updateLineNumberGutterState() @@ -328,51 +325,6 @@ class TextEditorPresenter tile.top = (index * linesPerTile * @lineHeight) - @scrollTop tile.lines = presenter.getState() - updateLinesState: -> - return unless @startRow? and @endRow? and @lineHeight? - - visibleLineIds = {} - row = @startRow - while row < @endRow - line = @model.tokenizedLineForScreenRow(row) - unless line? - throw new Error("No line exists for row #{row}. Last screen row: #{@model.getLastScreenRow()}") - - visibleLineIds[line.id] = true - if @state.content.lines.hasOwnProperty(line.id) - @updateLineState(row, line) - else - @buildLineState(row, line) - row++ - - if @mouseWheelScreenRow? - if preservedLine = @model.tokenizedLineForScreenRow(@mouseWheelScreenRow) - visibleLineIds[preservedLine.id] = true - - for id, line of @state.content.lines - unless visibleLineIds.hasOwnProperty(id) - delete @state.content.lines[id] - return - - updateLineState: (row, line) -> - lineState = @state.content.lines[line.id] - lineState.screenRow = row - lineState.top = row * @lineHeight - lineState.decorationClasses = @lineDecorationClassesForRow(row) - - buildLineState: (row, line) -> - @state.content.lines[line.id] = - screenRow: row - text: line.text - tokens: line.tokens - isOnlyWhitespace: line.isOnlyWhitespace() - endOfLineInvisibles: line.endOfLineInvisibles - indentLevel: line.indentLevel - tabLength: line.tabLength - fold: line.fold - top: row * @lineHeight - decorationClasses: @lineDecorationClassesForRow(row) - updateCursorsState: -> @state.content.cursors = {} @updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation @@ -748,7 +700,7 @@ class TextEditorPresenter @shouldUpdateVerticalScrollState = true @shouldUpdateHiddenInputState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateCursorsState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true @@ -771,7 +723,7 @@ class TextEditorPresenter @state.content.scrollingVertically = false if @mouseWheelScreenRow? @mouseWheelScreenRow = null - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true @@ -834,7 +786,7 @@ class TextEditorPresenter @shouldUpdateVerticalScrollState = true @shouldUpdateScrollbarsState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateCursorsState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true @@ -862,7 +814,7 @@ class TextEditorPresenter @shouldUpdateScrollbarsState = true @shouldUpdateContentState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateCursorsState = true unless oldContentFrameWidth? @emitDidUpdateState() @@ -928,7 +880,7 @@ class TextEditorPresenter @shouldUpdateScrollbarsState = true @shouldUpdateHiddenInputState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateCursorsState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true @@ -980,7 +932,7 @@ class TextEditorPresenter @shouldUpdateHiddenInputState = true @shouldUpdateContentState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateCursorsState = true @shouldUpdateOverlaysState = true @@ -1072,7 +1024,7 @@ class TextEditorPresenter intersectsVisibleRowRange = true if intersectsVisibleRowRange - @shouldUpdateLinesState = true if decoration.isType('line') + @shouldUpdateTilesState = true if decoration.isType('line') if decoration.isType('line-number') @shouldUpdateLineNumbersState = true else if decoration.isType('gutter') @@ -1097,7 +1049,7 @@ class TextEditorPresenter decoration.getMarker().getScreenRange()) @addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange()) if decoration.isType('line') or Decoration.isType(oldProperties, 'line') - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true if decoration.isType('line-number') or Decoration.isType(oldProperties, 'line-number') @shouldUpdateLineNumbersState = true if (decoration.isType('gutter') and not decoration.isType('line-number')) or @@ -1113,7 +1065,7 @@ class TextEditorPresenter didDestroyDecoration: (decoration) -> if decoration.isType('line') or decoration.isType('gutter') @removeFromLineDecorationCaches(decoration, decoration.getMarker().getScreenRange()) - @shouldUpdateLinesState = true if decoration.isType('line') + @shouldUpdateTilesState = true if decoration.isType('line') if decoration.isType('line-number') @shouldUpdateLineNumbersState = true else if decoration.isType('gutter') @@ -1138,7 +1090,7 @@ class TextEditorPresenter if decoration.isType('line') or decoration.isType('gutter') @addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange()) - @shouldUpdateLinesState = true if decoration.isType('line') + @shouldUpdateTilesState = true if decoration.isType('line') if decoration.isType('line-number') @shouldUpdateLineNumbersState = true else if decoration.isType('gutter') From 3d3d5d00b4a476eea27800b6be94d6969b1775c7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 May 2015 14:24:58 +0200 Subject: [PATCH 03/58] wip --- spec/text-editor-presenter-spec.coffee | 159 +++++++++++-------------- src/text-editor-presenter.coffee | 22 ++-- 2 files changed, 82 insertions(+), 99 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index de2dd780d..b2c40fc36 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -657,123 +657,96 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> editor.setPlaceholderText("new-placeholder-text") expect(presenter.getState().content.placeholderText).toBe "new-placeholder-text" - describe ".lines", -> - lineStateForScreenRow = (presenter, screenRow) -> - presenter.getState().content.lines[presenter.model.tokenizedLineForScreenRow(screenRow).id] + fdescribe ".tiles", -> + it "contains states for tiles that are visible on screen, plus and minus the overdraw margin", -> + presenter = buildPresenter(explicitHeight: 3, scrollTop: 1, lineHeight: 1, tileCount: 3, tileOverdrawMargin: 1) - it "contains states for lines that are visible on screen, plus and minus the overdraw margin", -> - presenter = buildPresenter(explicitHeight: 15, scrollTop: 50, lineHeight: 10, lineOverdrawMargin: 1) - - expect(lineStateForScreenRow(presenter, 3)).toBeUndefined() - - line4 = editor.tokenizedLineForScreenRow(4) - expectValues lineStateForScreenRow(presenter, 4), { - screenRow: 4 - text: line4.text - tokens: line4.tokens - top: 10 * 4 + expectValues presenter.getState().content.tiles[0], { + top: -1 } - - line5 = editor.tokenizedLineForScreenRow(5) - expectValues lineStateForScreenRow(presenter, 5), { - screenRow: 5 - text: line5.text - tokens: line5.tokens - top: 10 * 5 + expectValues presenter.getState().content.tiles[1], { + top: 0 } - - line6 = editor.tokenizedLineForScreenRow(6) - expectValues lineStateForScreenRow(presenter, 6), { - screenRow: 6 - text: line6.text - tokens: line6.tokens - top: 10 * 6 + expectValues presenter.getState().content.tiles[2], { + top: 1 } - - line7 = editor.tokenizedLineForScreenRow(7) - expectValues lineStateForScreenRow(presenter, 7), { - screenRow: 7 - text: line7.text - tokens: line7.tokens - top: 10 * 7 + expectValues presenter.getState().content.tiles[3], { + top: 2 } - - line8 = editor.tokenizedLineForScreenRow(8) - expectValues lineStateForScreenRow(presenter, 8), { - screenRow: 8 - text: line8.text - tokens: line8.tokens - top: 10 * 8 + expectValues presenter.getState().content.tiles[4], { + top: 3 } - - expect(lineStateForScreenRow(presenter, 9)).toBeUndefined() + expectValues presenter.getState().content.tiles[5], { + top: 4 + } + expect(presenter.getState().content.tiles[6]).toBeUndefined() it "does not overdraw above the first row", -> - presenter = buildPresenter(explicitHeight: 15, scrollTop: 10, lineHeight: 10, lineOverdrawMargin: 2) - expect(lineStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineStateForScreenRow(presenter, 1)).toBeDefined() - expect(lineStateForScreenRow(presenter, 2)).toBeDefined() - expect(lineStateForScreenRow(presenter, 3)).toBeDefined() - expect(lineStateForScreenRow(presenter, 4)).toBeDefined() - expect(lineStateForScreenRow(presenter, 5)).toBeDefined() - expect(lineStateForScreenRow(presenter, 6)).toBeUndefined() + presenter = buildPresenter(explicitHeight: 3, scrollTop: 0, lineHeight: 1, tileCount: 3, tileOverdrawMargin: 1) + expect(presenter.getState().content.tiles[-1]).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[1]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeDefined() + expect(presenter.getState().content.tiles[3]).toBeDefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[5]).toBeUndefined() it "does not overdraw below the last row", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 105, lineHeight: 10, lineOverdrawMargin: 2) - expect(lineStateForScreenRow(presenter, 7)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 8)).toBeDefined() - expect(lineStateForScreenRow(presenter, 9)).toBeDefined() - expect(lineStateForScreenRow(presenter, 10)).toBeDefined() - expect(lineStateForScreenRow(presenter, 11)).toBeDefined() - expect(lineStateForScreenRow(presenter, 12)).toBeDefined() + presenter = buildPresenter(explicitHeight: 3, scrollTop: 10, lineHeight: 1, tileCount: 3, tileOverdrawMargin: 1) + expect(presenter.getState().content.tiles[8]).toBeUndefined() + expect(presenter.getState().content.tiles[9]).toBeDefined() + expect(presenter.getState().content.tiles[10]).toBeDefined() + expect(presenter.getState().content.tiles[11]).toBeDefined() + expect(presenter.getState().content.tiles[12]).toBeDefined() + expect(presenter.getState().content.tiles[13]).toBeUndefined() - it "includes state for all lines if no external ::explicitHeight is assigned", -> - presenter = buildPresenter(explicitHeight: null) - expect(lineStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineStateForScreenRow(presenter, 12)).toBeDefined() + xit "includes state for all tiles if no external ::explicitHeight is assigned", -> + presenter = buildPresenter(explicitHeight: null, tileCount: 12, tileOverdrawMargin: 1) + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[12]).toBeDefined() it "is empty until all of the required measurements are assigned", -> - presenter = buildPresenter(explicitHeight: null, lineHeight: null, scrollTop: null) - expect(presenter.getState().content.lines).toEqual({}) + presenter = buildPresenter(explicitHeight: null, lineHeight: null, scrollTop: null, tileCount: 3, tileOverdrawMargin: 1) + expect(presenter.getState().content.tiles).toEqual({}) presenter.setExplicitHeight(25) - expect(presenter.getState().content.lines).toEqual({}) + expect(presenter.getState().content.tiles).toEqual({}) presenter.setLineHeight(10) - expect(presenter.getState().content.lines).toEqual({}) + expect(presenter.getState().content.tiles).toEqual({}) presenter.setScrollTop(0) - expect(presenter.getState().content.lines).not.toEqual({}) + expect(presenter.getState().content.tiles).not.toEqual({}) it "updates when ::scrollTop changes", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1) + presenter = buildPresenter(explicitHeight: 3, scrollTop: 0, lineHeight: 1, tileOverdrawMargin: 1, tileCount: 3) - expect(lineStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineStateForScreenRow(presenter, 4)).toBeDefined() - expect(lineStateForScreenRow(presenter, 5)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[5]).toBeUndefined() - expectStateUpdate presenter, -> presenter.setScrollTop(25) + expectStateUpdate presenter, -> presenter.setScrollTop(2) - expect(lineStateForScreenRow(presenter, 0)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 1)).toBeDefined() - expect(lineStateForScreenRow(presenter, 6)).toBeDefined() - expect(lineStateForScreenRow(presenter, 7)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeUndefined() + expect(presenter.getState().content.tiles[1]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[7]).toBeUndefined() - it "updates when ::explicitHeight changes", -> - presenter = buildPresenter(explicitHeight: 15, scrollTop: 15, lineHeight: 10, lineOverdrawMargin: 1) + xit "updates when ::explicitHeight changes", -> + presenter = buildPresenter(explicitHeight: 3, scrollTop: 0, lineHeight: 1, tileOverdrawMargin: 1, tileCount: 3) line5 = editor.tokenizedLineForScreenRow(5) - expect(lineStateForScreenRow(presenter, 4)).toBeDefined() - expect(lineStateForScreenRow(presenter, 5)).toBeUndefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[5]).toBeUndefined() expectStateUpdate presenter, -> presenter.setExplicitHeight(35) - expect(lineStateForScreenRow(presenter, 5)).toBeDefined() - expect(lineStateForScreenRow(presenter, 6)).toBeDefined() - expect(lineStateForScreenRow(presenter, 7)).toBeUndefined() + expect(presenter.getState().content.tiles[5]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[7]).toBeUndefined() - it "updates when ::lineHeight changes", -> + xit "updates when ::lineHeight changes", -> presenter = buildPresenter(explicitHeight: 15, scrollTop: 10, lineHeight: 10, lineOverdrawMargin: 0) expect(lineStateForScreenRow(presenter, 0)).toBeUndefined() @@ -789,29 +762,35 @@ describe "TextEditorPresenter", -> expect(lineStateForScreenRow(presenter, 5)).toBeDefined() expect(lineStateForScreenRow(presenter, 6)).toBeUndefined() - it "updates when the editor's content changes", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 10, lineHeight: 10) + fffit "updates when the editor's content changes", -> + lineStateForScreenRow = (presenter, tile, row) -> + lineId = presenter.model.tokenizedLineForScreenRow(row).id + presenter.getState().content.tiles[tile].lines[lineId] + + presenter = buildPresenter(explicitHeight: 25, scrollTop: 10, lineHeight: 10, tileCount: 3, tileOverdrawMargin: 1) expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n") line1 = editor.tokenizedLineForScreenRow(1) - expectValues lineStateForScreenRow(presenter, 1), { + expectValues lineStateForScreenRow(presenter, 1, 1), { text: line1.text tokens: line1.tokens } line2 = editor.tokenizedLineForScreenRow(2) - expectValues lineStateForScreenRow(presenter, 2), { + expectValues lineStateForScreenRow(presenter, 2, 2), { text: line2.text tokens: line2.tokens } line3 = editor.tokenizedLineForScreenRow(3) - expectValues lineStateForScreenRow(presenter, 3), { + expectValues lineStateForScreenRow(presenter, 3, 3), { text: line3.text tokens: line3.tokens } + xdescribe ".lines", -> + it "does not remove out-of-view lines corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1, stoppedScrollingDelay: 200) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 835a89182..fb762e758 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -16,12 +16,11 @@ class TextEditorPresenter constructor: (params) -> {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params {horizontalScrollbarHeight, verticalScrollbarWidth} = params - {@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor} = params + {@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor, @tileCount, @tileOverdrawMargin} = params {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @measuredVerticalScrollbarWidth = verticalScrollbarWidth @gutterWidth ?= 0 - @tileCount ?= 3 @linesPresentersByTileIndex = {} @disposables = new CompositeDisposable @@ -306,16 +305,21 @@ class TextEditorPresenter linesPerTile = @height / @lineHeight / @tileCount - startIndex = Math.floor(@startRow / linesPerTile) - endIndex = Math.ceil(@endRow / linesPerTile) - visibleIndexesRange = [startIndex..endIndex] + startIndex = Math.max( + 0, Math.floor(@startRow / linesPerTile) - @tileOverdrawMargin + ) + endIndex = Math.min( + Math.ceil(@model.getScreenLineCount() / linesPerTile), + Math.ceil(@endRow / linesPerTile) + @tileOverdrawMargin + ) + visibleTilesIndexes = [startIndex...endIndex] for index, tile of @state.content.tiles - unless index in visibleIndexesRange + unless index in visibleTilesIndexes delete @state.content.tiles[index] delete @linesPresentersByTileIndex[index] - for index in visibleIndexesRange + for index in visibleTilesIndexes presenter = @linesPresentersByTileIndex[index] ?= new LinesPresenter(@) presenter.startRow = Math.floor(index * linesPerTile) presenter.endRow = Math.ceil(Math.min(@endRow, (index + 1) * linesPerTile)) @@ -514,7 +518,7 @@ class TextEditorPresenter updateStartRow: -> return unless @scrollTop? and @lineHeight? - startRow = Math.floor(@scrollTop / @lineHeight) - @lineOverdrawMargin + startRow = Math.floor(@scrollTop / @lineHeight) @startRow = Math.max(0, startRow) updateEndRow: -> @@ -522,7 +526,7 @@ class TextEditorPresenter startRow = Math.max(0, Math.floor(@scrollTop / @lineHeight)) visibleLinesCount = Math.ceil(@height / @lineHeight) + 1 - endRow = startRow + visibleLinesCount + @lineOverdrawMargin + endRow = startRow + visibleLinesCount @endRow = Math.min(@model.getScreenLineCount(), endRow) updateScrollWidth: -> From 3e78fe3019b8c539b0b30fb27c311808e18c1864 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 May 2015 16:21:28 +0200 Subject: [PATCH 04/58] wip --- src/lines-component.coffee | 196 +++----------------- src/text-editor-presenter.coffee | 5 +- src/tile-component.coffee | 301 +++++++++++++++++++++++++++++++ 3 files changed, 328 insertions(+), 174 deletions(-) create mode 100644 src/tile-component.coffee diff --git a/src/lines-component.coffee b/src/lines-component.coffee index fbec40b79..951d5499a 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -4,6 +4,7 @@ _ = require 'underscore-plus' CursorsComponent = require './cursors-component' HighlightsComponent = require './highlights-component' +TileComponent = require './tile-component' DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} @@ -19,11 +20,7 @@ class LinesComponent placeholderTextDiv: null constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) -> - @measuredLines = new Set - @lineNodesByLineId = {} - @screenRowsByLineId = {} - @lineIdsByScreenRow = {} - @renderedDecorationsByLineId = {} + @tileComponentsByTileId = {} @domNode = document.createElement('div') @domNode.classList.add('lines') @@ -44,15 +41,14 @@ class LinesComponent updateSync: (state) -> @newState = state.content - @oldState ?= {lines: {}} + @oldState ?= {tiles: {}} if @newState.scrollHeight isnt @oldState.scrollHeight @domNode.style.height = @newState.scrollHeight + 'px' @oldState.scrollHeight = @newState.scrollHeight - if @newState.scrollTop isnt @oldState.scrollTop or @newState.scrollLeft isnt @oldState.scrollLeft - @domNode.style['-webkit-transform'] = "translate3d(#{-@newState.scrollLeft}px, #{-@newState.scrollTop}px, 0px)" - @oldState.scrollTop = @newState.scrollTop + if @newState.scrollLeft isnt @oldState.scrollLeft + @domNode.style['-webkit-transform'] = "translate3d(#{-@newState.scrollLeft}px, 0, 0px)" @oldState.scrollLeft = @newState.scrollLeft if @newState.backgroundColor isnt @oldState.backgroundColor @@ -67,8 +63,8 @@ class LinesComponent @placeholderTextDiv.textContent = @newState.placeholderText @domNode.appendChild(@placeholderTextDiv) - @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible - @updateLineNodes() + @removeTileNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible + @updateTileNodes() if @newState.scrollWidth isnt @oldState.scrollWidth @domNode.style.width = @newState.scrollWidth + 'px' @@ -80,177 +76,31 @@ class LinesComponent @oldState.indentGuidesVisible = @newState.indentGuidesVisible @oldState.scrollWidth = @newState.scrollWidth - removeLineNodes: -> - @removeLineNode(id) for id of @oldState.lines + removeTileNodes: -> + @removeTileNode(id) for id of @oldState.tiles return - removeLineNode: (id) -> - @lineNodesByLineId[id].remove() - delete @lineNodesByLineId[id] - delete @lineIdsByScreenRow[@screenRowsByLineId[id]] - delete @screenRowsByLineId[id] - delete @oldState.lines[id] + removeTileNode: (id) -> + @tileComponentsByTileId[id].getDomNode().remove() + delete @tileComponentsByTileId[id] + delete @oldState.tiles[id] - updateLineNodes: -> - for id of @oldState.lines - unless @newState.lines.hasOwnProperty(id) - @removeLineNode(id) + updateTileNodes: -> + for id of @oldState.tiles + unless @newState.tiles.hasOwnProperty(id) + @removeTileNode(id) - newLineIds = null - newLinesHTML = null + for id, tileState of @newState.tiles + tileComponent = @tileComponentsByTileId[id] ?= new TileComponent({id, @presenter}) - for id, lineState of @newState.lines - if @oldState.lines.hasOwnProperty(id) - @updateLineNode(id) - else - newLineIds ?= [] - newLinesHTML ?= "" - newLineIds.push(id) - newLinesHTML += @buildLineHTML(id) - @screenRowsByLineId[id] = lineState.screenRow - @lineIdsByScreenRow[lineState.screenRow] = id - @oldState.lines[id] = cloneObject(lineState) + unless @oldState.tiles.hasOwnProperty(id) + @domNode.appendChild(tileComponent.getDomNode()) + @oldState.tiles[id] = cloneObject(tileState) - return unless newLineIds? - - WrapperDiv.innerHTML = newLinesHTML - newLineNodes = _.toArray(WrapperDiv.children) - for id, i in newLineIds - lineNode = newLineNodes[i] - @lineNodesByLineId[id] = lineNode - @domNode.appendChild(lineNode) + tileComponent.updateSync(@newState) return - buildLineHTML: (id) -> - {scrollWidth} = @newState - {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.lines[id] - - classes = '' - if decorationClasses? - for decorationClass in decorationClasses - classes += decorationClass + ' ' - classes += 'line' - - lineHTML = "
" - - if text is "" - lineHTML += @buildEmptyLineInnerHTML(id) - else - lineHTML += @buildLineInnerHTML(id) - - lineHTML += '' if fold - lineHTML += "
" - lineHTML - - buildEmptyLineInnerHTML: (id) -> - {indentGuidesVisible} = @newState - {indentLevel, tabLength, endOfLineInvisibles} = @newState.lines[id] - - if indentGuidesVisible and indentLevel > 0 - invisibleIndex = 0 - lineHTML = '' - for i in [0...indentLevel] - lineHTML += "" - for j in [0...tabLength] - if invisible = endOfLineInvisibles?[invisibleIndex++] - lineHTML += "#{invisible}" - else - lineHTML += ' ' - lineHTML += "" - - while invisibleIndex < endOfLineInvisibles?.length - lineHTML += "#{endOfLineInvisibles[invisibleIndex++]}" - - lineHTML - else - @buildEndOfLineHTML(id) or ' ' - - buildLineInnerHTML: (id) -> - {indentGuidesVisible} = @newState - {tokens, text, isOnlyWhitespace} = @newState.lines[id] - innerHTML = "" - - scopeStack = [] - for token in tokens - innerHTML += @updateScopeStack(scopeStack, token.scopes) - hasIndentGuide = indentGuidesVisible and (token.hasLeadingWhitespace() or (token.hasTrailingWhitespace() and isOnlyWhitespace)) - innerHTML += token.getValueAsHtml({hasIndentGuide}) - - innerHTML += @popScope(scopeStack) while scopeStack.length > 0 - innerHTML += @buildEndOfLineHTML(id) - innerHTML - - buildEndOfLineHTML: (id) -> - {endOfLineInvisibles} = @newState.lines[id] - - html = '' - if endOfLineInvisibles? - for invisible in endOfLineInvisibles - html += "#{invisible}" - html - - updateScopeStack: (scopeStack, desiredScopeDescriptor) -> - html = "" - - # Find a common prefix - for scope, i in desiredScopeDescriptor - break unless scopeStack[i] is desiredScopeDescriptor[i] - - # Pop scopeDescriptor until we're at the common prefx - until scopeStack.length is i - html += @popScope(scopeStack) - - # Push onto common prefix until scopeStack equals desiredScopeDescriptor - for j in [i...desiredScopeDescriptor.length] - html += @pushScope(scopeStack, desiredScopeDescriptor[j]) - - html - - popScope: (scopeStack) -> - scopeStack.pop() - "" - - pushScope: (scopeStack, scope) -> - scopeStack.push(scope) - "" - - updateLineNode: (id) -> - oldLineState = @oldState.lines[id] - newLineState = @newState.lines[id] - - lineNode = @lineNodesByLineId[id] - - if @newState.scrollWidth isnt @oldState.scrollWidth - lineNode.style.width = @newState.scrollWidth + 'px' - - newDecorationClasses = newLineState.decorationClasses - oldDecorationClasses = oldLineState.decorationClasses - - if oldDecorationClasses? - for decorationClass in oldDecorationClasses - unless newDecorationClasses? and decorationClass in newDecorationClasses - lineNode.classList.remove(decorationClass) - - if newDecorationClasses? - for decorationClass in newDecorationClasses - unless oldDecorationClasses? and decorationClass in oldDecorationClasses - lineNode.classList.add(decorationClass) - - oldLineState.decorationClasses = newLineState.decorationClasses - - if newLineState.top isnt oldLineState.top - lineNode.style.top = newLineState.top + 'px' - oldLineState.top = newLineState.cop - - if newLineState.screenRow isnt oldLineState.screenRow - lineNode.dataset.screenRow = newLineState.screenRow - oldLineState.screenRow = newLineState.screenRow - @lineIdsByScreenRow[newLineState.screenRow] = id - - lineNodeForScreenRow: (screenRow) -> - @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] - measureLineHeightAndDefaultCharWidth: -> @domNode.appendChild(DummyLineNode) lineHeightInPixels = DummyLineNode.getBoundingClientRect().height diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index fb762e758..ac7c33609 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -21,6 +21,8 @@ class TextEditorPresenter @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @measuredVerticalScrollbarWidth = verticalScrollbarWidth @gutterWidth ?= 0 + @tileOverdrawMargin ?= 0 + @tileCount ?= 4 @linesPresentersByTileIndex = {} @disposables = new CompositeDisposable @@ -303,7 +305,7 @@ class TextEditorPresenter updateTilesState: -> return unless @startRow? and @endRow? and @lineHeight? - linesPerTile = @height / @lineHeight / @tileCount + linesPerTile = Math.floor(@height / @lineHeight / @tileCount) startIndex = Math.max( 0, Math.floor(@startRow / linesPerTile) - @tileOverdrawMargin @@ -328,6 +330,7 @@ class TextEditorPresenter tile = @state.content.tiles[index] ?= {} tile.top = (index * linesPerTile * @lineHeight) - @scrollTop tile.lines = presenter.getState() + tile.height = linesPerTile * @lineHeight updateCursorsState: -> @state.content.cursors = {} diff --git a/src/tile-component.coffee b/src/tile-component.coffee new file mode 100644 index 000000000..017856368 --- /dev/null +++ b/src/tile-component.coffee @@ -0,0 +1,301 @@ +_ = require 'underscore-plus' +{toArray} = require 'underscore-plus' +{$$} = require 'space-pen' + +CursorsComponent = require './cursors-component' +HighlightsComponent = require './highlights-component' + +DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] +AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} +WrapperDiv = document.createElement('div') + +cloneObject = (object) -> + clone = {} + clone[key] = value for key, value of object + clone + +module.exports = +class TileComponent + placeholderTextDiv: null + + constructor: ({@presenter, @id}) -> + @measuredLines = new Set + @lineNodesByLineId = {} + @screenRowsByLineId = {} + @lineIdsByScreenRow = {} + @domNode = document.createElement("div") + @domNode.style.position = "absolute" + @domNode.classList.add("tile") + + getDomNode: -> + @domNode + + updateSync: (state) -> + @newState = state + unless @oldState + @oldState = {tiles: {}} + @oldState.tiles[@id] = { lines: {}} + + if @newState.tiles[@id].height isnt @oldState.tiles[@id]?.height + @domNode.style.height = @newState.tiles[@id].height + 'px' + @oldState.tiles[@id]?.height = @newState.tiles[@id].height + + if @newState.tiles[@id].top isnt @oldState.tiles[@id]?.top + @domNode.style['-webkit-transform'] = "translate3d(0, #{@newState.tiles[@id].top}px, 0px)" + @oldState.tiles[@id]?.top = @newState.tiles[@id].top + + if @newState.backgroundColor isnt @oldState.backgroundColor + @domNode.style.backgroundColor = @newState.backgroundColor + @oldState.backgroundColor = @newState.backgroundColor + + @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible + @updateLineNodes() + + if @newState.scrollWidth isnt @oldState.scrollWidth + @domNode.style.width = @newState.scrollWidth + 'px' + @oldState.scrollWidth = @newState.scrollWidth + + @oldState.indentGuidesVisible = @newState.indentGuidesVisible + @oldState.scrollWidth = @newState.scrollWidth + + removeLineNodes: -> + @removeLineNode(id) for id of @oldState.tiles[@id]?.lines + return + + removeLineNode: (id) -> + @lineNodesByLineId[id].remove() + delete @lineNodesByLineId[id] + delete @lineIdsByScreenRow[@screenRowsByLineId[id]] + delete @screenRowsByLineId[id] + delete @oldState.tiles[@id]?.lines[id] + + updateLineNodes: -> + for id of @oldState.tiles[@id]?.lines + unless @newState.tiles[@id].lines.hasOwnProperty(id) + @removeLineNode(id) + + newLineIds = null + newLinesHTML = null + + for id, lineState of @newState.tiles[@id].lines + if @oldState.tiles[@id]?.lines.hasOwnProperty(id) + @updateLineNode(id) + else + newLineIds ?= [] + newLinesHTML ?= "" + newLineIds.push(id) + newLinesHTML += @buildLineHTML(id) + @screenRowsByLineId[id] = lineState.screenRow + @lineIdsByScreenRow[lineState.screenRow] = id + @oldState.tiles[@id]?.lines[id] = cloneObject(lineState) + + return unless newLineIds? + + WrapperDiv.innerHTML = newLinesHTML + newLineNodes = _.toArray(WrapperDiv.children) + for id, i in newLineIds + lineNode = newLineNodes[i] + @lineNodesByLineId[id] = lineNode + @domNode.appendChild(lineNode) + + return + + buildLineHTML: (id) -> + {scrollWidth} = @newState + {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.tiles[@id].lines[id] + + classes = '' + if decorationClasses? + for decorationClass in decorationClasses + classes += decorationClass + ' ' + classes += 'line' + + lineHTML = "
" + + if text is "" + lineHTML += @buildEmptyLineInnerHTML(id) + else + lineHTML += @buildLineInnerHTML(id) + + lineHTML += '' if fold + lineHTML += "
" + lineHTML + + buildEmptyLineInnerHTML: (id) -> + {indentGuidesVisible} = @newState + {indentLevel, tabLength, endOfLineInvisibles} = @newState.tiles[@id].lines[id] + + if indentGuidesVisible and indentLevel > 0 + invisibleIndex = 0 + lineHTML = '' + for i in [0...indentLevel] + lineHTML += "" + for j in [0...tabLength] + if invisible = endOfLineInvisibles?[invisibleIndex++] + lineHTML += "#{invisible}" + else + lineHTML += ' ' + lineHTML += "" + + while invisibleIndex < endOfLineInvisibles?.length + lineHTML += "#{endOfLineInvisibles[invisibleIndex++]}" + + lineHTML + else + @buildEndOfLineHTML(id) or ' ' + + buildLineInnerHTML: (id) -> + {indentGuidesVisible} = @newState + {tokens, text, isOnlyWhitespace} = @newState.tiles[@id].lines[id] + innerHTML = "" + + scopeStack = [] + for token in tokens + innerHTML += @updateScopeStack(scopeStack, token.scopes) + hasIndentGuide = indentGuidesVisible and (token.hasLeadingWhitespace() or (token.hasTrailingWhitespace() and isOnlyWhitespace)) + innerHTML += token.getValueAsHtml({hasIndentGuide}) + + innerHTML += @popScope(scopeStack) while scopeStack.length > 0 + innerHTML += @buildEndOfLineHTML(id) + innerHTML + + buildEndOfLineHTML: (id) -> + {endOfLineInvisibles} = @newState.tiles[@id].lines[id] + + html = '' + if endOfLineInvisibles? + for invisible in endOfLineInvisibles + html += "#{invisible}" + html + + updateScopeStack: (scopeStack, desiredScopeDescriptor) -> + html = "" + + # Find a common prefix + for scope, i in desiredScopeDescriptor + break unless scopeStack[i] is desiredScopeDescriptor[i] + + # Pop scopeDescriptor until we're at the common prefx + until scopeStack.length is i + html += @popScope(scopeStack) + + # Push onto common prefix until scopeStack equals desiredScopeDescriptor + for j in [i...desiredScopeDescriptor.length] + html += @pushScope(scopeStack, desiredScopeDescriptor[j]) + + html + + popScope: (scopeStack) -> + scopeStack.pop() + "
" + + pushScope: (scopeStack, scope) -> + scopeStack.push(scope) + "" + + updateLineNode: (id) -> + oldLineState = @oldState.tiles[@id]?.lines[id] + newLineState = @newState.tiles[@id].lines[id] + + lineNode = @lineNodesByLineId[id] + + if @newState.scrollWidth isnt @oldState.scrollWidth + lineNode.style.width = @newState.scrollWidth + 'px' + + newDecorationClasses = newLineState.decorationClasses + oldDecorationClasses = oldLineState.decorationClasses + + if oldDecorationClasses? + for decorationClass in oldDecorationClasses + unless newDecorationClasses? and decorationClass in newDecorationClasses + lineNode.classList.remove(decorationClass) + + if newDecorationClasses? + for decorationClass in newDecorationClasses + unless oldDecorationClasses? and decorationClass in oldDecorationClasses + lineNode.classList.add(decorationClass) + + oldLineState.decorationClasses = newLineState.decorationClasses + + if newLineState.top isnt oldLineState.top + lineNode.style.top = newLineState.top + 'px' + oldLineState.top = newLineState.cop + + if newLineState.screenRow isnt oldLineState.screenRow + lineNode.dataset.screenRow = newLineState.screenRow + oldLineState.screenRow = newLineState.screenRow + @lineIdsByScreenRow[newLineState.screenRow] = id + + lineNodeForScreenRow: (screenRow) -> + @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] + + measureLineHeightAndDefaultCharWidth: -> + @domNode.appendChild(DummyLineNode) + lineHeightInPixels = DummyLineNode.getBoundingClientRect().height + charWidth = DummyLineNode.firstChild.getBoundingClientRect().width + @domNode.removeChild(DummyLineNode) + + @presenter.setLineHeight(lineHeightInPixels) + @presenter.setBaseCharacterWidth(charWidth) + + remeasureCharacterWidths: -> + return unless @presenter.baseCharacterWidth + + @clearScopedCharWidths() + @measureCharactersInNewLines() + + measureCharactersInNewLines: -> + @presenter.batchCharacterMeasurement => + for id, lineState of @oldState.tiles[@id]?.lines + unless @measuredLines.has(id) + lineNode = @lineNodesByLineId[id] + @measureCharactersInLine(id, lineState, lineNode) + return + + measureCharactersInLine: (lineId, tokenizedLine, lineNode) -> + rangeForMeasurement = null + iterator = null + charIndex = 0 + + for {value, scopes, hasPairedCharacter} in tokenizedLine.tokens + charWidths = @presenter.getScopedCharacterWidths(scopes) + + valueIndex = 0 + while valueIndex < value.length + if hasPairedCharacter + char = value.substr(valueIndex, 2) + charLength = 2 + valueIndex += 2 + else + char = value[valueIndex] + charLength = 1 + valueIndex++ + + continue if char is '\0' + + unless charWidths[char]? + unless textNode? + rangeForMeasurement ?= document.createRange() + iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) + textNode = iterator.nextNode() + textNodeIndex = 0 + nextTextNodeIndex = textNode.textContent.length + + while nextTextNodeIndex <= charIndex + textNode = iterator.nextNode() + textNodeIndex = nextTextNodeIndex + nextTextNodeIndex = textNodeIndex + textNode.textContent.length + + i = charIndex - textNodeIndex + rangeForMeasurement.setStart(textNode, i) + rangeForMeasurement.setEnd(textNode, i + charLength) + charWidth = rangeForMeasurement.getBoundingClientRect().width + @presenter.setScopedCharacterWidth(scopes, char, charWidth) + + charIndex += charLength + + @measuredLines.add(lineId) + + clearScopedCharWidths: -> + @measuredLines.clear() + @presenter.clearScopedCharacterWidths() From 414d5d1c8737fc3d0eb8c5f91b383a1a0e13d8c0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 May 2015 18:10:21 +0200 Subject: [PATCH 05/58] wip --- src/text-editor-presenter.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index ac7c33609..c8d74b428 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -324,7 +324,7 @@ class TextEditorPresenter for index in visibleTilesIndexes presenter = @linesPresentersByTileIndex[index] ?= new LinesPresenter(@) presenter.startRow = Math.floor(index * linesPerTile) - presenter.endRow = Math.ceil(Math.min(@endRow, (index + 1) * linesPerTile)) + presenter.endRow = Math.ceil(Math.min(@model.getScreenLineCount(), (index + 1) * linesPerTile)) presenter.lineHeight = @lineHeight tile = @state.content.tiles[index] ?= {} From de53e1dccc911089f5daea6fba37b6acee269b57 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 May 2015 18:36:10 +0200 Subject: [PATCH 06/58] Fix char measurement --- src/lines-component.coffee | 55 +++++--------------------------------- src/tile-component.coffee | 34 +++++------------------ 2 files changed, 13 insertions(+), 76 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 951d5499a..547b54e56 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -118,56 +118,13 @@ class LinesComponent measureCharactersInNewLines: -> @presenter.batchCharacterMeasurement => - for id, lineState of @oldState.lines - unless @measuredLines.has(id) - lineNode = @lineNodesByLineId[id] - @measureCharactersInLine(id, lineState, lineNode) + for id, component of @tileComponentsByTileId + component.measureCharactersInNewLines() + return - measureCharactersInLine: (lineId, tokenizedLine, lineNode) -> - rangeForMeasurement = null - iterator = null - charIndex = 0 - - for {value, scopes, hasPairedCharacter} in tokenizedLine.tokens - charWidths = @presenter.getScopedCharacterWidths(scopes) - - valueIndex = 0 - while valueIndex < value.length - if hasPairedCharacter - char = value.substr(valueIndex, 2) - charLength = 2 - valueIndex += 2 - else - char = value[valueIndex] - charLength = 1 - valueIndex++ - - continue if char is '\0' - - unless charWidths[char]? - unless textNode? - rangeForMeasurement ?= document.createRange() - iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) - textNode = iterator.nextNode() - textNodeIndex = 0 - nextTextNodeIndex = textNode.textContent.length - - while nextTextNodeIndex <= charIndex - textNode = iterator.nextNode() - textNodeIndex = nextTextNodeIndex - nextTextNodeIndex = textNodeIndex + textNode.textContent.length - - i = charIndex - textNodeIndex - rangeForMeasurement.setStart(textNode, i) - rangeForMeasurement.setEnd(textNode, i + charLength) - charWidth = rangeForMeasurement.getBoundingClientRect().width - @presenter.setScopedCharacterWidth(scopes, char, charWidth) - - charIndex += charLength - - @measuredLines.add(lineId) - clearScopedCharWidths: -> - @measuredLines.clear() + for id, component of @tileComponentsByTileId + component.clearMeasurements() + @presenter.clearScopedCharacterWidths() diff --git a/src/tile-component.coffee b/src/tile-component.coffee index 017856368..0b97467c7 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -2,9 +2,6 @@ _ = require 'underscore-plus' {toArray} = require 'underscore-plus' {$$} = require 'space-pen' -CursorsComponent = require './cursors-component' -HighlightsComponent = require './highlights-component' - DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} WrapperDiv = document.createElement('div') @@ -219,7 +216,7 @@ class TileComponent if newLineState.top isnt oldLineState.top lineNode.style.top = newLineState.top + 'px' - oldLineState.top = newLineState.cop + oldLineState.top = newLineState.top if newLineState.screenRow isnt oldLineState.screenRow lineNode.dataset.screenRow = newLineState.screenRow @@ -229,28 +226,12 @@ class TileComponent lineNodeForScreenRow: (screenRow) -> @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] - measureLineHeightAndDefaultCharWidth: -> - @domNode.appendChild(DummyLineNode) - lineHeightInPixels = DummyLineNode.getBoundingClientRect().height - charWidth = DummyLineNode.firstChild.getBoundingClientRect().width - @domNode.removeChild(DummyLineNode) - - @presenter.setLineHeight(lineHeightInPixels) - @presenter.setBaseCharacterWidth(charWidth) - - remeasureCharacterWidths: -> - return unless @presenter.baseCharacterWidth - - @clearScopedCharWidths() - @measureCharactersInNewLines() - measureCharactersInNewLines: -> - @presenter.batchCharacterMeasurement => - for id, lineState of @oldState.tiles[@id]?.lines - unless @measuredLines.has(id) - lineNode = @lineNodesByLineId[id] - @measureCharactersInLine(id, lineState, lineNode) - return + for id, lineState of @oldState.tiles[@id]?.lines + unless @measuredLines.has(id) + lineNode = @lineNodesByLineId[id] + @measureCharactersInLine(id, lineState, lineNode) + return measureCharactersInLine: (lineId, tokenizedLine, lineNode) -> rangeForMeasurement = null @@ -296,6 +277,5 @@ class TileComponent @measuredLines.add(lineId) - clearScopedCharWidths: -> + clearMeasurements: -> @measuredLines.clear() - @presenter.clearScopedCharacterWidths() From 40cde496d72d9de89e3fe36381705bfc806e5cc5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 May 2015 19:03:13 +0200 Subject: [PATCH 07/58] Draw highlight components appropriately --- src/text-editor-presenter.coffee | 8 ++++---- src/tile-component.coffee | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index c8d74b428..9f31c2a54 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1218,7 +1218,7 @@ class TextEditorPresenter if spannedRows is 1 [ - top: startPixelPosition.top + top: startPixelPosition.top - @scrollTop height: lineHeightInPixels left: startPixelPosition.left width: endPixelPosition.left - startPixelPosition.left @@ -1228,7 +1228,7 @@ class TextEditorPresenter # First row, extending from selection start to the right side of screen regions.push( - top: startPixelPosition.top + top: startPixelPosition.top - @scrollTop left: startPixelPosition.left height: lineHeightInPixels right: 0 @@ -1237,7 +1237,7 @@ class TextEditorPresenter # Middle rows, extending from left side to right side of screen if spannedRows > 2 regions.push( - top: startPixelPosition.top + lineHeightInPixels + top: startPixelPosition.top + lineHeightInPixels - @scrollTop height: endPixelPosition.top - startPixelPosition.top - lineHeightInPixels left: 0 right: 0 @@ -1246,7 +1246,7 @@ class TextEditorPresenter # Last row, extending from left side of screen to selection end if screenRange.end.column > 0 regions.push( - top: endPixelPosition.top + top: endPixelPosition.top - @scrollTop height: lineHeightInPixels left: 0 width: endPixelPosition.left diff --git a/src/tile-component.coffee b/src/tile-component.coffee index 0b97467c7..550e2c272 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -41,10 +41,6 @@ class TileComponent @domNode.style['-webkit-transform'] = "translate3d(0, #{@newState.tiles[@id].top}px, 0px)" @oldState.tiles[@id]?.top = @newState.tiles[@id].top - if @newState.backgroundColor isnt @oldState.backgroundColor - @domNode.style.backgroundColor = @newState.backgroundColor - @oldState.backgroundColor = @newState.backgroundColor - @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible @updateLineNodes() From 936133cfdcb70a159c9d3e144ca311d02281452e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 May 2015 19:06:37 +0200 Subject: [PATCH 08/58] Always subtract @scrollTop --- src/text-editor-presenter.coffee | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 9f31c2a54..f0fbe7f7a 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -977,11 +977,12 @@ class TextEditorPresenter charLength = 1 valueIndex++ - return {top, left} if column is targetColumn + break if column is targetColumn left += characterWidths[char] ? baseCharacterWidth unless char is '\0' column += charLength - {top, left} + + {top: top - @scrollTop, left} hasPixelRectRequirements: -> @hasPixelPositionRequirements() and @scrollWidth? @@ -1218,7 +1219,7 @@ class TextEditorPresenter if spannedRows is 1 [ - top: startPixelPosition.top - @scrollTop + top: startPixelPosition.top height: lineHeightInPixels left: startPixelPosition.left width: endPixelPosition.left - startPixelPosition.left @@ -1228,7 +1229,7 @@ class TextEditorPresenter # First row, extending from selection start to the right side of screen regions.push( - top: startPixelPosition.top - @scrollTop + top: startPixelPosition.top left: startPixelPosition.left height: lineHeightInPixels right: 0 @@ -1237,7 +1238,7 @@ class TextEditorPresenter # Middle rows, extending from left side to right side of screen if spannedRows > 2 regions.push( - top: startPixelPosition.top + lineHeightInPixels - @scrollTop + top: startPixelPosition.top + lineHeightInPixels height: endPixelPosition.top - startPixelPosition.top - lineHeightInPixels left: 0 right: 0 @@ -1246,7 +1247,7 @@ class TextEditorPresenter # Last row, extending from left side of screen to selection end if screenRange.end.column > 0 regions.push( - top: endPixelPosition.top - @scrollTop + top: endPixelPosition.top height: lineHeightInPixels left: 0 width: endPixelPosition.left From 3a1bb898c33160f3d2c8cfe37b584bb5ab6f241f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 May 2015 19:09:48 +0200 Subject: [PATCH 09/58] Fix clicking on line --- src/text-editor-component.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 4c6480510..df88d0706 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -763,6 +763,7 @@ class TextEditorComponent screenPositionForMouseEvent: (event) -> pixelPosition = @pixelPositionForMouseEvent(event) + pixelPosition.top += @presenter.scrollTop @editor.screenPositionForPixelPosition(pixelPosition) pixelPositionForMouseEvent: (event) -> From 9aaa8b483388896ab9c5ad6ef0e680fd7f555214 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 May 2015 19:21:34 +0200 Subject: [PATCH 10/58] Better defaults --- 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 f0fbe7f7a..2cf2188d7 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -21,8 +21,8 @@ class TextEditorPresenter @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @measuredVerticalScrollbarWidth = verticalScrollbarWidth @gutterWidth ?= 0 - @tileOverdrawMargin ?= 0 - @tileCount ?= 4 + @tileOverdrawMargin ?= 1 + @tileCount ?= 3 @linesPresentersByTileIndex = {} @disposables = new CompositeDisposable From 216b757ac0aa5aa96f4fb6a65f17eacd841fc86b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 May 2015 19:49:49 +0200 Subject: [PATCH 11/58] Keep scrolling tile around for mousewheel --- src/text-editor-component.coffee | 1 - src/text-editor-presenter.coffee | 18 ++++++++++-------- src/tile-component.coffee | 4 ++++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index df88d0706..71ca96d87 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -354,7 +354,6 @@ class TextEditorComponent event.preventDefault() unless previousScrollLeft is @presenter.getScrollLeft() else # Scrolling vertically - @presenter.setMouseWheelScreenRow(@screenRowForNode(event.target)) previousScrollTop = @presenter.getScrollTop() @presenter.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity)) event.preventDefault() unless previousScrollTop is @presenter.getScrollTop() diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 2cf2188d7..1411470c1 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -317,9 +317,11 @@ class TextEditorPresenter visibleTilesIndexes = [startIndex...endIndex] for index, tile of @state.content.tiles - unless index in visibleTilesIndexes - delete @state.content.tiles[index] - delete @linesPresentersByTileIndex[index] + continue if index is @scrollingTile + continue if index in visibleTilesIndexes + + delete @state.content.tiles[index] + delete @linesPresentersByTileIndex[index] for index in visibleTilesIndexes presenter = @linesPresentersByTileIndex[index] ?= new LinesPresenter(@) @@ -728,8 +730,8 @@ class TextEditorPresenter didStopScrolling: -> @state.content.scrollingVertically = false - if @mouseWheelScreenRow? - @mouseWheelScreenRow = null + if @scrollingTile? + @scrollingTile = null @shouldUpdateTilesState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true @@ -895,9 +897,9 @@ class TextEditorPresenter @emitDidUpdateState() - setMouseWheelScreenRow: (mouseWheelScreenRow) -> - unless @mouseWheelScreenRow is mouseWheelScreenRow - @mouseWheelScreenRow = mouseWheelScreenRow + setScrollingTile: (tileId) -> + if @scrollingTile isnt tileId + @scrollingTile = tileId @didStartScrolling() setBaseCharacterWidth: (baseCharacterWidth) -> diff --git a/src/tile-component.coffee b/src/tile-component.coffee index 550e2c272..ff2d7eb46 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -21,9 +21,13 @@ class TileComponent @screenRowsByLineId = {} @lineIdsByScreenRow = {} @domNode = document.createElement("div") + @domNode.addEventListener("mousewheel", @onMouseWheel) @domNode.style.position = "absolute" @domNode.classList.add("tile") + onMouseWheel: => + @presenter.setScrollingTile(@id) + getDomNode: -> @domNode From daa4b33e649842475475f3aab985bbff505c9934 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 May 2015 08:55:53 +0200 Subject: [PATCH 12/58] wip --- spec/text-editor-presenter-spec.coffee | 4 ++-- src/text-editor-presenter.coffee | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index b2c40fc36..6db369f69 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -657,7 +657,7 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> editor.setPlaceholderText("new-placeholder-text") expect(presenter.getState().content.placeholderText).toBe "new-placeholder-text" - fdescribe ".tiles", -> + describe ".tiles", -> it "contains states for tiles that are visible on screen, plus and minus the overdraw margin", -> presenter = buildPresenter(explicitHeight: 3, scrollTop: 1, lineHeight: 1, tileCount: 3, tileOverdrawMargin: 1) @@ -762,7 +762,7 @@ describe "TextEditorPresenter", -> expect(lineStateForScreenRow(presenter, 5)).toBeDefined() expect(lineStateForScreenRow(presenter, 6)).toBeUndefined() - fffit "updates when the editor's content changes", -> + it "updates when the editor's content changes", -> lineStateForScreenRow = (presenter, tile, row) -> lineId = presenter.model.tokenizedLineForScreenRow(row).id presenter.getState().content.tiles[tile].lines[lineId] diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 1411470c1..071195ec3 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -21,7 +21,7 @@ class TextEditorPresenter @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @measuredVerticalScrollbarWidth = verticalScrollbarWidth @gutterWidth ?= 0 - @tileOverdrawMargin ?= 1 + @tileOverdrawMargin ?= 0 @tileCount ?= 3 @linesPresentersByTileIndex = {} From 02838ad193c6e3d06f3f32e5426476e2b9127ecf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 May 2015 09:46:47 +0200 Subject: [PATCH 13/58] Recycle tiles --- src/lines-component.coffee | 15 +++++++++++---- src/text-editor-presenter.coffee | 4 +++- src/tile-component.coffee | 28 ++++++++++++++++++++++++---- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 547b54e56..fda77a76a 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -21,6 +21,7 @@ class LinesComponent constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) -> @tileComponentsByTileId = {} + @freeDomNodes = [] @domNode = document.createElement('div') @domNode.classList.add('lines') @@ -81,7 +82,10 @@ class LinesComponent return removeTileNode: (id) -> - @tileComponentsByTileId[id].getDomNode().remove() + node = @tileComponentsByTileId[id].getDomNode() + + node.style.display = "none" + @freeDomNodes.push(node) delete @tileComponentsByTileId[id] delete @oldState.tiles[id] @@ -91,10 +95,13 @@ class LinesComponent @removeTileNode(id) for id, tileState of @newState.tiles - tileComponent = @tileComponentsByTileId[id] ?= new TileComponent({id, @presenter}) + if @oldState.tiles.hasOwnProperty(id) + tileComponent = @tileComponentsByTileId[id] + else + domNode = @freeDomNodes.pop() + tileComponent = @tileComponentsByTileId[id] = new TileComponent({id, @presenter, domNode}) - unless @oldState.tiles.hasOwnProperty(id) - @domNode.appendChild(tileComponent.getDomNode()) + @domNode.appendChild(tileComponent.getDomNode()) unless domNode? @oldState.tiles[id] = cloneObject(tileState) tileComponent.updateSync(@newState) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 071195ec3..4ff358cec 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -318,7 +318,7 @@ class TextEditorPresenter for index, tile of @state.content.tiles continue if index is @scrollingTile - continue if index in visibleTilesIndexes + continue if startIndex <= index <= endIndex delete @state.content.tiles[index] delete @linesPresentersByTileIndex[index] @@ -329,10 +329,12 @@ class TextEditorPresenter presenter.endRow = Math.ceil(Math.min(@model.getScreenLineCount(), (index + 1) * linesPerTile)) presenter.lineHeight = @lineHeight + isNewTile = not @state.content.tiles.hasOwnProperty(index) tile = @state.content.tiles[index] ?= {} tile.top = (index * linesPerTile * @lineHeight) - @scrollTop tile.lines = presenter.getState() tile.height = linesPerTile * @lineHeight + tile.newlyCreated = isNewTile updateCursorsState: -> @state.content.cursors = {} diff --git a/src/tile-component.coffee b/src/tile-component.coffee index ff2d7eb46..023f9a369 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -15,14 +15,15 @@ module.exports = class TileComponent placeholderTextDiv: null - constructor: ({@presenter, @id}) -> + constructor: ({@presenter, @id, @domNode}) -> @measuredLines = new Set @lineNodesByLineId = {} @screenRowsByLineId = {} @lineIdsByScreenRow = {} - @domNode = document.createElement("div") + @domNode ?= document.createElement("div") @domNode.addEventListener("mousewheel", @onMouseWheel) @domNode.style.position = "absolute" + @domNode.style.display = "block" @domNode.classList.add("tile") onMouseWheel: => @@ -45,8 +46,27 @@ class TileComponent @domNode.style['-webkit-transform'] = "translate3d(0, #{@newState.tiles[@id].top}px, 0px)" @oldState.tiles[@id]?.top = @newState.tiles[@id].top - @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible - @updateLineNodes() + if @newState.tiles[@id].newlyCreated + newLineIds = [] + newLinesHTML = "" + + for id, lineState of @newState.tiles[@id].lines + newLineIds.push(id) + newLinesHTML += @buildLineHTML(id) + @screenRowsByLineId[id] = lineState.screenRow + @lineIdsByScreenRow[lineState.screenRow] = id + @oldState.tiles[@id]?.lines[id] = cloneObject(lineState) + + return if newLineIds.length is 0 + + @domNode.innerHTML = newLinesHTML + newLineNodes = _.toArray(@domNode.children) + for id, i in newLineIds + lineNode = newLineNodes[i] + @lineNodesByLineId[id] = lineNode + else + @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible + @updateLineNodes() if @newState.scrollWidth isnt @oldState.scrollWidth @domNode.style.width = @newState.scrollWidth + 'px' From c99ffa04f11307fab3593bf8ad3087744a4e2ede Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 May 2015 10:03:49 +0200 Subject: [PATCH 14/58] Hide scrolling tile --- src/text-editor-presenter.coffee | 24 +++++++++++++++--------- src/tile-component.coffee | 4 ++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 4ff358cec..59ff21afe 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -314,16 +314,9 @@ class TextEditorPresenter Math.ceil(@model.getScreenLineCount() / linesPerTile), Math.ceil(@endRow / linesPerTile) + @tileOverdrawMargin ) - visibleTilesIndexes = [startIndex...endIndex] - for index, tile of @state.content.tiles - continue if index is @scrollingTile - continue if startIndex <= index <= endIndex - - delete @state.content.tiles[index] - delete @linesPresentersByTileIndex[index] - - for index in visibleTilesIndexes + visibleTiles = {} + for index in [startIndex...endIndex] presenter = @linesPresentersByTileIndex[index] ?= new LinesPresenter(@) presenter.startRow = Math.floor(index * linesPerTile) presenter.endRow = Math.ceil(Math.min(@model.getScreenLineCount(), (index + 1) * linesPerTile)) @@ -335,6 +328,19 @@ class TextEditorPresenter tile.lines = presenter.getState() tile.height = linesPerTile * @lineHeight tile.newlyCreated = isNewTile + tile.display = "block" + + visibleTiles[index] = true + + for index, tile of @state.content.tiles + continue if visibleTiles.hasOwnProperty(index) + + if index is @scrollingTile + tile.display = "none" + else + delete @state.content.tiles[index] + delete @linesPresentersByTileIndex[index] + updateCursorsState: -> @state.content.cursors = {} diff --git a/src/tile-component.coffee b/src/tile-component.coffee index 023f9a369..0f366dbdc 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -38,6 +38,10 @@ class TileComponent @oldState = {tiles: {}} @oldState.tiles[@id] = { lines: {}} + if @newState.tiles[@id].display isnt @oldState.tiles[@id]?.display + @domNode.style.display = @newState.tiles[@id].display + @oldState.tiles[@id]?.display = @newState.tiles[@id].display + if @newState.tiles[@id].height isnt @oldState.tiles[@id]?.height @domNode.style.height = @newState.tiles[@id].height + 'px' @oldState.tiles[@id]?.height = @newState.tiles[@id].height From a6e0fa6e9142d71fc5cd9c5ab79ff546d5a107f1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 11 May 2015 09:42:04 +0200 Subject: [PATCH 15/58] Scroll exactly as we did before tiling Except that now we store the scrolling tile, instead of the scrolling row. --- src/text-editor-component.coffee | 8 +++++--- src/text-editor-presenter.coffee | 2 +- src/tile-component.coffee | 5 +---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 71ca96d87..8c497bd98 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -354,6 +354,7 @@ class TextEditorComponent event.preventDefault() unless previousScrollLeft is @presenter.getScrollLeft() else # Scrolling vertically + @presenter.setScrollingTileId(@tileIdForNode(event.target)) previousScrollTop = @presenter.getScrollTop() @presenter.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity)) event.preventDefault() unless previousScrollTop is @presenter.getScrollTop() @@ -728,11 +729,12 @@ class TextEditorComponent lineNumberNodeForScreenRow: (screenRow) -> @gutterContainerComponent.getLineNumberGutterComponent().lineNumberNodeForScreenRow(screenRow) - screenRowForNode: (node) -> + tileIdForNode: (node) -> while node? - if screenRow = node.dataset.screenRow - return parseInt(screenRow) + if tileId = node.dataset.tileId + return tileId node = node.parentElement + null getFontSize: -> diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 59ff21afe..12044bff0 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -905,7 +905,7 @@ class TextEditorPresenter @emitDidUpdateState() - setScrollingTile: (tileId) -> + setScrollingTileId: (tileId) -> if @scrollingTile isnt tileId @scrollingTile = tileId @didStartScrolling() diff --git a/src/tile-component.coffee b/src/tile-component.coffee index 0f366dbdc..5bd299d79 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -21,14 +21,11 @@ class TileComponent @screenRowsByLineId = {} @lineIdsByScreenRow = {} @domNode ?= document.createElement("div") - @domNode.addEventListener("mousewheel", @onMouseWheel) + @domNode.dataset.tileId = @id @domNode.style.position = "absolute" @domNode.style.display = "block" @domNode.classList.add("tile") - onMouseWheel: => - @presenter.setScrollingTile(@id) - getDomNode: -> @domNode From 8aaac870dcb94696cbc8174a1d92a311e07cefa3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 11 May 2015 09:45:55 +0200 Subject: [PATCH 16/58] Ensure we have at least one line per tile This will prevent, for example, a mini editor (which has no `@height` by default) from not showing characters. --- src/text-editor-presenter.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 12044bff0..0111c7a8a 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -306,6 +306,7 @@ class TextEditorPresenter return unless @startRow? and @endRow? and @lineHeight? linesPerTile = Math.floor(@height / @lineHeight / @tileCount) + linesPerTile = Math.max(1, linesPerTile) startIndex = Math.max( 0, Math.floor(@startRow / linesPerTile) - @tileOverdrawMargin From a6dab4860c7ced496f9fd4c5ffc6e3c448c5f8ff Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 11 May 2015 10:01:02 +0200 Subject: [PATCH 17/58] Position overlays absolutely We did so by introducing an `absolute` (optional) parameter in `pixelPositionForScreenPosition`. --- src/text-editor-presenter.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 0111c7a8a..e9944afce 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -375,7 +375,7 @@ class TextEditorPresenter else screenPosition = decoration.getMarker().getHeadScreenPosition() - pixelPosition = @pixelPositionForScreenPosition(screenPosition) + pixelPosition = @pixelPositionForScreenPosition(screenPosition, true, absolute: true) {scrollTop, scrollLeft} = @state.content @@ -963,7 +963,7 @@ class TextEditorPresenter hasPixelPositionRequirements: -> @lineHeight? and @baseCharacterWidth? - pixelPositionForScreenPosition: (screenPosition, clip=true) -> + pixelPositionForScreenPosition: (screenPosition, clip=true, {absolute}={}) -> screenPosition = Point.fromObject(screenPosition) screenPosition = @model.clipScreenPosition(screenPosition) if clip @@ -993,7 +993,8 @@ class TextEditorPresenter left += characterWidths[char] ? baseCharacterWidth unless char is '\0' column += charLength - {top: top - @scrollTop, left} + top -= @scrollTop unless absolute + {top, left} hasPixelRectRequirements: -> @hasPixelPositionRequirements() and @scrollWidth? From 19a95651dcbbba12550be80991b3b9086d89f3d5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 12 May 2015 09:25:31 +0200 Subject: [PATCH 18/58] Relativize `pixelPositionForScreenPosition`.`top` This commit makes the method return a `top` value which is relative to the viewport. However, we still need to return an absolute value for `left` because lines are moved horizontally through a single transform on their parent node. I would prefer being consistent (i.e. make everything relative), but moving every single tile to simulate scrolling, on the other hand, seems overkill and it's not really intuitive given that we are not tiling horizontally. /cc: @nathansobo --- src/text-editor-presenter.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index e9944afce..c7bf282ed 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -375,11 +375,11 @@ class TextEditorPresenter else screenPosition = decoration.getMarker().getHeadScreenPosition() - pixelPosition = @pixelPositionForScreenPosition(screenPosition, true, absolute: true) + pixelPosition = @pixelPositionForScreenPosition(screenPosition, true) - {scrollTop, scrollLeft} = @state.content + {scrollLeft} = @state.content - top = pixelPosition.top + @lineHeight - scrollTop + top = pixelPosition.top + @lineHeight left = pixelPosition.left + @gutterWidth - scrollLeft if overlayDimensions = @overlayDimensions[decoration.id] @@ -963,7 +963,7 @@ class TextEditorPresenter hasPixelPositionRequirements: -> @lineHeight? and @baseCharacterWidth? - pixelPositionForScreenPosition: (screenPosition, clip=true, {absolute}={}) -> + pixelPositionForScreenPosition: (screenPosition, clip=true) -> screenPosition = Point.fromObject(screenPosition) screenPosition = @model.clipScreenPosition(screenPosition) if clip @@ -993,7 +993,7 @@ class TextEditorPresenter left += characterWidths[char] ? baseCharacterWidth unless char is '\0' column += charLength - top -= @scrollTop unless absolute + top -= @scrollTop {top, left} hasPixelRectRequirements: -> From 63e2764b7f455f6e590dfe1ee8ab1a57335bc449 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 May 2015 10:45:42 +0200 Subject: [PATCH 19/58] :art: Inline LinesPresenter --- src/lines-presenter.coffee | 41 ------------------------------ src/text-editor-presenter.coffee | 43 ++++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 48 deletions(-) delete mode 100644 src/lines-presenter.coffee diff --git a/src/lines-presenter.coffee b/src/lines-presenter.coffee deleted file mode 100644 index c1e3676ba..000000000 --- a/src/lines-presenter.coffee +++ /dev/null @@ -1,41 +0,0 @@ -module.exports = -class LinesPresenter - startRow: null - endRow: null - lineHeight: null - - constructor: (@presenter) -> - @lines = {} - - getState: -> - visibleLineIds = {} - row = @startRow - while row < @endRow - line = @presenter.model.tokenizedLineForScreenRow(row) - unless line? - throw new Error("No line exists for row #{row}. Last screen row: #{@model.getLastScreenRow()}") - - visibleLineIds[line.id] = true - if @lines.hasOwnProperty(line.id) - lineState = @lines[line.id] - lineState.screenRow = row - lineState.top = (row - @startRow) * @lineHeight - lineState.decorationClasses = @presenter.lineDecorationClassesForRow(row) - else - @lines[line.id] = - screenRow: row - text: line.text - tokens: line.tokens - isOnlyWhitespace: line.isOnlyWhitespace() - endOfLineInvisibles: line.endOfLineInvisibles - indentLevel: line.indentLevel - tabLength: line.tabLength - fold: line.fold - top: (row - @startRow) * @lineHeight - decorationClasses: @presenter.lineDecorationClassesForRow(row) - row++ - - for id, line of @lines - delete @lines[id] unless visibleLineIds.hasOwnProperty(id) - - @lines diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index c7bf282ed..9d8c22c3c 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -23,7 +23,6 @@ class TextEditorPresenter @gutterWidth ?= 0 @tileOverdrawMargin ?= 0 @tileCount ?= 3 - @linesPresentersByTileIndex = {} @disposables = new CompositeDisposable @emitter = new Emitter @@ -318,19 +317,18 @@ class TextEditorPresenter visibleTiles = {} for index in [startIndex...endIndex] - presenter = @linesPresentersByTileIndex[index] ?= new LinesPresenter(@) - presenter.startRow = Math.floor(index * linesPerTile) - presenter.endRow = Math.ceil(Math.min(@model.getScreenLineCount(), (index + 1) * linesPerTile)) - presenter.lineHeight = @lineHeight + startRow = Math.floor(index * linesPerTile) + endRow = Math.ceil(Math.min(@model.getScreenLineCount(), (index + 1) * linesPerTile)) isNewTile = not @state.content.tiles.hasOwnProperty(index) tile = @state.content.tiles[index] ?= {} tile.top = (index * linesPerTile * @lineHeight) - @scrollTop - tile.lines = presenter.getState() tile.height = linesPerTile * @lineHeight tile.newlyCreated = isNewTile tile.display = "block" + @updateLinesState(tile, startRow, endRow) + visibleTiles[index] = true for index, tile of @state.content.tiles @@ -340,9 +338,40 @@ class TextEditorPresenter tile.display = "none" else delete @state.content.tiles[index] - delete @linesPresentersByTileIndex[index] + updateLinesState: (tileState, startRow, endRow) -> + tileState.lines ?= {} + visibleLineIds = {} + row = startRow + while row < endRow + line = @model.tokenizedLineForScreenRow(row) + 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.top = (row - startRow) * @lineHeight + lineState.decorationClasses = @lineDecorationClassesForRow(row) + else + tileState.lines[line.id] = + screenRow: row + text: line.text + tokens: line.tokens + isOnlyWhitespace: line.isOnlyWhitespace() + endOfLineInvisibles: line.endOfLineInvisibles + indentLevel: line.indentLevel + tabLength: line.tabLength + fold: line.fold + top: (row - startRow) * @lineHeight + decorationClasses: @lineDecorationClassesForRow(row) + row++ + + for id, line of tileState.lines + delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) + updateCursorsState: -> @state.content.cursors = {} @updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation From 74a0ce0720860cbca2970d1a54e8606d6c837c8d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 May 2015 10:59:50 +0200 Subject: [PATCH 20/58] Remove require to LinesPresenter --- src/text-editor-presenter.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 9d8c22c3c..28d677734 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -2,7 +2,6 @@ {Point, Range} = require 'text-buffer' _ = require 'underscore-plus' Decoration = require './decoration' -LinesPresenter = require './lines-presenter' module.exports = class TextEditorPresenter From 4c2683a378d7a3089d1c435369ffb9276dd313a4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 May 2015 11:14:21 +0200 Subject: [PATCH 21/58] :art: Clarify `@updateTilesState` --- src/text-editor-presenter.coffee | 59 +++++++++++++++++--------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 28d677734..37b5daee1 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -300,43 +300,48 @@ class TextEditorPresenter @state.content.backgroundColor = if @model.isMini() then null else @backgroundColor @state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null + # REFACTOR: This should be a @field rather than a function. + linesPerTile: -> + linesPerTile = Math.floor(@height / @lineHeight / @tileCount) + Math.max(1, linesPerTile) + + tileForRow: (row) -> + row - (row % @linesPerTile()) + + getVisibleTileRange: -> + startTileRow = Math.max(0, @tileForRow(@startRow) - @tileOverdrawMargin) + endTileRow = Math.min( + @tileForRow(@model.getScreenLineCount()), + @tileForRow(@endRow) + @tileOverdrawMargin + ) + + [startTileRow..endTileRow] + updateTilesState: -> return unless @startRow? and @endRow? and @lineHeight? - linesPerTile = Math.floor(@height / @lineHeight / @tileCount) - linesPerTile = Math.max(1, linesPerTile) - - startIndex = Math.max( - 0, Math.floor(@startRow / linesPerTile) - @tileOverdrawMargin - ) - endIndex = Math.min( - Math.ceil(@model.getScreenLineCount() / linesPerTile), - Math.ceil(@endRow / linesPerTile) + @tileOverdrawMargin - ) - visibleTiles = {} - for index in [startIndex...endIndex] - startRow = Math.floor(index * linesPerTile) - endRow = Math.ceil(Math.min(@model.getScreenLineCount(), (index + 1) * linesPerTile)) + for startRow in @getVisibleTileRange() by @linesPerTile() + endRow = Math.min(@model.getScreenLineCount(), startRow + @linesPerTile()) - isNewTile = not @state.content.tiles.hasOwnProperty(index) - tile = @state.content.tiles[index] ?= {} - tile.top = (index * linesPerTile * @lineHeight) - @scrollTop - tile.height = linesPerTile * @lineHeight + isNewTile = not @state.content.tiles.hasOwnProperty(startRow) + tile = @state.content.tiles[startRow] ?= {} + tile.top = startRow * @lineHeight - @scrollTop + tile.height = @linesPerTile() * @lineHeight tile.newlyCreated = isNewTile tile.display = "block" @updateLinesState(tile, startRow, endRow) - visibleTiles[index] = true + visibleTiles[startRow] = true - for index, tile of @state.content.tiles - continue if visibleTiles.hasOwnProperty(index) + for id, tile of @state.content.tiles + continue if visibleTiles.hasOwnProperty(id) - if index is @scrollingTile + if id is @scrollingTileId tile.display = "none" else - delete @state.content.tiles[index] + delete @state.content.tiles[id] updateLinesState: (tileState, startRow, endRow) -> @@ -767,8 +772,8 @@ class TextEditorPresenter didStopScrolling: -> @state.content.scrollingVertically = false - if @scrollingTile? - @scrollingTile = null + if @scrollingTileId? + @scrollingTileId = null @shouldUpdateTilesState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true @@ -935,8 +940,8 @@ class TextEditorPresenter @emitDidUpdateState() setScrollingTileId: (tileId) -> - if @scrollingTile isnt tileId - @scrollingTile = tileId + if @scrollingTileId isnt tileId + @scrollingTileId = tileId @didStartScrolling() setBaseCharacterWidth: (baseCharacterWidth) -> From 948b8f95339b3f335b79724c476b6374cf5c5de4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 May 2015 11:22:02 +0200 Subject: [PATCH 22/58] Remember to fix scrolling for line-numbers --- src/text-editor-presenter.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 37b5daee1..e5eadbbcd 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -550,6 +550,7 @@ class TextEditorPresenter @state.gutters.lineNumberGutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable} visibleLineNumberIds[id] = true + # FIXME: We should either rely on @mouseWheelScreenRow or convert this to use tiles. if @mouseWheelScreenRow? bufferRow = @model.bufferRowForScreenRow(@mouseWheelScreenRow) wrapCount = @mouseWheelScreenRow - @model.screenRowForBufferRow(bufferRow) From 94373e434c5e52f64a8890bb7ee1fa90dd4f057d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 May 2015 11:26:02 +0200 Subject: [PATCH 23/58] Make `tileSize` a field rather than a fn --- 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 e5eadbbcd..f5fb1257a 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -67,6 +67,7 @@ class TextEditorPresenter @updateScrollbarDimensions() @updateStartRow() @updateEndRow() + @updateTileSize() @updateFocusedState() if @shouldUpdateFocusedState @updateHeightState() if @shouldUpdateHeightState @@ -300,15 +301,10 @@ class TextEditorPresenter @state.content.backgroundColor = if @model.isMini() then null else @backgroundColor @state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null - # REFACTOR: This should be a @field rather than a function. - linesPerTile: -> - linesPerTile = Math.floor(@height / @lineHeight / @tileCount) - Math.max(1, linesPerTile) - tileForRow: (row) -> - row - (row % @linesPerTile()) + row - (row % @tileSize) - getVisibleTileRange: -> + getVisibleTilesRange: -> startTileRow = Math.max(0, @tileForRow(@startRow) - @tileOverdrawMargin) endTileRow = Math.min( @tileForRow(@model.getScreenLineCount()), @@ -321,13 +317,13 @@ class TextEditorPresenter return unless @startRow? and @endRow? and @lineHeight? visibleTiles = {} - for startRow in @getVisibleTileRange() by @linesPerTile() - endRow = Math.min(@model.getScreenLineCount(), startRow + @linesPerTile()) + for startRow in @getVisibleTilesRange() by @tileSize + endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize) isNewTile = not @state.content.tiles.hasOwnProperty(startRow) tile = @state.content.tiles[startRow] ?= {} tile.top = startRow * @lineHeight - @scrollTop - tile.height = @linesPerTile() * @lineHeight + tile.height = @tileSize * @lineHeight tile.newlyCreated = isNewTile tile.display = "block" @@ -343,7 +339,6 @@ class TextEditorPresenter else delete @state.content.tiles[id] - updateLinesState: (tileState, startRow, endRow) -> tileState.lines ?= {} visibleLineIds = {} @@ -577,6 +572,12 @@ class TextEditorPresenter endRow = startRow + visibleLinesCount @endRow = Math.min(@model.getScreenLineCount(), endRow) + updateTileSize: -> + return unless @height? and @lineHeight? and @tileCount? + + @tileSize = Math.floor(@height / @lineHeight / @tileCount) + @tileSize = Math.max(1, @tileSize) + updateScrollWidth: -> return unless @contentWidth? and @clientWidth? From 11a0fa7a1246901810cbf3fcedc00739c94931c0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 May 2015 11:39:28 +0200 Subject: [PATCH 24/58] updateTileSize on start --- spec/text-editor-presenter-spec.coffee | 18 ++++++++---------- src/text-editor-presenter.coffee | 1 + 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 6db369f69..98c4caecb 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -658,28 +658,26 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.placeholderText).toBe "new-placeholder-text" describe ".tiles", -> - it "contains states for tiles that are visible on screen, plus and minus the overdraw margin", -> - presenter = buildPresenter(explicitHeight: 3, scrollTop: 1, lineHeight: 1, tileCount: 3, tileOverdrawMargin: 1) + fffit "contains states for tiles that are visible on screen", -> + presenter = buildPresenter(explicitHeight: 3, scrollTop: 1, lineHeight: 1, tileCount: 3) + console.log JSON.stringify(presenter.getState().content.tiles) expectValues presenter.getState().content.tiles[0], { - top: -1 - } - expectValues presenter.getState().content.tiles[1], { top: 0 } - expectValues presenter.getState().content.tiles[2], { + expectValues presenter.getState().content.tiles[1], { top: 1 } - expectValues presenter.getState().content.tiles[3], { + expectValues presenter.getState().content.tiles[2], { top: 2 } - expectValues presenter.getState().content.tiles[4], { + expectValues presenter.getState().content.tiles[3], { top: 3 } - expectValues presenter.getState().content.tiles[5], { + expectValues presenter.getState().content.tiles[4], { top: 4 } - expect(presenter.getState().content.tiles[6]).toBeUndefined() + expect(presenter.getState().content.tiles[5]).toBeUndefined() it "does not overdraw above the first row", -> presenter = buildPresenter(explicitHeight: 3, scrollTop: 0, lineHeight: 1, tileCount: 3, tileOverdrawMargin: 1) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index f5fb1257a..a8d845c72 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -223,6 +223,7 @@ class TextEditorPresenter @updateScrollbarDimensions() @updateStartRow() @updateEndRow() + @updateTileSize() @updateFocusedState() @updateHeightState() From 29c6e9d89cbef26cd55ce4fa0de89bfae17e4f00 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 May 2015 11:57:38 +0200 Subject: [PATCH 25/58] :green_heart: Start fixing presenter specs --- spec/text-editor-presenter-spec.coffee | 105 +++++++++++-------------- 1 file changed, 46 insertions(+), 59 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 98c4caecb..e0fbda5ae 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -658,53 +658,32 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.placeholderText).toBe "new-placeholder-text" describe ".tiles", -> - fffit "contains states for tiles that are visible on screen", -> - presenter = buildPresenter(explicitHeight: 3, scrollTop: 1, lineHeight: 1, tileCount: 3) + it "contains states for tiles that are visible on screen", -> + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3) - console.log JSON.stringify(presenter.getState().content.tiles) expectValues presenter.getState().content.tiles[0], { top: 0 } - expectValues presenter.getState().content.tiles[1], { - top: 1 - } expectValues presenter.getState().content.tiles[2], { top: 2 } - expectValues presenter.getState().content.tiles[3], { - top: 3 - } expectValues presenter.getState().content.tiles[4], { top: 4 } - expect(presenter.getState().content.tiles[5]).toBeUndefined() + expectValues presenter.getState().content.tiles[6], { + top: 6 + } - it "does not overdraw above the first row", -> - presenter = buildPresenter(explicitHeight: 3, scrollTop: 0, lineHeight: 1, tileCount: 3, tileOverdrawMargin: 1) - expect(presenter.getState().content.tiles[-1]).toBeUndefined() - expect(presenter.getState().content.tiles[0]).toBeDefined() - expect(presenter.getState().content.tiles[1]).toBeDefined() - expect(presenter.getState().content.tiles[2]).toBeDefined() - expect(presenter.getState().content.tiles[3]).toBeDefined() - expect(presenter.getState().content.tiles[4]).toBeDefined() - expect(presenter.getState().content.tiles[5]).toBeUndefined() - - it "does not overdraw below the last row", -> - presenter = buildPresenter(explicitHeight: 3, scrollTop: 10, lineHeight: 1, tileCount: 3, tileOverdrawMargin: 1) expect(presenter.getState().content.tiles[8]).toBeUndefined() - expect(presenter.getState().content.tiles[9]).toBeDefined() - expect(presenter.getState().content.tiles[10]).toBeDefined() - expect(presenter.getState().content.tiles[11]).toBeDefined() - expect(presenter.getState().content.tiles[12]).toBeDefined() - expect(presenter.getState().content.tiles[13]).toBeUndefined() - xit "includes state for all tiles if no external ::explicitHeight is assigned", -> + + it "includes state for all tiles if no external ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null, tileCount: 12, tileOverdrawMargin: 1) expect(presenter.getState().content.tiles[0]).toBeDefined() expect(presenter.getState().content.tiles[12]).toBeDefined() it "is empty until all of the required measurements are assigned", -> - presenter = buildPresenter(explicitHeight: null, lineHeight: null, scrollTop: null, tileCount: 3, tileOverdrawMargin: 1) + presenter = buildPresenter(explicitHeight: null, lineHeight: null, scrollTop: null, tileCount: 3) expect(presenter.getState().content.tiles).toEqual({}) presenter.setExplicitHeight(25) @@ -717,48 +696,57 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.tiles).not.toEqual({}) it "updates when ::scrollTop changes", -> - presenter = buildPresenter(explicitHeight: 3, scrollTop: 0, lineHeight: 1, tileOverdrawMargin: 1, tileCount: 3) + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3) expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeDefined() expect(presenter.getState().content.tiles[4]).toBeDefined() - expect(presenter.getState().content.tiles[5]).toBeUndefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[8]).toBeUndefined() expectStateUpdate presenter, -> presenter.setScrollTop(2) expect(presenter.getState().content.tiles[0]).toBeUndefined() - expect(presenter.getState().content.tiles[1]).toBeDefined() - expect(presenter.getState().content.tiles[6]).toBeDefined() - expect(presenter.getState().content.tiles[7]).toBeUndefined() - - xit "updates when ::explicitHeight changes", -> - presenter = buildPresenter(explicitHeight: 3, scrollTop: 0, lineHeight: 1, tileOverdrawMargin: 1, tileCount: 3) - - line5 = editor.tokenizedLineForScreenRow(5) - + expect(presenter.getState().content.tiles[2]).toBeDefined() expect(presenter.getState().content.tiles[4]).toBeDefined() - expect(presenter.getState().content.tiles[5]).toBeUndefined() - - expectStateUpdate presenter, -> presenter.setExplicitHeight(35) - - expect(presenter.getState().content.tiles[5]).toBeDefined() expect(presenter.getState().content.tiles[6]).toBeDefined() - expect(presenter.getState().content.tiles[7]).toBeUndefined() + expect(presenter.getState().content.tiles[8]).toBeDefined() + expect(presenter.getState().content.tiles[10]).toBeUndefined() - xit "updates when ::lineHeight changes", -> - presenter = buildPresenter(explicitHeight: 15, scrollTop: 10, lineHeight: 10, lineOverdrawMargin: 0) + it "updates when ::explicitHeight changes", -> + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3) - expect(lineStateForScreenRow(presenter, 0)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 1)).toBeDefined() - expect(lineStateForScreenRow(presenter, 2)).toBeDefined() - expect(lineStateForScreenRow(presenter, 4)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeDefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[8]).toBeUndefined() - expectStateUpdate presenter, -> presenter.setLineHeight(5) + expectStateUpdate presenter, -> presenter.setExplicitHeight(8) - expect(lineStateForScreenRow(presenter, 0)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 1)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 2)).toBeDefined() - expect(lineStateForScreenRow(presenter, 5)).toBeDefined() - expect(lineStateForScreenRow(presenter, 6)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeDefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[8]).toBeDefined() + expect(presenter.getState().content.tiles[10]).toBeUndefined() + + + it "updates when ::lineHeight changes", -> + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3) + + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeDefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[8]).toBeUndefined() + + expectStateUpdate presenter, -> presenter.setLineHeight(2) + + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeDefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeUndefined() it "updates when the editor's content changes", -> lineStateForScreenRow = (presenter, tile, row) -> @@ -788,7 +776,6 @@ describe "TextEditorPresenter", -> } xdescribe ".lines", -> - it "does not remove out-of-view lines corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1, stoppedScrollingDelay: 200) From 49c48234f2d094f64d6da19fd7b7bae267cbd770 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 May 2015 12:26:20 +0200 Subject: [PATCH 26/58] :green_heart: Finish fixing tiles/lines specs --- spec/text-editor-presenter-spec.coffee | 76 ++++++++++++-------------- src/text-editor-presenter.coffee | 19 ++++--- 2 files changed, 44 insertions(+), 51 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index e0fbda5ae..7c5c9d6a3 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -658,6 +658,11 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.placeholderText).toBe "new-placeholder-text" describe ".tiles", -> + lineStateForScreenRow = (presenter, row) -> + lineId = presenter.model.tokenizedLineForScreenRow(row).id + tileRow = presenter.tileForRow(row) + presenter.getState().content.tiles[tileRow].lines[lineId] + it "contains states for tiles that are visible on screen", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3) @@ -676,7 +681,6 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.tiles[8]).toBeUndefined() - it "includes state for all tiles if no external ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null, tileCount: 12, tileOverdrawMargin: 1) expect(presenter.getState().content.tiles[0]).toBeDefined() @@ -749,80 +753,68 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.tiles[6]).toBeUndefined() it "updates when the editor's content changes", -> - lineStateForScreenRow = (presenter, tile, row) -> - lineId = presenter.model.tokenizedLineForScreenRow(row).id - presenter.getState().content.tiles[tile].lines[lineId] - presenter = buildPresenter(explicitHeight: 25, scrollTop: 10, lineHeight: 10, tileCount: 3, tileOverdrawMargin: 1) expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n") line1 = editor.tokenizedLineForScreenRow(1) - expectValues lineStateForScreenRow(presenter, 1, 1), { + expectValues lineStateForScreenRow(presenter, 1), { text: line1.text tokens: line1.tokens } line2 = editor.tokenizedLineForScreenRow(2) - expectValues lineStateForScreenRow(presenter, 2, 2), { + expectValues lineStateForScreenRow(presenter, 2), { text: line2.text tokens: line2.tokens } line3 = editor.tokenizedLineForScreenRow(3) - expectValues lineStateForScreenRow(presenter, 3, 3), { + expectValues lineStateForScreenRow(presenter, 3), { text: line3.text tokens: line3.tokens } - xdescribe ".lines", -> - it "does not remove out-of-view lines corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1, stoppedScrollingDelay: 200) + it "does not remove out-of-view tiles corresponding to ::scrollingTileId until ::stoppedScrollingDelay elapses", -> + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3, stoppedScrollingDelay: 200) - expect(lineStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineStateForScreenRow(presenter, 4)).toBeDefined() - expect(lineStateForScreenRow(presenter, 5)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[8]).toBeUndefined() - presenter.setMouseWheelScreenRow(0) - expectStateUpdate presenter, -> presenter.setScrollTop(35) + presenter.setScrollingTileId(0) + expectStateUpdate presenter, -> presenter.setScrollTop(4) - expect(lineStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineStateForScreenRow(presenter, 1)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 7)).toBeDefined() - expect(lineStateForScreenRow(presenter, 8)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeUndefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[12]).toBeUndefined() expectStateUpdate presenter, -> advanceClock(200) - expect(lineStateForScreenRow(presenter, 0)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 1)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 2)).toBeDefined() - expect(lineStateForScreenRow(presenter, 7)).toBeDefined() - expect(lineStateForScreenRow(presenter, 8)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeUndefined() + expect(presenter.getState().content.tiles[2]).toBeUndefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[12]).toBeUndefined() + # should clear ::mouseWheelScreenRow after stoppedScrollingDelay elapses even if we don't scroll first - presenter.setMouseWheelScreenRow(2) + presenter.setScrollingTileId(4) advanceClock(200) - expectStateUpdate presenter, -> presenter.setScrollTop(45) - expect(lineStateForScreenRow(presenter, 2)).toBeUndefined() + expectStateUpdate presenter, -> presenter.setScrollTop(6) + expect(presenter.getState().content.tiles[4]).toBeUndefined() - it "does not preserve on-screen lines even if they correspond to ::mouseWheelScreenRow", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1, stoppedScrollingDelay: 200) - oldLine3 = editor.tokenizedLineForScreenRow(6) + it "does not preserve deleted on-screen tiles even if they correspond to ::scrollingTileId", -> + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3, stoppedScrollingDelay: 200) - presenter.setMouseWheelScreenRow(3) + presenter.setScrollingTileId(2) - expectStateUpdate presenter, -> editor.getBuffer().insert([3, Infinity], 'xyz') - newLine3 = editor.tokenizedLineForScreenRow(3) + expectStateUpdate presenter, -> editor.setText("") - expect(presenter.getState().content.lines[oldLine3.id]).toBeUndefined() - expect(presenter.getState().content.lines[newLine3.id]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() - it "does not attempt to preserve lines corresponding to ::mouseWheelScreenRow if they have been deleted", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1, stoppedScrollingDelay: 200) - presenter.setMouseWheelScreenRow(10) - editor.setText('') - - describe "[lineId]", -> # line state objects + describe "[tileId].lines[lineId]", -> # line state objects it "includes the .endOfLineInvisibles if the editor.showInvisibles config option is true", -> editor.setText("hello\nworld\r\n") presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index a8d845c72..7ca810cba 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -15,12 +15,11 @@ class TextEditorPresenter constructor: (params) -> {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params {horizontalScrollbarHeight, verticalScrollbarWidth} = params - {@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor, @tileCount, @tileOverdrawMargin} = params + {@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor, @tileCount} = params {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @measuredVerticalScrollbarWidth = verticalScrollbarWidth @gutterWidth ?= 0 - @tileOverdrawMargin ?= 0 @tileCount ?= 3 @disposables = new CompositeDisposable @@ -306,10 +305,9 @@ class TextEditorPresenter row - (row % @tileSize) getVisibleTilesRange: -> - startTileRow = Math.max(0, @tileForRow(@startRow) - @tileOverdrawMargin) + startTileRow = Math.max(0, @tileForRow(@startRow)) endTileRow = Math.min( - @tileForRow(@model.getScreenLineCount()), - @tileForRow(@endRow) + @tileOverdrawMargin + @tileForRow(@model.getScreenLineCount()), @tileForRow(@endRow) ) [startTileRow..endTileRow] @@ -332,13 +330,15 @@ class TextEditorPresenter visibleTiles[startRow] = true + if @scrollingTileId? + if @scrollingTileId <= @tileForRow(@model.getLastScreenRow()) + visibleTiles[@scrollingTileId] = true + @state.content.tiles[@scrollingTileId].display = "none" + for id, tile of @state.content.tiles continue if visibleTiles.hasOwnProperty(id) - if id is @scrollingTileId - tile.display = "none" - else - delete @state.content.tiles[id] + delete @state.content.tiles[id] updateLinesState: (tileState, startRow, endRow) -> tileState.lines ?= {} @@ -943,6 +943,7 @@ class TextEditorPresenter @emitDidUpdateState() setScrollingTileId: (tileId) -> + tileId = tileId.toString() if @scrollingTileId isnt tileId @scrollingTileId = tileId @didStartScrolling() From 6d38d1c4e0649ec3c564f576b25ceafb5e6265e7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 May 2015 12:30:44 +0200 Subject: [PATCH 27/58] :fire: Remove leftover code --- src/text-editor-presenter.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 7ca810cba..7d060f367 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -943,7 +943,6 @@ class TextEditorPresenter @emitDidUpdateState() setScrollingTileId: (tileId) -> - tileId = tileId.toString() if @scrollingTileId isnt tileId @scrollingTileId = tileId @didStartScrolling() From 467f4a30d7b8d8c4e1fe4b68f55208dac805bbc0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 May 2015 12:39:32 +0200 Subject: [PATCH 28/58] :art: Simplify conditional --- src/text-editor-presenter.coffee | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 7d060f367..82c050314 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -304,6 +304,11 @@ class TextEditorPresenter tileForRow: (row) -> row - (row % @tileSize) + isValidTile: (tileId) -> + return false unless tileId? + + tileId <= @tileForRow(@model.getLastScreenRow()) + getVisibleTilesRange: -> startTileRow = Math.max(0, @tileForRow(@startRow)) endTileRow = Math.min( @@ -330,10 +335,9 @@ class TextEditorPresenter visibleTiles[startRow] = true - if @scrollingTileId? - if @scrollingTileId <= @tileForRow(@model.getLastScreenRow()) - visibleTiles[@scrollingTileId] = true - @state.content.tiles[@scrollingTileId].display = "none" + if @isValidTile(@scrollingTileId) and not visibleTiles[@scrollingTileId]? + @state.content.tiles[@scrollingTileId].display = "none" + visibleTiles[@scrollingTileId] = true for id, tile of @state.content.tiles continue if visibleTiles.hasOwnProperty(id) From 25acaf26c19bdc0414104eacda604bb900baf396 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 13 May 2015 12:44:06 +0200 Subject: [PATCH 29/58] :art: `tileId` -> `tileRow` --- spec/text-editor-presenter-spec.coffee | 6 +++--- src/text-editor-component.coffee | 4 ++-- src/text-editor-presenter.coffee | 22 +++++++++++----------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 7c5c9d6a3..95f941fde 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -782,7 +782,7 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.tiles[6]).toBeDefined() expect(presenter.getState().content.tiles[8]).toBeUndefined() - presenter.setScrollingTileId(0) + presenter.setScrollingTileRow(0) expectStateUpdate presenter, -> presenter.setScrollTop(4) expect(presenter.getState().content.tiles[0]).toBeDefined() @@ -799,7 +799,7 @@ describe "TextEditorPresenter", -> # should clear ::mouseWheelScreenRow after stoppedScrollingDelay elapses even if we don't scroll first - presenter.setScrollingTileId(4) + presenter.setScrollingTileRow(4) advanceClock(200) expectStateUpdate presenter, -> presenter.setScrollTop(6) expect(presenter.getState().content.tiles[4]).toBeUndefined() @@ -807,7 +807,7 @@ describe "TextEditorPresenter", -> it "does not preserve deleted on-screen tiles even if they correspond to ::scrollingTileId", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3, stoppedScrollingDelay: 200) - presenter.setScrollingTileId(2) + presenter.setScrollingTileRow(2) expectStateUpdate presenter, -> editor.setText("") diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 8c497bd98..c97fb11a0 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -354,7 +354,7 @@ class TextEditorComponent event.preventDefault() unless previousScrollLeft is @presenter.getScrollLeft() else # Scrolling vertically - @presenter.setScrollingTileId(@tileIdForNode(event.target)) + @presenter.setScrollingTileRow(@tileRowForNode(event.target)) previousScrollTop = @presenter.getScrollTop() @presenter.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity)) event.preventDefault() unless previousScrollTop is @presenter.getScrollTop() @@ -729,7 +729,7 @@ class TextEditorComponent lineNumberNodeForScreenRow: (screenRow) -> @gutterContainerComponent.getLineNumberGutterComponent().lineNumberNodeForScreenRow(screenRow) - tileIdForNode: (node) -> + tileRowForNode: (node) -> while node? if tileId = node.dataset.tileId return tileId diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 82c050314..6b38f0114 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -304,10 +304,10 @@ class TextEditorPresenter tileForRow: (row) -> row - (row % @tileSize) - isValidTile: (tileId) -> - return false unless tileId? + isValidTile: (tileRow) -> + return false unless tileRow? - tileId <= @tileForRow(@model.getLastScreenRow()) + tileRow <= @tileForRow(@model.getLastScreenRow()) getVisibleTilesRange: -> startTileRow = Math.max(0, @tileForRow(@startRow)) @@ -335,9 +335,9 @@ class TextEditorPresenter visibleTiles[startRow] = true - if @isValidTile(@scrollingTileId) and not visibleTiles[@scrollingTileId]? - @state.content.tiles[@scrollingTileId].display = "none" - visibleTiles[@scrollingTileId] = true + if @isValidTile(@scrollingTileRow) and not visibleTiles[@scrollingTileRow]? + @state.content.tiles[@scrollingTileRow].display = "none" + visibleTiles[@scrollingTileRow] = true for id, tile of @state.content.tiles continue if visibleTiles.hasOwnProperty(id) @@ -779,8 +779,8 @@ class TextEditorPresenter didStopScrolling: -> @state.content.scrollingVertically = false - if @scrollingTileId? - @scrollingTileId = null + if @scrollingTileRow? + @scrollingTileRow = null @shouldUpdateTilesState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true @@ -946,9 +946,9 @@ class TextEditorPresenter @emitDidUpdateState() - setScrollingTileId: (tileId) -> - if @scrollingTileId isnt tileId - @scrollingTileId = tileId + setScrollingTileRow: (tileRow) -> + if @scrollingTileRow isnt tileRow + @scrollingTileRow = tileRow @didStartScrolling() setBaseCharacterWidth: (baseCharacterWidth) -> From 6be88fd645a199e1b68e59e46a549f2371f69160 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 14 May 2015 11:36:20 +0200 Subject: [PATCH 30/58] Scroll every single tile left/right --- src/lines-component.coffee | 4 ---- src/text-editor-component.coffee | 5 ++--- src/text-editor-presenter.coffee | 10 ++++++---- src/tile-component.coffee | 5 +++-- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index fda77a76a..c4341bbbf 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -48,10 +48,6 @@ class LinesComponent @domNode.style.height = @newState.scrollHeight + 'px' @oldState.scrollHeight = @newState.scrollHeight - if @newState.scrollLeft isnt @oldState.scrollLeft - @domNode.style['-webkit-transform'] = "translate3d(#{-@newState.scrollLeft}px, 0, 0px)" - @oldState.scrollLeft = @newState.scrollLeft - if @newState.backgroundColor isnt @oldState.backgroundColor @domNode.style.backgroundColor = @newState.backgroundColor @oldState.backgroundColor = @newState.backgroundColor diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index c97fb11a0..ce17243e4 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -764,15 +764,14 @@ class TextEditorComponent screenPositionForMouseEvent: (event) -> pixelPosition = @pixelPositionForMouseEvent(event) - pixelPosition.top += @presenter.scrollTop @editor.screenPositionForPixelPosition(pixelPosition) pixelPositionForMouseEvent: (event) -> {clientX, clientY} = event linesClientRect = @linesComponent.getDomNode().getBoundingClientRect() - top = clientY - linesClientRect.top - left = clientX - linesClientRect.left + top = clientY - linesClientRect.top + @presenter.scrollTop + left = clientX - linesClientRect.left + @presenter.scrollLeft {top, left} getModel: -> diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 6b38f0114..0418fb6c2 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -327,6 +327,7 @@ class TextEditorPresenter isNewTile = not @state.content.tiles.hasOwnProperty(startRow) tile = @state.content.tiles[startRow] ?= {} tile.top = startRow * @lineHeight - @scrollTop + tile.left = -@scrollLeft tile.height = @tileSize * @lineHeight tile.newlyCreated = isNewTile tile.display = "block" @@ -410,10 +411,8 @@ class TextEditorPresenter pixelPosition = @pixelPositionForScreenPosition(screenPosition, true) - {scrollLeft} = @state.content - top = pixelPosition.top + @lineHeight - left = pixelPosition.left + @gutterWidth - scrollLeft + left = pixelPosition.left + @gutterWidth if overlayDimensions = @overlayDimensions[decoration.id] {itemWidth, itemHeight, contentMargin} = overlayDimensions @@ -795,8 +794,10 @@ class TextEditorPresenter @model.setScrollLeft(scrollLeft) @shouldUpdateHorizontalScrollState = true @shouldUpdateHiddenInputState = true - @shouldUpdateCursorsState = true unless oldScrollLeft? + @shouldUpdateCursorsState = true @shouldUpdateOverlaysState = true + @shouldUpdateDecorations = true + @shouldUpdateTilesState = true @emitDidUpdateState() @@ -1034,6 +1035,7 @@ class TextEditorPresenter column += charLength top -= @scrollTop + left -= @scrollLeft {top, left} hasPixelRectRequirements: -> diff --git a/src/tile-component.coffee b/src/tile-component.coffee index 5bd299d79..197a9adce 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -43,9 +43,10 @@ class TileComponent @domNode.style.height = @newState.tiles[@id].height + 'px' @oldState.tiles[@id]?.height = @newState.tiles[@id].height - if @newState.tiles[@id].top isnt @oldState.tiles[@id]?.top - @domNode.style['-webkit-transform'] = "translate3d(0, #{@newState.tiles[@id].top}px, 0px)" + if (@newState.tiles[@id].top isnt @oldState.tiles[@id]?.top) or (@newState.tiles[@id].left isnt @oldState.tiles[@id]?.left) + @domNode.style['-webkit-transform'] = "translate3d(#{@newState.tiles[@id].left}px, #{@newState.tiles[@id].top}px, 0px)" @oldState.tiles[@id]?.top = @newState.tiles[@id].top + @oldState.tiles[@id]?.left = @newState.tiles[@id].left if @newState.tiles[@id].newlyCreated newLineIds = [] From c4dff15c736022f4129c58feb33b189901ee2a81 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 14 May 2015 14:57:04 +0200 Subject: [PATCH 31/58] Add `@scrollLeft` when calculating `@contentWidth` --- src/text-editor-presenter.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 0418fb6c2..1caa666df 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -612,6 +612,7 @@ class TextEditorPresenter oldContentWidth = @contentWidth clip = @model.tokenizedLineForScreenRow(@model.getLongestScreenRow())?.isSoftWrapped() @contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), @model.getMaxScreenLineLength()], clip).left + @contentWidth += @scrollLeft @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width if @contentHeight isnt oldContentHeight From 5b23a002cf6b01f94619153b012f6d926cf7d243 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 14 May 2015 15:18:53 +0200 Subject: [PATCH 32/58] :green_heart: Fix position specs --- spec/text-editor-presenter-spec.coffee | 56 +++++++++++++------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 95f941fde..a13cc2dce 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -343,26 +343,26 @@ describe "TextEditorPresenter", -> expectValues presenter.getState().hiddenInput, {top: 3 * 10, left: 6 * 10} expectStateUpdate presenter, -> presenter.setScrollTop(15) - expectValues presenter.getState().hiddenInput, {top: (3 * 10) - 15, left: 6 * 10} + expectValues presenter.getState().hiddenInput, {top: 0, left: 6 * 10} expectStateUpdate presenter, -> presenter.setScrollLeft(35) - expectValues presenter.getState().hiddenInput, {top: (3 * 10) - 15, left: (6 * 10) - 35} + expectValues presenter.getState().hiddenInput, {top: 0, left: 0} expectStateUpdate presenter, -> presenter.setScrollTop(40) - expectValues presenter.getState().hiddenInput, {top: 0, left: (6 * 10) - 35} + expectValues presenter.getState().hiddenInput, {top: 0, left: 0} expectStateUpdate presenter, -> presenter.setScrollLeft(70) expectValues presenter.getState().hiddenInput, {top: 0, left: 0} expectStateUpdate presenter, -> editor.setCursorBufferPosition([11, 43]) - expectValues presenter.getState().hiddenInput, {top: 11 * 10 - editor.getScrollTop(), left: 43 * 10 - editor.getScrollLeft()} + expectValues presenter.getState().hiddenInput, {top: 0, left: 30} newCursor = null expectStateUpdate presenter, -> newCursor = editor.addCursorAtBufferPosition([6, 10]) - expectValues presenter.getState().hiddenInput, {top: (6 * 10) - editor.getScrollTop(), left: (10 * 10) - editor.getScrollLeft()} + expectValues presenter.getState().hiddenInput, {top: 0, left: 20} expectStateUpdate presenter, -> newCursor.destroy() - expectValues presenter.getState().hiddenInput, {top: 50 - 10, left: 300 - 10} + expectValues presenter.getState().hiddenInput, {top: 30, left: 300 - 10} expectStateUpdate presenter, -> presenter.setFocused(false) expectValues presenter.getState().hiddenInput, {top: 0, left: 0} @@ -967,9 +967,9 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(explicitHeight: 30, scrollTop: 20) expect(stateForCursor(presenter, 0)).toBeUndefined() - expect(stateForCursor(presenter, 1)).toEqual {top: 2 * 10, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 1)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10} expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10 - 20, left: 12 * 10, width: 10, height: 10} expect(stateForCursor(presenter, 4)).toBeUndefined() it "is empty until all of the required measurements are assigned", -> @@ -1005,8 +1005,8 @@ describe "TextEditorPresenter", -> expect(stateForCursor(presenter, 0)).toBeUndefined() expect(stateForCursor(presenter, 1)).toBeUndefined() expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} - expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 3)).toEqual {top: 0, left: 12 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10 - 50, left: 4 * 10, width: 10, height: 10} it "updates when ::explicitHeight changes", -> editor.setSelectedBufferRanges([ @@ -1020,9 +1020,9 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setExplicitHeight(30) expect(stateForCursor(presenter, 0)).toBeUndefined() - expect(stateForCursor(presenter, 1)).toEqual {top: 2 * 10, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 1)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10} expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10 - 20, left: 12 * 10, width: 10, height: 10} expect(stateForCursor(presenter, 4)).toBeUndefined() it "updates when ::lineHeight changes", -> @@ -1039,15 +1039,15 @@ describe "TextEditorPresenter", -> expect(stateForCursor(presenter, 0)).toBeUndefined() expect(stateForCursor(presenter, 1)).toBeUndefined() expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 5, left: 12 * 10, width: 10, height: 5} - expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 5, left: 4 * 10, width: 10, height: 5} + 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: 2 * 10, left: 4 * 20, width: 20, height: 10} + expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 20, width: 20, height: 10} it "updates when scoped character widths change", -> waitsForPromise -> @@ -1073,11 +1073,11 @@ describe "TextEditorPresenter", -> # moving into view expect(stateForCursor(presenter, 0)).toBeUndefined() editor.getCursors()[0].setBufferPosition([2, 4]) - expect(stateForCursor(presenter, 0)).toEqual {top: 2 * 10, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10} # showing expectStateUpdate presenter, -> editor.getSelections()[1].clear() - expect(stateForCursor(presenter, 1)).toEqual {top: 3 * 10, left: 5 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 1)).toEqual {top: 5, left: 5 * 10, width: 10, height: 10} # hiding expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 5]]) @@ -1089,11 +1089,11 @@ describe "TextEditorPresenter", -> # adding expectStateUpdate presenter, -> editor.addCursorAtBufferPosition([4, 4]) - expect(stateForCursor(presenter, 2)).toEqual {top: 4 * 10, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 2)).toEqual {top: 5, left: 4 * 10, width: 10, height: 10} # moving added cursor expectStateUpdate presenter, -> editor.getCursors()[2].setBufferPosition([4, 6]) - expect(stateForCursor(presenter, 2)).toEqual {top: 4 * 10, left: 6 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 2)).toEqual {top: 5, left: 6 * 10, width: 10, height: 10} # destroying destroyedCursor = editor.getCursors()[2] @@ -1211,39 +1211,39 @@ describe "TextEditorPresenter", -> expectValues stateForHighlight(presenter, highlight2), { class: 'b' regions: [ - {top: 2 * 10, left: 0 * 10, width: 6 * 10, height: 1 * 10} + {top: 2 * 10 - 20, left: 0 * 10, width: 6 * 10, height: 1 * 10} ] } expectValues stateForHighlight(presenter, highlight3), { class: 'c' regions: [ - {top: 2 * 10, left: 0 * 10, right: 0, height: 1 * 10} - {top: 3 * 10, left: 0 * 10, width: 6 * 10, height: 1 * 10} + {top: 2 * 10 - 20, left: 0 * 10, right: 0, height: 1 * 10} + {top: 3 * 10 - 20, left: 0 * 10, width: 6 * 10, height: 1 * 10} ] } expectValues stateForHighlight(presenter, highlight4), { class: 'd' regions: [ - {top: 2 * 10, left: 6 * 10, right: 0, height: 1 * 10} - {top: 3 * 10, left: 0, right: 0, height: 1 * 10} - {top: 4 * 10, left: 0, width: 6 * 10, height: 1 * 10} + {top: 2 * 10 - 20, left: 6 * 10, right: 0, height: 1 * 10} + {top: 3 * 10 - 20, left: 0, right: 0, height: 1 * 10} + {top: 4 * 10 - 20, left: 0, width: 6 * 10, height: 1 * 10} ] } expectValues stateForHighlight(presenter, highlight5), { class: 'e' regions: [ - {top: 3 * 10, left: 6 * 10, right: 0, height: 1 * 10} - {top: 4 * 10, left: 0 * 10, right: 0, height: 2 * 10} + {top: 3 * 10 - 20, left: 6 * 10, right: 0, height: 1 * 10} + {top: 4 * 10 - 20, left: 0 * 10, right: 0, height: 2 * 10} ] } expectValues stateForHighlight(presenter, highlight6), { class: 'f' regions: [ - {top: 5 * 10, left: 6 * 10, right: 0, height: 1 * 10} + {top: 5 * 10 - 20, left: 6 * 10, right: 0, height: 1 * 10} ] } From e7ddb3d8ad61c3ba34dcc4b15cd38890bacf58db Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 14 May 2015 15:54:09 +0200 Subject: [PATCH 33/58] :green_heart: Fix all TextEditorPresenter specs --- spec/text-editor-presenter-spec.coffee | 76 +++++++++++++------------- src/text-editor-component.coffee | 9 ++- src/text-editor-presenter.coffee | 24 ++++---- 3 files changed, 52 insertions(+), 57 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index a13cc2dce..8d5a4c0a8 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -39,7 +39,6 @@ describe "TextEditorPresenter", -> verticalScrollbarWidth: 10 scrollTop: 0 scrollLeft: 0 - lineOverdrawMargin: 0 new TextEditorPresenter(params) @@ -775,14 +774,14 @@ describe "TextEditorPresenter", -> tokens: line3.tokens } - it "does not remove out-of-view tiles corresponding to ::scrollingTileId until ::stoppedScrollingDelay elapses", -> + it "does not remove out-of-view tiles corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3, stoppedScrollingDelay: 200) expect(presenter.getState().content.tiles[0]).toBeDefined() expect(presenter.getState().content.tiles[6]).toBeDefined() expect(presenter.getState().content.tiles[8]).toBeUndefined() - presenter.setScrollingTileRow(0) + presenter.setMouseWheelScreenRow(0) expectStateUpdate presenter, -> presenter.setScrollTop(4) expect(presenter.getState().content.tiles[0]).toBeDefined() @@ -799,15 +798,15 @@ describe "TextEditorPresenter", -> # should clear ::mouseWheelScreenRow after stoppedScrollingDelay elapses even if we don't scroll first - presenter.setScrollingTileRow(4) + presenter.setMouseWheelScreenRow(4) advanceClock(200) expectStateUpdate presenter, -> presenter.setScrollTop(6) expect(presenter.getState().content.tiles[4]).toBeUndefined() - it "does not preserve deleted on-screen tiles even if they correspond to ::scrollingTileId", -> + it "does not preserve deleted on-screen tiles even if they correspond to ::mouseWheelScreenRow", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3, stoppedScrollingDelay: 200) - presenter.setScrollingTileRow(2) + presenter.setMouseWheelScreenRow(2) expectStateUpdate presenter, -> editor.setText("") @@ -1750,20 +1749,19 @@ describe "TextEditorPresenter", -> presenter.getState().gutters.lineNumberGutter.lineNumbers[key] - it "contains states for line numbers that are visible on screen, plus and minus the overdraw margin", -> + it "contains states for line numbers that are visible on screen", -> editor.foldBufferRow(4) editor.setSoftWrapped(true) editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, lineOverdrawMargin: 1) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10) expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {screenRow: 2, bufferRow: 2, softWrapped: false, top: 2 * 10} + expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() expectValues lineNumberStateForScreenRow(presenter, 3), {screenRow: 3, bufferRow: 3, softWrapped: false, top: 3 * 10} expectValues lineNumberStateForScreenRow(presenter, 4), {screenRow: 4, bufferRow: 3, softWrapped: true, top: 4 * 10} expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false, top: 5 * 10} expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 7, softWrapped: false, top: 6 * 10} - expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 8, softWrapped: false, top: 7 * 10} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() it "includes states for all line numbers if no ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null) @@ -1774,43 +1772,43 @@ describe "TextEditorPresenter", -> editor.foldBufferRow(4) editor.setSoftWrapped(true) editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineOverdrawMargin: 1) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 30) - expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} - expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} + expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} + expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() expectStateUpdate presenter, -> presenter.setScrollTop(20) - expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 1), {bufferRow: 1} - expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} - expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} + expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4} + expect(lineNumberStateForScreenRow(presenter, 6)).toBeUndefined() it "updates when ::explicitHeight changes", -> editor.foldBufferRow(4) editor.setSoftWrapped(true) editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineOverdrawMargin: 1) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 30) - expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} - expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} + expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} + expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() expectStateUpdate presenter, -> presenter.setExplicitHeight(35) - expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} - expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 9)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} + expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} + expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() it "updates when ::lineHeight changes", -> editor.foldBufferRow(4) editor.setSoftWrapped(true) editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineOverdrawMargin: 0) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 0) expectValues lineNumberStateForScreenRow(presenter, 0), {bufferRow: 0} expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} @@ -1826,7 +1824,7 @@ describe "TextEditorPresenter", -> editor.foldBufferRow(4) editor.setSoftWrapped(true) editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 35, scrollTop: 30, lineOverdrawMargin: 0) + presenter = buildPresenter(explicitHeight: 35, scrollTop: 30) expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} @@ -1848,26 +1846,26 @@ describe "TextEditorPresenter", -> expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() it "does not remove out-of-view line numbers corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> - presenter = buildPresenter(explicitHeight: 25, lineOverdrawMargin: 1, stoppedScrollingDelay: 200) + presenter = buildPresenter(explicitHeight: 25, stoppedScrollingDelay: 200) expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 4)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 5)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 3)).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 4)).toBeUndefined() presenter.setMouseWheelScreenRow(0) expectStateUpdate presenter, -> presenter.setScrollTop(35) expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined() expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expect(lineNumberStateForScreenRow(presenter, 7)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 6)).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() expectStateUpdate presenter, -> advanceClock(200) expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined() expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expect(lineNumberStateForScreenRow(presenter, 7)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 6)).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() it "correctly handles the first screen line being soft-wrapped", -> editor.setSoftWrapped(true) @@ -2481,7 +2479,7 @@ describe "TextEditorPresenter", -> editor.setEditorWidthInChars(80) presenterParams = model: editor - lineOverdrawMargin: 1 + presenter = new TextEditorPresenter(presenterParams) statements = [] diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index ce17243e4..703d4141e 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -354,7 +354,7 @@ class TextEditorComponent event.preventDefault() unless previousScrollLeft is @presenter.getScrollLeft() else # Scrolling vertically - @presenter.setScrollingTileRow(@tileRowForNode(event.target)) + @presenter.setMouseWheelScreenRow(@screenRowForNode(event.target)) previousScrollTop = @presenter.getScrollTop() @presenter.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity)) event.preventDefault() unless previousScrollTop is @presenter.getScrollTop() @@ -729,12 +729,11 @@ class TextEditorComponent lineNumberNodeForScreenRow: (screenRow) -> @gutterContainerComponent.getLineNumberGutterComponent().lineNumberNodeForScreenRow(screenRow) - tileRowForNode: (node) -> + screenRowForNode: (node) -> while node? - if tileId = node.dataset.tileId - return tileId + if screenRow = node.dataset.screenRow + return parseInt(screenRow) node = node.parentElement - null getFontSize: -> diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 1caa666df..4944c4842 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -304,11 +304,6 @@ class TextEditorPresenter tileForRow: (row) -> row - (row % @tileSize) - isValidTile: (tileRow) -> - return false unless tileRow? - - tileRow <= @tileForRow(@model.getLastScreenRow()) - getVisibleTilesRange: -> startTileRow = Math.max(0, @tileForRow(@startRow)) endTileRow = Math.min( @@ -336,9 +331,12 @@ class TextEditorPresenter visibleTiles[startRow] = true - if @isValidTile(@scrollingTileRow) and not visibleTiles[@scrollingTileRow]? - @state.content.tiles[@scrollingTileRow].display = "none" - visibleTiles[@scrollingTileRow] = true + if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)? + mouseWheelTile = @tileForRow(@mouseWheelScreenRow) + + unless visibleTiles[mouseWheelTile]? + @state.content.tiles[mouseWheelTile].display = "none" + visibleTiles[mouseWheelTile] = true for id, tile of @state.content.tiles continue if visibleTiles.hasOwnProperty(id) @@ -779,8 +777,8 @@ class TextEditorPresenter didStopScrolling: -> @state.content.scrollingVertically = false - if @scrollingTileRow? - @scrollingTileRow = null + if @mouseWheelScreenRow? + @mouseWheelScreenRow = null @shouldUpdateTilesState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true @@ -948,9 +946,9 @@ class TextEditorPresenter @emitDidUpdateState() - setScrollingTileRow: (tileRow) -> - if @scrollingTileRow isnt tileRow - @scrollingTileRow = tileRow + setMouseWheelScreenRow: (screenRow) -> + if @mouseWheelScreenRow isnt screenRow + @mouseWheelScreenRow = screenRow @didStartScrolling() setBaseCharacterWidth: (baseCharacterWidth) -> From 723bf5a302424383a4433e7dbd8606b3471b0454 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 14 May 2015 18:30:29 +0200 Subject: [PATCH 34/58] Reimplement `lineNodeForScreenRow` --- src/lines-component.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index c4341bbbf..663dd9a31 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -131,3 +131,8 @@ class LinesComponent component.clearMeasurements() @presenter.clearScopedCharacterWidths() + + lineNodeForScreenRow: (screenRow) -> + tile = @presenter.tileForRow(screenRow) + + @tileComponentsByTileId[tile].lineNodeForScreenRow(screenRow) From 26b7c5e58e26529ce2dcdda8f9606f3379d943c9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 14 May 2015 18:40:44 +0200 Subject: [PATCH 35/58] :fire: Get rid of unused `tileId` property --- src/tile-component.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tile-component.coffee b/src/tile-component.coffee index 197a9adce..a68eb58e5 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -21,7 +21,6 @@ class TileComponent @screenRowsByLineId = {} @lineIdsByScreenRow = {} @domNode ?= document.createElement("div") - @domNode.dataset.tileId = @id @domNode.style.position = "absolute" @domNode.style.display = "block" @domNode.classList.add("tile") From 265090e7bba546175607a0fd74d944a5f0700738 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 14 May 2015 18:47:51 +0200 Subject: [PATCH 36/58] Store tile state in a instance variable --- src/tile-component.coffee | 57 ++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/tile-component.coffee b/src/tile-component.coffee index a68eb58e5..dc0324e18 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -34,29 +34,32 @@ class TileComponent @oldState = {tiles: {}} @oldState.tiles[@id] = { lines: {}} - if @newState.tiles[@id].display isnt @oldState.tiles[@id]?.display - @domNode.style.display = @newState.tiles[@id].display - @oldState.tiles[@id]?.display = @newState.tiles[@id].display + @newTileState = @newState.tiles[@id] + @oldTileState = @oldState.tiles[@id] - if @newState.tiles[@id].height isnt @oldState.tiles[@id]?.height - @domNode.style.height = @newState.tiles[@id].height + 'px' - @oldState.tiles[@id]?.height = @newState.tiles[@id].height + if @newTileState.display isnt @oldTileState.display + @domNode.style.display = @newTileState.display + @oldTileState.display = @newTileState.display - if (@newState.tiles[@id].top isnt @oldState.tiles[@id]?.top) or (@newState.tiles[@id].left isnt @oldState.tiles[@id]?.left) - @domNode.style['-webkit-transform'] = "translate3d(#{@newState.tiles[@id].left}px, #{@newState.tiles[@id].top}px, 0px)" - @oldState.tiles[@id]?.top = @newState.tiles[@id].top - @oldState.tiles[@id]?.left = @newState.tiles[@id].left + if @newTileState.height isnt @oldTileState.height + @domNode.style.height = @newTileState.height + 'px' + @oldTileState.height = @newTileState.height - if @newState.tiles[@id].newlyCreated + if @newTileState.top isnt @oldTileState.top or @newTileState.left isnt @oldTileState.left + @domNode.style['-webkit-transform'] = "translate3d(#{@newTileState.left}px, #{@newTileState.top}px, 0px)" + @oldTileState.top = @newTileState.top + @oldTileState.left = @newTileState.left + + if @newTileState.newlyCreated newLineIds = [] newLinesHTML = "" - for id, lineState of @newState.tiles[@id].lines + for id, lineState of @newTileState.lines newLineIds.push(id) newLinesHTML += @buildLineHTML(id) @screenRowsByLineId[id] = lineState.screenRow @lineIdsByScreenRow[lineState.screenRow] = id - @oldState.tiles[@id]?.lines[id] = cloneObject(lineState) + @oldTileState.lines[id] = cloneObject(lineState) return if newLineIds.length is 0 @@ -77,7 +80,7 @@ class TileComponent @oldState.scrollWidth = @newState.scrollWidth removeLineNodes: -> - @removeLineNode(id) for id of @oldState.tiles[@id]?.lines + @removeLineNode(id) for id of @oldTileState.lines return removeLineNode: (id) -> @@ -85,18 +88,18 @@ class TileComponent delete @lineNodesByLineId[id] delete @lineIdsByScreenRow[@screenRowsByLineId[id]] delete @screenRowsByLineId[id] - delete @oldState.tiles[@id]?.lines[id] + delete @oldTileState.lines[id] updateLineNodes: -> - for id of @oldState.tiles[@id]?.lines - unless @newState.tiles[@id].lines.hasOwnProperty(id) + for id of @oldTileState.lines + unless @newTileState.lines.hasOwnProperty(id) @removeLineNode(id) newLineIds = null newLinesHTML = null - for id, lineState of @newState.tiles[@id].lines - if @oldState.tiles[@id]?.lines.hasOwnProperty(id) + for id, lineState of @newTileState.lines + if @oldTileState.lines.hasOwnProperty(id) @updateLineNode(id) else newLineIds ?= [] @@ -105,7 +108,7 @@ class TileComponent newLinesHTML += @buildLineHTML(id) @screenRowsByLineId[id] = lineState.screenRow @lineIdsByScreenRow[lineState.screenRow] = id - @oldState.tiles[@id]?.lines[id] = cloneObject(lineState) + @oldTileState.lines[id] = cloneObject(lineState) return unless newLineIds? @@ -120,7 +123,7 @@ class TileComponent buildLineHTML: (id) -> {scrollWidth} = @newState - {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.tiles[@id].lines[id] + {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id] classes = '' if decorationClasses? @@ -141,7 +144,7 @@ class TileComponent buildEmptyLineInnerHTML: (id) -> {indentGuidesVisible} = @newState - {indentLevel, tabLength, endOfLineInvisibles} = @newState.tiles[@id].lines[id] + {indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id] if indentGuidesVisible and indentLevel > 0 invisibleIndex = 0 @@ -164,7 +167,7 @@ class TileComponent buildLineInnerHTML: (id) -> {indentGuidesVisible} = @newState - {tokens, text, isOnlyWhitespace} = @newState.tiles[@id].lines[id] + {tokens, text, isOnlyWhitespace} = @newTileState.lines[id] innerHTML = "" scopeStack = [] @@ -178,7 +181,7 @@ class TileComponent innerHTML buildEndOfLineHTML: (id) -> - {endOfLineInvisibles} = @newState.tiles[@id].lines[id] + {endOfLineInvisibles} = @newTileState.lines[id] html = '' if endOfLineInvisibles? @@ -212,8 +215,8 @@ class TileComponent "" updateLineNode: (id) -> - oldLineState = @oldState.tiles[@id]?.lines[id] - newLineState = @newState.tiles[@id].lines[id] + oldLineState = @oldTileState.lines[id] + newLineState = @newTileState.lines[id] lineNode = @lineNodesByLineId[id] @@ -248,7 +251,7 @@ class TileComponent @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] measureCharactersInNewLines: -> - for id, lineState of @oldState.tiles[@id]?.lines + for id, lineState of @oldTileState.lines unless @measuredLines.has(id) lineNode = @lineNodesByLineId[id] @measureCharactersInLine(id, lineState, lineNode) From 5201e4547b3db895c2cc98b502c25d998344c878 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 14 May 2015 19:08:07 +0200 Subject: [PATCH 37/58] :art: --- src/lines-component.coffee | 8 +++----- src/text-editor-presenter.coffee | 2 -- src/tile-component.coffee | 23 ++--------------------- 3 files changed, 5 insertions(+), 28 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 663dd9a31..6a49f16be 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -80,8 +80,7 @@ class LinesComponent removeTileNode: (id) -> node = @tileComponentsByTileId[id].getDomNode() - node.style.display = "none" - @freeDomNodes.push(node) + node.remove() delete @tileComponentsByTileId[id] delete @oldState.tiles[id] @@ -94,10 +93,9 @@ class LinesComponent if @oldState.tiles.hasOwnProperty(id) tileComponent = @tileComponentsByTileId[id] else - domNode = @freeDomNodes.pop() - tileComponent = @tileComponentsByTileId[id] = new TileComponent({id, @presenter, domNode}) + tileComponent = @tileComponentsByTileId[id] = new TileComponent({id, @presenter}) - @domNode.appendChild(tileComponent.getDomNode()) unless domNode? + @domNode.appendChild(tileComponent.getDomNode()) @oldState.tiles[id] = cloneObject(tileState) tileComponent.updateSync(@newState) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 4944c4842..48a410faa 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -319,12 +319,10 @@ class TextEditorPresenter for startRow in @getVisibleTilesRange() by @tileSize endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize) - isNewTile = not @state.content.tiles.hasOwnProperty(startRow) tile = @state.content.tiles[startRow] ?= {} tile.top = startRow * @lineHeight - @scrollTop tile.left = -@scrollLeft tile.height = @tileSize * @lineHeight - tile.newlyCreated = isNewTile tile.display = "block" @updateLinesState(tile, startRow, endRow) diff --git a/src/tile-component.coffee b/src/tile-component.coffee index dc0324e18..c31c4dcb6 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -50,27 +50,8 @@ class TileComponent @oldTileState.top = @newTileState.top @oldTileState.left = @newTileState.left - if @newTileState.newlyCreated - newLineIds = [] - newLinesHTML = "" - - for id, lineState of @newTileState.lines - newLineIds.push(id) - newLinesHTML += @buildLineHTML(id) - @screenRowsByLineId[id] = lineState.screenRow - @lineIdsByScreenRow[lineState.screenRow] = id - @oldTileState.lines[id] = cloneObject(lineState) - - return if newLineIds.length is 0 - - @domNode.innerHTML = newLinesHTML - newLineNodes = _.toArray(@domNode.children) - for id, i in newLineIds - lineNode = newLineNodes[i] - @lineNodesByLineId[id] = lineNode - else - @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible - @updateLineNodes() + @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible + @updateLineNodes() if @newState.scrollWidth isnt @oldState.scrollWidth @domNode.style.width = @newState.scrollWidth + 'px' From dbc57abeb0f391119b013fb3ccd6d48712dbca1a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 18 May 2015 11:50:13 +0200 Subject: [PATCH 38/58] :fire: Remove obsolete node pool --- src/lines-component.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 6a49f16be..d99fbfbe1 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -21,7 +21,6 @@ class LinesComponent constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) -> @tileComponentsByTileId = {} - @freeDomNodes = [] @domNode = document.createElement('div') @domNode.classList.add('lines') From d116a33202cd43e72da1aec0f8327d880290a805 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 18 May 2015 11:56:41 +0200 Subject: [PATCH 39/58] :fire: --- spec/text-editor-presenter-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 0e6632e52..889328386 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' -fdescribe "TextEditorPresenter", -> +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()", -> From dc6938ae377a837249a23260902cf860101537e4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 18 May 2015 14:48:25 +0200 Subject: [PATCH 40/58] :art: --- src/tile-component.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tile-component.coffee b/src/tile-component.coffee index c31c4dcb6..868653d85 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -32,7 +32,7 @@ class TileComponent @newState = state unless @oldState @oldState = {tiles: {}} - @oldState.tiles[@id] = { lines: {}} + @oldState.tiles[@id] = {lines: {}} @newTileState = @newState.tiles[@id] @oldTileState = @oldState.tiles[@id] From ba0df9fecb64ef93a7967575334fee970827ef8f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 18 May 2015 14:55:23 +0200 Subject: [PATCH 41/58] Avoid allocating an extra array --- src/text-editor-presenter.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index d65eada41..a95d4e9fd 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -306,19 +306,19 @@ class TextEditorPresenter tileForRow: (row) -> row - (row % @tileSize) - getVisibleTilesRange: -> - startTileRow = Math.max(0, @tileForRow(@startRow)) - endTileRow = Math.min( + getStartTileRow: -> + Math.max(0, @tileForRow(@startRow)) + + getEndTileRow: -> + Math.min( @tileForRow(@model.getScreenLineCount()), @tileForRow(@endRow) ) - [startTileRow..endTileRow] - updateTilesState: -> return unless @startRow? and @endRow? and @lineHeight? visibleTiles = {} - for startRow in @getVisibleTilesRange() by @tileSize + for startRow in [@getStartTileRow()..@getEndTileRow()] by @tileSize endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize) tile = @state.content.tiles[startRow] ?= {} From b9138967e185f238c26cbf0ff4c79a6e93c18c09 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 19 May 2015 09:24:44 +0200 Subject: [PATCH 42/58] :art: --- src/tile-component.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tile-component.coffee b/src/tile-component.coffee index 868653d85..0f8fd0209 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -15,15 +15,14 @@ module.exports = class TileComponent placeholderTextDiv: null - constructor: ({@presenter, @id, @domNode}) -> + constructor: ({@presenter, @id}) -> @measuredLines = new Set @lineNodesByLineId = {} @screenRowsByLineId = {} @lineIdsByScreenRow = {} - @domNode ?= document.createElement("div") + @domNode = document.createElement("div") @domNode.style.position = "absolute" @domNode.style.display = "block" - @domNode.classList.add("tile") getDomNode: -> @domNode From 90b1228622d3052efdadf328148380c761606f07 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 19 May 2015 10:26:44 +0200 Subject: [PATCH 43/58] :racehorse: Bump tileCount to 6 --- src/text-editor-presenter.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index a95d4e9fd..fae99f209 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -20,7 +20,7 @@ class TextEditorPresenter @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @measuredVerticalScrollbarWidth = verticalScrollbarWidth @gutterWidth ?= 0 - @tileCount ?= 3 + @tileCount ?= 6 @disposables = new CompositeDisposable @emitter = new Emitter From 9fee62d884b41dc3d52b4178651a294e56cff060 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 19 May 2015 19:01:11 +0200 Subject: [PATCH 44/58] :fire: Remove comment --- src/text-editor-presenter.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index fae99f209..a86d5afc1 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -577,7 +577,6 @@ class TextEditorPresenter @lineNumberGutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable} visibleLineNumberIds[id] = true - # FIXME: We should either rely on @mouseWheelScreenRow or convert this to use tiles. if @mouseWheelScreenRow? bufferRow = @model.bufferRowForScreenRow(@mouseWheelScreenRow) wrapCount = @mouseWheelScreenRow - @model.screenRowForBufferRow(bufferRow) From ffc73f3b7bf879e547b003bbf20f2aa7f95f3f18 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 20 May 2015 12:07:12 +0200 Subject: [PATCH 45/58] Reduce tile count to 4 --- src/text-editor-presenter.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index a86d5afc1..00a884eb4 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -20,7 +20,7 @@ class TextEditorPresenter @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @measuredVerticalScrollbarWidth = verticalScrollbarWidth @gutterWidth ?= 0 - @tileCount ?= 6 + @tileCount ?= 4 @disposables = new CompositeDisposable @emitter = new Emitter From 752dbf2c6eac831771c705ced0a3c6f542b5272a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 20 May 2015 18:55:47 +0200 Subject: [PATCH 46/58] Manually config tileSize Dealing with a manually entered `tileSize` is actually easier to reason about, therefore we no longer calculate it based on `tileCount.` --- spec/text-editor-component-spec.coffee | 18 ++++++++++++---- spec/text-editor-presenter-spec.coffee | 30 +++++++++++++------------- src/text-editor-component.coffee | 8 +++---- src/text-editor-element.coffee | 4 ++-- src/text-editor-presenter.coffee | 14 ++---------- src/text-editor-view.coffee | 2 +- src/tile-component.coffee | 1 + 7 files changed, 39 insertions(+), 38 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 12d189035..d9a577c25 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -1920,13 +1920,23 @@ describe "TextEditorComponent", -> component.measureDimensions() nextAnimationFrame() - linesNode = componentNode.querySelector('.lines') - expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + tilesNodes = componentNode.querySelectorAll(".tile") + + top = 0 + for tileNode in tilesNodes + expect(tileNode.style['-webkit-transform']).toBe "translate3d(0px, #{top}px, 0px)" + top += tileNode.offsetHeight + expect(horizontalScrollbarNode.scrollLeft).toBe 0 editor.setScrollLeft(100) nextAnimationFrame() - expect(linesNode.style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0px)" + + top = 0 + for tileNode in tilesNodes + expect(tileNode.style['-webkit-transform']).toBe "translate3d(-100px, #{top}px, 0px)" + top += tileNode.offsetHeight + expect(horizontalScrollbarNode.scrollLeft).toBe 100 it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> @@ -2508,7 +2518,7 @@ describe "TextEditorComponent", -> advanceClock(atom.views.documentPollingInterval) nextAnimationFrame() - expect(componentNode.querySelectorAll('.line')).toHaveLength(4 + lineOverdrawMargin + 1) + 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 889328386..c147fc4e1 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -342,26 +342,26 @@ describe "TextEditorPresenter", -> expectValues presenter.getState().hiddenInput, {top: 3 * 10, left: 6 * 10} expectStateUpdate presenter, -> presenter.setScrollTop(15) - expectValues presenter.getState().hiddenInput, {top: 0, left: 6 * 10} + expectValues presenter.getState().hiddenInput, {top: (3 * 10) - 15, left: 6 * 10} expectStateUpdate presenter, -> presenter.setScrollLeft(35) - expectValues presenter.getState().hiddenInput, {top: 0, left: 0} + expectValues presenter.getState().hiddenInput, {top: (3 * 10) - 15, left: (6 * 10) - 35} expectStateUpdate presenter, -> presenter.setScrollTop(40) - expectValues presenter.getState().hiddenInput, {top: 0, left: 0} + expectValues presenter.getState().hiddenInput, {top: 0, left: (6 * 10) - 35} expectStateUpdate presenter, -> presenter.setScrollLeft(70) expectValues presenter.getState().hiddenInput, {top: 0, left: 0} expectStateUpdate presenter, -> editor.setCursorBufferPosition([11, 43]) - expectValues presenter.getState().hiddenInput, {top: 0, left: 30} + expectValues presenter.getState().hiddenInput, {top: 11 * 10 - editor.getScrollTop(), left: 43 * 10 - editor.getScrollLeft()} newCursor = null expectStateUpdate presenter, -> newCursor = editor.addCursorAtBufferPosition([6, 10]) - expectValues presenter.getState().hiddenInput, {top: 0, left: 20} + expectValues presenter.getState().hiddenInput, {top: (6 * 10) - editor.getScrollTop(), left: (10 * 10) - editor.getScrollLeft()} expectStateUpdate presenter, -> newCursor.destroy() - expectValues presenter.getState().hiddenInput, {top: 30, left: 300 - 10} + expectValues presenter.getState().hiddenInput, {top: 50 - 10, left: 300 - 10} expectStateUpdate presenter, -> presenter.setFocused(false) expectValues presenter.getState().hiddenInput, {top: 0, left: 0} @@ -663,7 +663,7 @@ describe "TextEditorPresenter", -> presenter.getState().content.tiles[tileRow].lines[lineId] it "contains states for tiles that are visible on screen", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3) + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) expectValues presenter.getState().content.tiles[0], { top: 0 @@ -681,12 +681,12 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.tiles[8]).toBeUndefined() it "includes state for all tiles if no external ::explicitHeight is assigned", -> - presenter = buildPresenter(explicitHeight: null, tileCount: 12, tileOverdrawMargin: 1) + presenter = buildPresenter(explicitHeight: null, tileSize: 2) expect(presenter.getState().content.tiles[0]).toBeDefined() expect(presenter.getState().content.tiles[12]).toBeDefined() it "is empty until all of the required measurements are assigned", -> - presenter = buildPresenter(explicitHeight: null, lineHeight: null, scrollTop: null, tileCount: 3) + presenter = buildPresenter(explicitHeight: null, lineHeight: null, scrollTop: null) expect(presenter.getState().content.tiles).toEqual({}) presenter.setExplicitHeight(25) @@ -699,7 +699,7 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.tiles).not.toEqual({}) it "updates when ::scrollTop changes", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3) + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) expect(presenter.getState().content.tiles[0]).toBeDefined() expect(presenter.getState().content.tiles[2]).toBeDefined() @@ -717,7 +717,7 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.tiles[10]).toBeUndefined() it "updates when ::explicitHeight changes", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3) + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) expect(presenter.getState().content.tiles[0]).toBeDefined() expect(presenter.getState().content.tiles[2]).toBeDefined() @@ -736,7 +736,7 @@ describe "TextEditorPresenter", -> it "updates when ::lineHeight changes", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3) + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) expect(presenter.getState().content.tiles[0]).toBeDefined() expect(presenter.getState().content.tiles[2]).toBeDefined() @@ -752,7 +752,7 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.tiles[6]).toBeUndefined() it "updates when the editor's content changes", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 10, lineHeight: 10, tileCount: 3, tileOverdrawMargin: 1) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 10, lineHeight: 10, tileSize: 2) expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n") @@ -775,7 +775,7 @@ describe "TextEditorPresenter", -> } it "does not remove out-of-view tiles corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3, stoppedScrollingDelay: 200) + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) expect(presenter.getState().content.tiles[0]).toBeDefined() expect(presenter.getState().content.tiles[6]).toBeDefined() @@ -804,7 +804,7 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.tiles[4]).toBeUndefined() it "does not preserve deleted on-screen tiles even if they correspond to ::mouseWheelScreenRow", -> - presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileCount: 3, stoppedScrollingDelay: 200) + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) presenter.setMouseWheelScreenRow(2) diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index fb840f534..f8f6de6bf 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -18,7 +18,7 @@ class TextEditorComponent scrollSensitivity: 0.4 cursorBlinkPeriod: 800 cursorBlinkResumeDelay: 100 - lineOverdrawMargin: 15 + tileSize: 12 pendingScrollTop: null pendingScrollLeft: null @@ -36,8 +36,8 @@ class TextEditorComponent gutterComponent: null mounted: true - constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, lineOverdrawMargin}) -> - @lineOverdrawMargin = lineOverdrawMargin if lineOverdrawMargin? + constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize}) -> + @tileSize = tileSize if tileSize? @disposables = new CompositeDisposable @observeConfig() @@ -47,7 +47,7 @@ class TextEditorComponent model: @editor scrollTop: @editor.getScrollTop() scrollLeft: @editor.getScrollLeft() - lineOverdrawMargin: lineOverdrawMargin + tileSize: tileSize cursorBlinkPeriod: @cursorBlinkPeriod cursorBlinkResumeDelay: @cursorBlinkResumeDelay stoppedScrollingDelay: 200 diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index ab18be8bf..bc1dc05d6 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -15,7 +15,7 @@ class TextEditorElement extends HTMLElement componentDescriptor: null component: null attached: false - lineOverdrawMargin: null + tileSize: null focusOnAttach: false createdCallback: -> @@ -110,7 +110,7 @@ class TextEditorElement extends HTMLElement rootElement: @rootElement stylesElement: @stylesElement editor: @model - lineOverdrawMargin: @lineOverdrawMargin + tileSize: @tileSize useShadowDOM: @useShadowDOM ) @rootElement.appendChild(@component.getDomNode()) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 00a884eb4..4db15ade4 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -15,12 +15,12 @@ class TextEditorPresenter constructor: (params) -> {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params {horizontalScrollbarHeight, verticalScrollbarWidth} = params - {@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor, @tileCount} = params + {@lineHeight, @baseCharacterWidth, @backgroundColor, @gutterBackgroundColor, @tileSize} = params {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @measuredVerticalScrollbarWidth = verticalScrollbarWidth @gutterWidth ?= 0 - @tileCount ?= 4 + @tileSize ?= 12 @disposables = new CompositeDisposable @emitter = new Emitter @@ -66,7 +66,6 @@ class TextEditorPresenter @updateScrollbarDimensions() @updateStartRow() @updateEndRow() - @updateTileSize() @updateFocusedState() if @shouldUpdateFocusedState @updateHeightState() if @shouldUpdateHeightState @@ -224,7 +223,6 @@ class TextEditorPresenter @updateScrollbarDimensions() @updateStartRow() @updateEndRow() - @updateTileSize() @updateFocusedState() @updateHeightState() @@ -285,8 +283,6 @@ class TextEditorPresenter {top, left, height, width} = @pixelRectForScreenRange(lastCursor.getScreenRange()) if @focused - top -= @scrollTop - left -= @scrollLeft @state.hiddenInput.top = Math.max(Math.min(top, @clientHeight - height), 0) @state.hiddenInput.left = Math.max(Math.min(left, @clientWidth - width), 0) else @@ -603,12 +599,6 @@ class TextEditorPresenter endRow = startRow + visibleLinesCount @endRow = Math.min(@model.getScreenLineCount(), endRow) - updateTileSize: -> - return unless @height? and @lineHeight? and @tileCount? - - @tileSize = Math.floor(@height / @lineHeight / @tileCount) - @tileSize = Math.max(1, @tileSize) - updateScrollWidth: -> return unless @contentWidth? and @clientWidth? diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index b86367ef4..ffb59cc66 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -60,7 +60,7 @@ class TextEditorView extends View placeholderText: placeholderText element = new TextEditorElement - element.lineOverdrawMargin = props?.lineOverdrawMargin + element.tileSize = props?.tileSize element.setAttribute(name, value) for name, value of attributes if attributes? element.setModel(model) return element.__spacePenView diff --git a/src/tile-component.coffee b/src/tile-component.coffee index 0f8fd0209..20c9d7ff1 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -21,6 +21,7 @@ class TileComponent @screenRowsByLineId = {} @lineIdsByScreenRow = {} @domNode = document.createElement("div") + @domNode.classList.add("tile") @domNode.style.position = "absolute" @domNode.style.display = "block" From 1a18cda000a7651e63b1857c93deac7f1d52c7c0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 20 May 2015 20:54:11 +0200 Subject: [PATCH 47/58] :green_heart: Fix remaining specs --- spec/text-editor-component-spec.coffee | 146 ++++++++++++++++++------- src/lines-component.coffee | 2 +- 2 files changed, 106 insertions(+), 42 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index d9a577c25..30c955c57 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -7,10 +7,10 @@ nbsp = String.fromCharCode(160) describe "TextEditorComponent", -> [contentNode, editor, wrapperView, wrapperNode, component, componentNode, verticalScrollbarNode, horizontalScrollbarNode] = [] - [lineHeightInPixels, charWidth, nextAnimationFrame, noAnimationFrame, lineOverdrawMargin] = [] + [lineHeightInPixels, charWidth, nextAnimationFrame, noAnimationFrame, tileSize] = [] beforeEach -> - lineOverdrawMargin = 2 + tileSize = 3 waitsForPromise -> atom.packages.activatePackage('language-javascript') @@ -34,7 +34,7 @@ describe "TextEditorComponent", -> contentNode = document.querySelector('#jasmine-content') contentNode.style.width = '1000px' - wrapperView = new TextEditorView(editor, {lineOverdrawMargin}) + wrapperView = new TextEditorView(editor, {tileSize}) wrapperView.attachToDom() wrapperNode = wrapperView.element wrapperNode.setUpdatedSynchronously(false) @@ -68,48 +68,111 @@ describe "TextEditorComponent", -> expect(nextAnimationFrame).not.toThrow() describe "line rendering", -> - it "renders the currently-visible lines plus the overdraw margin", -> - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + expectLineRender = (tileNode, {screenRow, top}) -> + lineNode = tileNode.querySelector("[data-screen-row='#{screenRow}']") + tokenizedLine = editor.tokenizedLineForScreenRow(screenRow) + + expect(lineNode.offsetTop).toBe(top) + if tokenizedLine.text is "" + expect(lineNode.innerHTML).toBe(" ") + else + expect(lineNode.textContent).toBe(tokenizedLine.text) + + it "renders the currently-visible lines into a tiled fashion", -> + wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' + tileHeight = tileSize * lineHeightInPixels component.measureDimensions() nextAnimationFrame() - linesNode = componentNode.querySelector('.lines') - expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(componentNode.querySelectorAll('.line').length).toBe 6 + 2 # no margin above - expect(component.lineNodeForScreenRow(0).textContent).toBe editor.tokenizedLineForScreenRow(0).text - expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 - expect(component.lineNodeForScreenRow(5).textContent).toBe editor.tokenizedLineForScreenRow(5).text - expect(component.lineNodeForScreenRow(5).offsetTop).toBe 5 * lineHeightInPixels + tilesNodes = componentNode.querySelectorAll(".tile") - verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels + expect(tilesNodes.length).toBe(3) + + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(tilesNodes[0].children.length).toBe(tileSize) + expectLineRender(tilesNodes[0], screenRow: 0, top: 0 * lineHeightInPixels) + expectLineRender(tilesNodes[0], screenRow: 1, top: 1 * lineHeightInPixels) + expectLineRender(tilesNodes[0], screenRow: 2, top: 2 * lineHeightInPixels) + + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight}px, 0px)" + expect(tilesNodes[1].children.length).toBe(tileSize) + expectLineRender(tilesNodes[1], screenRow: 3, top: 0 * lineHeightInPixels) + expectLineRender(tilesNodes[1], screenRow: 4, top: 1 * lineHeightInPixels) + expectLineRender(tilesNodes[1], screenRow: 5, top: 2 * lineHeightInPixels) + + expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeight}px, 0px)" + expect(tilesNodes[2].children.length).toBe(tileSize) + expectLineRender(tilesNodes[2], screenRow: 6, top: 0 * lineHeightInPixels) + expectLineRender(tilesNodes[2], screenRow: 7, top: 1 * lineHeightInPixels) + expectLineRender(tilesNodes[2], screenRow: 8, top: 2 * lineHeightInPixels) + + expect(component.lineNodeForScreenRow(9)).toBeUndefined() + + verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5 verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) nextAnimationFrame() - expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, #{-4.5 * lineHeightInPixels}px, 0px)" - expect(componentNode.querySelectorAll('.line').length).toBe 6 + 4 # margin above and below - expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels - expect(component.lineNodeForScreenRow(2).textContent).toBe editor.tokenizedLineForScreenRow(2).text - expect(component.lineNodeForScreenRow(9).offsetTop).toBe 9 * lineHeightInPixels - expect(component.lineNodeForScreenRow(9).textContent).toBe editor.tokenizedLineForScreenRow(9).text + tilesNodes = componentNode.querySelectorAll(".tile") - it "updates the top position of subsequent lines when lines are inserted or removed", -> + expect(component.lineNodeForScreenRow(2)).toBeUndefined() + expect(tilesNodes.length).toBe(3) + + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, -5px, 0px)" + expect(tilesNodes[0].children.length).toBe(tileSize) + expectLineRender(tilesNodes[0], screenRow: 3, top: 0 * lineHeightInPixels) + expectLineRender(tilesNodes[0], screenRow: 4, top: 1 * lineHeightInPixels) + expectLineRender(tilesNodes[0], screenRow: 5, top: 2 * lineHeightInPixels) + + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight - 5}px, 0px)" + expect(tilesNodes[1].children.length).toBe(tileSize) + expectLineRender(tilesNodes[1], screenRow: 6, top: 0 * lineHeightInPixels) + expectLineRender(tilesNodes[1], screenRow: 7, top: 1 * lineHeightInPixels) + expectLineRender(tilesNodes[1], screenRow: 8, top: 2 * lineHeightInPixels) + + expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeight - 5}px, 0px)" + expect(tilesNodes[2].children.length).toBe(tileSize) + expectLineRender(tilesNodes[2], screenRow: 9, top: 0 * lineHeightInPixels) + expectLineRender(tilesNodes[2], screenRow: 10, top: 1 * lineHeightInPixels) + expectLineRender(tilesNodes[2], screenRow: 11, top: 2 * lineHeightInPixels) + + it "updates the top position of subsequent tiles when lines are inserted or removed", -> + wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' + tileHeight = tileSize * lineHeightInPixels + component.measureDimensions() editor.getBuffer().deleteRows(0, 1) nextAnimationFrame() - lineNodes = componentNode.querySelectorAll('.line') - expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 - expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + tilesNodes = componentNode.querySelectorAll(".tile") + + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expectLineRender(tilesNodes[0], screenRow: 0, top: 0 * lineHeightInPixels) + expectLineRender(tilesNodes[0], screenRow: 1, top: 1 * lineHeightInPixels) + expectLineRender(tilesNodes[0], screenRow: 2, top: 2 * lineHeightInPixels) + + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight}px, 0px)" + expectLineRender(tilesNodes[1], screenRow: 3, top: 0 * lineHeightInPixels) + expectLineRender(tilesNodes[1], screenRow: 4, top: 1 * lineHeightInPixels) + expectLineRender(tilesNodes[1], screenRow: 5, top: 2 * lineHeightInPixels) editor.getBuffer().insert([0, 0], '\n\n') nextAnimationFrame() - lineNodes = componentNode.querySelectorAll('.line') - expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels - expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels - expect(component.lineNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels - expect(component.lineNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels + tilesNodes = componentNode.querySelectorAll(".tile") + + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expectLineRender(tilesNodes[0], screenRow: 0, top: 0 * lineHeightInPixels) + expectLineRender(tilesNodes[0], screenRow: 1, top: 1 * lineHeightInPixels) + expectLineRender(tilesNodes[0], screenRow: 2, top: 2 * lineHeightInPixels) + + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight}px, 0px)" + expectLineRender(tilesNodes[1], screenRow: 3, top: 0 * lineHeightInPixels) + expectLineRender(tilesNodes[1], screenRow: 4, top: 1 * lineHeightInPixels) + expectLineRender(tilesNodes[1], screenRow: 5, top: 2 * lineHeightInPixels) + + expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeight}px, 0px)" + expectLineRender(tilesNodes[2], screenRow: 6, top: 0 * lineHeightInPixels) + expectLineRender(tilesNodes[2], screenRow: 7, top: 1 * lineHeightInPixels) + expectLineRender(tilesNodes[2], screenRow: 8, top: 2 * lineHeightInPixels) it "updates the lines when lines are inserted or removed above the rendered row range", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' @@ -483,7 +546,7 @@ describe "TextEditorComponent", -> component.measureDimensions() nextAnimationFrame() - expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 2 + 1 # line overdraw margin below + dummy line number + expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 1 # visible line-numbers + dummy line number expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}6" @@ -491,7 +554,7 @@ describe "TextEditorComponent", -> verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) nextAnimationFrame() - expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 4 + 1 # line overdraw margin above/below + dummy line number + expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 1 # visible line-numbers + dummy line number expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}3" expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels @@ -527,7 +590,7 @@ describe "TextEditorComponent", -> component.measureDimensions() nextAnimationFrame() - expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + lineOverdrawMargin + 1 # 1 dummy line componentNode + expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 1 # 1 dummy line expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" expect(component.lineNumberNodeForScreenRow(1).textContent).toBe "#{nbsp}•" expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}2" @@ -725,13 +788,13 @@ describe "TextEditorComponent", -> cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{10 * charWidth}px, #{4 * lineHeightInPixels}px)" - expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{8 * lineHeightInPixels}px)" + 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)" editor.onDidChangeCursorPosition cursorMovedListener = jasmine.createSpy('cursorMovedListener') cursor3.setScreenPosition([4, 11], autoscroll: false) nextAnimationFrame() - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{4 * lineHeightInPixels}px)" + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" expect(cursorMovedListener).toHaveBeenCalled() cursor3.destroy() @@ -739,7 +802,7 @@ describe "TextEditorComponent", -> cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{8 * lineHeightInPixels}px)" + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{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') @@ -1001,7 +1064,7 @@ describe "TextEditorComponent", -> nextAnimationFrame() # Scroll decorations into view - verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels + verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) nextAnimationFrame() expect(lineAndLineNumberHaveClass(9, 'b')).toBe true @@ -1147,15 +1210,16 @@ describe "TextEditorComponent", -> # Nothing when outside the rendered row range expect(regions.length).toBe 0 - verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels + verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) nextAnimationFrame() + expect(component.presenter.endRow).toBeGreaterThan(8) regions = componentNode.querySelectorAll('.some-highlight .region') expect(regions.length).toBe 1 regionRect = regions[0].style - expect(regionRect.top).toBe 9 * lineHeightInPixels + 'px' + expect(regionRect.top).toBe (9 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px' expect(regionRect.height).toBe 1 * lineHeightInPixels + 'px' expect(regionRect.left).toBe 2 * charWidth + 'px' expect(regionRect.width).toBe 2 * charWidth + 'px' @@ -2397,7 +2461,7 @@ describe "TextEditorComponent", -> hiddenParent.style.display = 'none' contentNode.appendChild(hiddenParent) - wrapperView = new TextEditorView(editor, {lineOverdrawMargin}) + wrapperView = new TextEditorView(editor, {tileSize}) wrapperNode = wrapperView.element wrapperView.appendTo(hiddenParent) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index d99fbfbe1..1c855e125 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -132,4 +132,4 @@ class LinesComponent lineNodeForScreenRow: (screenRow) -> tile = @presenter.tileForRow(screenRow) - @tileComponentsByTileId[tile].lineNodeForScreenRow(screenRow) + @tileComponentsByTileId[tile]?.lineNodeForScreenRow(screenRow) From f49078d394b333d8c0fc7400fa6f2c9eec22f3a4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 May 2015 10:40:37 +0200 Subject: [PATCH 48/58] wip --- src/foo-component.coffee | 135 ++++++++++++++++ src/lines-component.coffee | 313 +++++++++++++++++++++++++++---------- src/tile-component.coffee | 286 --------------------------------- 3 files changed, 367 insertions(+), 367 deletions(-) create mode 100644 src/foo-component.coffee delete mode 100644 src/tile-component.coffee diff --git a/src/foo-component.coffee b/src/foo-component.coffee new file mode 100644 index 000000000..1c855e125 --- /dev/null +++ b/src/foo-component.coffee @@ -0,0 +1,135 @@ +_ = require 'underscore-plus' +{toArray} = require 'underscore-plus' +{$$} = require 'space-pen' + +CursorsComponent = require './cursors-component' +HighlightsComponent = require './highlights-component' +TileComponent = require './tile-component' + +DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] +AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} +WrapperDiv = document.createElement('div') + +cloneObject = (object) -> + clone = {} + clone[key] = value for key, value of object + clone + +module.exports = +class LinesComponent + placeholderTextDiv: null + + constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) -> + @tileComponentsByTileId = {} + + @domNode = document.createElement('div') + @domNode.classList.add('lines') + + @cursorsComponent = new CursorsComponent(@presenter) + @domNode.appendChild(@cursorsComponent.getDomNode()) + + @highlightsComponent = new HighlightsComponent(@presenter) + @domNode.appendChild(@highlightsComponent.getDomNode()) + + if @useShadowDOM + insertionPoint = document.createElement('content') + insertionPoint.setAttribute('select', '.overlayer') + @domNode.appendChild(insertionPoint) + + getDomNode: -> + @domNode + + updateSync: (state) -> + @newState = state.content + @oldState ?= {tiles: {}} + + if @newState.scrollHeight isnt @oldState.scrollHeight + @domNode.style.height = @newState.scrollHeight + 'px' + @oldState.scrollHeight = @newState.scrollHeight + + if @newState.backgroundColor isnt @oldState.backgroundColor + @domNode.style.backgroundColor = @newState.backgroundColor + @oldState.backgroundColor = @newState.backgroundColor + + if @newState.placeholderText isnt @oldState.placeholderText + @placeholderTextDiv?.remove() + if @newState.placeholderText? + @placeholderTextDiv = document.createElement('div') + @placeholderTextDiv.classList.add('placeholder-text') + @placeholderTextDiv.textContent = @newState.placeholderText + @domNode.appendChild(@placeholderTextDiv) + + @removeTileNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible + @updateTileNodes() + + if @newState.scrollWidth isnt @oldState.scrollWidth + @domNode.style.width = @newState.scrollWidth + 'px' + @oldState.scrollWidth = @newState.scrollWidth + + @cursorsComponent.updateSync(state) + @highlightsComponent.updateSync(state) + + @oldState.indentGuidesVisible = @newState.indentGuidesVisible + @oldState.scrollWidth = @newState.scrollWidth + + removeTileNodes: -> + @removeTileNode(id) for id of @oldState.tiles + return + + removeTileNode: (id) -> + node = @tileComponentsByTileId[id].getDomNode() + + node.remove() + delete @tileComponentsByTileId[id] + delete @oldState.tiles[id] + + updateTileNodes: -> + for id of @oldState.tiles + unless @newState.tiles.hasOwnProperty(id) + @removeTileNode(id) + + for id, tileState of @newState.tiles + if @oldState.tiles.hasOwnProperty(id) + tileComponent = @tileComponentsByTileId[id] + else + tileComponent = @tileComponentsByTileId[id] = new TileComponent({id, @presenter}) + + @domNode.appendChild(tileComponent.getDomNode()) + @oldState.tiles[id] = cloneObject(tileState) + + tileComponent.updateSync(@newState) + + return + + measureLineHeightAndDefaultCharWidth: -> + @domNode.appendChild(DummyLineNode) + lineHeightInPixels = DummyLineNode.getBoundingClientRect().height + charWidth = DummyLineNode.firstChild.getBoundingClientRect().width + @domNode.removeChild(DummyLineNode) + + @presenter.setLineHeight(lineHeightInPixels) + @presenter.setBaseCharacterWidth(charWidth) + + remeasureCharacterWidths: -> + return unless @presenter.baseCharacterWidth + + @clearScopedCharWidths() + @measureCharactersInNewLines() + + measureCharactersInNewLines: -> + @presenter.batchCharacterMeasurement => + for id, component of @tileComponentsByTileId + component.measureCharactersInNewLines() + + return + + clearScopedCharWidths: -> + for id, component of @tileComponentsByTileId + component.clearMeasurements() + + @presenter.clearScopedCharacterWidths() + + lineNodeForScreenRow: (screenRow) -> + tile = @presenter.tileForRow(screenRow) + + @tileComponentsByTileId[tile]?.lineNodeForScreenRow(screenRow) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 1c855e125..20c9d7ff1 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -2,10 +2,6 @@ _ = require 'underscore-plus' {toArray} = require 'underscore-plus' {$$} = require 'space-pen' -CursorsComponent = require './cursors-component' -HighlightsComponent = require './highlights-component' -TileComponent = require './tile-component' - DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} WrapperDiv = document.createElement('div') @@ -16,120 +12,275 @@ cloneObject = (object) -> clone module.exports = -class LinesComponent +class TileComponent placeholderTextDiv: null - constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) -> - @tileComponentsByTileId = {} - - @domNode = document.createElement('div') - @domNode.classList.add('lines') - - @cursorsComponent = new CursorsComponent(@presenter) - @domNode.appendChild(@cursorsComponent.getDomNode()) - - @highlightsComponent = new HighlightsComponent(@presenter) - @domNode.appendChild(@highlightsComponent.getDomNode()) - - if @useShadowDOM - insertionPoint = document.createElement('content') - insertionPoint.setAttribute('select', '.overlayer') - @domNode.appendChild(insertionPoint) + constructor: ({@presenter, @id}) -> + @measuredLines = new Set + @lineNodesByLineId = {} + @screenRowsByLineId = {} + @lineIdsByScreenRow = {} + @domNode = document.createElement("div") + @domNode.classList.add("tile") + @domNode.style.position = "absolute" + @domNode.style.display = "block" getDomNode: -> @domNode updateSync: (state) -> - @newState = state.content - @oldState ?= {tiles: {}} + @newState = state + unless @oldState + @oldState = {tiles: {}} + @oldState.tiles[@id] = {lines: {}} - if @newState.scrollHeight isnt @oldState.scrollHeight - @domNode.style.height = @newState.scrollHeight + 'px' - @oldState.scrollHeight = @newState.scrollHeight + @newTileState = @newState.tiles[@id] + @oldTileState = @oldState.tiles[@id] - if @newState.backgroundColor isnt @oldState.backgroundColor - @domNode.style.backgroundColor = @newState.backgroundColor - @oldState.backgroundColor = @newState.backgroundColor + if @newTileState.display isnt @oldTileState.display + @domNode.style.display = @newTileState.display + @oldTileState.display = @newTileState.display - if @newState.placeholderText isnt @oldState.placeholderText - @placeholderTextDiv?.remove() - if @newState.placeholderText? - @placeholderTextDiv = document.createElement('div') - @placeholderTextDiv.classList.add('placeholder-text') - @placeholderTextDiv.textContent = @newState.placeholderText - @domNode.appendChild(@placeholderTextDiv) + if @newTileState.height isnt @oldTileState.height + @domNode.style.height = @newTileState.height + 'px' + @oldTileState.height = @newTileState.height - @removeTileNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible - @updateTileNodes() + if @newTileState.top isnt @oldTileState.top or @newTileState.left isnt @oldTileState.left + @domNode.style['-webkit-transform'] = "translate3d(#{@newTileState.left}px, #{@newTileState.top}px, 0px)" + @oldTileState.top = @newTileState.top + @oldTileState.left = @newTileState.left + + @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible + @updateLineNodes() if @newState.scrollWidth isnt @oldState.scrollWidth @domNode.style.width = @newState.scrollWidth + 'px' @oldState.scrollWidth = @newState.scrollWidth - @cursorsComponent.updateSync(state) - @highlightsComponent.updateSync(state) - @oldState.indentGuidesVisible = @newState.indentGuidesVisible @oldState.scrollWidth = @newState.scrollWidth - removeTileNodes: -> - @removeTileNode(id) for id of @oldState.tiles + removeLineNodes: -> + @removeLineNode(id) for id of @oldTileState.lines return - removeTileNode: (id) -> - node = @tileComponentsByTileId[id].getDomNode() + removeLineNode: (id) -> + @lineNodesByLineId[id].remove() + delete @lineNodesByLineId[id] + delete @lineIdsByScreenRow[@screenRowsByLineId[id]] + delete @screenRowsByLineId[id] + delete @oldTileState.lines[id] - node.remove() - delete @tileComponentsByTileId[id] - delete @oldState.tiles[id] + updateLineNodes: -> + for id of @oldTileState.lines + unless @newTileState.lines.hasOwnProperty(id) + @removeLineNode(id) - updateTileNodes: -> - for id of @oldState.tiles - unless @newState.tiles.hasOwnProperty(id) - @removeTileNode(id) + newLineIds = null + newLinesHTML = null - for id, tileState of @newState.tiles - if @oldState.tiles.hasOwnProperty(id) - tileComponent = @tileComponentsByTileId[id] + for id, lineState of @newTileState.lines + if @oldTileState.lines.hasOwnProperty(id) + @updateLineNode(id) else - tileComponent = @tileComponentsByTileId[id] = new TileComponent({id, @presenter}) + newLineIds ?= [] + newLinesHTML ?= "" + newLineIds.push(id) + newLinesHTML += @buildLineHTML(id) + @screenRowsByLineId[id] = lineState.screenRow + @lineIdsByScreenRow[lineState.screenRow] = id + @oldTileState.lines[id] = cloneObject(lineState) - @domNode.appendChild(tileComponent.getDomNode()) - @oldState.tiles[id] = cloneObject(tileState) + return unless newLineIds? - tileComponent.updateSync(@newState) + WrapperDiv.innerHTML = newLinesHTML + newLineNodes = _.toArray(WrapperDiv.children) + for id, i in newLineIds + lineNode = newLineNodes[i] + @lineNodesByLineId[id] = lineNode + @domNode.appendChild(lineNode) return - measureLineHeightAndDefaultCharWidth: -> - @domNode.appendChild(DummyLineNode) - lineHeightInPixels = DummyLineNode.getBoundingClientRect().height - charWidth = DummyLineNode.firstChild.getBoundingClientRect().width - @domNode.removeChild(DummyLineNode) + buildLineHTML: (id) -> + {scrollWidth} = @newState + {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id] - @presenter.setLineHeight(lineHeightInPixels) - @presenter.setBaseCharacterWidth(charWidth) + classes = '' + if decorationClasses? + for decorationClass in decorationClasses + classes += decorationClass + ' ' + classes += 'line' - remeasureCharacterWidths: -> - return unless @presenter.baseCharacterWidth + lineHTML = "
" - @clearScopedCharWidths() - @measureCharactersInNewLines() + if text is "" + lineHTML += @buildEmptyLineInnerHTML(id) + else + lineHTML += @buildLineInnerHTML(id) - measureCharactersInNewLines: -> - @presenter.batchCharacterMeasurement => - for id, component of @tileComponentsByTileId - component.measureCharactersInNewLines() + lineHTML += '' if fold + lineHTML += "
" + lineHTML - return + buildEmptyLineInnerHTML: (id) -> + {indentGuidesVisible} = @newState + {indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id] - clearScopedCharWidths: -> - for id, component of @tileComponentsByTileId - component.clearMeasurements() + if indentGuidesVisible and indentLevel > 0 + invisibleIndex = 0 + lineHTML = '' + for i in [0...indentLevel] + lineHTML += "" + for j in [0...tabLength] + if invisible = endOfLineInvisibles?[invisibleIndex++] + lineHTML += "#{invisible}" + else + lineHTML += ' ' + lineHTML += "" - @presenter.clearScopedCharacterWidths() + while invisibleIndex < endOfLineInvisibles?.length + lineHTML += "#{endOfLineInvisibles[invisibleIndex++]}" + + lineHTML + else + @buildEndOfLineHTML(id) or ' ' + + buildLineInnerHTML: (id) -> + {indentGuidesVisible} = @newState + {tokens, text, isOnlyWhitespace} = @newTileState.lines[id] + innerHTML = "" + + scopeStack = [] + for token in tokens + innerHTML += @updateScopeStack(scopeStack, token.scopes) + hasIndentGuide = indentGuidesVisible and (token.hasLeadingWhitespace() or (token.hasTrailingWhitespace() and isOnlyWhitespace)) + innerHTML += token.getValueAsHtml({hasIndentGuide}) + + innerHTML += @popScope(scopeStack) while scopeStack.length > 0 + innerHTML += @buildEndOfLineHTML(id) + innerHTML + + buildEndOfLineHTML: (id) -> + {endOfLineInvisibles} = @newTileState.lines[id] + + html = '' + if endOfLineInvisibles? + for invisible in endOfLineInvisibles + html += "#{invisible}" + html + + updateScopeStack: (scopeStack, desiredScopeDescriptor) -> + html = "" + + # Find a common prefix + for scope, i in desiredScopeDescriptor + break unless scopeStack[i] is desiredScopeDescriptor[i] + + # Pop scopeDescriptor until we're at the common prefx + until scopeStack.length is i + html += @popScope(scopeStack) + + # Push onto common prefix until scopeStack equals desiredScopeDescriptor + for j in [i...desiredScopeDescriptor.length] + html += @pushScope(scopeStack, desiredScopeDescriptor[j]) + + html + + popScope: (scopeStack) -> + scopeStack.pop() + "
" + + pushScope: (scopeStack, scope) -> + scopeStack.push(scope) + "" + + updateLineNode: (id) -> + oldLineState = @oldTileState.lines[id] + newLineState = @newTileState.lines[id] + + lineNode = @lineNodesByLineId[id] + + if @newState.scrollWidth isnt @oldState.scrollWidth + lineNode.style.width = @newState.scrollWidth + 'px' + + newDecorationClasses = newLineState.decorationClasses + oldDecorationClasses = oldLineState.decorationClasses + + if oldDecorationClasses? + for decorationClass in oldDecorationClasses + unless newDecorationClasses? and decorationClass in newDecorationClasses + lineNode.classList.remove(decorationClass) + + if newDecorationClasses? + for decorationClass in newDecorationClasses + unless oldDecorationClasses? and decorationClass in oldDecorationClasses + lineNode.classList.add(decorationClass) + + oldLineState.decorationClasses = newLineState.decorationClasses + + if newLineState.top isnt oldLineState.top + lineNode.style.top = newLineState.top + 'px' + oldLineState.top = newLineState.top + + if newLineState.screenRow isnt oldLineState.screenRow + lineNode.dataset.screenRow = newLineState.screenRow + oldLineState.screenRow = newLineState.screenRow + @lineIdsByScreenRow[newLineState.screenRow] = id lineNodeForScreenRow: (screenRow) -> - tile = @presenter.tileForRow(screenRow) + @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] - @tileComponentsByTileId[tile]?.lineNodeForScreenRow(screenRow) + 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 + + for {value, scopes, hasPairedCharacter} in tokenizedLine.tokens + charWidths = @presenter.getScopedCharacterWidths(scopes) + + valueIndex = 0 + while valueIndex < value.length + if hasPairedCharacter + char = value.substr(valueIndex, 2) + charLength = 2 + valueIndex += 2 + else + char = value[valueIndex] + charLength = 1 + valueIndex++ + + continue if char is '\0' + + unless charWidths[char]? + unless textNode? + rangeForMeasurement ?= document.createRange() + iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) + textNode = iterator.nextNode() + textNodeIndex = 0 + nextTextNodeIndex = textNode.textContent.length + + while nextTextNodeIndex <= charIndex + textNode = iterator.nextNode() + textNodeIndex = nextTextNodeIndex + nextTextNodeIndex = textNodeIndex + textNode.textContent.length + + i = charIndex - textNodeIndex + rangeForMeasurement.setStart(textNode, i) + rangeForMeasurement.setEnd(textNode, i + charLength) + charWidth = rangeForMeasurement.getBoundingClientRect().width + @presenter.setScopedCharacterWidth(scopes, char, charWidth) + + charIndex += charLength + + @measuredLines.add(lineId) + + clearMeasurements: -> + @measuredLines.clear() diff --git a/src/tile-component.coffee b/src/tile-component.coffee deleted file mode 100644 index 20c9d7ff1..000000000 --- a/src/tile-component.coffee +++ /dev/null @@ -1,286 +0,0 @@ -_ = require 'underscore-plus' -{toArray} = require 'underscore-plus' -{$$} = require 'space-pen' - -DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] -AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} -WrapperDiv = document.createElement('div') - -cloneObject = (object) -> - clone = {} - clone[key] = value for key, value of object - clone - -module.exports = -class TileComponent - placeholderTextDiv: null - - constructor: ({@presenter, @id}) -> - @measuredLines = new Set - @lineNodesByLineId = {} - @screenRowsByLineId = {} - @lineIdsByScreenRow = {} - @domNode = document.createElement("div") - @domNode.classList.add("tile") - @domNode.style.position = "absolute" - @domNode.style.display = "block" - - getDomNode: -> - @domNode - - updateSync: (state) -> - @newState = state - unless @oldState - @oldState = {tiles: {}} - @oldState.tiles[@id] = {lines: {}} - - @newTileState = @newState.tiles[@id] - @oldTileState = @oldState.tiles[@id] - - if @newTileState.display isnt @oldTileState.display - @domNode.style.display = @newTileState.display - @oldTileState.display = @newTileState.display - - if @newTileState.height isnt @oldTileState.height - @domNode.style.height = @newTileState.height + 'px' - @oldTileState.height = @newTileState.height - - if @newTileState.top isnt @oldTileState.top or @newTileState.left isnt @oldTileState.left - @domNode.style['-webkit-transform'] = "translate3d(#{@newTileState.left}px, #{@newTileState.top}px, 0px)" - @oldTileState.top = @newTileState.top - @oldTileState.left = @newTileState.left - - @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible - @updateLineNodes() - - if @newState.scrollWidth isnt @oldState.scrollWidth - @domNode.style.width = @newState.scrollWidth + 'px' - @oldState.scrollWidth = @newState.scrollWidth - - @oldState.indentGuidesVisible = @newState.indentGuidesVisible - @oldState.scrollWidth = @newState.scrollWidth - - removeLineNodes: -> - @removeLineNode(id) for id of @oldTileState.lines - return - - removeLineNode: (id) -> - @lineNodesByLineId[id].remove() - delete @lineNodesByLineId[id] - delete @lineIdsByScreenRow[@screenRowsByLineId[id]] - delete @screenRowsByLineId[id] - delete @oldTileState.lines[id] - - updateLineNodes: -> - for id of @oldTileState.lines - unless @newTileState.lines.hasOwnProperty(id) - @removeLineNode(id) - - newLineIds = null - newLinesHTML = null - - for id, lineState of @newTileState.lines - if @oldTileState.lines.hasOwnProperty(id) - @updateLineNode(id) - else - newLineIds ?= [] - newLinesHTML ?= "" - newLineIds.push(id) - newLinesHTML += @buildLineHTML(id) - @screenRowsByLineId[id] = lineState.screenRow - @lineIdsByScreenRow[lineState.screenRow] = id - @oldTileState.lines[id] = cloneObject(lineState) - - return unless newLineIds? - - WrapperDiv.innerHTML = newLinesHTML - newLineNodes = _.toArray(WrapperDiv.children) - for id, i in newLineIds - lineNode = newLineNodes[i] - @lineNodesByLineId[id] = lineNode - @domNode.appendChild(lineNode) - - return - - buildLineHTML: (id) -> - {scrollWidth} = @newState - {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id] - - classes = '' - if decorationClasses? - for decorationClass in decorationClasses - classes += decorationClass + ' ' - classes += 'line' - - lineHTML = "
" - - if text is "" - lineHTML += @buildEmptyLineInnerHTML(id) - else - lineHTML += @buildLineInnerHTML(id) - - lineHTML += '' if fold - lineHTML += "
" - lineHTML - - buildEmptyLineInnerHTML: (id) -> - {indentGuidesVisible} = @newState - {indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id] - - if indentGuidesVisible and indentLevel > 0 - invisibleIndex = 0 - lineHTML = '' - for i in [0...indentLevel] - lineHTML += "" - for j in [0...tabLength] - if invisible = endOfLineInvisibles?[invisibleIndex++] - lineHTML += "#{invisible}" - else - lineHTML += ' ' - lineHTML += "" - - while invisibleIndex < endOfLineInvisibles?.length - lineHTML += "#{endOfLineInvisibles[invisibleIndex++]}" - - lineHTML - else - @buildEndOfLineHTML(id) or ' ' - - buildLineInnerHTML: (id) -> - {indentGuidesVisible} = @newState - {tokens, text, isOnlyWhitespace} = @newTileState.lines[id] - innerHTML = "" - - scopeStack = [] - for token in tokens - innerHTML += @updateScopeStack(scopeStack, token.scopes) - hasIndentGuide = indentGuidesVisible and (token.hasLeadingWhitespace() or (token.hasTrailingWhitespace() and isOnlyWhitespace)) - innerHTML += token.getValueAsHtml({hasIndentGuide}) - - innerHTML += @popScope(scopeStack) while scopeStack.length > 0 - innerHTML += @buildEndOfLineHTML(id) - innerHTML - - buildEndOfLineHTML: (id) -> - {endOfLineInvisibles} = @newTileState.lines[id] - - html = '' - if endOfLineInvisibles? - for invisible in endOfLineInvisibles - html += "#{invisible}" - html - - updateScopeStack: (scopeStack, desiredScopeDescriptor) -> - html = "" - - # Find a common prefix - for scope, i in desiredScopeDescriptor - break unless scopeStack[i] is desiredScopeDescriptor[i] - - # Pop scopeDescriptor until we're at the common prefx - until scopeStack.length is i - html += @popScope(scopeStack) - - # Push onto common prefix until scopeStack equals desiredScopeDescriptor - for j in [i...desiredScopeDescriptor.length] - html += @pushScope(scopeStack, desiredScopeDescriptor[j]) - - html - - popScope: (scopeStack) -> - scopeStack.pop() - "
" - - pushScope: (scopeStack, scope) -> - scopeStack.push(scope) - "" - - updateLineNode: (id) -> - oldLineState = @oldTileState.lines[id] - newLineState = @newTileState.lines[id] - - lineNode = @lineNodesByLineId[id] - - if @newState.scrollWidth isnt @oldState.scrollWidth - lineNode.style.width = @newState.scrollWidth + 'px' - - newDecorationClasses = newLineState.decorationClasses - oldDecorationClasses = oldLineState.decorationClasses - - if oldDecorationClasses? - for decorationClass in oldDecorationClasses - unless newDecorationClasses? and decorationClass in newDecorationClasses - lineNode.classList.remove(decorationClass) - - if newDecorationClasses? - for decorationClass in newDecorationClasses - unless oldDecorationClasses? and decorationClass in oldDecorationClasses - lineNode.classList.add(decorationClass) - - oldLineState.decorationClasses = newLineState.decorationClasses - - if newLineState.top isnt oldLineState.top - lineNode.style.top = newLineState.top + 'px' - oldLineState.top = newLineState.top - - if newLineState.screenRow isnt oldLineState.screenRow - lineNode.dataset.screenRow = newLineState.screenRow - oldLineState.screenRow = newLineState.screenRow - @lineIdsByScreenRow[newLineState.screenRow] = id - - lineNodeForScreenRow: (screenRow) -> - @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] - - 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 - - for {value, scopes, hasPairedCharacter} in tokenizedLine.tokens - charWidths = @presenter.getScopedCharacterWidths(scopes) - - valueIndex = 0 - while valueIndex < value.length - if hasPairedCharacter - char = value.substr(valueIndex, 2) - charLength = 2 - valueIndex += 2 - else - char = value[valueIndex] - charLength = 1 - valueIndex++ - - continue if char is '\0' - - unless charWidths[char]? - unless textNode? - rangeForMeasurement ?= document.createRange() - iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) - textNode = iterator.nextNode() - textNodeIndex = 0 - nextTextNodeIndex = textNode.textContent.length - - while nextTextNodeIndex <= charIndex - textNode = iterator.nextNode() - textNodeIndex = nextTextNodeIndex - nextTextNodeIndex = textNodeIndex + textNode.textContent.length - - i = charIndex - textNodeIndex - rangeForMeasurement.setStart(textNode, i) - rangeForMeasurement.setEnd(textNode, i + charLength) - charWidth = rangeForMeasurement.getBoundingClientRect().width - @presenter.setScopedCharacterWidth(scopes, char, charWidth) - - charIndex += charLength - - @measuredLines.add(lineId) - - clearMeasurements: -> - @measuredLines.clear() From efeb129cff9a81e38cfa8e8d8663680b63b0959c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 May 2015 10:47:23 +0200 Subject: [PATCH 49/58] Rename LinesComponent to TileComponent --- src/{lines-component.coffee => tile-component.coffee} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{lines-component.coffee => tile-component.coffee} (100%) diff --git a/src/lines-component.coffee b/src/tile-component.coffee similarity index 100% rename from src/lines-component.coffee rename to src/tile-component.coffee From 1a5e2fe5dd6b0632b38c956822fc6223433abbca Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 May 2015 10:48:04 +0200 Subject: [PATCH 50/58] :art: --- src/{foo-component.coffee => lines-component.coffee} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{foo-component.coffee => lines-component.coffee} (100%) diff --git a/src/foo-component.coffee b/src/lines-component.coffee similarity index 100% rename from src/foo-component.coffee rename to src/lines-component.coffee From 490ab2c468605ca0165a359b3f623eee1808f149 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 May 2015 11:07:03 +0200 Subject: [PATCH 51/58] :green_heart: Conflicts: spec/text-editor-presenter-spec.coffee src/lines-component.coffee src/text-editor-presenter.coffee --- spec/text-editor-presenter-spec.coffee | 50 +++++++++++++++++++++----- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 424f4bc85..872b71c58 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -660,7 +660,7 @@ 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] it "contains states for tiles that are visible on screen", -> presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) @@ -680,6 +680,25 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.tiles[8]).toBeUndefined() + expectStateUpdate -> presenter.setScrollTop(3) + + expect(presenter.getState().content.tiles[0]).toBeUndefined() + + expectValues presenter.getState().content.tiles[2], { + top: -1 + } + expectValues presenter.getState().content.tiles[4], { + top: 1 + } + expectValues presenter.getState().content.tiles[6], { + top: 3 + } + expectValues presenter.getState().content.tiles[8], { + top: 5 + } + + expect(presenter.getState().content.tiles[10]).toBeUndefined() + it "includes state for all tiles if no external ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null, tileSize: 2) expect(presenter.getState().content.tiles[0]).toBeDefined() @@ -814,8 +833,22 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.tiles[0]).toBeDefined() describe "[tileId].lines[lineId]", -> # line state objects - xit "includes the state for lines in a tile", -> - expect(lineStateForScreenRow(presenter, 3)).toBeUndefined() + it "includes the state for visible lines in a tile", -> + presenter = buildPresenter(explicitHeight: 3, scrollTop: 4, lineHeight: 1, tileSize: 3, stoppedScrollingDelay: 200) + + expect(lineStateForScreenRow(presenter, 2)).toBeUndefined() + + line3 = editor.tokenizedLineForScreenRow(3) + expectValues lineStateForScreenRow(presenter, 3), { + screenRow: 3 + text: line3.text + tags: line3.tags + specialTokens: line3.specialTokens + firstNonWhitespaceIndex: line3.firstNonWhitespaceIndex + firstTrailingWhitespaceIndex: line3.firstTrailingWhitespaceIndex + invisibles: line3.invisibles + top: 0 + } line4 = editor.tokenizedLineForScreenRow(4) expectValues lineStateForScreenRow(presenter, 4), { @@ -826,7 +859,7 @@ describe "TextEditorPresenter", -> firstNonWhitespaceIndex: line4.firstNonWhitespaceIndex firstTrailingWhitespaceIndex: line4.firstTrailingWhitespaceIndex invisibles: line4.invisibles - top: 10 * 4 + top: 1 } line5 = editor.tokenizedLineForScreenRow(5) @@ -838,7 +871,7 @@ describe "TextEditorPresenter", -> firstNonWhitespaceIndex: line5.firstNonWhitespaceIndex firstTrailingWhitespaceIndex: line5.firstTrailingWhitespaceIndex invisibles: line5.invisibles - top: 10 * 5 + top: 2 } line6 = editor.tokenizedLineForScreenRow(6) @@ -850,7 +883,7 @@ describe "TextEditorPresenter", -> firstNonWhitespaceIndex: line6.firstNonWhitespaceIndex firstTrailingWhitespaceIndex: line6.firstTrailingWhitespaceIndex invisibles: line6.invisibles - top: 10 * 6 + top: 0 } line7 = editor.tokenizedLineForScreenRow(7) @@ -862,7 +895,7 @@ describe "TextEditorPresenter", -> firstNonWhitespaceIndex: line7.firstNonWhitespaceIndex firstTrailingWhitespaceIndex: line7.firstTrailingWhitespaceIndex invisibles: line7.invisibles - top: 10 * 7 + top: 1 } line8 = editor.tokenizedLineForScreenRow(8) @@ -874,9 +907,10 @@ describe "TextEditorPresenter", -> firstNonWhitespaceIndex: line8.firstNonWhitespaceIndex firstTrailingWhitespaceIndex: line8.firstTrailingWhitespaceIndex invisibles: line8.invisibles - top: 10 * 8 + top: 2 } + expect(lineStateForScreenRow(presenter, 9)).toBeUndefined() it "includes the .endOfLineInvisibles if the editor.showInvisibles config option is true", -> editor.setText("hello\nworld\r\n") From c9a159aab39bf242383534d7365d29fc87cca5e7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 21 May 2015 11:16:28 +0200 Subject: [PATCH 52/58] :fire: Remove unused requires --- src/lines-component.coffee | 4 ---- src/tile-component.coffee | 3 --- 2 files changed, 7 deletions(-) diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 1c855e125..23f1c0015 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -1,5 +1,3 @@ -_ = require 'underscore-plus' -{toArray} = require 'underscore-plus' {$$} = require 'space-pen' CursorsComponent = require './cursors-component' @@ -7,8 +5,6 @@ HighlightsComponent = require './highlights-component' TileComponent = require './tile-component' DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] -AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} -WrapperDiv = document.createElement('div') cloneObject = (object) -> clone = {} diff --git a/src/tile-component.coffee b/src/tile-component.coffee index 31ff61616..88e06afd2 100644 --- a/src/tile-component.coffee +++ b/src/tile-component.coffee @@ -1,9 +1,6 @@ _ = require 'underscore-plus' -{toArray} = require 'underscore-plus' -{$$} = require 'space-pen' TokenIterator = require './token-iterator' -DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} WrapperDiv = document.createElement('div') TokenTextEscapeRegex = /[&"'<>]/g From 17834157147bb3cffe2ed70d71998e3ee6e81699 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 22 May 2015 09:51:48 +0200 Subject: [PATCH 53/58] Fix spec failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @as-cii this was just a typo, but there’s a failure later in the spec that you’re probably in a better position to fix quickly. --- spec/text-editor-presenter-spec.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 872b71c58..d8d1dbbbb 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -680,7 +680,7 @@ describe "TextEditorPresenter", -> expect(presenter.getState().content.tiles[8]).toBeUndefined() - expectStateUpdate -> presenter.setScrollTop(3) + expectStateUpdate presenter, -> presenter.setScrollTop(3) expect(presenter.getState().content.tiles[0]).toBeUndefined() From 538fcfece349cc8acf4fe27999b22ee394124a14 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 May 2015 14:01:52 +0200 Subject: [PATCH 54/58] :art: Better test naming --- spec/text-editor-component-spec.coffee | 70 +++++++++++++------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 30c955c57..b2bb09acd 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -68,7 +68,7 @@ describe "TextEditorComponent", -> expect(nextAnimationFrame).not.toThrow() describe "line rendering", -> - expectLineRender = (tileNode, {screenRow, top}) -> + expectTileContainsRow = (tileNode, screenRow, {top}) -> lineNode = tileNode.querySelector("[data-screen-row='#{screenRow}']") tokenizedLine = editor.tokenizedLineForScreenRow(screenRow) @@ -78,7 +78,7 @@ describe "TextEditorComponent", -> else expect(lineNode.textContent).toBe(tokenizedLine.text) - it "renders the currently-visible lines into a tiled fashion", -> + it "renders the currently-visible lines in a tiled fashion", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' tileHeight = tileSize * lineHeightInPixels component.measureDimensions() @@ -90,21 +90,21 @@ describe "TextEditorComponent", -> expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" expect(tilesNodes[0].children.length).toBe(tileSize) - expectLineRender(tilesNodes[0], screenRow: 0, top: 0 * lineHeightInPixels) - expectLineRender(tilesNodes[0], screenRow: 1, top: 1 * lineHeightInPixels) - expectLineRender(tilesNodes[0], screenRow: 2, top: 2 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight}px, 0px)" expect(tilesNodes[1].children.length).toBe(tileSize) - expectLineRender(tilesNodes[1], screenRow: 3, top: 0 * lineHeightInPixels) - expectLineRender(tilesNodes[1], screenRow: 4, top: 1 * lineHeightInPixels) - expectLineRender(tilesNodes[1], screenRow: 5, top: 2 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeight}px, 0px)" expect(tilesNodes[2].children.length).toBe(tileSize) - expectLineRender(tilesNodes[2], screenRow: 6, top: 0 * lineHeightInPixels) - expectLineRender(tilesNodes[2], screenRow: 7, top: 1 * lineHeightInPixels) - expectLineRender(tilesNodes[2], screenRow: 8, top: 2 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) expect(component.lineNodeForScreenRow(9)).toBeUndefined() @@ -119,21 +119,21 @@ describe "TextEditorComponent", -> expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, -5px, 0px)" expect(tilesNodes[0].children.length).toBe(tileSize) - expectLineRender(tilesNodes[0], screenRow: 3, top: 0 * lineHeightInPixels) - expectLineRender(tilesNodes[0], screenRow: 4, top: 1 * lineHeightInPixels) - expectLineRender(tilesNodes[0], screenRow: 5, top: 2 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 3, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 4, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 5, top: 2 * lineHeightInPixels) expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight - 5}px, 0px)" expect(tilesNodes[1].children.length).toBe(tileSize) - expectLineRender(tilesNodes[1], screenRow: 6, top: 0 * lineHeightInPixels) - expectLineRender(tilesNodes[1], screenRow: 7, top: 1 * lineHeightInPixels) - expectLineRender(tilesNodes[1], screenRow: 8, top: 2 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 6, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 7, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 8, top: 2 * lineHeightInPixels) expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeight - 5}px, 0px)" expect(tilesNodes[2].children.length).toBe(tileSize) - expectLineRender(tilesNodes[2], screenRow: 9, top: 0 * lineHeightInPixels) - expectLineRender(tilesNodes[2], screenRow: 10, top: 1 * lineHeightInPixels) - expectLineRender(tilesNodes[2], screenRow: 11, top: 2 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 9, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 10, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 11, top: 2 * lineHeightInPixels) it "updates the top position of subsequent tiles when lines are inserted or removed", -> wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' @@ -145,14 +145,14 @@ describe "TextEditorComponent", -> tilesNodes = componentNode.querySelectorAll(".tile") expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expectLineRender(tilesNodes[0], screenRow: 0, top: 0 * lineHeightInPixels) - expectLineRender(tilesNodes[0], screenRow: 1, top: 1 * lineHeightInPixels) - expectLineRender(tilesNodes[0], screenRow: 2, top: 2 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight}px, 0px)" - expectLineRender(tilesNodes[1], screenRow: 3, top: 0 * lineHeightInPixels) - expectLineRender(tilesNodes[1], screenRow: 4, top: 1 * lineHeightInPixels) - expectLineRender(tilesNodes[1], screenRow: 5, top: 2 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) editor.getBuffer().insert([0, 0], '\n\n') nextAnimationFrame() @@ -160,19 +160,19 @@ describe "TextEditorComponent", -> tilesNodes = componentNode.querySelectorAll(".tile") expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expectLineRender(tilesNodes[0], screenRow: 0, top: 0 * lineHeightInPixels) - expectLineRender(tilesNodes[0], screenRow: 1, top: 1 * lineHeightInPixels) - expectLineRender(tilesNodes[0], screenRow: 2, top: 2 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight}px, 0px)" - expectLineRender(tilesNodes[1], screenRow: 3, top: 0 * lineHeightInPixels) - expectLineRender(tilesNodes[1], screenRow: 4, top: 1 * lineHeightInPixels) - expectLineRender(tilesNodes[1], screenRow: 5, top: 2 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeight}px, 0px)" - expectLineRender(tilesNodes[2], screenRow: 6, top: 0 * lineHeightInPixels) - expectLineRender(tilesNodes[2], screenRow: 7, top: 1 * lineHeightInPixels) - expectLineRender(tilesNodes[2], screenRow: 8, top: 2 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) it "updates the lines when lines are inserted or removed above the rendered row range", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' From 80a3294f8e97e606f77ae3dd622adce8b8a17d3d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 22 May 2015 14:05:21 +0200 Subject: [PATCH 55/58] :green_heart: Fix wrong spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @nathansobo: I just forgot to include an actually visible tile in this test which, therefore, complained. “Production code”-wise the behavior was consistent and correct. --- spec/text-editor-presenter-spec.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index d8d1dbbbb..6a1b2a6d0 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -696,8 +696,11 @@ describe "TextEditorPresenter", -> expectValues presenter.getState().content.tiles[8], { top: 5 } + expectValues presenter.getState().content.tiles[10], { + top: 7 + } - expect(presenter.getState().content.tiles[10]).toBeUndefined() + expect(presenter.getState().content.tiles[12]).toBeUndefined() it "includes state for all tiles if no external ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null, tileSize: 2) From 7926ca5f231c9cd361a1c6f2781b39a273df09ce Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 27 May 2015 09:01:41 +0200 Subject: [PATCH 56/58] Declare `atom.hasTiledEditor = true` This way packages that are affected by relative coordinates will be able to smoothly upgrade using a feature toggle. --- src/atom.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/atom.coffee b/src/atom.coffee index 0d029b61d..35ee786f7 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -34,6 +34,8 @@ class Atom extends Model atom = @deserialize(@loadState(mode)) ? new this({mode, @version}) atom.deserializeTimings.atom = Date.now() - startTime + Object.defineProperty(atom, "hasTiledEditor", value: true) + if includeDeprecatedAPIs workspaceViewDeprecationMessage = """ atom.workspaceView is no longer available. From a0ce1e6d01e581f4461c6b42d57319eb682bb8e5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 27 May 2015 12:59:36 +0200 Subject: [PATCH 57/58] Narrow scope and rename flag --- src/atom.coffee | 2 -- src/text-editor-element.coffee | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/atom.coffee b/src/atom.coffee index 35ee786f7..0d029b61d 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -34,8 +34,6 @@ class Atom extends Model atom = @deserialize(@loadState(mode)) ? new this({mode, @version}) atom.deserializeTimings.atom = Date.now() - startTime - Object.defineProperty(atom, "hasTiledEditor", value: true) - if includeDeprecatedAPIs workspaceViewDeprecationMessage = """ atom.workspaceView is no longer available. diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index 88e07a18d..e3eaaeb2a 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -17,6 +17,7 @@ class TextEditorElement extends HTMLElement attached: false tileSize: null focusOnAttach: false + hasTiledRendering: true createdCallback: -> @emitter = new Emitter From 9b4d62b687bc91b28ac4be16453f5fb148d5f1c7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 29 May 2015 15:21:00 +0200 Subject: [PATCH 58/58] :bug: Update cursors after model changes /cc: @nathansobo --- spec/text-editor-presenter-spec.coffee | 13 +++++++++++++ src/text-editor-presenter.coffee | 1 + 2 files changed, 14 insertions(+) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 0d5c5cffb..142026913 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -1108,6 +1108,19 @@ describe "TextEditorPresenter", -> expect(stateForCursor(presenter, 3)).toEqual {top: 0, left: 12 * 10, width: 10, height: 10} expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10 - 50, left: 4 * 10, width: 10, height: 10} + it "updates when ::scrollTop changes after the model was changed", -> + editor.setCursorBufferPosition([8, 22]) + presenter = buildPresenter(explicitHeight: 50, scrollTop: 10 * 8) + + expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 10 * 22, width: 10, height: 10} + + expectStateUpdate presenter, -> + editor.getBuffer().deleteRow(12) + editor.getBuffer().deleteRow(11) + editor.getBuffer().deleteRow(10) + + expect(stateForCursor(presenter, 0)).toEqual {top: 20, left: 10 * 22, width: 10, height: 10} + it "updates when ::explicitHeight changes", -> editor.setSelectedBufferRanges([ [[1, 2], [1, 2]], diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 2acd39863..7b63fa10a 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -115,6 +115,7 @@ class TextEditorPresenter @shouldUpdateScrollbarsState = true @shouldUpdateContentState = true @shouldUpdateDecorations = true + @shouldUpdateCursorsState = true @shouldUpdateTilesState = true @shouldUpdateLineNumberGutterState = true @shouldUpdateLineNumbersState = true