mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
Merge remote-tracking branch 'origin/master' into cj-add-deprecation-warning-to-specs
This commit is contained in:
@@ -157,6 +157,9 @@ class Atom extends Model
|
||||
# Still set NODE_PATH since tasks may need it.
|
||||
process.env.NODE_PATH = exportsPath
|
||||
|
||||
# Make react.js faster
|
||||
process.env.NODE_ENV ?= 'production'
|
||||
|
||||
@config = new Config({configDirPath, resourcePath})
|
||||
@keymaps = new KeymapManager({configDirPath, resourcePath})
|
||||
@keymap = @keymaps # Deprecated
|
||||
|
||||
@@ -311,6 +311,7 @@ class AtomApplication
|
||||
if existingWindow
|
||||
openedWindow = existingWindow
|
||||
openedWindow.openPath(pathToOpen, initialLine)
|
||||
openedWindow.restore()
|
||||
else
|
||||
if devMode
|
||||
try
|
||||
|
||||
@@ -151,6 +151,8 @@ class AtomWindow
|
||||
|
||||
maximize: -> @browserWindow.maximize()
|
||||
|
||||
restore: -> @browserWindow.restore()
|
||||
|
||||
handlesAtomCommands: ->
|
||||
not @isSpecWindow() and @isWebViewFocused()
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ CoffeeScript = require 'coffee-script'
|
||||
CSON = require 'season'
|
||||
fs = require 'fs-plus'
|
||||
|
||||
cacheDir = path.join(fs.getHomeDirectory(), 'compile-cache')
|
||||
cacheDir = path.join(fs.absolute('~/.atom'), 'compile-cache')
|
||||
coffeeCacheDir = path.join(cacheDir, 'coffee')
|
||||
CSON.setCacheDir(path.join(cacheDir, 'cson'))
|
||||
|
||||
|
||||
13
src/cursor-component.coffee
Normal file
13
src/cursor-component.coffee
Normal file
@@ -0,0 +1,13 @@
|
||||
React = require 'react'
|
||||
{div} = require 'reactionary'
|
||||
|
||||
module.exports =
|
||||
CursorComponent = React.createClass
|
||||
displayName: 'CursorComponent'
|
||||
|
||||
render: ->
|
||||
{top, left, height, width} = @props.cursor.getPixelRect()
|
||||
className = 'cursor'
|
||||
className += ' blink-off' if @props.blinkOff
|
||||
|
||||
div className: className, style: {top, left, height, width}
|
||||
@@ -1,5 +1,5 @@
|
||||
{Point, Range} = require 'text-buffer'
|
||||
{Emitter} = require 'emissary'
|
||||
{Model} = require 'theorist'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
# Public: The `Cursor` class represents the little blinking line identifying
|
||||
@@ -8,9 +8,7 @@ _ = require 'underscore-plus'
|
||||
# Cursors belong to {Editor}s and have some metadata attached in the form
|
||||
# of a {Marker}.
|
||||
module.exports =
|
||||
class Cursor
|
||||
Emitter.includeInto(this)
|
||||
|
||||
class Cursor extends Model
|
||||
screenPosition: null
|
||||
bufferPosition: null
|
||||
goalColumn: null
|
||||
@@ -18,7 +16,8 @@ class Cursor
|
||||
needsAutoscroll: null
|
||||
|
||||
# Instantiated by an {Editor}
|
||||
constructor: ({@editor, @marker}) ->
|
||||
constructor: ({@editor, @marker, id}) ->
|
||||
@assignId(id)
|
||||
@updateVisibility()
|
||||
@marker.on 'changed', (e) =>
|
||||
@updateVisibility()
|
||||
@@ -27,7 +26,12 @@ class Cursor
|
||||
{textChanged} = e
|
||||
return if oldHeadScreenPosition.isEqual(newHeadScreenPosition)
|
||||
|
||||
# Supports old editor view
|
||||
@needsAutoscroll ?= @isLastCursor() and !textChanged
|
||||
|
||||
# Supports react editor view
|
||||
@autoscroll() if @needsAutoscroll and @editor.manageScrollPosition
|
||||
|
||||
@goalColumn = null
|
||||
|
||||
movedEvent =
|
||||
@@ -38,7 +42,7 @@ class Cursor
|
||||
textChanged: textChanged
|
||||
|
||||
@emit 'moved', movedEvent
|
||||
@editor.emit 'cursor-moved', movedEvent
|
||||
@editor.cursorMoved(movedEvent)
|
||||
@marker.on 'destroyed', =>
|
||||
@destroyed = true
|
||||
@editor.removeCursor(this)
|
||||
@@ -54,6 +58,9 @@ class Cursor
|
||||
unless fn()
|
||||
@emit 'autoscrolled' if @needsAutoscroll
|
||||
|
||||
getPixelRect: ->
|
||||
@editor.pixelRectForScreenRange(@getScreenRange())
|
||||
|
||||
# Public: Moves a cursor to a given screen position.
|
||||
#
|
||||
# screenPosition - An {Array} of two numbers: the screen row, and the screen
|
||||
@@ -69,6 +76,10 @@ class Cursor
|
||||
getScreenPosition: ->
|
||||
@marker.getHeadScreenPosition()
|
||||
|
||||
getScreenRange: ->
|
||||
{row, column} = @getScreenPosition()
|
||||
new Range(new Point(row, column), new Point(row, column + 1))
|
||||
|
||||
# Public: Moves a cursor to a given buffer position.
|
||||
#
|
||||
# bufferPosition - An {Array} of two numbers: the buffer row, and the buffer
|
||||
@@ -84,6 +95,9 @@ class Cursor
|
||||
getBufferPosition: ->
|
||||
@marker.getHeadBufferPosition()
|
||||
|
||||
autoscroll: ->
|
||||
@editor.scrollToScreenRange(@getScreenRange())
|
||||
|
||||
# Public: If the marker range is empty, the cursor is marked as being visible.
|
||||
updateVisibility: ->
|
||||
@setVisible(@marker.getBufferRange().isEmpty())
|
||||
|
||||
50
src/cursors-component.coffee
Normal file
50
src/cursors-component.coffee
Normal file
@@ -0,0 +1,50 @@
|
||||
React = require 'react'
|
||||
{div} = require 'reactionary'
|
||||
{debounce} = require 'underscore-plus'
|
||||
SubscriberMixin = require './subscriber-mixin'
|
||||
CursorComponent = require './cursor-component'
|
||||
|
||||
|
||||
module.exports =
|
||||
CursorsComponent = React.createClass
|
||||
displayName: 'CursorsComponent'
|
||||
mixins: [SubscriberMixin]
|
||||
|
||||
cursorBlinkIntervalHandle: null
|
||||
|
||||
render: ->
|
||||
{editor} = @props
|
||||
blinkOff = @state.blinkCursorsOff
|
||||
|
||||
div className: 'cursors',
|
||||
if @isMounted()
|
||||
for selection in editor.getSelections()
|
||||
if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)
|
||||
{cursor} = selection
|
||||
CursorComponent({key: cursor.id, cursor, blinkOff})
|
||||
|
||||
getInitialState: ->
|
||||
blinkCursorsOff: false
|
||||
|
||||
componentDidMount: ->
|
||||
{editor} = @props
|
||||
@startBlinkingCursors()
|
||||
|
||||
componentWillUnmount: ->
|
||||
clearInterval(@cursorBlinkIntervalHandle)
|
||||
|
||||
componentWillUpdate: ({cursorsMoved}) ->
|
||||
@pauseCursorBlinking() if cursorsMoved
|
||||
|
||||
startBlinkingCursors: ->
|
||||
@cursorBlinkIntervalHandle = setInterval(@toggleCursorBlink, @props.cursorBlinkPeriod / 2)
|
||||
|
||||
startBlinkingCursorsAfterDelay: null # Created lazily
|
||||
|
||||
toggleCursorBlink: -> @setState(blinkCursorsOff: not @state.blinkCursorsOff)
|
||||
|
||||
pauseCursorBlinking: ->
|
||||
@state.blinkCursorsOff = false
|
||||
clearInterval(@cursorBlinkIntervalHandle)
|
||||
@startBlinkingCursorsAfterDelay ?= debounce(@startBlinkingCursors, @props.cursorBlinkResumeDelay)
|
||||
@startBlinkingCursorsAfterDelay()
|
||||
15
src/custom-event-mixin.coffee
Normal file
15
src/custom-event-mixin.coffee
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports =
|
||||
CustomEventMixin =
|
||||
componentWillMount: ->
|
||||
@customEventListeners = {}
|
||||
|
||||
componentWillUnmount: ->
|
||||
for name, listeners in @customEventListeners
|
||||
for listener in listeners
|
||||
@getDOMNode().removeEventListener(name, listener)
|
||||
|
||||
addCustomEventListeners: (customEventListeners) ->
|
||||
for name, listener of customEventListeners
|
||||
@customEventListeners[name] ?= []
|
||||
@customEventListeners[name].push(listener)
|
||||
@getDOMNode().addEventListener(name, listener)
|
||||
@@ -54,6 +54,9 @@ class DisplayBufferMarker
|
||||
setBufferRange: (bufferRange, options) ->
|
||||
@bufferMarker.setRange(bufferRange, options)
|
||||
|
||||
getPixelRange: ->
|
||||
@displayBuffer.pixelRangeForScreenRange(@getScreenRange(), false)
|
||||
|
||||
# Retrieves the screen position of the marker's head.
|
||||
#
|
||||
# Returns a {Point}.
|
||||
|
||||
@@ -20,14 +20,25 @@ class DisplayBuffer extends Model
|
||||
Serializable.includeInto(this)
|
||||
|
||||
@properties
|
||||
manageScrollPosition: false
|
||||
softWrap: null
|
||||
editorWidthInChars: null
|
||||
lineHeight: null
|
||||
defaultCharWidth: null
|
||||
height: null
|
||||
width: null
|
||||
scrollTop: 0
|
||||
scrollLeft: 0
|
||||
|
||||
verticalScrollMargin: 2
|
||||
horizontalScrollMargin: 6
|
||||
|
||||
constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer}={}) ->
|
||||
super
|
||||
@softWrap ?= atom.config.get('editor.softWrap') ? false
|
||||
@tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer})
|
||||
@buffer = @tokenizedBuffer.buffer
|
||||
@charWidthsByScope = {}
|
||||
@markers = {}
|
||||
@foldsByMarkerId = {}
|
||||
@updateAllScreenLines()
|
||||
@@ -51,6 +62,8 @@ class DisplayBuffer extends Model
|
||||
id: @id
|
||||
softWrap: @softWrap
|
||||
editorWidthInChars: @editorWidthInChars
|
||||
scrollTop: @scrollTop
|
||||
scrollLeft: @scrollLeft
|
||||
tokenizedBuffer: @tokenizedBuffer.serialize()
|
||||
|
||||
deserializeParams: (params) ->
|
||||
@@ -59,6 +72,9 @@ class DisplayBuffer extends Model
|
||||
|
||||
copy: ->
|
||||
newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength()})
|
||||
newDisplayBuffer.setScrollTop(@getScrollTop())
|
||||
newDisplayBuffer.setScrollLeft(@getScrollLeft())
|
||||
|
||||
for marker in @findMarkers(displayBufferId: @id)
|
||||
marker.copy(displayBufferId: newDisplayBuffer.id)
|
||||
newDisplayBuffer
|
||||
@@ -89,6 +105,151 @@ class DisplayBuffer extends Model
|
||||
# visible - A {Boolean} indicating of the tokenized buffer is shown
|
||||
setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
|
||||
|
||||
getVerticalScrollMargin: -> @verticalScrollMargin
|
||||
setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin
|
||||
|
||||
getHorizontalScrollMargin: -> @horizontalScrollMargin
|
||||
setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin
|
||||
|
||||
getHeight: -> @height ? @getScrollHeight()
|
||||
setHeight: (@height) -> @height
|
||||
|
||||
getWidth: -> @width ? @getScrollWidth()
|
||||
setWidth: (newWidth) ->
|
||||
oldWidth = @width
|
||||
@width = newWidth
|
||||
@updateWrappedScreenLines() if newWidth isnt oldWidth and @softWrap
|
||||
@width
|
||||
|
||||
getScrollTop: -> @scrollTop
|
||||
setScrollTop: (scrollTop) ->
|
||||
if @manageScrollPosition
|
||||
@scrollTop = Math.max(0, Math.min(@getScrollHeight() - @getHeight(), scrollTop))
|
||||
else
|
||||
@scrollTop = scrollTop
|
||||
|
||||
getScrollBottom: -> @scrollTop + @height
|
||||
setScrollBottom: (scrollBottom) ->
|
||||
@setScrollTop(scrollBottom - @height)
|
||||
@getScrollBottom()
|
||||
|
||||
getScrollLeft: -> @scrollLeft
|
||||
setScrollLeft: (scrollLeft) ->
|
||||
if @manageScrollPosition
|
||||
@scrollLeft = Math.max(0, Math.min(@getScrollWidth() - @getWidth(), scrollLeft))
|
||||
else
|
||||
@scrollLeft = scrollLeft
|
||||
|
||||
getScrollRight: -> @scrollLeft + @width
|
||||
setScrollRight: (scrollRight) ->
|
||||
@setScrollLeft(scrollRight - @width)
|
||||
@getScrollRight()
|
||||
|
||||
getLineHeight: -> @lineHeight
|
||||
setLineHeight: (@lineHeight) -> @lineHeight
|
||||
|
||||
getDefaultCharWidth: -> @defaultCharWidth
|
||||
setDefaultCharWidth: (@defaultCharWidth) -> @defaultCharWidth
|
||||
|
||||
getScopedCharWidth: (scopeNames, char) ->
|
||||
@getScopedCharWidths(scopeNames)[char]
|
||||
|
||||
getScopedCharWidths: (scopeNames) ->
|
||||
scope = @charWidthsByScope
|
||||
for scopeName in scopeNames
|
||||
scope[scopeName] ?= {}
|
||||
scope = scope[scopeName]
|
||||
scope.charWidths ?= {}
|
||||
scope.charWidths
|
||||
|
||||
setScopedCharWidth: (scopeNames, char, width) ->
|
||||
@getScopedCharWidths(scopeNames)[char] = width
|
||||
|
||||
setScopedCharWidths: (scopeNames, charWidths) ->
|
||||
_.extend(@getScopedCharWidths(scopeNames), charWidths)
|
||||
|
||||
clearScopedCharWidths: ->
|
||||
@charWidthsByScope = {}
|
||||
|
||||
getScrollHeight: ->
|
||||
unless @getLineHeight() > 0
|
||||
throw new Error("You must assign lineHeight before calling ::getScrollHeight()")
|
||||
|
||||
@getLineCount() * @getLineHeight()
|
||||
|
||||
getScrollWidth: ->
|
||||
@getMaxLineLength() * @getDefaultCharWidth()
|
||||
|
||||
getVisibleRowRange: ->
|
||||
unless @getLineHeight() > 0
|
||||
throw new Error("You must assign a non-zero lineHeight before calling ::getVisibleRowRange()")
|
||||
|
||||
heightInLines = Math.ceil(@getHeight() / @getLineHeight()) + 1
|
||||
startRow = Math.floor(@getScrollTop() / @getLineHeight())
|
||||
endRow = Math.min(@getLineCount(), Math.ceil(startRow + heightInLines))
|
||||
[startRow, endRow]
|
||||
|
||||
intersectsVisibleRowRange: (startRow, endRow) ->
|
||||
[visibleStart, visibleEnd] = @getVisibleRowRange()
|
||||
not (endRow <= visibleStart or visibleEnd <= startRow)
|
||||
|
||||
selectionIntersectsVisibleRowRange: (selection) ->
|
||||
{start, end} = selection.getScreenRange()
|
||||
@intersectsVisibleRowRange(start.row, end.row + 1)
|
||||
|
||||
scrollToScreenRange: (screenRange) ->
|
||||
verticalScrollMarginInPixels = @getVerticalScrollMargin() * @getLineHeight()
|
||||
horizontalScrollMarginInPixels = @getHorizontalScrollMargin() * @getDefaultCharWidth()
|
||||
|
||||
{top, left, height, width} = @pixelRectForScreenRange(screenRange)
|
||||
bottom = top + height
|
||||
right = left + width
|
||||
desiredScrollTop = top - verticalScrollMarginInPixels
|
||||
desiredScrollBottom = bottom + verticalScrollMarginInPixels
|
||||
desiredScrollLeft = left - horizontalScrollMarginInPixels
|
||||
desiredScrollRight = right + horizontalScrollMarginInPixels
|
||||
|
||||
if desiredScrollTop < @getScrollTop()
|
||||
@setScrollTop(desiredScrollTop)
|
||||
else if desiredScrollBottom > @getScrollBottom()
|
||||
@setScrollBottom(desiredScrollBottom)
|
||||
|
||||
if desiredScrollLeft < @getScrollLeft()
|
||||
@setScrollLeft(desiredScrollLeft)
|
||||
else if desiredScrollRight > @getScrollRight()
|
||||
@setScrollRight(desiredScrollRight)
|
||||
|
||||
scrollToScreenPosition: (screenPosition) ->
|
||||
@scrollToScreenRange(new Range(screenPosition, screenPosition))
|
||||
|
||||
scrollToBufferPosition: (bufferPosition) ->
|
||||
@scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition))
|
||||
|
||||
pixelRectForScreenRange: (screenRange) ->
|
||||
if screenRange.end.row > screenRange.start.row
|
||||
top = @pixelPositionForScreenPosition(screenRange.start).top
|
||||
left = 0
|
||||
height = (screenRange.end.row - screenRange.start.row + 1) * @getLineHeight()
|
||||
width = @getScrollWidth()
|
||||
else
|
||||
{top, left} = @pixelPositionForScreenPosition(screenRange.start)
|
||||
height = @getLineHeight()
|
||||
width = @pixelPositionForScreenPosition(screenRange.end).left - left
|
||||
|
||||
{top, left, width, height}
|
||||
|
||||
# Retrieves the current tab length.
|
||||
#
|
||||
# Returns a {Number}.
|
||||
getTabLength: ->
|
||||
@tokenizedBuffer.getTabLength()
|
||||
|
||||
# Specifies the tab length.
|
||||
#
|
||||
# tabLength - A {Number} that defines the new tab length.
|
||||
setTabLength: (tabLength) ->
|
||||
@tokenizedBuffer.setTabLength(tabLength)
|
||||
|
||||
# Deprecated: Use the softWrap property directly
|
||||
setSoftWrap: (@softWrap) -> @softWrap
|
||||
|
||||
@@ -105,12 +266,19 @@ class DisplayBuffer extends Model
|
||||
if editorWidthInChars isnt previousWidthInChars and @softWrap
|
||||
@updateWrappedScreenLines()
|
||||
|
||||
getSoftWrapColumn: ->
|
||||
if atom.config.get('editor.softWrapAtPreferredLineLength')
|
||||
Math.min(@editorWidthInChars, atom.config.getPositiveInt('editor.preferredLineLength', @editorWidthInChars))
|
||||
getEditorWidthInChars: ->
|
||||
width = @getWidth()
|
||||
if width? and @defaultCharWidth > 0
|
||||
Math.floor(width / @defaultCharWidth)
|
||||
else
|
||||
@editorWidthInChars
|
||||
|
||||
getSoftWrapColumn: ->
|
||||
if atom.config.get('editor.softWrapAtPreferredLineLength')
|
||||
Math.min(@getEditorWidthInChars(), atom.config.getPositiveInt('editor.preferredLineLength', @getEditorWidthInChars()))
|
||||
else
|
||||
@getEditorWidthInChars()
|
||||
|
||||
# Gets the screen line for the given screen row.
|
||||
#
|
||||
# screenRow - A {Number} indicating the screen row.
|
||||
@@ -134,6 +302,9 @@ class DisplayBuffer extends Model
|
||||
getLines: ->
|
||||
new Array(@screenLines...)
|
||||
|
||||
indentLevelForLine: (line) ->
|
||||
@tokenizedBuffer.indentLevelForLine(line)
|
||||
|
||||
# Given starting and ending screen rows, this returns an array of the
|
||||
# buffer rows corresponding to every screen row in the range
|
||||
#
|
||||
@@ -273,6 +444,52 @@ class DisplayBuffer extends Model
|
||||
end = @bufferPositionForScreenPosition(screenRange.end)
|
||||
new Range(start, end)
|
||||
|
||||
pixelRangeForScreenRange: (screenRange, clip=true) ->
|
||||
{start, end} = Range.fromObject(screenRange)
|
||||
{start: @pixelPositionForScreenPosition(start, clip), end: @pixelPositionForScreenPosition(end, clip)}
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition, clip=true) ->
|
||||
screenPosition = Point.fromObject(screenPosition)
|
||||
screenPosition = @clipScreenPosition(screenPosition) if clip
|
||||
|
||||
targetRow = screenPosition.row
|
||||
targetColumn = screenPosition.column
|
||||
defaultCharWidth = @defaultCharWidth
|
||||
|
||||
top = targetRow * @lineHeight
|
||||
left = 0
|
||||
column = 0
|
||||
for token in @lineForRow(targetRow).tokens
|
||||
charWidths = @getScopedCharWidths(token.scopes)
|
||||
for char in token.value
|
||||
return {top, left} if column is targetColumn
|
||||
left += charWidths[char] ? defaultCharWidth
|
||||
column++
|
||||
{top, left}
|
||||
|
||||
screenPositionForPixelPosition: (pixelPosition) ->
|
||||
targetTop = pixelPosition.top
|
||||
targetLeft = pixelPosition.left
|
||||
defaultCharWidth = @defaultCharWidth
|
||||
row = Math.floor(targetTop / @getLineHeight())
|
||||
row = Math.min(row, @getLastRow())
|
||||
row = Math.max(0, row)
|
||||
|
||||
left = 0
|
||||
column = 0
|
||||
for token in @lineForRow(row).tokens
|
||||
charWidths = @getScopedCharWidths(token.scopes)
|
||||
for char in token.value
|
||||
charWidth = charWidths[char] ? defaultCharWidth
|
||||
break if targetLeft <= left + (charWidth / 2)
|
||||
left += charWidth
|
||||
column++
|
||||
|
||||
new Point(row, column)
|
||||
|
||||
pixelPositionForBufferPosition: (bufferPosition) ->
|
||||
@pixelPositionForScreenPosition(@screenPositionForBufferPosition(bufferPosition))
|
||||
|
||||
# Gets the number of screen lines.
|
||||
#
|
||||
# Returns a {Number}.
|
||||
@@ -358,18 +575,6 @@ class DisplayBuffer extends Model
|
||||
tokenForBufferPosition: (bufferPosition) ->
|
||||
@tokenizedBuffer.tokenForPosition(bufferPosition)
|
||||
|
||||
# Retrieves the current tab length.
|
||||
#
|
||||
# Returns a {Number}.
|
||||
getTabLength: ->
|
||||
@tokenizedBuffer.getTabLength()
|
||||
|
||||
# Specifies the tab length.
|
||||
#
|
||||
# tabLength - A {Number} that defines the new tab length.
|
||||
setTabLength: (tabLength) ->
|
||||
@tokenizedBuffer.setTabLength(tabLength)
|
||||
|
||||
# Get the grammar for this buffer.
|
||||
#
|
||||
# Returns the current {Grammar} or the {NullGrammar}.
|
||||
|
||||
338
src/editor-component.coffee
Normal file
338
src/editor-component.coffee
Normal file
@@ -0,0 +1,338 @@
|
||||
React = require 'react'
|
||||
{div, span} = require 'reactionary'
|
||||
{debounce} = require 'underscore-plus'
|
||||
|
||||
GutterComponent = require './gutter-component'
|
||||
EditorScrollViewComponent = require './editor-scroll-view-component'
|
||||
ScrollbarComponent = require './scrollbar-component'
|
||||
SubscriberMixin = require './subscriber-mixin'
|
||||
|
||||
module.exports =
|
||||
EditorComponent = React.createClass
|
||||
displayName: 'EditorComponent'
|
||||
mixins: [SubscriberMixin]
|
||||
|
||||
pendingScrollTop: null
|
||||
pendingScrollLeft: null
|
||||
selectOnMouseMove: false
|
||||
batchingUpdates: false
|
||||
updateRequested: false
|
||||
cursorsMoved: false
|
||||
preservedRowRange: null
|
||||
scrollingVertically: false
|
||||
|
||||
render: ->
|
||||
{focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state
|
||||
{editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
|
||||
if @isMounted()
|
||||
renderedRowRange = @getRenderedRowRange()
|
||||
scrollHeight = editor.getScrollHeight()
|
||||
scrollWidth = editor.getScrollWidth()
|
||||
scrollTop = editor.getScrollTop()
|
||||
scrollLeft = editor.getScrollLeft()
|
||||
lineHeightInPixels = editor.getLineHeight()
|
||||
|
||||
className = 'editor editor-colors react'
|
||||
className += ' is-focused' if focused
|
||||
|
||||
div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1,
|
||||
GutterComponent {
|
||||
editor, renderedRowRange, scrollTop, scrollHeight,
|
||||
lineHeight: lineHeightInPixels, @pendingChanges
|
||||
}
|
||||
|
||||
EditorScrollViewComponent {
|
||||
ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide
|
||||
scrollHeight, scrollWidth, lineHeight: lineHeightInPixels,
|
||||
renderedRowRange, @pendingChanges, @scrollingVertically, @cursorsMoved,
|
||||
cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred
|
||||
}
|
||||
|
||||
ScrollbarComponent
|
||||
ref: 'verticalScrollbar'
|
||||
className: 'vertical-scrollbar'
|
||||
orientation: 'vertical'
|
||||
onScroll: @onVerticalScroll
|
||||
scrollTop: scrollTop
|
||||
scrollHeight: scrollHeight
|
||||
|
||||
ScrollbarComponent
|
||||
ref: 'horizontalScrollbar'
|
||||
className: 'horizontal-scrollbar'
|
||||
orientation: 'horizontal'
|
||||
onScroll: @onHorizontalScroll
|
||||
scrollLeft: scrollLeft
|
||||
scrollWidth: scrollWidth
|
||||
|
||||
getRenderedRowRange: ->
|
||||
renderedRowRange = @props.editor.getVisibleRowRange()
|
||||
if @preservedRowRange?
|
||||
renderedRowRange[0] = Math.min(@preservedRowRange[0], renderedRowRange[0])
|
||||
renderedRowRange[1] = Math.max(@preservedRowRange[1], renderedRowRange[1])
|
||||
renderedRowRange
|
||||
|
||||
getInitialState: -> {}
|
||||
|
||||
getDefaultProps: ->
|
||||
cursorBlinkPeriod: 800
|
||||
cursorBlinkResumeDelay: 200
|
||||
|
||||
componentWillMount: ->
|
||||
@pendingChanges = []
|
||||
@props.editor.manageScrollPosition = true
|
||||
@observeConfig()
|
||||
|
||||
componentDidMount: ->
|
||||
@observeEditor()
|
||||
@listenForDOMEvents()
|
||||
@listenForCommands()
|
||||
@props.editor.setVisible(true)
|
||||
@requestUpdate()
|
||||
|
||||
componentWillUnmount: ->
|
||||
@unsubscribe()
|
||||
@getDOMNode().removeEventListener 'mousewheel', @onMouseWheel
|
||||
|
||||
componentWillUpdate: ->
|
||||
@props.parentView.trigger 'cursor:moved' if @cursorsMoved
|
||||
|
||||
componentDidUpdate: ->
|
||||
@pendingChanges.length = 0
|
||||
@cursorsMoved = false
|
||||
@props.parentView.trigger 'editor:display-updated'
|
||||
|
||||
observeEditor: ->
|
||||
{editor} = @props
|
||||
@subscribe editor, 'batched-updates-started', @onBatchedUpdatesStarted
|
||||
@subscribe editor, 'batched-updates-ended', @onBatchedUpdatesEnded
|
||||
@subscribe editor, 'screen-lines-changed', @onScreenLinesChanged
|
||||
@subscribe editor, 'cursors-moved', @onCursorsMoved
|
||||
@subscribe editor, 'selection-screen-range-changed', @requestUpdate
|
||||
@subscribe editor, 'selection-added', @onSelectionAdded
|
||||
@subscribe editor, 'selection-removed', @onSelectionAdded
|
||||
@subscribe editor.$scrollTop.changes, @onScrollTopChanged
|
||||
@subscribe editor.$scrollLeft.changes, @requestUpdate
|
||||
@subscribe editor.$height.changes, @requestUpdate
|
||||
@subscribe editor.$width.changes, @requestUpdate
|
||||
@subscribe editor.$defaultCharWidth.changes, @requestUpdate
|
||||
@subscribe editor.$lineHeight.changes, @requestUpdate
|
||||
|
||||
listenForDOMEvents: ->
|
||||
node = @getDOMNode()
|
||||
node.addEventListener 'mousewheel', @onMouseWheel
|
||||
node.addEventListener 'focus', @onFocus # For some reason, React's built in focus events seem to bubble
|
||||
|
||||
listenForCommands: ->
|
||||
{parentView, editor, mini} = @props
|
||||
|
||||
@addCommandListeners
|
||||
'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': @consolidateSelections
|
||||
'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
|
||||
@addCommandListeners
|
||||
'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')
|
||||
'editor:scroll-to-cursor': => editor.scrollToCursorPosition()
|
||||
'core:page-up': => editor.pageUp()
|
||||
'core:page-down': => editor.pageDown()
|
||||
|
||||
addCommandListeners: (listenersByCommandName) ->
|
||||
{parentView} = @props
|
||||
|
||||
for command, listener of listenersByCommandName
|
||||
parentView.command command, listener
|
||||
|
||||
observeConfig: ->
|
||||
@subscribe atom.config.observe 'editor.fontFamily', @setFontFamily
|
||||
@subscribe atom.config.observe 'editor.fontSize', @setFontSize
|
||||
@subscribe atom.config.observe 'editor.showIndentGuide', @setShowIndentGuide
|
||||
|
||||
setFontSize: (fontSize) ->
|
||||
@setState({fontSize})
|
||||
|
||||
setLineHeight: (lineHeight) ->
|
||||
@setState({lineHeight})
|
||||
|
||||
setFontFamily: (fontFamily) ->
|
||||
@setState({fontFamily})
|
||||
|
||||
setShowIndentGuide: (showIndentGuide) ->
|
||||
@setState({showIndentGuide})
|
||||
|
||||
onFocus: ->
|
||||
@refs.scrollView.focus()
|
||||
|
||||
onInputFocused: ->
|
||||
@setState(focused: true)
|
||||
|
||||
onInputBlurred: ->
|
||||
@setState(focused: false)
|
||||
|
||||
onVerticalScroll: (scrollTop) ->
|
||||
{editor} = @props
|
||||
|
||||
return if scrollTop is editor.getScrollTop()
|
||||
|
||||
animationFramePending = @pendingScrollTop?
|
||||
@pendingScrollTop = scrollTop
|
||||
unless animationFramePending
|
||||
requestAnimationFrame =>
|
||||
@props.editor.setScrollTop(@pendingScrollTop)
|
||||
@pendingScrollTop = null
|
||||
|
||||
onHorizontalScroll: (scrollLeft) ->
|
||||
{editor} = @props
|
||||
|
||||
return if scrollLeft is editor.getScrollLeft()
|
||||
|
||||
animationFramePending = @pendingScrollLeft?
|
||||
@pendingScrollLeft = scrollLeft
|
||||
unless animationFramePending
|
||||
requestAnimationFrame =>
|
||||
@props.editor.setScrollLeft(@pendingScrollLeft)
|
||||
@pendingScrollLeft = null
|
||||
|
||||
onMouseWheel: (event) ->
|
||||
# Only scroll in one direction at a time
|
||||
{wheelDeltaX, wheelDeltaY} = event
|
||||
if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)
|
||||
@refs.horizontalScrollbar.getDOMNode().scrollLeft -= wheelDeltaX
|
||||
else
|
||||
@refs.verticalScrollbar.getDOMNode().scrollTop -= wheelDeltaY
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
clearPreservedRowRange: ->
|
||||
@preservedRowRange = null
|
||||
@scrollingVertically = false
|
||||
@requestUpdate()
|
||||
|
||||
clearPreservedRowRangeAfterDelay: null # Created lazily
|
||||
|
||||
onBatchedUpdatesStarted: ->
|
||||
@batchingUpdates = true
|
||||
|
||||
onBatchedUpdatesEnded: ->
|
||||
updateRequested = @updateRequested
|
||||
@updateRequested = false
|
||||
@batchingUpdates = false
|
||||
if updateRequested
|
||||
@requestUpdate()
|
||||
|
||||
onScreenLinesChanged: (change) ->
|
||||
{editor} = @props
|
||||
@pendingChanges.push(change)
|
||||
@requestUpdate() if editor.intersectsVisibleRowRange(change.start, change.end + 1) # TODO: Use closed-open intervals for change events
|
||||
|
||||
onSelectionAdded: (selection) ->
|
||||
{editor} = @props
|
||||
@requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection)
|
||||
|
||||
onScrollTopChanged: ->
|
||||
@preservedRowRange = @getRenderedRowRange()
|
||||
@scrollingVertically = true
|
||||
@clearPreservedRowRangeAfterDelay ?= debounce(@clearPreservedRowRange, 200)
|
||||
@clearPreservedRowRangeAfterDelay()
|
||||
@requestUpdate()
|
||||
|
||||
onSelectionRemoved: (selection) ->
|
||||
{editor} = @props
|
||||
@requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection)
|
||||
|
||||
onCursorsMoved: ->
|
||||
@cursorsMoved = true
|
||||
|
||||
requestUpdate: ->
|
||||
if @batchingUpdates
|
||||
@updateRequested = true
|
||||
else
|
||||
@forceUpdate()
|
||||
|
||||
measureHeightAndWidth: ->
|
||||
@refs.scrollView.measureHeightAndWidth()
|
||||
|
||||
consolidateSelections: (e) ->
|
||||
e.abortKeyBinding() unless @props.editor.consolidateSelections()
|
||||
198
src/editor-scroll-view-component.coffee
Normal file
198
src/editor-scroll-view-component.coffee
Normal file
@@ -0,0 +1,198 @@
|
||||
React = require 'react'
|
||||
{div} = require 'reactionary'
|
||||
{debounce} = require 'underscore-plus'
|
||||
|
||||
InputComponent = require './input-component'
|
||||
LinesComponent = require './lines-component'
|
||||
CursorsComponent = require './cursors-component'
|
||||
SelectionsComponent = require './selections-component'
|
||||
|
||||
module.exports =
|
||||
EditorScrollViewComponent = React.createClass
|
||||
displayName: 'EditorScrollViewComponent'
|
||||
|
||||
measurementPending: false
|
||||
overflowChangedEventsPaused: false
|
||||
overflowChangedWhilePaused: false
|
||||
|
||||
render: ->
|
||||
{editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props
|
||||
{scrollHeight, scrollWidth, renderedRowRange, pendingChanges, scrollingVertically} = @props
|
||||
{cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props
|
||||
|
||||
if @isMounted()
|
||||
inputStyle = @getHiddenInputPosition()
|
||||
inputStyle.WebkitTransform = 'translateZ(0)'
|
||||
|
||||
contentStyle =
|
||||
height: scrollHeight
|
||||
minWidth: scrollWidth
|
||||
WebkitTransform: "translate3d(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px, 0)"
|
||||
|
||||
div className: 'scroll-view',
|
||||
InputComponent
|
||||
ref: 'input'
|
||||
className: 'hidden-input'
|
||||
style: inputStyle
|
||||
onInput: @onInput
|
||||
onFocus: onInputFocused
|
||||
onBlur: onInputBlurred
|
||||
|
||||
div className: 'scroll-view-content', style: contentStyle, onMouseDown: @onMouseDown,
|
||||
CursorsComponent({editor, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay})
|
||||
LinesComponent {
|
||||
ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide,
|
||||
renderedRowRange, pendingChanges, scrollingVertically
|
||||
}
|
||||
div className: 'underlayer',
|
||||
SelectionsComponent({editor})
|
||||
|
||||
componentDidMount: ->
|
||||
@getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged
|
||||
window.addEventListener('resize', @onWindowResize)
|
||||
|
||||
@measureHeightAndWidth()
|
||||
|
||||
componentDidUnmount: ->
|
||||
window.removeEventListener('resize', @onWindowResize)
|
||||
|
||||
componentDidUpdate: ->
|
||||
@pauseOverflowChangedEvents()
|
||||
|
||||
onOverflowChanged: ->
|
||||
if @overflowChangedEventsPaused
|
||||
@overflowChangedWhilePaused = true
|
||||
else
|
||||
@requestMeasurement()
|
||||
|
||||
onWindowResize: ->
|
||||
@requestMeasurement()
|
||||
|
||||
pauseOverflowChangedEvents: ->
|
||||
@overflowChangedEventsPaused = true
|
||||
@resumeOverflowChangedEventsAfterDelay ?= debounce(@resumeOverflowChangedEvents, 500)
|
||||
@resumeOverflowChangedEventsAfterDelay()
|
||||
|
||||
resumeOverflowChangedEvents: ->
|
||||
if @overflowChangedWhilePaused
|
||||
@overflowChangedWhilePaused = false
|
||||
@requestMeasurement()
|
||||
|
||||
resumeOverflowChangedEventsAfterDelay: null
|
||||
|
||||
requestMeasurement: ->
|
||||
return if @measurementPending
|
||||
|
||||
@measurementPending = true
|
||||
requestAnimationFrame =>
|
||||
@measurementPending = false
|
||||
@measureHeightAndWidth()
|
||||
|
||||
onInput: (char, replaceLastCharacter) ->
|
||||
{editor} = @props
|
||||
|
||||
if replaceLastCharacter
|
||||
editor.transact ->
|
||||
editor.selectLeft()
|
||||
editor.insertText(char)
|
||||
else
|
||||
editor.insertText(char)
|
||||
|
||||
onMouseDown: (event) ->
|
||||
{editor} = @props
|
||||
{detail, shiftKey, metaKey} = event
|
||||
screenPosition = @screenPositionForMouseEvent(event)
|
||||
|
||||
if shiftKey
|
||||
editor.selectToScreenPosition(screenPosition)
|
||||
else if metaKey
|
||||
editor.addCursorAtScreenPosition(screenPosition)
|
||||
else
|
||||
editor.setCursorScreenPosition(screenPosition)
|
||||
switch detail
|
||||
when 2 then editor.selectWord()
|
||||
when 3 then editor.selectLine()
|
||||
|
||||
@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)
|
||||
editor.finalizeSelections()
|
||||
|
||||
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 = @getDOMNode().getBoundingClientRect()
|
||||
top = clientY - editorClientRect.top + editor.getScrollTop()
|
||||
left = clientX - editorClientRect.left + editor.getScrollLeft()
|
||||
{top, left}
|
||||
|
||||
getHiddenInputPosition: ->
|
||||
{editor} = @props
|
||||
return {top: 0, left: 0} unless @isMounted() and editor.getCursor()?
|
||||
|
||||
{top, left, height, width} = editor.getCursor().getPixelRect()
|
||||
top = top - editor.getScrollTop()
|
||||
top = Math.max(0, Math.min(editor.getHeight() - height, top))
|
||||
left = left - editor.getScrollLeft()
|
||||
left = Math.max(0, Math.min(editor.getWidth() - width, left))
|
||||
|
||||
{top, left}
|
||||
|
||||
# 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.
|
||||
measureHeightAndWidth: ->
|
||||
return unless @isMounted()
|
||||
|
||||
node = @getDOMNode()
|
||||
computedStyle = getComputedStyle(node)
|
||||
{editor} = @props
|
||||
|
||||
unless computedStyle.height is '0px'
|
||||
clientHeight = node.clientHeight
|
||||
editor.setHeight(clientHeight) if clientHeight > 0
|
||||
|
||||
unless computedStyle.width is '0px'
|
||||
clientWidth = node.clientWidth
|
||||
editor.setWidth(clientWidth) if clientHeight > 0
|
||||
|
||||
focus: ->
|
||||
@refs.input.focus()
|
||||
@@ -1482,16 +1482,13 @@ class EditorView extends View
|
||||
html = @buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, editor, mini)
|
||||
line.push(html) if html
|
||||
else
|
||||
firstNonWhitespacePosition = text.search(/\S/)
|
||||
firstTrailingWhitespacePosition = text.search(/\s*$/)
|
||||
lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0
|
||||
position = 0
|
||||
for token in tokens
|
||||
@updateScopeStack(line, scopeStack, token.scopes)
|
||||
hasLeadingWhitespace = position < firstNonWhitespacePosition
|
||||
hasTrailingWhitespace = position + token.value.length > firstTrailingWhitespacePosition
|
||||
hasIndentGuide = not mini and showIndentGuide and (hasLeadingWhitespace or lineIsWhitespaceOnly)
|
||||
line.push(token.getValueAsHtml({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide}))
|
||||
hasIndentGuide = not mini and showIndentGuide and token.hasLeadingWhitespace or (token.hasTrailingWhitespace and lineIsWhitespaceOnly)
|
||||
line.push(token.getValueAsHtml({invisibles, hasIndentGuide}))
|
||||
position += token.value.length
|
||||
|
||||
@popScope(line, scopeStack) while scopeStack.length > 0
|
||||
|
||||
@@ -136,10 +136,6 @@ class Editor extends Model
|
||||
atom.deserializers.add(this)
|
||||
Delegator.includeInto(this)
|
||||
|
||||
@properties
|
||||
scrollTop: 0
|
||||
scrollLeft: 0
|
||||
|
||||
deserializing: false
|
||||
callDisplayBufferCreatedHook: false
|
||||
registerEditor: false
|
||||
@@ -153,6 +149,9 @@ class Editor extends Model
|
||||
'autoDecreaseIndentForBufferRow', 'toggleLineCommentForBufferRow', 'toggleLineCommentsForBufferRows',
|
||||
toProperty: 'languageMode'
|
||||
|
||||
@delegatesProperties '$lineHeight', '$defaultCharWidth', '$height', '$width',
|
||||
'$scrollTop', '$scrollLeft', 'manageScrollPosition', toProperty: 'displayBuffer'
|
||||
|
||||
constructor: ({@softTabs, initialLine, tabLength, softWrap, @displayBuffer, buffer, registerEditor, suppressCursorCreation}) ->
|
||||
super
|
||||
|
||||
@@ -217,7 +216,10 @@ class Editor extends Model
|
||||
@subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args...
|
||||
|
||||
getViewClass: ->
|
||||
require './editor-view'
|
||||
if atom.config.get('core.useReactEditor')
|
||||
require './react-editor-view'
|
||||
else
|
||||
require './editor-view'
|
||||
|
||||
destroyed: ->
|
||||
@unsubscribe()
|
||||
@@ -232,8 +234,6 @@ class Editor extends Model
|
||||
displayBuffer = @displayBuffer.copy()
|
||||
softTabs = @getSoftTabs()
|
||||
newEditor = new Editor({@buffer, displayBuffer, tabLength, softTabs, suppressCursorCreation: true, registerEditor: true})
|
||||
newEditor.setScrollTop(@getScrollTop())
|
||||
newEditor.setScrollLeft(@getScrollLeft())
|
||||
for marker in @findMarkers(editorId: @id)
|
||||
marker.copy(editorId: newEditor.id, preserveFolds: true)
|
||||
newEditor
|
||||
@@ -269,18 +269,6 @@ class Editor extends Model
|
||||
# Controls visiblity based on the given {Boolean}.
|
||||
setVisible: (visible) -> @displayBuffer.setVisible(visible)
|
||||
|
||||
# Called by {EditorView} when the scroll position changes so it can be
|
||||
# persisted across reloads.
|
||||
setScrollTop: (@scrollTop) -> @scrollTop
|
||||
|
||||
getScrollTop: -> @scrollTop
|
||||
|
||||
# Called by {EditorView} when the scroll position changes so it can be
|
||||
# persisted across reloads.
|
||||
setScrollLeft: (@scrollLeft) -> @scrollLeft
|
||||
|
||||
getScrollLeft: -> @scrollLeft
|
||||
|
||||
# Set the number of characters that can be displayed horizontally in the
|
||||
# editor.
|
||||
#
|
||||
@@ -301,6 +289,9 @@ class Editor extends Model
|
||||
# softTabs - A {Boolean}
|
||||
setSoftTabs: (@softTabs) -> @softTabs
|
||||
|
||||
# Public: Toggle soft tabs for this editor
|
||||
toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs())
|
||||
|
||||
# Public: Get whether soft wrap is enabled for this editor.
|
||||
getSoftWrap: -> @displayBuffer.getSoftWrap()
|
||||
|
||||
@@ -309,6 +300,9 @@ class Editor extends Model
|
||||
# softWrap - A {Boolean}
|
||||
setSoftWrap: (softWrap) -> @displayBuffer.setSoftWrap(softWrap)
|
||||
|
||||
# Public: Toggle soft wrap for this editor
|
||||
toggleSoftWrap: -> @setSoftWrap(not @getSoftWrap())
|
||||
|
||||
# Public: Get the text representing a single level of indent.
|
||||
#
|
||||
# If soft tabs are enabled, the text is composed of N spaces, where N is the
|
||||
@@ -394,13 +388,7 @@ class Editor extends Model
|
||||
#
|
||||
# Returns a {Number}.
|
||||
indentLevelForLine: (line) ->
|
||||
if match = line.match(/^[\t ]+/)
|
||||
leadingWhitespace = match[0]
|
||||
tabCount = leadingWhitespace.match(/\t/g)?.length ? 0
|
||||
spaceCount = leadingWhitespace.match(/[ ]/g)?.length ? 0
|
||||
tabCount + (spaceCount / @getTabLength())
|
||||
else
|
||||
0
|
||||
@displayBuffer.indentLevelForLine(line)
|
||||
|
||||
# Constructs the string used for tabs.
|
||||
buildIndentString: (number) ->
|
||||
@@ -421,6 +409,15 @@ class Editor extends Model
|
||||
# filePath - A {String} path.
|
||||
saveAs: (filePath) -> @buffer.saveAs(filePath)
|
||||
|
||||
checkoutHead: ->
|
||||
if path = @getPath()
|
||||
atom.project.getRepo()?.checkoutHead(path)
|
||||
|
||||
# Copies the current file path to the native clipboard.
|
||||
copyPathToClipboard: ->
|
||||
path = @getPath()
|
||||
atom.clipboard.write(path) if path?
|
||||
|
||||
# Public: Returns the {String} path of this editor's text buffer.
|
||||
getPath: -> @buffer.getPath()
|
||||
|
||||
@@ -599,6 +596,9 @@ class Editor extends Model
|
||||
# Returns an {Array} of {String}s.
|
||||
getCursorScopes: -> @getCursor().getScopes()
|
||||
|
||||
logCursorScope: ->
|
||||
console.log @getCursorScopes()
|
||||
|
||||
# Public: For each selection, replace the selected text with the given text.
|
||||
#
|
||||
# text - A {String} representing the text to insert.
|
||||
@@ -1178,6 +1178,16 @@ class Editor extends Model
|
||||
setSelectedBufferRange: (bufferRange, options) ->
|
||||
@setSelectedBufferRanges([bufferRange], options)
|
||||
|
||||
# Public: Set the selected range in screen coordinates. If there are multiple
|
||||
# selections, they are reduced to a single selection with the given range.
|
||||
#
|
||||
# screenRange - A {Range} or range-compatible {Array}.
|
||||
# options - An options {Object}:
|
||||
# :reversed - A {Boolean} indicating whether to create the selection in a
|
||||
# reversed orientation.
|
||||
setSelectedScreenRange: (screenRange, options) ->
|
||||
@setSelectedBufferRange(@bufferRangeForScreenRange(screenRange, options), options)
|
||||
|
||||
# Public: Set the selected ranges in buffer coordinates. If there are multiple
|
||||
# selections, they are replaced by new selections with the given ranges.
|
||||
#
|
||||
@@ -1202,6 +1212,7 @@ class Editor extends Model
|
||||
# Remove the given selection.
|
||||
removeSelection: (selection) ->
|
||||
_.remove(@selections, selection)
|
||||
@emit 'selection-removed', selection
|
||||
|
||||
# Reduce one or more selections to a single empty selection based on the most
|
||||
# recently added cursor.
|
||||
@@ -1218,6 +1229,9 @@ class Editor extends Model
|
||||
else
|
||||
false
|
||||
|
||||
selectionScreenRangeChanged: (selection) ->
|
||||
@emit 'selection-screen-range-changed', selection
|
||||
|
||||
# Public: Get current {Selection}s.
|
||||
#
|
||||
# Returns: An {Array} of {Selection}s.
|
||||
@@ -1328,6 +1342,14 @@ class Editor extends Model
|
||||
getSelectedBufferRanges: ->
|
||||
selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition()
|
||||
|
||||
# Public: Get the {Range}s of all selections in screen coordinates.
|
||||
#
|
||||
# The ranges are sorted by their position in the buffer.
|
||||
#
|
||||
# Returns an {Array} of {Range}s.
|
||||
getSelectedScreenRanges: ->
|
||||
selection.getScreenRange() for selection in @getSelectionsOrderedByBufferPosition()
|
||||
|
||||
# Public: Get the selected text of the most recently added selection.
|
||||
#
|
||||
# Returns a {String}.
|
||||
@@ -1431,9 +1453,26 @@ class Editor extends Model
|
||||
moveCursorToNextWordBoundary: ->
|
||||
@moveCursors (cursor) -> cursor.moveToNextWordBoundary()
|
||||
|
||||
scrollToCursorPosition: ->
|
||||
@getCursor().autoscroll()
|
||||
|
||||
pageUp: ->
|
||||
@setScrollTop(@getScrollTop() - @getHeight())
|
||||
|
||||
pageDown: ->
|
||||
@setScrollTop(@getScrollTop() + @getHeight())
|
||||
|
||||
moveCursors: (fn) ->
|
||||
fn(cursor) for cursor in @getCursors()
|
||||
@mergeCursors()
|
||||
@movingCursors = true
|
||||
@batchUpdates =>
|
||||
fn(cursor) for cursor in @getCursors()
|
||||
@mergeCursors()
|
||||
@movingCursors = false
|
||||
@emit 'cursors-moved'
|
||||
|
||||
cursorMoved: (event) ->
|
||||
@emit 'cursor-moved', event
|
||||
@emit 'cursors-moved' unless @movingCursors
|
||||
|
||||
# Public: Select from the current cursor position to the given position in
|
||||
# screen coordinates.
|
||||
@@ -1735,7 +1774,9 @@ class Editor extends Model
|
||||
# execution and revert any changes performed up to the abortion.
|
||||
#
|
||||
# fn - A {Function} to call inside the transaction.
|
||||
transact: (fn) -> @buffer.transact(fn)
|
||||
transact: (fn) ->
|
||||
@batchUpdates =>
|
||||
@buffer.transact(fn)
|
||||
|
||||
# Public: Start an open-ended transaction.
|
||||
#
|
||||
@@ -1755,6 +1796,12 @@ class Editor extends Model
|
||||
# within the transaction.
|
||||
abortTransaction: -> @buffer.abortTransaction()
|
||||
|
||||
batchUpdates: (fn) ->
|
||||
@emit 'batched-updates-started'
|
||||
result = fn()
|
||||
@emit 'batched-updates-ended'
|
||||
result
|
||||
|
||||
inspect: ->
|
||||
"<Editor #{@id}>"
|
||||
|
||||
@@ -1771,6 +1818,66 @@ class Editor extends Model
|
||||
getSelectionMarkerAttributes: ->
|
||||
type: 'selection', editorId: @id, invalidate: 'never'
|
||||
|
||||
getVerticalScrollMargin: -> @displayBuffer.getVerticalScrollMargin()
|
||||
setVerticalScrollMargin: (verticalScrollMargin) -> @displayBuffer.setVerticalScrollMargin(verticalScrollMargin)
|
||||
|
||||
getHorizontalScrollMargin: -> @displayBuffer.getHorizontalScrollMargin()
|
||||
setHorizontalScrollMargin: (horizontalScrollMargin) -> @displayBuffer.setHorizontalScrollMargin(horizontalScrollMargin)
|
||||
|
||||
getLineHeight: -> @displayBuffer.getLineHeight()
|
||||
setLineHeight: (lineHeight) -> @displayBuffer.setLineHeight(lineHeight)
|
||||
|
||||
getScopedCharWidth: (scopeNames, char) -> @displayBuffer.getScopedCharWidth(scopeNames, char)
|
||||
setScopedCharWidth: (scopeNames, char, width) -> @displayBuffer.setScopedCharWidth(scopeNames, char, width)
|
||||
|
||||
getScopedCharWidths: (scopeNames) -> @displayBuffer.getScopedCharWidths(scopeNames)
|
||||
|
||||
clearScopedCharWidths: -> @displayBuffer.clearScopedCharWidths()
|
||||
|
||||
getDefaultCharWidth: -> @displayBuffer.getDefaultCharWidth()
|
||||
setDefaultCharWidth: (defaultCharWidth) -> @displayBuffer.setDefaultCharWidth(defaultCharWidth)
|
||||
|
||||
setHeight: (height) -> @displayBuffer.setHeight(height)
|
||||
getHeight: -> @displayBuffer.getHeight()
|
||||
|
||||
setWidth: (width) -> @displayBuffer.setWidth(width)
|
||||
getWidth: -> @displayBuffer.getWidth()
|
||||
|
||||
getScrollTop: -> @displayBuffer.getScrollTop()
|
||||
setScrollTop: (scrollTop) -> @displayBuffer.setScrollTop(scrollTop)
|
||||
|
||||
getScrollBottom: -> @displayBuffer.getScrollBottom()
|
||||
setScrollBottom: (scrollBottom) -> @displayBuffer.setScrollBottom(scrollBottom)
|
||||
|
||||
getScrollLeft: -> @displayBuffer.getScrollLeft()
|
||||
setScrollLeft: (scrollLeft) -> @displayBuffer.setScrollLeft(scrollLeft)
|
||||
|
||||
getScrollRight: -> @displayBuffer.getScrollRight()
|
||||
setScrollRight: (scrollRight) -> @displayBuffer.setScrollRight(scrollRight)
|
||||
|
||||
getScrollHeight: -> @displayBuffer.getScrollHeight()
|
||||
getScrollWidth: (scrollWidth) -> @displayBuffer.getScrollWidth(scrollWidth)
|
||||
|
||||
getVisibleRowRange: -> @displayBuffer.getVisibleRowRange()
|
||||
|
||||
intersectsVisibleRowRange: (startRow, endRow) -> @displayBuffer.intersectsVisibleRowRange(startRow, endRow)
|
||||
|
||||
selectionIntersectsVisibleRowRange: (selection) -> @displayBuffer.selectionIntersectsVisibleRowRange(selection)
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition) -> @displayBuffer.pixelPositionForScreenPosition(screenPosition)
|
||||
|
||||
pixelPositionForBufferPosition: (bufferPosition) -> @displayBuffer.pixelPositionForBufferPosition(bufferPosition)
|
||||
|
||||
screenPositionForPixelPosition: (pixelPosition) -> @displayBuffer.screenPositionForPixelPosition(pixelPosition)
|
||||
|
||||
pixelRectForScreenRange: (screenRange) -> @displayBuffer.pixelRectForScreenRange(screenRange)
|
||||
|
||||
scrollToScreenRange: (screenRange) -> @displayBuffer.scrollToScreenRange(screenRange)
|
||||
|
||||
scrollToScreenPosition: (screenPosition) -> @displayBuffer.scrollToScreenPosition(screenPosition)
|
||||
|
||||
scrollToBufferPosition: (bufferPosition) -> @displayBuffer.scrollToBufferPosition(bufferPosition)
|
||||
|
||||
# Deprecated: Call {::joinLines} instead.
|
||||
joinLine: ->
|
||||
deprecate("Use Editor::joinLines() instead")
|
||||
|
||||
79
src/gutter-component.coffee
Normal file
79
src/gutter-component.coffee
Normal file
@@ -0,0 +1,79 @@
|
||||
React = require 'react'
|
||||
{div} = require 'reactionary'
|
||||
{isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus'
|
||||
SubscriberMixin = require './subscriber-mixin'
|
||||
|
||||
module.exports =
|
||||
GutterComponent = React.createClass
|
||||
displayName: 'GutterComponent'
|
||||
mixins: [SubscriberMixin]
|
||||
|
||||
render: ->
|
||||
div className: 'gutter',
|
||||
@renderLineNumbers() if @isMounted()
|
||||
|
||||
renderLineNumbers: ->
|
||||
{editor, renderedRowRange, scrollTop, scrollHeight} = @props
|
||||
[startRow, endRow] = renderedRowRange
|
||||
charWidth = editor.getDefaultCharWidth()
|
||||
lineHeight = editor.getLineHeight()
|
||||
maxDigits = editor.getLastBufferRow().toString().length
|
||||
style =
|
||||
width: charWidth * (maxDigits + 1.5)
|
||||
height: scrollHeight
|
||||
WebkitTransform: "translate3d(0, #{-scrollTop}px, 0)"
|
||||
|
||||
lineNumbers = []
|
||||
tokenizedLines = editor.linesForScreenRows(startRow, endRow - 1)
|
||||
tokenizedLines.push({id: 0}) if tokenizedLines.length is 0
|
||||
for bufferRow, i in editor.bufferRowsForScreenRows(startRow, endRow - 1)
|
||||
if bufferRow is lastBufferRow
|
||||
lineNumber = '•'
|
||||
else
|
||||
lastBufferRow = bufferRow
|
||||
lineNumber = (bufferRow + 1).toString()
|
||||
|
||||
key = tokenizedLines[i].id
|
||||
screenRow = startRow + i
|
||||
lineNumbers.push(LineNumberComponent({key, lineNumber, maxDigits, bufferRow, screenRow, lineHeight}))
|
||||
lastBufferRow = bufferRow
|
||||
|
||||
div className: 'line-numbers', style: style,
|
||||
lineNumbers
|
||||
|
||||
# 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', 'lineHeight')
|
||||
|
||||
{renderedRowRange, pendingChanges} = newProps
|
||||
for change in pendingChanges when change.screenDelta > 0 or change.bufferDelta > 0
|
||||
return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start
|
||||
|
||||
false
|
||||
|
||||
LineNumberComponent = React.createClass
|
||||
displayName: 'LineNumberComponent'
|
||||
|
||||
render: ->
|
||||
{bufferRow, screenRow, lineHeight} = @props
|
||||
div
|
||||
className: "line-number line-number-#{bufferRow}"
|
||||
style: {top: screenRow * lineHeight}
|
||||
'data-buffer-row': bufferRow
|
||||
'data-screen-row': screenRow
|
||||
dangerouslySetInnerHTML: {__html: @buildInnerHTML()}
|
||||
|
||||
buildInnerHTML: ->
|
||||
{lineNumber, maxDigits} = @props
|
||||
if lineNumber.length < maxDigits
|
||||
padding = multiplyString(' ', maxDigits - lineNumber.length)
|
||||
padding + lineNumber + @iconDivHTML
|
||||
else
|
||||
lineNumber + @iconDivHTML
|
||||
|
||||
iconDivHTML: '<div class="icon-right"></div>'
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
not isEqualForProperties(newProps, @props, 'lineHeight', 'screenRow')
|
||||
51
src/input-component.coffee
Normal file
51
src/input-component.coffee
Normal file
@@ -0,0 +1,51 @@
|
||||
punycode = require 'punycode'
|
||||
{last, isEqual} = require 'underscore-plus'
|
||||
React = require 'react'
|
||||
{input} = require 'reactionary'
|
||||
|
||||
module.exports =
|
||||
InputComponent = React.createClass
|
||||
displayName: 'InputComponent'
|
||||
|
||||
render: ->
|
||||
{className, style, onFocus, onBlur} = @props
|
||||
|
||||
input {className, style, onFocus, onBlur}
|
||||
|
||||
getInitialState: ->
|
||||
{lastChar: ''}
|
||||
|
||||
componentDidMount: ->
|
||||
@getDOMNode().addEventListener 'input', @onInput
|
||||
@getDOMNode().addEventListener 'compositionupdate', @onCompositionUpdate
|
||||
|
||||
# Don't let text accumulate in the input forever, but avoid excessive reflows
|
||||
componentDidUpdate: ->
|
||||
if @lastValueLength > 500 and not @isPressAndHoldCharacter(@state.lastChar)
|
||||
@getDOMNode().value = ''
|
||||
@lastValueLength = 0
|
||||
|
||||
# This should actually consult the property lists in /System/Library/Input Methods/PressAndHold.app
|
||||
isPressAndHoldCharacter: (char) ->
|
||||
@state.lastChar.match /[aeiouAEIOU]/
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
not isEqual(newProps.style, @props.style)
|
||||
|
||||
onInput: (e) ->
|
||||
e.stopPropagation()
|
||||
valueCharCodes = punycode.ucs2.decode(@getDOMNode().value)
|
||||
valueLength = valueCharCodes.length
|
||||
replaceLastChar = valueLength is @lastValueLength
|
||||
@lastValueLength = valueLength
|
||||
lastChar = String.fromCharCode(last(valueCharCodes))
|
||||
@props.onInput?(lastChar, replaceLastChar)
|
||||
|
||||
onFocus: ->
|
||||
@props.onFocus?()
|
||||
|
||||
onBlur: ->
|
||||
@props.onBlur?()
|
||||
|
||||
focus: ->
|
||||
@getDOMNode().focus()
|
||||
@@ -8,7 +8,7 @@ module.exports =
|
||||
class LessCompileCache
|
||||
Subscriber.includeInto(this)
|
||||
|
||||
@cacheDir: path.join(fs.getHomeDirectory(), 'compile-cache', 'less')
|
||||
@cacheDir: path.join(atom.getConfigDirPath(), 'compile-cache', 'less')
|
||||
|
||||
constructor: ({resourcePath, importPaths}) ->
|
||||
@lessSearchPaths = [
|
||||
|
||||
140
src/lines-component.coffee
Normal file
140
src/lines-component.coffee
Normal file
@@ -0,0 +1,140 @@
|
||||
React = require 'react'
|
||||
{div, span} = require 'reactionary'
|
||||
{debounce, isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus'
|
||||
{$$} = require 'space-pen'
|
||||
|
||||
DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
|
||||
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
|
||||
|
||||
module.exports =
|
||||
LinesComponent = React.createClass
|
||||
displayName: 'LinesComponent'
|
||||
|
||||
render: ->
|
||||
if @isMounted()
|
||||
{editor, renderedRowRange, lineHeight, showIndentGuide} = @props
|
||||
[startRow, endRow] = renderedRowRange
|
||||
|
||||
lines =
|
||||
for tokenizedLine, i in editor.linesForScreenRows(startRow, endRow - 1)
|
||||
LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, screenRow: startRow + i})
|
||||
|
||||
div {className: 'lines'}, lines
|
||||
|
||||
componentWillMount: ->
|
||||
@measuredLines = new WeakSet
|
||||
|
||||
componentDidMount: ->
|
||||
@measureLineHeightAndCharWidth()
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide')
|
||||
|
||||
{renderedRowRange, pendingChanges} = newProps
|
||||
for change in pendingChanges
|
||||
return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start
|
||||
|
||||
false
|
||||
|
||||
componentDidUpdate: (prevProps) ->
|
||||
@measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight')
|
||||
@clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily')
|
||||
@measureCharactersInNewLines() unless @props.scrollingVertically
|
||||
|
||||
measureLineHeightAndCharWidth: ->
|
||||
node = @getDOMNode()
|
||||
node.appendChild(DummyLineNode)
|
||||
lineHeight = DummyLineNode.getBoundingClientRect().height
|
||||
charWidth = DummyLineNode.firstChild.getBoundingClientRect().width
|
||||
node.removeChild(DummyLineNode)
|
||||
|
||||
{editor} = @props
|
||||
editor.setLineHeight(lineHeight)
|
||||
editor.setDefaultCharWidth(charWidth)
|
||||
|
||||
measureCharactersInNewLines: ->
|
||||
[visibleStartRow, visibleEndRow] = @props.renderedRowRange
|
||||
node = @getDOMNode()
|
||||
|
||||
for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1)
|
||||
unless @measuredLines.has(tokenizedLine)
|
||||
lineNode = node.children[i]
|
||||
@measureCharactersInLine(tokenizedLine, lineNode)
|
||||
|
||||
measureCharactersInLine: (tokenizedLine, lineNode) ->
|
||||
{editor} = @props
|
||||
rangeForMeasurement = null
|
||||
iterator = null
|
||||
charIndex = 0
|
||||
|
||||
for {value, scopes}, tokenIndex in tokenizedLine.tokens
|
||||
charWidths = editor.getScopedCharWidths(scopes)
|
||||
|
||||
for char in value
|
||||
unless charWidths[char]?
|
||||
unless textNode?
|
||||
rangeForMeasurement ?= document.createRange()
|
||||
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
|
||||
textNode = iterator.nextNode()
|
||||
textNodeIndex = 0
|
||||
nextTextNodeIndex = textNode.textContent.length
|
||||
|
||||
while nextTextNodeIndex <= charIndex
|
||||
textNode = iterator.nextNode()
|
||||
textNodeIndex = nextTextNodeIndex
|
||||
nextTextNodeIndex = textNodeIndex + textNode.textContent.length
|
||||
|
||||
i = charIndex - textNodeIndex
|
||||
rangeForMeasurement.setStart(textNode, i)
|
||||
rangeForMeasurement.setEnd(textNode, i + 1)
|
||||
charWidth = rangeForMeasurement.getBoundingClientRect().width
|
||||
editor.setScopedCharWidth(scopes, char, charWidth)
|
||||
|
||||
charIndex++
|
||||
|
||||
@measuredLines.add(tokenizedLine)
|
||||
|
||||
clearScopedCharWidths: ->
|
||||
@measuredLines.clear()
|
||||
@props.editor.clearScopedCharWidths()
|
||||
|
||||
|
||||
LineComponent = React.createClass
|
||||
displayName: 'LineComponent'
|
||||
|
||||
render: ->
|
||||
{screenRow, lineHeight} = @props
|
||||
|
||||
style =
|
||||
top: screenRow * lineHeight
|
||||
position: 'absolute'
|
||||
|
||||
div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()}
|
||||
|
||||
buildInnerHTML: ->
|
||||
if @props.tokenizedLine.text.length is 0
|
||||
@buildEmptyLineHTML()
|
||||
else
|
||||
@buildScopeTreeHTML(@props.tokenizedLine.getScopeTree())
|
||||
|
||||
buildEmptyLineHTML: ->
|
||||
{showIndentGuide, tokenizedLine} = @props
|
||||
{indentLevel, tabLength} = tokenizedLine
|
||||
|
||||
if showIndentGuide and indentLevel > 0
|
||||
indentSpan = "<span class='indent-guide'>#{multiplyString(' ', tabLength)}</span>"
|
||||
multiplyString(indentSpan, indentLevel + 1)
|
||||
else
|
||||
" "
|
||||
|
||||
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({hasIndentGuide: @props.showIndentGuide})}</span>"
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow')
|
||||
@@ -290,6 +290,7 @@ class Package
|
||||
$(event.target).trigger(event)
|
||||
@restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
|
||||
@unsubscribeFromActivationEvents()
|
||||
false
|
||||
|
||||
unsubscribeFromActivationEvents: ->
|
||||
return unless atom.workspaceView?
|
||||
|
||||
81
src/react-editor-view.coffee
Normal file
81
src/react-editor-view.coffee
Normal file
@@ -0,0 +1,81 @@
|
||||
{View, $} = require 'space-pen'
|
||||
React = require 'react'
|
||||
EditorComponent = require './editor-component'
|
||||
|
||||
module.exports =
|
||||
class ReactEditorView extends View
|
||||
@content: -> @div class: 'editor react-wrapper'
|
||||
|
||||
focusOnAttach: false
|
||||
|
||||
constructor: (@editor) ->
|
||||
super
|
||||
|
||||
getEditor: -> @editor
|
||||
|
||||
Object.defineProperty @::, 'lineHeight', get: -> @editor.getLineHeight()
|
||||
Object.defineProperty @::, 'charWidth', get: -> @editor.getDefaultCharWidth()
|
||||
|
||||
scrollTop: (scrollTop) ->
|
||||
if scrollTop?
|
||||
@editor.setScrollTop(scrollTop)
|
||||
else
|
||||
@editor.getScrollTop()
|
||||
|
||||
scrollLeft: (scrollLeft) ->
|
||||
if scrollLeft?
|
||||
@editor.setScrollLeft(scrollLeft)
|
||||
else
|
||||
@editor.getScrollLeft()
|
||||
|
||||
scrollToScreenPosition: (screenPosition) ->
|
||||
@editor.scrollToScreenPosition(screenPosition)
|
||||
|
||||
scrollToBufferPosition: (bufferPosition) ->
|
||||
@editor.scrollToBufferPosition(bufferPosition)
|
||||
|
||||
afterAttach: (onDom) ->
|
||||
return unless onDom
|
||||
@attached = true
|
||||
@component = React.renderComponent(EditorComponent({@editor, parentView: this}), @element)
|
||||
|
||||
node = @component.getDOMNode()
|
||||
|
||||
@underlayer = $(node).find('.underlayer')
|
||||
|
||||
@gutter = $(node).find('.gutter')
|
||||
@gutter.removeClassFromAllLines = (klass) =>
|
||||
@gutter.find('.line-number').removeClass(klass)
|
||||
|
||||
@gutter.addClassToLine = (bufferRow, klass) =>
|
||||
lines = @gutter.find(".line-number-#{bufferRow}")
|
||||
lines.addClass(klass)
|
||||
lines.length > 0
|
||||
|
||||
@focus() if @focusOnAttach
|
||||
|
||||
@trigger 'editor:attached', [this]
|
||||
|
||||
pixelPositionForBufferPosition: (bufferPosition) ->
|
||||
@editor.pixelPositionForBufferPosition(bufferPosition)
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition) ->
|
||||
@editor.pixelPositionForScreenPosition(screenPosition)
|
||||
|
||||
appendToLinesView: (view) ->
|
||||
view.css('position', 'absolute')
|
||||
@find('.scroll-view-content').prepend(view)
|
||||
|
||||
beforeRemove: ->
|
||||
React.unmountComponentAtNode(@element)
|
||||
@attached = false
|
||||
@trigger 'editor:detached', this
|
||||
|
||||
getPane: ->
|
||||
@closest('.pane').view()
|
||||
|
||||
focus: ->
|
||||
if @component?
|
||||
@component.onFocus()
|
||||
else
|
||||
@focusOnAttach = true
|
||||
54
src/scrollbar-component.coffee
Normal file
54
src/scrollbar-component.coffee
Normal file
@@ -0,0 +1,54 @@
|
||||
React = require 'react'
|
||||
{div} = require 'reactionary'
|
||||
{isEqualForProperties} = require 'underscore-plus'
|
||||
|
||||
module.exports =
|
||||
ScrollbarComponent = React.createClass
|
||||
render: ->
|
||||
{orientation, className, scrollHeight, scrollWidth} = @props
|
||||
|
||||
div {className, @onScroll},
|
||||
switch orientation
|
||||
when 'vertical'
|
||||
div className: 'scrollbar-content', style: {height: scrollHeight}
|
||||
when 'horizontal'
|
||||
div className: 'scrollbar-content', style: {width: scrollWidth}
|
||||
|
||||
componentDidMount: ->
|
||||
{orientation} = @props
|
||||
|
||||
unless orientation is 'vertical' or orientation is 'horizontal'
|
||||
throw new Error("Must specify an orientation property of 'vertical' or 'horizontal'")
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
switch @props.orientation
|
||||
when 'vertical'
|
||||
not isEqualForProperties(newProps, @props, 'scrollHeight', 'scrollTop')
|
||||
when 'horizontal'
|
||||
not isEqualForProperties(newProps, @props, 'scrollWidth', 'scrollLeft')
|
||||
|
||||
componentDidUpdate: ->
|
||||
{orientation, scrollTop, scrollLeft} = @props
|
||||
node = @getDOMNode()
|
||||
|
||||
switch orientation
|
||||
when 'vertical'
|
||||
node.scrollTop = scrollTop
|
||||
@props.scrollTop = node.scrollTop # Ensure scrollTop reflects actual DOM without triggering another update
|
||||
when 'horizontal'
|
||||
node.scrollLeft = scrollLeft
|
||||
@props.scrollLeft = node.scrollLeft # Ensure scrollLeft reflects actual DOM without triggering another update
|
||||
|
||||
onScroll: ->
|
||||
{orientation, onScroll} = @props
|
||||
node = @getDOMNode()
|
||||
|
||||
switch orientation
|
||||
when 'vertical'
|
||||
scrollTop = node.scrollTop
|
||||
@props.scrollTop = scrollTop # Ensure scrollTop reflects actual DOM without triggering another update
|
||||
onScroll(scrollTop)
|
||||
when 'horizontal'
|
||||
scrollLeft = node.scrollLeft
|
||||
@props.scrollLeft = scrollLeft # Ensure scrollLeft reflects actual DOM without triggering another update
|
||||
onScroll(scrollLeft)
|
||||
11
src/selection-component.coffee
Normal file
11
src/selection-component.coffee
Normal file
@@ -0,0 +1,11 @@
|
||||
React = require 'react'
|
||||
{div} = require 'reactionary'
|
||||
|
||||
module.exports =
|
||||
SelectionComponent = React.createClass
|
||||
displayName: 'SelectionComponent'
|
||||
|
||||
render: ->
|
||||
div className: 'selection',
|
||||
for regionRect, i in @props.selection.getRegionRects()
|
||||
div className: 'region', key: i, style: regionRect
|
||||
@@ -1,12 +1,10 @@
|
||||
{Point, Range} = require 'text-buffer'
|
||||
{Emitter} = require 'emissary'
|
||||
{Model} = require 'theorist'
|
||||
{pick} = require 'underscore-plus'
|
||||
|
||||
# Public: Represents a selection in the {Editor}.
|
||||
module.exports =
|
||||
class Selection
|
||||
Emitter.includeInto(this)
|
||||
|
||||
class Selection extends Model
|
||||
cursor: null
|
||||
marker: null
|
||||
editor: null
|
||||
@@ -14,7 +12,8 @@ class Selection
|
||||
wordwise: false
|
||||
needsAutoscroll: null
|
||||
|
||||
constructor: ({@cursor, @marker, @editor}) ->
|
||||
constructor: ({@cursor, @marker, @editor, id}) ->
|
||||
@assignId(id)
|
||||
@cursor.selection = this
|
||||
@marker.on 'changed', => @screenRangeChanged()
|
||||
@marker.on 'destroyed', =>
|
||||
@@ -77,8 +76,9 @@ class Selection
|
||||
options.reversed ?= @isReversed()
|
||||
@editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
|
||||
@modifySelection =>
|
||||
@cursor.needsAutoscroll = false if options.autoscroll?
|
||||
@cursor.needsAutoscroll = false if @needsAutoscroll?
|
||||
@marker.setBufferRange(bufferRange, options)
|
||||
@autoscroll() if @needsAutoscroll and @editor.manageScrollPosition
|
||||
|
||||
# Public: Returns the starting and ending buffer rows the selection is
|
||||
# highlighting.
|
||||
@@ -91,6 +91,9 @@ class Selection
|
||||
end = Math.max(start, end - 1) if range.end.column == 0
|
||||
[start, end]
|
||||
|
||||
autoscroll: ->
|
||||
@editor.scrollToScreenRange(@getScreenRange())
|
||||
|
||||
# Public: Returns the text in the selection.
|
||||
getText: ->
|
||||
@editor.buffer.getTextInRange(@getBufferRange())
|
||||
@@ -570,6 +573,48 @@ class Selection
|
||||
compare: (otherSelection) ->
|
||||
@getBufferRange().compare(otherSelection.getBufferRange())
|
||||
|
||||
# Get the pixel dimensions of rectangular regions that cover selection's area
|
||||
# on the screen. Used by SelectionComponent for rendering.
|
||||
getRegionRects: ->
|
||||
lineHeight = @editor.getLineHeight()
|
||||
{start, end} = @getScreenRange()
|
||||
rowCount = end.row - start.row + 1
|
||||
startPixelPosition = @editor.pixelPositionForScreenPosition(start)
|
||||
endPixelPosition = @editor.pixelPositionForScreenPosition(end)
|
||||
|
||||
if rowCount is 1
|
||||
# Single line selection
|
||||
rects = [{
|
||||
top: startPixelPosition.top
|
||||
height: lineHeight
|
||||
left: startPixelPosition.left
|
||||
width: endPixelPosition.left - startPixelPosition.left
|
||||
}]
|
||||
else
|
||||
# Multi-line selection
|
||||
rects = []
|
||||
|
||||
# First row, extending from selection start to the right side of screen
|
||||
rects.push {
|
||||
top: startPixelPosition.top
|
||||
left: startPixelPosition.left
|
||||
height: lineHeight
|
||||
right: 0
|
||||
}
|
||||
if rowCount > 2
|
||||
# Middle rows, extending from left side to right side of screen
|
||||
rects.push {
|
||||
top: startPixelPosition.top + lineHeight
|
||||
height: (rowCount - 2) * lineHeight
|
||||
left: 0
|
||||
right: 0
|
||||
}
|
||||
# Last row, extending from left side of screen to selection end
|
||||
rects.push {top: endPixelPosition.top, height: lineHeight, left: 0, width: endPixelPosition.left }
|
||||
|
||||
rects
|
||||
|
||||
screenRangeChanged: ->
|
||||
screenRange = @getScreenRange()
|
||||
@emit 'screen-range-changed', screenRange
|
||||
@editor.selectionScreenRangeChanged(this)
|
||||
|
||||
16
src/selections-component.coffee
Normal file
16
src/selections-component.coffee
Normal file
@@ -0,0 +1,16 @@
|
||||
React = require 'react'
|
||||
{div} = require 'reactionary'
|
||||
SelectionComponent = require './selection-component'
|
||||
|
||||
module.exports =
|
||||
SelectionsComponent = React.createClass
|
||||
displayName: 'SelectionsComponent'
|
||||
|
||||
render: ->
|
||||
{editor} = @props
|
||||
|
||||
div className: 'selections',
|
||||
if @isMounted()
|
||||
for selection in editor.getSelections()
|
||||
if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)
|
||||
SelectionComponent({key: selection.id, selection})
|
||||
@@ -69,4 +69,6 @@ jQuery(document.body).on 'show.bs.tooltip', ({target}) ->
|
||||
jQuery.fn.setTooltip.getKeystroke = getKeystroke
|
||||
jQuery.fn.setTooltip.humanizeKeystrokes = humanizeKeystrokes
|
||||
|
||||
Object.defineProperty jQuery.fn, 'element', get: -> @[0]
|
||||
|
||||
module.exports = spacePen
|
||||
|
||||
4
src/subscriber-mixin.coffee
Normal file
4
src/subscriber-mixin.coffee
Normal file
@@ -0,0 +1,4 @@
|
||||
{Subscriber} = require 'emissary'
|
||||
SubscriberMixin = componentDidUnmount: -> @unsubscribe()
|
||||
Subscriber.extend(SubscriberMixin)
|
||||
module.exports = SubscriberMixin
|
||||
@@ -20,6 +20,8 @@ class Token
|
||||
scopes: null
|
||||
isAtomic: null
|
||||
isHardTab: null
|
||||
hasLeadingWhitespace: false
|
||||
hasTrailingWhitespace: false
|
||||
|
||||
constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @isHardTab}) ->
|
||||
@screenDelta = @value.length
|
||||
@@ -40,7 +42,7 @@ class Token
|
||||
whitespaceRegexForTabLength: (tabLength) ->
|
||||
WhitespaceRegexesByTabLength[tabLength] ?= new RegExp("([ ]{#{tabLength}})|(\t)|([^\t]+)", "g")
|
||||
|
||||
breakOutAtomicTokens: (tabLength, breakOutLeadingWhitespace) ->
|
||||
breakOutAtomicTokens: (tabLength, breakOutLeadingSoftTabs) ->
|
||||
if @hasSurrogatePair
|
||||
outputTokens = []
|
||||
|
||||
@@ -48,14 +50,14 @@ class Token
|
||||
if token.isAtomic
|
||||
outputTokens.push(token)
|
||||
else
|
||||
outputTokens.push(token.breakOutAtomicTokens(tabLength, breakOutLeadingWhitespace)...)
|
||||
breakOutLeadingWhitespace = token.isOnlyWhitespace() if breakOutLeadingWhitespace
|
||||
outputTokens.push(token.breakOutAtomicTokens(tabLength, breakOutLeadingSoftTabs)...)
|
||||
breakOutLeadingSoftTabs = token.isOnlyWhitespace() if breakOutLeadingSoftTabs
|
||||
|
||||
outputTokens
|
||||
else
|
||||
return [this] if @isAtomic
|
||||
|
||||
if breakOutLeadingWhitespace
|
||||
if breakOutLeadingSoftTabs
|
||||
return [this] unless /^[ ]|\t/.test(@value)
|
||||
else
|
||||
return [this] unless /\t/.test(@value)
|
||||
@@ -64,13 +66,13 @@ class Token
|
||||
regex = @whitespaceRegexForTabLength(tabLength)
|
||||
while match = regex.exec(@value)
|
||||
[fullMatch, softTab, hardTab] = match
|
||||
if softTab and breakOutLeadingWhitespace
|
||||
outputTokens.push(@buildSoftTabToken(tabLength, false))
|
||||
if softTab and breakOutLeadingSoftTabs
|
||||
outputTokens.push(@buildSoftTabToken(tabLength))
|
||||
else if hardTab
|
||||
breakOutLeadingWhitespace = false
|
||||
outputTokens.push(@buildHardTabToken(tabLength, true))
|
||||
breakOutLeadingSoftTabs = false
|
||||
outputTokens.push(@buildHardTabToken(tabLength))
|
||||
else
|
||||
breakOutLeadingWhitespace = false
|
||||
breakOutLeadingSoftTabs = false
|
||||
value = match[0]
|
||||
outputTokens.push(new Token({value, @scopes}))
|
||||
|
||||
@@ -127,7 +129,7 @@ class Token
|
||||
scopeClasses = scope.split('.')
|
||||
_.isSubset(targetClasses, scopeClasses)
|
||||
|
||||
getValueAsHtml: ({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide})->
|
||||
getValueAsHtml: ({invisibles, hasIndentGuide})->
|
||||
invisibles ?= {}
|
||||
if @isHardTab
|
||||
classes = 'hard-tab'
|
||||
@@ -142,7 +144,7 @@ class Token
|
||||
leadingHtml = ''
|
||||
trailingHtml = ''
|
||||
|
||||
if hasLeadingWhitespace and match = LeadingWhitespaceRegex.exec(@value)
|
||||
if @hasLeadingWhitespace and match = LeadingWhitespaceRegex.exec(@value)
|
||||
classes = 'leading-whitespace'
|
||||
classes += ' indent-guide' if hasIndentGuide
|
||||
classes += ' invisible-character' if invisibles.space
|
||||
@@ -152,9 +154,10 @@ class Token
|
||||
|
||||
startIndex = match[0].length
|
||||
|
||||
if hasTrailingWhitespace and match = TrailingWhitespaceRegex.exec(@value)
|
||||
if @hasTrailingWhitespace and match = TrailingWhitespaceRegex.exec(@value)
|
||||
tokenIsOnlyWhitespace = match[0].length is @value.length
|
||||
classes = 'trailing-whitespace'
|
||||
classes += ' indent-guide' if hasIndentGuide and not hasLeadingWhitespace
|
||||
classes += ' indent-guide' if hasIndentGuide and not @hasLeadingWhitespace and tokenIsOnlyWhitespace
|
||||
classes += ' invisible-character' if invisibles.space
|
||||
|
||||
match[0] = match[0].replace(CharacterRegex, invisibles.space) if invisibles.space
|
||||
|
||||
@@ -185,14 +185,16 @@ class TokenizedBuffer extends Model
|
||||
line = @buffer.lineForRow(row)
|
||||
tokens = [new Token(value: line, scopes: [@grammar.scopeName])]
|
||||
tabLength = @getTabLength()
|
||||
new TokenizedLine({tokens, tabLength})
|
||||
indentLevel = @indentLevelForRow(row)
|
||||
new TokenizedLine({tokens, tabLength, indentLevel})
|
||||
|
||||
buildTokenizedTokenizedLineForRow: (row, ruleStack) ->
|
||||
line = @buffer.lineForRow(row)
|
||||
lineEnding = @buffer.lineEndingForRow(row)
|
||||
tabLength = @getTabLength()
|
||||
indentLevel = @indentLevelForRow(row)
|
||||
{ tokens, ruleStack } = @grammar.tokenizeLine(line, ruleStack, row is 0)
|
||||
new TokenizedLine({tokens, ruleStack, tabLength, lineEnding})
|
||||
new TokenizedLine({tokens, ruleStack, tabLength, lineEnding, indentLevel})
|
||||
|
||||
# FIXME: benogle says: These are actually buffer rows as all buffer rows are
|
||||
# accounted for in @tokenizedLines
|
||||
@@ -207,6 +209,36 @@ class TokenizedBuffer extends Model
|
||||
stackForRow: (row) ->
|
||||
@tokenizedLines[row]?.ruleStack
|
||||
|
||||
indentLevelForRow: (row) ->
|
||||
line = @buffer.lineForRow(row)
|
||||
|
||||
if line is ''
|
||||
nextRow = row + 1
|
||||
lineCount = @getLineCount()
|
||||
while nextRow < lineCount
|
||||
nextLine = @buffer.lineForRow(nextRow)
|
||||
return @indentLevelForLine(nextLine) unless nextLine is ''
|
||||
nextRow++
|
||||
|
||||
previousRow = row - 1
|
||||
while previousRow >= 0
|
||||
previousLine = @buffer.lineForRow(previousRow)
|
||||
return @indentLevelForLine(previousLine) unless previousLine is ''
|
||||
previousRow--
|
||||
|
||||
0
|
||||
else
|
||||
@indentLevelForLine(line)
|
||||
|
||||
indentLevelForLine: (line) ->
|
||||
if match = line.match(/^[\t ]+/)
|
||||
leadingWhitespace = match[0]
|
||||
tabCount = leadingWhitespace.match(/\t/g)?.length ? 0
|
||||
spaceCount = leadingWhitespace.match(/[ ]/g)?.length ? 0
|
||||
tabCount + (spaceCount / @getTabLength())
|
||||
else
|
||||
0
|
||||
|
||||
scopesForPosition: (position) ->
|
||||
@tokenForPosition(position).scopes
|
||||
|
||||
@@ -306,6 +338,9 @@ class TokenizedBuffer extends Model
|
||||
getLastRow: ->
|
||||
@buffer.getLastRow()
|
||||
|
||||
getLineCount: ->
|
||||
@buffer.getLineCount()
|
||||
|
||||
logLines: (start=0, end=@buffer.getLastRow()) ->
|
||||
for row in [start..end]
|
||||
line = @lineForScreenRow(row).text
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
idCounter = 1
|
||||
|
||||
module.exports =
|
||||
class TokenizedLine
|
||||
constructor: ({tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold, tabLength}) ->
|
||||
@tokens = @breakOutAtomicTokens(tokens, tabLength)
|
||||
constructor: ({tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold, @tabLength, @indentLevel}) ->
|
||||
@tokens = @breakOutAtomicTokens(tokens)
|
||||
@startBufferColumn ?= 0
|
||||
@text = _.pluck(@tokens, 'value').join('')
|
||||
@bufferDelta = _.sum(_.pluck(@tokens, 'bufferDelta'))
|
||||
@id = idCounter++
|
||||
@markLeadingAndTrailingWhitespaceTokens()
|
||||
|
||||
copy: ->
|
||||
new TokenizedLine({@tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold})
|
||||
@@ -106,14 +110,25 @@ class TokenizedLine
|
||||
delta = nextDelta
|
||||
delta
|
||||
|
||||
breakOutAtomicTokens: (inputTokens, tabLength) ->
|
||||
breakOutAtomicTokens: (inputTokens) ->
|
||||
outputTokens = []
|
||||
breakOutLeadingWhitespace = true
|
||||
breakOutLeadingSoftTabs = true
|
||||
for token in inputTokens
|
||||
outputTokens.push(token.breakOutAtomicTokens(tabLength, breakOutLeadingWhitespace)...)
|
||||
breakOutLeadingWhitespace = token.isOnlyWhitespace() if breakOutLeadingWhitespace
|
||||
outputTokens.push(token.breakOutAtomicTokens(@tabLength, breakOutLeadingSoftTabs)...)
|
||||
breakOutLeadingSoftTabs = token.isOnlyWhitespace() if breakOutLeadingSoftTabs
|
||||
outputTokens
|
||||
|
||||
markLeadingAndTrailingWhitespaceTokens: ->
|
||||
firstNonWhitespacePosition = @text.search(/\S/)
|
||||
firstTrailingWhitespacePosition = @text.search(/\s*$/)
|
||||
lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0
|
||||
position = 0
|
||||
for token, i in @tokens
|
||||
token.hasLeadingWhitespace = position < firstNonWhitespacePosition
|
||||
# Only the *last* segment of a soft-wrapped line can have trailing whitespace
|
||||
token.hasTrailingWhitespace = @lineEnding? and (position + token.value.length > firstTrailingWhitespacePosition)
|
||||
position += token.value.length
|
||||
|
||||
isComment: ->
|
||||
for token in @tokens
|
||||
continue if token.scopes.length is 1
|
||||
@@ -134,3 +149,33 @@ class TokenizedLine
|
||||
for token in @tokens
|
||||
return column if token is targetToken
|
||||
column += token.bufferDelta
|
||||
|
||||
getScopeTree: ->
|
||||
return @scopeTree if @scopeTree?
|
||||
|
||||
scopeStack = []
|
||||
for token in @tokens
|
||||
@updateScopeStack(scopeStack, token.scopes)
|
||||
_.last(scopeStack).children.push(token)
|
||||
|
||||
@scopeTree = scopeStack[0]
|
||||
@updateScopeStack(scopeStack, [])
|
||||
@scopeTree
|
||||
|
||||
updateScopeStack: (scopeStack, desiredScopes) ->
|
||||
# Find a common prefix
|
||||
for scope, i in desiredScopes
|
||||
break unless scopeStack[i]?.scope is desiredScopes[i]
|
||||
|
||||
# Pop scopes until we're at the common prefx
|
||||
until scopeStack.length is i
|
||||
poppedScope = scopeStack.pop()
|
||||
_.last(scopeStack)?.children.push(poppedScope)
|
||||
|
||||
# Push onto common prefix until scopeStack equals desiredScopes
|
||||
for j in [i...desiredScopes.length]
|
||||
scopeStack.push(new Scope(desiredScopes[j]))
|
||||
|
||||
class Scope
|
||||
constructor: (@scope) ->
|
||||
@children = []
|
||||
|
||||
@@ -71,6 +71,7 @@ class WorkspaceView extends View
|
||||
projectHome: path.join(fs.getHomeDirectory(), 'github')
|
||||
audioBeep: true
|
||||
destroyEmptyPanes: true
|
||||
useReactEditor: false
|
||||
|
||||
@content: ->
|
||||
@div class: 'workspace', tabindex: -1, =>
|
||||
|
||||
Reference in New Issue
Block a user