Files
atom/src/editor-view.coffee
2014-05-27 12:58:17 -07:00

1576 lines
54 KiB
CoffeeScript

{View, $, $$$} = require './space-pen-extensions'
GutterView = require './gutter-view'
{Point, Range} = require 'text-buffer'
Editor = require './editor'
CursorView = require './cursor-view'
SelectionView = require './selection-view'
fs = require 'fs-plus'
_ = require 'underscore-plus'
TextBuffer = require 'text-buffer'
MeasureRange = document.createRange()
TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT }
NoScope = ['no-scope']
LongLineLength = 1000
# Public: Represents the entire visual pane in Atom.
#
# The EditorView manages the {Editor}, which manages the file buffers.
#
# ## Requiring in packages
#
# ```coffee
# {EditorView} = require 'atom'
#
# miniEditorView = new EditorView(mini: true)
# ```
#
# ## Iterating over the open editor views
#
# ```coffee
# for editorView in atom.workspaceView.getEditorViews()
# console.log(editorView.getEditor().getPath())
# ```
#
# ## Subscribing to every current and future editor
#
# ```coffee
# atom.workspace.eachEditorView (editorView) ->
# console.log(editorView.getEditor().getPath())
# ```
module.exports =
class EditorView extends View
@characterWidthCache: {}
@configDefaults:
fontFamily: ''
fontSize: 16
lineHeight: 1.3
showInvisibles: false
showIndentGuide: false
showLineNumbers: true
autoIndent: true
normalizeIndentOnPaste: true
nonWordCharacters: "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-"
preferredLineLength: 80
tabLength: 2
softWrap: false
softTabs: true
softWrapAtPreferredLineLength: false
@nextEditorId: 1
@content: (params) ->
attributes = { class: @classes(params), tabindex: -1 }
_.extend(attributes, params.attributes) if params.attributes
@div attributes, =>
@subview 'gutter', new GutterView
@div class: 'scroll-view', outlet: 'scrollView', =>
@div class: 'overlayer', outlet: 'overlayer'
@div class: 'lines', outlet: 'renderedLines'
@div class: 'underlayer', outlet: 'underlayer', =>
@input class: 'hidden-input', outlet: 'hiddenInput'
@div class: 'vertical-scrollbar', outlet: 'verticalScrollbar', =>
@div outlet: 'verticalScrollbarContent'
@classes: ({mini} = {}) ->
classes = ['editor', 'editor-colors']
classes.push 'mini' if mini
classes.join(' ')
vScrollMargin: 2
hScrollMargin: 10
lineHeight: null
charWidth: null
charHeight: null
cursorViews: null
selectionViews: null
lineCache: null
isFocused: false
editor: null
attached: false
lineOverdraw: 10
pendingChanges: null
newCursors: null
newSelections: null
redrawOnReattach: false
bottomPaddingInLines: 10
# The constructor for setting up an `EditorView` instance.
#
# editorOrOptions - Either an {Editor}, or an object with one property, `mini`.
# If `mini` is `true`, a "miniature" `Editor` is constructed.
# Typically, this is ideal for scenarios where you need an Atom editor,
# but without all the chrome, like scrollbars, gutter, _e.t.c._.
#
initialize: (editorOrOptions) ->
if editorOrOptions instanceof Editor
editor = editorOrOptions
else
{editor, @mini, placeholderText} = editorOrOptions ? {}
@id = EditorView.nextEditorId++
@lineCache = []
@configure()
@bindKeys()
@handleEvents()
@handleInputEvents()
@cursorViews = []
@selectionViews = []
@pendingChanges = []
@newCursors = []
@newSelections = []
@setPlaceholderText(placeholderText) if placeholderText
if editor?
@edit(editor)
else if @mini
@edit(new Editor
buffer: new TextBuffer
softWrap: false
tabLength: 2
softTabs: true
)
else
throw new Error("Must supply an Editor or mini: true")
# Sets up the core Atom commands.
#
# Some commands are excluded from mini-editors.
bindKeys: ->
editorBindings =
'core:move-left': => @editor.moveCursorLeft()
'core:move-right': => @editor.moveCursorRight()
'core:select-left': => @editor.selectLeft()
'core:select-right': => @editor.selectRight()
'core:select-all': => @editor.selectAll()
'core:backspace': => @editor.backspace()
'core:delete': => @editor.delete()
'core:undo': => @editor.undo()
'core:redo': => @editor.redo()
'core:cut': => @editor.cutSelectedText()
'core:copy': => @editor.copySelectedText()
'core:paste': => @editor.pasteText()
'editor:move-to-previous-word': => @editor.moveCursorToPreviousWord()
'editor:select-word': => @editor.selectWord()
'editor:consolidate-selections': (event) => @consolidateSelections(event)
'editor:delete-to-beginning-of-word': => @editor.deleteToBeginningOfWord()
'editor:delete-to-beginning-of-line': => @editor.deleteToBeginningOfLine()
'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-next-paragraph': => @editor.moveCursorToBeginningOfNextParagraph()
'editor:move-to-beginning-of-previous-paragraph': => @editor.moveCursorToBeginningOfPreviousParagraph()
'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
_.extend editorBindings,
'core:move-up': => @editor.moveCursorUp()
'core:move-down': => @editor.moveCursorDown()
'core:move-to-top': => @editor.moveCursorToTop()
'core:move-to-bottom': => @editor.moveCursorToBottom()
'core:page-down': => @pageDown()
'core:page-up': => @pageUp()
'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': => @toggleSoftTabs()
'editor:toggle-soft-wrap': => @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': => @editor.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': => @toggleLineCommentsInSelection()
'editor:log-cursor-scope': => @logCursorScope()
'editor:checkout-head-revision': => @checkoutHead()
'editor:copy-path': => @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': => @scrollToCursorPosition()
documentation = {}
for name, method of editorBindings
do (name, method) =>
@command name, (e) -> method(e); false
# Public: Get the underlying editor model for this view.
#
# Returns an {Editor}.
getEditor: ->
@editor
# {Delegates to: Editor.getText}
getText: ->
@editor.getText()
# {Delegates to: Editor.setText}
setText: (text) ->
@editor.setText(text)
# {Delegates to: Editor.insertText}
insertText: (text, options) ->
@editor.insertText(text, options)
setHeightInLines: (heightInLines)->
heightInLines ?= @calculateHeightInLines()
@heightInLines = heightInLines if heightInLines
# {Delegates to: Editor.setEditorWidthInChars}
setWidthInChars: (widthInChars) ->
widthInChars ?= @calculateWidthInChars()
@editor.setEditorWidthInChars(widthInChars) if widthInChars
# Public: Emulates the "page down" key, where the last row of a buffer scrolls
# to become the first.
pageDown: ->
newScrollTop = @scrollTop() + @scrollView[0].clientHeight
@editor.moveCursorDown(@getPageRows())
@scrollTop(newScrollTop, adjustVerticalScrollbar: true)
# Public: Emulates the "page up" key, where the frst row of a buffer scrolls
# to become the last.
pageUp: ->
newScrollTop = @scrollTop() - @scrollView[0].clientHeight
@editor.moveCursorUp(@getPageRows())
@scrollTop(newScrollTop, adjustVerticalScrollbar: true)
# Public: Gets the number of actual page rows existing in an editor.
#
# Returns a {Number}.
getPageRows: ->
Math.max(1, Math.ceil(@scrollView[0].clientHeight / @lineHeight))
# Public: Set whether invisible characters are shown.
#
# showInvisibles - A {Boolean} which, if `true`, show invisible characters.
setShowInvisibles: (showInvisibles) ->
return if showInvisibles == @showInvisibles
@showInvisibles = showInvisibles
@resetDisplay()
# Public: Defines which characters are invisible.
#
# invisibles - An {Object} defining the invisible characters:
# :eol - The end of line invisible {String} (default: `\u00ac`).
# :space - The space invisible {String} (default: `\u00b7`).
# :tab - The tab invisible {String} (default: `\u00bb`).
# :cr - The carriage return invisible {String} (default: `\u00a4`).
setInvisibles: (@invisibles={}) ->
_.defaults @invisibles,
eol: '\u00ac'
space: '\u00b7'
tab: '\u00bb'
cr: '\u00a4'
@resetDisplay()
# Public: Sets whether you want to show the indentation guides.
#
# showIndentGuide - A {Boolean} you can set to `true` if you want to see the
# indentation guides.
setShowIndentGuide: (showIndentGuide) ->
return if showIndentGuide == @showIndentGuide
@showIndentGuide = showIndentGuide
@resetDisplay()
# Public: Set the text to appear in the editor when it is empty.
#
# This only affects mini editors.
#
# placeholderText - A {String} of text to display when empty.
setPlaceholderText: (placeholderText) ->
return unless @mini
@placeholderText = placeholderText
@requestDisplayUpdate()
getPlaceholderText: ->
@placeholderText
# Checkout the HEAD revision of this editor's file.
checkoutHead: ->
if path = @editor.getPath()
atom.project.getRepo()?.checkoutHead(path)
configure: ->
@subscribe atom.config.observe 'editor.showLineNumbers', (showLineNumbers) => @gutter.setShowLineNumbers(showLineNumbers)
@subscribe atom.config.observe 'editor.showInvisibles', (showInvisibles) => @setShowInvisibles(showInvisibles)
@subscribe atom.config.observe 'editor.showIndentGuide', (showIndentGuide) => @setShowIndentGuide(showIndentGuide)
@subscribe atom.config.observe 'editor.invisibles', (invisibles) => @setInvisibles(invisibles)
@subscribe atom.config.observe 'editor.fontSize', (fontSize) => @setFontSize(fontSize)
@subscribe atom.config.observe 'editor.fontFamily', (fontFamily) => @setFontFamily(fontFamily)
@subscribe atom.config.observe 'editor.lineHeight', (lineHeight) => @setLineHeight(lineHeight)
handleEvents: ->
@on 'focus', =>
@hiddenInput.focus()
false
@hiddenInput.on 'focus', =>
@bringHiddenInputIntoView()
@isFocused = true
@addClass 'is-focused'
@hiddenInput.on 'focusout', =>
@bringHiddenInputIntoView()
@isFocused = false
@removeClass 'is-focused'
@underlayer.on 'mousedown', (e) =>
@renderedLines.trigger(e)
false if @isFocused
@overlayer.on 'mousedown', (e) =>
@overlayer.hide()
clickedElement = document.elementFromPoint(e.pageX, e.pageY)
@overlayer.show()
e.target = clickedElement
$(clickedElement).trigger(e)
false if @isFocused
@renderedLines.on 'mousedown', '.fold.line', (e) =>
id = $(e.currentTarget).attr('fold-id')
marker = @editor.displayBuffer.getMarker(id)
@editor.setCursorBufferPosition(marker.getBufferRange().start)
@editor.destroyFoldWithId(id)
false
@gutter.on 'mousedown', '.foldable .icon-right', (e) =>
bufferRow = $(e.target).parent().data('bufferRow')
@editor.toggleFoldAtBufferRow(bufferRow)
false
@renderedLines.on 'mousedown', (e) =>
clickCount = e.originalEvent.detail
screenPosition = @screenPositionFromMouseEvent(e)
if clickCount == 1
if e.metaKey or (process.platform isnt 'darwin' and e.ctrlKey)
@editor.addCursorAtScreenPosition(screenPosition)
else if e.shiftKey
@editor.selectToScreenPosition(screenPosition)
else
@editor.setCursorScreenPosition(screenPosition)
else if clickCount == 2
@editor.selectWord() unless e.shiftKey
else if clickCount == 3
@editor.selectLine() unless e.shiftKey
@selectOnMousemoveUntilMouseup() unless e.ctrlKey or e.originalEvent.which > 1
unless @mini
@scrollView.on 'mousewheel', (e) =>
if delta = e.originalEvent.wheelDeltaY
@scrollTop(@scrollTop() - delta)
false
@verticalScrollbar.on 'scroll', =>
@scrollTop(@verticalScrollbar.scrollTop(), adjustVerticalScrollbar: false)
@scrollView.on 'scroll', =>
if @scrollLeft() == 0
@gutter.removeClass('drop-shadow')
else
@gutter.addClass('drop-shadow')
# Listen for overflow events to detect when the editor's width changes
# to update the soft wrap column.
updateWidthInChars = _.debounce((=> @setWidthInChars()), 100)
@scrollView.on 'overflowchanged', =>
updateWidthInChars() if @[0].classList.contains('soft-wrap')
@subscribe atom.themes, 'stylesheets-changed', => @recalculateDimensions()
handleInputEvents: ->
@on 'cursor:moved', =>
return unless @isFocused
cursorView = @getCursorView()
if cursorView.isVisible()
# This is an order of magnitude faster than checking .offset().
style = cursorView[0].style
@hiddenInput[0].style.top = style.top
@hiddenInput[0].style.left = style.left
selectedText = null
@hiddenInput.on 'compositionstart', =>
selectedText = @editor.getSelectedText()
@hiddenInput.css('width', '100%')
@hiddenInput.on 'compositionupdate', (e) =>
@editor.insertText(e.originalEvent.data, {select: true, undo: 'skip'})
@hiddenInput.on 'compositionend', =>
@editor.insertText(selectedText, {select: true, undo: 'skip'})
@hiddenInput.css('width', '1px')
lastInput = ''
@on "textInput", (e) =>
# Work around of the accented character suggestion feature in OS X.
selectedLength = @hiddenInput[0].selectionEnd - @hiddenInput[0].selectionStart
if selectedLength is 1 and lastInput is @hiddenInput.val()
@editor.selectLeft()
lastInput = e.originalEvent.data
@editor.insertText(lastInput)
if lastInput is ' '
true # Prevents parent elements from scrolling when a space is typed
else
@hiddenInput.val(lastInput)
false
# Ignore paste event, on Linux is wrongly emitted when user presses ctrl-v.
@on "paste", -> false
bringHiddenInputIntoView: ->
@hiddenInput.css(top: @scrollTop(), left: @scrollLeft())
selectOnMousemoveUntilMouseup: ->
lastMoveEvent = null
finalizeSelections = =>
clearInterval(interval)
$(document).off 'mousemove', moveHandler
$(document).off 'mouseup', finalizeSelections
unless @editor.isDestroyed()
@editor.mergeIntersectingSelections(reversed: @editor.getLastSelection().isReversed())
@editor.finalizeSelections()
@syncCursorAnimations()
moveHandler = (event = lastMoveEvent) =>
return unless event?
if event.which is 1 and @[0].style.display isnt 'none'
@editor.selectToScreenPosition(@screenPositionFromMouseEvent(event))
lastMoveEvent = event
else
finalizeSelections()
$(document).on "mousemove.editor-#{@id}", moveHandler
interval = setInterval(moveHandler, 20)
$(document).one "mouseup.editor-#{@id}", finalizeSelections
afterAttach: (onDom) ->
return unless onDom
# TODO: Remove this guard when we understand why this is happening
unless @editor.isAlive()
if atom.isReleasedVersion()
return
else
throw new Error("Assertion failure: EditorView is getting attached to a dead editor. Why?")
@redraw() if @redrawOnReattach
return if @attached
@attached = true
@calculateDimensions()
@setWidthInChars()
@subscribe $(window), "resize.editor-#{@id}", =>
@setHeightInLines()
@setWidthInChars()
@updateLayerDimensions()
@requestDisplayUpdate()
@focus() if @isFocused
if pane = @getPane()
@active = @is(pane.activeView)
@subscribe pane, 'pane:active-item-changed', (event, item) =>
wasActive = @active
@active = @is(pane.activeView)
@redraw() if @active and not wasActive
@resetDisplay()
@trigger 'editor:attached', [this]
edit: (editor) ->
return if editor is @editor
if @editor
@saveScrollPositionForEditor()
@unsubscribe(@editor)
@editor = editor
return unless @editor?
@editor.setVisible(true)
@subscribe @editor, "destroyed", =>
@remove()
@subscribe @editor, "contents-conflicted", =>
@showBufferConflictAlert(@editor)
@subscribe @editor, "path-changed", =>
@trigger 'editor:path-changed'
@subscribe @editor, "grammar-changed", =>
@trigger 'editor:grammar-changed'
@subscribe @editor, 'selection-added', (selection) =>
@newCursors.push(selection.cursor)
@newSelections.push(selection)
@requestDisplayUpdate()
@subscribe @editor, 'screen-lines-changed', (e) =>
@handleScreenLinesChange(e)
@subscribe @editor, 'scroll-top-changed', (scrollTop) =>
@scrollTop(scrollTop)
@subscribe @editor, 'scroll-left-changed', (scrollLeft) =>
@scrollLeft(scrollLeft)
@subscribe @editor, 'soft-wrap-changed', (softWrap) =>
@setSoftWrap(softWrap)
@trigger 'editor:path-changed'
@resetDisplay()
if @attached and @editor.buffer.isInConflict()
_.defer => @showBufferConflictAlert(@editor) # Display after editor has a chance to display
getModel: ->
@editor
setModel: (editor) ->
@edit(editor)
showBufferConflictAlert: (editor) ->
atom.confirm
message: editor.getPath()
detailedMessage: "Has changed on disk. Do you want to reload it?"
buttons:
Reload: -> editor.getBuffer().reload()
Cancel: null
scrollTop: (scrollTop, options={}) ->
return @cachedScrollTop or 0 unless scrollTop?
maxScrollTop = @verticalScrollbar.prop('scrollHeight') - @verticalScrollbar.height()
scrollTop = Math.floor(Math.max(0, Math.min(maxScrollTop, scrollTop)))
return if scrollTop == @cachedScrollTop
@cachedScrollTop = scrollTop
@updateDisplay() if @attached
@renderedLines.css('top', -scrollTop)
@underlayer.css('top', -scrollTop)
@overlayer.css('top', -scrollTop)
@gutter.lineNumbers.css('top', -scrollTop)
if options?.adjustVerticalScrollbar ? true
@verticalScrollbar.scrollTop(scrollTop)
@editor.setScrollTop(@scrollTop())
scrollBottom: (scrollBottom) ->
if scrollBottom?
@scrollTop(scrollBottom - @scrollView.height())
else
@scrollTop() + @scrollView.height()
scrollLeft: (scrollLeft) ->
if scrollLeft?
@scrollView.scrollLeft(scrollLeft)
@editor.setScrollLeft(@scrollLeft())
else
@scrollView.scrollLeft()
scrollRight: (scrollRight) ->
if scrollRight?
@scrollView.scrollRight(scrollRight)
@editor.setScrollLeft(@scrollLeft())
else
@scrollView.scrollRight()
# Public: Scrolls the editor to the bottom.
scrollToBottom: ->
@scrollBottom(@editor.getScreenLineCount() * @lineHeight)
# Public: Scrolls the editor to the position of the most recently added
# cursor.
#
# The editor is also centered.
scrollToCursorPosition: ->
@scrollToBufferPosition(@editor.getCursorBufferPosition(), center: true)
# Public: Scrolls the editor to the given buffer position.
#
# bufferPosition - An object that represents a buffer position. It can be either
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
# options - A hash matching the options available to {::scrollToPixelPosition}
scrollToBufferPosition: (bufferPosition, options) ->
@scrollToPixelPosition(@pixelPositionForBufferPosition(bufferPosition), options)
# Public: Scrolls the editor to the given screen position.
#
# screenPosition - An object that represents a buffer position. It can be either
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
# options - A hash matching the options available to {::scrollToPixelPosition}
scrollToScreenPosition: (screenPosition, options) ->
@scrollToPixelPosition(@pixelPositionForScreenPosition(screenPosition), options)
# Public: Scrolls the editor to the given pixel position.
#
# pixelPosition - An object that represents a pixel position. It can be either
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or
# {Point}.
# options - A hash with the following keys:
# :center - if `true`, the position is scrolled such that it's in
# the center of the editor
scrollToPixelPosition: (pixelPosition, options) ->
return unless @attached
@scrollVertically(pixelPosition, options)
@scrollHorizontally(pixelPosition)
# Public: Highlight all the folds within the given buffer range.
#
# "Highlighting" essentially just adds the `fold-selected` class to the line's
# DOM element.
#
# bufferRange - The {Range} to check.
highlightFoldsContainingBufferRange: (bufferRange) ->
screenLines = @editor.linesForScreenRows(@firstRenderedScreenRow, @lastRenderedScreenRow)
for screenLine, i in screenLines
if fold = screenLine.fold
screenRow = @firstRenderedScreenRow + i
element = @lineElementForScreenRow(screenRow)
if bufferRange.intersectsWith(fold.getBufferRange())
element.addClass('fold-selected')
else
element.removeClass('fold-selected')
saveScrollPositionForEditor: ->
if @attached
@editor.setScrollTop(@scrollTop())
@editor.setScrollLeft(@scrollLeft())
# Public: Toggle soft tabs on the edit session.
toggleSoftTabs: ->
@editor.setSoftTabs(not @editor.getSoftTabs())
# Public: Toggle soft wrap on the edit session.
toggleSoftWrap: ->
@setWidthInChars()
@editor.setSoftWrap(not @editor.getSoftWrap())
calculateWidthInChars: ->
Math.floor((@scrollView.width() - @getScrollbarWidth()) / @charWidth)
calculateHeightInLines: ->
Math.ceil($(window).height() / @lineHeight)
getScrollbarWidth: ->
scrollbarElement = @verticalScrollbar[0]
scrollbarElement.offsetWidth - scrollbarElement.clientWidth
# Public: Enables/disables soft wrap on the editor.
#
# softWrap - A {Boolean} which, if `true`, enables soft wrap
setSoftWrap: (softWrap) ->
if softWrap
@addClass 'soft-wrap'
@scrollLeft(0)
else
@removeClass 'soft-wrap'
# Public: Sets the font size for the editor.
#
# fontSize - A {Number} indicating the font size in pixels.
setFontSize: (fontSize) ->
@css('font-size', "#{fontSize}px")
@clearCharacterWidthCache()
if @isOnDom()
@redraw()
else
@redrawOnReattach = @attached
# Public: Retrieves the font size for the editor.
#
# Returns a {Number} indicating the font size in pixels.
getFontSize: ->
parseInt(@css("font-size"))
# Public: Sets the font family for the editor.
#
# fontFamily - A {String} identifying the CSS `font-family`.
setFontFamily: (fontFamily='') ->
@css('font-family', fontFamily)
@clearCharacterWidthCache()
@redraw()
# Public: Gets the font family for the editor.
#
# Returns a {String} identifying the CSS `font-family`.
getFontFamily: -> @css("font-family")
# Public: Sets the line height of the editor.
#
# Calling this method has no effect when called on a mini editor.
#
# lineHeight - A {Number} without a unit suffix identifying the CSS
# `line-height`.
setLineHeight: (lineHeight) ->
return if @mini
@css('line-height', lineHeight)
@redraw()
# Public: Redraw the editor
redraw: ->
return unless @hasParent()
return unless @attached
@redrawOnReattach = false
@calculateDimensions()
@updatePaddingOfRenderedLines()
@updateLayerDimensions()
@requestDisplayUpdate()
# Public: Split the editor view left.
splitLeft: ->
pane = @getPane()
pane?.splitLeft(pane?.copyActiveItem()).activeView
# Public: Split the editor view right.
splitRight: ->
pane = @getPane()
pane?.splitRight(pane?.copyActiveItem()).activeView
# Public: Split the editor view up.
splitUp: ->
pane = @getPane()
pane?.splitUp(pane?.copyActiveItem()).activeView
# Public: Split the editor view down.
splitDown: ->
pane = @getPane()
pane?.splitDown(pane?.copyActiveItem()).activeView
# Public: Get this view's pane.
#
# Returns a {Pane}.
getPane: ->
@parent('.item-views').parents('.pane').view()
remove: (selector, keepData) ->
return super if keepData or @removed
super
atom.workspaceView?.focus()
beforeRemove: ->
@trigger 'editor:will-be-removed'
@removed = true
@editor?.destroy()
$(window).off(".editor-#{@id}")
$(document).off(".editor-#{@id}")
getCursorView: (index) ->
index ?= @cursorViews.length - 1
@cursorViews[index]
getCursorViews: ->
new Array(@cursorViews...)
addCursorView: (cursor, options) ->
cursorView = new CursorView(cursor, this, options)
@cursorViews.push(cursorView)
@overlayer.append(cursorView)
cursorView
removeCursorView: (cursorView) ->
_.remove(@cursorViews, cursorView)
getSelectionView: (index) ->
index ?= @selectionViews.length - 1
@selectionViews[index]
getSelectionViews: ->
new Array(@selectionViews...)
addSelectionView: (selection) ->
selectionView = new SelectionView({editorView: this, selection})
@selectionViews.push(selectionView)
@underlayer.append(selectionView)
selectionView
removeSelectionView: (selectionView) ->
_.remove(@selectionViews, selectionView)
removeAllCursorAndSelectionViews: ->
cursorView.remove() for cursorView in @getCursorViews()
selectionView.remove() for selectionView in @getSelectionViews()
appendToLinesView: (view) ->
@overlayer.append(view)
# Scrolls the editor vertically to a given position.
scrollVertically: (pixelPosition, {center}={}) ->
scrollViewHeight = @scrollView.height()
scrollTop = @scrollTop()
scrollBottom = scrollTop + scrollViewHeight
if center
unless scrollTop < pixelPosition.top < scrollBottom
@scrollTop(pixelPosition.top - (scrollViewHeight / 2))
else
linesInView = @scrollView.height() / @lineHeight
maxScrollMargin = Math.floor((linesInView - 1) / 2)
scrollMargin = Math.min(@vScrollMargin, maxScrollMargin)
margin = scrollMargin * @lineHeight
desiredTop = pixelPosition.top - margin
desiredBottom = pixelPosition.top + @lineHeight + margin
if desiredBottom > scrollBottom
@scrollTop(desiredBottom - scrollViewHeight)
else if desiredTop < scrollTop
@scrollTop(desiredTop)
# Scrolls the editor horizontally to a given position.
scrollHorizontally: (pixelPosition) ->
return if @editor.getSoftWrap()
charsInView = @scrollView.width() / @charWidth
maxScrollMargin = Math.floor((charsInView - 1) / 2)
scrollMargin = Math.min(@hScrollMargin, maxScrollMargin)
margin = scrollMargin * @charWidth
desiredRight = pixelPosition.left + @charWidth + margin
desiredLeft = pixelPosition.left - margin
if desiredRight > @scrollRight()
@scrollRight(desiredRight)
else if desiredLeft < @scrollLeft()
@scrollLeft(desiredLeft)
@saveScrollPositionForEditor()
calculateDimensions: ->
fragment = $('<div class="line" style="position: absolute; visibility: hidden;"><span>x</span></div>')
@renderedLines.append(fragment)
lineRect = fragment[0].getBoundingClientRect()
charRect = fragment.find('span')[0].getBoundingClientRect()
@lineHeight = lineRect.height
@charWidth = charRect.width
@charHeight = charRect.height
fragment.remove()
@setHeightInLines()
recalculateDimensions: ->
return unless @attached and @isVisible()
oldCharWidth = @charWidth
oldLineHeight = @lineHeight
@calculateDimensions()
unless @charWidth is oldCharWidth and @lineHeight is oldLineHeight
@clearCharacterWidthCache()
@requestDisplayUpdate()
updateLayerDimensions: (scrollViewWidth) ->
height = @lineHeight * @editor.getScreenLineCount()
unless @layerHeight == height
@layerHeight = height
@underlayer.height(@layerHeight)
@renderedLines.height(@layerHeight)
@overlayer.height(@layerHeight)
@verticalScrollbarContent.height(@layerHeight)
@scrollBottom(height) if @scrollBottom() > height
minWidth = Math.max(@charWidth * @editor.getMaxScreenLineLength() + 20, scrollViewWidth)
unless @layerMinWidth == minWidth
@renderedLines.css('min-width', minWidth)
@underlayer.css('min-width', minWidth)
@overlayer.css('min-width', minWidth)
@layerMinWidth = minWidth
@trigger 'editor:min-width-changed'
# Override for speed. The base function checks computedStyle, unnecessary here.
isHidden: ->
style = this[0].style
if style.display == 'none' or not @isOnDom()
true
else
false
clearRenderedLines: ->
@renderedLines.empty()
@firstRenderedScreenRow = null
@lastRenderedScreenRow = null
resetDisplay: ->
return unless @attached
@clearRenderedLines()
@removeAllCursorAndSelectionViews()
editorScrollTop = @editor.getScrollTop() ? 0
editorScrollLeft = @editor.getScrollLeft() ? 0
@updateLayerDimensions()
@scrollTop(editorScrollTop)
@scrollLeft(editorScrollLeft)
@setSoftWrap(@editor.getSoftWrap())
@newCursors = @editor.getCursors()
@newSelections = @editor.getSelections()
@updateDisplay(suppressAutoscroll: true)
requestDisplayUpdate: ->
return if @pendingDisplayUpdate
return unless @isVisible()
@pendingDisplayUpdate = true
setImmediate =>
@updateDisplay()
@pendingDisplayUpdate = false
updateDisplay: (options) ->
return unless @attached and @editor
return if @editor.isDestroyed()
unless @isOnDom() and @isVisible()
@redrawOnReattach = true
return
scrollViewWidth = @scrollView.width()
@updateRenderedLines(scrollViewWidth)
@updatePlaceholderText()
@highlightCursorLine()
@updateCursorViews()
@updateSelectionViews()
@autoscroll(options?.suppressAutoscroll ? false)
@trigger 'editor:display-updated'
updateCursorViews: ->
if @newCursors.length > 0
@addCursorView(cursor) for cursor in @newCursors when not cursor.destroyed
@syncCursorAnimations()
@newCursors = []
for cursorView in @getCursorViews()
if cursorView.needsRemoval
cursorView.remove()
else if @shouldUpdateCursor(cursorView)
cursorView.updateDisplay()
shouldUpdateCursor: (cursorView) ->
return false unless cursorView.needsUpdate
pos = cursorView.getScreenPosition()
pos.row >= @firstRenderedScreenRow and pos.row <= @lastRenderedScreenRow
updateSelectionViews: ->
if @newSelections.length > 0
@addSelectionView(selection) for selection in @newSelections when not selection.destroyed
@newSelections = []
for selectionView in @getSelectionViews()
if selectionView.needsRemoval
selectionView.remove()
else if @shouldUpdateSelection(selectionView)
selectionView.updateDisplay()
shouldUpdateSelection: (selectionView) ->
screenRange = selectionView.getScreenRange()
startRow = screenRange.start.row
endRow = screenRange.end.row
(startRow >= @firstRenderedScreenRow and startRow <= @lastRenderedScreenRow) or # startRow in range
(endRow >= @firstRenderedScreenRow and endRow <= @lastRenderedScreenRow) or # endRow in range
(startRow <= @firstRenderedScreenRow and endRow >= @lastRenderedScreenRow) # selection surrounds the rendered items
syncCursorAnimations: ->
cursorView.resetBlinking() for cursorView in @getCursorViews()
autoscroll: (suppressAutoscroll) ->
for cursorView in @getCursorViews()
if !suppressAutoscroll and cursorView.needsAutoscroll()
@scrollToPixelPosition(cursorView.getPixelPosition())
cursorView.clearAutoscroll()
for selectionView in @getSelectionViews()
if !suppressAutoscroll and selectionView.needsAutoscroll()
@scrollToPixelPosition(selectionView.getCenterPixelPosition(), center: true)
selectionView.highlight()
selectionView.clearAutoscroll()
updatePlaceholderText: ->
return unless @mini
if (not @placeholderText) or @editor.getText()
@find('.placeholder-text').remove()
else if @placeholderText and not @editor.getText()
element = @find('.placeholder-text')
if element.length
element.text(@placeholderText)
else
@underlayer.append($('<span/>', class: 'placeholder-text', text: @placeholderText))
updateRenderedLines: (scrollViewWidth) ->
firstVisibleScreenRow = @getFirstVisibleScreenRow()
lastScreenRowToRender = firstVisibleScreenRow + @heightInLines - 1
lastScreenRow = @editor.getLastScreenRow()
if @firstRenderedScreenRow? and firstVisibleScreenRow >= @firstRenderedScreenRow and lastScreenRowToRender <= @lastRenderedScreenRow
renderFrom = Math.min(lastScreenRow, @firstRenderedScreenRow)
renderTo = Math.min(lastScreenRow, @lastRenderedScreenRow)
else
renderFrom = Math.min(lastScreenRow, Math.max(0, firstVisibleScreenRow - @lineOverdraw))
renderTo = Math.min(lastScreenRow, lastScreenRowToRender + @lineOverdraw)
if @pendingChanges.length == 0 and @firstRenderedScreenRow and @firstRenderedScreenRow <= renderFrom and renderTo <= @lastRenderedScreenRow
return
changes = @pendingChanges
intactRanges = @computeIntactRanges(renderFrom, renderTo)
@gutter.updateLineNumbers(changes, renderFrom, renderTo)
@clearDirtyRanges(intactRanges)
@fillDirtyRanges(intactRanges, renderFrom, renderTo)
@firstRenderedScreenRow = renderFrom
@lastRenderedScreenRow = renderTo
@updateLayerDimensions(scrollViewWidth)
@updatePaddingOfRenderedLines()
computeSurroundingEmptyLineChanges: (change) ->
emptyLineChanges = []
if change.bufferDelta?
afterStart = change.end + change.bufferDelta + 1
if @editor.lineForBufferRow(afterStart) is ''
afterEnd = afterStart
afterEnd++ while @editor.lineForBufferRow(afterEnd + 1) is ''
emptyLineChanges.push({start: afterStart, end: afterEnd, screenDelta: 0})
beforeEnd = change.start - 1
if @editor.lineForBufferRow(beforeEnd) is ''
beforeStart = beforeEnd
beforeStart-- while @editor.lineForBufferRow(beforeStart - 1) is ''
emptyLineChanges.push({start: beforeStart, end: beforeEnd, screenDelta: 0})
emptyLineChanges
computeIntactRanges: (renderFrom, renderTo) ->
return [] if !@firstRenderedScreenRow? and !@lastRenderedScreenRow?
intactRanges = [{start: @firstRenderedScreenRow, end: @lastRenderedScreenRow, domStart: 0}]
if not @mini and @showIndentGuide
emptyLineChanges = []
for change in @pendingChanges
emptyLineChanges.push(@computeSurroundingEmptyLineChanges(change)...)
@pendingChanges.push(emptyLineChanges...)
for change in @pendingChanges
newIntactRanges = []
for range in intactRanges
if change.end < range.start and change.screenDelta != 0
newIntactRanges.push(
start: range.start + change.screenDelta
end: range.end + change.screenDelta
domStart: range.domStart
)
else if change.end < range.start or change.start > range.end
newIntactRanges.push(range)
else
if change.start > range.start
newIntactRanges.push(
start: range.start
end: change.start - 1
domStart: range.domStart)
if change.end < range.end
newIntactRanges.push(
start: change.end + change.screenDelta + 1
end: range.end + change.screenDelta
domStart: range.domStart + change.end + 1 - range.start
)
intactRanges = newIntactRanges
@truncateIntactRanges(intactRanges, renderFrom, renderTo)
@pendingChanges = []
intactRanges
truncateIntactRanges: (intactRanges, renderFrom, renderTo) ->
i = 0
while i < intactRanges.length
range = intactRanges[i]
if range.start < renderFrom
range.domStart += renderFrom - range.start
range.start = renderFrom
if range.end > renderTo
range.end = renderTo
if range.start >= range.end
intactRanges.splice(i--, 1)
i++
intactRanges.sort (a, b) -> a.domStart - b.domStart
clearDirtyRanges: (intactRanges) ->
if intactRanges.length == 0
@renderedLines[0].innerHTML = ''
else if currentLine = @renderedLines[0].firstChild
domPosition = 0
for intactRange in intactRanges
while intactRange.domStart > domPosition
currentLine = @clearLine(currentLine)
domPosition++
for i in [intactRange.start..intactRange.end]
currentLine = currentLine.nextSibling
domPosition++
while currentLine
currentLine = @clearLine(currentLine)
clearLine: (lineElement) ->
next = lineElement.nextSibling
@renderedLines[0].removeChild(lineElement)
next
fillDirtyRanges: (intactRanges, renderFrom, renderTo) ->
i = 0
nextIntact = intactRanges[i]
currentLine = @renderedLines[0].firstChild
row = renderFrom
while row <= renderTo
if row == nextIntact?.end + 1
nextIntact = intactRanges[++i]
if !nextIntact or row < nextIntact.start
if nextIntact
dirtyRangeEnd = nextIntact.start - 1
else
dirtyRangeEnd = renderTo
for lineElement in @buildLineElementsForScreenRows(row, dirtyRangeEnd)
@renderedLines[0].insertBefore(lineElement, currentLine)
row++
else
currentLine = currentLine.nextSibling
row++
updatePaddingOfRenderedLines: ->
paddingTop = @firstRenderedScreenRow * @lineHeight
@renderedLines.css('padding-top', paddingTop)
@gutter.lineNumbers.css('padding-top', paddingTop)
paddingBottom = (@editor.getLastScreenRow() - @lastRenderedScreenRow) * @lineHeight
@renderedLines.css('padding-bottom', paddingBottom)
@gutter.lineNumbers.css('padding-bottom', paddingBottom)
# Public: Retrieves the number of the row that is visible and currently at the
# top of the editor.
#
# Returns a {Number}.
getFirstVisibleScreenRow: ->
screenRow = Math.floor(@scrollTop() / @lineHeight)
screenRow = 0 if isNaN(screenRow)
screenRow
# Public: Retrieves the number of the row that is visible and currently at the
# bottom of the editor.
#
# Returns a {Number}.
getLastVisibleScreenRow: ->
calculatedRow = Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1
screenRow = Math.max(0, Math.min(@editor.getScreenLineCount() - 1, calculatedRow))
screenRow = 0 if isNaN(screenRow)
screenRow
# Public: Given a row number, identifies if it is currently visible.
#
# row - A row {Number} to check
#
# Returns a {Boolean}.
isScreenRowVisible: (row) ->
@getFirstVisibleScreenRow() <= row <= @getLastVisibleScreenRow()
handleScreenLinesChange: (change) ->
@pendingChanges.push(change)
@requestDisplayUpdate()
buildLineElementForScreenRow: (screenRow) ->
@buildLineElementsForScreenRows(screenRow, screenRow)[0]
buildLineElementsForScreenRows: (startRow, endRow) ->
div = document.createElement('div')
div.innerHTML = @htmlForScreenRows(startRow, endRow)
new Array(div.children...)
htmlForScreenRows: (startRow, endRow) ->
htmlLines = ''
screenRow = startRow
for line in @editor.linesForScreenRows(startRow, endRow)
htmlLines += @htmlForScreenLine(line, screenRow++)
htmlLines
htmlForScreenLine: (screenLine, screenRow) ->
{ tokens, text, lineEnding, fold, isSoftWrapped } = screenLine
if fold
attributes = { class: 'fold line', 'fold-id': fold.id }
else
attributes = { class: 'line' }
invisibles = @invisibles if @showInvisibles
eolInvisibles = @getEndOfLineInvisibles(screenLine)
htmlEolInvisibles = @buildHtmlEndOfLineInvisibles(screenLine)
indentation = EditorView.buildIndentation(screenRow, @editor)
EditorView.buildLineHtml({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, @showIndentGuide, indentation, @editor, @mini})
@buildIndentation: (screenRow, editor) ->
bufferRow = editor.bufferPositionForScreenPosition([screenRow]).row
bufferLine = editor.lineForBufferRow(bufferRow)
if bufferLine is ''
indentation = 0
nextRow = screenRow + 1
while nextRow < editor.getBuffer().getLineCount()
bufferRow = editor.bufferPositionForScreenPosition([nextRow]).row
bufferLine = editor.lineForBufferRow(bufferRow)
if bufferLine isnt ''
indentation = Math.ceil(editor.indentLevelForLine(bufferLine))
break
nextRow++
previousRow = screenRow - 1
while previousRow >= 0
bufferRow = editor.bufferPositionForScreenPosition([previousRow]).row
bufferLine = editor.lineForBufferRow(bufferRow)
if bufferLine isnt ''
indentation = Math.max(indentation, Math.ceil(editor.indentLevelForLine(bufferLine)))
break
previousRow--
indentation
else
Math.ceil(editor.indentLevelForLine(bufferLine))
buildHtmlEndOfLineInvisibles: (screenLine) ->
invisibles = []
for invisible in @getEndOfLineInvisibles(screenLine)
invisibles.push("<span class='invisible-character'>#{invisible}</span>")
invisibles.join('')
getEndOfLineInvisibles: (screenLine) ->
return [] unless @showInvisibles and @invisibles
return [] if @mini or screenLine.isSoftWrapped()
invisibles = []
invisibles.push(@invisibles.cr) if @invisibles.cr and screenLine.lineEnding is '\r\n'
invisibles.push(@invisibles.eol) if @invisibles.eol
invisibles
lineElementForScreenRow: (screenRow) ->
@renderedLines.children(":eq(#{screenRow - @firstRenderedScreenRow})")
toggleLineCommentsInSelection: ->
@editor.toggleLineCommentsInSelection()
# Public: Converts a buffer position to a pixel position.
#
# position - An object that represents a buffer position. It can be either
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
#
# Returns an object with two values: `top` and `left`, representing the pixel positions.
pixelPositionForBufferPosition: (position) ->
@pixelPositionForScreenPosition(@editor.screenPositionForBufferPosition(position))
# Public: Converts a screen position to a pixel position.
#
# position - An object that represents a screen position. It can be either
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
#
# Returns an object with two values: `top` and `left`, representing the pixel positions.
pixelPositionForScreenPosition: (position) ->
return { top: 0, left: 0 } unless @isOnDom() and @isVisible()
{row, column} = Point.fromObject(position)
actualRow = Math.floor(row)
lineElement = existingLineElement = @lineElementForScreenRow(actualRow)[0]
unless existingLineElement
lineElement = @buildLineElementForScreenRow(actualRow)
@renderedLines.append(lineElement)
left = @positionLeftForLineAndColumn(lineElement, actualRow, column)
unless existingLineElement
@renderedLines[0].removeChild(lineElement)
{ top: row * @lineHeight, left }
positionLeftForLineAndColumn: (lineElement, screenRow, screenColumn) ->
return 0 if screenColumn == 0
tokenizedLine = @editor.displayBuffer.lineForRow(screenRow)
textContent = lineElement.textContent
left = 0
index = 0
for token in tokenizedLine.tokens
for bufferChar in token.value
return left if index >= screenColumn
# Invisibles might cause renderedChar to be different than bufferChar
renderedChar = textContent[index]
val = @getCharacterWidthCache(token.scopes, renderedChar)
if val?
left += val
else
return @measureToColumn(lineElement, tokenizedLine, screenColumn)
index++
left
measureToColumn: (lineElement, tokenizedLine, screenColumn) ->
left = oldLeft = index = 0
iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TextNodeFilter)
returnLeft = null
offsetLeft = @scrollView.offset().left
paddingLeft = parseInt(@scrollView.css('padding-left'))
while textNode = iterator.nextNode()
content = textNode.textContent
for char, i in content
# Don't continue caching long lines :racehorse:
break if index > LongLineLength and screenColumn < index
# Dont return right away, finish caching the whole line
returnLeft = left if index == screenColumn
oldLeft = left
scopes = tokenizedLine.tokenAtBufferColumn(index)?.scopes
cachedCharWidth = @getCharacterWidthCache(scopes, char)
if cachedCharWidth?
left = oldLeft + cachedCharWidth
else
# i + 1 to measure to the end of the current character
MeasureRange.setEnd(textNode, i + 1)
MeasureRange.collapse()
rects = MeasureRange.getClientRects()
return 0 if rects.length == 0
left = rects[0].left - Math.floor(offsetLeft) + Math.floor(@scrollLeft()) - paddingLeft
if scopes?
cachedCharWidth = left - oldLeft
@setCharacterWidthCache(scopes, char, cachedCharWidth)
# Assume all the characters are the same width when dealing with long
# lines :racehorse:
return screenColumn * cachedCharWidth if index > LongLineLength
index++
returnLeft ? left
getCharacterWidthCache: (scopes, char) ->
scopes ?= NoScope
obj = @constructor.characterWidthCache
for scope in scopes
obj = obj[scope]
return null unless obj?
obj[char]
setCharacterWidthCache: (scopes, char, val) ->
scopes ?= NoScope
obj = @constructor.characterWidthCache
for scope in scopes
obj[scope] ?= {}
obj = obj[scope]
obj[char] = val
clearCharacterWidthCache: ->
@constructor.characterWidthCache = {}
pixelOffsetForScreenPosition: (position) ->
{top, left} = @pixelPositionForScreenPosition(position)
offset = @renderedLines.offset()
{top: top + offset.top, left: left + offset.left}
screenPositionFromMouseEvent: (e) ->
{ pageX, pageY } = e
offset = @scrollView.offset()
editorRelativeTop = pageY - offset.top + @scrollTop()
row = Math.floor(editorRelativeTop / @lineHeight)
column = 0
if pageX > offset.left and lineElement = @lineElementForScreenRow(row)[0]
range = document.createRange()
iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, acceptNode: -> NodeFilter.FILTER_ACCEPT)
while node = iterator.nextNode()
range.selectNodeContents(node)
column += node.textContent.length
{left, right} = range.getClientRects()[0]
break if left <= pageX <= right
if node
for characterPosition in [node.textContent.length...0]
range.setStart(node, characterPosition - 1)
range.setEnd(node, characterPosition)
{left, right, width} = range.getClientRects()[0]
break if left <= pageX - width / 2 <= right
column--
range.detach()
new Point(row, column)
# Highlights the current line the cursor is on.
highlightCursorLine: ->
return if @mini
@highlightedLine?.removeClass('cursor-line')
if @editor.getSelection().isEmpty()
@highlightedLine = @lineElementForScreenRow(@editor.getCursorScreenRow())
@highlightedLine.addClass('cursor-line')
else
@highlightedLine = null
# Copies the current file path to the native clipboard.
copyPathToClipboard: ->
path = @editor.getPath()
atom.clipboard.write(path) if path?
@buildLineHtml: ({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, showIndentGuide, indentation, editor, mini}) ->
scopeStack = []
line = []
attributePairs = ''
attributePairs += " #{attributeName}=\"#{value}\"" for attributeName, value of attributes
line.push("<div #{attributePairs}>")
if text == ''
html = @buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, editor, mini)
line.push(html) if html
else
firstTrailingWhitespacePosition = text.search(/\s*$/)
lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0
position = 0
for token in tokens
@updateScopeStack(line, scopeStack, token.scopes)
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
line.push(htmlEolInvisibles) unless text == ''
line.push("<span class='fold-marker'/>") if fold
line.push('</div>')
line.join('')
@updateScopeStack: (line, scopeStack, desiredScopes) ->
excessScopes = scopeStack.length - desiredScopes.length
if excessScopes > 0
@popScope(line, scopeStack) while excessScopes--
# pop until common prefix
for i in [scopeStack.length..0]
break if _.isEqual(scopeStack[0...i], desiredScopes[0...i])
@popScope(line, scopeStack)
# push on top of common prefix until scopeStack == desiredScopes
for j in [i...desiredScopes.length]
@pushScope(line, scopeStack, desiredScopes[j])
null
@pushScope: (line, scopeStack, scope) ->
scopeStack.push(scope)
line.push("<span class=\"#{scope.replace(/\.+/g, ' ')}\">")
@popScope: (line, scopeStack) ->
scopeStack.pop()
line.push("</span>")
@buildEmptyLineHtml: (showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, editor, mini) ->
indentCharIndex = 0
if not mini and showIndentGuide
if indentation > 0
tabLength = editor.getTabLength()
indentGuideHtml = ''
for level in [0...indentation]
indentLevelHtml = "<span class='indent-guide'>"
for characterPosition in [0...tabLength]
if invisible = eolInvisibles[indentCharIndex++]
indentLevelHtml += "<span class='invisible-character'>#{invisible}</span>"
else
indentLevelHtml += ' '
indentLevelHtml += "</span>"
indentGuideHtml += indentLevelHtml
while indentCharIndex < eolInvisibles.length
indentGuideHtml += "<span class='invisible-character'>#{eolInvisibles[indentCharIndex++]}</span>"
return indentGuideHtml
if htmlEolInvisibles.length > 0
htmlEolInvisibles
else
'&nbsp;'
replaceSelectedText: (replaceFn) ->
selection = @editor.getSelection()
return false if selection.isEmpty()
text = replaceFn(@editor.getTextInRange(selection.getBufferRange()))
return false if text is null or text is undefined
@editor.insertText(text, select: true)
true
consolidateSelections: (e) -> e.abortKeyBinding() unless @editor.consolidateSelections()
logCursorScope: ->
console.log @editor.getCursorScopes()
logScreenLines: (start, end) ->
@editor.logScreenLines(start, end)
logRenderedLines: ->
@renderedLines.find('.line').each (n) ->
console.log n, $(this).text()