{View, $$} = require 'space-pen' Buffer = require 'text-buffer' Gutter = require 'gutter' Point = require 'point' Range = require 'range' EditSession = require 'edit-session' CursorView = require 'cursor-view' SelectionView = require 'selection-view' fsUtils = require 'fs-utils' $ = require 'jquery' _ = require 'underscore' # Public: 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 autoIndentOnPaste: false nonWordCharacters: "./\\()\"':,.;<>~!@#$%^&*|+=[]{}`~?-" preferredLineLength: 80 @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'] 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 ? {} requireStylesheet 'editor' @id = Editor.nextEditorId++ @lineCache = [] @configure() @bindKeys() @handleEvents() @cursorViews = [] @selectionViews = [] @pendingChanges = [] @newCursors = [] @newSelections = [] if editSession? @edit(editSession) else if @mini @edit(new EditSession buffer: new Buffer() 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: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: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: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 ### # Public # ### # Public: Retrieves a single cursor # # Returns a {Cursor}. getCursor: -> @activeEditSession.getCursor() # Public: Retrieves an array of all the cursors. # # Returns a {[Cursor]}. getCursors: -> @activeEditSession.getCursors() # Public: Adds a cursor at the provided `screenPosition`. # # screenPosition - An {Array} of two numbers: the screen row, and the screen column. # # Returns the new {Cursor}. addCursorAtScreenPosition: (screenPosition) -> @activeEditSession.addCursorAtScreenPosition(screenPosition) # Public: Adds a cursor at the provided `bufferPosition`. # # bufferPosition - An {Array} of two numbers: the buffer row, and the buffer column. # # Returns the new {Cursor}. addCursorAtBufferPosition: (bufferPosition) -> @activeEditSession.addCursorAtBufferPosition(bufferPosition) # Public: Moves every cursor up one row. moveCursorUp: -> @activeEditSession.moveCursorUp() # Public: Moves every cursor down one row. moveCursorDown: -> @activeEditSession.moveCursorDown() # Public: Moves every cursor left one column. moveCursorLeft: -> @activeEditSession.moveCursorLeft() # Public: Moves every cursor right one column. moveCursorRight: -> @activeEditSession.moveCursorRight() # Public: Moves every cursor to the beginning of the current word. moveCursorToBeginningOfWord: -> @activeEditSession.moveCursorToBeginningOfWord() # Public: Moves every cursor to the end of the current word. moveCursorToEndOfWord: -> @activeEditSession.moveCursorToEndOfWord() # Public: Moves the cursor to the beginning of the next word. moveCursorToBeginningOfNextWord: -> @activeEditSession.moveCursorToBeginningOfNextWord() # Public: Moves every cursor to the top of the buffer. moveCursorToTop: -> @activeEditSession.moveCursorToTop() # Public: Moves every cursor to the bottom of the buffer. moveCursorToBottom: -> @activeEditSession.moveCursorToBottom() # Public: Moves every cursor to the beginning of the line. moveCursorToBeginningOfLine: -> @activeEditSession.moveCursorToBeginningOfLine() # Public: Moves every cursor to the first non-whitespace character of the line. moveCursorToFirstCharacterOfLine: -> @activeEditSession.moveCursorToFirstCharacterOfLine() # Public: Moves every cursor to the end of the line. moveCursorToEndOfLine: -> @activeEditSession.moveCursorToEndOfLine() # Public: Moves the selected line up one row. moveLineUp: -> @activeEditSession.moveLineUp() # Public: Moves the selected line down one row. moveLineDown: -> @activeEditSession.moveLineDown() # Public: Sets the cursor based on a given screen position. # # position - An {Array} of two numbers: the screen row, and the screen column. # options - An object with properties based on {Cursor.setScreenPosition}. # setCursorScreenPosition: (position, options) -> @activeEditSession.setCursorScreenPosition(position, options) # Public: Duplicates the current line. # # If more than one cursor is present, only the most recently added one is considered. duplicateLine: -> @activeEditSession.duplicateLine() # Public: Joins the current line with the one below it. # # Multiple cursors are considered equally. If there's a selection in the editor, # all the lines are joined together. joinLine: -> @activeEditSession.joinLine() # Public: Gets the current screen position. # # Returns an {Array} of two numbers: the screen row, and the screen column. getCursorScreenPosition: -> @activeEditSession.getCursorScreenPosition() # Public: Gets the current screen row. # # Returns a {Number}. getCursorScreenRow: -> @activeEditSession.getCursorScreenRow() # Public: Sets the cursor based on a given buffer position. # # position - An {Array} of two numbers: the buffer row, and the buffer column. # options - An object with properties based on {Cursor.setBufferPosition}. # setCursorBufferPosition: (position, options) -> @activeEditSession.setCursorBufferPosition(position, options) # Public: Gets the current buffer position of the cursor. # # Returns an {Array} of two numbers: the buffer row, and the buffer column. getCursorBufferPosition: -> @activeEditSession.getCursorBufferPosition() # Public: Retrieves the range for the current paragraph. # # A paragraph is defined as a block of text surrounded by empty lines. # # Returns a {Range}. getCurrentParagraphBufferRange: -> @activeEditSession.getCurrentParagraphBufferRange() # Public: Gets the word located under the cursor. # # options - An object with properties based on {Cursor.getBeginningOfCurrentWordBufferPosition}. # # Returns a {String}. getWordUnderCursor: (options) -> @activeEditSession.getWordUnderCursor(options) # Public: Gets the selection at the specified index. # # index - The id {Number} of the selection # # Returns a {Selection}. getSelection: (index) -> @activeEditSession.getSelection(index) # Public: Gets the last selection, _i.e._ the most recently added. # # Returns a {Selection}. getSelections: -> @activeEditSession.getSelections() # Public: Gets all selections, ordered by their position in the buffer. # # Returns an {Array} of {Selection}s. getSelectionsOrderedByBufferPosition: -> @activeEditSession.getSelectionsOrderedByBufferPosition() # Public: Gets the very last selection, as it's ordered in the buffer. # # Returns a {Selection}. getLastSelectionInBuffer: -> @activeEditSession.getLastSelectionInBuffer() # Public: Gets the currently selected text. # # Returns a {String}. getSelectedText: -> @activeEditSession.getSelectedText() # Public: Gets the buffer ranges of all the {Selection}s. # # This is ordered by their buffer position. # # Returns an {Array} of {Range}s. getSelectedBufferRanges: -> @activeEditSession.getSelectedBufferRanges() # Public: Gets the buffer range of the most recently added {Selection}. # # Returns a {Range}. getSelectedBufferRange: -> @activeEditSession.getSelectedBufferRange() # Public: Given a buffer range, this removes all previous selections and creates a new selection for it. # # bufferRange - A {Range} in the buffer # options - A hash of options setSelectedBufferRange: (bufferRange, options) -> @activeEditSession.setSelectedBufferRange(bufferRange, options) # Public: Given an array of buffer ranges, this removes all previous selections and creates new selections for them. # # bufferRanges - An {Array} of {Range}s in the buffer # options - A hash of options setSelectedBufferRanges: (bufferRanges, options) -> @activeEditSession.setSelectedBufferRanges(bufferRanges, options) # Public: Given a buffer range, this adds a new selection for it. # # bufferRange - A {Range} in the buffer # options - A hash of options # # Returns the new {Selection}. addSelectionForBufferRange: (bufferRange, options) -> @activeEditSession.addSelectionForBufferRange(bufferRange, options) # Public: Selects the text one position right of the cursor. selectRight: -> @activeEditSession.selectRight() # Public: Selects the text one position left of the cursor. selectLeft: -> @activeEditSession.selectLeft() # Public: Selects all the text one position above the cursor. selectUp: -> @activeEditSession.selectUp() # Public: Selects all the text one position below the cursor. selectDown: -> @activeEditSession.selectDown() # Public: Selects all the text from the current cursor position to the top of the buffer. selectToTop: -> @activeEditSession.selectToTop() # Public: Selects all the text from the current cursor position to the bottom of the buffer. selectToBottom: -> @activeEditSession.selectToBottom() # Public: Selects all the text in the buffer. selectAll: -> @activeEditSession.selectAll() # Public: Selects all the text from the current cursor position to the beginning of the line. selectToBeginningOfLine: -> @activeEditSession.selectToBeginningOfLine() # Public: Selects all the text from the current cursor position to the end of the line. selectToEndOfLine: -> @activeEditSession.selectToEndOfLine() # Public: Moves the current selection down one row. addSelectionBelow: -> @activeEditSession.addSelectionBelow() # Public: Moves the current selection up one row. addSelectionAbove: -> @activeEditSession.addSelectionAbove() # Public: Selects all the text from the current cursor position to the beginning of the word. selectToBeginningOfWord: -> @activeEditSession.selectToBeginningOfWord() # Public: Selects all the text from the current cursor position to the end of the word. selectToEndOfWord: -> @activeEditSession.selectToEndOfWord() # Public: Selects all the text from the current cursor position to the beginning of the next word. selectToBeginningOfNextWord: -> @activeEditSession.selectToBeginningOfNextWord() # Public: Selects the current word. selectWord: -> @activeEditSession.selectWord() # Public: Selects the current line. selectLine: -> @activeEditSession.selectLine() # Public: Selects the text from the current cursor position to a given position. # # position - An instance of {Point}, with a given `row` and `column`. selectToScreenPosition: (position) -> @activeEditSession.selectToScreenPosition(position) # Public: Transposes the current text selections. # # This only works if there is more than one selection. Each selection is transferred # to the position of the selection after it. The last selection is transferred to the # position of the first. transpose: -> @activeEditSession.transpose() # Public: Turns the current selection into upper case. upperCase: -> @activeEditSession.upperCase() # Public: Turns the current selection into lower case. lowerCase: -> @activeEditSession.lowerCase() # Public: Clears every selection. TODO clearSelections: -> @activeEditSession.clearSelections() # Public: Performs a backspace, removing the character found behind the cursor position. backspace: -> @activeEditSession.backspace() # Public: Performs a backspace to the beginning of the current word, removing characters found there. backspaceToBeginningOfWord: -> @activeEditSession.backspaceToBeginningOfWord() # Public: Performs a backspace to the beginning of the current line, removing characters found there. backspaceToBeginningOfLine: -> @activeEditSession.backspaceToBeginningOfLine() # Public: Performs a delete, removing the character found ahead the cursor position. delete: -> @activeEditSession.delete() # Public: Performs a delete to the end of the current word, removing characters found there. deleteToEndOfWord: -> @activeEditSession.deleteToEndOfWord() # Public: Performs a delete to the end of the current line, removing characters found there. deleteLine: -> @activeEditSession.deleteLine() # Public: Performs a cut to the end of the current line. # # Characters are removed, but the text remains in the clipboard. cutToEndOfLine: -> @activeEditSession.cutToEndOfLine() # Public: Inserts text at the current cursor positions. # # text - A {String} representing the text to insert. # options - A set of options equivalent to {Selection.insertText}. insertText: (text, options) -> @activeEditSession.insertText(text, options) # Public: Inserts a new line at the current cursor positions. insertNewline: -> @activeEditSession.insertNewline() # Internal: consolidateSelections: (e) -> e.abortKeyBinding() unless @activeEditSession.consolidateSelections() # Public: Inserts a new line below the current cursor positions. insertNewlineBelow: -> @activeEditSession.insertNewlineBelow() # Public: Inserts a new line above the current cursor positions. insertNewlineAbove: -> @activeEditSession.insertNewlineAbove() # Public: Indents the current line. # # options - A set of options equivalent to {Selection.indent}. indent: (options) -> @activeEditSession.indent(options) # Public: TODO autoIndent: (options) -> @activeEditSession.autoIndentSelectedRows() # Public: Indents the selected rows. indentSelectedRows: -> @activeEditSession.indentSelectedRows() # Public: Outdents the selected rows. outdentSelectedRows: -> @activeEditSession.outdentSelectedRows() # Public: Cuts the selected text. cutSelection: -> @activeEditSession.cutSelectedText() # Public: Copies the selected text. copySelection: -> @activeEditSession.copySelectedText() # Public: Pastes the text in the clipboard. # # options - A set of options equivalent to {Selection.insertText}. paste: (options) -> @activeEditSession.pasteText(options) # Public: Undos the last {Buffer} change. undo: -> @activeEditSession.undo() # Public: Redos the last {Buffer} change. redo: -> @activeEditSession.redo() # Public: Creates a new fold between two row numbers. # # startRow - The row {Number} to start folding at # endRow - The row {Number} to end the fold # # Returns the new {Fold}. createFold: (startRow, endRow) -> @activeEditSession.createFold(startRow, endRow) # Public: Folds the current row. foldCurrentRow: -> @activeEditSession.foldCurrentRow() # Public: Unfolds the current row. unfoldCurrentRow: -> @activeEditSession.unfoldCurrentRow() # Public: Folds all the rows. foldAll: -> @activeEditSession.foldAll() # Public: Unfolds all the rows. unfoldAll: -> @activeEditSession.unfoldAll() # Public: Folds the most recent selection. foldSelection: -> @activeEditSession.foldSelection() # Public: Given the id of a {Fold}, this removes it. # # foldId - The fold id {Number} to remove destroyFold: (foldId) -> @activeEditSession.destroyFold(foldId) # Public: Removes any {Fold}s found that contain the given buffer row. # # bufferRow - The buffer row {Number} to check against destroyFoldsContainingBufferRow: (bufferRow) -> @activeEditSession.destroyFoldsContainingBufferRow(bufferRow) # Public: Determines if the given screen row is folded. # # screenRow - A {Number} indicating the screen row. # # Returns `true` if the screen row is folded, `false` otherwise. isFoldedAtScreenRow: (screenRow) -> @activeEditSession.isFoldedAtScreenRow(screenRow) # Public: Determines if the given buffer row is folded. # # screenRow - A {Number} indicating the buffer row. # # Returns `true` if the buffer row is folded, `false` otherwise. isFoldedAtBufferRow: (bufferRow) -> @activeEditSession.isFoldedAtBufferRow(bufferRow) # Public: Determines if the given row that the cursor is at is folded. # # Returns `true` if the row is folded, `false` otherwise. isFoldedAtCursorRow: -> @activeEditSession.isFoldedAtCursorRow() # Public: Gets the line for the given screen row. # # screenRow - A {Number} indicating the screen row. # # Returns a {String}. lineForScreenRow: (screenRow) -> @activeEditSession.lineForScreenRow(screenRow) # Public: Gets the lines for the given screen row boundaries. # # start - A {Number} indicating the beginning screen row. # end - A {Number} indicating the ending screen row. # # Returns an {Array} of {String}s. linesForScreenRows: (start, end) -> @activeEditSession.linesForScreenRows(start, end) # Public: Gets the number of screen rows. # # Returns a {Number}. getScreenLineCount: -> @activeEditSession.getScreenLineCount() # Public: Defines the limit at which the buffer begins to soft wrap text. # # softWrapColumn - A {Number} defining the soft wrap limit setSoftWrapColumn: (softWrapColumn) -> softWrapColumn ?= @calcSoftWrapColumn() @activeEditSession.setSoftWrapColumn(softWrapColumn) if softWrapColumn # Public: Gets the length of the longest screen line. # # Returns a {Number}. maxScreenLineLength: -> @activeEditSession.maxScreenLineLength() # Public: Gets the text in the last screen row. # # Returns a {String}. getLastScreenRow: -> @activeEditSession.getLastScreenRow() # Public: Given a position, this clips it to a real position. # # For example, if `position`'s row exceeds the row count of the buffer, # or if its column goes beyond a line's length, this "sanitizes" the value # to a real position. # # position - The {Point} to clip # options - A hash with the following values: # :wrapBeyondNewlines - if `true`, continues wrapping past newlines # :wrapAtSoftNewlines - if `true`, continues wrapping past soft newlines # :screenLine - if `true`, indicates that you're using a line number, not a row number # # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed. clipScreenPosition: (screenPosition, options={}) -> @activeEditSession.clipScreenPosition(screenPosition, options) # Public: Given a buffer position, this converts it into a screen position. # # bufferPosition - An object that represents a buffer position. It can be either # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} # options - The same options available to {DisplayBuffer.screenPositionForBufferPosition}. # # Returns a {Point}. screenPositionForBufferPosition: (position, options) -> @activeEditSession.screenPositionForBufferPosition(position, options) # Public: Given a buffer range, this converts it into a screen position. # # screenPosition - An object that represents a buffer position. It can be either # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} # options - The same options available to {DisplayBuffer.bufferPositionForScreenPosition}. # # Returns a {Point}. bufferPositionForScreenPosition: (position, options) -> @activeEditSession.bufferPositionForScreenPosition(position, options) # Public: Given a buffer range, this converts it into a screen position. # # bufferRange - The {Range} to convert # # Returns a {Range}. screenRangeForBufferRange: (range) -> @activeEditSession.screenRangeForBufferRange(range) # Public: Given a screen range, this converts it into a buffer position. # # screenRange - The {Range} to convert # # Returns a {Range}. bufferRangeForScreenRange: (range) -> @activeEditSession.bufferRangeForScreenRange(range) # Public: Given a starting and ending row, this converts every row into a buffer position. # # startRow - The row {Number} to start at # endRow - The row {Number} to end at (default: {.getLastScreenRow}) # # Returns an {Array} of {Range}s. bufferRowsForScreenRows: (startRow, endRow) -> @activeEditSession.bufferRowsForScreenRows(startRow, endRow) # Public: Gets the number of the last row in the buffer. # # Returns a {Number}. getLastScreenRow: -> @activeEditSession.getLastScreenRow() # Internal: logCursorScope: -> console.log @activeEditSession.getCursorScopes() # 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) # 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 - 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() # 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: Checks out the current HEAD revision of the file. checkoutHead: -> @getBuffer().checkoutHead() # Public: Replaces the current buffer contents. # # text - A {String} containing the new buffer contents. setText: (text) -> @activeEditSession.setText(text) # Public: Retrieves the current buffer contents. # # Returns a {String}. getText: -> @activeEditSession.getText() # Public: Retrieves the current buffer's file path. # # Returns a {String}. getPath: -> @activeEditSession?.getPath() # Public: Gets the number of lines in a file. # # Returns a {Number}. getLineCount: -> @getBuffer().getLineCount() # Public: Gets the row number of the last line. # # Returns a {Number}. getLastBufferRow: -> @getBuffer().getLastRow() # Public: Given a range, returns the lines of text within it. # # range - A {Range} object specifying your points of interest # # Returns a {String} of the combined lines. getTextInRange: (range) -> @getBuffer().getTextInRange(range) # Public: Finds the last point in the current buffer. # # Returns a {Point} representing the last position. getEofPosition: -> @getBuffer().getEofPosition() # Public: Given a row, returns the line of text. # # row - A {Number} indicating the row. # # Returns a {String}. lineForBufferRow: (row) -> @getBuffer().lineForRow(row) # Public: Given a row, returns the length of the line of text. # # row - A {Number} indicating the row # # Returns a {Number}. lineLengthForBufferRow: (row) -> @getBuffer().lineLengthForRow(row) # Public: Given a buffer row, this retrieves the range for that line. # # row - A {Number} identifying the row # options - A hash with one key, `includeNewline`, which specifies whether you # want to include the trailing newline # # Returns a {Range}. rangeForBufferRow: (row) -> @getBuffer().rangeForRow(row) # Public: Scans for text in the buffer, calling a function on each match. # # regex - A {RegExp} representing the text to find # range - A {Range} in the buffer to search within # iterator - A {Function} that's called on each match scanInBufferRange: (args...) -> @getBuffer().scanInRange(args...) # Public: Scans for text in the buffer _backwards_, calling a function on each match. # # regex - A {RegExp} representing the text to find # range - A {Range} in the buffer to search within # iterator - A {Function} that's called on each match 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 'click', (e) => return unless e.target is @underlayer[0] return unless e.offsetY > @overlayer.height() if e.shiftKey @selectToBottom() else @moveCursorToBottom() @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) => @destroyFold($(e.currentTarget).attr('fold-id')) false onMouseDown = (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 @renderedLines.on 'mousedown', onMouseDown @on "textInput", (e) => @insertText(e.originalEvent.data) false @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 @scrollView.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 reverse = @activeEditSession.getLastSelection().isReversed() @activeEditSession.mergeIntersectingSelections({reverse}) @activeEditSession.finalizeSelections() @syncCursorAnimations() afterAttach: (onDom) -> return unless onDom @redraw() if @redrawOnReattach return if @attached @attached = true @calculateDimensions() @setSoftWrapColumn() if @activeEditSession.getSoftWrap() @subscribe $(window), "resize.editor-#{@id}", => @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) @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) scrollBottom: (scrollBottom) -> if scrollBottom? @scrollTop(scrollBottom - @scrollView.height()) else @scrollTop() + @scrollView.height() ### # Public # ### # Public: Retrieves the {EditSession}'s buffer. # # Returns the current {Buffer}. getBuffer: -> @activeEditSession.buffer # Public: Scrolls the editor to the bottom. scrollToBottom: -> @scrollBottom(@getScreenLineCount() * @lineHeight) # Public: Scrolls the editor to the position of the most recently added cursor. # # The editor is also centered. scrollToCursorPosition: -> @scrollToBufferPosition(@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) # 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) # Internal: 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 > @scrollView.scrollRight() @scrollView.scrollRight(desiredRight) else if desiredLeft < @scrollView.scrollLeft() @scrollView.scrollLeft(desiredLeft) # Public: 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') setScrollPositionFromActiveEditSession: -> @scrollTop(@activeEditSession.scrollTop ? 0) @scrollView.scrollLeft(@activeEditSession.scrollLeft ? 0) saveScrollPositionForActiveEditSession: -> @activeEditSession.setScrollTop(@scrollTop()) @activeEditSession.setScrollLeft(@scrollView.scrollLeft()) # Public: Activates soft tabs in the editor. toggleSoftTabs: -> @activeEditSession.setSoftTabs(not @activeEditSession.softTabs) # Public: Activates soft wraps in the editor. toggleSoftWrap: -> @setSoftWrap(not @activeEditSession.getSoftWrap()) calcSoftWrapColumn: -> if @activeEditSession.getSoftWrap() Math.floor(@scrollView.width() / @charWidth) else Infinity # Public: Sets the soft wrap column for the editor. # # softWrap - A {Boolean} which, if `true`, sets soft wraps # softWrapColumn - A {Number} indicating the length of a line in the editor when soft # wrapping turns on setSoftWrap: (softWrap, softWrapColumn=undefined) -> @activeEditSession.setSoftWrap(softWrap) @setSoftWrapColumn(softWrapColumn) if @attached if @activeEditSession.getSoftWrap() @addClass 'soft-wrap' @scrollView.scrollLeft(0) @_setSoftWrapColumn = => @setSoftWrapColumn() $(window).on "resize.editor-#{@id}", @_setSoftWrapColumn else @removeClass 'soft-wrap' $(window).off 'resize', @_setSoftWrapColumn # Public: 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 # 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) -> 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() # Public: Gets the font family for the editor. # # Returns a {String} identifying the CSS `font-family`, getFontFamily: -> @css("font-family") # Public: Clears the CSS `font-family` property from the editor. clearFontFamily: -> $('head style.editor-font-family').remove() # Public: 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 # Public: 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 # ### 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 @underlayer.css('min-height', height) @renderedLines.height(height) @overlayer.height(height) @layerHeight = height bottomPaddingInLines = if @mini then 0 else @bottomPaddingInLines heightWithPadding = height + (@lineHeight * bottomPaddingInLines) @verticalScrollbarContent.height(heightWithPadding) @scrollBottom(height) if @scrollBottom() > height minWidth = @charWidth * @maxScreenLineLength() + 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() @updateLayerDimensions() @setScrollPositionFromActiveEditSession() @newCursors = @activeEditSession.getCursors() @newSelections = @activeEditSession.getSelections() @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() if @firstRenderedScreenRow? and firstVisibleScreenRow >= @firstRenderedScreenRow and lastVisibleScreenRow <= @lastRenderedScreenRow renderFrom = @firstRenderedScreenRow renderTo = Math.min(@getLastScreenRow(), @lastRenderedScreenRow) else renderFrom = Math.max(0, firstVisibleScreenRow - @lineOverdraw) renderTo = Math.min(@getLastScreenRow(), 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() computeIntactRanges: -> return [] if !@firstRenderedScreenRow? and !@lastRenderedScreenRow? intactRanges = [{start: @firstRenderedScreenRow, end: @lastRenderedScreenRow, domStart: 0}] if not @mini and @showIndentGuide trailingEmptyLineChanges = [] for change in @pendingChanges continue unless change.bufferDelta? start = change.end + change.bufferDelta + 1 continue unless @lineForBufferRow(start) is '' end = start end++ while @lineForBufferRow(end + 1) is '' trailingEmptyLineChanges.push({start, end, screenDelta: 0}) @pendingChanges.push(trailingEmptyLineChanges...) 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 # ### # 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) # Public: 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)) # 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() ### # Internal # ### handleScreenLinesChange: (change) -> @pendingChanges.push(change) @requestDisplayUpdate() buildLineElementForScreenRow: (screenRow) -> @buildLineElementsForScreenRows(screenRow, screenRow)[0] buildLineElementsForScreenRows: (startRow, endRow) -> div = document.createElement('div') div.innerHTML = @buildLinesHtml(startRow, endRow) new Array(div.children...) buildLinesHtml: (startRow, endRow) -> lines = @activeEditSession.linesForScreenRows(startRow, endRow) htmlLines = [] screenRow = startRow for line in @activeEditSession.linesForScreenRows(startRow, endRow) htmlLines.push(@buildLineHtml(line, screenRow++)) htmlLines.join('\n\n') buildEmptyLineHtml: (screenRow) -> if not @mini and @showIndentGuide indentation = 0 while --screenRow >= 0 bufferRow = @activeEditSession.bufferPositionForScreenPosition([screenRow]).row bufferLine = @activeEditSession.lineForBufferRow(bufferRow) unless bufferLine is '' indentation = Math.ceil(@activeEditSession.indentLevelForLine(bufferLine)) break if indentation > 0 indentationHtml = "#{_.multiplyString(' ', @activeEditSession.getTabLength())}" return _.multiplyString(indentationHtml, indentation) return ' ' unless @showInvisibles buildLineHtml: (screenLine, screenRow) -> 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("") if fold = screenLine.fold lineAttributes = { class: 'fold line', 'fold-id': fold.id } else lineAttributes = { class: 'line' } attributePairs = [] attributePairs.push "#{attributeName}=\"#{value}\"" for attributeName, value of lineAttributes line.push("
") invisibles = @invisibles if @showInvisibles if screenLine.text == '' html = @buildEmptyLineHtml(screenRow) line.push(html) if html else firstNonWhitespacePosition = screenLine.text.search(/\S/) firstTrailingWhitespacePosition = screenLine.text.search(/\s*$/) lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 position = 0 for token in screenLine.tokens updateScopeStack(token.scopes) hasLeadingWhitespace = position < firstNonWhitespacePosition hasTrailingWhitespace = position + token.value.length > firstTrailingWhitespacePosition hasIndentGuide = not @mini and @showIndentGuide and (hasLeadingWhitespace or lineIsWhitespaceOnly) line.push(token.getValueAsHtml({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide})) position += token.value.length popScope() while scopeStack.length > 0 if invisibles and not @mini and not screenLine.isSoftWrapped() if invisibles.cr and screenLine.lineEnding is '\r\n' line.push("#{invisibles.cr}") if invisibles.eol line.push("#{invisibles.eol}") line.push("") if fold line.push('
') line.join('') lineElementForScreenRow: (screenRow) -> @renderedLines.children(":eq(#{screenRow - @firstRenderedScreenRow})") toggleLineCommentsInSelection: -> @activeEditSession.toggleLineCommentsInSelection() ### # Public # ### # 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)) # 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, 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 - @scrollView.offset().left + @scrollView.scrollLeft() range.detach() leftPixels pixelOffsUtilsetForScreenPosition: (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) # Public: 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 # Public: Retrieves the current {EditSession}'s grammar. # # Returns a {String} indicating the language's grammar rules. getGrammar: -> @activeEditSession.getGrammar() # Public: Sets the current {EditSession}'s grammar. This only works for mini-editors. # # grammar - A {String} indicating the language's grammar rules. setGrammar: (grammar) -> throw new Error("Only mini-editors can explicity set their grammar") unless @mini @activeEditSession.setGrammar(grammar) # Public: Reloads the current grammar. reloadGrammar: -> @activeEditSession.reloadGrammar() bindToKeyedEvent: (key, event, callback) -> binding = {} binding[key] = event window.keymap.bindKeys '.editor', binding @on event, => callback(this, event) # Internal: Replaces all the currently selected text. replaceSelectedText: (replaceFn) -> selection = @getSelection() return false if selection.isEmpty() text = replaceFn(@getTextInRange(selection.getBufferRange())) return false if text is null or text is undefined @insertText(text, select: true) true # Public: Copies the current file path to the native clipboard. copyPathToPasteboard: -> path = @getPath() pasteboard.write(path) if path? ### # Internal # ### transact: (fn) -> @activeEditSession.transact(fn) commit: -> @activeEditSession.commit() abort: -> @activeEditSession.abort() saveDebugSnapshot: -> atom.showSaveDialog (path) => fsUtils.write(path, @getDebugSnapshot()) if path getDebugSnapshot: -> [ "Debug Snapshot: #{@getPath()}" @getRenderedLinesDebugSnapshot() @activeEditSession.getDebugSnapshot() @getBuffer().getDebugSnapshot() ].join('\n\n') getRenderedLinesDebugSnapshot: -> lines = ['Rendered Lines:'] firstRenderedScreenRow = @firstRenderedScreenRow @renderedLines.find('.line').each (n) -> lines.push "#{firstRenderedScreenRow + n}: #{$(this).text()}" lines.join('\n') logScreenLines: (start, end) -> @activeEditSession.logScreenLines(start, end) logRenderedLines: -> @renderedLines.find('.line').each (n) -> console.log n, $(this).text()