mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
There's a distinction to make between rendered and visible rows, and we were using the former as if it was the latter. In particular, if a tile is visible, all its rows get rendered on screen, even though they might not necessarily be visible by the user.
985 lines
35 KiB
CoffeeScript
985 lines
35 KiB
CoffeeScript
_ = require 'underscore-plus'
|
|
scrollbarStyle = require 'scrollbar-style'
|
|
{Range, Point} = require 'text-buffer'
|
|
{CompositeDisposable} = require 'event-kit'
|
|
{ipcRenderer} = require 'electron'
|
|
|
|
TextEditorPresenter = require './text-editor-presenter'
|
|
GutterContainerComponent = require './gutter-container-component'
|
|
InputComponent = require './input-component'
|
|
LinesComponent = require './lines-component'
|
|
ScrollbarComponent = require './scrollbar-component'
|
|
ScrollbarCornerComponent = require './scrollbar-corner-component'
|
|
OverlayManager = require './overlay-manager'
|
|
DOMElementPool = require './dom-element-pool'
|
|
LinesYardstick = require './lines-yardstick'
|
|
BlockDecorationsComponent = require './block-decorations-component'
|
|
LineTopIndex = require 'line-top-index'
|
|
|
|
module.exports =
|
|
class TextEditorComponent
|
|
scrollSensitivity: 0.4
|
|
cursorBlinkPeriod: 800
|
|
cursorBlinkResumeDelay: 100
|
|
tileSize: 12
|
|
|
|
pendingScrollTop: null
|
|
pendingScrollLeft: null
|
|
updateRequested: false
|
|
updatesPaused: false
|
|
updateRequestedWhilePaused: false
|
|
heightAndWidthMeasurementRequested: false
|
|
inputEnabled: true
|
|
measureScrollbarsWhenShown: true
|
|
measureLineHeightAndDefaultCharWidthWhenShown: true
|
|
stylingChangeAnimationFrameRequested: false
|
|
gutterComponent: null
|
|
mounted: true
|
|
initialized: false
|
|
|
|
Object.defineProperty @prototype, "domNode",
|
|
get: -> @domNodeValue
|
|
set: (domNode) ->
|
|
@assert domNode?, "TextEditorComponent::domNode was set to null."
|
|
@domNodeValue = domNode
|
|
|
|
constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars, scrollPastEnd}) ->
|
|
@tileSize = tileSize if tileSize?
|
|
@disposables = new CompositeDisposable
|
|
|
|
@observeConfig()
|
|
@setScrollSensitivity(@config.get('editor.scrollSensitivity'))
|
|
|
|
lineTopIndex = new LineTopIndex({
|
|
defaultLineHeight: @editor.getLineHeightInPixels()
|
|
})
|
|
@presenter = new TextEditorPresenter
|
|
model: @editor
|
|
tileSize: tileSize
|
|
cursorBlinkPeriod: @cursorBlinkPeriod
|
|
cursorBlinkResumeDelay: @cursorBlinkResumeDelay
|
|
stoppedScrollingDelay: 200
|
|
config: @config
|
|
lineTopIndex: lineTopIndex
|
|
scrollPastEnd: scrollPastEnd
|
|
|
|
@presenter.onDidUpdateState(@requestUpdate)
|
|
|
|
@domElementPool = new DOMElementPool
|
|
@domNode = document.createElement('div')
|
|
if @useShadowDOM
|
|
@domNode.classList.add('editor-contents--private')
|
|
|
|
insertionPoint = document.createElement('content')
|
|
insertionPoint.setAttribute('select', 'atom-overlay')
|
|
@domNode.appendChild(insertionPoint)
|
|
@overlayManager = new OverlayManager(@presenter, @hostElement, @views)
|
|
@blockDecorationsComponent = new BlockDecorationsComponent(@hostElement, @views, @presenter, @domElementPool)
|
|
else
|
|
@domNode.classList.add('editor-contents')
|
|
@overlayManager = new OverlayManager(@presenter, @domNode, @views)
|
|
|
|
@scrollViewNode = document.createElement('div')
|
|
@scrollViewNode.classList.add('scroll-view')
|
|
@domNode.appendChild(@scrollViewNode)
|
|
|
|
@hiddenInputComponent = new InputComponent
|
|
@scrollViewNode.appendChild(@hiddenInputComponent.getDomNode())
|
|
|
|
@linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool, @assert, @grammars})
|
|
@scrollViewNode.appendChild(@linesComponent.getDomNode())
|
|
|
|
if @blockDecorationsComponent?
|
|
@linesComponent.getDomNode().appendChild(@blockDecorationsComponent.getDomNode())
|
|
|
|
@linesYardstick = new LinesYardstick(@editor, @linesComponent, lineTopIndex, @grammars)
|
|
@presenter.setLinesYardstick(@linesYardstick)
|
|
|
|
@horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll})
|
|
@scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode())
|
|
|
|
@verticalScrollbarComponent = new ScrollbarComponent({orientation: 'vertical', onScroll: @onVerticalScroll})
|
|
@domNode.appendChild(@verticalScrollbarComponent.getDomNode())
|
|
|
|
@scrollbarCornerComponent = new ScrollbarCornerComponent
|
|
@domNode.appendChild(@scrollbarCornerComponent.getDomNode())
|
|
|
|
@observeEditor()
|
|
@listenForDOMEvents()
|
|
|
|
@disposables.add @stylesElement.onDidAddStyleElement @onStylesheetsChanged
|
|
@disposables.add @stylesElement.onDidUpdateStyleElement @onStylesheetsChanged
|
|
@disposables.add @stylesElement.onDidRemoveStyleElement @onStylesheetsChanged
|
|
unless @themes.isInitialLoadComplete()
|
|
@disposables.add @themes.onDidChangeActiveThemes @onAllThemesLoaded
|
|
@disposables.add scrollbarStyle.onDidChangePreferredScrollbarStyle @refreshScrollbars
|
|
|
|
@disposables.add @views.pollDocument(@pollDOM)
|
|
|
|
@updateSync()
|
|
@checkForVisibilityChange()
|
|
@initialized = true
|
|
|
|
destroy: ->
|
|
@mounted = false
|
|
@disposables.dispose()
|
|
@presenter.destroy()
|
|
@gutterContainerComponent?.destroy()
|
|
@domElementPool.clear()
|
|
|
|
@verticalScrollbarComponent.destroy()
|
|
@horizontalScrollbarComponent.destroy()
|
|
|
|
@onVerticalScroll = null
|
|
@onHorizontalScroll = null
|
|
|
|
getDomNode: ->
|
|
@domNode
|
|
|
|
updateSync: ->
|
|
@updateSyncPreMeasurement()
|
|
|
|
@oldState ?= {}
|
|
@newState = @presenter.getPostMeasurementState()
|
|
|
|
if @editor.getLastSelection()? and not @editor.getLastSelection().isEmpty()
|
|
@domNode.classList.add('has-selection')
|
|
else
|
|
@domNode.classList.remove('has-selection')
|
|
|
|
if @newState.focused isnt @oldState.focused
|
|
@domNode.classList.toggle('is-focused', @newState.focused)
|
|
|
|
@performedInitialMeasurement = false if @editor.isDestroyed()
|
|
|
|
if @performedInitialMeasurement
|
|
if @newState.height isnt @oldState.height
|
|
if @newState.height?
|
|
@domNode.style.height = @newState.height + 'px'
|
|
else
|
|
@domNode.style.height = ''
|
|
|
|
if @newState.gutters.length
|
|
@mountGutterContainerComponent() unless @gutterContainerComponent?
|
|
@gutterContainerComponent.updateSync(@newState)
|
|
else
|
|
@gutterContainerComponent?.getDomNode()?.remove()
|
|
@gutterContainerComponent = null
|
|
|
|
@hiddenInputComponent.updateSync(@newState)
|
|
@linesComponent.updateSync(@newState)
|
|
@blockDecorationsComponent?.updateSync(@newState)
|
|
@horizontalScrollbarComponent.updateSync(@newState)
|
|
@verticalScrollbarComponent.updateSync(@newState)
|
|
@scrollbarCornerComponent.updateSync(@newState)
|
|
|
|
@overlayManager?.render(@newState)
|
|
|
|
if @clearPoolAfterUpdate
|
|
@domElementPool.clear()
|
|
@clearPoolAfterUpdate = false
|
|
|
|
if @editor.isAlive()
|
|
@updateParentViewFocusedClassIfNeeded()
|
|
@updateParentViewMiniClass()
|
|
|
|
updateSyncPreMeasurement: ->
|
|
@linesComponent.updateSync(@presenter.getPreMeasurementState())
|
|
|
|
readAfterUpdateSync: =>
|
|
@overlayManager?.measureOverlays()
|
|
@blockDecorationsComponent?.measureBlockDecorations() if @isVisible()
|
|
|
|
mountGutterContainerComponent: ->
|
|
@gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool, @views})
|
|
@domNode.insertBefore(@gutterContainerComponent.getDomNode(), @domNode.firstChild)
|
|
|
|
becameVisible: ->
|
|
@updatesPaused = true
|
|
@measureScrollbars() if @measureScrollbarsWhenShown
|
|
@sampleFontStyling()
|
|
@sampleBackgroundColors()
|
|
@measureWindowSize()
|
|
@measureDimensions()
|
|
@measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown
|
|
@editor.setVisible(true)
|
|
@performedInitialMeasurement = true
|
|
@updatesPaused = false
|
|
@updateSync() if @canUpdate()
|
|
|
|
requestUpdate: =>
|
|
return unless @canUpdate()
|
|
|
|
if @updatesPaused
|
|
@updateRequestedWhilePaused = true
|
|
return
|
|
|
|
if @hostElement.isUpdatedSynchronously()
|
|
@updateSync()
|
|
else unless @updateRequested
|
|
@updateRequested = true
|
|
@views.updateDocument =>
|
|
@updateRequested = false
|
|
@updateSync() if @canUpdate()
|
|
@views.readDocument(@readAfterUpdateSync)
|
|
|
|
canUpdate: ->
|
|
@mounted and @editor.isAlive()
|
|
|
|
requestAnimationFrame: (fn) ->
|
|
@updatesPaused = true
|
|
requestAnimationFrame =>
|
|
fn()
|
|
@updatesPaused = false
|
|
if @updateRequestedWhilePaused and @canUpdate()
|
|
@updateRequestedWhilePaused = false
|
|
@requestUpdate()
|
|
|
|
getTopmostDOMNode: ->
|
|
@hostElement
|
|
|
|
observeEditor: ->
|
|
@disposables.add @editor.observeGrammar(@onGrammarChanged)
|
|
|
|
listenForDOMEvents: ->
|
|
@domNode.addEventListener 'mousewheel', @onMouseWheel
|
|
@domNode.addEventListener 'textInput', @onTextInput
|
|
@scrollViewNode.addEventListener 'mousedown', @onMouseDown
|
|
@scrollViewNode.addEventListener 'scroll', @onScrollViewScroll
|
|
|
|
@detectAccentedCharacterMenu()
|
|
@listenForIMEEvents()
|
|
@trackSelectionClipboard() if process.platform is 'linux'
|
|
|
|
detectAccentedCharacterMenu: ->
|
|
# We need to get clever to detect when the accented character menu is
|
|
# opened on OS X. Usually, every keydown event that could cause input is
|
|
# followed by a corresponding keypress. However, pressing and holding
|
|
# long enough to open the accented character menu causes additional keydown
|
|
# events to fire that aren't followed by their own keypress and textInput
|
|
# events.
|
|
#
|
|
# Therefore, we assume the accented character menu has been deployed if,
|
|
# before observing any keyup event, we observe events in the following
|
|
# sequence:
|
|
#
|
|
# keydown(keyCode: X), keypress, keydown(keyCode: X)
|
|
#
|
|
# The keyCode X must be the same in the keydown events that bracket the
|
|
# keypress, meaning we're *holding* the _same_ key we intially pressed.
|
|
# Got that?
|
|
lastKeydown = null
|
|
lastKeydownBeforeKeypress = null
|
|
|
|
@domNode.addEventListener 'keydown', (event) =>
|
|
if lastKeydownBeforeKeypress
|
|
if lastKeydownBeforeKeypress.keyCode is event.keyCode
|
|
@openedAccentedCharacterMenu = true
|
|
lastKeydownBeforeKeypress = null
|
|
else
|
|
lastKeydown = event
|
|
|
|
@domNode.addEventListener 'keypress', =>
|
|
lastKeydownBeforeKeypress = lastKeydown
|
|
lastKeydown = null
|
|
|
|
# This cancels the accented character behavior if we type a key normally
|
|
# with the menu open.
|
|
@openedAccentedCharacterMenu = false
|
|
|
|
@domNode.addEventListener 'keyup', ->
|
|
lastKeydownBeforeKeypress = null
|
|
lastKeydown = null
|
|
|
|
listenForIMEEvents: ->
|
|
# The IME composition events work like this:
|
|
#
|
|
# User types 's', chromium pops up the completion helper
|
|
# 1. compositionstart fired
|
|
# 2. compositionupdate fired; event.data == 's'
|
|
# User hits arrow keys to move around in completion helper
|
|
# 3. compositionupdate fired; event.data == 's' for each arry key press
|
|
# User escape to cancel
|
|
# 4. compositionend fired
|
|
# OR User chooses a completion
|
|
# 4. compositionend fired
|
|
# 5. textInput fired; event.data == the completion string
|
|
|
|
checkpoint = null
|
|
@domNode.addEventListener 'compositionstart', =>
|
|
if @openedAccentedCharacterMenu
|
|
@editor.selectLeft()
|
|
@openedAccentedCharacterMenu = false
|
|
checkpoint = @editor.createCheckpoint()
|
|
@domNode.addEventListener 'compositionupdate', (event) =>
|
|
@editor.insertText(event.data, select: true)
|
|
@domNode.addEventListener 'compositionend', (event) =>
|
|
@editor.revertToCheckpoint(checkpoint)
|
|
event.target.value = ''
|
|
|
|
# Listen for selection changes and store the currently selected text
|
|
# in the selection clipboard. This is only applicable on Linux.
|
|
trackSelectionClipboard: ->
|
|
timeoutId = null
|
|
writeSelectedTextToSelectionClipboard = =>
|
|
return if @editor.isDestroyed()
|
|
if selectedText = @editor.getSelectedText()
|
|
# This uses ipcRenderer.send instead of clipboard.writeText because
|
|
# clipboard.writeText is a sync ipcRenderer call on Linux and that
|
|
# will slow down selections.
|
|
ipcRenderer.send('write-text-to-selection-clipboard', selectedText)
|
|
@disposables.add @editor.onDidChangeSelectionRange ->
|
|
clearTimeout(timeoutId)
|
|
timeoutId = setTimeout(writeSelectedTextToSelectionClipboard)
|
|
|
|
observeConfig: ->
|
|
@disposables.add @config.onDidChange 'editor.fontSize', =>
|
|
@sampleFontStyling()
|
|
@invalidateMeasurements()
|
|
@disposables.add @config.onDidChange 'editor.fontFamily', =>
|
|
@sampleFontStyling()
|
|
@invalidateMeasurements()
|
|
@disposables.add @config.onDidChange 'editor.lineHeight', =>
|
|
@sampleFontStyling()
|
|
@invalidateMeasurements()
|
|
|
|
onGrammarChanged: =>
|
|
if @scopedConfigDisposables?
|
|
@scopedConfigDisposables.dispose()
|
|
@disposables.remove(@scopedConfigDisposables)
|
|
|
|
@scopedConfigDisposables = new CompositeDisposable
|
|
@disposables.add(@scopedConfigDisposables)
|
|
|
|
scope = @editor.getRootScopeDescriptor()
|
|
@scopedConfigDisposables.add @config.observe 'editor.scrollSensitivity', {scope}, @setScrollSensitivity
|
|
|
|
focused: ->
|
|
if @mounted
|
|
@presenter.setFocused(true)
|
|
@hiddenInputComponent.getDomNode().focus()
|
|
|
|
blurred: ->
|
|
if @mounted
|
|
@presenter.setFocused(false)
|
|
|
|
onTextInput: (event) =>
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
|
|
return unless @isInputEnabled()
|
|
|
|
# Workaround of the accented character suggestion feature in OS X.
|
|
# This will only occur when the user is not composing in IME mode.
|
|
# When the user selects a modified character from the OSX menu, `textInput`
|
|
# will occur twice, once for the initial character, and once for the
|
|
# modified character. However, only a single keypress will have fired. If
|
|
# this is the case, select backward to replace the original character.
|
|
if @openedAccentedCharacterMenu
|
|
@editor.selectLeft()
|
|
@openedAccentedCharacterMenu = false
|
|
|
|
@editor.insertText(event.data, groupUndo: true)
|
|
|
|
onVerticalScroll: (scrollTop) =>
|
|
return if @updateRequested or scrollTop is @presenter.getScrollTop()
|
|
|
|
animationFramePending = @pendingScrollTop?
|
|
@pendingScrollTop = scrollTop
|
|
unless animationFramePending
|
|
@requestAnimationFrame =>
|
|
pendingScrollTop = @pendingScrollTop
|
|
@pendingScrollTop = null
|
|
@presenter.setScrollTop(pendingScrollTop)
|
|
@presenter.commitPendingScrollTopPosition()
|
|
|
|
onHorizontalScroll: (scrollLeft) =>
|
|
return if @updateRequested or scrollLeft is @presenter.getScrollLeft()
|
|
|
|
animationFramePending = @pendingScrollLeft?
|
|
@pendingScrollLeft = scrollLeft
|
|
unless animationFramePending
|
|
@requestAnimationFrame =>
|
|
@presenter.setScrollLeft(@pendingScrollLeft)
|
|
@presenter.commitPendingScrollLeftPosition()
|
|
@pendingScrollLeft = null
|
|
|
|
onMouseWheel: (event) =>
|
|
# Only scroll in one direction at a time
|
|
{wheelDeltaX, wheelDeltaY} = event
|
|
|
|
# Ctrl+MouseWheel adjusts font size.
|
|
if event.ctrlKey and @config.get('editor.zoomFontWhenCtrlScrolling')
|
|
if wheelDeltaY > 0
|
|
@workspace.increaseFontSize()
|
|
else if wheelDeltaY < 0
|
|
@workspace.decreaseFontSize()
|
|
event.preventDefault()
|
|
return
|
|
|
|
if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)
|
|
# Scrolling horizontally
|
|
previousScrollLeft = @presenter.getScrollLeft()
|
|
updatedScrollLeft = previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity)
|
|
|
|
event.preventDefault() if @presenter.canScrollLeftTo(updatedScrollLeft)
|
|
@presenter.setScrollLeft(updatedScrollLeft)
|
|
else
|
|
# Scrolling vertically
|
|
@presenter.setMouseWheelScreenRow(@screenRowForNode(event.target))
|
|
previousScrollTop = @presenter.getScrollTop()
|
|
updatedScrollTop = previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity)
|
|
|
|
event.preventDefault() if @presenter.canScrollTopTo(updatedScrollTop)
|
|
@presenter.setScrollTop(updatedScrollTop)
|
|
|
|
onScrollViewScroll: =>
|
|
if @mounted
|
|
console.warn "TextEditorScrollView scrolled when it shouldn't have."
|
|
@scrollViewNode.scrollTop = 0
|
|
@scrollViewNode.scrollLeft = 0
|
|
|
|
onDidChangeScrollTop: (callback) ->
|
|
@presenter.onDidChangeScrollTop(callback)
|
|
|
|
onDidChangeScrollLeft: (callback) ->
|
|
@presenter.onDidChangeScrollLeft(callback)
|
|
|
|
setScrollLeft: (scrollLeft) ->
|
|
@presenter.setScrollLeft(scrollLeft)
|
|
|
|
setScrollRight: (scrollRight) ->
|
|
@presenter.setScrollRight(scrollRight)
|
|
|
|
setScrollTop: (scrollTop) ->
|
|
@presenter.setScrollTop(scrollTop)
|
|
|
|
setScrollBottom: (scrollBottom) ->
|
|
@presenter.setScrollBottom(scrollBottom)
|
|
|
|
getScrollTop: ->
|
|
@presenter.getScrollTop()
|
|
|
|
getScrollLeft: ->
|
|
@presenter.getScrollLeft()
|
|
|
|
getScrollRight: ->
|
|
@presenter.getScrollRight()
|
|
|
|
getScrollBottom: ->
|
|
@presenter.getScrollBottom()
|
|
|
|
getScrollHeight: ->
|
|
@presenter.getScrollHeight()
|
|
|
|
getScrollWidth: ->
|
|
@presenter.getScrollWidth()
|
|
|
|
getMaxScrollTop: ->
|
|
@presenter.getMaxScrollTop()
|
|
|
|
getVerticalScrollbarWidth: ->
|
|
@presenter.getVerticalScrollbarWidth()
|
|
|
|
getHorizontalScrollbarHeight: ->
|
|
@presenter.getHorizontalScrollbarHeight()
|
|
|
|
getVisibleRowRange: ->
|
|
@presenter.getVisibleRowRange()
|
|
|
|
pixelPositionForScreenPosition: (screenPosition, clip=true) ->
|
|
screenPosition = Point.fromObject(screenPosition)
|
|
screenPosition = @editor.clipScreenPosition(screenPosition) if clip
|
|
|
|
unless @presenter.isRowRendered(screenPosition.row)
|
|
@presenter.setScreenRowsToMeasure([screenPosition.row])
|
|
|
|
unless @linesComponent.lineNodeForScreenRow(screenPosition.row)?
|
|
@updateSyncPreMeasurement()
|
|
|
|
pixelPosition = @linesYardstick.pixelPositionForScreenPosition(screenPosition)
|
|
@presenter.clearScreenRowsToMeasure()
|
|
pixelPosition
|
|
|
|
screenPositionForPixelPosition: (pixelPosition) ->
|
|
row = @linesYardstick.measuredRowForPixelPosition(pixelPosition)
|
|
if row? and not @presenter.isRowRendered(row)
|
|
@presenter.setScreenRowsToMeasure([row])
|
|
@updateSyncPreMeasurement()
|
|
|
|
position = @linesYardstick.screenPositionForPixelPosition(pixelPosition)
|
|
@presenter.clearScreenRowsToMeasure()
|
|
position
|
|
|
|
pixelRectForScreenRange: (screenRange) ->
|
|
rowsToMeasure = []
|
|
unless @presenter.isRowRendered(screenRange.start.row)
|
|
rowsToMeasure.push(screenRange.start.row)
|
|
unless @presenter.isRowRendered(screenRange.end.row)
|
|
rowsToMeasure.push(screenRange.end.row)
|
|
|
|
if rowsToMeasure.length > 0
|
|
@presenter.setScreenRowsToMeasure(rowsToMeasure)
|
|
@updateSyncPreMeasurement()
|
|
|
|
rect = @presenter.absolutePixelRectForScreenRange(screenRange)
|
|
|
|
if rowsToMeasure.length > 0
|
|
@presenter.clearScreenRowsToMeasure()
|
|
|
|
rect
|
|
|
|
pixelRangeForScreenRange: (screenRange, clip=true) ->
|
|
{start, end} = Range.fromObject(screenRange)
|
|
{start: @pixelPositionForScreenPosition(start, clip), end: @pixelPositionForScreenPosition(end, clip)}
|
|
|
|
pixelPositionForBufferPosition: (bufferPosition) ->
|
|
@pixelPositionForScreenPosition(
|
|
@editor.screenPositionForBufferPosition(bufferPosition)
|
|
)
|
|
|
|
invalidateBlockDecorationDimensions: ->
|
|
@presenter.invalidateBlockDecorationDimensions(arguments...)
|
|
|
|
onMouseDown: (event) =>
|
|
unless event.button is 0 or (event.button is 1 and process.platform is 'linux')
|
|
# Only handle mouse down events for left mouse button on all platforms
|
|
# and middle mouse button on Linux since it pastes the selection clipboard
|
|
return
|
|
|
|
return if event.target?.classList.contains('horizontal-scrollbar')
|
|
|
|
{detail, shiftKey, metaKey, ctrlKey} = event
|
|
|
|
# CTRL+click brings up the context menu on OSX, so don't handle those either
|
|
return if ctrlKey and process.platform is 'darwin'
|
|
|
|
# Prevent focusout event on hidden input if editor is already focused
|
|
event.preventDefault() if @oldState.focused
|
|
|
|
screenPosition = @screenPositionForMouseEvent(event)
|
|
|
|
if event.target?.classList.contains('fold-marker')
|
|
bufferPosition = @editor.bufferPositionForScreenPosition(screenPosition)
|
|
@editor.destroyFoldsIntersectingBufferRange([bufferPosition, bufferPosition])
|
|
return
|
|
|
|
switch detail
|
|
when 1
|
|
if shiftKey
|
|
@editor.selectToScreenPosition(screenPosition)
|
|
else if metaKey or (ctrlKey and process.platform isnt 'darwin')
|
|
cursorAtScreenPosition = @editor.getCursorAtScreenPosition(screenPosition)
|
|
if cursorAtScreenPosition and @editor.hasMultipleCursors()
|
|
cursorAtScreenPosition.destroy()
|
|
else
|
|
@editor.addCursorAtScreenPosition(screenPosition, autoscroll: false)
|
|
else
|
|
@editor.setCursorScreenPosition(screenPosition, autoscroll: false)
|
|
when 2
|
|
@editor.getLastSelection().selectWord(autoscroll: false)
|
|
when 3
|
|
@editor.getLastSelection().selectLine(null, autoscroll: false)
|
|
|
|
@handleDragUntilMouseUp (screenPosition) =>
|
|
@editor.selectToScreenPosition(screenPosition, suppressSelectionMerge: true, autoscroll: false)
|
|
|
|
onLineNumberGutterMouseDown: (event) =>
|
|
return unless event.button is 0 # only handle the left mouse button
|
|
|
|
{shiftKey, metaKey, ctrlKey} = event
|
|
|
|
if shiftKey
|
|
@onGutterShiftClick(event)
|
|
else if metaKey or (ctrlKey and process.platform isnt 'darwin')
|
|
@onGutterMetaClick(event)
|
|
else
|
|
@onGutterClick(event)
|
|
|
|
onGutterClick: (event) =>
|
|
clickedScreenRow = @screenPositionForMouseEvent(event).row
|
|
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
|
|
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
|
|
@editor.setSelectedScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
|
|
@handleGutterDrag(initialScreenRange)
|
|
|
|
onGutterMetaClick: (event) =>
|
|
clickedScreenRow = @screenPositionForMouseEvent(event).row
|
|
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
|
|
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
|
|
@editor.addSelectionForScreenRange(initialScreenRange, autoscroll: false)
|
|
@handleGutterDrag(initialScreenRange)
|
|
|
|
onGutterShiftClick: (event) =>
|
|
tailScreenPosition = @editor.getLastSelection().getTailScreenPosition()
|
|
clickedScreenRow = @screenPositionForMouseEvent(event).row
|
|
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
|
|
clickedLineScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
|
|
|
|
if clickedScreenRow < tailScreenPosition.row
|
|
@editor.selectToScreenPosition(clickedLineScreenRange.start, suppressSelectionMerge: true, autoscroll: false)
|
|
else
|
|
@editor.selectToScreenPosition(clickedLineScreenRange.end, suppressSelectionMerge: true, autoscroll: false)
|
|
|
|
@handleGutterDrag(new Range(tailScreenPosition, tailScreenPosition))
|
|
|
|
handleGutterDrag: (initialRange) ->
|
|
@handleDragUntilMouseUp (screenPosition) =>
|
|
dragRow = screenPosition.row
|
|
if dragRow < initialRange.start.row
|
|
startPosition = @editor.clipScreenPosition([dragRow, 0], skipSoftWrapIndentation: true)
|
|
screenRange = new Range(startPosition, startPosition).union(initialRange)
|
|
@editor.getLastSelection().setScreenRange(screenRange, reversed: true, autoscroll: false, preserveFolds: true)
|
|
else
|
|
endPosition = [dragRow + 1, 0]
|
|
screenRange = new Range(endPosition, endPosition).union(initialRange)
|
|
@editor.getLastSelection().setScreenRange(screenRange, reversed: false, autoscroll: false, preserveFolds: true)
|
|
|
|
onStylesheetsChanged: (styleElement) =>
|
|
return unless @performedInitialMeasurement
|
|
return unless @themes.isInitialLoadComplete()
|
|
|
|
# This delay prevents the styling from going haywire when stylesheets are
|
|
# reloaded in dev mode. It seems like a workaround for a browser bug, but
|
|
# not totally sure.
|
|
|
|
unless @stylingChangeAnimationFrameRequested
|
|
@stylingChangeAnimationFrameRequested = true
|
|
requestAnimationFrame =>
|
|
@stylingChangeAnimationFrameRequested = false
|
|
if @mounted
|
|
@refreshScrollbars() if not styleElement.sheet? or @containsScrollbarSelector(styleElement.sheet)
|
|
@handleStylingChange()
|
|
|
|
onAllThemesLoaded: =>
|
|
@refreshScrollbars()
|
|
@handleStylingChange()
|
|
|
|
handleStylingChange: =>
|
|
@sampleFontStyling()
|
|
@sampleBackgroundColors()
|
|
@invalidateMeasurements()
|
|
|
|
handleDragUntilMouseUp: (dragHandler) ->
|
|
dragging = false
|
|
lastMousePosition = {}
|
|
animationLoop = =>
|
|
@requestAnimationFrame =>
|
|
if dragging and @mounted
|
|
linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
|
|
autoscroll(lastMousePosition, linesClientRect)
|
|
screenPosition = @screenPositionForMouseEvent(lastMousePosition, linesClientRect)
|
|
dragHandler(screenPosition)
|
|
animationLoop()
|
|
else if not @mounted
|
|
stopDragging()
|
|
|
|
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 = (event) =>
|
|
if dragging
|
|
stopDragging()
|
|
@editor.finalizeSelections()
|
|
@editor.mergeIntersectingSelections()
|
|
pasteSelectionClipboard(event)
|
|
|
|
stopDragging = ->
|
|
dragging = false
|
|
window.removeEventListener('mousemove', onMouseMove)
|
|
window.removeEventListener('mouseup', onMouseUp)
|
|
disposables.dispose()
|
|
|
|
autoscroll = (mouseClientPosition) =>
|
|
{top, bottom, left, right} = @scrollViewNode.getBoundingClientRect()
|
|
top += 30
|
|
bottom -= 30
|
|
left += 30
|
|
right -= 30
|
|
|
|
if mouseClientPosition.clientY < top
|
|
mouseYDelta = top - mouseClientPosition.clientY
|
|
yDirection = -1
|
|
else if mouseClientPosition.clientY > bottom
|
|
mouseYDelta = mouseClientPosition.clientY - bottom
|
|
yDirection = 1
|
|
|
|
if mouseClientPosition.clientX < left
|
|
mouseXDelta = left - mouseClientPosition.clientX
|
|
xDirection = -1
|
|
else if mouseClientPosition.clientX > right
|
|
mouseXDelta = mouseClientPosition.clientX - right
|
|
xDirection = 1
|
|
|
|
if mouseYDelta?
|
|
@presenter.setScrollTop(@presenter.getScrollTop() + yDirection * scaleScrollDelta(mouseYDelta))
|
|
@presenter.commitPendingScrollTopPosition()
|
|
|
|
if mouseXDelta?
|
|
@presenter.setScrollLeft(@presenter.getScrollLeft() + xDirection * scaleScrollDelta(mouseXDelta))
|
|
@presenter.commitPendingScrollLeftPosition()
|
|
|
|
scaleScrollDelta = (scrollDelta) ->
|
|
Math.pow(scrollDelta / 2, 3) / 280
|
|
|
|
pasteSelectionClipboard = (event) =>
|
|
if event?.which is 2 and process.platform is 'linux'
|
|
if selection = require('./safe-clipboard').readText('selection')
|
|
@editor.insertText(selection)
|
|
|
|
window.addEventListener('mousemove', onMouseMove)
|
|
window.addEventListener('mouseup', onMouseUp)
|
|
disposables = new CompositeDisposable
|
|
disposables.add(@editor.getBuffer().onWillChange(onMouseUp))
|
|
disposables.add(@editor.onDidDestroy(stopDragging))
|
|
|
|
isVisible: ->
|
|
# Investigating an exception that occurs here due to ::domNode being null.
|
|
@assert @domNode?, "TextEditorComponent::domNode was null.", (error) =>
|
|
error.metadata = {@initialized}
|
|
|
|
@domNode? and (@domNode.offsetHeight > 0 or @domNode.offsetWidth > 0)
|
|
|
|
pollDOM: =>
|
|
unless @checkForVisibilityChange()
|
|
@sampleBackgroundColors()
|
|
@measureDimensions()
|
|
@sampleFontStyling()
|
|
@overlayManager?.measureOverlays()
|
|
|
|
checkForVisibilityChange: ->
|
|
if @isVisible()
|
|
if @wasVisible
|
|
false
|
|
else
|
|
@becameVisible()
|
|
@wasVisible = true
|
|
else
|
|
@wasVisible = false
|
|
|
|
# Measure explicitly-styled height and width and relay them to the model. If
|
|
# these values aren't explicitly styled, we assume the editor is unconstrained
|
|
# and use the scrollHeight / scrollWidth as its height and width in
|
|
# calculations.
|
|
measureDimensions: ->
|
|
return unless @mounted
|
|
|
|
{position} = getComputedStyle(@hostElement)
|
|
{height} = @hostElement.style
|
|
|
|
if position is 'absolute' or height
|
|
@presenter.setAutoHeight(false)
|
|
height = @hostElement.offsetHeight
|
|
if height > 0
|
|
@presenter.setExplicitHeight(height)
|
|
else
|
|
@presenter.setAutoHeight(true)
|
|
@presenter.setExplicitHeight(null)
|
|
|
|
clientWidth = @scrollViewNode.clientWidth
|
|
paddingLeft = parseInt(getComputedStyle(@scrollViewNode).paddingLeft)
|
|
clientWidth -= paddingLeft
|
|
if clientWidth > 0
|
|
@presenter.setContentFrameWidth(clientWidth)
|
|
|
|
@presenter.setGutterWidth(@gutterContainerComponent?.getDomNode().offsetWidth ? 0)
|
|
@presenter.setBoundingClientRect(@hostElement.getBoundingClientRect())
|
|
|
|
measureWindowSize: ->
|
|
return unless @mounted
|
|
|
|
# FIXME: on Ubuntu (via xvfb) `window.innerWidth` reports an incorrect value
|
|
# when window gets resized through `atom.setWindowDimensions({width:
|
|
# windowWidth, height: windowHeight})`.
|
|
@presenter.setWindowSize(window.innerWidth, window.innerHeight)
|
|
|
|
sampleFontStyling: =>
|
|
oldFontSize = @fontSize
|
|
oldFontFamily = @fontFamily
|
|
oldLineHeight = @lineHeight
|
|
|
|
{@fontSize, @fontFamily, @lineHeight} = getComputedStyle(@getTopmostDOMNode())
|
|
|
|
if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight
|
|
@clearPoolAfterUpdate = true
|
|
@measureLineHeightAndDefaultCharWidth()
|
|
@invalidateMeasurements()
|
|
|
|
sampleBackgroundColors: (suppressUpdate) ->
|
|
{backgroundColor} = getComputedStyle(@hostElement)
|
|
|
|
@presenter.setBackgroundColor(backgroundColor)
|
|
|
|
lineNumberGutter = @gutterContainerComponent?.getLineNumberGutterComponent()
|
|
if lineNumberGutter
|
|
gutterBackgroundColor = getComputedStyle(lineNumberGutter.getDomNode()).backgroundColor
|
|
@presenter.setGutterBackgroundColor(gutterBackgroundColor)
|
|
|
|
measureLineHeightAndDefaultCharWidth: ->
|
|
if @isVisible()
|
|
@measureLineHeightAndDefaultCharWidthWhenShown = false
|
|
@linesComponent.measureLineHeightAndDefaultCharWidth()
|
|
else
|
|
@measureLineHeightAndDefaultCharWidthWhenShown = true
|
|
|
|
measureScrollbars: ->
|
|
@measureScrollbarsWhenShown = false
|
|
|
|
cornerNode = @scrollbarCornerComponent.getDomNode()
|
|
originalDisplayValue = cornerNode.style.display
|
|
|
|
cornerNode.style.display = 'block'
|
|
|
|
width = (cornerNode.offsetWidth - cornerNode.clientWidth) or 15
|
|
height = (cornerNode.offsetHeight - cornerNode.clientHeight) or 15
|
|
|
|
@presenter.setVerticalScrollbarWidth(width)
|
|
@presenter.setHorizontalScrollbarHeight(height)
|
|
|
|
cornerNode.style.display = originalDisplayValue
|
|
|
|
containsScrollbarSelector: (stylesheet) ->
|
|
for rule in stylesheet.cssRules
|
|
if rule.selectorText?.indexOf('scrollbar') > -1
|
|
return true
|
|
false
|
|
|
|
refreshScrollbars: =>
|
|
if @isVisible()
|
|
@measureScrollbarsWhenShown = false
|
|
else
|
|
@measureScrollbarsWhenShown = true
|
|
return
|
|
|
|
verticalNode = @verticalScrollbarComponent.getDomNode()
|
|
horizontalNode = @horizontalScrollbarComponent.getDomNode()
|
|
cornerNode = @scrollbarCornerComponent.getDomNode()
|
|
|
|
originalVerticalDisplayValue = verticalNode.style.display
|
|
originalHorizontalDisplayValue = horizontalNode.style.display
|
|
originalCornerDisplayValue = cornerNode.style.display
|
|
|
|
# First, hide all scrollbars in case they are visible so they take on new
|
|
# styles when they are shown again.
|
|
verticalNode.style.display = 'none'
|
|
horizontalNode.style.display = 'none'
|
|
cornerNode.style.display = 'none'
|
|
|
|
# Force a reflow
|
|
cornerNode.offsetWidth
|
|
|
|
# Now measure the new scrollbar dimensions
|
|
@measureScrollbars()
|
|
|
|
# Now restore the display value for all scrollbars, since they were
|
|
# previously hidden
|
|
verticalNode.style.display = originalVerticalDisplayValue
|
|
horizontalNode.style.display = originalHorizontalDisplayValue
|
|
cornerNode.style.display = originalCornerDisplayValue
|
|
|
|
consolidateSelections: (e) ->
|
|
e.abortKeyBinding() unless @editor.consolidateSelections()
|
|
|
|
lineNodeForScreenRow: (screenRow) ->
|
|
@linesComponent.lineNodeForScreenRow(screenRow)
|
|
|
|
lineNumberNodeForScreenRow: (screenRow) ->
|
|
tileRow = @presenter.tileForRow(screenRow)
|
|
gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent()
|
|
tileComponent = gutterComponent.getComponentForTile(tileRow)
|
|
|
|
tileComponent?.lineNumberNodeForScreenRow(screenRow)
|
|
|
|
tileNodesForLines: ->
|
|
@linesComponent.getTiles()
|
|
|
|
tileNodesForLineNumbers: ->
|
|
gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent()
|
|
gutterComponent.getTiles()
|
|
|
|
screenRowForNode: (node) ->
|
|
while node?
|
|
if screenRow = node.dataset.screenRow
|
|
return parseInt(screenRow)
|
|
node = node.parentElement
|
|
null
|
|
|
|
getFontSize: ->
|
|
parseInt(getComputedStyle(@getTopmostDOMNode()).fontSize)
|
|
|
|
setFontSize: (fontSize) ->
|
|
@getTopmostDOMNode().style.fontSize = fontSize + 'px'
|
|
@sampleFontStyling()
|
|
@invalidateMeasurements()
|
|
|
|
getFontFamily: ->
|
|
getComputedStyle(@getTopmostDOMNode()).fontFamily
|
|
|
|
setFontFamily: (fontFamily) ->
|
|
@getTopmostDOMNode().style.fontFamily = fontFamily
|
|
@sampleFontStyling()
|
|
@invalidateMeasurements()
|
|
|
|
setLineHeight: (lineHeight) ->
|
|
@getTopmostDOMNode().style.lineHeight = lineHeight
|
|
@sampleFontStyling()
|
|
@invalidateMeasurements()
|
|
|
|
invalidateMeasurements: ->
|
|
@linesYardstick.invalidateCache()
|
|
@presenter.measurementsChanged()
|
|
|
|
setShowIndentGuide: (showIndentGuide) ->
|
|
@config.set("editor.showIndentGuide", showIndentGuide)
|
|
|
|
setScrollSensitivity: (scrollSensitivity) =>
|
|
if scrollSensitivity = parseInt(scrollSensitivity)
|
|
@scrollSensitivity = Math.abs(scrollSensitivity) / 100
|
|
|
|
screenPositionForMouseEvent: (event, linesClientRect) ->
|
|
pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect)
|
|
@screenPositionForPixelPosition(pixelPosition)
|
|
|
|
pixelPositionForMouseEvent: (event, linesClientRect) ->
|
|
{clientX, clientY} = event
|
|
|
|
linesClientRect ?= @linesComponent.getDomNode().getBoundingClientRect()
|
|
top = clientY - linesClientRect.top + @presenter.getRealScrollTop()
|
|
left = clientX - linesClientRect.left + @presenter.getRealScrollLeft()
|
|
bottom = linesClientRect.top + @presenter.getRealScrollTop() + linesClientRect.height - clientY
|
|
right = linesClientRect.left + @presenter.getRealScrollLeft() + linesClientRect.width - clientX
|
|
|
|
{top, left, bottom, right}
|
|
|
|
getGutterWidth: ->
|
|
@presenter.getGutterWidth()
|
|
|
|
getModel: ->
|
|
@editor
|
|
|
|
isInputEnabled: -> @inputEnabled
|
|
|
|
setInputEnabled: (@inputEnabled) -> @inputEnabled
|
|
|
|
setContinuousReflow: (continuousReflow) ->
|
|
@presenter.setContinuousReflow(continuousReflow)
|
|
|
|
updateParentViewFocusedClassIfNeeded: ->
|
|
if @oldState.focused isnt @newState.focused
|
|
@hostElement.classList.toggle('is-focused', @newState.focused)
|
|
@rootElement.classList.toggle('is-focused', @newState.focused)
|
|
@oldState.focused = @newState.focused
|
|
|
|
updateParentViewMiniClass: ->
|
|
@hostElement.classList.toggle('mini', @editor.isMini())
|
|
@rootElement.classList.toggle('mini', @editor.isMini())
|