mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
Previously, the animation loop would run multiple times prior to the the mouseup event on click. We only want to select to the current mouse position if the mouse is actually dragged.
439 lines
17 KiB
CoffeeScript
439 lines
17 KiB
CoffeeScript
React = require 'react'
|
|
ReactUpdates = require 'react/lib/ReactUpdates'
|
|
{div, span} = require 'reactionary'
|
|
{$$} = require 'space-pen'
|
|
{debounce} = require 'underscore-plus'
|
|
|
|
InputComponent = require './input-component'
|
|
SelectionComponent = require './selection-component'
|
|
CursorComponent = require './cursor-component'
|
|
CustomEventMixin = require './custom-event-mixin'
|
|
SubscriberMixin = require './subscriber-mixin'
|
|
|
|
DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
|
|
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
|
|
|
|
module.exports =
|
|
EditorCompont = React.createClass
|
|
pendingScrollTop: null
|
|
lastScrollTop: null
|
|
selectOnMouseMove: false
|
|
|
|
statics: {DummyLineNode}
|
|
|
|
mixins: [CustomEventMixin, SubscriberMixin]
|
|
|
|
render: ->
|
|
{fontSize, lineHeight, fontFamily} = @state
|
|
{editor} = @props
|
|
|
|
div className: 'editor react', tabIndex: -1, style: {fontSize, lineHeight, fontFamily},
|
|
div className: 'scroll-view', ref: 'scrollView',
|
|
InputComponent ref: 'hiddenInput', className: 'hidden-input', onInput: @onInput
|
|
@renderScrollableContent()
|
|
div className: 'vertical-scrollbar', ref: 'verticalScrollbar', onScroll: @onVerticalScroll,
|
|
div outlet: 'verticalScrollbarContent', style: {height: editor.getScrollHeight()}
|
|
|
|
renderScrollableContent: ->
|
|
{editor} = @props
|
|
style =
|
|
height: editor.getScrollHeight()
|
|
WebkitTransform: "translateY(#{-editor.getScrollTop()}px)"
|
|
|
|
div {className: 'scrollable-content', style, @onMouseDown},
|
|
@renderCursors()
|
|
@renderVisibleLines()
|
|
@renderUnderlayer()
|
|
|
|
renderVisibleLines: ->
|
|
{editor} = @props
|
|
[startRow, endRow] = @getVisibleRowRange()
|
|
lineHeightInPixels = editor.getLineHeight()
|
|
precedingHeight = startRow * lineHeightInPixels
|
|
followingHeight = (editor.getScreenLineCount() - endRow) * lineHeightInPixels
|
|
|
|
div className: 'lines', ref: 'lines', [
|
|
div className: 'spacer', key: 'top-spacer', style: {height: precedingHeight}
|
|
(for tokenizedLine in @props.editor.linesForScreenRows(startRow, endRow - 1)
|
|
LineComponent({tokenizedLine, key: tokenizedLine.id}))...
|
|
div className: 'spacer', key: 'bottom-spacer', style: {height: followingHeight}
|
|
]
|
|
|
|
renderCursors: ->
|
|
{editor} = @props
|
|
|
|
for selection in editor.getSelections() when editor.selectionIntersectsVisibleRowRange(selection)
|
|
CursorComponent(cursor: selection.cursor)
|
|
|
|
renderUnderlayer: ->
|
|
{editor} = @props
|
|
|
|
div className: 'underlayer',
|
|
for selection in editor.getSelections() when editor.selectionIntersectsVisibleRowRange(selection)
|
|
SelectionComponent({selection})
|
|
|
|
getVisibleRowRange: ->
|
|
visibleRowRange = @props.editor.getVisibleRowRange()
|
|
if @visibleRowOverrides?
|
|
visibleRowRange[0] = Math.min(visibleRowRange[0], @visibleRowOverrides[0])
|
|
visibleRowRange[1] = Math.max(visibleRowRange[1], @visibleRowOverrides[1])
|
|
visibleRowRange
|
|
|
|
getInitialState: -> {}
|
|
|
|
getDefaultProps: -> updateSync: true
|
|
|
|
componentDidMount: ->
|
|
@measuredLines = new WeakSet
|
|
|
|
@listenForDOMEvents()
|
|
@listenForCustomEvents()
|
|
@observeEditor()
|
|
@observeConfig()
|
|
|
|
@updateAllDimensions()
|
|
@props.editor.setVisible(true)
|
|
|
|
componentWillUnmount: ->
|
|
@getDOMNode().removeEventListener 'mousewheel', @onMouseWheel
|
|
|
|
componentDidUpdate: ->
|
|
@updateVerticalScrollbar()
|
|
@measureNewLines()
|
|
|
|
# The React-provided scrollTop property doesn't work in this case because when
|
|
# initially rendering, the synthetic scrollHeight hasn't been computed yet.
|
|
# trying to assign it before the element inside is tall enough?
|
|
updateVerticalScrollbar: ->
|
|
{editor} = @props
|
|
scrollTop = editor.getScrollTop()
|
|
|
|
return if scrollTop is @lastScrollTop
|
|
|
|
scrollbarNode = @refs.verticalScrollbar.getDOMNode()
|
|
scrollbarNode.scrollTop = scrollTop
|
|
@lastScrollTop = scrollbarNode.scrollTop
|
|
|
|
observeEditor: ->
|
|
{editor} = @props
|
|
@subscribe editor, 'screen-lines-changed', @onScreenLinesChanged
|
|
@subscribe editor, 'selection-added', @onSelectionAdded
|
|
@subscribe editor, 'selection-removed', @onSelectionAdded
|
|
@subscribe editor.$scrollTop.changes, @requestUpdate
|
|
@subscribe editor.$height.changes, @requestUpdate
|
|
@subscribe editor.$width.changes, @requestUpdate
|
|
@subscribe editor.$defaultCharWidth.changes, @requestUpdate
|
|
@subscribe editor.$lineHeight.changes, @requestUpdate
|
|
|
|
listenForDOMEvents: ->
|
|
scrollViewNode = @refs.scrollView.getDOMNode()
|
|
scrollViewNode.addEventListener 'mousewheel', @onMouseWheel
|
|
scrollViewNode.addEventListener 'overflowchanged', @onOverflowChanged
|
|
@getDOMNode().addEventListener 'focus', @onFocus
|
|
|
|
listenForCustomEvents: ->
|
|
{editor, mini} = @props
|
|
|
|
@addCustomEventListeners
|
|
'core:move-left': => editor.moveCursorLeft()
|
|
'core:move-right': => editor.moveCursorRight()
|
|
'core:select-left': => editor.selectLeft()
|
|
'core:select-right': => editor.selectRight()
|
|
'core:select-all': => editor.selectAll()
|
|
'core:backspace': => editor.backspace()
|
|
'core:delete': => editor.delete()
|
|
'core:undo': => editor.undo()
|
|
'core:redo': => editor.redo()
|
|
'core:cut': => editor.cutSelectedText()
|
|
'core:copy': => editor.copySelectedText()
|
|
'core:paste': => editor.pasteText()
|
|
'editor:move-to-previous-word': => editor.moveCursorToPreviousWord()
|
|
'editor:select-word': => editor.selectWord()
|
|
# 'editor:consolidate-selections': (event) => @consolidateSelections(event)
|
|
'editor:backspace-to-beginning-of-word': => editor.backspaceToBeginningOfWord()
|
|
'editor:backspace-to-beginning-of-line': => editor.backspaceToBeginningOfLine()
|
|
'editor:delete-to-end-of-word': => editor.deleteToEndOfWord()
|
|
'editor:delete-line': => editor.deleteLine()
|
|
'editor:cut-to-end-of-line': => editor.cutToEndOfLine()
|
|
'editor:move-to-beginning-of-screen-line': => editor.moveCursorToBeginningOfScreenLine()
|
|
'editor:move-to-beginning-of-line': => editor.moveCursorToBeginningOfLine()
|
|
'editor:move-to-end-of-screen-line': => editor.moveCursorToEndOfScreenLine()
|
|
'editor:move-to-end-of-line': => editor.moveCursorToEndOfLine()
|
|
'editor:move-to-first-character-of-line': => editor.moveCursorToFirstCharacterOfLine()
|
|
'editor:move-to-beginning-of-word': => editor.moveCursorToBeginningOfWord()
|
|
'editor:move-to-end-of-word': => editor.moveCursorToEndOfWord()
|
|
'editor:move-to-beginning-of-next-word': => editor.moveCursorToBeginningOfNextWord()
|
|
'editor:move-to-previous-word-boundary': => editor.moveCursorToPreviousWordBoundary()
|
|
'editor:move-to-next-word-boundary': => editor.moveCursorToNextWordBoundary()
|
|
'editor:select-to-end-of-line': => editor.selectToEndOfLine()
|
|
'editor:select-to-beginning-of-line': => editor.selectToBeginningOfLine()
|
|
'editor:select-to-end-of-word': => editor.selectToEndOfWord()
|
|
'editor:select-to-beginning-of-word': => editor.selectToBeginningOfWord()
|
|
'editor:select-to-beginning-of-next-word': => editor.selectToBeginningOfNextWord()
|
|
'editor:select-to-next-word-boundary': => editor.selectToNextWordBoundary()
|
|
'editor:select-to-previous-word-boundary': => editor.selectToPreviousWordBoundary()
|
|
'editor:select-to-first-character-of-line': => editor.selectToFirstCharacterOfLine()
|
|
'editor:select-line': => editor.selectLine()
|
|
'editor:transpose': => editor.transpose()
|
|
'editor:upper-case': => editor.upperCase()
|
|
'editor:lower-case': => editor.lowerCase()
|
|
|
|
unless mini
|
|
@addCustomEventListeners
|
|
'core:move-up': => editor.moveCursorUp()
|
|
'core:move-down': => editor.moveCursorDown()
|
|
'core:move-to-top': => editor.moveCursorToTop()
|
|
'core:move-to-bottom': => editor.moveCursorToBottom()
|
|
'core:select-up': => editor.selectUp()
|
|
'core:select-down': => editor.selectDown()
|
|
'core:select-to-top': => editor.selectToTop()
|
|
'core:select-to-bottom': => editor.selectToBottom()
|
|
'editor:indent': => editor.indent()
|
|
'editor:auto-indent': => editor.autoIndentSelectedRows()
|
|
'editor:indent-selected-rows': => editor.indentSelectedRows()
|
|
'editor:outdent-selected-rows': => editor.outdentSelectedRows()
|
|
'editor:newline': => editor.insertNewline()
|
|
'editor:newline-below': => editor.insertNewlineBelow()
|
|
'editor:newline-above': => editor.insertNewlineAbove()
|
|
'editor:add-selection-below': => editor.addSelectionBelow()
|
|
'editor:add-selection-above': => editor.addSelectionAbove()
|
|
'editor:split-selections-into-lines': => editor.splitSelectionsIntoLines()
|
|
'editor:toggle-soft-tabs': => editor.toggleSoftTabs()
|
|
'editor:toggle-soft-wrap': => editor.toggleSoftWrap()
|
|
'editor:fold-all': => editor.foldAll()
|
|
'editor:unfold-all': => editor.unfoldAll()
|
|
'editor:fold-current-row': => editor.foldCurrentRow()
|
|
'editor:unfold-current-row': => editor.unfoldCurrentRow()
|
|
'editor:fold-selection': => neditor.foldSelectedLines()
|
|
'editor:fold-at-indent-level-1': => editor.foldAllAtIndentLevel(0)
|
|
'editor:fold-at-indent-level-2': => editor.foldAllAtIndentLevel(1)
|
|
'editor:fold-at-indent-level-3': => editor.foldAllAtIndentLevel(2)
|
|
'editor:fold-at-indent-level-4': => editor.foldAllAtIndentLevel(3)
|
|
'editor:fold-at-indent-level-5': => editor.foldAllAtIndentLevel(4)
|
|
'editor:fold-at-indent-level-6': => editor.foldAllAtIndentLevel(5)
|
|
'editor:fold-at-indent-level-7': => editor.foldAllAtIndentLevel(6)
|
|
'editor:fold-at-indent-level-8': => editor.foldAllAtIndentLevel(7)
|
|
'editor:fold-at-indent-level-9': => editor.foldAllAtIndentLevel(8)
|
|
'editor:toggle-line-comments': => editor.toggleLineCommentsInSelection()
|
|
'editor:log-cursor-scope': => editor.logCursorScope()
|
|
'editor:checkout-head-revision': => editor.checkoutHead()
|
|
'editor:copy-path': => editor.copyPathToClipboard()
|
|
'editor:move-line-up': => editor.moveLineUp()
|
|
'editor:move-line-down': => editor.moveLineDown()
|
|
'editor:duplicate-lines': => editor.duplicateLines()
|
|
'editor:join-lines': => editor.joinLines()
|
|
'editor:toggle-indent-guide': => atom.config.toggle('editor.showIndentGuide')
|
|
'editor:toggle-line-numbers': => atom.config.toggle('editor.showLineNumbers')
|
|
# 'core:page-down': => @pageDown()
|
|
# 'core:page-up': => @pageUp()
|
|
# 'editor:scroll-to-cursor': => @scrollToCursorPosition()
|
|
|
|
observeConfig: ->
|
|
@subscribe atom.config.observe 'editor.fontFamily', @setFontFamily
|
|
|
|
setFontSize: (fontSize) ->
|
|
@clearScopedCharWidths()
|
|
@setState({fontSize})
|
|
@updateLineDimensions()
|
|
|
|
setLineHeight: (lineHeight) ->
|
|
@setState({lineHeight})
|
|
|
|
setFontFamily: (fontFamily) ->
|
|
@clearScopedCharWidths()
|
|
@setState({fontFamily})
|
|
@updateLineDimensions()
|
|
|
|
onFocus: ->
|
|
@refs.hiddenInput.focus()
|
|
|
|
onVerticalScroll: ->
|
|
scrollTop = @refs.verticalScrollbar.getDOMNode().scrollTop
|
|
return if @props.editor.getScrollTop() is scrollTop
|
|
|
|
animationFramePending = @pendingScrollTop?
|
|
@pendingScrollTop = scrollTop
|
|
unless animationFramePending
|
|
requestAnimationFrame =>
|
|
@props.editor.setScrollTop(@pendingScrollTop)
|
|
@pendingScrollTop = null
|
|
|
|
onMouseWheel: (event) ->
|
|
# To preserve velocity scrolling, delay removal of the event's target until
|
|
# after mousewheel events stop being fired. Removing the target before then
|
|
# will cause scrolling to stop suddenly.
|
|
@visibleRowOverrides = @getVisibleRowRange()
|
|
@clearVisibleRowOverridesAfterDelay ?= debounce(@clearVisibleRowOverrides, 100)
|
|
@clearVisibleRowOverridesAfterDelay()
|
|
|
|
@refs.verticalScrollbar.getDOMNode().scrollTop -= event.wheelDeltaY
|
|
event.preventDefault()
|
|
|
|
onMouseDown: (event) ->
|
|
{editor} = @props
|
|
{shiftKey, metaKey} = event
|
|
screenPosition = @screenPositionForMouseEvent(event)
|
|
|
|
if shiftKey
|
|
editor.selectToScreenPosition(screenPosition)
|
|
else if metaKey
|
|
editor.addCursorAtScreenPosition(screenPosition)
|
|
else
|
|
editor.setCursorScreenPosition(screenPosition)
|
|
|
|
@selectToMousePositionUntilMouseUp(event)
|
|
|
|
selectToMousePositionUntilMouseUp: (event) ->
|
|
{editor} = @props
|
|
dragging = false
|
|
lastMousePosition = {}
|
|
|
|
animationLoop = =>
|
|
requestAnimationFrame =>
|
|
if dragging
|
|
@selectToMousePosition(lastMousePosition)
|
|
animationLoop()
|
|
|
|
onMouseMove = (event) ->
|
|
lastMousePosition.clientX = event.clientX
|
|
lastMousePosition.clientY = event.clientY
|
|
|
|
# Start the animation loop when the mouse moves prior to a mouseup event
|
|
unless dragging
|
|
dragging = true
|
|
animationLoop()
|
|
|
|
# Stop dragging when cursor enters dev tools because we can't detect mouseup
|
|
onMouseUp() if event.which is 0
|
|
|
|
onMouseUp = ->
|
|
dragging = false
|
|
window.removeEventListener('mousemove', onMouseMove)
|
|
window.removeEventListener('mouseup', onMouseUp)
|
|
|
|
window.addEventListener('mousemove', onMouseMove)
|
|
window.addEventListener('mouseup', onMouseUp)
|
|
|
|
selectToMousePosition: (event) ->
|
|
@props.editor.selectToScreenPosition(@screenPositionForMouseEvent(event))
|
|
|
|
screenPositionForMouseEvent: (event) ->
|
|
pixelPosition = @pixelPositionForMouseEvent(event)
|
|
@props.editor.screenPositionForPixelPosition(pixelPosition)
|
|
|
|
pixelPositionForMouseEvent: (event) ->
|
|
{editor} = @props
|
|
{clientX, clientY} = event
|
|
|
|
editorClientRect = @refs.scrollView.getDOMNode().getBoundingClientRect()
|
|
top = clientY - editorClientRect.top + editor.getScrollTop()
|
|
left = clientX - editorClientRect.left
|
|
{top, left}
|
|
|
|
clearVisibleRowOverrides: ->
|
|
@visibleRowOverrides = null
|
|
@forceUpdate()
|
|
|
|
clearVisibleRowOverridesAfterDelay: null
|
|
|
|
onOverflowChanged: ->
|
|
@props.editor.setHeight(@refs.scrollView.getDOMNode().clientHeight)
|
|
|
|
onInput: (char, replaceLastChar) ->
|
|
ReactUpdates.batchedUpdates => @props.editor.insertText(char)
|
|
|
|
onScreenLinesChanged: ({start, end}) ->
|
|
{editor} = @props
|
|
@requestUpdate() if editor.intersectsVisibleRowRange(start, end + 1) # TODO: Use closed-open intervals for change events
|
|
|
|
onSelectionAdded: (selection) ->
|
|
{editor} = @props
|
|
@requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection)
|
|
|
|
onSelectionRemoved: (selection) ->
|
|
{editor} = @props
|
|
@requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection)
|
|
|
|
requestUpdate: ->
|
|
@forceUpdate()
|
|
|
|
updateAllDimensions: ->
|
|
{height, width} = @measureScrollViewDimensions()
|
|
{lineHeightInPixels, charWidth} = @measureLineDimensions()
|
|
{editor} = @props
|
|
|
|
editor.setHeight(height)
|
|
editor.setWidth(width)
|
|
editor.setLineHeight(lineHeightInPixels)
|
|
editor.setDefaultCharWidth(charWidth)
|
|
|
|
updateLineDimensions: ->
|
|
{lineHeightInPixels, charWidth} = @measureLineDimensions()
|
|
{editor} = @props
|
|
|
|
editor.setLineHeight(lineHeightInPixels)
|
|
editor.setDefaultCharWidth(charWidth)
|
|
|
|
measureScrollViewDimensions: ->
|
|
scrollViewNode = @refs.scrollView.getDOMNode()
|
|
{height: scrollViewNode.clientHeight, width: scrollViewNode.clientWidth}
|
|
|
|
measureLineDimensions: ->
|
|
linesNode = @refs.lines.getDOMNode()
|
|
linesNode.appendChild(DummyLineNode)
|
|
lineHeightInPixels = DummyLineNode.getBoundingClientRect().height
|
|
charWidth = DummyLineNode.firstChild.getBoundingClientRect().width
|
|
linesNode.removeChild(DummyLineNode)
|
|
{lineHeightInPixels, charWidth}
|
|
|
|
measureNewLines: ->
|
|
[visibleStartRow, visibleEndRow] = @getVisibleRowRange()
|
|
linesNode = @refs.lines.getDOMNode()
|
|
|
|
for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1)
|
|
unless @measuredLines.has(tokenizedLine)
|
|
lineNode = linesNode.children[i + 1]
|
|
@measureCharactersInLine(tokenizedLine, lineNode)
|
|
|
|
measureCharactersInLine: (tokenizedLine, lineNode) ->
|
|
{editor} = @props
|
|
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
|
|
rangeForMeasurement = document.createRange()
|
|
|
|
for {value, scopes} in tokenizedLine.tokens
|
|
textNode = iterator.nextNode()
|
|
charWidths = editor.getScopedCharWidths(scopes)
|
|
for char, i in value
|
|
unless charWidths[char]?
|
|
rangeForMeasurement.setStart(textNode, i)
|
|
rangeForMeasurement.setEnd(textNode, i + 1)
|
|
charWidth = rangeForMeasurement.getBoundingClientRect().width
|
|
editor.setScopedCharWidth(scopes, char, charWidth)
|
|
|
|
@measuredLines.add(tokenizedLine)
|
|
|
|
clearScopedCharWidths: ->
|
|
@measuredLines.clear()
|
|
@props.editor.clearScopedCharWidths()
|
|
|
|
LineComponent = React.createClass
|
|
render: ->
|
|
div className: 'line', dangerouslySetInnerHTML: {__html: @buildInnerHTML()}
|
|
|
|
buildInnerHTML: ->
|
|
if @props.tokenizedLine.text.length is 0
|
|
"<span> </span>"
|
|
else
|
|
@buildScopeTreeHTML(@props.tokenizedLine.getScopeTree())
|
|
|
|
buildScopeTreeHTML: (scopeTree) ->
|
|
if scopeTree.children?
|
|
html = "<span class='#{scopeTree.scope.replace(/\./g, ' ')}'>"
|
|
html += @buildScopeTreeHTML(child) for child in scopeTree.children
|
|
html += "</span>"
|
|
html
|
|
else
|
|
"<span>#{scopeTree.getValueAsHtml({})}</span>"
|
|
|
|
shouldComponentUpdate: -> false
|