Merge remote-tracking branch 'origin/master' into cj-add-react-editor-shims

Conflicts:
	src/gutter-component.coffee
This commit is contained in:
probablycorey
2014-06-10 15:42:23 -07:00
15 changed files with 779 additions and 51 deletions

View File

@@ -111,6 +111,34 @@ class DisplayBufferMarker
setTailBufferPosition: (bufferPosition) ->
@bufferMarker.setTailPosition(bufferPosition)
# Retrieves the screen position of the marker's start. This will always be
# less than or equal to the result of {DisplayBufferMarker::getEndScreenPosition}.
#
# Returns a {Point}.
getStartScreenPosition: ->
@displayBuffer.screenPositionForBufferPosition(@getStartBufferPosition(), wrapAtSoftNewlines: true)
# Retrieves the buffer position of the marker's start. This will always be
# less than or equal to the result of {DisplayBufferMarker::getEndBufferPosition}.
#
# Returns a {Point}.
getStartBufferPosition: ->
@bufferMarker.getStartPosition()
# Retrieves the screen position of the marker's end. This will always be
# greater than or equal to the result of {DisplayBufferMarker::getStartScreenPosition}.
#
# Returns a {Point}.
getEndScreenPosition: ->
@displayBuffer.screenPositionForBufferPosition(@getEndBufferPosition(), wrapAtSoftNewlines: true)
# Retrieves the buffer position of the marker's end. This will always be
# greater than or equal to the result of {DisplayBufferMarker::getStartBufferPosition}.
#
# Returns a {Point}.
getEndBufferPosition: ->
@bufferMarker.getEndPosition()
# Sets the marker's tail to the same position as the marker's head.
#
# This only works if there isn't already a tail position.

View File

@@ -43,6 +43,8 @@ class DisplayBuffer extends Model
@charWidthsByScope = {}
@markers = {}
@foldsByMarkerId = {}
@decorations = {}
@decorationMarkerSubscriptions = {}
@updateAllScreenLines()
@createFoldForMarker(marker) for marker in @buffer.findMarkers(@getFoldMarkerAttributes())
@subscribe @tokenizedBuffer, 'grammar-changed', (grammar) => @emit 'grammar-changed', grammar
@@ -716,6 +718,97 @@ class DisplayBuffer extends Model
rangeForAllLines: ->
new Range([0, 0], @clipScreenPosition([Infinity, Infinity]))
decorationsForBufferRow: (bufferRow, decorationType) ->
decorations = @decorations[bufferRow] ? []
decorations = (dec for dec in decorations when not dec.type? or dec.type is decorationType) if decorationType?
decorations
decorationsForBufferRowRange: (startBufferRow, endBufferRow, decorationType) ->
decorations = {}
for bufferRow in [startBufferRow..endBufferRow]
decorations[bufferRow] = @decorationsForBufferRow(bufferRow, decorationType)
decorations
addDecorationToBufferRow: (bufferRow, decoration) ->
@decorations[bufferRow] ?= []
for current in @decorations[bufferRow]
return if _.isEqual(current, decoration)
@decorations[bufferRow].push(decoration)
@emit 'decoration-changed', {bufferRow, decoration, action: 'add'}
removeDecorationFromBufferRow: (bufferRow, decorationPattern) ->
return unless decorations = @decorations[bufferRow]
removed = []
i = decorations.length - 1
while i >= 0
if @decorationMatchesPattern(decorations[i], decorationPattern)
removed.push decorations[i]
decorations.splice(i, 1)
i--
delete @decorations[bufferRow] unless @decorations[bufferRow]?
for decoration in removed
@emit 'decoration-changed', {bufferRow, decoration, action: 'remove'}
removed
addDecorationToBufferRowRange: (startBufferRow, endBufferRow, decoration) ->
for bufferRow in [startBufferRow..endBufferRow]
@addDecorationToBufferRow(bufferRow, decoration)
return
removeDecorationFromBufferRowRange: (startBufferRow, endBufferRow, decoration) ->
for bufferRow in [startBufferRow..endBufferRow]
@removeDecorationFromBufferRow(bufferRow, decoration)
return
decorationMatchesPattern: (decoration, decorationPattern) ->
return false unless decoration? and decorationPattern?
for key, value of decorationPattern
return false if decoration[key] != value
true
addDecorationForMarker: (marker, decoration) ->
startRow = marker.getStartBufferPosition().row
endRow = marker.getEndBufferPosition().row
@addDecorationToBufferRowRange(startRow, endRow, decoration)
changedSubscription = @subscribe marker, 'changed', (e) =>
oldStartRow = e.oldHeadBufferPosition.row
oldEndRow = e.oldTailBufferPosition.row
newStartRow = e.newHeadBufferPosition.row
newEndRow = e.newTailBufferPosition.row
# swap so head is always <= than tail
[oldEndRow, oldStartRow] = [oldStartRow, oldEndRow] if oldStartRow > oldEndRow
[newEndRow, newStartRow] = [newStartRow, newEndRow] if newStartRow > newEndRow
@removeDecorationFromBufferRowRange(oldStartRow, oldEndRow, decoration)
@addDecorationToBufferRowRange(newStartRow, newEndRow, decoration) if e.isValid
destroyedSubscription = @subscribe marker, 'destroyed', (e) =>
@removeDecorationForMarker(marker, decoration)
@decorationMarkerSubscriptions[marker.id] ?= []
@decorationMarkerSubscriptions[marker.id].push {decoration, changedSubscription, destroyedSubscription}
removeDecorationForMarker: (marker, decorationPattern) ->
return unless @decorationMarkerSubscriptions[marker.id]?
startRow = marker.getStartBufferPosition().row
endRow = marker.getEndBufferPosition().row
@removeDecorationFromBufferRowRange(startRow, endRow, decorationPattern)
for subscription in _.clone(@decorationMarkerSubscriptions[marker.id])
if @decorationMatchesPattern(subscription.decoration, decorationPattern)
subscription.changedSubscription.off()
subscription.destroyedSubscription.off()
@decorationMarkerSubscriptions[marker.id] = _.without(@decorationMarkerSubscriptions[marker.id], subscription)
return
# Retrieves a {DisplayBufferMarker} based on its id.
#
# id - A {Number} representing a marker id
@@ -959,6 +1052,8 @@ class DisplayBuffer extends Model
@emit 'marker-created', @getMarker(marker.id)
createFoldForMarker: (marker) ->
bufferMarker = new DisplayBufferMarker({bufferMarker: marker, displayBuffer: this})
@addDecorationForMarker(bufferMarker, type: 'gutter', class: 'folded')
new Fold(this, marker)
foldForMarker: (marker) ->

View File

@@ -43,12 +43,14 @@ EditorComponent = React.createClass
{editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
maxLineNumberDigits = editor.getScreenLineCount().toString().length
invisibles = if showInvisibles then @state.invisibles else {}
hasSelection = editor.getSelection()? and !editor.getSelection().isEmpty()
if @isMounted()
renderedRowRange = @getRenderedRowRange()
[renderedStartRow, renderedEndRow] = renderedRowRange
cursorScreenRanges = @getCursorScreenRanges(renderedRowRange)
selectionScreenRanges = @getSelectionScreenRanges(renderedRowRange)
decorations = @getGutterDecorations(renderedRowRange)
scrollHeight = editor.getScrollHeight()
scrollWidth = editor.getScrollWidth()
scrollTop = editor.getScrollTop()
@@ -67,11 +69,13 @@ EditorComponent = React.createClass
className = 'editor-contents editor-colors'
className += ' is-focused' if focused
className += ' has-selection' if hasSelection
div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1,
GutterComponent {
ref: 'gutter', editor, renderedRowRange, maxLineNumberDigits, scrollTop,
scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow
scrollHeight, lineHeightInPixels, @pendingChanges, mouseWheelScreenRow,
decorations
}
div ref: 'scrollView', className: 'scroll-view', onMouseDown: @onMouseDown,
@@ -227,6 +231,18 @@ EditorComponent = React.createClass
selectionScreenRanges
getGutterDecorations: (renderedRowRange) ->
{editor} = @props
[renderedStartRow, renderedEndRow] = renderedRowRange
bufferRows = editor.bufferRowsForScreenRows(renderedStartRow, renderedEndRow - 1)
decorations = {}
for bufferRow in bufferRows
decorations[bufferRow] = editor.decorationsForBufferRow(bufferRow, 'gutter')
decorations[bufferRow].push {class: 'foldable'} if editor.isFoldableAtBufferRow(bufferRow)
decorations
observeEditor: ->
{editor} = @props
@subscribe editor, 'batched-updates-started', @onBatchedUpdatesStarted
@@ -235,6 +251,7 @@ EditorComponent = React.createClass
@subscribe editor, 'cursors-moved', @onCursorsMoved
@subscribe editor, 'selection-removed selection-screen-range-changed', @onSelectionChanged
@subscribe editor, 'selection-added', @onSelectionAdded
@subscribe editor, 'decoration-changed', @onDecorationChanged
@subscribe editor.$scrollTop.changes, @onScrollTopChanged
@subscribe editor.$scrollLeft.changes, @requestUpdate
@subscribe editor.$height.changes, @requestUpdate
@@ -503,6 +520,8 @@ EditorComponent = React.createClass
@onStoppedScrollingAfterDelay()
onStoppedScrolling: ->
return unless @isMounted()
@scrollingVertically = false
@mouseWheelScreenRow = null
@requestUpdate()
@@ -513,6 +532,11 @@ EditorComponent = React.createClass
@cursorsMoved = true
@requestUpdate()
onDecorationChanged: ->
@decorationChangedImmediate ?= setImmediate =>
@requestUpdate()
@decorationChangedImmediate = null
selectToMousePositionUntilMouseUp: (event) ->
{editor} = @props
dragging = false

View File

@@ -214,6 +214,7 @@ class Editor extends Model
@subscribe @displayBuffer, 'grammar-changed', => @handleGrammarChange()
@subscribe @displayBuffer, 'tokenized', => @handleTokenization()
@subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args...
@subscribe @displayBuffer, "decoration-changed", (e) => @emit 'decoration-changed', e
getViewClass: ->
if atom.config.get('core.useReactEditor')
@@ -1057,6 +1058,106 @@ class Editor extends Model
selection.insertText(fn(text))
selection.setBufferRange(range)
# Public: Get all the decorations for a buffer row.
#
# bufferRow - the {int} buffer row
# decorationType - the {String} decoration type to filter by eg. 'gutter'
#
# Returns an {Array} of decorations in the form `[{type: 'gutter', class: 'someclass'}, ...]`
# Returns an empty array when no decorations are found
decorationsForBufferRow: (bufferRow, decorationType) ->
@displayBuffer.decorationsForBufferRow(bufferRow, decorationType)
# Public: Get all the decorations for a range of buffer rows (inclusive)
#
# startBufferRow - the {int} start of the buffer row range
# endBufferRow - the {int} end of the buffer row range (inclusive)
# decorationType - the {String} decoration type to filter by eg. 'gutter'
#
# Returns an {Object} of decorations in the form `{23: [{type: 'gutter', class: 'someclass'}, ...], 24: [...]}`
# Returns an {Object} with keyed with all buffer rows in the range containing empty {Array}s when no decorations are found
decorationsForBufferRowRange: (startBufferRow, endBufferRow, decorationType) ->
@displayBuffer.decorationsForBufferRowRange(startBufferRow, endBufferRow, decorationType)
# Public: Adds a decoration to a buffer row. For example, use to mark a gutter
# line number with a class by using the form `{type: 'gutter', class: 'linter-error'}`
#
# bufferRow - the {int} buffer row
# decoration - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}`
#
# Returns nothing
addDecorationToBufferRow: (bufferRow, decoration) ->
@displayBuffer.addDecorationToBufferRow(bufferRow, decoration)
# Public: Removes a decoration from a buffer row.
#
# ```coffee
# editor.removeDecorationFromBufferRow(2, {type: 'gutter', class: 'linter-error'})
# ```
#
# All decorations matching a pattern will be removed. For example, you might
# have decorations with a namespace like this attached to a row:
#
# ```coffee
# [
# {type: 'gutter', namespace: 'myns', class: 'something'},
# {type: 'gutter', namespace: 'myns', class: 'something-else'}
# ]
# ```
#
# You can remove both with:
#
# ```coffee
# editor.removeDecorationFromBufferRow(2, {namespace: 'myns'})
# ```
#
# bufferRow - the {int} buffer row
# decorationPattern - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}`
#
# Returns an {Array} of the removed decorations
removeDecorationFromBufferRow: (bufferRow, decorationPattern) ->
@displayBuffer.removeDecorationFromBufferRow(bufferRow, decorationPattern)
# Public: Adds a decoration to line numbers in a buffer row range
#
# startBufferRow - the {int} start of the buffer row range
# endBufferRow - the {int} end of the buffer row range (inclusive)
# decoration - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}`
#
# Returns nothing
addDecorationToBufferRowRange: (startBufferRow, endBufferRow, decoration) ->
@displayBuffer.addDecorationToBufferRowRange(startBufferRow, endBufferRow, decoration)
# Public: Removes a decoration from line numbers in a buffer row range
#
# startBufferRow - the {int} start of the buffer row range
# endBufferRow - the {int} end of the buffer row range (inclusive)
# decoration - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}`
#
# Returns nothing
removeDecorationFromBufferRowRange: (startBufferRow, endBufferRow, decoration) ->
@displayBuffer.removeDecorationFromBufferRowRange(startBufferRow, endBufferRow, decoration)
# Public: Adds a decoration that tracks a {Marker}. When the marker moves,
# is invalidated, or is destroyed, the decoration will be updated to reflect the marker's state.
#
# marker - the {Marker} you want this decoration to follow
# decoration - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}`
#
# Returns nothing
addDecorationForMarker: (marker, decoration) ->
@displayBuffer.addDecorationForMarker(marker, decoration)
# Public: Removes all decorations associated with a {Marker} that match a
# `decorationPattern` and stop tracking the {Marker}.
#
# marker - the {Marker} to detach from
# decorationPattern - the {Object} decoration type to filter by eg. `{type: 'gutter', class: 'linter-error'}`
#
# Returns nothing
removeDecorationForMarker: (marker, decorationPattern) ->
@displayBuffer.removeDecorationForMarker(marker, decorationPattern)
# Public: Get the {DisplayBufferMarker} for the given marker id.
getMarker: (id) ->
@displayBuffer.getMarker(id)
@@ -1162,6 +1263,7 @@ class Editor extends Model
addCursor: (marker) ->
cursor = new Cursor(editor: this, marker: marker)
@cursors.push(cursor)
@addDecorationForMarker(marker, {class: 'cursor-line'})
@emit 'cursor-added', cursor
cursor

View File

@@ -1,3 +1,4 @@
_ = require 'underscore-plus'
React = require 'react-atom-fork'
{div} = require 'reactionary-atom-fork'
{isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus'
@@ -15,7 +16,7 @@ GutterComponent = React.createClass
render: ->
{scrollHeight, scrollTop} = @props
div className: 'gutter',
div className: 'gutter', onClick: @onClick,
div className: 'line-numbers', ref: 'lineNumbers', style:
height: scrollHeight
WebkitTransform: "translate3d(0px, #{-scrollTop}px, 0px)"
@@ -24,6 +25,7 @@ GutterComponent = React.createClass
@lineNumberNodesById = {}
@lineNumberIdsByScreenRow = {}
@screenRowsByLineNumberId = {}
@previousDecorations = {}
componentDidMount: ->
@appendDummyLineNumber()
@@ -36,10 +38,12 @@ GutterComponent = React.createClass
'renderedRowRange', 'scrollTop', 'lineHeightInPixels', 'mouseWheelScreenRow'
)
{renderedRowRange, pendingChanges} = newProps
{renderedRowRange, pendingChanges, decorations} = newProps
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
return true unless _.isEqual(@previousDecorations, decorations)
false
componentDidUpdate: (oldProps) ->
@@ -70,7 +74,7 @@ GutterComponent = React.createClass
@removeLineNumberNodes(lineNumberIdsToPreserve)
appendOrUpdateVisibleLineNumberNodes: ->
{editor, renderedRowRange, scrollTop, maxLineNumberDigits} = @props
{editor, renderedRowRange, scrollTop, maxLineNumberDigits, decorations} = @props
[startRow, endRow] = renderedRowRange
newLineNumberIds = null
@@ -91,12 +95,12 @@ GutterComponent = React.createClass
visibleLineNumberIds.add(id)
if @hasLineNumberNode(id)
@updateLineNumberNode(id, screenRow)
@updateLineNumberNode(id, bufferRow, screenRow, wrapCount > 0, decorations[bufferRow])
else
newLineNumberIds ?= []
newLineNumbersHTML ?= ""
newLineNumberIds.push(id)
newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow)
newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow, decorations[bufferRow])
@screenRowsByLineNumberId[id] = screenRow
@lineNumberIdsByScreenRow[screenRow] = id
@@ -110,6 +114,7 @@ GutterComponent = React.createClass
@lineNumberNodesById[lineNumberId] = lineNumberNode
node.appendChild(lineNumberNode)
@previousDecorations = decorations
visibleLineNumberIds
removeLineNumberNodes: (lineNumberIdsToPreserve) ->
@@ -123,7 +128,7 @@ GutterComponent = React.createClass
delete @screenRowsByLineNumberId[lineNumberId]
node.removeChild(lineNumberNode)
buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow) ->
buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow, decorations) ->
if screenRow?
{lineHeightInPixels} = @props
style = "position: absolute; top: #{screenRow * lineHeightInPixels}px;"
@@ -131,7 +136,13 @@ GutterComponent = React.createClass
style = "visibility: hidden;"
innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits)
"<div class=\"line-number line-number-#{bufferRow}\" style=\"#{style}\" data-buffer-row=\"#{bufferRow}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
classes = ''
if decorations?
for decoration in decorations
classes += decoration.class + ' ' if not softWrapped or softWrapped and decoration.softWrap
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
@@ -143,11 +154,23 @@ GutterComponent = React.createClass
iconHTML = '<div class="icon-right"></div>'
padding + lineNumber + iconHTML
updateLineNumberNode: (lineNumberId, screenRow) ->
updateLineNumberNode: (lineNumberId, bufferRow, screenRow, softWrapped, decorations) ->
node = @lineNumberNodesById[lineNumberId]
previousDecorations = @previousDecorations[bufferRow]
if previousDecorations?
for decoration in previousDecorations
node.classList.remove(decoration.class) if not contains(decorations, decoration)
if decorations?
for decoration in decorations
if not contains(previousDecorations, decoration) and (not softWrapped or softWrapped and decoration.softWrap)
node.classList.add(decoration.class)
unless @screenRowsByLineNumberId[lineNumberId] is screenRow
{lineHeightInPixels} = @props
@lineNumberNodesById[lineNumberId].style.top = screenRow * lineHeightInPixels + 'px'
@lineNumberNodesById[lineNumberId].dataset.screenRow = screenRow
node.style.top = screenRow * lineHeightInPixels + 'px'
node.dataset.screenRow = screenRow
@screenRowsByLineNumberId[lineNumberId] = screenRow
@lineNumberIdsByScreenRow[screenRow] = lineNumberId
@@ -156,3 +179,22 @@ GutterComponent = React.createClass
lineNumberNodeForScreenRow: (screenRow) ->
@lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]]
onClick: (event) ->
{editor} = @props
{target} = event
lineNumber = target.parentNode
if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row'))
if lineNumber.classList.contains('folded')
editor.unfoldBufferRow(bufferRow)
else
editor.foldBufferRow(bufferRow)
# Created because underscore uses === not _.isEqual, which we need
contains = (array, target) ->
return false unless array?
for object in array
return true if _.isEqual(object, target)
false

View File

@@ -1,4 +1,5 @@
{View, $} = require 'space-pen'
Grim = require 'Grim'
React = require 'react-atom-fork'
EditorComponent = require './editor-component'
{defaults} = require 'underscore-plus'
@@ -16,6 +17,8 @@ class ReactEditorView extends View
getEditor: -> @editor
getModel: -> @editor
Object.defineProperty @::, 'lineHeight', get: -> @editor.getLineHeightInPixels()
Object.defineProperty @::, 'charWidth', get: -> @editor.getDefaultCharWidth()
Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0]
@@ -40,12 +43,14 @@ class ReactEditorView extends View
@gutter = $(node).find('.gutter')
@gutter.removeClassFromAllLines = (klass) =>
Grim.deprecate 'You no longer need to manually add and remove classes. Use `Editor::removeDecorationFromBufferRow()` and related functions'
@gutter.find('.line-number').removeClass(klass)
@gutter.getLineNumberElement = (bufferRow) =>
@gutter.find("[data-buffer-row='#{bufferRow}']")
@gutter.addClassToLine = (bufferRow, klass) =>
Grim.deprecate 'You no longer need to manually add and remove classes. Use `Editor::addDecorationToBufferRow()` and related functions'
lines = @gutter.find("[data-buffer-row='#{bufferRow}']")
lines.addClass(klass)
lines.length > 0