Merge remote-tracking branch 'origin/master' into cj-add-deprecation-warning-to-specs

This commit is contained in:
Corey Johnson
2014-04-23 16:51:14 -07:00
40 changed files with 2552 additions and 99 deletions

View File

@@ -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

View File

@@ -311,6 +311,7 @@ class AtomApplication
if existingWindow
openedWindow = existingWindow
openedWindow.openPath(pathToOpen, initialLine)
openedWindow.restore()
else
if devMode
try

View File

@@ -151,6 +151,8 @@ class AtomWindow
maximize: -> @browserWindow.maximize()
restore: -> @browserWindow.restore()
handlesAtomCommands: ->
not @isSpecWindow() and @isWebViewFocused()

View File

@@ -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'))

View 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}

View File

@@ -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())

View 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()

View 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)

View File

@@ -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}.

View File

@@ -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
View 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()

View 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()

View File

@@ -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

View File

@@ -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")

View 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('&nbsp;', maxDigits - lineNumber.length)
padding + lineNumber + @iconDivHTML
else
lineNumber + @iconDivHTML
iconDivHTML: '<div class="icon-right"></div>'
shouldComponentUpdate: (newProps) ->
not isEqualForProperties(newProps, @props, 'lineHeight', 'screenRow')

View 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()

View File

@@ -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
View 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
"&nbsp;"
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')

View File

@@ -290,6 +290,7 @@ class Package
$(event.target).trigger(event)
@restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
@unsubscribeFromActivationEvents()
false
unsubscribeFromActivationEvents: ->
return unless atom.workspaceView?

View 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

View 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)

View 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

View File

@@ -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)

View 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})

View File

@@ -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

View File

@@ -0,0 +1,4 @@
{Subscriber} = require 'emissary'
SubscriberMixin = componentDidUnmount: -> @unsubscribe()
Subscriber.extend(SubscriberMixin)
module.exports = SubscriberMixin

View File

@@ -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

View File

@@ -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

View File

@@ -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 = []

View File

@@ -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, =>