Files
atom/src/selection.js
2018-04-05 19:55:05 -04:00

1101 lines
40 KiB
JavaScript

const {Point, Range} = require('text-buffer')
const {pick} = require('underscore-plus')
const {Emitter} = require('event-kit')
const NonWhitespaceRegExp = /\S/
let nextId = 0
// Extended: Represents a selection in the {TextEditor}.
module.exports =
class Selection {
constructor ({cursor, marker, editor, id}) {
this.id = (id != null) ? id : nextId++
this.cursor = cursor
this.marker = marker
this.editor = editor
this.emitter = new Emitter()
this.initialScreenRange = null
this.wordwise = false
this.cursor.selection = this
this.decoration = this.editor.decorateMarker(this.marker, {type: 'highlight', class: 'selection'})
this.marker.onDidChange(e => this.markerDidChange(e))
this.marker.onDidDestroy(() => this.markerDidDestroy())
}
destroy () {
this.marker.destroy()
}
isLastSelection () {
return this === this.editor.getLastSelection()
}
/*
Section: Event Subscription
*/
// Extended: Calls your `callback` when the selection was moved.
//
// * `callback` {Function}
// * `event` {Object}
// * `oldBufferRange` {Range}
// * `oldScreenRange` {Range}
// * `newBufferRange` {Range}
// * `newScreenRange` {Range}
// * `selection` {Selection} that triggered the event
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeRange (callback) {
return this.emitter.on('did-change-range', callback)
}
// Extended: Calls your `callback` when the selection was destroyed
//
// * `callback` {Function}
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidDestroy (callback) {
return this.emitter.once('did-destroy', callback)
}
/*
Section: Managing the selection range
*/
// Public: Returns the screen {Range} for the selection.
getScreenRange () {
return this.marker.getScreenRange()
}
// Public: Modifies the screen range for the selection.
//
// * `screenRange` The new {Range} to use.
// * `options` (optional) {Object} options matching those found in {::setBufferRange}.
setScreenRange (screenRange, options) {
return this.setBufferRange(this.editor.bufferRangeForScreenRange(screenRange), options)
}
// Public: Returns the buffer {Range} for the selection.
getBufferRange () {
return this.marker.getBufferRange()
}
// Public: Modifies the buffer {Range} for the selection.
//
// * `bufferRange` The new {Range} to select.
// * `options` (optional) {Object} with the keys:
// * `preserveFolds` if `true`, the fold settings are preserved after the
// selection moves.
// * `autoscroll` {Boolean} indicating whether to autoscroll to the new
// range. Defaults to `true` if this is the most recently added selection,
// `false` otherwise.
setBufferRange (bufferRange, options = {}) {
bufferRange = Range.fromObject(bufferRange)
if (options.reversed == null) options.reversed = this.isReversed()
if (!options.preserveFolds) this.editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true)
this.modifySelection(() => {
const needsFlash = options.flash
options.flash = null
this.marker.setBufferRange(bufferRange, options)
const autoscroll = options.autoscroll != null ? options.autoscroll : this.isLastSelection()
if (autoscroll) this.autoscroll()
if (needsFlash) this.decoration.flash('flash', this.editor.selectionFlashDuration)
})
}
// Public: Returns the starting and ending buffer rows the selection is
// highlighting.
//
// Returns an {Array} of two {Number}s: the starting row, and the ending row.
getBufferRowRange () {
const range = this.getBufferRange()
const start = range.start.row
let end = range.end.row
if (range.end.column === 0) end = Math.max(start, end - 1)
return [start, end]
}
getTailScreenPosition () {
return this.marker.getTailScreenPosition()
}
getTailBufferPosition () {
return this.marker.getTailBufferPosition()
}
getHeadScreenPosition () {
return this.marker.getHeadScreenPosition()
}
getHeadBufferPosition () {
return this.marker.getHeadBufferPosition()
}
/*
Section: Info about the selection
*/
// Public: Determines if the selection contains anything.
isEmpty () {
return this.getBufferRange().isEmpty()
}
// Public: Determines if the ending position of a marker is greater than the
// starting position.
//
// This can happen when, for example, you highlight text "up" in a {TextBuffer}.
isReversed () {
return this.marker.isReversed()
}
// Public: Returns whether the selection is a single line or not.
isSingleScreenLine () {
return this.getScreenRange().isSingleLine()
}
// Public: Returns the text in the selection.
getText () {
return this.editor.buffer.getTextInRange(this.getBufferRange())
}
// Public: Identifies if a selection intersects with a given buffer range.
//
// * `bufferRange` A {Range} to check against.
//
// Returns a {Boolean}
intersectsBufferRange (bufferRange) {
return this.getBufferRange().intersectsWith(bufferRange)
}
intersectsScreenRowRange (startRow, endRow) {
return this.getScreenRange().intersectsRowRange(startRow, endRow)
}
intersectsScreenRow (screenRow) {
return this.getScreenRange().intersectsRow(screenRow)
}
// Public: Identifies if a selection intersects with another selection.
//
// * `otherSelection` A {Selection} to check against.
//
// Returns a {Boolean}
intersectsWith (otherSelection, exclusive) {
return this.getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive)
}
/*
Section: Modifying the selected range
*/
// Public: Clears the selection, moving the marker to the head.
//
// * `options` (optional) {Object} with the following keys:
// * `autoscroll` {Boolean} indicating whether to autoscroll to the new
// range. Defaults to `true` if this is the most recently added selection,
// `false` otherwise.
clear (options) {
this.goalScreenRange = null
if (!this.retainSelection) this.marker.clearTail()
const autoscroll = options && options.autoscroll != null
? options.autoscroll
: this.isLastSelection()
if (autoscroll) this.autoscroll()
this.finalize()
}
// 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, options) {
position = Point.fromObject(position)
this.modifySelection(() => {
if (this.initialScreenRange) {
if (position.isLessThan(this.initialScreenRange.start)) {
this.marker.setScreenRange([position, this.initialScreenRange.end], {reversed: true})
} else {
this.marker.setScreenRange([this.initialScreenRange.start, position], {reversed: false})
}
} else {
this.cursor.setScreenPosition(position, options)
}
if (this.linewise) {
this.expandOverLine(options)
} else if (this.wordwise) {
this.expandOverWord(options)
}
})
}
// Public: Selects the text from the current cursor position to a given buffer
// position.
//
// * `position` An instance of {Point}, with a given `row` and `column`.
selectToBufferPosition (position) {
this.modifySelection(() => this.cursor.setBufferPosition(position))
}
// Public: Selects the text one position right of the cursor.
//
// * `columnCount` (optional) {Number} number of columns to select (default: 1)
selectRight (columnCount) {
this.modifySelection(() => this.cursor.moveRight(columnCount))
}
// Public: Selects the text one position left of the cursor.
//
// * `columnCount` (optional) {Number} number of columns to select (default: 1)
selectLeft (columnCount) {
this.modifySelection(() => this.cursor.moveLeft(columnCount))
}
// Public: Selects all the text one position above the cursor.
//
// * `rowCount` (optional) {Number} number of rows to select (default: 1)
selectUp (rowCount) {
this.modifySelection(() => this.cursor.moveUp(rowCount))
}
// Public: Selects all the text one position below the cursor.
//
// * `rowCount` (optional) {Number} number of rows to select (default: 1)
selectDown (rowCount) {
this.modifySelection(() => this.cursor.moveDown(rowCount))
}
// Public: Selects all the text from the current cursor position to the top of
// the buffer.
selectToTop () {
this.modifySelection(() => this.cursor.moveToTop())
}
// Public: Selects all the text from the current cursor position to the bottom
// of the buffer.
selectToBottom () {
this.modifySelection(() => this.cursor.moveToBottom())
}
// Public: Selects all the text in the buffer.
selectAll () {
this.setBufferRange(this.editor.buffer.getRange(), {autoscroll: false})
}
// Public: Selects all the text from the current cursor position to the
// beginning of the line.
selectToBeginningOfLine () {
this.modifySelection(() => this.cursor.moveToBeginningOfLine())
}
// Public: Selects all the text from the current cursor position to the first
// character of the line.
selectToFirstCharacterOfLine () {
this.modifySelection(() => this.cursor.moveToFirstCharacterOfLine())
}
// Public: Selects all the text from the current cursor position to the end of
// the screen line.
selectToEndOfLine () {
this.modifySelection(() => this.cursor.moveToEndOfScreenLine())
}
// Public: Selects all the text from the current cursor position to the end of
// the buffer line.
selectToEndOfBufferLine () {
this.modifySelection(() => this.cursor.moveToEndOfLine())
}
// Public: Selects all the text from the current cursor position to the
// beginning of the word.
selectToBeginningOfWord () {
this.modifySelection(() => this.cursor.moveToBeginningOfWord())
}
// Public: Selects all the text from the current cursor position to the end of
// the word.
selectToEndOfWord () {
this.modifySelection(() => this.cursor.moveToEndOfWord())
}
// Public: Selects all the text from the current cursor position to the
// beginning of the next word.
selectToBeginningOfNextWord () {
this.modifySelection(() => this.cursor.moveToBeginningOfNextWord())
}
// Public: Selects text to the previous word boundary.
selectToPreviousWordBoundary () {
this.modifySelection(() => this.cursor.moveToPreviousWordBoundary())
}
// Public: Selects text to the next word boundary.
selectToNextWordBoundary () {
this.modifySelection(() => this.cursor.moveToNextWordBoundary())
}
// Public: Selects text to the previous subword boundary.
selectToPreviousSubwordBoundary () {
this.modifySelection(() => this.cursor.moveToPreviousSubwordBoundary())
}
// Public: Selects text to the next subword boundary.
selectToNextSubwordBoundary () {
this.modifySelection(() => this.cursor.moveToNextSubwordBoundary())
}
// Public: Selects all the text from the current cursor position to the
// beginning of the next paragraph.
selectToBeginningOfNextParagraph () {
this.modifySelection(() => this.cursor.moveToBeginningOfNextParagraph())
}
// Public: Selects all the text from the current cursor position to the
// beginning of the previous paragraph.
selectToBeginningOfPreviousParagraph () {
this.modifySelection(() => this.cursor.moveToBeginningOfPreviousParagraph())
}
// Public: Modifies the selection to encompass the current word.
//
// Returns a {Range}.
selectWord (options = {}) {
if (this.cursor.isSurroundedByWhitespace()) options.wordRegex = /[\t ]*/
if (this.cursor.isBetweenWordAndNonWord()) {
options.includeNonWordCharacters = false
}
this.setBufferRange(this.cursor.getCurrentWordBufferRange(options), options)
this.wordwise = true
this.initialScreenRange = this.getScreenRange()
}
// Public: Expands the newest selection to include the entire word on which
// the cursors rests.
expandOverWord (options) {
this.setBufferRange(this.getBufferRange().union(this.cursor.getCurrentWordBufferRange()), {autoscroll: false})
const autoscroll = options && options.autoscroll != null ? options.autoscroll : this.isLastSelection()
if (autoscroll) this.cursor.autoscroll()
}
// Public: Selects an entire line in the buffer.
//
// * `row` The line {Number} to select (default: the row of the cursor).
selectLine (row, options) {
if (row != null) {
this.setBufferRange(this.editor.bufferRangeForBufferRow(row, {includeNewline: true}), options)
} else {
const startRange = this.editor.bufferRangeForBufferRow(this.marker.getStartBufferPosition().row)
const endRange = this.editor.bufferRangeForBufferRow(this.marker.getEndBufferPosition().row, {includeNewline: true})
this.setBufferRange(startRange.union(endRange), options)
}
this.linewise = true
this.wordwise = false
this.initialScreenRange = this.getScreenRange()
}
// Public: Expands the newest selection to include the entire line on which
// the cursor currently rests.
//
// It also includes the newline character.
expandOverLine (options) {
const range = this.getBufferRange().union(this.cursor.getCurrentLineBufferRange({includeNewline: true}))
this.setBufferRange(range, {autoscroll: false})
const autoscroll = options && options.autoscroll != null ? options.autoscroll : this.isLastSelection()
if (autoscroll) this.cursor.autoscroll()
}
// Private: Ensure that the {TextEditor} is not marked read-only before allowing a buffer modification to occur. if
// the editor is read-only, require an explicit opt-in option to proceed (`bypassReadOnly`) or throw an Error.
ensureWritable (methodName, opts) {
if (!opts.bypassReadOnly && this.editor.isReadOnly()) {
if (atom.inDevMode() || atom.inSpecMode()) {
const e = new Error('Attempt to mutate a read-only TextEditor through a Selection')
e.detail =
`Your package is attempting to call ${methodName} on a selection within an editor that has been marked ` +
' read-only. Pass {bypassReadOnly: true} to modify it anyway, or test editors with .isReadOnly() before ' +
' attempting modifications.'
throw e
}
return false
}
return true
}
/*
Section: Modifying the selected text
*/
// Public: Replaces text at the current selection.
//
// * `text` A {String} representing the text to add
// * `options` (optional) {Object} with keys:
// * `select` If `true`, selects the newly added text.
// * `autoIndent` If `true`, indents all inserted text appropriately.
// * `autoIndentNewline` If `true`, indent newline appropriately.
// * `autoDecreaseIndent` If `true`, decreases indent level appropriately
// (for example, when a closing bracket is inserted).
// * `preserveTrailingLineIndentation` By default, when pasting multiple
// lines, Atom attempts to preserve the relative indent level between the
// first line and trailing lines, even if the indent level of the first
// line has changed from the copied text. If this option is `true`, this
// behavior is suppressed.
// level between the first lines and the trailing lines.
// * `normalizeLineEndings` (optional) {Boolean} (default: true)
// * `undo` *Deprecated* If `skip`, skips the undo stack for this operation. This property is deprecated. Call groupLastChanges() on the {TextBuffer} afterward instead.
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
insertText (text, options = {}) {
if (!this.ensureWritable('insertText', options)) return
let desiredIndentLevel, indentAdjustment
const oldBufferRange = this.getBufferRange()
const wasReversed = this.isReversed()
this.clear(options)
let autoIndentFirstLine = false
const precedingText = this.editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start])
const remainingLines = text.split('\n')
const firstInsertedLine = remainingLines.shift()
if (options.indentBasis != null && !options.preserveTrailingLineIndentation) {
indentAdjustment = this.editor.indentLevelForLine(precedingText) - options.indentBasis
this.adjustIndent(remainingLines, indentAdjustment)
}
const textIsAutoIndentable = (text === '\n') || (text === '\r\n') || NonWhitespaceRegExp.test(text)
if (options.autoIndent && textIsAutoIndentable && !NonWhitespaceRegExp.test(precedingText) && (remainingLines.length > 0)) {
autoIndentFirstLine = true
const firstLine = precedingText + firstInsertedLine
const languageMode = this.editor.buffer.getLanguageMode()
desiredIndentLevel = (
languageMode.suggestedIndentForLineAtBufferRow &&
languageMode.suggestedIndentForLineAtBufferRow(
oldBufferRange.start.row,
firstLine,
this.editor.getTabLength()
)
)
if (desiredIndentLevel != null) {
indentAdjustment = desiredIndentLevel - this.editor.indentLevelForLine(firstLine)
this.adjustIndent(remainingLines, indentAdjustment)
}
}
text = firstInsertedLine
if (remainingLines.length > 0) text += `\n${remainingLines.join('\n')}`
const newBufferRange = this.editor.buffer.setTextInRange(oldBufferRange, text, pick(options, 'undo', 'normalizeLineEndings'))
if (options.select) {
this.setBufferRange(newBufferRange, {reversed: wasReversed})
} else {
if (wasReversed) this.cursor.setBufferPosition(newBufferRange.end)
}
if (autoIndentFirstLine) {
this.editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel)
}
if (options.autoIndentNewline && (text === '\n')) {
this.editor.autoIndentBufferRow(newBufferRange.end.row, {preserveLeadingWhitespace: true, skipBlankLines: false})
} else if (options.autoDecreaseIndent && NonWhitespaceRegExp.test(text)) {
this.editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row)
}
const autoscroll = options.autoscroll != null ? options.autoscroll : this.isLastSelection()
if (autoscroll) this.autoscroll()
return newBufferRange
}
// Public: Removes the first character before the selection if the selection
// is empty otherwise it deletes the selection.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
backspace (options = {}) {
if (!this.ensureWritable('backspace', options)) return
if (this.isEmpty()) this.selectLeft()
this.deleteSelectedText(options)
}
// Public: Removes the selection or, if nothing is selected, then all
// characters from the start of the selection back to the previous word
// boundary.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToPreviousWordBoundary (options = {}) {
if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return
if (this.isEmpty()) this.selectToPreviousWordBoundary()
this.deleteSelectedText(options)
}
// Public: Removes the selection or, if nothing is selected, then all
// characters from the start of the selection up to the next word
// boundary.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToNextWordBoundary (options = {}) {
if (!this.ensureWritable('deleteToNextWordBoundary', options)) return
if (this.isEmpty()) this.selectToNextWordBoundary()
this.deleteSelectedText(options)
}
// Public: Removes from the start of the selection to the beginning of the
// current word if the selection is empty otherwise it deletes the selection.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToBeginningOfWord (options = {}) {
if (!this.ensureWritable('deleteToBeginningOfWord', options)) return
if (this.isEmpty()) this.selectToBeginningOfWord()
this.deleteSelectedText(options)
}
// Public: Removes from the beginning of the line which the selection begins on
// all the way through to the end of the selection.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToBeginningOfLine (options = {}) {
if (!this.ensureWritable('deleteToBeginningOfLine', options)) return
if (this.isEmpty() && this.cursor.isAtBeginningOfLine()) {
this.selectLeft()
} else {
this.selectToBeginningOfLine()
}
this.deleteSelectedText(options)
}
// Public: Removes the selection or the next character after the start of the
// selection if the selection is empty.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
delete (options = {}) {
if (!this.ensureWritable('delete', options)) return
if (this.isEmpty()) this.selectRight()
this.deleteSelectedText(options)
}
// Public: If the selection is empty, removes all text from the cursor to the
// end of the line. If the cursor is already at the end of the line, it
// removes the following newline. If the selection isn't empty, only deletes
// the contents of the selection.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToEndOfLine (options = {}) {
if (!this.ensureWritable('deleteToEndOfLine', options)) return
if (this.isEmpty()) {
if (this.cursor.isAtEndOfLine()) {
this.delete(options)
return
}
this.selectToEndOfLine()
}
this.deleteSelectedText(options)
}
// Public: Removes the selection or all characters from the start of the
// selection to the end of the current word if nothing is selected.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToEndOfWord (options = {}) {
if (!this.ensureWritable('deleteToEndOfWord', options)) return
if (this.isEmpty()) this.selectToEndOfWord()
this.deleteSelectedText(options)
}
// Public: Removes the selection or all characters from the start of the
// selection to the end of the current word if nothing is selected.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToBeginningOfSubword (options = {}) {
if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return
if (this.isEmpty()) this.selectToPreviousSubwordBoundary()
this.deleteSelectedText(options)
}
// Public: Removes the selection or all characters from the start of the
// selection to the end of the current word if nothing is selected.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToEndOfSubword (options = {}) {
if (!this.ensureWritable('deleteToEndOfSubword', options)) return
if (this.isEmpty()) this.selectToNextSubwordBoundary()
this.deleteSelectedText(options)
}
// Public: Removes only the selected text.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteSelectedText (options = {}) {
if (!this.ensureWritable('deleteSelectedText', options)) return
const bufferRange = this.getBufferRange()
if (!bufferRange.isEmpty()) this.editor.buffer.delete(bufferRange)
if (this.cursor) this.cursor.setBufferPosition(bufferRange.start)
}
// Public: Removes the line at the beginning of the selection if the selection
// is empty unless the selection spans multiple lines in which case all lines
// are removed.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteLine (options = {}) {
if (!this.ensureWritable('deleteLine', options)) return
const range = this.getBufferRange()
if (range.isEmpty()) {
const start = this.cursor.getScreenRow()
const range = this.editor.bufferRowsForScreenRows(start, start + 1)
if (range[1] > range[0]) {
this.editor.buffer.deleteRows(range[0], range[1] - 1)
} else {
this.editor.buffer.deleteRow(range[0])
}
} else {
const start = range.start.row
let end = range.end.row
if (end !== this.editor.buffer.getLastRow() && range.end.column === 0) end--
this.editor.buffer.deleteRows(start, end)
}
this.cursor.setBufferPosition({row: this.cursor.getBufferRow(), column: range.start.column})
}
// Public: Joins the current line with the one below it. Lines will
// be separated by a single space.
//
// If there selection spans more than one line, all the lines are joined together.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
joinLines (options = {}) {
if (!this.ensureWritable('joinLines', options)) return
let joinMarker
const selectedRange = this.getBufferRange()
if (selectedRange.isEmpty()) {
if (selectedRange.start.row === this.editor.buffer.getLastRow()) return
} else {
joinMarker = this.editor.markBufferRange(selectedRange, {invalidate: 'never'})
}
const rowCount = Math.max(1, selectedRange.getRowCount() - 1)
for (let i = 0; i < rowCount; i++) {
this.cursor.setBufferPosition([selectedRange.start.row])
this.cursor.moveToEndOfLine()
// Remove trailing whitespace from the current line
const scanRange = this.cursor.getCurrentLineBufferRange()
let trailingWhitespaceRange = null
this.editor.scanInBufferRange(/[ \t]+$/, scanRange, ({range}) => {
trailingWhitespaceRange = range
})
if (trailingWhitespaceRange) {
this.setBufferRange(trailingWhitespaceRange)
this.deleteSelectedText(options)
}
const currentRow = selectedRange.start.row
const nextRow = currentRow + 1
const insertSpace =
(nextRow <= this.editor.buffer.getLastRow()) &&
(this.editor.buffer.lineLengthForRow(nextRow) > 0) &&
(this.editor.buffer.lineLengthForRow(currentRow) > 0)
if (insertSpace) this.insertText(' ', options)
this.cursor.moveToEndOfLine()
// Remove leading whitespace from the line below
this.modifySelection(() => {
this.cursor.moveRight()
this.cursor.moveToFirstCharacterOfLine()
})
this.deleteSelectedText(options)
if (insertSpace) this.cursor.moveLeft()
}
if (joinMarker) {
const newSelectedRange = joinMarker.getBufferRange()
this.setBufferRange(newSelectedRange)
joinMarker.destroy()
}
}
// Public: Removes one level of indent from the currently selected rows.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
outdentSelectedRows (options = {}) {
if (!this.ensureWritable('outdentSelectedRows', options)) return
const [start, end] = this.getBufferRowRange()
const {buffer} = this.editor
const leadingTabRegex = new RegExp(`^( {1,${this.editor.getTabLength()}}|\t)`)
for (let row = start; row <= end; row++) {
const match = buffer.lineForRow(row).match(leadingTabRegex)
if (match && match[0].length > 0) {
buffer.delete([[row, 0], [row, match[0].length]])
}
}
}
// Public: Sets the indentation level of all selected rows to values suggested
// by the relevant grammars.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
autoIndentSelectedRows (options = {}) {
if (!this.ensureWritable('autoIndentSelectedRows', options)) return
const [start, end] = this.getBufferRowRange()
return this.editor.autoIndentBufferRows(start, end)
}
// Public: Wraps the selected lines in comments if they aren't currently part
// of a comment.
//
// Removes the comment if they are currently wrapped in a comment.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
toggleLineComments (options = {}) {
if (!this.ensureWritable('toggleLineComments', options)) return
this.editor.toggleLineCommentsForBufferRows(...(this.getBufferRowRange() || []))
}
// Public: Cuts the selection until the end of the screen line.
//
// * `maintainClipboard` {Boolean}
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
cutToEndOfLine (maintainClipboard, options = {}) {
if (!this.ensureWritable('cutToEndOfLine', options)) return
if (this.isEmpty()) this.selectToEndOfLine()
return this.cut(maintainClipboard, false, options.bypassReadOnly)
}
// Public: Cuts the selection until the end of the buffer line.
//
// * `maintainClipboard` {Boolean}
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
cutToEndOfBufferLine (maintainClipboard, options = {}) {
if (!this.ensureWritable('cutToEndOfBufferLine', options)) return
if (this.isEmpty()) this.selectToEndOfBufferLine()
this.cut(maintainClipboard, false, options.bypassReadOnly)
}
// Public: Copies the selection to the clipboard and then deletes it.
//
// * `maintainClipboard` {Boolean} (default: false) See {::copy}
// * `fullLine` {Boolean} (default: false) See {::copy}
// * `bypassReadOnly` {Boolean} (default: false) Must be `true` to modify text within a read-only editor.
cut (maintainClipboard = false, fullLine = false, bypassReadOnly = false) {
if (!this.ensureWritable('cut', {bypassReadOnly})) return
this.copy(maintainClipboard, fullLine)
this.delete({bypassReadOnly})
}
// Public: Copies the current selection to the clipboard.
//
// * `maintainClipboard` {Boolean} if `true`, a specific metadata property
// is created to store each content copied to the clipboard. The clipboard
// `text` still contains the concatenation of the clipboard with the
// current selection. (default: false)
// * `fullLine` {Boolean} if `true`, the copied text will always be pasted
// at the beginning of the line containing the cursor, regardless of the
// cursor's horizontal position. (default: false)
copy (maintainClipboard = false, fullLine = false) {
if (this.isEmpty()) return
const {start, end} = this.getBufferRange()
const selectionText = this.editor.getTextInRange([start, end])
const precedingText = this.editor.getTextInRange([[start.row, 0], start])
const startLevel = this.editor.indentLevelForLine(precedingText)
if (maintainClipboard) {
let {text: clipboardText, metadata} = this.editor.constructor.clipboard.readWithMetadata()
if (!metadata) metadata = {}
if (!metadata.selections) {
metadata.selections = [{
text: clipboardText,
indentBasis: metadata.indentBasis,
fullLine: metadata.fullLine
}]
}
metadata.selections.push({
text: selectionText,
indentBasis: startLevel,
fullLine
})
this.editor.constructor.clipboard.write([clipboardText, selectionText].join('\n'), metadata)
} else {
this.editor.constructor.clipboard.write(selectionText, {
indentBasis: startLevel,
fullLine
})
}
}
// Public: Creates a fold containing the current selection.
fold () {
const range = this.getBufferRange()
if (!range.isEmpty()) {
this.editor.foldBufferRange(range)
this.cursor.setBufferPosition(range.end)
}
}
// Private: Increase the indentation level of the given text by given number
// of levels. Leaves the first line unchanged.
adjustIndent (lines, indentAdjustment) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
if (indentAdjustment === 0 || line === '') {
continue
} else if (indentAdjustment > 0) {
lines[i] = this.editor.buildIndentString(indentAdjustment) + line
} else {
const currentIndentLevel = this.editor.indentLevelForLine(lines[i])
const indentLevel = Math.max(0, currentIndentLevel + indentAdjustment)
lines[i] = line.replace(/^[\t ]+/, this.editor.buildIndentString(indentLevel))
}
}
}
// Indent the current line(s).
//
// If the selection is empty, indents the current line if the cursor precedes
// non-whitespace characters, and otherwise inserts a tab. If the selection is
// non empty, calls {::indentSelectedRows}.
//
// * `options` (optional) {Object} with the keys:
// * `autoIndent` If `true`, the line is indented to an automatically-inferred
// level. Otherwise, {TextEditor::getTabText} is inserted.
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
indent ({autoIndent, bypassReadOnly} = {}) {
if (!this.ensureWritable('indent', {bypassReadOnly})) return
const {row} = this.cursor.getBufferPosition()
if (this.isEmpty()) {
this.cursor.skipLeadingWhitespace()
const desiredIndent = this.editor.suggestedIndentForBufferRow(row)
let delta = desiredIndent - this.cursor.getIndentLevel()
if (autoIndent && delta > 0) {
if (!this.editor.getSoftTabs()) delta = Math.max(delta, 1)
this.insertText(this.editor.buildIndentString(delta), {bypassReadOnly})
} else {
this.insertText(this.editor.buildIndentString(1, this.cursor.getBufferColumn()), {bypassReadOnly})
}
} else {
this.indentSelectedRows({bypassReadOnly})
}
}
// Public: If the selection spans multiple rows, indent all of them.
//
// * `options` (optional) {Object} with the keys:
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
indentSelectedRows (options = {}) {
if (!this.ensureWritable('indentSelectedRows', options)) return
const [start, end] = this.getBufferRowRange()
for (let row = start; row <= end; row++) {
if (this.editor.buffer.lineLengthForRow(row) !== 0) {
this.editor.buffer.insert([row, 0], this.editor.getTabText())
}
}
}
/*
Section: Managing multiple selections
*/
// Public: Moves the selection down one row.
addSelectionBelow () {
const range = this.getGoalScreenRange().copy()
const nextRow = range.end.row + 1
for (let row = nextRow, end = this.editor.getLastScreenRow(); row <= end; row++) {
range.start.row = row
range.end.row = row
const clippedRange = this.editor.clipScreenRange(range, {skipSoftWrapIndentation: true})
if (range.isEmpty()) {
if (range.end.column > 0 && clippedRange.end.column === 0) continue
} else {
if (clippedRange.isEmpty()) continue
}
const containingSelections = this.editor.selectionsMarkerLayer.findMarkers({containsScreenRange: clippedRange})
if (containingSelections.length === 0) {
const selection = this.editor.addSelectionForScreenRange(clippedRange)
selection.setGoalScreenRange(range)
}
break
}
}
// Public: Moves the selection up one row.
addSelectionAbove () {
const range = this.getGoalScreenRange().copy()
const previousRow = range.end.row - 1
for (let row = previousRow; row >= 0; row--) {
range.start.row = row
range.end.row = row
const clippedRange = this.editor.clipScreenRange(range, {skipSoftWrapIndentation: true})
if (range.isEmpty()) {
if (range.end.column > 0 && clippedRange.end.column === 0) continue
} else {
if (clippedRange.isEmpty()) continue
}
const containingSelections = this.editor.selectionsMarkerLayer.findMarkers({containsScreenRange: clippedRange})
if (containingSelections.length === 0) {
const selection = this.editor.addSelectionForScreenRange(clippedRange)
selection.setGoalScreenRange(range)
}
break
}
}
// Public: Combines the given selection into this selection and then destroys
// the given selection.
//
// * `otherSelection` A {Selection} to merge with.
// * `options` (optional) {Object} options matching those found in {::setBufferRange}.
merge (otherSelection, options = {}) {
const myGoalScreenRange = this.getGoalScreenRange()
const otherGoalScreenRange = otherSelection.getGoalScreenRange()
if (myGoalScreenRange && otherGoalScreenRange) {
options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange)
} else {
options.goalScreenRange = myGoalScreenRange || otherGoalScreenRange
}
const bufferRange = this.getBufferRange().union(otherSelection.getBufferRange())
this.setBufferRange(bufferRange, Object.assign({autoscroll: false}, options))
otherSelection.destroy()
}
/*
Section: Comparing to other selections
*/
// Public: Compare this selection's buffer range to another selection's buffer
// range.
//
// See {Range::compare} for more details.
//
// * `otherSelection` A {Selection} to compare against
compare (otherSelection) {
return this.marker.compare(otherSelection.marker)
}
/*
Section: Private Utilities
*/
setGoalScreenRange (range) {
this.goalScreenRange = Range.fromObject(range)
}
getGoalScreenRange () {
return this.goalScreenRange || this.getScreenRange()
}
markerDidChange (e) {
const {oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition} = e
const {oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e
const {textChanged} = e
if (!oldHeadScreenPosition.isEqual(newHeadScreenPosition)) {
this.cursor.goalColumn = null
const cursorMovedEvent = {
oldBufferPosition: oldHeadBufferPosition,
oldScreenPosition: oldHeadScreenPosition,
newBufferPosition: newHeadBufferPosition,
newScreenPosition: newHeadScreenPosition,
textChanged,
cursor: this.cursor
}
this.cursor.emitter.emit('did-change-position', cursorMovedEvent)
this.editor.cursorMoved(cursorMovedEvent)
}
this.emitter.emit('did-change-range')
this.editor.selectionRangeChanged({
oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition),
oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition),
newBufferRange: this.getBufferRange(),
newScreenRange: this.getScreenRange(),
selection: this
})
}
markerDidDestroy () {
if (this.editor.isDestroyed()) return
this.destroyed = true
this.cursor.destroyed = true
this.editor.removeSelection(this)
this.cursor.emitter.emit('did-destroy')
this.emitter.emit('did-destroy')
this.cursor.emitter.dispose()
this.emitter.dispose()
}
finalize () {
if (!this.initialScreenRange || !this.initialScreenRange.isEqual(this.getScreenRange())) {
this.initialScreenRange = null
}
if (this.isEmpty()) {
this.wordwise = false
this.linewise = false
}
}
autoscroll (options) {
if (this.marker.hasTail()) {
this.editor.scrollToScreenRange(this.getScreenRange(), Object.assign({reversed: this.isReversed()}, options))
} else {
this.cursor.autoscroll(options)
}
}
clearAutoscroll () {}
modifySelection (fn) {
this.retainSelection = true
this.plantTail()
fn()
this.retainSelection = false
}
// Sets the marker's tail to the same position as the marker's head.
//
// This only works if there isn't already a tail position.
//
// Returns a {Point} representing the new tail position.
plantTail () {
this.marker.plantTail()
}
}