{View, $$} = require 'space-pen' TextBuffer = require 'text-buffer' Gutter = require 'gutter' {Point, Range} = require 'telepath' EditSession = require 'edit-session' CursorView = require 'cursor-view' SelectionView = require 'selection-view' fsUtils = require 'fs-utils' $ = require 'jquery' _ = require 'underscore' # Private: Represents the entire visual pane in Atom. # # The Editor manages the {EditSession}, which manages the file buffers. module.exports = class Editor extends View @configDefaults: fontSize: 20 showInvisibles: false showIndentGuide: false showLineNumbers: true autoIndent: true normalizeIndentOnPaste: true nonWordCharacters: "./\\()\"':,.;<>~!@#$%^&*|+=[]{}`~?-" preferredLineLength: 80 tabLength: 2 softWrap: false softTabs: true softWrapAtPreferredLineLength: false @nextEditorId: 1 ### Internal ### @content: (params) -> attributes = { class: @classes(params), tabindex: -1 } _.extend(attributes, params.attributes) if params.attributes @div attributes, => @subview 'gutter', new Gutter @input class: 'hidden-input', outlet: 'hiddenInput' @div class: 'scroll-view', outlet: 'scrollView', => @div class: 'overlayer', outlet: 'overlayer' @div class: 'lines', outlet: 'renderedLines' @div class: 'underlayer', outlet: 'underlayer' @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 activeEditSession: null attached: false lineOverdraw: 10 pendingChanges: null newCursors: null newSelections: null redrawOnReattach: false bottomPaddingInLines: 10 ### Public ### # The constructor for setting up an `Editor` instance. # # editSessionOrOptions - Either an {EditSession}, or an object with one property, `mini`. # If `mini` is `true`, a "miniature" `EditSession` 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: (editSessionOrOptions) -> if editSessionOrOptions instanceof EditSession editSession = editSessionOrOptions else {editSession, @mini} = editSessionOrOptions ? {} @id = Editor.nextEditorId++ @lineCache = [] @configure() @bindKeys() @handleEvents() @cursorViews = [] @selectionViews = [] @pendingChanges = [] @newCursors = [] @newSelections = [] if editSession? @edit(editSession) else if @mini @edit(new EditSession buffer: new TextBuffer softWrap: false tabLength: 2 softTabs: true ) else throw new Error("Must supply an EditSession or mini: true") # Internal: Sets up the core Atom commands. # # Some commands are excluded from mini-editors. bindKeys: -> editorBindings = 'core:move-left': @moveCursorLeft 'core:move-right': @moveCursorRight 'core:select-left': @selectLeft 'core:select-right': @selectRight 'core:select-all': @selectAll 'core:backspace': @backspace 'core:delete': @delete 'core:undo': @undo 'core:redo': @redo 'core:cut': @cutSelection 'core:copy': @copySelection 'core:paste': @paste 'editor:move-to-previous-word': @moveCursorToPreviousWord 'editor:select-word': @selectWord 'editor:newline': @insertNewline 'editor:consolidate-selections': @consolidateSelections 'editor:indent': @indent 'editor:auto-indent': @autoIndent 'editor:indent-selected-rows': @indentSelectedRows 'editor:outdent-selected-rows': @outdentSelectedRows 'editor:backspace-to-beginning-of-word': @backspaceToBeginningOfWord 'editor:backspace-to-beginning-of-line': @backspaceToBeginningOfLine 'editor:delete-to-end-of-word': @deleteToEndOfWord 'editor:delete-line': @deleteLine 'editor:cut-to-end-of-line': @cutToEndOfLine 'editor:move-to-beginning-of-line': @moveCursorToBeginningOfLine 'editor:move-to-end-of-line': @moveCursorToEndOfLine 'editor:move-to-first-character-of-line': @moveCursorToFirstCharacterOfLine 'editor:move-to-beginning-of-word': @moveCursorToBeginningOfWord 'editor:move-to-end-of-word': @moveCursorToEndOfWord 'editor:move-to-beginning-of-next-word': @moveCursorToBeginningOfNextWord 'editor:move-to-previous-word-boundary': @moveCursorToPreviousWordBoundary 'editor:move-to-next-word-boundary': @moveCursorToNextWordBoundary 'editor:select-to-end-of-line': @selectToEndOfLine 'editor:select-to-beginning-of-line': @selectToBeginningOfLine 'editor:select-to-end-of-word': @selectToEndOfWord 'editor:select-to-beginning-of-word': @selectToBeginningOfWord 'editor:select-to-beginning-of-next-word': @selectToBeginningOfNextWord 'editor:select-to-next-word-boundary': @selectToNextWordBoundary 'editor:select-to-previous-word-boundary': @selectToPreviousWordBoundary 'editor:select-to-first-character-of-line': @selectToFirstCharacterOfLine 'editor:add-selection-below': @addSelectionBelow 'editor:add-selection-above': @addSelectionAbove 'editor:select-line': @selectLine 'editor:transpose': @transpose 'editor:upper-case': @upperCase 'editor:lower-case': @lowerCase unless @mini _.extend editorBindings, 'core:move-up': @moveCursorUp 'core:move-down': @moveCursorDown 'core:move-to-top': @moveCursorToTop 'core:move-to-bottom': @moveCursorToBottom 'core:page-down': @pageDown 'core:page-up': @pageUp 'core:select-up': @selectUp 'core:select-down': @selectDown 'core:select-to-top': @selectToTop 'core:select-to-bottom': @selectToBottom 'editor:newline-below': @insertNewlineBelow 'editor:newline-above': @insertNewlineAbove 'editor:toggle-soft-tabs': @toggleSoftTabs 'editor:toggle-soft-wrap': @toggleSoftWrap 'editor:fold-all': @foldAll 'editor:unfold-all': @unfoldAll 'editor:fold-current-row': @foldCurrentRow 'editor:unfold-current-row': @unfoldCurrentRow 'editor:fold-selection': @foldSelection 'editor:fold-at-indent-level-1': => @foldAllAtIndentLevel(0) 'editor:fold-at-indent-level-2': => @foldAllAtIndentLevel(1) 'editor:fold-at-indent-level-3': => @foldAllAtIndentLevel(2) 'editor:fold-at-indent-level-4': => @foldAllAtIndentLevel(3) 'editor:fold-at-indent-level-5': => @foldAllAtIndentLevel(4) 'editor:fold-at-indent-level-6': => @foldAllAtIndentLevel(5) 'editor:fold-at-indent-level-7': => @foldAllAtIndentLevel(6) 'editor:fold-at-indent-level-8': => @foldAllAtIndentLevel(7) 'editor:fold-at-indent-level-9': => @foldAllAtIndentLevel(8) 'editor:toggle-line-comments': @toggleLineCommentsInSelection 'editor:log-cursor-scope': @logCursorScope 'editor:checkout-head-revision': @checkoutHead 'editor:copy-path': @copyPathToPasteboard 'editor:move-line-up': @moveLineUp 'editor:move-line-down': @moveLineDown 'editor:duplicate-line': @duplicateLine 'editor:join-line': @joinLine 'editor:toggle-indent-guide': => config.set('editor.showIndentGuide', !config.get('editor.showIndentGuide')) 'editor:save-debug-snapshot': @saveDebugSnapshot 'editor:toggle-line-numbers': => config.set('editor.showLineNumbers', !config.get('editor.showLineNumbers')) 'editor:scroll-to-cursor': @scrollToCursorPosition documentation = {} for name, method of editorBindings do (name, method) => @command name, (e) => method.call(this, e); false # {Delegates to: EditSession.getCursor} getCursor: -> @activeEditSession.getCursor() # {Delegates to: EditSession.getCursors} getCursors: -> @activeEditSession.getCursors() # {Delegates to: EditSession.addCursorAtScreenPosition} addCursorAtScreenPosition: (screenPosition) -> @activeEditSession.addCursorAtScreenPosition(screenPosition) # {Delegates to: EditSession.addCursorAtBufferPosition} addCursorAtBufferPosition: (bufferPosition) -> @activeEditSession.addCursorAtBufferPosition(bufferPosition) # {Delegates to: EditSession.moveCursorUp} moveCursorUp: -> @activeEditSession.moveCursorUp() # {Delegates to: EditSession.moveCursorDown} moveCursorDown: -> @activeEditSession.moveCursorDown() # {Delegates to: EditSession.moveCursorLeft} moveCursorLeft: -> @activeEditSession.moveCursorLeft() # {Delegates to: EditSession.moveCursorRight} moveCursorRight: -> @activeEditSession.moveCursorRight() # {Delegates to: EditSession.moveCursorToBeginningOfWord} moveCursorToBeginningOfWord: -> @activeEditSession.moveCursorToBeginningOfWord() # {Delegates to: EditSession.moveCursorToEndOfWord} moveCursorToEndOfWord: -> @activeEditSession.moveCursorToEndOfWord() # {Delegates to: EditSession.moveCursorToBeginningOfNextWord} moveCursorToBeginningOfNextWord: -> @activeEditSession.moveCursorToBeginningOfNextWord() # {Delegates to: EditSession.moveCursorToTop} moveCursorToTop: -> @activeEditSession.moveCursorToTop() # {Delegates to: EditSession.moveCursorToBottom} moveCursorToBottom: -> @activeEditSession.moveCursorToBottom() # {Delegates to: EditSession.moveCursorToBeginningOfLine} moveCursorToBeginningOfLine: -> @activeEditSession.moveCursorToBeginningOfLine() # {Delegates to: EditSession.moveCursorToFirstCharacterOfLine} moveCursorToFirstCharacterOfLine: -> @activeEditSession.moveCursorToFirstCharacterOfLine() # {Delegates to: EditSession.moveCursorToPreviousWordBoundary} moveCursorToPreviousWordBoundary: -> @activeEditSession.moveCursorToPreviousWordBoundary() # {Delegates to: EditSession.moveCursorToNextWordBoundary} moveCursorToNextWordBoundary: -> @activeEditSession.moveCursorToNextWordBoundary() # {Delegates to: EditSession.moveCursorToEndOfLine} moveCursorToEndOfLine: -> @activeEditSession.moveCursorToEndOfLine() # {Delegates to: EditSession.moveLineUp} moveLineUp: -> @activeEditSession.moveLineUp() # {Delegates to: EditSession.moveLineDown} moveLineDown: -> @activeEditSession.moveLineDown() # {Delegates to: EditSession.setCursorScreenPosition} setCursorScreenPosition: (position, options) -> @activeEditSession.setCursorScreenPosition(position, options) # {Delegates to: EditSession.duplicateLine} duplicateLine: -> @activeEditSession.duplicateLine() # {Delegates to: EditSession.joinLine} joinLine: -> @activeEditSession.joinLine() # {Delegates to: EditSession.getCursorScreenPosition} getCursorScreenPosition: -> @activeEditSession.getCursorScreenPosition() # {Delegates to: EditSession.getCursorScreenRow} getCursorScreenRow: -> @activeEditSession.getCursorScreenRow() # {Delegates to: EditSession.setCursorBufferPosition} setCursorBufferPosition: (position, options) -> @activeEditSession.setCursorBufferPosition(position, options) # {Delegates to: EditSession.getCursorBufferPosition} getCursorBufferPosition: -> @activeEditSession.getCursorBufferPosition() # {Delegates to: EditSession.getCurrentParagraphBufferRange} getCurrentParagraphBufferRange: -> @activeEditSession.getCurrentParagraphBufferRange() # {Delegates to: EditSession.getWordUnderCursor} getWordUnderCursor: (options) -> @activeEditSession.getWordUnderCursor(options) # {Delegates to: EditSession.getSelection} getSelection: (index) -> @activeEditSession.getSelection(index) # {Delegates to: EditSession.getSelections} getSelections: -> @activeEditSession.getSelections() # {Delegates to: EditSession.getSelectionsOrderedByBufferPosition} getSelectionsOrderedByBufferPosition: -> @activeEditSession.getSelectionsOrderedByBufferPosition() # {Delegates to: EditSession.getLastSelectionInBuffer} getLastSelectionInBuffer: -> @activeEditSession.getLastSelectionInBuffer() # {Delegates to: EditSession.getSelectedText} getSelectedText: -> @activeEditSession.getSelectedText() # {Delegates to: EditSession.getSelectedBufferRanges} getSelectedBufferRanges: -> @activeEditSession.getSelectedBufferRanges() # {Delegates to: EditSession.getSelectedBufferRange} getSelectedBufferRange: -> @activeEditSession.getSelectedBufferRange() # {Delegates to: EditSession.setSelectedBufferRange} setSelectedBufferRange: (bufferRange, options) -> @activeEditSession.setSelectedBufferRange(bufferRange, options) # {Delegates to: EditSession.setSelectedBufferRanges} setSelectedBufferRanges: (bufferRanges, options) -> @activeEditSession.setSelectedBufferRanges(bufferRanges, options) # {Delegates to: EditSession.addSelectionForBufferRange} addSelectionForBufferRange: (bufferRange, options) -> @activeEditSession.addSelectionForBufferRange(bufferRange, options) # {Delegates to: EditSession.selectRight} selectRight: -> @activeEditSession.selectRight() # {Delegates to: EditSession.selectLeft} selectLeft: -> @activeEditSession.selectLeft() # {Delegates to: EditSession.selectUp} selectUp: -> @activeEditSession.selectUp() # {Delegates to: EditSession.selectDown} selectDown: -> @activeEditSession.selectDown() # {Delegates to: EditSession.selectToTop} selectToTop: -> @activeEditSession.selectToTop() # {Delegates to: EditSession.selectToBottom} selectToBottom: -> @activeEditSession.selectToBottom() # {Delegates to: EditSession.selectAll} selectAll: -> @activeEditSession.selectAll() # {Delegates to: EditSession.selectToBeginningOfLine} selectToBeginningOfLine: -> @activeEditSession.selectToBeginningOfLine() # {Delegates to: EditSession.selectToFirstCharacterOfLine} selectToFirstCharacterOfLine: -> @activeEditSession.selectToFirstCharacterOfLine() # {Delegates to: EditSession.selectToEndOfLine} selectToEndOfLine: -> @activeEditSession.selectToEndOfLine() # {Delegates to: EditSession.selectToPreviousWordBoundary} selectToPreviousWordBoundary: -> @activeEditSession.selectToPreviousWordBoundary() # {Delegates to: EditSession.selectToNextWordBoundary} selectToNextWordBoundary: -> @activeEditSession.selectToNextWordBoundary() # {Delegates to: EditSession.addSelectionBelow} addSelectionBelow: -> @activeEditSession.addSelectionBelow() # {Delegates to: EditSession.addSelectionAbove} addSelectionAbove: -> @activeEditSession.addSelectionAbove() # {Delegates to: EditSession.selectToBeginningOfWord} selectToBeginningOfWord: -> @activeEditSession.selectToBeginningOfWord() # {Delegates to: EditSession.selectToEndOfWord} selectToEndOfWord: -> @activeEditSession.selectToEndOfWord() # {Delegates to: EditSession.selectToBeginningOfNextWord} selectToBeginningOfNextWord: -> @activeEditSession.selectToBeginningOfNextWord() # {Delegates to: EditSession.selectWord} selectWord: -> @activeEditSession.selectWord() # {Delegates to: EditSession.selectLine} selectLine: -> @activeEditSession.selectLine() # {Delegates to: EditSession.selectToScreenPosition} selectToScreenPosition: (position) -> @activeEditSession.selectToScreenPosition(position) # {Delegates to: EditSession.transpose} transpose: -> @activeEditSession.transpose() # {Delegates to: EditSession.upperCase} upperCase: -> @activeEditSession.upperCase() # {Delegates to: EditSession.lowerCase} lowerCase: -> @activeEditSession.lowerCase() # {Delegates to: EditSession.clearSelections} clearSelections: -> @activeEditSession.clearSelections() # {Delegates to: EditSession.backspace} backspace: -> @activeEditSession.backspace() # {Delegates to: EditSession.backspaceToBeginningOfWord} backspaceToBeginningOfWord: -> @activeEditSession.backspaceToBeginningOfWord() # {Delegates to: EditSession.backspaceToBeginningOfLine} backspaceToBeginningOfLine: -> @activeEditSession.backspaceToBeginningOfLine() # {Delegates to: EditSession.delete} delete: -> @activeEditSession.delete() # {Delegates to: EditSession.deleteToEndOfWord} deleteToEndOfWord: -> @activeEditSession.deleteToEndOfWord() # {Delegates to: EditSession.deleteLine} deleteLine: -> @activeEditSession.deleteLine() # {Delegates to: EditSession.cutToEndOfLine} cutToEndOfLine: -> @activeEditSession.cutToEndOfLine() # {Delegates to: EditSession.insertText} insertText: (text, options) -> @activeEditSession.insertText(text, options) # {Delegates to: EditSession.insertNewline} insertNewline: -> @activeEditSession.insertNewline() # {Delegates to: EditSession.insertNewlineBelow} insertNewlineBelow: -> @activeEditSession.insertNewlineBelow() # {Delegates to: EditSession.insertNewlineAbove} insertNewlineAbove: -> @activeEditSession.insertNewlineAbove() # {Delegates to: EditSession.indent} indent: (options) -> @activeEditSession.indent(options) # {Delegates to: EditSession.autoIndentSelectedRows} autoIndent: (options) -> @activeEditSession.autoIndentSelectedRows() # {Delegates to: EditSession.indentSelectedRows} indentSelectedRows: -> @activeEditSession.indentSelectedRows() # {Delegates to: EditSession.outdentSelectedRows} outdentSelectedRows: -> @activeEditSession.outdentSelectedRows() # {Delegates to: EditSession.cutSelectedText} cutSelection: -> @activeEditSession.cutSelectedText() # {Delegates to: EditSession.copySelectedText} copySelection: -> @activeEditSession.copySelectedText() # {Delegates to: EditSession.pasteText} paste: (options) -> @activeEditSession.pasteText(options) # {Delegates to: EditSession.undo} undo: -> @activeEditSession.undo() # {Delegates to: EditSession.redo} redo: -> @activeEditSession.redo() # {Delegates to: EditSession.createFold} createFold: (startRow, endRow) -> @activeEditSession.createFold(startRow, endRow) # {Delegates to: EditSession.foldCurrentRow} foldCurrentRow: -> @activeEditSession.foldCurrentRow() # {Delegates to: EditSession.unfoldCurrentRow} unfoldCurrentRow: -> @activeEditSession.unfoldCurrentRow() # {Delegates to: EditSession.foldAll} foldAll: -> @activeEditSession.foldAll() # {Delegates to: EditSession.unfoldAll} unfoldAll: -> @activeEditSession.unfoldAll() # {Delegates to: EditSession.foldSelection} foldSelection: -> @activeEditSession.foldSelection() # {Delegates to: EditSession.destroyFoldsContainingBufferRow} destroyFoldsContainingBufferRow: (bufferRow) -> @activeEditSession.destroyFoldsContainingBufferRow(bufferRow) # {Delegates to: EditSession.isFoldedAtScreenRow} isFoldedAtScreenRow: (screenRow) -> @activeEditSession.isFoldedAtScreenRow(screenRow) # {Delegates to: EditSession.isFoldedAtBufferRow} isFoldedAtBufferRow: (bufferRow) -> @activeEditSession.isFoldedAtBufferRow(bufferRow) # {Delegates to: EditSession.isFoldedAtCursorRow} isFoldedAtCursorRow: -> @activeEditSession.isFoldedAtCursorRow() foldAllAtIndentLevel: (indentLevel) -> @activeEditSession.foldAllAtIndentLevel(indentLevel) # {Delegates to: EditSession.lineForScreenRow} lineForScreenRow: (screenRow) -> @activeEditSession.lineForScreenRow(screenRow) # {Delegates to: EditSession.linesForScreenRows} linesForScreenRows: (start, end) -> @activeEditSession.linesForScreenRows(start, end) # {Delegates to: EditSession.getScreenLineCount} getScreenLineCount: -> @activeEditSession.getScreenLineCount() # {Delegates to: EditSession.setEditorWidthInChars} setWidthInChars: (widthInChars) -> widthInChars ?= @calculateWidthInChars() @activeEditSession.setEditorWidthInChars(widthInChars) if widthInChars # {Delegates to: EditSession.getMaxScreenLineLength} getMaxScreenLineLength: -> @activeEditSession.getMaxScreenLineLength() # {Delegates to: EditSession.getLastScreenRow} getLastScreenRow: -> @activeEditSession.getLastScreenRow() # {Delegates to: EditSession.clipScreenPosition} clipScreenPosition: (screenPosition, options={}) -> @activeEditSession.clipScreenPosition(screenPosition, options) # {Delegates to: EditSession.screenPositionForBufferPosition} screenPositionForBufferPosition: (position, options) -> @activeEditSession.screenPositionForBufferPosition(position, options) # {Delegates to: EditSession.bufferPositionForScreenPosition} bufferPositionForScreenPosition: (position, options) -> @activeEditSession.bufferPositionForScreenPosition(position, options) # {Delegates to: EditSession.screenRangeForBufferRange} screenRangeForBufferRange: (range) -> @activeEditSession.screenRangeForBufferRange(range) # {Delegates to: EditSession.bufferRangeForScreenRange} bufferRangeForScreenRange: (range) -> @activeEditSession.bufferRangeForScreenRange(range) # {Delegates to: EditSession.bufferRowsForScreenRows} bufferRowsForScreenRows: (startRow, endRow) -> @activeEditSession.bufferRowsForScreenRows(startRow, endRow) # Public: Emulates the "page down" key, where the last row of a buffer scrolls to become the first. pageDown: -> newScrollTop = @scrollTop() + @scrollView[0].clientHeight @activeEditSession.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 @activeEditSession.moveCursorUp(@getPageRows()) @scrollTop(newScrollTop, adjustVerticalScrollbar: true) # Gets the number of actual page rows existing in an editor. # # Returns a {Number}. getPageRows: -> Math.max(1, Math.ceil(@scrollView[0].clientHeight / @lineHeight)) # Set whether invisible characters are shown. # # showInvisibles - A {Boolean} which, if `true`, show invisible characters setShowInvisibles: (showInvisibles) -> return if showInvisibles == @showInvisibles @showInvisibles = showInvisibles @resetDisplay() # Defines which characters are invisible. # # invisibles - A hash defining the invisible characters: The defaults are: # eol: `\u00ac` # space: `\u00b7` # tab: `\u00bb` # cr: `\u00a4` setInvisibles: (@invisibles={}) -> _.defaults @invisibles, eol: '\u00ac' space: '\u00b7' tab: '\u00bb' cr: '\u00a4' @resetDisplay() # 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() # {Delegates to: TextBuffer.checkoutHead} checkoutHead: -> @getBuffer().checkoutHead() # {Delegates to: EditSession.setText} setText: (text) -> @activeEditSession.setText(text) # {Delegates to: EditSession.getText} getText: -> @activeEditSession.getText() # {Delegates to: EditSession.getPath} getPath: -> @activeEditSession?.getPath() # {Delegates to: EditSession.getRelativePath} getRelativePath: -> @activeEditSession?.getRelativePath() # {Delegates to: TextBuffer.getLineCount} getLineCount: -> @getBuffer().getLineCount() # {Delegates to: TextBuffer.getLastRow} getLastBufferRow: -> @getBuffer().getLastRow() # {Delegates to: TextBuffer.getTextInRange} getTextInRange: (range) -> @getBuffer().getTextInRange(range) # {Delegates to: TextBuffer.getEofPosition} getEofPosition: -> @getBuffer().getEofPosition() # {Delegates to: TextBuffer.lineForRow} lineForBufferRow: (row) -> @getBuffer().lineForRow(row) # {Delegates to: TextBuffer.lineLengthForRow} lineLengthForBufferRow: (row) -> @getBuffer().lineLengthForRow(row) # {Delegates to: TextBuffer.rangeForRow} rangeForBufferRow: (row) -> @getBuffer().rangeForRow(row) # {Delegates to: TextBuffer.scanInRange} scanInBufferRange: (args...) -> @getBuffer().scanInRange(args...) # {Delegates to: TextBuffer.backwardsScanInRange} backwardsScanInBufferRange: (args...) -> @getBuffer().backwardsScanInRange(args...) ### Internal ### configure: -> @observeConfig 'editor.showLineNumbers', (showLineNumbers) => @gutter.setShowLineNumbers(showLineNumbers) @observeConfig 'editor.showInvisibles', (showInvisibles) => @setShowInvisibles(showInvisibles) @observeConfig 'editor.showIndentGuide', (showIndentGuide) => @setShowIndentGuide(showIndentGuide) @observeConfig 'editor.invisibles', (invisibles) => @setInvisibles(invisibles) @observeConfig 'editor.fontSize', (fontSize) => @setFontSize(fontSize) @observeConfig 'editor.fontFamily', (fontFamily) => @setFontFamily(fontFamily) handleEvents: -> @on 'focus', => @hiddenInput.focus() false @hiddenInput.on 'focus', => @isFocused = true @addClass 'is-focused' @hiddenInput.on 'focusout', => @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 = @activeEditSession.displayBuffer.getMarker(id) @activeEditSession.setCursorBufferPosition(marker.getBufferRange().start) @activeEditSession.destroyFoldWithId(id) false @renderedLines.on 'mousedown', (e) => clickCount = e.originalEvent.detail screenPosition = @screenPositionFromMouseEvent(e) if clickCount == 1 if e.metaKey @addCursorAtScreenPosition(screenPosition) else if e.shiftKey @selectToScreenPosition(screenPosition) else @setCursorScreenPosition(screenPosition) else if clickCount == 2 @activeEditSession.selectWord() unless e.shiftKey else if clickCount == 3 @activeEditSession.selectLine() unless e.shiftKey @selectOnMousemoveUntilMouseup() unless e.ctrlKey or e.originalEvent.which > 1 @on "textInput", (e) => @insertText(e.originalEvent.data) false 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') selectOnMousemoveUntilMouseup: -> lastMoveEvent = null moveHandler = (event = lastMoveEvent) => if event @selectToScreenPosition(@screenPositionFromMouseEvent(event)) lastMoveEvent = event $(document).on "mousemove.editor-#{@id}", moveHandler interval = setInterval(moveHandler, 20) $(document).one "mouseup.editor-#{@id}", => clearInterval(interval) $(document).off 'mousemove', moveHandler @activeEditSession.mergeIntersectingSelections(isReversed: @activeEditSession.getLastSelection().isReversed()) @activeEditSession.finalizeSelections() @syncCursorAnimations() afterAttach: (onDom) -> return unless onDom @redraw() if @redrawOnReattach return if @attached @attached = true @calculateDimensions() @setWidthInChars() @subscribe $(window), "resize.editor-#{@id}", => @setWidthInChars() @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: (editSession) -> return if editSession is @activeEditSession if @activeEditSession @saveScrollPositionForActiveEditSession() @activeEditSession.off(".editor") @activeEditSession = editSession return unless @activeEditSession? @activeEditSession.setVisible(true) @activeEditSession.on "contents-conflicted.editor", => @showBufferConflictAlert(@activeEditSession) @activeEditSession.on "path-changed.editor", => @reloadGrammar() @trigger 'editor:path-changed' @activeEditSession.on "grammar-changed.editor", => @trigger 'editor:grammar-changed' @activeEditSession.on 'selection-added.editor', (selection) => @newCursors.push(selection.cursor) @newSelections.push(selection) @requestDisplayUpdate() @activeEditSession.on 'screen-lines-changed.editor', (e) => @handleScreenLinesChange(e) @activeEditSession.on 'scroll-top-changed.editor', (scrollTop) => @scrollTop(scrollTop) @activeEditSession.on 'scroll-left-changed.editor', (scrollLeft) => @scrollLeft(scrollLeft) @activeEditSession.on 'soft-wrap-changed.editor', (softWrap) => @setSoftWrap(softWrap) @trigger 'editor:path-changed' @resetDisplay() if @attached and @activeEditSession.buffer.isInConflict() _.defer => @showBufferConflictAlert(@activeEditSession) # Display after editSession has a chance to display getModel: -> @activeEditSession setModel: (editSession) -> @edit(editSession) showBufferConflictAlert: (editSession) -> atom.confirm( editSession.getPath(), "Has changed on disk. Do you want to reload it?", "Reload", (=> editSession.buffer.reload()), "Cancel" ) 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) @activeEditSession.setScrollTop(@scrollTop()) scrollBottom: (scrollBottom) -> if scrollBottom? @scrollTop(scrollBottom - @scrollView.height()) else @scrollTop() + @scrollView.height() scrollLeft: (scrollLeft) -> if scrollLeft? @scrollView.scrollLeft(scrollLeft) @activeEditSession.setScrollLeft(@scrollLeft()) else @scrollView.scrollLeft() scrollRight: (scrollRight) -> if scrollRight? @scrollView.scrollRight(scrollRight) @activeEditSession.setScrollLeft(@scrollLeft()) else @scrollView.scrollRight() ### Public ### # Retrieves the {EditSession}'s buffer. # # Returns the current {TextBuffer}. getBuffer: -> @activeEditSession.buffer # Scrolls the editor to the bottom. scrollToBottom: -> @scrollBottom(@getScreenLineCount() * @lineHeight) # Scrolls the editor to the position of the most recently added cursor. # # The editor is also centered. scrollToCursorPosition: -> @scrollToBufferPosition(@getCursorBufferPosition(), center: true) # 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) # 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) # 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) # Given a buffer range, this highlights all the folds within that range # # "Highlighting" essentially just adds the `selected` class to the line # # bufferRange - The {Range} to check highlightFoldsContainingBufferRange: (bufferRange) -> screenLines = @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('selected') else element.removeClass('selected') saveScrollPositionForActiveEditSession: -> if @attached @activeEditSession.setScrollTop(@scrollTop()) @activeEditSession.setScrollLeft(@scrollLeft()) # Toggle soft tabs on the edit session. toggleSoftTabs: -> @activeEditSession.setSoftTabs(not @activeEditSession.softTabs) # Toggle soft wrap on the edit session. toggleSoftWrap: -> @activeEditSession.setSoftWrap(not @activeEditSession.getSoftWrap()) calculateWidthInChars: -> Math.floor(@scrollView.width() / @charWidth) # 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' # Sets the font size for the editor. # # fontSize - A {Number} indicating the font size in pixels. setFontSize: (fontSize) -> headTag = $("head") styleTag = headTag.find("style.font-size") if styleTag.length == 0 styleTag = $$ -> @style class: 'font-size' headTag.append styleTag styleTag.text(".editor {font-size: #{fontSize}px}") if @isOnDom() @redraw() else @redrawOnReattach = @attached # Retrieves the font size for the editor. # # Returns a {Number} indicating the font size in pixels. getFontSize: -> parseInt(@css("font-size")) # Sets the font family for the editor. # # fontFamily - A {String} identifying the CSS `font-family`, setFontFamily: (fontFamily) -> headTag = $("head") styleTag = headTag.find("style.editor-font-family") if fontFamily? if styleTag.length == 0 styleTag = $$ -> @style class: 'editor-font-family' headTag.append styleTag styleTag.text(".editor {font-family: #{fontFamily}}") else styleTag.remove() @redraw() # Gets the font family for the editor. # # Returns a {String} identifying the CSS `font-family`, getFontFamily: -> @css("font-family") # Clears the CSS `font-family` property from the editor. clearFontFamily: -> $('head style.editor-font-family').remove() # Clears the CSS `font-family` property from the editor. redraw: -> return unless @hasParent() return unless @attached @redrawOnReattach = false @calculateDimensions() @updatePaddingOfRenderedLines() @updateLayerDimensions() @requestDisplayUpdate() splitLeft: (items...) -> @getPane()?.splitLeft(items...).activeView splitRight: (items...) -> @getPane()?.splitRight(items...).activeView splitUp: (items...) -> @getPane()?.splitUp(items...).activeView splitDown: (items...) -> @getPane()?.splitDown(items...).activeView # Retrieve's the `Editor`'s pane. # # Returns a {Pane}. getPane: -> @parent('.item-views').parent('.pane').view() remove: (selector, keepData) -> return super if keepData or @removed super rootView?.focus() beforeRemove: -> @trigger 'editor:will-be-removed' @removed = true @activeEditSession?.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({editor: 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) ### Internal ### # 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 @activeEditSession.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) @saveScrollPositionForActiveEditSession() calculateDimensions: -> fragment = $('
') @renderedLines.append(fragment) lineRect = fragment[0].getBoundingClientRect() charRect = fragment.find('span')[0].getBoundingClientRect() @lineHeight = lineRect.height @charWidth = charRect.width @charHeight = charRect.height fragment.remove() updateLayerDimensions: -> height = @lineHeight * @getScreenLineCount() unless @layerHeight == height @layerHeight = height @underlayer.height(@layerHeight) @renderedLines.height(@layerHeight) @overlayer.height(@layerHeight) @verticalScrollbarContent.height(@layerHeight) @scrollBottom(height) if @scrollBottom() > height minWidth = @charWidth * @getMaxScreenLineLength() + 20 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' clearRenderedLines: -> @renderedLines.empty() @firstRenderedScreenRow = null @lastRenderedScreenRow = null resetDisplay: -> return unless @attached @clearRenderedLines() @removeAllCursorAndSelectionViews() editSessionScrollTop = @activeEditSession.getScrollTop() ? 0 editSessionScrollLeft = @activeEditSession.getScrollLeft() ? 0 @updateLayerDimensions() @scrollTop(editSessionScrollTop) @scrollLeft(editSessionScrollLeft) @setSoftWrap(@activeEditSession.getSoftWrap()) @newCursors = @activeEditSession.getAllCursors() @newSelections = @activeEditSession.getAllSelections() @updateDisplay(suppressAutoScroll: true) requestDisplayUpdate: -> return if @pendingDisplayUpdate return unless @isVisible() @pendingDisplayUpdate = true _.nextTick => @updateDisplay() @pendingDisplayUpdate = false updateDisplay: (options={}) -> return unless @attached and @activeEditSession return if @activeEditSession.destroyed unless @isVisible() @redrawOnReattach = true return @updateRenderedLines() @highlightCursorLine() @updateCursorViews() @updateSelectionViews() @autoscroll(options) @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 cursorView.needsUpdate cursorView.updateDisplay() 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 selectionView.updateDisplay() syncCursorAnimations: -> for cursorView in @getCursorViews() do (cursorView) -> cursorView.resetBlinking() autoscroll: (options={}) -> for cursorView in @getCursorViews() if !options.suppressAutoScroll and cursorView.needsAutoscroll() @scrollToPixelPosition(cursorView.getPixelPosition()) cursorView.clearAutoscroll() for selectionView in @getSelectionViews() if !options.suppressAutoScroll and selectionView.needsAutoscroll() @scrollToPixelPosition(selectionView.getCenterPixelPosition(), center: true) selectionView.highlight() selectionView.clearAutoscroll() updateRenderedLines: -> firstVisibleScreenRow = @getFirstVisibleScreenRow() lastVisibleScreenRow = @getLastVisibleScreenRow() lastScreenRow = @getLastScreenRow() if @firstRenderedScreenRow? and firstVisibleScreenRow >= @firstRenderedScreenRow and lastVisibleScreenRow <= @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, lastVisibleScreenRow + @lineOverdraw) if @pendingChanges.length == 0 and @firstRenderedScreenRow and @firstRenderedScreenRow <= renderFrom and renderTo <= @lastRenderedScreenRow return @gutter.updateLineNumbers(@pendingChanges, renderFrom, renderTo) intactRanges = @computeIntactRanges() @pendingChanges = [] @truncateIntactRanges(intactRanges, renderFrom, renderTo) @clearDirtyRanges(intactRanges) @fillDirtyRanges(intactRanges, renderFrom, renderTo) @firstRenderedScreenRow = renderFrom @lastRenderedScreenRow = renderTo @updateLayerDimensions() @updatePaddingOfRenderedLines() computeSurroundingEmptyLineChanges: (change) -> emptyLineChanges = [] if change.bufferDelta? afterStart = change.end + change.bufferDelta + 1 if @lineForBufferRow(afterStart) is '' afterEnd = afterStart afterEnd++ while @lineForBufferRow(afterEnd + 1) is '' emptyLineChanges.push({start: afterStart, end: afterEnd, screenDelta: 0}) beforeEnd = change.start - 1 if @lineForBufferRow(beforeEnd) is '' beforeStart = beforeEnd beforeStart-- while @lineForBufferRow(beforeStart - 1) is '' emptyLineChanges.push({start: beforeStart, end: beforeEnd, screenDelta: 0}) emptyLineChanges computeIntactRanges: -> 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 @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) -> renderedLines = @renderedLines[0] killLine = (line) -> next = line.nextSibling renderedLines.removeChild(line) next if intactRanges.length == 0 @renderedLines.empty() else if currentLine = renderedLines.firstChild domPosition = 0 for intactRange in intactRanges while intactRange.domStart > domPosition currentLine = killLine(currentLine) domPosition++ for i in [intactRange.start..intactRange.end] currentLine = currentLine.nextSibling domPosition++ while currentLine currentLine = killLine(currentLine) fillDirtyRanges: (intactRanges, renderFrom, renderTo) -> renderedLines = @renderedLines[0] nextIntact = intactRanges.shift() currentLine = renderedLines.firstChild row = renderFrom while row <= renderTo if row == nextIntact?.end + 1 nextIntact = intactRanges.shift() if !nextIntact or row < nextIntact.start if nextIntact dirtyRangeEnd = nextIntact.start - 1 else dirtyRangeEnd = renderTo for lineElement in @buildLineElementsForScreenRows(row, dirtyRangeEnd) renderedLines.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 = (@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: -> Math.floor(@scrollTop() / @lineHeight) # Retrieves the number of the row that is visible and currently at the top of the editor. # # Returns a {Number}. getLastVisibleScreenRow: -> calculatedRow = Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1 Math.max(0, Math.min(@getScreenLineCount() - 1, calculatedRow)) # Given a row number, identifies if it is currently visible. # # row - A row {Number} to check # # Returns a {Boolean}. isScreenRowVisible: (row) -> @getFirstVisibleScreenRow() <= row <= @getLastVisibleScreenRow() ### Internal ### 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 @activeEditSession.linesForScreenRows(startRow, endRow) htmlLines.push(@htmlForScreenLine(line, screenRow++)) htmlLines.join('\n\n') 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 = Editor.buildIndentation(screenRow, @activeEditSession) Editor.buildLineHtml({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, @showIndentGuide, indentation, @activeEditSession, @mini}) @buildIndentation: (screenRow, activeEditSession) -> bufferRow = activeEditSession.bufferPositionForScreenPosition([screenRow]).row bufferLine = activeEditSession.lineForBufferRow(bufferRow) if bufferLine is '' indentation = 0 nextRow = screenRow + 1 while nextRow < activeEditSession.getBuffer().getLineCount() bufferRow = activeEditSession.bufferPositionForScreenPosition([nextRow]).row bufferLine = activeEditSession.lineForBufferRow(bufferRow) if bufferLine isnt '' indentation = Math.ceil(activeEditSession.indentLevelForLine(bufferLine)) break nextRow++ previousRow = screenRow - 1 while previousRow >= 0 bufferRow = activeEditSession.bufferPositionForScreenPosition([previousRow]).row bufferLine = activeEditSession.lineForBufferRow(bufferRow) if bufferLine isnt '' indentation = Math.max(indentation, Math.ceil(activeEditSession.indentLevelForLine(bufferLine))) break previousRow-- indentation else Math.ceil(activeEditSession.indentLevelForLine(bufferLine)) buildHtmlEndOfLineInvisibles: (screenLine) -> invisibles = [] for invisible in @getEndOfLineInvisibles(screenLine) invisibles.push("#{invisible}") 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: -> @activeEditSession.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(@screenPositionForBufferPosition(position)) # 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, column) unless existingLineElement @renderedLines[0].removeChild(lineElement) { top: row * @lineHeight, left } positionLeftForLineAndColumn: (lineElement, column) -> return 0 if column is 0 delta = 0 iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, acceptNode: -> NodeFilter.FILTER_ACCEPT) while textNode = iterator.nextNode() nextDelta = delta + textNode.textContent.length if nextDelta >= column offset = column - delta break delta = nextDelta range = document.createRange() range.setEnd(textNode, offset) range.collapse() leftPixels = range.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) range.detach() leftPixels pixelOffsetForScreenPosition: (position) -> {top, left} = @pixelPositionForScreenPosition(position) offset = @renderedLines.offset() {top: top + offset.top, left: left + offset.left} screenPositionFromMouseEvent: (e) -> { pageX, pageY } = e editorRelativeTop = pageY - @scrollView.offset().top + @scrollTop() row = Math.floor(editorRelativeTop / @lineHeight) column = 0 if 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 @getSelection().isEmpty() @highlightedLine = @lineElementForScreenRow(@getCursorScreenRow()) @highlightedLine.addClass('cursor-line') else @highlightedLine = null # {Delegates to: EditSession.getGrammar} getGrammar: -> @activeEditSession.getGrammar() # {Delegates to: EditSession.setGrammar} setGrammar: (grammar) -> throw new Error("Only mini-editors can explicity set their grammar") unless @mini @activeEditSession.setGrammar(grammar) # {Delegates to: EditSession.reloadGrammar} reloadGrammar: -> @activeEditSession.reloadGrammar() # {Delegates to: EditSession.scopesForBufferPosition} scopesForBufferPosition: (bufferPosition) -> @activeEditSession.scopesForBufferPosition(bufferPosition) # Copies the current file path to the native clipboard. copyPathToPasteboard: -> path = @getPath() pasteboard.write(path) if path? ### Internal ### @buildLineHtml: ({tokens, text, lineEnding, fold, isSoftWrapped, invisibles, eolInvisibles, htmlEolInvisibles, attributes, showIndentGuide, indentation, activeEditSession, mini}) -> scopeStack = [] line = [] updateScopeStack = (desiredScopes) -> excessScopes = scopeStack.length - desiredScopes.length _.times(excessScopes, popScope) if excessScopes > 0 # pop until common prefix for i in [scopeStack.length..0] break if _.isEqual(scopeStack[0...i], desiredScopes[0...i]) popScope() # push on top of common prefix until scopeStack == desiredScopes for j in [i...desiredScopes.length] pushScope(desiredScopes[j]) pushScope = (scope) -> scopeStack.push(scope) line.push("") popScope = -> scopeStack.pop() line.push("") attributePairs = [] attributePairs.push "#{attributeName}=\"#{value}\"" for attributeName, value of attributes line.push("