Comparar commits
40 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| da864b6eec | |||
| ce75ee011a | |||
| 71b7fb3b5d | |||
| f413a21a11 | |||
| 544cd54a41 | |||
| 8b1eec39b5 | |||
| 02b955f823 | |||
| 30d386527d | |||
| 0af6dfc94c | |||
| ef7a6b94b2 | |||
| 4d6be72f56 | |||
| 3ded5ec0f8 | |||
| 2f15f72139 | |||
| 6f9fa39ad2 | |||
| f2eeb0629f | |||
| 1fdb4dbeda | |||
| 54c2139992 | |||
| 6e7dedb198 | |||
| 1fc33e1e21 | |||
| 583096a916 | |||
| 4d88acd756 | |||
| a3afaed950 | |||
| 716219b9a3 | |||
| bc558194e9 | |||
| c1ee2f420b | |||
| 58d9304c22 | |||
| 3226f54b97 | |||
| 63969a6467 | |||
| d5f7aca0ad | |||
| 94160044ad | |||
| 4dcb35152f | |||
| 45ac962681 | |||
| c5d502ab88 | |||
| c75fc8aa01 | |||
| e954430599 | |||
| 44795da767 | |||
| 11df5c2855 | |||
| 11189692b7 | |||
| a7fb07ccfa | |||
| a8df20271f |
+1
-2
@@ -45,7 +45,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 +109,6 @@
|
||||
"welcome": "0.17.0",
|
||||
"whitespace": "0.25.0",
|
||||
"wrap-guide": "0.21.0",
|
||||
|
||||
"language-c": "0.26.0",
|
||||
"language-coffee-script": "0.29.0",
|
||||
"language-css": "0.17.0",
|
||||
|
||||
@@ -1167,3 +1167,11 @@ describe "DisplayBuffer", ->
|
||||
|
||||
expect(displayBuffer.getScrollWidth()).toBe 10 * 63 + operatorWidth * 2 + cursorWidth
|
||||
expect(changedSpy.callCount).toBe 1
|
||||
|
||||
describe "::lineNumbersForScreenRows(startRow, endRow)", ->
|
||||
it "returns the line numbers for the given screen row range, inclusive of the endRow", ->
|
||||
displayBuffer.createFold(4, 7)
|
||||
displayBuffer.setEditorWidthInChars(30)
|
||||
displayBuffer.setSoftWrap(true)
|
||||
|
||||
expect(displayBuffer.lineNumbersForScreenRows(6, 11)).toEqual ['4.2', '5', '9', '9.1', '9.2', '10']
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
TextBuffer = require 'text-buffer'
|
||||
_ = require 'underscore-plus'
|
||||
EditorPresenter = require '../src/editor-presenter'
|
||||
Editor = require '../src/editor'
|
||||
|
||||
describe "DisplayStateManager", ->
|
||||
[buffer, editor, presenter] = []
|
||||
|
||||
beforeEach ->
|
||||
@addMatchers(toHaveValues: ToHaveValuesMatcher)
|
||||
spyOn(EditorPresenter::, 'getContentTileSize').andReturn 5
|
||||
spyOn(EditorPresenter::, 'getGutterTileSize').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)
|
||||
|
||||
presenter = new EditorPresenter(editor)
|
||||
|
||||
afterEach ->
|
||||
editor.destroy()
|
||||
|
||||
describe "scrollPosition", ->
|
||||
it "maintains the scrollTop and scrollLeft as top-level presenter properties", ->
|
||||
expect(presenter.scrollTop).toBe 0
|
||||
expect(presenter.scrollLeft).toBe 0
|
||||
editor.setScrollTop(20)
|
||||
editor.setScrollLeft(30)
|
||||
expect(presenter.scrollTop).toBe 20
|
||||
expect(presenter.scrollLeft).toBe 30
|
||||
|
||||
describe "tiles", ->
|
||||
describe "initial state", ->
|
||||
it "renders tiles that overlap the visible row range", ->
|
||||
expect(presenter).toHaveValues
|
||||
content:
|
||||
tiles:
|
||||
0:
|
||||
startRow: 0
|
||||
top: 0
|
||||
width: editor.getWidth()
|
||||
height: 5 * 10
|
||||
lineHeightInPixels: 10
|
||||
lines: editor.linesForScreenRows(0, 4)
|
||||
5:
|
||||
startRow: 5
|
||||
top: 50
|
||||
width: editor.getWidth()
|
||||
height: 5 * 10
|
||||
lineHeightInPixels: 10
|
||||
lines: editor.linesForScreenRows(5, 9)
|
||||
10:
|
||||
startRow: 10
|
||||
top: 100
|
||||
width: editor.getWidth()
|
||||
height: 5 * 10
|
||||
lineHeightInPixels: 10
|
||||
lines: editor.linesForScreenRows(10, 14)
|
||||
gutter:
|
||||
tiles:
|
||||
0:
|
||||
startRow: 0
|
||||
top: 0
|
||||
height: 5 * 10
|
||||
lineHeightInPixels: 10
|
||||
lineNumbers: editor.lineNumbersForScreenRows(0, 4)
|
||||
5:
|
||||
startRow: 5
|
||||
top: 50
|
||||
height: 5 * 10
|
||||
lineHeightInPixels: 10
|
||||
lineNumbers: editor.lineNumbersForScreenRows(5, 9)
|
||||
10:
|
||||
startRow: 10
|
||||
top: 100
|
||||
height: 5 * 10
|
||||
lineHeightInPixels: 10
|
||||
lineNumbers: editor.lineNumbersForScreenRows(10, 14)
|
||||
|
||||
it "renders a dummy gutter tile to maintain the proper gutter width", ->
|
||||
expect(presenter.gutter).toHaveValues
|
||||
dummyTile:
|
||||
dummy: true
|
||||
maxLineNumberDigits: 2
|
||||
|
||||
describe "when the width is changed", ->
|
||||
it "updates the line tiles with the new width", ->
|
||||
editor.setWidth(700)
|
||||
expect(presenter.content.tiles).toHaveValues
|
||||
0:
|
||||
width: 700
|
||||
5:
|
||||
width: 700
|
||||
10:
|
||||
width: 700
|
||||
|
||||
describe "when the height is changed", ->
|
||||
it "updates the rendered tiles to reflect the change", ->
|
||||
editor.setHeight(160)
|
||||
expect(presenter).toHaveValues
|
||||
content:
|
||||
tiles:
|
||||
0:
|
||||
startRow: 0
|
||||
top: 0
|
||||
5:
|
||||
startRow: 5
|
||||
top: 50
|
||||
10:
|
||||
startRow: 10
|
||||
top: 100
|
||||
15:
|
||||
startRow: 15
|
||||
top: 150
|
||||
gutter:
|
||||
tiles:
|
||||
0:
|
||||
startRow: 0
|
||||
top: 0
|
||||
5:
|
||||
startRow: 5
|
||||
top: 50
|
||||
10:
|
||||
startRow: 10
|
||||
top: 100
|
||||
15:
|
||||
startRow: 15
|
||||
top: 150
|
||||
|
||||
editor.setHeight(70)
|
||||
expect(presenter).toHaveValues
|
||||
content:
|
||||
tiles:
|
||||
0:
|
||||
startRow: 0
|
||||
top: 0
|
||||
5:
|
||||
startRow: 5
|
||||
top: 50
|
||||
gutter:
|
||||
tiles:
|
||||
0:
|
||||
startRow: 0
|
||||
top: 0
|
||||
5:
|
||||
startRow: 5
|
||||
top: 50
|
||||
|
||||
describe "when lineHeightInPixels changes", ->
|
||||
it "updates the rendered tiles to reflect the change", ->
|
||||
editor.setScrollTop(10)
|
||||
editor.setLineHeightInPixels(7)
|
||||
|
||||
expect(presenter).toHaveValues
|
||||
content:
|
||||
tiles:
|
||||
0:
|
||||
startRow: 0
|
||||
top: 0 - 10
|
||||
height: 5 * 7
|
||||
lineHeightInPixels: 7
|
||||
5:
|
||||
startRow: 5
|
||||
top: 7 * 5 - 10
|
||||
height: 5 * 7
|
||||
lineHeightInPixels: 7
|
||||
10:
|
||||
startRow: 10
|
||||
top: 7 * 10 - 10
|
||||
height: 5 * 7
|
||||
lineHeightInPixels: 7
|
||||
15:
|
||||
startRow: 15
|
||||
top: 7 * 15 - 10
|
||||
height: 5 * 7
|
||||
lineHeightInPixels: 7
|
||||
gutter:
|
||||
tiles:
|
||||
0:
|
||||
startRow: 0
|
||||
top: 0 - 10
|
||||
height: 5 * 7
|
||||
lineHeightInPixels: 7
|
||||
5:
|
||||
startRow: 5
|
||||
top: 7 * 5 - 10
|
||||
height: 5 * 7
|
||||
lineHeightInPixels: 7
|
||||
10:
|
||||
startRow: 10
|
||||
top: 7 * 10 - 10
|
||||
height: 5 * 7
|
||||
lineHeightInPixels: 7
|
||||
15:
|
||||
startRow: 15
|
||||
top: 7 * 15 - 10
|
||||
height: 5 * 7
|
||||
lineHeightInPixels: 7
|
||||
|
||||
describe "when scrollTop changes", ->
|
||||
it "updates the rendered tiles to reflect the change", ->
|
||||
editor.setScrollTop(20)
|
||||
expect(presenter).toHaveValues
|
||||
content:tiles:
|
||||
0:
|
||||
top: -20
|
||||
lines: editor.linesForScreenRows(0, 4)
|
||||
5:
|
||||
top: 30
|
||||
lines: editor.linesForScreenRows(5, 9)
|
||||
10:
|
||||
top: 80
|
||||
lines: editor.linesForScreenRows(10, 14)
|
||||
gutter:tiles:
|
||||
0:
|
||||
top: -20
|
||||
lineNumbers: editor.lineNumbersForScreenRows(0, 4)
|
||||
5:
|
||||
top: 30
|
||||
lineNumbers: editor.lineNumbersForScreenRows(5, 9)
|
||||
10:
|
||||
top: 80
|
||||
lineNumbers: editor.lineNumbersForScreenRows(10, 14)
|
||||
|
||||
editor.setScrollTop(70)
|
||||
expect(presenter.content.tiles).toHaveValues
|
||||
5:
|
||||
top: -20
|
||||
lines: editor.linesForScreenRows(5, 9)
|
||||
10:
|
||||
top: 30
|
||||
lines: editor.linesForScreenRows(10, 14)
|
||||
15:
|
||||
top: 80
|
||||
lines: editor.linesForScreenRows(15, 19)
|
||||
|
||||
describe "when scrollLeft changes", ->
|
||||
it "updates the rendered tiles to reflect the change", ->
|
||||
expect(presenter.content.tiles).toHaveValues
|
||||
0:
|
||||
left: 0
|
||||
5
|
||||
left: 0
|
||||
10:
|
||||
left: 0
|
||||
|
||||
editor.setScrollLeft(30)
|
||||
expect(presenter.content.tiles).toHaveValues
|
||||
0:
|
||||
left: -30
|
||||
5:
|
||||
left: -30
|
||||
10:
|
||||
left: -30
|
||||
|
||||
describe "lines", ->
|
||||
describe "initial state", ->
|
||||
it "breaks lines into tiles", ->
|
||||
expect(presenter.content.tiles).toHaveValues
|
||||
0:
|
||||
startRow: 0
|
||||
lines: editor.linesForScreenRows(0, 4)
|
||||
5:
|
||||
startRow: 5
|
||||
lines: editor.linesForScreenRows(5, 9)
|
||||
10:
|
||||
startRow: 10
|
||||
lines: editor.linesForScreenRows(10, 14)
|
||||
|
||||
describe "when the screen lines change", ->
|
||||
it "updates the lines in the tiles to reflect the change", ->
|
||||
buffer.setTextInRange([[3, 5], [7, 0]], "a\nb\nc\nd")
|
||||
expect(presenter.content.tiles).toHaveValues
|
||||
0:
|
||||
startRow: 0
|
||||
lines: editor.linesForScreenRows(0, 4)
|
||||
5:
|
||||
startRow: 5
|
||||
lines: editor.linesForScreenRows(5, 9)
|
||||
10
|
||||
startRow: 10
|
||||
lines: editor.linesForScreenRows(10, 14)
|
||||
|
||||
describe "line decorations", ->
|
||||
marker = null
|
||||
|
||||
beforeEach ->
|
||||
marker = editor.markBufferRange([[3, 4], [5, 6]], invalidate: 'touch')
|
||||
|
||||
describe "initial state", ->
|
||||
it "renders existing line decorations on the appropriate lines", ->
|
||||
decoration = editor.decorateMarker(marker, type: 'line', class: 'test')
|
||||
|
||||
presenter = new EditorPresenter(editor)
|
||||
|
||||
decorationsById = {}
|
||||
decorationsById[decoration.id] = decoration.getParams()
|
||||
expect(presenter.content.tiles).toHaveValues
|
||||
0:
|
||||
lineDecorations:
|
||||
3: decorationsById
|
||||
4: decorationsById
|
||||
5:
|
||||
lineDecorations:
|
||||
5: decorationsById
|
||||
|
||||
describe "when a line decoration is added, updated, invalidated, or removed", ->
|
||||
it "updates the presented line decorations accordingly", ->
|
||||
decoration = editor.decorateMarker(marker, type: 'line', class: 'test')
|
||||
|
||||
decorationsById = {}
|
||||
decorationsById[decoration.id] = decoration.getParams()
|
||||
expect(presenter.content.tiles).toHaveValues
|
||||
0:
|
||||
lineDecorations:
|
||||
3: decorationsById
|
||||
4: decorationsById
|
||||
5:
|
||||
lineDecorations:
|
||||
5: decorationsById
|
||||
|
||||
marker.setBufferRange([[8, 4], [10, 6]])
|
||||
expect(presenter.content.tiles).toHaveValues
|
||||
0:
|
||||
lineDecorations:
|
||||
3: null
|
||||
4: null
|
||||
5:
|
||||
lineDecorations:
|
||||
5: null
|
||||
8: decorationsById
|
||||
9: decorationsById
|
||||
10:
|
||||
lineDecorations:
|
||||
10: decorationsById
|
||||
|
||||
buffer.insert([8, 5], 'invalidate marker')
|
||||
expect(presenter.content.tiles).toHaveValues
|
||||
5:
|
||||
lineDecorations:
|
||||
8: null
|
||||
9: null
|
||||
10:
|
||||
lineDecorations:
|
||||
10: null
|
||||
|
||||
buffer.undo()
|
||||
expect(presenter.content.tiles).toHaveValues
|
||||
5:
|
||||
lineDecorations:
|
||||
8: decorationsById
|
||||
9: decorationsById
|
||||
10:
|
||||
lineDecorations:
|
||||
10: decorationsById
|
||||
|
||||
marker.destroy()
|
||||
expect(presenter.content.tiles).toHaveValues
|
||||
5:
|
||||
lineDecorations:
|
||||
8: null
|
||||
9: null
|
||||
10:
|
||||
lineDecorations:
|
||||
10: null
|
||||
|
||||
describe "line numbers", ->
|
||||
describe "when the screen lines change", ->
|
||||
it "updates the line numbers to reflect the change", ->
|
||||
editor.createFold(4, 7)
|
||||
expect(presenter.gutter.tiles).toHaveValues
|
||||
0:
|
||||
lineNumbers: editor.lineNumbersForScreenRows(0, 4)
|
||||
5:
|
||||
lineNumbers: editor.lineNumbersForScreenRows(5, 9)
|
||||
10:
|
||||
lineNumbers: editor.lineNumbersForScreenRows(10, 14)
|
||||
|
||||
it "updates the maxLineNumberDigits if necessary", ->
|
||||
buffer.setText('')
|
||||
expect(presenter.gutter.dummyTile.maxLineNumberDigits).toBe 1
|
||||
expect(presenter.gutter.tiles).toHaveValues
|
||||
0:
|
||||
maxLineNumberDigits: 1
|
||||
|
||||
buffer.setText([0..10].join('\n'))
|
||||
expect(presenter.gutter.dummyTile.maxLineNumberDigits).toBe 2
|
||||
expect(presenter.gutter.tiles).toHaveValues
|
||||
0:
|
||||
maxLineNumberDigits: 2
|
||||
5:
|
||||
maxLineNumberDigits: 2
|
||||
10:
|
||||
maxLineNumberDigits: 2
|
||||
|
||||
buffer.delete([[8, 0], [Infinity, 0]])
|
||||
expect(presenter.gutter.dummyTile.maxLineNumberDigits).toBe 1
|
||||
expect(presenter.gutter.tiles).toHaveValues
|
||||
0:
|
||||
maxLineNumberDigits: 1
|
||||
5:
|
||||
maxLineNumberDigits: 1
|
||||
|
||||
describe "line number decorations", ->
|
||||
marker = null
|
||||
|
||||
beforeEach ->
|
||||
marker = editor.markBufferRange([[3, 4], [5, 6]], invalidate: 'touch')
|
||||
|
||||
describe "initial state", ->
|
||||
it "renders existing line number decorations on the appropriate lines", ->
|
||||
decoration = editor.decorateMarker(marker, type: 'gutter', class: 'test')
|
||||
|
||||
presenter = new EditorPresenter(editor)
|
||||
|
||||
decorationsById = {}
|
||||
decorationsById[decoration.id] = decoration.getParams()
|
||||
expect(presenter.gutter.tiles).toHaveValues
|
||||
0:
|
||||
lineNumberDecorations:
|
||||
3: decorationsById
|
||||
4: decorationsById
|
||||
5:
|
||||
lineNumberDecorations:
|
||||
5: decorationsById
|
||||
|
||||
describe "when a line number decorations is added, updated, invalidated, or removed", ->
|
||||
it "updates the presented line decorations accordingly", ->
|
||||
decoration = editor.decorateMarker(marker, type: 'gutter', class: 'test')
|
||||
|
||||
decorationsById = {}
|
||||
decorationsById[decoration.id] = decoration.getParams()
|
||||
expect(presenter.gutter.tiles).toHaveValues
|
||||
0:
|
||||
lineNumberDecorations:
|
||||
3: decorationsById
|
||||
4: decorationsById
|
||||
5:
|
||||
lineNumberDecorations:
|
||||
5: decorationsById
|
||||
|
||||
marker.setBufferRange([[8, 4], [10, 6]])
|
||||
expect(presenter.gutter.tiles).toHaveValues
|
||||
0:
|
||||
lineNumberDecorations:
|
||||
3: null
|
||||
4: null
|
||||
5:
|
||||
lineNumberDecorations:
|
||||
5: null
|
||||
8: decorationsById
|
||||
9: decorationsById
|
||||
10:
|
||||
lineNumberDecorations:
|
||||
10: decorationsById
|
||||
|
||||
buffer.insert([8, 5], 'invalidate marker')
|
||||
expect(presenter.gutter.tiles).toHaveValues
|
||||
5:
|
||||
lineNumberDecorations:
|
||||
8: null
|
||||
9: null
|
||||
10:
|
||||
lineNumberDecorations:
|
||||
10: null
|
||||
|
||||
buffer.undo()
|
||||
expect(presenter.gutter.tiles).toHaveValues
|
||||
5:
|
||||
lineNumberDecorations:
|
||||
8: decorationsById
|
||||
9: decorationsById
|
||||
10:
|
||||
lineNumberDecorations:
|
||||
10: decorationsById
|
||||
|
||||
marker.destroy()
|
||||
expect(presenter.gutter.tiles).toHaveValues
|
||||
5:
|
||||
lineNumberDecorations:
|
||||
8: null
|
||||
9: null
|
||||
10:
|
||||
lineNumberDecorations:
|
||||
10: 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})
|
||||
|
||||
|
||||
this.message = => "Object did not have expected values: #{jasmine.pp(wrongValues)}"
|
||||
checkValues(@actual, expected)
|
||||
console.warn "Object did not have expected values:", wrongValues unless hasAllValues
|
||||
hasAllValues
|
||||
@@ -220,8 +220,8 @@ class DisplayBufferMarker
|
||||
@oldTailScreenPosition, newTailScreenPosition,
|
||||
@oldHeadBufferPosition, newHeadBufferPosition,
|
||||
@oldTailBufferPosition, newTailBufferPosition,
|
||||
textChanged,
|
||||
isValid
|
||||
@wasValid, isValid,
|
||||
textChanged
|
||||
}
|
||||
|
||||
@oldHeadBufferPosition = newHeadBufferPosition
|
||||
|
||||
@@ -401,13 +401,45 @@ class DisplayBuffer extends Model
|
||||
# buffer rows corresponding to every screen row in the range
|
||||
#
|
||||
# startScreenRow - The screen row {Number} to start at
|
||||
# endScreenRow - The screen row {Number} to end at (default: the last screen row)
|
||||
# endScreenRow - The screen row {Number} to end at, inclusive.
|
||||
#
|
||||
# Returns an {Array} of buffer rows as {Numbers}s.
|
||||
bufferRowsForScreenRows: (startScreenRow, endScreenRow) ->
|
||||
for screenRow in [startScreenRow..endScreenRow]
|
||||
@rowMap.bufferRowRangeForScreenRow(screenRow)[0]
|
||||
|
||||
# Given starting and ending screen rows, this returns an array of the line
|
||||
# number strings corresponding to every screen row in the range.
|
||||
#
|
||||
# Line numbers start at 1 as opposed to row numbers which start at 0.
|
||||
# Soft wrapped lines are indicated by dot-separated strings. For example, if
|
||||
# line 9 wraps twice, it will appear as '9', '9.1', '9.2'.
|
||||
#
|
||||
# startScreenRow - The screen row {Number} to start at
|
||||
# endScreenRow - The screen row {Number} to end at, inclusive.
|
||||
#
|
||||
# Returns an {Array} of line numbers as {String}s.
|
||||
lineNumbersForScreenRows: (startScreenRow, endScreenRow) ->
|
||||
bufferRows = @bufferRowsForScreenRows(startScreenRow, endScreenRow)
|
||||
|
||||
# Pad the leading
|
||||
leadingSoftWraps = 0
|
||||
while @bufferRowForScreenRow(startScreenRow - leadingSoftWraps - 1) is bufferRows[0]
|
||||
bufferRows.unshift(bufferRows[0])
|
||||
leadingSoftWraps++
|
||||
|
||||
lineNumbers = []
|
||||
for bufferRow in bufferRows
|
||||
lineNumber = (bufferRow + 1).toString()
|
||||
if bufferRow is lastBufferRow
|
||||
lineNumber += ".#{++softWraps}"
|
||||
else
|
||||
lastBufferRow = bufferRow
|
||||
softWraps = 0
|
||||
lineNumbers.push(lineNumber)
|
||||
|
||||
lineNumbers[leadingSoftWraps..]
|
||||
|
||||
# Creates a new fold between two row numbers.
|
||||
#
|
||||
# startRow - The row {Number} to start folding at
|
||||
|
||||
@@ -10,6 +10,7 @@ LinesComponent = require './lines-component'
|
||||
ScrollbarComponent = require './scrollbar-component'
|
||||
ScrollbarCornerComponent = require './scrollbar-corner-component'
|
||||
SubscriberMixin = require './subscriber-mixin'
|
||||
EditorPresenter = require './editor-presenter'
|
||||
|
||||
module.exports =
|
||||
EditorComponent = React.createClass
|
||||
@@ -50,6 +51,8 @@ EditorComponent = React.createClass
|
||||
render: ->
|
||||
{focused, showIndentGuide, showInvisibles, showLineNumbers, visible} = @state
|
||||
{editor, mini, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
|
||||
contentPresenter = @presenter.content
|
||||
gutterPresenter = @presenter.gutter
|
||||
maxLineNumberDigits = editor.getLineCount().toString().length
|
||||
invisibles = if showInvisibles and not mini then @state.invisibles else {}
|
||||
hasSelection = editor.getSelection()? and !editor.getSelection().isEmpty()
|
||||
@@ -91,7 +94,8 @@ EditorComponent = React.createClass
|
||||
div {className, style, tabIndex: -1},
|
||||
if @shouldRenderGutter()
|
||||
GutterComponent {
|
||||
ref: 'gutter', onMouseDown: @onGutterMouseDown, lineDecorations,
|
||||
ref: 'gutter', gutterPresenter
|
||||
onMouseDown: @onGutterMouseDown, lineDecorations,
|
||||
defaultCharWidth, editor, renderedRowRange, maxLineNumberDigits, scrollViewHeight,
|
||||
scrollTop, scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow,
|
||||
@useHardwareAcceleration, @performedInitialMeasurement, @backgroundColor, @gutterBackgroundColor
|
||||
@@ -106,7 +110,7 @@ EditorComponent = React.createClass
|
||||
onBlur: @onInputBlurred
|
||||
|
||||
LinesComponent {
|
||||
ref: 'lines',
|
||||
ref: 'lines', contentPresenter,
|
||||
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'))
|
||||
|
||||
@presenter = new EditorPresenter(@props.editor)
|
||||
@subscribe @presenter, 'did-change', @requestUpdate
|
||||
|
||||
componentDidMount: ->
|
||||
{editor} = @props
|
||||
|
||||
@@ -214,19 +221,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
|
||||
@@ -294,7 +303,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) ->
|
||||
@@ -532,6 +543,7 @@ EditorComponent = React.createClass
|
||||
event.preventDefault() unless event.data is ' '
|
||||
|
||||
return unless @isInputEnabled()
|
||||
event.reactSkipEventDispatch = true
|
||||
|
||||
{editor} = @props
|
||||
inputNode = event.target
|
||||
@@ -978,8 +990,8 @@ EditorComponent = React.createClass
|
||||
{clientX, clientY} = event
|
||||
|
||||
linesClientRect = @refs.lines.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: ->
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
{Emitter, Subscriber} = require 'emissary'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
module.exports =
|
||||
class EditorPresenter
|
||||
Emitter.includeInto(this)
|
||||
Subscriber.includeInto(this)
|
||||
|
||||
constructor: (@editor) ->
|
||||
@content = {tiles: {}}
|
||||
@gutter = {tiles: {}, dummyTile: {dummy: true}}
|
||||
@updateTiles()
|
||||
@updateDummyGutterTile()
|
||||
@scrollTop = @editor.getScrollTop()
|
||||
@scrollLeft = @editor.getScrollLeft()
|
||||
|
||||
@subscribe @editor.$width, @onWidthChanged
|
||||
@subscribe @editor.$height, @onHeightChanged
|
||||
@subscribe @editor.$lineHeightInPixels, @onLineHeightInPixelsChanged
|
||||
@subscribe @editor.$scrollTop, @onScrollTopChanged
|
||||
@subscribe @editor.$scrollLeft, @onScrollLeftChanged
|
||||
@subscribe @editor, 'screen-lines-changed', @onScreenLinesChanged
|
||||
@subscribe @editor, 'decoration-added', @onDecorationAdded
|
||||
@subscribe @editor, 'decoration-removed', @onDecorationRemoved
|
||||
@subscribe @editor, 'decoration-changed', @onDecorationChanged
|
||||
|
||||
getContentTileSize: -> 5
|
||||
|
||||
getGutterTileSize: -> 20
|
||||
|
||||
getVisibleRowRange: ->
|
||||
heightInLines = Math.ceil(@editor.getHeight() / @editor.getLineHeightInPixels())
|
||||
startRow = Math.floor(@editor.getScrollTop() / @editor.getLineHeightInPixels())
|
||||
endRow = Math.min(@editor.getLineCount(), startRow + heightInLines)
|
||||
[startRow, endRow]
|
||||
|
||||
contentTileStartRowForRow: (startRow) ->
|
||||
startRow - (startRow % @getContentTileSize())
|
||||
|
||||
gutterTileStartRowForRow: (startRow) ->
|
||||
startRow - (startRow % @getGutterTileSize())
|
||||
|
||||
getContentTileRowRange: ->
|
||||
[startRow, endRow] = @getVisibleRowRange()
|
||||
startRow = @contentTileStartRowForRow(startRow)
|
||||
endRow = @contentTileStartRowForRow(endRow) + @getContentTileSize()
|
||||
[startRow, endRow]
|
||||
|
||||
getGutterTileRowRange: ->
|
||||
[startRow, endRow] = @getVisibleRowRange()
|
||||
startRow = @gutterTileStartRowForRow(startRow)
|
||||
endRow = @gutterTileStartRowForRow(endRow) + @getGutterTileSize()
|
||||
[startRow, endRow]
|
||||
|
||||
updateTiles: (fn) ->
|
||||
@updateContentTiles(fn)
|
||||
@updateGutterTiles(fn)
|
||||
@emit 'did-change'
|
||||
|
||||
updateContentTiles: (fn) ->
|
||||
[startRow, endRow] = @getContentTileRowRange()
|
||||
|
||||
for tileStartRow of @content.tiles
|
||||
delete @content.tiles[tileStartRow] unless startRow <= tileStartRow < endRow
|
||||
|
||||
for tileStartRow in [startRow...endRow] by @getContentTileSize()
|
||||
if existingTile = @content.tiles[tileStartRow]
|
||||
fn?(existingTile)
|
||||
else
|
||||
tileEndRow = tileStartRow + @getContentTileSize()
|
||||
@content.tiles[tileStartRow] = new ContentTilePresenter(@editor, tileStartRow, tileEndRow)
|
||||
|
||||
updateGutterTiles: (fn) ->
|
||||
[startRow, endRow] = @getGutterTileRowRange()
|
||||
|
||||
for tileStartRow of @gutter.tiles
|
||||
delete @gutter.tiles[tileStartRow] unless startRow <= tileStartRow < endRow
|
||||
|
||||
for tileStartRow in [startRow...endRow] by @getGutterTileSize()
|
||||
if existingTile = @gutter.tiles[tileStartRow]
|
||||
fn?(existingTile)
|
||||
else
|
||||
tileEndRow = tileStartRow + @getGutterTileSize()
|
||||
@gutter.tiles[tileStartRow] = new GutterTilePresenter(@editor, tileStartRow, tileEndRow)
|
||||
|
||||
updateDummyGutterTile: ->
|
||||
@gutter.dummyTile.maxLineNumberDigits = @editor.getLineCount().toString().length
|
||||
|
||||
onWidthChanged: =>
|
||||
@updateTiles (tile) -> tile.updateWidth()
|
||||
|
||||
onHeightChanged: =>
|
||||
@updateTiles()
|
||||
|
||||
onLineHeightInPixelsChanged: =>
|
||||
@updateTiles (tile) -> tile.updateLineHeightInPixels()
|
||||
|
||||
onScrollTopChanged: (@scrollTop) =>
|
||||
@updateTiles (tile) -> tile.updateScrollTop()
|
||||
|
||||
onScrollLeftChanged: (@scrollLeft) =>
|
||||
@updateTiles (tile) -> tile.updateScrollLeft()
|
||||
|
||||
onScreenLinesChanged: (change) =>
|
||||
@updateDummyGutterTile() if change.bufferDelta isnt 0
|
||||
@updateTiles (tile) -> tile.onScreenLinesChanged(change)
|
||||
|
||||
onDecorationAdded: (marker, decoration) =>
|
||||
@updateTiles (tile) -> tile.onDecorationAdded(decoration)
|
||||
|
||||
onDecorationRemoved: (marker, decoration) =>
|
||||
@updateTiles (tile) -> tile.onDecorationRemoved(decoration)
|
||||
|
||||
onDecorationChanged: (marker, decoration, change) =>
|
||||
@updateTiles (tile) -> tile.onDecorationChanged(decoration, change)
|
||||
|
||||
class ContentTilePresenter
|
||||
constructor: (@editor, @startRow, @endRow) ->
|
||||
@lineDecorations = {}
|
||||
@updateWidth()
|
||||
@updateLineHeightInPixels()
|
||||
@updateScrollTop()
|
||||
@updateScrollLeft()
|
||||
@updateLines()
|
||||
@populateDecorations()
|
||||
|
||||
updateWidth: ->
|
||||
@width = @editor.getWidth()
|
||||
|
||||
updateHeight: ->
|
||||
@height = (@endRow - @startRow) * @editor.getLineHeightInPixels()
|
||||
|
||||
updateLineHeightInPixels: ->
|
||||
@lineHeightInPixels = @editor.getLineHeightInPixels()
|
||||
@updateTop()
|
||||
@updateHeight()
|
||||
|
||||
updateScrollTop: ->
|
||||
@updateTop()
|
||||
|
||||
updateScrollLeft: ->
|
||||
@left = 0 - @editor.getScrollLeft()
|
||||
|
||||
updateTop: ->
|
||||
@top = @startRow * @editor.getLineHeightInPixels() - @editor.getScrollTop()
|
||||
|
||||
updateLines: ->
|
||||
@lines = @editor.linesForScreenRows(@startRow, @endRow - 1)
|
||||
|
||||
populateDecorations: ->
|
||||
for markerId, decorations of @editor.decorationsForScreenRowRange(@startRow, @endRow)
|
||||
for decoration in decorations
|
||||
@onDecorationAdded(decoration)
|
||||
|
||||
onScreenLinesChanged: (change) ->
|
||||
@updateLines() if change.start < @endRow
|
||||
|
||||
onDecorationAdded: (decoration) ->
|
||||
if decoration.isType('line')
|
||||
@onLineDecorationAdded(decoration)
|
||||
|
||||
onDecorationRemoved: (decoration) ->
|
||||
if decoration.isType('line')
|
||||
@onLineDecorationRemoved(decoration)
|
||||
|
||||
onDecorationChanged: (decoration, change) ->
|
||||
if decoration.isType('line')
|
||||
@onLineDecorationChanged(decoration, change)
|
||||
|
||||
onLineDecorationAdded: (decoration) ->
|
||||
marker = decoration.getMarker()
|
||||
headRow = marker.getHeadScreenPosition().row
|
||||
tailRow = marker.getTailScreenPosition().row
|
||||
valid = marker.isValid()
|
||||
params = decoration.getParams()
|
||||
|
||||
if rowRange = @rowRangeForLineDecoration(params, headRow, tailRow, valid)
|
||||
@addLineDecorations(params, rowRange...)
|
||||
|
||||
onLineDecorationRemoved: (decoration) ->
|
||||
marker = decoration.getMarker()
|
||||
headRow = marker.getHeadScreenPosition().row
|
||||
tailRow = marker.getTailScreenPosition().row
|
||||
valid = true # FIXME: Markers shouldn't always be invalidated when destroyed
|
||||
params = decoration.getParams()
|
||||
|
||||
if rowRange = @rowRangeForLineDecoration(params, headRow, tailRow, valid)
|
||||
@removeLineDecorations(params, rowRange...)
|
||||
|
||||
onLineDecorationChanged: (decoration, change) ->
|
||||
params = decoration.getParams()
|
||||
|
||||
{oldHeadScreenPosition, oldTailScreenPosition, wasValid} = change
|
||||
if rowRange = @rowRangeForLineDecoration(params, oldHeadScreenPosition.row, oldTailScreenPosition.row, wasValid)
|
||||
@removeLineDecorations(params, rowRange...)
|
||||
|
||||
{newHeadScreenPosition, newTailScreenPosition, isValid} = change
|
||||
if rowRange = @rowRangeForLineDecoration(params, newHeadScreenPosition.row, newTailScreenPosition.row, isValid)
|
||||
@addLineDecorations(params, rowRange...)
|
||||
|
||||
addLineDecorations: (params, decorationStartRow, decorationEndRow) ->
|
||||
unless decorationEndRow < @startRow or @endRow <= decorationStartRow
|
||||
for row in [decorationStartRow..decorationEndRow]
|
||||
@lineDecorations[row] ?= {}
|
||||
@lineDecorations[row][params.id] = params
|
||||
|
||||
removeLineDecorations: (params, decorationStartRow, decorationEndRow) ->
|
||||
unless decorationEndRow < @startRow or @endRow <= decorationStartRow
|
||||
for row in [decorationStartRow..decorationEndRow]
|
||||
delete @lineDecorations[row][params.id]
|
||||
delete @lineDecorations[row] if _.size(@lineDecorations[row]) is 0
|
||||
|
||||
rowRangeForLineDecoration: (decoration, headRow, tailRow, valid) ->
|
||||
return unless valid
|
||||
|
||||
startRow = Math.min(headRow, tailRow)
|
||||
endRow = Math.max(headRow, tailRow)
|
||||
[startRow, endRow]
|
||||
|
||||
class GutterTilePresenter
|
||||
constructor: (@editor, @startRow, @endRow) ->
|
||||
@lineNumberDecorations = {}
|
||||
|
||||
@populateDecorations()
|
||||
@updateLineHeightInPixels()
|
||||
@updateScrollTop()
|
||||
|
||||
populateDecorations: ->
|
||||
for markerId, decorations of @editor.decorationsForScreenRowRange(@startRow, @endRow)
|
||||
for decoration in decorations
|
||||
@onDecorationAdded(decoration)
|
||||
|
||||
updateLineHeightInPixels: ->
|
||||
@lineHeightInPixels = @editor.getLineHeightInPixels()
|
||||
@updateTop()
|
||||
@updateHeight()
|
||||
@updateLineNumbers()
|
||||
|
||||
updateScrollLeft: -> # NO-OP
|
||||
|
||||
updateScrollTop: ->
|
||||
@updateTop()
|
||||
|
||||
updateTop: ->
|
||||
@top = @startRow * @editor.getLineHeightInPixels() - @editor.getScrollTop()
|
||||
|
||||
updateWidth: -> # NO-OP
|
||||
|
||||
updateHeight: ->
|
||||
@height = (@endRow - @startRow) * @editor.getLineHeightInPixels()
|
||||
|
||||
updateLineNumbers: ->
|
||||
@lineNumbers = @editor.lineNumbersForScreenRows(@startRow, @endRow - 1)
|
||||
@maxLineNumberDigits = @editor.getLineCount().toString().length
|
||||
|
||||
updateMaxLineNumberDigits: ->
|
||||
@maxLineNumberDigits = @editor.getLineCount().toString().length
|
||||
|
||||
onScreenLinesChanged: (change) ->
|
||||
if change.bufferDelta isnt 0 or change.screenDelta isnt 0
|
||||
@updateMaxLineNumberDigits()
|
||||
@updateLineNumbers() if change.start < @endRow
|
||||
|
||||
onDecorationAdded: (decoration) ->
|
||||
return unless decoration.isType('gutter')
|
||||
|
||||
marker = decoration.getMarker()
|
||||
headRow = marker.getHeadScreenPosition().row
|
||||
tailRow = marker.getTailScreenPosition().row
|
||||
valid = marker.isValid()
|
||||
params = decoration.getParams()
|
||||
|
||||
if rowRange = @rowRangeForLineNumberDecoration(params, headRow, tailRow, valid)
|
||||
@addLineNumberDecorations(params, rowRange...)
|
||||
|
||||
onDecorationRemoved: (decoration) ->
|
||||
return unless decoration.isType('gutter')
|
||||
|
||||
marker = decoration.getMarker()
|
||||
headRow = marker.getHeadScreenPosition().row
|
||||
tailRow = marker.getTailScreenPosition().row
|
||||
valid = true # FIXME: Markers shouldn't always be invalidated when destroyed
|
||||
params = decoration.getParams()
|
||||
|
||||
if rowRange = @rowRangeForLineNumberDecoration(params, headRow, tailRow, valid)
|
||||
@removeLineNumberDecorations(params, rowRange...)
|
||||
|
||||
onDecorationChanged: (decoration, change) ->
|
||||
return unless decoration.isType('gutter')
|
||||
|
||||
params = decoration.getParams()
|
||||
|
||||
{oldHeadScreenPosition, oldTailScreenPosition, wasValid} = change
|
||||
if rowRange = @rowRangeForLineNumberDecoration(params, oldHeadScreenPosition.row, oldTailScreenPosition.row, wasValid)
|
||||
@removeLineNumberDecorations(params, rowRange...)
|
||||
|
||||
{newHeadScreenPosition, newTailScreenPosition, isValid} = change
|
||||
if rowRange = @rowRangeForLineNumberDecoration(params, newHeadScreenPosition.row, newTailScreenPosition.row, isValid)
|
||||
@addLineNumberDecorations(params, rowRange...)
|
||||
|
||||
addLineNumberDecorations: (params, decorationStartRow, decorationEndRow) ->
|
||||
unless decorationEndRow < @startRow or @endRow <= decorationStartRow
|
||||
for row in [decorationStartRow..decorationEndRow]
|
||||
@lineNumberDecorations[row] ?= {}
|
||||
@lineNumberDecorations[row][params.id] = params
|
||||
|
||||
removeLineNumberDecorations: (params, decorationStartRow, decorationEndRow) ->
|
||||
unless decorationEndRow < @startRow or @endRow <= decorationStartRow
|
||||
for row in [decorationStartRow..decorationEndRow]
|
||||
delete @lineNumberDecorations[row][params.id]
|
||||
delete @lineNumberDecorations[row] if _.size(@lineNumberDecorations[row]) is 0
|
||||
|
||||
rowRangeForLineNumberDecoration: (decoration, headRow, tailRow, valid) ->
|
||||
return unless valid
|
||||
|
||||
startRow = Math.min(headRow, tailRow)
|
||||
endRow = Math.max(headRow, tailRow)
|
||||
[startRow, endRow]
|
||||
@@ -0,0 +1,328 @@
|
||||
{extend, toArray, isEqual, clone} = require 'underscore-plus'
|
||||
Decoration = require './decoration'
|
||||
|
||||
WrapperDiv = document.createElement('div')
|
||||
|
||||
module.exports =
|
||||
class EditorTileComponent
|
||||
top: null
|
||||
left: null
|
||||
height: null
|
||||
width: null
|
||||
lineHeightInPixels: null
|
||||
lineWidths: null
|
||||
|
||||
constructor: (@presenter) ->
|
||||
@lineNodesByLineId = {}
|
||||
@screenRowsByLineId = {}
|
||||
@lineIdsByScreenRow = {}
|
||||
@lineDecorationsByLineId = {}
|
||||
@cursorPixelRectsById = {}
|
||||
@cursorNodesById = {}
|
||||
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.style.position = 'absolute'
|
||||
@domNode.style.overflow = 'hidden'
|
||||
|
||||
@buildLines()
|
||||
@update()
|
||||
|
||||
stateChangedForKeys: ->
|
||||
for key in arguments
|
||||
return true if @prevState?.get(key) isnt @state.get(key)
|
||||
false
|
||||
|
||||
update: ->
|
||||
@updateTransform()
|
||||
@updateWidth()
|
||||
@updateHeight()
|
||||
@updateLines()
|
||||
|
||||
# @clearScreenRowCaches() if newProps.lineHeightInPixels isnt @props.lineHeightInPixels
|
||||
# @updateCursors()
|
||||
|
||||
updateTransform: ->
|
||||
{left, top} = @presenter
|
||||
unless left is @left and top is @top
|
||||
@domNode.style['-webkit-transform'] = "translate3d(#{left}px, #{top}px, 0px)"
|
||||
@left = left
|
||||
@top = top
|
||||
|
||||
updateWidth: ->
|
||||
{width} = @presenter
|
||||
unless width is @width
|
||||
@domNode.style.width = width + 'px'
|
||||
@width = width
|
||||
|
||||
updateHeight: ->
|
||||
{height} = @presenter
|
||||
unless height is @height
|
||||
@domNode.style.height = height + 'px'
|
||||
@height = height
|
||||
|
||||
buildLines: ->
|
||||
{startRow, lines, lineDecorations} = @presenter
|
||||
|
||||
linesHTML = ""
|
||||
for line, i in lines
|
||||
screenRow = startRow + i
|
||||
linesHTML += @buildLineHTML(line, screenRow)
|
||||
@domNode.innerHTML = linesHTML
|
||||
|
||||
for line, i in lines
|
||||
screenRow = startRow + i
|
||||
lineNode = @domNode.children[i]
|
||||
@lineNodesByLineId[line.id] = lineNode
|
||||
@screenRowsByLineId[line.id] = screenRow
|
||||
@lineDecorationsByLineId[line.id] = clone(lineDecorations[screenRow])
|
||||
@lineIdsByScreenRow[screenRow] = line.id
|
||||
|
||||
updateLines: ->
|
||||
{lines, width} = @presenter
|
||||
@removeLineNodes()
|
||||
@appendOrUpdateVisibleLineNodes()
|
||||
@lineWidths = width
|
||||
|
||||
removeLineNodes: () ->
|
||||
lineIds = new Set
|
||||
lineIds.add(line.id.toString()) for line in @presenter.lines
|
||||
|
||||
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 @lineDecorationsByLineId[lineId]
|
||||
@domNode.removeChild(lineNode)
|
||||
|
||||
appendOrUpdateVisibleLineNodes: ->
|
||||
{startRow, lines, lineHeightInPixels, width} = @presenter
|
||||
|
||||
newLines = null
|
||||
newLinesHTML = null
|
||||
|
||||
for line, index in lines
|
||||
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
|
||||
|
||||
@lineHeightInPixels = lineHeightInPixels
|
||||
@lineWidths = width
|
||||
|
||||
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, lineHeightInPixels, width} = @presenter
|
||||
|
||||
lineNode = @lineNodesByLineId[line.id]
|
||||
|
||||
unless width is @lineWidths
|
||||
lineNode.style.width = width + 'px'
|
||||
|
||||
unless @screenRowsByLineId[line.id] is screenRow and lineHeightInPixels is @lineHeightInPixels
|
||||
lineNode.style.top = (screenRow - startRow) * lineHeightInPixels + 'px'
|
||||
|
||||
unless @screenRowsByLineId[line.id] is screenRow
|
||||
lineNode.dataset.screenRow = screenRow
|
||||
@screenRowsByLineId[line.id] = screenRow
|
||||
@lineIdsByScreenRow[screenRow] = line.id
|
||||
|
||||
@updateLineDecorations(lineNode, line, screenRow)
|
||||
|
||||
updateLineDecorations: (lineNode, line, screenRow) ->
|
||||
desiredDecorations = @presenter.lineDecorations[screenRow]
|
||||
|
||||
if currentDecorations = @lineDecorationsByLineId[line.id]
|
||||
for id, decoration of currentDecorations
|
||||
unless desiredDecorations?[id]?
|
||||
lineNode.classList.remove(decoration.class)
|
||||
delete currentDecorations[id]
|
||||
|
||||
if desiredDecorations?
|
||||
currentDecorations = (@lineDecorationsByLineId[line.id] ?= {})
|
||||
for id, decoration of desiredDecorations
|
||||
unless currentDecorations[id]?
|
||||
lineNode.classList.add(decoration.class)
|
||||
currentDecorations[id] = decoration
|
||||
|
||||
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, lineHeightInPixels, width} = @presenter
|
||||
{text, fold, isSoftWrapped, indentLevel} = line
|
||||
|
||||
classes = @getLineClasses(screenRow)
|
||||
top = (screenRow - startRow) * lineHeightInPixels
|
||||
style = "position: absolute; top: #{top}px; width: #{width}px;"
|
||||
|
||||
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 lineDecorationsForScreenRow = @presenter.lineDecorations[screenRow]
|
||||
for id, decoration of lineDecorationsForScreenRow
|
||||
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]
|
||||
@@ -591,6 +591,9 @@ class Editor extends Model
|
||||
# {Delegates to: DisplayBuffer.bufferRowsForScreenRows}
|
||||
bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow)
|
||||
|
||||
# {Delegats to: DisplayBuffer.lineNumbersForScreenRows}
|
||||
lineNumbersForScreenRows: (startRow, endRow) -> @displayBuffer.lineNumbersForScreenRows(startRow, endRow)
|
||||
|
||||
bufferRowForScreenRow: (row) -> @displayBuffer.bufferRowForScreenRow(row)
|
||||
|
||||
# Public: Get the syntactic scopes for the given position in buffer
|
||||
|
||||
+25
-174
@@ -4,6 +4,7 @@ React = require 'react-atom-fork'
|
||||
{isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus'
|
||||
Decoration = require './decoration'
|
||||
SubscriberMixin = require './subscriber-mixin'
|
||||
GutterTileComponent = require './gutter-tile-component'
|
||||
|
||||
WrapperDiv = document.createElement('div')
|
||||
|
||||
@@ -16,196 +17,46 @@ GutterComponent = React.createClass
|
||||
measuredWidth: null
|
||||
|
||||
render: ->
|
||||
{scrollHeight, scrollViewHeight, onMouseDown, backgroundColor, gutterBackgroundColor} = @props
|
||||
|
||||
if gutterBackgroundColor isnt 'rbga(0, 0, 0, 0)'
|
||||
backgroundColor = gutterBackgroundColor
|
||||
|
||||
div className: 'gutter', onClick: @onClick, onMouseDown: onMouseDown,
|
||||
div className: 'line-numbers', ref: 'lineNumbers', style:
|
||||
height: Math.max(scrollHeight, scrollViewHeight)
|
||||
WebkitTransform: @getTransform()
|
||||
backgroundColor: backgroundColor
|
||||
|
||||
getTransform: ->
|
||||
{scrollTop, useHardwareAcceleration} = @props
|
||||
|
||||
if useHardwareAcceleration
|
||||
"translate3d(0px, #{-scrollTop}px, 0px)"
|
||||
else
|
||||
"translate(0px, #{-scrollTop}px)"
|
||||
div className: 'gutter', onClick: @onClick, onMouseDown: @props.onMouseDown
|
||||
|
||||
componentWillMount: ->
|
||||
@lineNumberNodesById = {}
|
||||
@lineNumberIdsByScreenRow = {}
|
||||
@screenRowsByLineNumberId = {}
|
||||
@renderedDecorationsByLineNumberId = {}
|
||||
@tileComponentsByStartRow = {}
|
||||
|
||||
componentDidMount: ->
|
||||
@appendDummyLineNumber()
|
||||
@updateLineNumbers() if @props.performedInitialMeasurement
|
||||
|
||||
# Only update the gutter if the visible row range has changed or if a
|
||||
# non-zero-delta change to the screen lines has occurred within the current
|
||||
# visible row range.
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
return true unless isEqualForProperties(newProps, @props,
|
||||
'renderedRowRange', 'scrollTop', 'lineHeightInPixels', 'mouseWheelScreenRow', 'lineDecorations',
|
||||
'scrollViewHeight', 'useHardwareAcceleration', 'backgroundColor', 'gutterBackgroundColor'
|
||||
)
|
||||
|
||||
{renderedRowRange, pendingChanges, lineDecorations} = newProps
|
||||
return false unless renderedRowRange?
|
||||
|
||||
for change in pendingChanges when Math.abs(change.screenDelta) > 0 or Math.abs(change.bufferDelta) > 0
|
||||
return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start
|
||||
|
||||
false
|
||||
@appendDummyTile()
|
||||
|
||||
componentDidUpdate: (oldProps) ->
|
||||
return unless @props.performedInitialMeasurement
|
||||
if @props.performedInitialMeasurement
|
||||
@updateTiles()
|
||||
|
||||
unless isEqualForProperties(oldProps, @props, 'maxLineNumberDigits')
|
||||
@updateDummyLineNumber()
|
||||
@removeLineNumberNodes()
|
||||
updateTiles: ->
|
||||
{gutterPresenter} = @props
|
||||
|
||||
@clearScreenRowCaches() unless oldProps.lineHeightInPixels is @props.lineHeightInPixels
|
||||
@updateLineNumbers()
|
||||
domNode = @getDOMNode()
|
||||
|
||||
clearScreenRowCaches: ->
|
||||
@lineNumberIdsByScreenRow = {}
|
||||
@screenRowsByLineNumberId = {}
|
||||
for tileStartRow, tileComponent of @tileComponentsByStartRow
|
||||
unless gutterPresenter.tiles[tileStartRow]?
|
||||
domNode.removeChild(tileComponent.domNode)
|
||||
delete @tileComponentsByStartRow[tileStartRow]
|
||||
|
||||
for tileStartRow, tilePresenter of gutterPresenter.tiles
|
||||
if tileComponent = @tileComponentsByStartRow[tileStartRow]
|
||||
tileComponent = @tileComponentsByStartRow[tileStartRow]
|
||||
tileComponent.update()
|
||||
else
|
||||
tileComponent = new GutterTileComponent(tilePresenter)
|
||||
@tileComponentsByStartRow[tileStartRow] = tileComponent
|
||||
domNode.appendChild(tileComponent.domNode)
|
||||
|
||||
# This dummy line number element holds the gutter to the appropriate width,
|
||||
# since the real line numbers are absolutely positioned for performance reasons.
|
||||
appendDummyLineNumber: ->
|
||||
{maxLineNumberDigits} = @props
|
||||
WrapperDiv.innerHTML = @buildLineNumberHTML(-1, false, maxLineNumberDigits)
|
||||
@dummyLineNumberNode = WrapperDiv.children[0]
|
||||
@refs.lineNumbers.getDOMNode().appendChild(@dummyLineNumberNode)
|
||||
appendDummyTile: ->
|
||||
@dummyTileComponent = new GutterTileComponent(@props.gutterPresenter.dummyTile)
|
||||
@getDOMNode().appendChild(@dummyTileComponent.domNode)
|
||||
|
||||
updateDummyLineNumber: ->
|
||||
@dummyLineNumberNode.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits)
|
||||
|
||||
updateLineNumbers: ->
|
||||
lineNumberIdsToPreserve = @appendOrUpdateVisibleLineNumberNodes()
|
||||
@removeLineNumberNodes(lineNumberIdsToPreserve)
|
||||
|
||||
appendOrUpdateVisibleLineNumberNodes: ->
|
||||
{editor, renderedRowRange, scrollTop, maxLineNumberDigits, lineDecorations} = @props
|
||||
[startRow, endRow] = renderedRowRange
|
||||
|
||||
newLineNumberIds = null
|
||||
newLineNumbersHTML = null
|
||||
visibleLineNumberIds = new Set
|
||||
|
||||
wrapCount = 0
|
||||
for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1)
|
||||
screenRow = startRow + index
|
||||
|
||||
if bufferRow is lastBufferRow
|
||||
id = "#{bufferRow}-#{wrapCount++}"
|
||||
else
|
||||
id = bufferRow.toString()
|
||||
lastBufferRow = bufferRow
|
||||
wrapCount = 0
|
||||
|
||||
visibleLineNumberIds.add(id)
|
||||
|
||||
if @hasLineNumberNode(id)
|
||||
@updateLineNumberNode(id, bufferRow, screenRow, wrapCount > 0)
|
||||
else
|
||||
newLineNumberIds ?= []
|
||||
newLineNumbersHTML ?= ""
|
||||
newLineNumberIds.push(id)
|
||||
newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow)
|
||||
@screenRowsByLineNumberId[id] = screenRow
|
||||
@lineNumberIdsByScreenRow[screenRow] = id
|
||||
|
||||
@renderedDecorationsByLineNumberId[id] = lineDecorations[screenRow]
|
||||
|
||||
if newLineNumberIds?
|
||||
WrapperDiv.innerHTML = newLineNumbersHTML
|
||||
newLineNumberNodes = toArray(WrapperDiv.children)
|
||||
|
||||
node = @refs.lineNumbers.getDOMNode()
|
||||
for lineNumberId, i in newLineNumberIds
|
||||
lineNumberNode = newLineNumberNodes[i]
|
||||
@lineNumberNodesById[lineNumberId] = lineNumberNode
|
||||
node.appendChild(lineNumberNode)
|
||||
|
||||
visibleLineNumberIds
|
||||
|
||||
removeLineNumberNodes: (lineNumberIdsToPreserve) ->
|
||||
{mouseWheelScreenRow} = @props
|
||||
node = @refs.lineNumbers.getDOMNode()
|
||||
for lineNumberId, lineNumberNode of @lineNumberNodesById when not lineNumberIdsToPreserve?.has(lineNumberId)
|
||||
screenRow = @screenRowsByLineNumberId[lineNumberId]
|
||||
if not screenRow? or screenRow isnt mouseWheelScreenRow
|
||||
delete @lineNumberNodesById[lineNumberId]
|
||||
delete @lineNumberIdsByScreenRow[screenRow] if @lineNumberIdsByScreenRow[screenRow] is lineNumberId
|
||||
delete @screenRowsByLineNumberId[lineNumberId]
|
||||
delete @renderedDecorationsByLineNumberId[lineNumberId]
|
||||
node.removeChild(lineNumberNode)
|
||||
|
||||
buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow) ->
|
||||
{editor, lineHeightInPixels, lineDecorations} = @props
|
||||
if screenRow?
|
||||
style = "position: absolute; top: #{screenRow * lineHeightInPixels}px;"
|
||||
else
|
||||
style = "visibility: hidden;"
|
||||
innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits)
|
||||
|
||||
classes = ''
|
||||
if lineDecorations? and decorations = lineDecorations[screenRow]
|
||||
for id, decoration of decorations
|
||||
if Decoration.isType(decoration, 'gutter')
|
||||
classes += decoration.class + ' '
|
||||
|
||||
classes += "foldable " if bufferRow >= 0 and editor.isFoldableAtBufferRow(bufferRow)
|
||||
classes += "line-number line-number-#{bufferRow}"
|
||||
|
||||
"<div class=\"#{classes}\" style=\"#{style}\" data-buffer-row=\"#{bufferRow}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
|
||||
|
||||
buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) ->
|
||||
if softWrapped
|
||||
lineNumber = "•"
|
||||
else
|
||||
lineNumber = (bufferRow + 1).toString()
|
||||
|
||||
padding = multiplyString(' ', maxLineNumberDigits - lineNumber.length)
|
||||
iconHTML = '<div class="icon-right"></div>'
|
||||
padding + lineNumber + iconHTML
|
||||
|
||||
updateLineNumberNode: (lineNumberId, bufferRow, screenRow, softWrapped) ->
|
||||
{editor, lineDecorations} = @props
|
||||
node = @lineNumberNodesById[lineNumberId]
|
||||
|
||||
if editor.isFoldableAtBufferRow(bufferRow)
|
||||
node.classList.add('foldable')
|
||||
else
|
||||
node.classList.remove('foldable')
|
||||
|
||||
decorations = lineDecorations[screenRow]
|
||||
previousDecorations = @renderedDecorationsByLineNumberId[lineNumberId]
|
||||
|
||||
if previousDecorations?
|
||||
for id, decoration of previousDecorations
|
||||
if Decoration.isType(decoration, 'gutter') and not @hasDecoration(decorations, decoration)
|
||||
node.classList.remove(decoration.class)
|
||||
|
||||
if decorations?
|
||||
for id, decoration of decorations
|
||||
if Decoration.isType(decoration, 'gutter') and not @hasDecoration(previousDecorations, decoration)
|
||||
node.classList.add(decoration.class)
|
||||
|
||||
unless @screenRowsByLineNumberId[lineNumberId] is screenRow
|
||||
{lineHeightInPixels} = @props
|
||||
node.style.top = screenRow * lineHeightInPixels + 'px'
|
||||
node.dataset.screenRow = screenRow
|
||||
@screenRowsByLineNumberId[lineNumberId] = screenRow
|
||||
@lineNumberIdsByScreenRow[screenRow] = lineNumberId
|
||||
|
||||
hasDecoration: (decorations, decoration) ->
|
||||
decorations? and decorations[decoration.id] is decoration
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
{toArray, multiplyString, clone} = require 'underscore-plus'
|
||||
WrapperDiv = document.createElement('div')
|
||||
|
||||
module.exports =
|
||||
class GutterTileComponent
|
||||
constructor: (@presenter) ->
|
||||
@lineNumberNodesById = {}
|
||||
@screenRowsByLineNumberId = {}
|
||||
@lineNumberDecorationsByLineNumberId = {}
|
||||
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.style.overflow = 'hidden'
|
||||
if @presenter.dummy
|
||||
@domNode.style.visibility = 'hidden'
|
||||
else
|
||||
@domNode.style.position = 'absolute'
|
||||
@domNode.style.top = '0px'
|
||||
|
||||
@appendDummyLineNumber()
|
||||
@update()
|
||||
|
||||
appendDummyLineNumber: ->
|
||||
WrapperDiv.innerHTML = @buildLineNumberHTML('0')
|
||||
@domNode.appendChild(WrapperDiv.firstChild)
|
||||
|
||||
update: ->
|
||||
return if @presenter.dummy
|
||||
@updateTransform()
|
||||
@updateHeight()
|
||||
@updateLineNumbers()
|
||||
|
||||
updateTransform: ->
|
||||
{top} = @presenter
|
||||
unless top is @top
|
||||
@domNode.style['-webkit-transform'] = "translate3d(0px, #{top}px, 0px)"
|
||||
@top = top
|
||||
|
||||
updateHeight: ->
|
||||
{height} = @presenter
|
||||
unless height is @height
|
||||
@domNode.style.height = height + 'px'
|
||||
@height = height
|
||||
|
||||
updateLineNumbers: ->
|
||||
{lineNumbers, lineNumberDecorations} = @presenter
|
||||
|
||||
lineNumbersSet = new Set
|
||||
lineNumbersSet.add(lineNumber) for lineNumber in lineNumbers
|
||||
|
||||
for lineNumberId, lineNumberNode of @lineNumberNodesById
|
||||
unless lineNumbersSet.has(lineNumberId)
|
||||
delete @lineNumberNodesById[lineNumberId]
|
||||
delete @screenRowsByLineNumberId[lineNumberId]
|
||||
delete @lineNumberDecorationsByLineNumberId[lineNumberId]
|
||||
@domNode.removeChild(lineNumberNode)
|
||||
|
||||
newLineNumberIds = []
|
||||
newLineNumbersHTML = ""
|
||||
|
||||
for lineNumberId, i in lineNumbers
|
||||
screenRow = @presenter.startRow + i
|
||||
|
||||
if @lineNumberNodesById[lineNumberId]?
|
||||
@updateLineNumberNode(lineNumberId, screenRow)
|
||||
else
|
||||
newLineNumberIds.push(lineNumberId)
|
||||
newLineNumbersHTML += @buildLineNumberHTML(lineNumberId, screenRow)
|
||||
@screenRowsByLineNumberId[lineNumberId] = screenRow
|
||||
# @lineNumberIdsByScreenRow[screenRow] = id
|
||||
@lineNumberDecorationsByLineNumberId[lineNumberId] = clone(lineNumberDecorations[screenRow])
|
||||
|
||||
if newLineNumberIds.length > 0
|
||||
WrapperDiv.innerHTML = newLineNumbersHTML
|
||||
newLineNumberNodes = toArray(WrapperDiv.children)
|
||||
|
||||
for lineNumberId, i in newLineNumberIds
|
||||
@lineNumberNodesById[lineNumberId] = newLineNumberNodes[i]
|
||||
@lineNumberDecorationsByLineNumberId
|
||||
@domNode.appendChild(newLineNumberNodes[i])
|
||||
|
||||
buildLineNumberHTML: (lineNumberId, screenRow) ->
|
||||
{lineHeightInPixels, lineNumberDecorations} = @presenter
|
||||
|
||||
if screenRow?
|
||||
top = (screenRow - @presenter.startRow) * lineHeightInPixels
|
||||
style = "position: absolute; top: #{top}px;"
|
||||
else
|
||||
style = "visibility: hidden;"
|
||||
|
||||
innerHTML = @buildLineNumberInnerHTML(lineNumberId)
|
||||
|
||||
classes = ''
|
||||
if decorationsById = lineNumberDecorations?[screenRow]
|
||||
for id, decoration of decorationsById
|
||||
classes += decoration.class + ' '
|
||||
|
||||
# classes += "foldable " if bufferRow >= 0 and editor.isFoldableAtBufferRow(bufferRow)
|
||||
classes += "line-number line-number-#{lineNumberId.replace(/\..*/, '')}"
|
||||
|
||||
"<div class=\"#{classes}\" style=\"#{style}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
|
||||
|
||||
buildLineNumberInnerHTML: (lineNumberId) ->
|
||||
{maxLineNumberDigits} = @presenter
|
||||
softWrapped = lineNumberId.indexOf('.') isnt -1
|
||||
|
||||
if softWrapped
|
||||
lineNumber = "•"
|
||||
else
|
||||
lineNumber = lineNumberId
|
||||
|
||||
padding = multiplyString(' ', maxLineNumberDigits - lineNumber.length)
|
||||
iconHTML = '<div class="icon-right"></div>'
|
||||
padding + lineNumber + iconHTML
|
||||
|
||||
updateLineNumberNode: (lineNumberId, screenRow) ->
|
||||
{lineDecorations} = @presenter
|
||||
node = @lineNumberNodesById[lineNumberId]
|
||||
|
||||
# if editor.isFoldableAtBufferRow(bufferRow)
|
||||
# node.classList.add('foldable')
|
||||
# else
|
||||
# node.classList.remove('foldable')
|
||||
|
||||
unless @screenRowsByLineNumberId[lineNumberId] is screenRow
|
||||
{lineHeightInPixels} = @presenter
|
||||
node.style.top = screenRow * lineHeightInPixels + 'px'
|
||||
node.dataset.screenRow = screenRow
|
||||
@screenRowsByLineNumberId[lineNumberId] = screenRow
|
||||
# @lineNumberIdsByScreenRow[screenRow] = lineNumberId
|
||||
|
||||
@updateLineNumberDecorations(node, lineNumberId, screenRow)
|
||||
|
||||
updateLineNumberDecorations: (lineNumberNode, lineNumberId, screenRow) ->
|
||||
desiredDecorations = @presenter.lineNumberDecorations[screenRow]
|
||||
|
||||
if currentDecorations = @lineNumberDecorationsByLineNumberId[lineNumberId]
|
||||
for id, decoration of currentDecorations
|
||||
unless desiredDecorations?[id]?
|
||||
lineNumberNode.classList.remove(decoration.class)
|
||||
delete currentDecorations[id]
|
||||
|
||||
if desiredDecorations?
|
||||
currentDecorations = (@lineNumberDecorationsByLineNumberId[lineNumberId] ?= {})
|
||||
for id, decoration of desiredDecorations
|
||||
unless currentDecorations[id]?
|
||||
lineNumberNode.classList.add(decoration.class)
|
||||
currentDecorations[id] = decoration
|
||||
+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 true
|
||||
|
||||
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()
|
||||
@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: ->
|
||||
{contentPresenter} = @props
|
||||
domNode = @getDOMNode()
|
||||
|
||||
for tileStartRow, tileComponent of @tileComponentsByStartRow
|
||||
unless contentPresenter.tiles[tileStartRow]?
|
||||
domNode.removeChild(tileComponent.domNode)
|
||||
delete @tileComponentsByStartRow[tileStartRow]
|
||||
|
||||
for tileStartRow, tilePresenter of contentPresenter.tiles
|
||||
if tileComponent = @tileComponentsByStartRow[tileStartRow]
|
||||
tileComponent = @tileComponentsByStartRow[tileStartRow]
|
||||
tileComponent.update()
|
||||
else
|
||||
tileComponent = new EditorTileComponent(tilePresenter)
|
||||
@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