Comparar commits

...

19 Commits

Autor SHA1 Mensagem Data
Nathan Sobo 6e42552891 Add proper line decorations to initial state 2014-08-06 11:52:45 -06:00
Nathan Sobo 70d4af9341 Handle 'onlyNonEmpty' option for line decorations 2014-08-06 11:26:14 -06:00
Nathan Sobo 9c17e8b705 Handle 'onlyEmpty' option for line decorations 2014-08-06 11:24:16 -06:00
Nathan Sobo 7954b5c1fd Support 'onlyHead' option in line decorations 2014-08-06 11:16:01 -06:00
Nathan Sobo 3d9b22bae4 Refactor line decoration updates 2014-08-06 11:13:21 -06:00
Nathan Sobo ae6a3fddb2 Improve custom matcher to express omission of values 2014-08-06 11:13:05 -06:00
Nathan Sobo f6a8a42a6d Use line decorations to update display
Basic line decorations are working, but we still need to handle special
decoration options.
2014-08-05 19:37:54 -06:00
Nathan Sobo 16765138b8 Build line decorations into initial state 2014-08-05 19:36:55 -06:00
Nathan Sobo 3e656f83b8 Fix fat finger 2014-08-05 19:36:24 -06:00
Nathan Sobo b04b025a0c Only handle line decorations for now 2014-08-05 19:36:02 -06:00
Nathan Sobo 88743e8a71 Update the display state on when line decorations are updated or removed 2014-08-05 18:24:57 -06:00
Nathan Sobo 30d71fc37a Add line decorations to display state when they're added 2014-08-05 17:14:54 -06:00
Nathan Sobo 09aa62fae8 Make ::updateTiles an iterator for operation-specific updates
Previously, I was trying to update everything with the same method. Now
I'm performing updates that are specifically tailored to each type of
operation on the model.
2014-08-05 16:35:27 -06:00
Nathan Sobo f29e50b730 Update tile states when display buffer changes 2014-08-05 14:10:18 -06:00
Nathan Sobo dcd60a275a WIP: Base editor updates on immutable display state 2014-08-04 21:09:17 -06:00
Nathan Sobo 5e6dff9175 Render cursors 2014-08-04 12:04:46 -06:00
Nathan Sobo dd5bc5891c Break lines out into manually-updated tiles 2014-08-04 12:04:46 -06:00
Nathan Sobo 79bc071353 Avoid React handling of keydown/textInput to save ~1ms on keystrokes 2014-08-04 12:04:46 -06:00
Nathan Sobo d3c2633bb3 WIP 2014-08-04 12:04:00 -06:00
7 arquivos alterados com 954 adições e 243 exclusões
+2 -2
Ver Arquivo
@@ -34,6 +34,7 @@
"git-utils": "^2.1.3",
"grim": "0.11.0",
"guid": "0.0.10",
"immutable": "^2.0.4",
"jasmine-tagged": "^1.1.2",
"less-cache": "0.13.0",
"mixto": "^1",
@@ -45,7 +46,7 @@
"property-accessors": "^1",
"q": "^1.0.1",
"random-words": "0.0.1",
"react-atom-fork": "^0.11.1",
"react-atom-fork": "^0.11.2",
"reactionary-atom-fork": "^1.0.0",
"runas": "1.0.1",
"scandal": "1.0.0",
@@ -109,7 +110,6 @@
"welcome": "0.17.0",
"whitespace": "0.25.0",
"wrap-guide": "0.21.0",
"language-c": "0.26.0",
"language-coffee-script": "0.28.0",
"language-css": "0.17.0",
+345
Ver Arquivo
@@ -0,0 +1,345 @@
Immutable = require 'immutable'
_ = require 'underscore-plus'
DisplayStateManager = require '../src/display-state-manager'
TextBuffer = require 'text-buffer'
Editor = require '../src/editor'
fdescribe "DisplayStateManager", ->
[buffer, editor, stateManager] = []
beforeEach ->
@addMatchers
toHaveValues: ToHaveValuesMatcher
spyOn(DisplayStateManager::, 'getTileSize').andReturn 5
buffer = new TextBuffer(filePath: atom.project.resolve('sample.js'))
buffer.loadSync()
buffer.insert([12, 3], '\n' + buffer.getText()) # repeat text so we have more lines
editor = new Editor({buffer})
editor.setLineHeightInPixels(10)
editor.setDefaultCharWidth(10)
editor.setHeight(100)
editor.setWidth(500)
stateManager = new DisplayStateManager(editor)
afterEach ->
editor.destroy()
describe "initial state", ->
it "breaks the visible lines into tiles", ->
expect(stateManager.getState().get('tiles')).toHaveValues
0:
startRow: 0
left: 0
top: 0
width: editor.getScrollWidth()
height: 50
lineHeightInPixels: 10
lines: editor.linesForScreenRows(0, 4)
5:
startRow: 5
left: 0
top: 50
width: editor.getScrollWidth()
height: 50
lineHeightInPixels: 10
lines: editor.linesForScreenRows(5, 9)
10:
startRow: 10
left: 0
top: 100
width: editor.getScrollWidth()
height: 50
lineHeightInPixels: 10
lines: editor.linesForScreenRows(10, 14)
describe "when the height is changed", ->
it "updates the rendered tiles based on the new height", ->
editor.setHeight(150)
expect(stateManager.getState().get('tiles')).toHaveValues
0:
startRow: 0
top: 0
5:
startRow: 5
top: 50
10:
startRow: 10
top: 100
15:
startRow: 15
top: 150
editor.setHeight(70)
expect(stateManager.getState().get('tiles')).toHaveValues
0:
startRow: 0
top: 0
5:
startRow: 5
top: 50
describe "when the width is changed", ->
it "updates the tiles with the new width", ->
editor.setWidth(700)
expect(stateManager.getState().get('tiles')).toHaveValues
0:
width: 700
5:
width: 700
10:
width: 700
describe "when the lineHeightInPixels is changed", ->
it "updates the rendered tiles and assigns a new lineHeightInPixels value to all tiles", ->
editor.setScrollTop(10)
editor.setLineHeightInPixels(7)
expect(stateManager.getState().get('tiles')).toHaveValues
0:
startRow: 0
top: 0 - 10
lineHeightInPixels: 7
5:
startRow: 5
top: 7 * 5 - 10
lineHeightInPixels: 7
10:
startRow: 10
top: 7 * 10 - 10
lineHeightInPixels: 7
15:
startRow: 15
top: 7 * 15 - 10
lineHeightInPixels: 7
describe "when the editor is scrolled vertically", ->
it "updates the visible tiles and their top positions", ->
editor.setScrollTop(20)
expect(stateManager.getState().get('tiles')).toHaveValues
0:
left: 0
top: -20
5:
left: 0
top: 30
10:
left: 0
top: 80
editor.setScrollTop(70)
expect(stateManager.getState().get('tiles')).toHaveValues
5:
left: 0
top: -20
10:
left: 0
top: 30
15:
left: 0
top: 80
describe "when the editor is scrolled horizontally", ->
it "updates the left position of the visible tiles", ->
editor.setScrollLeft(30)
expect(stateManager.getState().get('tiles')).toHaveValues
0:
left: -30
5:
left: -30
10:
left: -30
describe "when the lines are changed", ->
it "updates the lines in the tiles", ->
buffer.setTextInRange([[3, 5], [7, 0]], "a\nb\nc\nd")
expect(stateManager.getState().get('tiles')).toHaveValues
0:
lines: editor.linesForScreenRows(0, 4)
5:
lines: editor.linesForScreenRows(5, 9)
10:
lines: editor.linesForScreenRows(10, 14)
describe "line decorations", ->
marker = null
beforeEach ->
marker = editor.markBufferRange([[3, 4], [5, 6]], invalidate: 'touch')
it "updates the display state when decorations are added, updated, invalidated, or removed", ->
decoration = editor.decorateMarker(marker, type: 'line', class: 'test')
decorationParamsById = {}
decorationParamsById[decoration.id] = decoration.getParams()
expect(stateManager.getState().get('tiles')).toHaveValues
0:
lineDecorations:
3: decorationParamsById
4: decorationParamsById
5:
lineDecorations:
5: decorationParamsById
marker.setBufferRange([[8, 4], [10, 6]])
expect(stateManager.getState().get('tiles')).toHaveValues
0:
lineDecorations:
3: null
4: null
5:
lineDecorations:
5: null
8: decorationParamsById
9: decorationParamsById
10:
lineDecorations:
10: decorationParamsById
buffer.insert([8, 5], 'invalidate marker')
expect(stateManager.getState().get('tiles')).toHaveValues
5:
lineDecorations:
8: null
9: null
10:
lineDecorations:
10: null
buffer.undo()
expect(stateManager.getState().get('tiles')).toHaveValues
5:
lineDecorations:
8: decorationParamsById
9: decorationParamsById
10:
lineDecorations:
10: decorationParamsById
marker.destroy()
expect(stateManager.getState().get('tiles')).toHaveValues
5:
lineDecorations:
8: null
9: null
10:
lineDecorations:
10: null
it "renders line decorations in the initial state", ->
decoration = editor.decorateMarker(marker, type: 'line', class: 'test')
newStateManager = new DisplayStateManager(editor)
decorationParamsById = {}
decorationParamsById[decoration.id] = decoration.getParams()
expect(stateManager.getState().get('tiles')).toHaveValues
0:
lineDecorations:
3: decorationParamsById
4: decorationParamsById
5:
lineDecorations:
5: decorationParamsById
describe "when the decoration's 'onlyHead' property is true", ->
it "only applies the decoration to lines containing the marker's head", ->
decoration = editor.decorateMarker(marker, type: 'line', class: 'only-head', onlyHead: true)
decorationParamsById = {}
decorationParamsById[decoration.id] = decoration.getParams()
expect(stateManager.getState().get('tiles')).toHaveValues
0:
lineDecorations:
3: null
4: null
5:
lineDecorations:
5: decorationParamsById
describe "when the decoration's 'onlyEmpty' property is true", ->
it "only applies the decoration if the marker is empty", ->
decoration = editor.decorateMarker(marker, type: 'line', class: 'only-empty', onlyEmpty: true)
decorationParamsById = {}
decorationParamsById[decoration.id] = decoration.getParams()
expect(stateManager.getState().get('tiles')).toHaveValues
0:
lineDecorations:
3: null
4: null
5:
lineDecorations:
5: null
marker.clearTail()
expect(stateManager.getState().get('tiles')).toHaveValues
0:
lineDecorations:
3: null
4: null
5:
lineDecorations:
5: decorationParamsById
describe "when the decoration's 'onlyNonEmpty' property is true", ->
it "only applies the decoration if the marker is non-empty", ->
decoration = editor.decorateMarker(marker, type: 'line', class: 'only-non-empty', onlyNonEmpty: true)
decorationParamsById = {}
decorationParamsById[decoration.id] = decoration.getParams()
expect(stateManager.getState().get('tiles')).toHaveValues
0:
lineDecorations:
3: decorationParamsById
4: decorationParamsById
5:
lineDecorations:
5: decorationParamsById
marker.clearTail()
expect(stateManager.getState().get('tiles')).toHaveValues
0:
lineDecorations:
3: null
4: null
5:
lineDecorations:
5: null
ToHaveValuesMatcher = (expected) ->
hasAllValues = true
wrongValues = {}
checkValues = (actual, expected, keyPath=[]) ->
for key, expectedValue of expected
key = numericKey if numericKey = parseInt(key)
currentKeyPath = keyPath.concat([key])
if expectedValue?
if actual.hasOwnProperty(key)
actualValue = actual[key]
if expectedValue.constructor is Object and _.size(expectedValue) > 0
checkValues(actualValue, expectedValue, currentKeyPath)
else
unless _.isEqual(actualValue, expectedValue)
hasAllValues = false
_.setValueForKeyPath(wrongValues, currentKeyPath.join('.'), {actualValue, expectedValue})
else
hasAllValues = false
_.setValueForKeyPath(wrongValues, currentKeyPath.join('.'), {expectedValue})
else
actualValue = actual[key]
if actualValue?
hasAllValues = false
_.setValueForKeyPath(wrongValues, currentKeyPath.join('.'), {actualValue, expectedValue})
notText = if @isNot then " not" else ""
this.message = => "Immutable object did not have expected values: #{jasmine.pp(wrongValues)}"
checkValues(@actual.toJS(), expected)
console.warn "Invalid values:", wrongValues unless hasAllValues
hasAllValues
+2 -2
Ver Arquivo
@@ -220,8 +220,8 @@ class DisplayBufferMarker
@oldTailScreenPosition, newTailScreenPosition,
@oldHeadBufferPosition, newHeadBufferPosition,
@oldTailBufferPosition, newTailBufferPosition,
textChanged,
isValid
@wasValid, isValid,
textChanged
}
@oldHeadBufferPosition = newHeadBufferPosition
+246
Ver Arquivo
@@ -0,0 +1,246 @@
{Emitter, Subscriber} = require 'emissary'
Immutable = require 'immutable'
if Immutable.Map.update?
throw new Error("Remove the Immutable.Map::update shim now that you've upgraded immutable")
else
Immutable.Map::update = (key, fn) -> @set(key, fn(@get(key)))
module.exports =
class DisplayStateManager
Emitter.includeInto(this)
Subscriber.includeInto(this)
constructor: (@editor) ->
@buildInitialState()
@observeEditor()
getState: -> @state
setState: (@state) ->
@emit 'did-change-state', @state
@state
getTileSize: -> 5
getLineWidth: ->
Math.max(@editor.getScrollWidth(), @editor.getWidth())
observeEditor: ->
@subscribe @editor.$width.changes, @onWidthChanged
@subscribe @editor.$height.changes, @onHeightChanged
@subscribe @editor.$lineHeightInPixels.changes, @onLineHeightInPixelsChanged
@subscribe @editor.$scrollLeft.changes, @onScrollLeftChanged
@subscribe @editor.$scrollTop.changes, @onScrollTopChanged
@subscribe @editor, 'screen-lines-changed', @onScreenLinesChanged
@subscribe @editor, 'decoration-added', @onDecorationAdded
@subscribe @editor, 'decoration-removed', @onDecorationRemoved
@subscribe @editor, 'decoration-changed', @onDecorationChanged
tileStartRowForScreenRow: (screenRow) ->
screenRow - (screenRow % @getTileSize())
getVisibleRowRange: ->
heightInLines = Math.floor(@editor.getHeight() / @editor.getLineHeightInPixels())
startRow = Math.ceil(@editor.getScrollTop() / @editor.getLineHeightInPixels())
endRow = Math.min(@editor.getLineCount(), startRow + heightInLines)
[startRow, endRow]
getTileRowRange: ->
[startRow, endRow] = @getVisibleRowRange()
[@tileStartRowForScreenRow(startRow), @tileStartRowForScreenRow(endRow)]
buildInitialState: ->
[startRow, endRow] = @getTileRowRange()
@state = Immutable.Map
tiles: Immutable.Map().withMutations (tiles) =>
for tileStartRow in [startRow..endRow] by @getTileSize()
tiles.set(tileStartRow, @buildTile(tileStartRow))
updateTiles: (fn) ->
tileSize = @getTileSize()
[startRow, endRow] = @getTileRowRange()
@setState @state.update 'tiles', (tiles) ->
tiles.withMutations (tiles) ->
# delete any tiles that are outside of the row range
tiles.forEach (tile, tileStartRow) ->
unless startRow <= tileStartRow <= endRow
tiles.delete(tileStartRow)
# call the callback with the start row and existing state of visible tiles
for tileStartRow in [startRow..endRow] by tileSize
if newTile = fn(tileStartRow, tiles.get(tileStartRow))
tiles.set(tileStartRow, newTile)
updateTilesIntersectingRowRange: (rangeStartRow, rangeEndRow, fn) ->
tileSize = @getTileSize()
@updateTiles (tileStartRow, tile) ->
tileEndRow = tileStartRow + tileSize
if rangeEndRow < tileStartRow or tileEndRow <= rangeStartRow
tile
else
fn(tileStartRow, tile)
buildTile: (tileStartRow) ->
lineHeightInPixels = @editor.getLineHeightInPixels()
tileSize = @getTileSize()
tileEndRow = tileStartRow + tileSize
tile = Immutable.Map
startRow: tileStartRow
left: 0 - @editor.getScrollLeft()
top: tileStartRow * lineHeightInPixels - @editor.getScrollTop()
width: @getLineWidth()
height: lineHeightInPixels * tileSize
lineHeightInPixels: @editor.getLineHeightInPixels()
lines: Immutable.Vector(@editor.linesForScreenRows(tileStartRow, tileEndRow - 1)...)
lineDecorations: Immutable.Map()
@tileWithInitialLineDecorations(tile)
tileWithInitialLineDecorations: (tile) ->
tileStart = tile.get('startRow')
tileEnd = tileStart + @getTileSize()
for markerId, decorations of @editor.decorationsForScreenRowRange(tileStart, tileEnd)
marker = @editor.getMarker(markerId)
headPosition = marker.getHeadScreenPosition()
tailPosition = marker.getTailScreenPosition()
valid = marker.isValid()
for decoration in decorations
continue unless decoration.isType('line')
id = decoration.id
params = decoration.getParams()
if rowRange = @rowRangeForLineDecoration(params, headPosition, tailPosition, valid)
[start, end] = rowRange
unless end < tileStart or tileEnd <= start
tile = @tileWithLineDecorations(tile, start, end, id, params)
tile
onWidthChanged: (width) =>
@updateTiles (tileStartRow, tile) => tile.set('width', width)
onHeightChanged: =>
@updateTiles (tileStartRow, tile) => tile ? @buildTile(tileStartRow)
onLineHeightInPixelsChanged: (lineHeightInPixels) =>
scrollTop = @editor.getScrollTop()
@updateTiles (tileStartRow, tile) =>
if tile?
tile.withMutations (tile) ->
tile.set('top', tileStartRow * lineHeightInPixels - scrollTop)
tile.set('lineHeightInPixels', lineHeightInPixels)
else
@buildTile(tileStartRow)
onScrollTopChanged: (scrollTop) =>
lineHeightInPixels = @editor.getLineHeightInPixels()
@updateTiles (tileStartRow, tile) =>
if tile?
tile.set('top', tileStartRow * lineHeightInPixels - scrollTop)
else
@buildTile(tileStartRow)
onScrollLeftChanged: (scrollLeft) =>
@updateTiles (tileStartRow, tile) ->
tile.set('left', 0 - scrollLeft)
onScreenLinesChanged: (change) =>
@updateTiles (tileStartRow, tile) =>
tileEndRow = tileStartRow + @getTileSize()
if change.start < tileEndRow
tile.set 'lines',
Immutable.Vector(@editor.linesForScreenRows(tileStartRow, tileEndRow - 1)...)
onDecorationAdded: (marker, decoration) =>
return unless decoration.isType('line')
id = decoration.id
params = decoration.getParams()
headPosition = marker.getHeadScreenPosition()
tailPosition = marker.getTailScreenPosition()
valid = marker.isValid()
if rowRange = @rowRangeForLineDecoration(params, headPosition, tailPosition, valid)
[start, end] = rowRange
@updateTilesIntersectingRowRange start, end, (tileStart, tile) =>
@tileWithLineDecorations(tile, start, end, id, params)
onDecorationRemoved: (marker, decoration) =>
return unless decoration.isType('line')
id = decoration.id
params = decoration.getParams()
headPosition = marker.getHeadScreenPosition()
tailPosition = marker.getTailScreenPosition()
valid = true # FIXME: Why is a marker invalidated when destroyed? That seems wrong.
if rowRange = @rowRangeForLineDecoration(decoration, headPosition, tailPosition, valid)
[start, end] = rowRange
@updateTilesIntersectingRowRange start, end, (tileStart, tile) =>
@tileWithoutLineDecorations(tile, start, end, id)
onDecorationChanged: (marker, decoration, change) =>
return unless decoration.isType('line')
params = decoration.getParams()
{oldHeadScreenPosition, oldTailScreenPosition, wasValid} = change
if rowRangeToRemove = @rowRangeForLineDecoration(params, oldHeadScreenPosition, oldTailScreenPosition, wasValid)
[start, end] = rowRangeToRemove
@updateTilesIntersectingRowRange start, end, (tileStart, tile) =>
@tileWithoutLineDecorations(tile, start, end, decoration.id)
{newHeadScreenPosition, newTailScreenPosition, isValid} = change
if rowRangeToAdd = @rowRangeForLineDecoration(params, newHeadScreenPosition, newTailScreenPosition, isValid)
[start, end] = rowRangeToAdd
@updateTilesIntersectingRowRange start, end, (tileStart, tile) =>
@tileWithLineDecorations(tile, start, end, decoration.id, params)
rowRangeForLineDecoration: (params, headPosition, tailPosition, valid) ->
return unless valid
if params.onlyHead
return [headPosition.row, headPosition.row]
if params.onlyEmpty
return unless headPosition.isEqual(tailPosition)
if params.onlyNonEmpty
return if headPosition.isEqual(tailPosition)
start = Math.min(headPosition.row, tailPosition.row)
end = Math.max(headPosition.row, tailPosition.row)
[start, end]
tileWithLineDecorations: (tile, start, end, decorationId, decorationParams) ->
tileStart = tile.get('startRow')
tileEnd = tileStart + @getTileSize()
start = Math.max(start, tileStart)
end = Math.min(end, tileEnd)
tile.update 'lineDecorations', (lineDecorations) ->
lineDecorations.withMutations (lineDecorations) ->
for row in [start..end]
lineDecorations.update row, (decorationsById) ->
decorationsById ?= Immutable.Map()
decorationsById.set(decorationId, decorationParams)
tileWithoutLineDecorations: (tile, start, end, decorationId) ->
tileStart = tile.get('startRow')
tileEnd = tileStart + @getTileSize()
start = Math.max(start, tileStart)
end = Math.min(end, tileEnd)
tile.update 'lineDecorations', (lineDecorations) ->
lineDecorations.withMutations (lineDecorations) ->
for row in [start..end]
lineDecorations.update row, (decorationsById) ->
decorationsById?.delete(decorationId)
if lineDecorations.get(row)?.length is 0
lineDecorations.delete(row)
+15 -3
Ver Arquivo
@@ -10,6 +10,7 @@ LinesComponent = require './lines-component'
ScrollbarComponent = require './scrollbar-component'
ScrollbarCornerComponent = require './scrollbar-corner-component'
SubscriberMixin = require './subscriber-mixin'
DisplayStateManager = require './display-state-manager'
module.exports =
EditorComponent = React.createClass
@@ -56,6 +57,9 @@ EditorComponent = React.createClass
style = {}
if @performedInitialMeasurement
displayState = @displayStateManager.getState()
tilesState = displayState.get('tiles')
renderedRowRange = @getRenderedRowRange()
[renderedStartRow, renderedEndRow] = renderedRowRange
cursorPixelRects = @getCursorPixelRects(renderedRowRange)
@@ -106,7 +110,7 @@ EditorComponent = React.createClass
onBlur: @onInputBlurred
LinesComponent {
ref: 'lines',
ref: 'lines', tilesState,
editor, lineHeightInPixels, defaultCharWidth, lineDecorations, highlightDecorations,
showIndentGuide, renderedRowRange, @pendingChanges, scrollTop, scrollLeft,
@scrollingVertically, scrollHeight, scrollWidth, mouseWheelScreenRow, invisibles,
@@ -167,6 +171,9 @@ EditorComponent = React.createClass
@observeConfig()
@setScrollSensitivity(atom.config.get('editor.scrollSensitivity'))
@displayStateManager = new DisplayStateManager(@props.editor)
@subscribe @displayStateManager, 'did-change-state', @requestUpdate
componentDidMount: ->
{editor} = @props
@@ -213,19 +220,21 @@ EditorComponent = React.createClass
@measureScrollbars() if @measuringScrollbars
performInitialMeasurement: ->
console.log "INITIAL MEASUREMENT"
@updatesPaused = true
@measureHeightAndWidth()
@sampleFontStyling()
@sampleBackgroundColors()
@measureScrollbars()
@measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown
@remeasureCharacterWidths() if @remeasureCharacterWidthsWhenShown
# @remeasureCharacterWidths() if @remeasureCharacterWidthsWhenShown
@props.editor.setVisible(true)
@updatesPaused = false
@performedInitialMeasurement = true
requestUpdate: ->
return unless @isMounted()
@pauseDOMPolling()
if @updatesPaused
@updateRequestedWhilePaused = true
@@ -293,7 +302,9 @@ EditorComponent = React.createClass
{cursor} = selection
screenRange = cursor.getScreenRange()
if renderedStartRow <= screenRange.start.row < renderedEndRow
cursorPixelRects[cursor.id] = editor.pixelRectForScreenRange(screenRange)
pixelRect = editor.pixelRectForScreenRange(screenRange)
pixelRect.startRow = screenRange.start.row
cursorPixelRects[cursor.id] = pixelRect
cursorPixelRects
getLineDecorations: (decorationsByMarkerId) ->
@@ -531,6 +542,7 @@ EditorComponent = React.createClass
event.preventDefault() unless event.data is ' '
return unless @isInputEnabled()
event.reactSkipEventDispatch = true
{editor} = @props
inputNode = event.target
+302
Ver Arquivo
@@ -0,0 +1,302 @@
{extend, toArray, isEqual} = require 'underscore-plus'
Decoration = require './decoration'
WrapperDiv = document.createElement('div')
module.exports =
class EditorTileComponent
constructor: (@state) ->
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
@renderedDecorationsByLineId = {}
@cursorPixelRectsById = {}
@cursorNodesById = {}
@domNode = document.createElement('div')
@domNode.style.position = 'absolute'
@domNode.style['-webkit-transform'] = @getTransform()
@domNode.style.height = @state.get('height') + 'px'
@domNode.style.width = @state.get('width') + 'px'
@buildLines()
stateChangedForKeys: ->
for key in arguments
return true if @prevState?.get(key) isnt @state.get(key)
false
update: (newState) ->
@prevState = @state
@state = newState
if @stateChangedForKeys('top', 'left')
@domNode.style['-webkit-transform'] = @getTransform()
if @stateChangedForKeys('width')
@domNode.style.width = @state.get('width') + 'px'
if @stateChangedForKeys('lines', 'lineDecorations')
@updateLines()
# @clearScreenRowCaches() if newProps.lineHeightInPixels isnt @props.lineHeightInPixels
# @updateLines()
# @updateCursors()
getTransform: ->
"translate3d(#{@state.get('left')}px, #{@state.get('top')}px, 0px)"
buildLines: ->
startRow = @state.get('startRow')
lines = @state.get('lines')
linesHTML = ""
lines.forEach (line, i) =>
screenRow = startRow + i
linesHTML += @buildLineHTML(line, screenRow)
@domNode.innerHTML = linesHTML
lines.forEach (line, i) =>
screenRow = startRow + i
lineNode = @domNode.children[i]
@lineNodesByLineId[line.id] = lineNode
@screenRowsByLineId[line.id] = screenRow
@lineIdsByScreenRow[screenRow] = line.id
updateLines: ->
lines = @state.get('lines')
@removeLineNodes(lines)
@appendOrUpdateVisibleLineNodes(lines)
removeLineNodes: (lines=[]) ->
lineIds = new Set
lines.forEach (line) -> lineIds.add(line.id.toString())
for lineId, lineNode of @lineNodesByLineId when not lineIds.has(lineId)
screenRow = @screenRowsByLineId[lineId]
delete @lineNodesByLineId[lineId]
delete @lineIdsByScreenRow[screenRow] if @lineIdsByScreenRow[screenRow] is lineId
delete @screenRowsByLineId[lineId]
delete @renderedDecorationsByLineId[lineId]
@domNode.removeChild(lineNode)
appendOrUpdateVisibleLineNodes: (visibleLines) ->
startRow = @state.get('startRow')
newLines = null
newLinesHTML = null
visibleLines.forEach (line, index) =>
screenRow = startRow + index
if @hasLineNode(line.id)
@updateLineNode(line, screenRow)
else
newLines ?= []
newLinesHTML ?= ""
newLines.push(line)
newLinesHTML += @buildLineHTML(line, screenRow)
@screenRowsByLineId[line.id] = screenRow
@lineIdsByScreenRow[screenRow] = line.id
return unless newLines?
WrapperDiv.innerHTML = newLinesHTML
newLineNodes = toArray(WrapperDiv.children)
for line, i in newLines
lineNode = newLineNodes[i]
@lineNodesByLineId[line.id] = lineNode
@domNode.appendChild(lineNode)
updateLineNode: (line, screenRow) ->
startRow = @state.get('startRow')
lineWidth = @state.get('width')
lineHeightInPixels = @state.get('lineHeightInPixels')
lineNode = @lineNodesByLineId[line.id]
unless @screenRowsByLineId[line.id] is screenRow
lineNode.style.top = (screenRow - startRow) * lineHeightInPixels + 'px'
lineNode.dataset.screenRow = screenRow
@screenRowsByLineId[line.id] = screenRow
@lineIdsByScreenRow[screenRow] = line.id
prevLineDecorations = @prevState?.get('lineDecorations').get(screenRow)
lineDecorations = @state.get('lineDecorations').get(screenRow)
if lineDecorations isnt prevLineDecorations
prevLineDecorations?.forEach (decoration) ->
unless lineDecorations?.has(decoration.id)
lineNode.classList.remove(decoration.class)
lineDecorations?.forEach (decoration) ->
unless prevLineDecorations?.has(decoration.id)
lineNode.classList.add(decoration.class)
clearScreenRowCaches: ->
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
hasLineNode: (lineId) ->
@lineNodesByLineId.hasOwnProperty(lineId)
lineNodeForScreenRow: (screenRow) ->
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
hasDecoration: (decorations, decoration) ->
decorations? and decorations[decoration.id] is decoration
buildLineHTML: (line, screenRow) ->
startRow = @state.get('startRow')
lineHeightInPixels = @state.get('lineHeightInPixels')
width = @state.get('width')
{text, fold, isSoftWrapped, indentLevel} = line
classes = @getLineClasses(screenRow)
top = (screenRow - startRow) * lineHeightInPixels
style = "position: absolute; top: #{top}px; width: 100%;"
lineHTML = """<div class="#{classes}" style="#{style}">"""
if text is ""
lineHTML += @buildEmptyLineInnerHTML(line)
else
lineHTML += @buildLineInnerHTML(line)
lineHTML += '<span class="fold-marker"></span>' if fold?
lineHTML += "</div>"
lineHTML
getLineClasses: (screenRow) ->
classes = ''
if decorationsById = @state.get('lineDecorations').get(screenRow)
decorationsById.forEach (decoration) ->
classes += decoration.class + ' '
classes + 'line'
buildEmptyLineInnerHTML: (line) ->
invisibles = {}
showIndentGuide = false
# {showIndentGuide, invisibles} = @props
{cr, eol} = invisibles
{indentLevel, tabLength} = line
if showIndentGuide and indentLevel > 0
invisiblesToRender = []
invisiblesToRender.push(cr) if cr? and line.lineEnding is '\r\n'
invisiblesToRender.push(eol) if eol?
lineHTML = ''
for i in [0...indentLevel]
lineHTML += "<span class='indent-guide'>"
for j in [0...tabLength]
if invisible = invisiblesToRender.shift()
lineHTML += "<span class='invisible-character'>#{invisible}</span>"
else
lineHTML += ' '
lineHTML += "</span>"
while invisiblesToRender.length
lineHTML += "<span class='invisible-character'>#{invisiblesToRender.shift()}</span>"
lineHTML
else
# @buildEndOfLineHTML(line, @props.invisibles) or '&nbsp;'
@buildEndOfLineHTML(line, {}) or '&nbsp;'
buildLineInnerHTML: (line) ->
# {invisibles, mini, showIndentGuide} = @props
invisibles = {}
mini = false
showIndentGuide = false
{tokens, text} = line
innerHTML = ""
scopeStack = []
firstTrailingWhitespacePosition = text.search(/\s*$/)
lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0
for token in tokens
innerHTML += @updateScopeStack(scopeStack, token.scopes)
hasIndentGuide = not mini and showIndentGuide and (token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly))
innerHTML += token.getValueAsHtml({invisibles, hasIndentGuide})
innerHTML += @popScope(scopeStack) while scopeStack.length > 0
innerHTML += @buildEndOfLineHTML(line, invisibles)
innerHTML
buildEndOfLineHTML: (line, invisibles) ->
# return '' if @props.mini or line.isSoftWrapped()
return '' if line.isSoftWrapped()
html = ''
# Note the lack of '?' in the character checks. A user can set the chars
# to an empty string which we will interpret as not-set
if invisibles.cr and line.lineEnding is '\r\n'
html += "<span class='invisible-character'>#{invisibles.cr}</span>"
if invisibles.eol
html += "<span class='invisible-character'>#{invisibles.eol}</span>"
html
updateScopeStack: (scopeStack, desiredScopes) ->
html = ""
# Find a common prefix
for scope, i in desiredScopes
break unless scopeStack[i] is desiredScopes[i]
# Pop scopes until we're at the common prefx
until scopeStack.length is i
html += @popScope(scopeStack)
# Push onto common prefix until scopeStack equals desiredScopes
for j in [i...desiredScopes.length]
html += @pushScope(scopeStack, desiredScopes[j])
html
popScope: (scopeStack) ->
scopeStack.pop()
"</span>"
pushScope: (scopeStack, scope) ->
scopeStack.push(scope)
"<span class=\"#{scope.replace(/\.+/g, ' ')}\">"
updateCursors: ->
return
{cursorPixelRects, startRow, lineHeightInPixels} = @props
for id of @cursorPixelRectsById
@removeCursorNode(id) unless cursorPixelRects?.hasOwnProperty(id)
if cursorPixelRects?
for id, newPixelRect of cursorPixelRects
newPixelRect.top -= startRow * lineHeightInPixels
if oldPixelRect = @cursorPixelRectsById[id]
unless isEqual(oldPixelRect, newPixelRect)
@updateCursorNode(id, newPixelRect)
else
@buildCursorNode(id, newPixelRect)
updateCursorNode: (id, pixelRect) ->
{top, left, height, width} = pixelRect
@cursorNodesById[id].style.top = top + 'px'
@cursorNodesById[id].style.left = left + 'px'
@cursorNodesById[id].style.height = height + 'px'
@cursorNodesById[id].style.width = width + 'px'
@cursorPixelRectsById[id] = pixelRect
buildCursorNode: (id, pixelRect) ->
cursorNode = document.createElement('div')
cursorNode.className = 'cursor'
cursorNode.style.position = 'absolute'
@cursorNodesById[id] = cursorNode
@cursorPixelRectsById[id] = pixelRect
@updateCursorNode(id, pixelRect)
@domNode.appendChild(cursorNode)
removeCursorNode: (id) ->
@domNode.removeChild(@cursorNodesById[id])
delete @cursorPixelRectsById[id]
delete @cursorNodesById[id]
+42 -236
Ver Arquivo
@@ -7,6 +7,7 @@ React = require 'react-atom-fork'
Decoration = require './decoration'
CursorsComponent = require './cursors-component'
HighlightsComponent = require './highlights-component'
EditorTileComponent = require './editor-tile-component'
DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
@@ -16,48 +17,18 @@ module.exports =
LinesComponent = React.createClass
displayName: 'LinesComponent'
tileSize: 5
render: ->
{performedInitialMeasurement, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
if performedInitialMeasurement
{editor, highlightDecorations, scrollHeight, scrollWidth, placeholderText, backgroundColor} = @props
{lineHeightInPixels, defaultCharWidth, scrollViewHeight, scopedCharacterWidthsChangeCount} = @props
{scrollTop, scrollLeft, cursorPixelRects} = @props
style =
height: Math.max(scrollHeight, scrollViewHeight)
width: scrollWidth
WebkitTransform: @getTransform()
backgroundColor: backgroundColor
div {className: 'lines', style},
div className: 'placeholder-text', placeholderText if placeholderText?
CursorsComponent {
cursorPixelRects, cursorBlinkPeriod, cursorBlinkResumeDelay, lineHeightInPixels,
defaultCharWidth, scopedCharacterWidthsChangeCount, performedInitialMeasurement
}
HighlightsComponent {
editor, highlightDecorations, lineHeightInPixels, defaultCharWidth,
scopedCharacterWidthsChangeCount, performedInitialMeasurement
}
getTransform: ->
{scrollTop, scrollLeft, useHardwareAcceleration} = @props
if useHardwareAcceleration
"translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)"
else
"translate(#{-scrollLeft}px, #{-scrollTop}px)"
div className: 'lines'
componentWillMount: ->
@measuredLines = new WeakSet
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
@renderedDecorationsByLineId = {}
@tileComponentsByStartRow = {}
shouldComponentUpdate: (newProps) ->
return newProps.tilesState isnt @props.tilesState
return true unless isEqualForProperties(newProps, @props,
'renderedRowRange', 'lineDecorations', 'highlightDecorations', 'lineHeightInPixels', 'defaultCharWidth',
'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically', 'invisibles', 'visible',
@@ -78,209 +49,19 @@ LinesComponent = React.createClass
false
componentDidUpdate: (prevProps) ->
{visible, scrollingVertically, performedInitialMeasurement} = @props
{performedInitialMeasurement, visible, scrollingVertically} = @props
return unless performedInitialMeasurement
@clearScreenRowCaches() unless prevProps.lineHeightInPixels is @props.lineHeightInPixels
@removeLineNodes() unless isEqualForProperties(prevProps, @props, 'showIndentGuide', 'invisibles')
@updateLines(@props.lineWidth isnt prevProps.lineWidth)
@clearTiles() unless isEqualForProperties(prevProps, @props, 'showIndentGuide', 'invisibles')
@updateTiles(prevProps)
@measureCharactersInNewLines() if visible and not scrollingVertically
clearScreenRowCaches: ->
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}
updateLines: (updateWidth) ->
{editor, renderedRowRange, showIndentGuide, selectionChanged, lineDecorations} = @props
[startRow, endRow] = renderedRowRange
visibleLines = editor.linesForScreenRows(startRow, endRow - 1)
@removeLineNodes(visibleLines)
@appendOrUpdateVisibleLineNodes(visibleLines, startRow, updateWidth)
removeLineNodes: (visibleLines=[]) ->
{mouseWheelScreenRow} = @props
visibleLineIds = new Set
visibleLineIds.add(line.id.toString()) for line in visibleLines
node = @getDOMNode()
for lineId, lineNode of @lineNodesByLineId when not visibleLineIds.has(lineId)
screenRow = @screenRowsByLineId[lineId]
if not screenRow? or screenRow isnt mouseWheelScreenRow
delete @lineNodesByLineId[lineId]
delete @lineIdsByScreenRow[screenRow] if @lineIdsByScreenRow[screenRow] is lineId
delete @screenRowsByLineId[lineId]
delete @renderedDecorationsByLineId[lineId]
node.removeChild(lineNode)
appendOrUpdateVisibleLineNodes: (visibleLines, startRow, updateWidth) ->
{lineDecorations} = @props
newLines = null
newLinesHTML = null
for line, index in visibleLines
screenRow = startRow + index
if @hasLineNode(line.id)
@updateLineNode(line, screenRow, updateWidth)
else
newLines ?= []
newLinesHTML ?= ""
newLines.push(line)
newLinesHTML += @buildLineHTML(line, screenRow)
@screenRowsByLineId[line.id] = screenRow
@lineIdsByScreenRow[screenRow] = line.id
@renderedDecorationsByLineId[line.id] = lineDecorations[screenRow]
return unless newLines?
WrapperDiv.innerHTML = newLinesHTML
newLineNodes = toArray(WrapperDiv.children)
node = @getDOMNode()
for line, i in newLines
lineNode = newLineNodes[i]
@lineNodesByLineId[line.id] = lineNode
node.appendChild(lineNode)
hasLineNode: (lineId) ->
@lineNodesByLineId.hasOwnProperty(lineId)
buildLineHTML: (line, screenRow) ->
{editor, mini, showIndentGuide, lineHeightInPixels, lineDecorations, lineWidth} = @props
{tokens, text, lineEnding, fold, isSoftWrapped, indentLevel} = line
classes = ''
if decorations = lineDecorations[screenRow]
for id, decoration of decorations
if Decoration.isType(decoration, 'line')
classes += decoration.class + ' '
classes += 'line'
top = screenRow * lineHeightInPixels
lineHTML = "<div class=\"#{classes}\" style=\"position: absolute; top: #{top}px; width: #{lineWidth}px;\" data-screen-row=\"#{screenRow}\">"
if text is ""
lineHTML += @buildEmptyLineInnerHTML(line)
else
lineHTML += @buildLineInnerHTML(line)
lineHTML += '<span class="fold-marker"></span>' if fold
lineHTML += "</div>"
lineHTML
buildEmptyLineInnerHTML: (line) ->
{showIndentGuide, invisibles} = @props
{cr, eol} = invisibles
{indentLevel, tabLength} = line
if showIndentGuide and indentLevel > 0
invisiblesToRender = []
invisiblesToRender.push(cr) if cr? and line.lineEnding is '\r\n'
invisiblesToRender.push(eol) if eol?
lineHTML = ''
for i in [0...indentLevel]
lineHTML += "<span class='indent-guide'>"
for j in [0...tabLength]
if invisible = invisiblesToRender.shift()
lineHTML += "<span class='invisible-character'>#{invisible}</span>"
else
lineHTML += ' '
lineHTML += "</span>"
while invisiblesToRender.length
lineHTML += "<span class='invisible-character'>#{invisiblesToRender.shift()}</span>"
lineHTML
else
@buildEndOfLineHTML(line, @props.invisibles) or '&nbsp;'
buildLineInnerHTML: (line) ->
{invisibles, mini, showIndentGuide, invisibles} = @props
{tokens, text} = line
innerHTML = ""
scopeStack = []
firstTrailingWhitespacePosition = text.search(/\s*$/)
lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0
for token in tokens
innerHTML += @updateScopeStack(scopeStack, token.scopes)
hasIndentGuide = not mini and showIndentGuide and (token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly))
innerHTML += token.getValueAsHtml({invisibles, hasIndentGuide})
innerHTML += @popScope(scopeStack) while scopeStack.length > 0
innerHTML += @buildEndOfLineHTML(line, invisibles)
innerHTML
buildEndOfLineHTML: (line, invisibles) ->
return '' if @props.mini or line.isSoftWrapped()
html = ''
# Note the lack of '?' in the character checks. A user can set the chars
# to an empty string which we will interpret as not-set
if invisibles.cr and line.lineEnding is '\r\n'
html += "<span class='invisible-character'>#{invisibles.cr}</span>"
if invisibles.eol
html += "<span class='invisible-character'>#{invisibles.eol}</span>"
html
updateScopeStack: (scopeStack, desiredScopes) ->
html = ""
# Find a common prefix
for scope, i in desiredScopes
break unless scopeStack[i] is desiredScopes[i]
# Pop scopes until we're at the common prefx
until scopeStack.length is i
html += @popScope(scopeStack)
# Push onto common prefix until scopeStack equals desiredScopes
for j in [i...desiredScopes.length]
html += @pushScope(scopeStack, desiredScopes[j])
html
popScope: (scopeStack) ->
scopeStack.pop()
"</span>"
pushScope: (scopeStack, scope) ->
scopeStack.push(scope)
"<span class=\"#{scope.replace(/\.+/g, ' ')}\">"
updateLineNode: (line, screenRow, updateWidth) ->
{editor, lineHeightInPixels, lineDecorations, lineWidth} = @props
lineNode = @lineNodesByLineId[line.id]
decorations = lineDecorations[screenRow]
previousDecorations = @renderedDecorationsByLineId[line.id]
if previousDecorations?
for id, decoration of previousDecorations
if Decoration.isType(decoration, 'line') and not @hasDecoration(decorations, decoration)
lineNode.classList.remove(decoration.class)
if decorations?
for id, decoration of decorations
if Decoration.isType(decoration, 'line') and not @hasDecoration(previousDecorations, decoration)
lineNode.classList.add(decoration.class)
lineNode.style.width = lineWidth + 'px' if updateWidth
unless @screenRowsByLineId[line.id] is screenRow
lineNode.style.top = screenRow * lineHeightInPixels + 'px'
lineNode.dataset.screenRow = screenRow
@screenRowsByLineId[line.id] = screenRow
@lineIdsByScreenRow[screenRow] = line.id
hasDecoration: (decorations, decoration) ->
decorations? and decorations[decoration.id] is decoration
lineNodeForScreenRow: (screenRow) ->
@lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
tileComponent = @tileComponentsByStartRow[@tileStartRowForScreenRow(screenRow)]
tileComponent?.lineNodeForScreenRow(screenRow)
tileStartRowForScreenRow: (screenRow) ->
screenRow - (screenRow % @tileSize)
measureLineHeightAndDefaultCharWidth: ->
node = @getDOMNode()
@@ -293,19 +74,44 @@ LinesComponent = React.createClass
editor.setLineHeightInPixels(lineHeightInPixels)
editor.setDefaultCharWidth(charWidth)
updateTiles: (prevProps) ->
domNode = @getDOMNode()
prevProps.tilesState?.forEach (tileState, tileStartRow) =>
unless @props.tilesState.has(tileStartRow)
tileComponent = @tileComponentsByStartRow[tileStartRow]
domNode.removeChild(tileComponent.domNode)
delete @tileComponentsByStartRow[tileStartRow]
@props.tilesState.forEach (tileState, tileStartRow) =>
if prevProps.tilesState?.has(tileStartRow)
tileComponent = @tileComponentsByStartRow[tileStartRow]
tileComponent.update(tileState)
else
tileComponent = new EditorTileComponent(tileState)
@tileComponentsByStartRow[tileStartRow] = tileComponent
domNode.appendChild(tileComponent.domNode)
clearTiles: ->
for startRow, tileComponent of @tileComponentsByStartRow
domNode.removeChild(tileComponent.domNode)
delete @tileComponentsByStartRow[startRow]
remeasureCharacterWidths: ->
@clearScopedCharWidths()
@measureCharactersInNewLines()
measureCharactersInNewLines: ->
return
{editor} = @props
[visibleStartRow, visibleEndRow] = @props.renderedRowRange
node = @getDOMNode()
editor.batchCharacterMeasurement =>
for tokenizedLine in editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1)
for tokenizedLine, i in editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1)
screenRow = visibleStartRow + i
unless @measuredLines.has(tokenizedLine)
lineNode = @lineNodesByLineId[tokenizedLine.id]
lineNode = @lineNodeForScreenRow(screenRow)
@measureCharactersInLine(tokenizedLine, lineNode)
return