Point = require 'point' Buffer = require 'text-buffer' LanguageMode = require 'language-mode' DisplayBuffer = require 'display-buffer' Cursor = require 'cursor' Selection = require 'selection' EventEmitter = require 'event-emitter' Subscriber = require 'subscriber' Range = require 'range' _ = require 'underscore' fsUtils = require 'fs-utils' # Public: An `EditSession` manages the states between {Editor}s, {Buffer}s, and the project as a whole. module.exports = class EditSession registerDeserializer(this) ### # Internal # ### @version: 1 @deserialize: (state) -> session = project.buildEditSessionForBuffer(Buffer.deserialize(state.buffer)) if !session? console.warn "Could not build edit session for path '#{state.buffer}' because that file no longer exists" if state.buffer session = project.buildEditSession(null) session.setScrollTop(state.scrollTop) session.setScrollLeft(state.scrollLeft) session.setCursorScreenPosition(state.cursorScreenPosition) session scrollTop: 0 scrollLeft: 0 languageMode: null displayBuffer: null cursors: null selections: null softTabs: true softWrap: false constructor: ({@project, @buffer, tabLength, softTabs, @softWrap }) -> @softTabs = @buffer.usesSoftTabs() ? softTabs ? true @languageMode = new LanguageMode(this, @buffer.getExtension()) @displayBuffer = new DisplayBuffer(@buffer, { @languageMode, tabLength }) @cursors = [] @selections = [] @addCursorAtScreenPosition([0, 0]) @buffer.retain() @subscribe @buffer, "path-changed", => @project.setPath(fsUtils.directory(@getPath())) unless @project.getPath()? @trigger "title-changed" @trigger "path-changed" @subscribe @buffer, "contents-conflicted", => @trigger "contents-conflicted" @subscribe @buffer, "markers-updated", => @mergeCursors() @subscribe @buffer, "modified-status-changed", => @trigger "modified-status-changed" @preserveCursorPositionOnBufferReload() @subscribe @displayBuffer, "changed", (e) => @trigger 'screen-lines-changed', e @displayBuffer.on 'grammar-changed', => @handleGrammarChange() getViewClass: -> require 'editor' destroy: -> return if @destroyed @destroyed = true @unsubscribe() @buffer.release() selection.destroy() for selection in @getSelections() @displayBuffer.destroy() @languageMode.destroy() @project?.removeEditSession(this) @trigger 'destroyed' @off() serialize: -> deserializer: 'EditSession' version: @constructor.version buffer: @buffer.serialize() scrollTop: @getScrollTop() scrollLeft: @getScrollLeft() cursorScreenPosition: @getCursorScreenPosition().serialize() # Internal: Creates a copy of the current {EditSession}. # # Returns an identical `EditSession`. copy: -> EditSession.deserialize(@serialize(), @project) ### # Public # ### # Public: Retrieves the filename of the open file. # # This is `'untitled'` if the file is new and not saved to the disk. # # Returns a {String}. getTitle: -> if path = @getPath() fsUtils.base(path) else 'untitled' # Public: Retrieves the filename of the open file, followed by a dash, then the file's directory. # # If the file is brand new, the title is `untitled`. # # Returns a {String}. getLongTitle: -> if path = @getPath() fileName = fsUtils.base(path) directory = fsUtils.base(fsUtils.directory(path)) "#{fileName} - #{directory}" else 'untitled' # Public: Compares two `EditSession`s to determine equality. # # Equality is based on the condition that: # # * the two {Buffer}s are the same # * the two `scrollTop` and `scrollLeft` property are the same # * the two {Cursor} screen positions are the same # # Returns a {Boolean}. isEqual: (other) -> return false unless other instanceof EditSession @buffer == other.buffer and @scrollTop == other.getScrollTop() and @scrollLeft == other.getScrollLeft() and @getCursorScreenPosition().isEqual(other.getCursorScreenPosition()) setVisible: (visible) -> @displayBuffer.setVisible(visible) # Public: Defines the value of the `EditSession`'s `scrollTop` property. # # scrollTop - A {Number} defining the `scrollTop`, in pixels. setScrollTop: (@scrollTop) -> # Public: Gets the value of the `EditSession`'s `scrollTop` property. # # Returns a {Number} defining the `scrollTop`, in pixels. getScrollTop: -> @scrollTop # Public: Defines the value of the `EditSession`'s `scrollLeft` property. # # scrollLeft - A {Number} defining the `scrollLeft`, in pixels. setScrollLeft: (@scrollLeft) -> # Public: Gets the value of the `EditSession`'s `scrollLeft` property. # # Returns a {Number} defining the `scrollLeft`, in pixels. getScrollLeft: -> @scrollLeft # Public: Defines the limit at which the buffer begins to soft wrap text. # # softWrapColumn - A {Number} defining the soft wrap limit setSoftWrapColumn: (@softWrapColumn) -> @displayBuffer.setSoftWrapColumn(@softWrapColumn) # Public: Defines whether to use soft tabs. # # softTabs - A {Boolean} which, if `true`, indicates that you want soft tabs. setSoftTabs: (@softTabs) -> # Public: Retrieves whether soft tabs are enabled. # # Returns a {Boolean}. getSoftWrap: -> @softWrap # Public: Defines whether to use soft wrapping of text. # # softTabs - A {Boolean} which, if `true`, indicates that you want soft wraps. setSoftWrap: (@softWrap) -> # Public: Retrieves that character used to indicate a tab. # # If soft tabs are enabled, this is a space (`" "`) times the {.getTabLength} value. # Otherwise, it's a tab (`\t`). # # Returns a {String}. getTabText: -> @buildIndentString(1) # Public: Retrieves the current tab length. # # Returns a {Number}. getTabLength: -> @displayBuffer.getTabLength() # Public: Specifies the tab length. # # tabLength - A {Number} that defines the new tab length. setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) # 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 # # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed. clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) # Public: Given a range, this clips it to a real range. # # For example, if `range`'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 range. # # range - The {Point} to clip # # Returns the new, clipped {Point}. Note that this could be the same as `range` if no clipping was performed. clipBufferRange: (range) -> @buffer.clipRange(range) # Public: Given a buffer row, this retrieves the indentation level. # # bufferRow - A {Number} indicating the buffer row. # # Returns the indentation level as a {Number}. indentationForBufferRow: (bufferRow) -> @indentLevelForLine(@lineForBufferRow(bufferRow)) # Public: This specifies the new indentation level for a buffer row. # # bufferRow - A {Number} indicating the buffer row. # newLevel - A {Number} indicating the new indentation level. setIndentationForBufferRow: (bufferRow, newLevel) -> currentLevel = @indentationForBufferRow(bufferRow) currentIndentString = @buildIndentString(currentLevel) newIndentString = @buildIndentString(newLevel) @buffer.change([[bufferRow, 0], [bufferRow, currentIndentString.length]], newIndentString) # Internal: Given a line, this gets the indentation level. # # line - A {String} in the current {Buffer}. # # Returns a {Number}. indentLevelForLine: (line) -> if match = line.match(/^[\t ]+/) leadingWhitespace = match[0] tabCount = leadingWhitespace.match(/\t/g)?.length ? 0 spaceCount = leadingWhitespace.match(/[ ]/g)?.length ? 0 tabCount + (spaceCount / @getTabLength()) else 0 # Internal: Constructs the string used for tabs. buildIndentString: (number) -> if @softTabs _.multiplyString(" ", number * @getTabLength()) else _.multiplyString("\t", Math.floor(number)) # Public: Saves the buffer. save: -> @buffer.save() # Public: Saves the buffer at a specific path. # # path - The path to save at. saveAs: (path) -> @buffer.saveAs(path) # Public: Retrieves the current buffer's file extension. # # Returns a {String}. getFileExtension: -> @buffer.getExtension() # Public: Retrieves the current buffer's file path. # # Returns a {String}. getPath: -> @buffer.getPath() # Public: Retrieves the current buffer's text. # # Return a {String}. getText: -> @buffer.getText() # Public: Set the current buffer's text content. # # Return a {String}. setText: (text) -> @buffer.setText(text) # Public: Retrieves the current buffer. # # Returns a {String}. getBuffer: -> @buffer # Public: Retrieves the current buffer's URI. # # Returns a {String}. getUri: -> @getPath() # Public: Given a buffer row, identifies if it is blank. # # bufferRow - A buffer row {Number} to check # # Returns a {Boolean}. isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow) # Public: Given a buffer row, this finds the next row that's blank. # # bufferRow - A buffer row {Number} to check # # Returns a {Number}, or `null` if there's no other blank row. nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow) # Public: Finds the last point in the current buffer. # # Returns a {Point} representing the last position. getEofBufferPosition: -> @buffer.getEofPosition() # Public: Finds the last line in the current buffer. # # Returns a {Number}. getLastBufferRow: -> @buffer.getLastRow() # 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}. bufferRangeForBufferRow: (row, options) -> @buffer.rangeForRow(row, options) # Public: Given a buffer row, this retrieves that line. # # row - A {Number} identifying the row # # Returns a {String}. lineForBufferRow: (row) -> @buffer.lineForRow(row) # Public: Given a buffer row, this retrieves that line's length. # # row - A {Number} identifying the row # # Returns a {Number}. lineLengthForBufferRow: (row) -> @buffer.lineLengthForRow(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...) -> @buffer.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...) -> @buffer.backwardsScanInRange(args...) # Public: Identifies if the {Buffer} is modified (and not saved). # # Returns a {Boolean}. isModified: -> @buffer.isModified() # Public: Identifies if the modified buffer should let you know if it's closing # without being saved. # # Returns a {Boolean}. shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors() # 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: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, 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: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options) # Public: Given a buffer range, this converts it into a screen position. # # bufferRange - The {Range} to convert # # Returns a {Range}. screenRangeForBufferRange: (bufferRange) -> @displayBuffer.screenRangeForBufferRange(bufferRange) # Public: Given a screen range, this converts it into a buffer position. # # screenRange - The {Range} to convert # # Returns a {Range}. bufferRangeForScreenRange: (screenRange) -> @displayBuffer.bufferRangeForScreenRange(screenRange) # 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) -> @displayBuffer.clipScreenPosition(screenPosition, options) # Public: Gets the line for the given screen row. # # screenRow - A {Number} indicating the screen row. # # Returns a {String}. lineForScreenRow: (row) -> @displayBuffer.lineForRow(row) # 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) -> @displayBuffer.linesForRows(start, end) # Public: Gets the number of screen rows. # # Returns a {Number}. getScreenLineCount: -> @displayBuffer.getLineCount() # Public: Gets the length of the longest screen line. # # Returns a {Number}. maxScreenLineLength: -> @displayBuffer.maxLineLength() # Public: Gets the number of the last row in the buffer. # # Returns a {Number}. getLastScreenRow: -> @displayBuffer.getLastRow() # 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) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow) # Public: Retrieves the grammar's token scopes for a buffer position. # # bufferPosition - A {Point} in the {Buffer} # # Returns an {Array} of {String}s. scopesForBufferPosition: (bufferPosition) -> @displayBuffer.scopesForBufferPosition(bufferPosition) # Public: Retrieves the grammar's token for a buffer position. # # bufferPosition - A {Point} in the {Buffer} # # Returns a {Token}. tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) # Public: Retrieves the grammar's token scopes for the line with the most recently added cursor. # # Returns an {Array} of {String}s. getCursorScopes: -> @getCursor().getScopes() # Internal: logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) # Public: Determines whether the {Editor} will auto indent rows. # # Returns a {Boolean}. shouldAutoIndent: -> config.get("editor.autoIndent") # Public: Determines whether the {Editor} will auto indent pasted text. # # Returns a {Boolean}. shouldAutoIndentPastedText: -> config.get("editor.autoIndentOnPaste") # 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={}) -> options.autoIndent ?= @shouldAutoIndent() @mutateSelectedText (selection) -> selection.insertText(text, options) # Public: Inserts a new line at the current cursor positions. insertNewline: -> @insertText('\n') # Public: Inserts a new line below the current cursor positions. insertNewlineBelow: -> @transact => @moveCursorToEndOfLine() @insertNewline() # Public: Inserts a new line above the current cursor positions. insertNewlineAbove: -> @transact => onFirstLine = @getCursorBufferPosition().row is 0 @moveCursorToBeginningOfLine() @moveCursorLeft() @insertNewline() @moveCursorUp() if onFirstLine # Public: Indents the current line. # # options - A set of options equivalent to {Selection.indent}. indent: (options={})-> options.autoIndent ?= @shouldAutoIndent() @mutateSelectedText (selection) -> selection.indent(options) # Public: Performs a backspace, removing the character found behind the cursor position. backspace: -> @mutateSelectedText (selection) -> selection.backspace() # Public: Performs a backspace to the beginning of the current word, removing characters found there. backspaceToBeginningOfWord: -> @mutateSelectedText (selection) -> selection.backspaceToBeginningOfWord() # Public: Performs a backspace to the beginning of the current line, removing characters found there. backspaceToBeginningOfLine: -> @mutateSelectedText (selection) -> selection.backspaceToBeginningOfLine() # Public: Performs a delete, removing the character found ahead of the cursor position. delete: -> @mutateSelectedText (selection) -> selection.delete() # Public: Performs a delete to the end of the current word, removing characters found there. deleteToEndOfWord: -> @mutateSelectedText (selection) -> selection.deleteToEndOfWord() # Public: Deletes the entire line. deleteLine: -> @mutateSelectedText (selection) -> selection.deleteLine() # Public: Indents the selected rows. indentSelectedRows: -> @mutateSelectedText (selection) -> selection.indentSelectedRows() # Public: Outdents the selected rows. outdentSelectedRows: -> @mutateSelectedText (selection) -> selection.outdentSelectedRows() # Public: Wraps the lines within a selection in comments. # # If the language doesn't have comments, nothing happens. # # selection - The {Selection} to comment # # Returns an {Array} of the commented {Ranges}. toggleLineCommentsInSelection: -> @mutateSelectedText (selection) -> selection.toggleLineComments() autoIndentSelectedRows: -> @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() # Given a buffer range, this converts all `\t` characters to the appopriate {.getTabText} value. # # bufferRange - The {Range} to perform the replace in normalizeTabsInBufferRange: (bufferRange) -> return unless @softTabs @scanInBufferRange /\t/, bufferRange, ({replace}) => replace(@getTabText()) # Public: Performs a cut to the end of the current line. # # Characters are removed, but the text remains in the clipboard. cutToEndOfLine: -> maintainPasteboard = false @mutateSelectedText (selection) -> selection.cutToEndOfLine(maintainPasteboard) maintainPasteboard = true # Public: Cuts the selected text. cutSelectedText: -> maintainPasteboard = false @mutateSelectedText (selection) -> selection.cut(maintainPasteboard) maintainPasteboard = true # Public: Copies the selected text. copySelectedText: -> maintainPasteboard = false for selection in @getSelections() selection.copy(maintainPasteboard) maintainPasteboard = true # Public: Pastes the text in the clipboard. # # options - A set of options equivalent to {Selection.insertText}. pasteText: (options={}) -> options.normalizeIndent ?= true options.autoIndent ?= @shouldAutoIndentPastedText() [text, metadata] = pasteboard.read() _.extend(options, metadata) if metadata @insertText(text, options) # Public: Undos the last {Buffer} change. undo: -> @buffer.undo(this) # Public: Redos the last {Buffer} change. redo: -> @buffer.redo(this) ### # Internal # ### transact: (fn) -> isNewTransaction = @buffer.transact() oldSelectedRanges = @getSelectedBufferRanges() @pushOperation undo: (editSession) -> editSession?.setSelectedBufferRanges(oldSelectedRanges) if fn result = fn() @commit() if isNewTransaction result commit: -> newSelectedRanges = @getSelectedBufferRanges() @pushOperation redo: (editSession) -> editSession?.setSelectedBufferRanges(newSelectedRanges) @buffer.commit() abort: -> @buffer.abort() ### # Public # ### # Public: Folds all the rows. foldAll: -> @languageMode.foldAll() # Public: Unfolds all the rows. unfoldAll: -> @languageMode.unfoldAll() # Public: Folds the current row. foldCurrentRow: -> bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row @foldBufferRow(bufferRow) # Public: Given a buffer row, this folds it. # # bufferRow - A {Number} indicating the buffer row foldBufferRow: (bufferRow) -> @languageMode.foldBufferRow(bufferRow) # Public: Unfolds the current row. unfoldCurrentRow: -> bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row @unfoldBufferRow(bufferRow) # Public: Given a buffer row, this unfolds it. # # bufferRow - A {Number} indicating the buffer row unfoldBufferRow: (bufferRow) -> @languageMode.unfoldBufferRow(bufferRow) # Public: Folds all selections. foldSelection: -> selection.fold() for selection in @getSelections() # 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) -> @displayBuffer.createFold(startRow, endRow) # Public: Removes any {Fold}s found that contain the given buffer row. # # bufferRow - The buffer row {Number} to check against destroyFoldsContainingBufferRow: (bufferRow) -> @displayBuffer.destroyFoldsContainingBufferRow(bufferRow) # Public: Removes any {Fold}s found that intersect the given buffer row. # # bufferRow - The buffer row {Number} to check against destroyFoldsIntersectingBufferRange: (bufferRange) -> for row in [bufferRange.start.row..bufferRange.end.row] @destroyFoldsContainingBufferRow(row) # Public: Determines if the given row that the cursor is at is folded. # # Returns `true` if the row is folded, `false` otherwise. isFoldedAtCursorRow: -> @isFoldedAtScreenRow(@getCursorScreenRow()) # Public: Determines if the given buffer row is folded. # # bufferRow - A {Number} indicating the buffer row. # # Returns `true` if the buffer row is folded, `false` otherwise. isFoldedAtBufferRow: (bufferRow) -> screenRow = @screenPositionForBufferPosition([bufferRow]).row @isFoldedAtScreenRow(screenRow) # 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) -> @lineForScreenRow(screenRow)?.fold? # Public: Given a buffer row, this returns the largest fold that includes it. # # Largest is defined as the fold whose difference between its start and end points # are the greatest. # # bufferRow - A {Number} indicating the buffer row # # Returns a {Fold}. largestFoldContainingBufferRow: (bufferRow) -> @displayBuffer.largestFoldContainingBufferRow(bufferRow) # Public: Given a screen row, this returns the largest fold that starts there. # # Largest is defined as the fold whose difference between its start and end points # are the greatest. # # screenRow - A {Number} indicating the screen row # # Returns a {Fold}. largestFoldStartingAtScreenRow: (screenRow) -> @displayBuffer.largestFoldStartingAtScreenRow(screenRow) # Public: Given a buffer row, this returns a suggested indentation level. # # The indentation level provided is based on the current language. # # bufferRow - A {Number} indicating the buffer row # # Returns a {Number}. suggestedIndentForBufferRow: (bufferRow) -> @languageMode.suggestedIndentForBufferRow(bufferRow) # Public: Indents all the rows between two buffer rows. # # startRow - The row {Number} to start at # endRow - The row {Number} to end at autoIndentBufferRows: (startRow, endRow) -> @languageMode.autoIndentBufferRows(startRow, endRow) # Public: Given a buffer row, this indents it. # # bufferRow - The row {Number} autoIndentBufferRow: (bufferRow) -> @languageMode.autoIndentBufferRow(bufferRow) # Public: Given a buffer row, this increases the indentation. # # bufferRow - The row {Number} autoIncreaseIndentForBufferRow: (bufferRow) -> @languageMode.autoIncreaseIndentForBufferRow(bufferRow) # Public: Given a buffer row, this decreases the indentation. # # bufferRow - The row {Number} autoDecreaseIndentForRow: (bufferRow) -> @languageMode.autoDecreaseIndentForBufferRow(bufferRow) # Public: Wraps the lines between two rows in comments. # # If the language doesn't have comments, nothing happens. # # startRow - The row {Number} to start at # endRow - The row {Number} to end at # # Returns an {Array} of the commented {Ranges}. toggleLineCommentsForBufferRows: (start, end) -> @languageMode.toggleLineCommentsForBufferRows(start, end) # Public: Moves the selected line up one row. moveLineUp: -> selection = @getSelectedBufferRange() return if selection.start.row is 0 lastRow = @buffer.getLastRow() return if selection.isEmpty() and selection.start.row is lastRow and @buffer.getLastLine() is '' @transact => foldedRows = [] rows = [selection.start.row..selection.end.row] if selection.start.row isnt selection.end.row and selection.end.column is 0 rows.pop() unless @isFoldedAtBufferRow(selection.end.row) for row in rows screenRow = @screenPositionForBufferPosition([row]).row if @isFoldedAtScreenRow(screenRow) bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) startRow = bufferRange.start.row endRow = bufferRange.end.row - 1 foldedRows.push(endRow - 1) else startRow = row endRow = row endPosition = Point.min([endRow + 1], @buffer.getEofPosition()) lines = @buffer.getTextInRange([[startRow], endPosition]) if endPosition.row is lastRow and endPosition.column > 0 and not @buffer.lineEndingForRow(endPosition.row) lines = "#{lines}\n" @buffer.deleteRows(startRow, endRow) @buffer.insert([startRow - 1], lines) @foldBufferRow(foldedRow) for foldedRow in foldedRows @setSelectedBufferRange(selection.translate([-1]), preserveFolds: true) # Public: Moves the selected line down one row. moveLineDown: -> selection = @getSelectedBufferRange() lastRow = @buffer.getLastRow() return if selection.end.row is lastRow return if selection.end.row is lastRow - 1 and @buffer.getLastLine() is '' @transact => foldedRows = [] rows = [selection.end.row..selection.start.row] if selection.start.row isnt selection.end.row and selection.end.column is 0 rows.shift() unless @isFoldedAtBufferRow(selection.end.row) for row in rows screenRow = @screenPositionForBufferPosition([row]).row if @isFoldedAtScreenRow(screenRow) bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) startRow = bufferRange.start.row endRow = bufferRange.end.row - 1 foldedRows.push(endRow + 1) else startRow = row endRow = row if endRow + 1 is lastRow endPosition = [endRow, @buffer.lineLengthForRow(endRow)] else endPosition = [endRow + 1] lines = @buffer.getTextInRange([[startRow], endPosition]) @buffer.deleteRows(startRow, endRow) insertPosition = Point.min([startRow + 1], @buffer.getEofPosition()) if insertPosition.row is @buffer.getLastRow() and insertPosition.column > 0 lines = "\n#{lines}" @buffer.insert(insertPosition, lines) @foldBufferRow(foldedRow) for foldedRow in foldedRows @setSelectedBufferRange(selection.translate([1]), preserveFolds: true) # Public: Duplicates the current line. # # If more than one cursor is present, only the most recently added one is considered. duplicateLine: -> return unless @getSelection().isEmpty() @transact => cursorPosition = @getCursorBufferPosition() cursorRowFolded = @isFoldedAtCursorRow() if cursorRowFolded screenRow = @screenPositionForBufferPosition(cursorPosition).row bufferRange = @bufferRangeForScreenRange([[screenRow], [screenRow + 1]]) else bufferRange = new Range([cursorPosition.row], [cursorPosition.row + 1]) insertPosition = new Point(bufferRange.end.row) if insertPosition.row > @buffer.getLastRow() @unfoldCurrentRow() if cursorRowFolded @buffer.append("\n#{@getTextInBufferRange(bufferRange)}") @foldCurrentRow() if cursorRowFolded else @buffer.insert(insertPosition, @getTextInBufferRange(bufferRange)) @setCursorScreenPosition(@getCursorScreenPosition().translate([1])) @foldCurrentRow() if cursorRowFolded ### # Internal # ### mutateSelectedText: (fn) -> @transact => fn(selection) for selection in @getSelections() replaceSelectedText: (options={}, fn) -> {selectWordIfEmpty} = options @mutateSelectedText (selection) => range = selection.getBufferRange() if selectWordIfEmpty and selection.isEmpty() selection.selectWord() text = selection.getText() selection.delete() selection.insertText(fn(text)) selection.setBufferRange(range) pushOperation: (operation) -> @buffer.pushOperation(operation, this) ### # Public # ### # Returns a valid {DisplayBufferMarker} object for the given id if one exists. getMarker: (id) -> @displayBuffer.getMarker(id) # Public: Constructs a new marker at the given screen range. # # range - The marker {Range} (representing the distance between the head and tail) # options - Options to pass to the {BufferMarker} constructor # # Returns a {Number} representing the new marker's ID. markScreenRange: (args...) -> @displayBuffer.markScreenRange(args...) # Public: Constructs a new marker at the given buffer range. # # range - The marker {Range} (representing the distance between the head and tail) # options - Options to pass to the {BufferMarker} constructor # # Returns a {Number} representing the new marker's ID. markBufferRange: (args...) -> @displayBuffer.markBufferRange(args...) # Public: Constructs a new marker at the given screen position. # # range - The marker {Range} (representing the distance between the head and tail) # options - Options to pass to the {BufferMarker} constructor # # Returns a {Number} representing the new marker's ID. markScreenPosition: (args...) -> @displayBuffer.markScreenPosition(args...) # Public: Constructs a new marker at the given buffer position. # # range - The marker {Range} (representing the distance between the head and tail) # options - Options to pass to the {BufferMarker} constructor # # Returns a {Number} representing the new marker's ID. markBufferPosition: (args...) -> @displayBuffer.markBufferPosition(args...) # Public: Removes the marker with the given id. # # id - The {Number} of the ID to remove destroyMarker: (args...) -> @displayBuffer.destroyMarker(args...) # Public: Gets the number of markers in the buffer. # # Returns a {Number}. getMarkerCount: -> @buffer.getMarkerCount() # Public: Gets the screen range of the display marker. # # id - The {Number} of the ID to check # # Returns a {Range}. getMarkerScreenRange: (args...) -> @displayBuffer.getMarkerScreenRange(args...) # Public: Modifies the screen range of the display marker. # # id - The {Number} of the ID to change # screenRange - The new {Range} to use # options - A hash of options matching those found in {BufferMarker.setRange} setMarkerScreenRange: (args...) -> @displayBuffer.setMarkerScreenRange(args...) # Public: Returns `true` if there are multiple cursors in the edit session. # # Returns a {Boolean}. hasMultipleCursors: -> @getCursors().length > 1 # Public: Retrieves all the cursors. # # Returns an {Array} of {Cursor}s. getCursors: -> new Array(@cursors...) # Public: Retrieves a single cursor # # Returns a {Cursor}. getCursor: -> _.last(@cursors) # 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) -> marker = @markScreenPosition(screenPosition, invalidationStrategy: 'never') @addSelection(marker).cursor # 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) -> marker = @markBufferPosition(bufferPosition, invalidationStrategy: 'never') @addSelection(marker).cursor # Public: Adds a cursor to the `EditSession`. # # marker - The marker where the cursor should be added # # Returns the new {Cursor}. addCursor: (marker) -> cursor = new Cursor(editSession: this, marker: marker) @cursors.push(cursor) @trigger 'cursor-added', cursor cursor # Public: Removes a cursor from the `EditSession`. # # cursor - The cursor to remove # # Returns the removed {Cursor}. removeCursor: (cursor) -> _.remove(@cursors, cursor) # Public: Creates a new selection at the given marker. # # marker - The marker to highlight # options - A hash of options that pertain to the {Selection} constructor. # # Returns the new {Selection}. addSelection: (marker, options={}) -> unless options.preserveFolds @destroyFoldsIntersectingBufferRange(marker.getBufferRange()) cursor = @addCursor(marker) selection = new Selection(_.extend({editSession: this, marker, cursor}, options)) @selections.push(selection) selectionBufferRange = selection.getBufferRange() @mergeIntersectingSelections() unless options.suppressMerge if selection.destroyed for selection in @getSelections() if selection.intersectsBufferRange(selectionBufferRange) return selection else @trigger 'selection-added', selection selection # 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={}) -> options = _.defaults({invalidationStrategy: 'never'}, options) marker = @markBufferRange(bufferRange, options) @addSelection(marker, options) # 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) -> @setSelectedBufferRanges([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={}) -> throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length selections = @getSelections() selection.destroy() for selection in selections[bufferRanges.length...] for bufferRange, i in bufferRanges bufferRange = Range.fromObject(bufferRange) if selections[i] selections[i].setBufferRange(bufferRange, options) else @addSelectionForBufferRange(bufferRange, options) @mergeIntersectingSelections(options) # Public: Unselects a given selection. # # selection - The {Selection} to remove. removeSelection: (selection) -> _.remove(@selections, selection) # Public: Clears every selection. TODO clearSelections: -> @consolidateSelections() @getSelection().clear() consolidateSelections: -> selections = @getSelections() if selections.length > 1 selection.destroy() for selection in selections[0...-1] true else false # Public: Gets all the selections. # # Returns an {Array} of {Selection}s. getSelections: -> new Array(@selections...) # Public: Gets the selection at the specified index. # # index - The id {Number} of the selection # # Returns a {Selection}. getSelection: (index) -> index ?= @selections.length - 1 @selections[index] # Public: Gets the last selection, _i.e._ the most recently added. # # Returns a {Selection}. getLastSelection: -> _.last(@selections) # Public: Gets all selections, ordered by their position in the buffer. # # Returns an {Array} of {Selection}s. getSelectionsOrderedByBufferPosition: -> @getSelections().sort (a, b) -> aRange = a.getBufferRange() bRange = b.getBufferRange() aRange.end.compare(bRange.end) # Public: Gets the very last selection, as it's ordered in the buffer. # # Returns a {Selection}. getLastSelectionInBuffer: -> _.last(@getSelectionsOrderedByBufferPosition()) # Public: Determines if a given buffer range is included in a selection. # # bufferRange - The {Range} you're checking against # # Returns a {Boolean}. selectionIntersectsBufferRange: (bufferRange) -> _.any @getSelections(), (selection) -> selection.intersectsBufferRange(bufferRange) # Public: Moves every cursor to 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) -> @moveCursors (cursor) -> cursor.setScreenPosition(position, options) # Public: Gets the current screen position. # # Returns an {Array} of two numbers: the screen row, and the screen column. getCursorScreenPosition: -> @getCursor().getScreenPosition() # Public: Gets the current cursor's screen row. # # Returns the screen row. getCursorScreenRow: -> @getCursor().getScreenRow() # Public: Moves every cursor to 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) -> @moveCursors (cursor) -> cursor.setBufferPosition(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: -> @getCursor().getBufferPosition() # Public: Gets the screen range of the most recently added {Selection}. # # Returns a {Range}. getSelectedScreenRange: -> @getLastSelection().getScreenRange() # Public: Gets the buffer range of the most recently added {Selection}. # # Returns a {Range}. getSelectedBufferRange: -> @getLastSelection().getBufferRange() # Public: Gets the buffer ranges of all the {Selection}s. # # This is ordered by their buffer position. # # Returns an {Array} of {Range}s. getSelectedBufferRanges: -> selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition() # Public: Gets the currently selected text. # # Returns a {String}. getSelectedText: -> @getLastSelection().getText() # Public: Given a buffer range, this retrieves the text in that range. # # range - The {Range} you're interested in # # Returns a {String} of the combined lines. getTextInBufferRange: (range) -> @buffer.getTextInRange(range) # 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: -> @getCursor().getCurrentParagraphBufferRange() # Public: Gets the word located under the cursor. # # options - An object with properties based on {Cursor.getBeginningOfCurrentWordBufferPosition}. # # Returns a {String}. getWordUnderCursor: (options) -> @getTextInBufferRange(@getCursor().getCurrentWordBufferRange(options)) # Public: Moves every cursor up one row. moveCursorUp: (lineCount) -> @moveCursors (cursor) -> cursor.moveUp(lineCount) # Public: Moves every cursor down one row. moveCursorDown: (lineCount) -> @moveCursors (cursor) -> cursor.moveDown(lineCount) # Public: Moves every cursor left one column. moveCursorLeft: -> @moveCursors (cursor) -> cursor.moveLeft() # Public: Moves every cursor right one column. moveCursorRight: -> @moveCursors (cursor) -> cursor.moveRight() # Public: Moves every cursor to the top of the buffer. moveCursorToTop: -> @moveCursors (cursor) -> cursor.moveToTop() # Public: Moves every cursor to the bottom of the buffer. moveCursorToBottom: -> @moveCursors (cursor) -> cursor.moveToBottom() # Public: Moves every cursor to the beginning of the line. moveCursorToBeginningOfLine: -> @moveCursors (cursor) -> cursor.moveToBeginningOfLine() # Public: Moves every cursor to the first non-whitespace character of the line. moveCursorToFirstCharacterOfLine: -> @moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine() # Public: Moves every cursor to the end of the line. moveCursorToEndOfLine: -> @moveCursors (cursor) -> cursor.moveToEndOfLine() # Public: Moves every cursor to the beginning of the current word. moveCursorToBeginningOfWord: -> @moveCursors (cursor) -> cursor.moveToBeginningOfWord() # Public: Moves every cursor to the end of the current word. moveCursorToEndOfWord: -> @moveCursors (cursor) -> cursor.moveToEndOfWord() # Public: Moves every cursor to the beginning of the next word. moveCursorToBeginningOfNextWord: -> @moveCursors (cursor) -> cursor.moveToBeginningOfNextWord() # Internal: moveCursors: (fn) -> fn(cursor) for cursor in @getCursors() @mergeCursors() # Public: Selects the text from the current cursor position to a given screen position. # # position - An instance of {Point}, with a given `row` and `column`. selectToScreenPosition: (position) -> lastSelection = @getLastSelection() lastSelection.selectToScreenPosition(position) @mergeIntersectingSelections(reverse: lastSelection.isReversed()) # Public: Selects the text one position right of the cursor. selectRight: -> @expandSelectionsForward (selection) => selection.selectRight() # Public: Selects the text one position left of the cursor. selectLeft: -> @expandSelectionsBackward (selection) => selection.selectLeft() # Public: Selects all the text one position above the cursor. selectUp: -> @expandSelectionsBackward (selection) => selection.selectUp() # Public: Selects all the text one position below the cursor. selectDown: -> @expandSelectionsForward (selection) => selection.selectDown() # Public: Selects all the text from the current cursor position to the top of the buffer. selectToTop: -> @expandSelectionsBackward (selection) => selection.selectToTop() # Public: Selects all the text in the buffer. selectAll: -> @expandSelectionsForward (selection) => selection.selectAll() # Public: Selects all the text from the current cursor position to the bottom of the buffer. selectToBottom: -> @expandSelectionsForward (selection) => selection.selectToBottom() # Public: Selects all the text from the current cursor position to the beginning of the line. selectToBeginningOfLine: -> @expandSelectionsBackward (selection) => selection.selectToBeginningOfLine() # Public: Selects all the text from the current cursor position to the end of the line. selectToEndOfLine: -> @expandSelectionsForward (selection) => selection.selectToEndOfLine() # Public: Selects the current line. selectLine: -> @expandSelectionsForward (selection) => selection.selectLine() # Public: Moves the current selection down one row. addSelectionBelow: -> @expandSelectionsForward (selection) => selection.addSelectionBelow() # Public: Moves the current selection up one row. addSelectionAbove: -> @expandSelectionsBackward (selection) => selection.addSelectionAbove() # 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: -> @mutateSelectedText (selection) => if selection.isEmpty() selection.selectRight() text = selection.getText() selection.delete() selection.cursor.moveLeft() selection.insertText text else selection.insertText selection.getText().split('').reverse().join('') # Public: Turns the current selection into upper case. upperCase: -> @replaceSelectedText selectWordIfEmpty:true, (text) => text.toUpperCase() # Public: Turns the current selection into lower case. lowerCase: -> @replaceSelectedText selectWordIfEmpty:true, (text) => text.toLowerCase() # 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: -> @mutateSelectedText (selection) -> selection.joinLine() expandLastSelectionOverLine: -> @getLastSelection().expandOverLine() # Public: Selects all the text from the current cursor position to the beginning of the word. selectToBeginningOfWord: -> @expandSelectionsBackward (selection) => selection.selectToBeginningOfWord() # Public: Selects all the text from the current cursor position to the end of the word. selectToEndOfWord: -> @expandSelectionsForward (selection) => selection.selectToEndOfWord() # Public: Selects all the text from the current cursor position to the beginning of the next word. selectToBeginningOfNextWord: -> @expandSelectionsForward (selection) => selection.selectToBeginningOfNextWord() # Public: Selects the current word. selectWord: -> @expandSelectionsForward (selection) => selection.selectWord() expandLastSelectionOverWord: -> @getLastSelection().expandOverWord() # Selects the range associated with the given marker if it is valid. # # Returns the selected {Range} or a falsy value if the marker is invalid. selectMarker: (marker) -> if marker.isValid() range = marker.getBufferRange() @setSelectedBufferRange(range) range # Public: Given a buffer position, this finds all markers that contain the position. # # bufferPosition - A {Point} to check # # Returns an {Array} of {Numbers}, representing marker IDs containing `bufferPosition`. markersForBufferPosition: (bufferPosition) -> @buffer.markersForPosition(bufferPosition) mergeCursors: -> positions = [] for cursor in @getCursors() position = cursor.getBufferPosition().toString() if position in positions cursor.destroy() else positions.push(position) expandSelectionsForward: (fn) -> fn(selection) for selection in @getSelections() @mergeIntersectingSelections() expandSelectionsBackward: (fn) -> fn(selection) for selection in @getSelections() @mergeIntersectingSelections(reverse: true) finalizeSelections: -> selection.finalize() for selection in @getSelections() mergeIntersectingSelections: (options) -> for selection in @getSelections() otherSelections = @getSelections() _.remove(otherSelections, selection) for otherSelection in otherSelections if selection.intersectsWith(otherSelection) selection.merge(otherSelection, options) @mergeIntersectingSelections(options) return # Internal: inspect: -> JSON.stringify @serialize() preserveCursorPositionOnBufferReload: -> cursorPosition = null @subscribe @buffer, "will-reload", => cursorPosition = @getCursorBufferPosition() @subscribe @buffer, "reloaded", => @setCursorBufferPosition(cursorPosition) if cursorPosition cursorPosition = null # Public: Retrieves the current {EditSession}'s grammar. # # Returns a {String} indicating the language's grammar rules. getGrammar: -> @displayBuffer.getGrammar() # Public: Sets the current {EditSession}'s grammar. # # grammar - A {String} indicating the language's grammar rules. setGrammar: (grammar) -> @displayBuffer.setGrammar(grammar) # Public: Reloads the current grammar. reloadGrammar: -> @displayBuffer.reloadGrammar() # Internal: handleGrammarChange: -> @unfoldAll() @trigger 'grammar-changed' # Internal: getDebugSnapshot: -> [ @displayBuffer.getDebugSnapshot() @displayBuffer.tokenizedBuffer.getDebugSnapshot() ].join('\n\n') _.extend(EditSession.prototype, EventEmitter) _.extend(EditSession.prototype, Subscriber)