Comparar commits
19 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| 6e42552891 | |||
| 70d4af9341 | |||
| 9c17e8b705 | |||
| 7954b5c1fd | |||
| 3d9b22bae4 | |||
| ae6a3fddb2 | |||
| f6a8a42a6d | |||
| 16765138b8 | |||
| 3e656f83b8 | |||
| b04b025a0c | |||
| 88743e8a71 | |||
| 30d71fc37a | |||
| 09aa62fae8 | |||
| f29e50b730 | |||
| dcd60a275a | |||
| 5e6dff9175 | |||
| dd5bc5891c | |||
| 79bc071353 | |||
| d3c2633bb3 |
+2
-2
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -220,8 +220,8 @@ class DisplayBufferMarker
|
||||
@oldTailScreenPosition, newTailScreenPosition,
|
||||
@oldHeadBufferPosition, newHeadBufferPosition,
|
||||
@oldTailBufferPosition, newTailBufferPosition,
|
||||
textChanged,
|
||||
isValid
|
||||
@wasValid, isValid,
|
||||
textChanged
|
||||
}
|
||||
|
||||
@oldHeadBufferPosition = newHeadBufferPosition
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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 ' '
|
||||
@buildEndOfLineHTML(line, {}) or ' '
|
||||
|
||||
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
@@ -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 ' '
|
||||
|
||||
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
|
||||
|
||||
|
||||
Referência em uma Nova Issue
Bloquear um usuário