mirror of
https://github.com/atom/atom.git
synced 2026-01-23 22:08:08 -05:00
Merge pull request #16076 from atom/mb-convert-selection-to-js
Convert Selection from CoffeeScript to JavaScript
This commit is contained in:
@@ -1,128 +0,0 @@
|
||||
TextEditor = require '../src/text-editor'
|
||||
|
||||
describe "Selection", ->
|
||||
[buffer, editor, selection] = []
|
||||
|
||||
beforeEach ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
editor = new TextEditor({buffer: buffer, tabLength: 2})
|
||||
selection = editor.getLastSelection()
|
||||
|
||||
afterEach ->
|
||||
buffer.destroy()
|
||||
|
||||
describe ".deleteSelectedText()", ->
|
||||
describe "when nothing is selected", ->
|
||||
it "deletes nothing", ->
|
||||
selection.setBufferRange [[0, 3], [0, 3]]
|
||||
selection.deleteSelectedText()
|
||||
expect(buffer.lineForRow(0)).toBe "var quicksort = function () {"
|
||||
|
||||
describe "when one line is selected", ->
|
||||
it "deletes selected text and clears the selection", ->
|
||||
selection.setBufferRange [[0, 4], [0, 14]]
|
||||
selection.deleteSelectedText()
|
||||
expect(buffer.lineForRow(0)).toBe "var = function () {"
|
||||
|
||||
endOfLine = buffer.lineForRow(0).length
|
||||
selection.setBufferRange [[0, 0], [0, endOfLine]]
|
||||
selection.deleteSelectedText()
|
||||
expect(buffer.lineForRow(0)).toBe ""
|
||||
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
|
||||
describe "when multiple lines are selected", ->
|
||||
it "deletes selected text and clears the selection", ->
|
||||
selection.setBufferRange [[0, 1], [2, 39]]
|
||||
selection.deleteSelectedText()
|
||||
expect(buffer.lineForRow(0)).toBe "v;"
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
|
||||
describe "when the cursor precedes the tail", ->
|
||||
it "deletes selected text and clears the selection", ->
|
||||
selection.cursor.setScreenPosition [0, 13]
|
||||
selection.selectToScreenPosition [0, 4]
|
||||
|
||||
selection.delete()
|
||||
expect(buffer.lineForRow(0)).toBe "var = function () {"
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
|
||||
describe ".isReversed()", ->
|
||||
it "returns true if the cursor precedes the tail", ->
|
||||
selection.cursor.setScreenPosition([0, 20])
|
||||
selection.selectToScreenPosition([0, 10])
|
||||
expect(selection.isReversed()).toBeTruthy()
|
||||
|
||||
selection.selectToScreenPosition([0, 25])
|
||||
expect(selection.isReversed()).toBeFalsy()
|
||||
|
||||
describe ".selectLine(row)", ->
|
||||
describe "when passed a row", ->
|
||||
it "selects the specified row", ->
|
||||
selection.setBufferRange([[2, 4], [3, 4]])
|
||||
selection.selectLine(5)
|
||||
expect(selection.getBufferRange()).toEqual [[5, 0], [6, 0]]
|
||||
|
||||
describe "when not passed a row", ->
|
||||
it "selects all rows spanned by the selection", ->
|
||||
selection.setBufferRange([[2, 4], [3, 4]])
|
||||
selection.selectLine()
|
||||
expect(selection.getBufferRange()).toEqual [[2, 0], [4, 0]]
|
||||
|
||||
describe "when only the selection's tail is moved (regression)", ->
|
||||
it "notifies ::onDidChangeRange observers", ->
|
||||
selection.setBufferRange([[2, 0], [2, 10]], reversed: true)
|
||||
changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler')
|
||||
selection.onDidChangeRange changeScreenRangeHandler
|
||||
|
||||
buffer.insert([2, 5], 'abc')
|
||||
expect(changeScreenRangeHandler).toHaveBeenCalled()
|
||||
|
||||
describe "when the selection is destroyed", ->
|
||||
it "destroys its marker", ->
|
||||
selection.setBufferRange([[2, 0], [2, 10]])
|
||||
marker = selection.marker
|
||||
selection.destroy()
|
||||
expect(marker.isDestroyed()).toBeTruthy()
|
||||
|
||||
describe ".insertText(text, options)", ->
|
||||
it "allows pasting white space only lines when autoIndent is enabled", ->
|
||||
selection.setBufferRange [[0, 0], [0, 0]]
|
||||
selection.insertText(" \n \n\n", autoIndent: true)
|
||||
expect(buffer.lineForRow(0)).toBe " "
|
||||
expect(buffer.lineForRow(1)).toBe " "
|
||||
expect(buffer.lineForRow(2)).toBe ""
|
||||
|
||||
it "auto-indents if only a newline is inserted", ->
|
||||
selection.setBufferRange [[2, 0], [3, 0]]
|
||||
selection.insertText("\n", autoIndent: true)
|
||||
expect(buffer.lineForRow(2)).toBe " "
|
||||
|
||||
it "auto-indents if only a carriage return + newline is inserted", ->
|
||||
selection.setBufferRange [[2, 0], [3, 0]]
|
||||
selection.insertText("\r\n", autoIndent: true)
|
||||
expect(buffer.lineForRow(2)).toBe " "
|
||||
|
||||
it "does not adjust the indent of trailing lines if preserveTrailingLineIndentation is true", ->
|
||||
selection.setBufferRange [[5, 0], [5, 0]]
|
||||
selection.insertText(' foo\n bar\n', preserveTrailingLineIndentation: true, indentBasis: 1)
|
||||
expect(buffer.lineForRow(6)).toBe(' bar')
|
||||
|
||||
describe ".fold()", ->
|
||||
it "folds the buffer range spanned by the selection", ->
|
||||
selection.setBufferRange([[0, 3], [1, 6]])
|
||||
selection.fold()
|
||||
|
||||
expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]])
|
||||
expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]])
|
||||
expect(editor.lineTextForScreenRow(0)).toBe "var#{editor.displayLayer.foldCharacter}sort = function(items) {"
|
||||
expect(editor.isFoldedAtBufferRow(0)).toBe(true)
|
||||
|
||||
it "doesn't create a fold when the selection is empty", ->
|
||||
selection.setBufferRange([[0, 3], [0, 3]])
|
||||
selection.fold()
|
||||
|
||||
expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]])
|
||||
expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]])
|
||||
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
|
||||
expect(editor.isFoldedAtBufferRow(0)).toBe(false)
|
||||
157
spec/selection-spec.js
Normal file
157
spec/selection-spec.js
Normal file
@@ -0,0 +1,157 @@
|
||||
const TextEditor = require('../src/text-editor')
|
||||
|
||||
describe('Selection', () => {
|
||||
let buffer, editor, selection
|
||||
|
||||
beforeEach(() => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
editor = new TextEditor({buffer, tabLength: 2})
|
||||
selection = editor.getLastSelection()
|
||||
})
|
||||
|
||||
afterEach(() => buffer.destroy())
|
||||
|
||||
describe('.deleteSelectedText()', () => {
|
||||
describe('when nothing is selected', () => {
|
||||
it('deletes nothing', () => {
|
||||
selection.setBufferRange([[0, 3], [0, 3]])
|
||||
selection.deleteSelectedText()
|
||||
expect(buffer.lineForRow(0)).toBe('var quicksort = function () {')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when one line is selected', () => {
|
||||
it('deletes selected text and clears the selection', () => {
|
||||
selection.setBufferRange([[0, 4], [0, 14]])
|
||||
selection.deleteSelectedText()
|
||||
expect(buffer.lineForRow(0)).toBe('var = function () {')
|
||||
|
||||
const endOfLine = buffer.lineForRow(0).length
|
||||
selection.setBufferRange([[0, 0], [0, endOfLine]])
|
||||
selection.deleteSelectedText()
|
||||
expect(buffer.lineForRow(0)).toBe('')
|
||||
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when multiple lines are selected', () => {
|
||||
it('deletes selected text and clears the selection', () => {
|
||||
selection.setBufferRange([[0, 1], [2, 39]])
|
||||
selection.deleteSelectedText()
|
||||
expect(buffer.lineForRow(0)).toBe('v;')
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the cursor precedes the tail', () => {
|
||||
it('deletes selected text and clears the selection', () => {
|
||||
selection.cursor.setScreenPosition([0, 13])
|
||||
selection.selectToScreenPosition([0, 4])
|
||||
|
||||
selection.delete()
|
||||
expect(buffer.lineForRow(0)).toBe('var = function () {')
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.isReversed()', () => {
|
||||
it('returns true if the cursor precedes the tail', () => {
|
||||
selection.cursor.setScreenPosition([0, 20])
|
||||
selection.selectToScreenPosition([0, 10])
|
||||
expect(selection.isReversed()).toBeTruthy()
|
||||
|
||||
selection.selectToScreenPosition([0, 25])
|
||||
expect(selection.isReversed()).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('.selectLine(row)', () => {
|
||||
describe('when passed a row', () => {
|
||||
it('selects the specified row', () => {
|
||||
selection.setBufferRange([[2, 4], [3, 4]])
|
||||
selection.selectLine(5)
|
||||
expect(selection.getBufferRange()).toEqual([[5, 0], [6, 0]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when not passed a row', () => {
|
||||
it('selects all rows spanned by the selection', () => {
|
||||
selection.setBufferRange([[2, 4], [3, 4]])
|
||||
selection.selectLine()
|
||||
expect(selection.getBufferRange()).toEqual([[2, 0], [4, 0]])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("when only the selection's tail is moved (regression)", () => {
|
||||
it('notifies ::onDidChangeRange observers', () => {
|
||||
selection.setBufferRange([[2, 0], [2, 10]], {reversed: true})
|
||||
const changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler')
|
||||
selection.onDidChangeRange(changeScreenRangeHandler)
|
||||
|
||||
buffer.insert([2, 5], 'abc')
|
||||
expect(changeScreenRangeHandler).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the selection is destroyed', () => {
|
||||
it('destroys its marker', () => {
|
||||
selection.setBufferRange([[2, 0], [2, 10]])
|
||||
const { marker } = selection
|
||||
selection.destroy()
|
||||
expect(marker.isDestroyed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('.insertText(text, options)', () => {
|
||||
it('allows pasting white space only lines when autoIndent is enabled', () => {
|
||||
selection.setBufferRange([[0, 0], [0, 0]])
|
||||
selection.insertText(' \n \n\n', {autoIndent: true})
|
||||
expect(buffer.lineForRow(0)).toBe(' ')
|
||||
expect(buffer.lineForRow(1)).toBe(' ')
|
||||
expect(buffer.lineForRow(2)).toBe('')
|
||||
})
|
||||
|
||||
it('auto-indents if only a newline is inserted', () => {
|
||||
selection.setBufferRange([[2, 0], [3, 0]])
|
||||
selection.insertText('\n', {autoIndent: true})
|
||||
expect(buffer.lineForRow(2)).toBe(' ')
|
||||
})
|
||||
|
||||
it('auto-indents if only a carriage return + newline is inserted', () => {
|
||||
selection.setBufferRange([[2, 0], [3, 0]])
|
||||
selection.insertText('\r\n', {autoIndent: true})
|
||||
expect(buffer.lineForRow(2)).toBe(' ')
|
||||
})
|
||||
|
||||
it('does not adjust the indent of trailing lines if preserveTrailingLineIndentation is true', () => {
|
||||
selection.setBufferRange([[5, 0], [5, 0]])
|
||||
selection.insertText(' foo\n bar\n', {preserveTrailingLineIndentation: true, indentBasis: 1})
|
||||
expect(buffer.lineForRow(6)).toBe(' bar')
|
||||
})
|
||||
})
|
||||
|
||||
describe('.fold()', () => {
|
||||
it('folds the buffer range spanned by the selection', () => {
|
||||
selection.setBufferRange([[0, 3], [1, 6]])
|
||||
selection.fold()
|
||||
|
||||
expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]])
|
||||
expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]])
|
||||
expect(editor.lineTextForScreenRow(0)).toBe(`var${editor.displayLayer.foldCharacter}sort = function(items) {`)
|
||||
expect(editor.isFoldedAtBufferRow(0)).toBe(true)
|
||||
})
|
||||
|
||||
it("doesn't create a fold when the selection is empty", () => {
|
||||
selection.setBufferRange([[0, 3], [0, 3]])
|
||||
selection.fold()
|
||||
|
||||
expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]])
|
||||
expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]])
|
||||
expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {')
|
||||
expect(editor.isFoldedAtBufferRow(0)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,840 +0,0 @@
|
||||
{Point, Range} = require 'text-buffer'
|
||||
{pick} = require 'underscore-plus'
|
||||
{Emitter} = require 'event-kit'
|
||||
Model = require './model'
|
||||
|
||||
NonWhitespaceRegExp = /\S/
|
||||
|
||||
# Extended: Represents a selection in the {TextEditor}.
|
||||
module.exports =
|
||||
class Selection extends Model
|
||||
cursor: null
|
||||
marker: null
|
||||
editor: null
|
||||
initialScreenRange: null
|
||||
wordwise: false
|
||||
|
||||
constructor: ({@cursor, @marker, @editor, id}) ->
|
||||
@emitter = new Emitter
|
||||
|
||||
@assignId(id)
|
||||
@cursor.selection = this
|
||||
@decoration = @editor.decorateMarker(@marker, type: 'highlight', class: 'selection')
|
||||
|
||||
@marker.onDidChange (e) => @markerDidChange(e)
|
||||
@marker.onDidDestroy => @markerDidDestroy()
|
||||
|
||||
destroy: ->
|
||||
@marker.destroy()
|
||||
|
||||
isLastSelection: ->
|
||||
this is @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) ->
|
||||
@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) ->
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Managing the selection range
|
||||
###
|
||||
|
||||
# Public: Returns the screen {Range} for the selection.
|
||||
getScreenRange: ->
|
||||
@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) ->
|
||||
@setBufferRange(@editor.bufferRangeForScreenRange(screenRange), options)
|
||||
|
||||
# Public: Returns the buffer {Range} for the selection.
|
||||
getBufferRange: ->
|
||||
@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)
|
||||
options.reversed ?= @isReversed()
|
||||
@editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) unless options.preserveFolds
|
||||
@modifySelection =>
|
||||
needsFlash = options.flash
|
||||
delete options.flash if options.flash?
|
||||
@marker.setBufferRange(bufferRange, options)
|
||||
@autoscroll() if options?.autoscroll ? @isLastSelection()
|
||||
@decoration.flash('flash', @editor.selectionFlashDuration) if needsFlash
|
||||
|
||||
# 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: ->
|
||||
range = @getBufferRange()
|
||||
start = range.start.row
|
||||
end = range.end.row
|
||||
end = Math.max(start, end - 1) if range.end.column is 0
|
||||
[start, end]
|
||||
|
||||
getTailScreenPosition: ->
|
||||
@marker.getTailScreenPosition()
|
||||
|
||||
getTailBufferPosition: ->
|
||||
@marker.getTailBufferPosition()
|
||||
|
||||
getHeadScreenPosition: ->
|
||||
@marker.getHeadScreenPosition()
|
||||
|
||||
getHeadBufferPosition: ->
|
||||
@marker.getHeadBufferPosition()
|
||||
|
||||
###
|
||||
Section: Info about the selection
|
||||
###
|
||||
|
||||
# Public: Determines if the selection contains anything.
|
||||
isEmpty: ->
|
||||
@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: ->
|
||||
@marker.isReversed()
|
||||
|
||||
# Public: Returns whether the selection is a single line or not.
|
||||
isSingleScreenLine: ->
|
||||
@getScreenRange().isSingleLine()
|
||||
|
||||
# Public: Returns the text in the selection.
|
||||
getText: ->
|
||||
@editor.buffer.getTextInRange(@getBufferRange())
|
||||
|
||||
# Public: Identifies if a selection intersects with a given buffer range.
|
||||
#
|
||||
# * `bufferRange` A {Range} to check against.
|
||||
#
|
||||
# Returns a {Boolean}
|
||||
intersectsBufferRange: (bufferRange) ->
|
||||
@getBufferRange().intersectsWith(bufferRange)
|
||||
|
||||
intersectsScreenRowRange: (startRow, endRow) ->
|
||||
@getScreenRange().intersectsRowRange(startRow, endRow)
|
||||
|
||||
intersectsScreenRow: (screenRow) ->
|
||||
@getScreenRange().intersectsRow(screenRow)
|
||||
|
||||
# Public: Identifies if a selection intersects with another selection.
|
||||
#
|
||||
# * `otherSelection` A {Selection} to check against.
|
||||
#
|
||||
# Returns a {Boolean}
|
||||
intersectsWith: (otherSelection, exclusive) ->
|
||||
@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) ->
|
||||
@goalScreenRange = null
|
||||
@marker.clearTail() unless @retainSelection
|
||||
@autoscroll() if options?.autoscroll ? @isLastSelection()
|
||||
@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)
|
||||
|
||||
@modifySelection =>
|
||||
if @initialScreenRange
|
||||
if position.isLessThan(@initialScreenRange.start)
|
||||
@marker.setScreenRange([position, @initialScreenRange.end], reversed: true)
|
||||
else
|
||||
@marker.setScreenRange([@initialScreenRange.start, position], reversed: false)
|
||||
else
|
||||
@cursor.setScreenPosition(position, options)
|
||||
|
||||
if @linewise
|
||||
@expandOverLine(options)
|
||||
else if @wordwise
|
||||
@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) ->
|
||||
@modifySelection => @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) ->
|
||||
@modifySelection => @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) ->
|
||||
@modifySelection => @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) ->
|
||||
@modifySelection => @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) ->
|
||||
@modifySelection => @cursor.moveDown(rowCount)
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the top of
|
||||
# the buffer.
|
||||
selectToTop: ->
|
||||
@modifySelection => @cursor.moveToTop()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the bottom
|
||||
# of the buffer.
|
||||
selectToBottom: ->
|
||||
@modifySelection => @cursor.moveToBottom()
|
||||
|
||||
# Public: Selects all the text in the buffer.
|
||||
selectAll: ->
|
||||
@setBufferRange(@editor.buffer.getRange(), autoscroll: false)
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the
|
||||
# beginning of the line.
|
||||
selectToBeginningOfLine: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfLine()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the first
|
||||
# character of the line.
|
||||
selectToFirstCharacterOfLine: ->
|
||||
@modifySelection => @cursor.moveToFirstCharacterOfLine()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the end of
|
||||
# the screen line.
|
||||
selectToEndOfLine: ->
|
||||
@modifySelection => @cursor.moveToEndOfScreenLine()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the end of
|
||||
# the buffer line.
|
||||
selectToEndOfBufferLine: ->
|
||||
@modifySelection => @cursor.moveToEndOfLine()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the
|
||||
# beginning of the word.
|
||||
selectToBeginningOfWord: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfWord()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the end of
|
||||
# the word.
|
||||
selectToEndOfWord: ->
|
||||
@modifySelection => @cursor.moveToEndOfWord()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the
|
||||
# beginning of the next word.
|
||||
selectToBeginningOfNextWord: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfNextWord()
|
||||
|
||||
# Public: Selects text to the previous word boundary.
|
||||
selectToPreviousWordBoundary: ->
|
||||
@modifySelection => @cursor.moveToPreviousWordBoundary()
|
||||
|
||||
# Public: Selects text to the next word boundary.
|
||||
selectToNextWordBoundary: ->
|
||||
@modifySelection => @cursor.moveToNextWordBoundary()
|
||||
|
||||
# Public: Selects text to the previous subword boundary.
|
||||
selectToPreviousSubwordBoundary: ->
|
||||
@modifySelection => @cursor.moveToPreviousSubwordBoundary()
|
||||
|
||||
# Public: Selects text to the next subword boundary.
|
||||
selectToNextSubwordBoundary: ->
|
||||
@modifySelection => @cursor.moveToNextSubwordBoundary()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the
|
||||
# beginning of the next paragraph.
|
||||
selectToBeginningOfNextParagraph: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfNextParagraph()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the
|
||||
# beginning of the previous paragraph.
|
||||
selectToBeginningOfPreviousParagraph: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfPreviousParagraph()
|
||||
|
||||
# Public: Modifies the selection to encompass the current word.
|
||||
#
|
||||
# Returns a {Range}.
|
||||
selectWord: (options={}) ->
|
||||
options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace()
|
||||
if @cursor.isBetweenWordAndNonWord()
|
||||
options.includeNonWordCharacters = false
|
||||
|
||||
@setBufferRange(@cursor.getCurrentWordBufferRange(options), options)
|
||||
@wordwise = true
|
||||
@initialScreenRange = @getScreenRange()
|
||||
|
||||
# Public: Expands the newest selection to include the entire word on which
|
||||
# the cursors rests.
|
||||
expandOverWord: (options) ->
|
||||
@setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()), autoscroll: false)
|
||||
@cursor.autoscroll() if options?.autoscroll ? true
|
||||
|
||||
# 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?
|
||||
@setBufferRange(@editor.bufferRangeForBufferRow(row, includeNewline: true), options)
|
||||
else
|
||||
startRange = @editor.bufferRangeForBufferRow(@marker.getStartBufferPosition().row)
|
||||
endRange = @editor.bufferRangeForBufferRow(@marker.getEndBufferPosition().row, includeNewline: true)
|
||||
@setBufferRange(startRange.union(endRange), options)
|
||||
|
||||
@linewise = true
|
||||
@wordwise = false
|
||||
@initialScreenRange = @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) ->
|
||||
range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true))
|
||||
@setBufferRange(range, autoscroll: false)
|
||||
@cursor.autoscroll() if options?.autoscroll ? 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` If `skip`, skips the undo stack for this operation.
|
||||
insertText: (text, options={}) ->
|
||||
oldBufferRange = @getBufferRange()
|
||||
wasReversed = @isReversed()
|
||||
@clear(options)
|
||||
|
||||
autoIndentFirstLine = false
|
||||
precedingText = @editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start])
|
||||
remainingLines = text.split('\n')
|
||||
firstInsertedLine = remainingLines.shift()
|
||||
|
||||
if options.indentBasis? and not options.preserveTrailingLineIndentation
|
||||
indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis
|
||||
@adjustIndent(remainingLines, indentAdjustment)
|
||||
|
||||
textIsAutoIndentable = text is '\n' or text is '\r\n' or NonWhitespaceRegExp.test(text)
|
||||
if options.autoIndent and textIsAutoIndentable and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0
|
||||
autoIndentFirstLine = true
|
||||
firstLine = precedingText + firstInsertedLine
|
||||
desiredIndentLevel = @editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
|
||||
indentAdjustment = desiredIndentLevel - @editor.indentLevelForLine(firstLine)
|
||||
@adjustIndent(remainingLines, indentAdjustment)
|
||||
|
||||
text = firstInsertedLine
|
||||
text += '\n' + remainingLines.join('\n') if remainingLines.length > 0
|
||||
|
||||
newBufferRange = @editor.buffer.setTextInRange(oldBufferRange, text, pick(options, 'undo', 'normalizeLineEndings'))
|
||||
|
||||
if options.select
|
||||
@setBufferRange(newBufferRange, reversed: wasReversed)
|
||||
else
|
||||
@cursor.setBufferPosition(newBufferRange.end) if wasReversed
|
||||
|
||||
if autoIndentFirstLine
|
||||
@editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel)
|
||||
|
||||
if options.autoIndentNewline and text is '\n'
|
||||
@editor.autoIndentBufferRow(newBufferRange.end.row, preserveLeadingWhitespace: true, skipBlankLines: false)
|
||||
else if options.autoDecreaseIndent and NonWhitespaceRegExp.test(text)
|
||||
@editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row)
|
||||
|
||||
@autoscroll() if options.autoscroll ? @isLastSelection()
|
||||
|
||||
newBufferRange
|
||||
|
||||
# Public: Removes the first character before the selection if the selection
|
||||
# is empty otherwise it deletes the selection.
|
||||
backspace: ->
|
||||
@selectLeft() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes the selection or, if nothing is selected, then all
|
||||
# characters from the start of the selection back to the previous word
|
||||
# boundary.
|
||||
deleteToPreviousWordBoundary: ->
|
||||
@selectToPreviousWordBoundary() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes the selection or, if nothing is selected, then all
|
||||
# characters from the start of the selection up to the next word
|
||||
# boundary.
|
||||
deleteToNextWordBoundary: ->
|
||||
@selectToNextWordBoundary() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# 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.
|
||||
deleteToBeginningOfWord: ->
|
||||
@selectToBeginningOfWord() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes from the beginning of the line which the selection begins on
|
||||
# all the way through to the end of the selection.
|
||||
deleteToBeginningOfLine: ->
|
||||
if @isEmpty() and @cursor.isAtBeginningOfLine()
|
||||
@selectLeft()
|
||||
else
|
||||
@selectToBeginningOfLine()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes the selection or the next character after the start of the
|
||||
# selection if the selection is empty.
|
||||
delete: ->
|
||||
@selectRight() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# 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.
|
||||
deleteToEndOfLine: ->
|
||||
return @delete() if @isEmpty() and @cursor.isAtEndOfLine()
|
||||
@selectToEndOfLine() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes the selection or all characters from the start of the
|
||||
# selection to the end of the current word if nothing is selected.
|
||||
deleteToEndOfWord: ->
|
||||
@selectToEndOfWord() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes the selection or all characters from the start of the
|
||||
# selection to the end of the current word if nothing is selected.
|
||||
deleteToBeginningOfSubword: ->
|
||||
@selectToPreviousSubwordBoundary() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes the selection or all characters from the start of the
|
||||
# selection to the end of the current word if nothing is selected.
|
||||
deleteToEndOfSubword: ->
|
||||
@selectToNextSubwordBoundary() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes only the selected text.
|
||||
deleteSelectedText: ->
|
||||
bufferRange = @getBufferRange()
|
||||
@editor.buffer.delete(bufferRange) unless bufferRange.isEmpty()
|
||||
@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.
|
||||
deleteLine: ->
|
||||
if @isEmpty()
|
||||
start = @cursor.getScreenRow()
|
||||
range = @editor.bufferRowsForScreenRows(start, start + 1)
|
||||
if range[1] > range[0]
|
||||
@editor.buffer.deleteRows(range[0], range[1] - 1)
|
||||
else
|
||||
@editor.buffer.deleteRow(range[0])
|
||||
else
|
||||
range = @getBufferRange()
|
||||
start = range.start.row
|
||||
end = range.end.row
|
||||
if end isnt @editor.buffer.getLastRow() and range.end.column is 0
|
||||
end--
|
||||
@editor.buffer.deleteRows(start, end)
|
||||
|
||||
# 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.
|
||||
joinLines: ->
|
||||
selectedRange = @getBufferRange()
|
||||
if selectedRange.isEmpty()
|
||||
return if selectedRange.start.row is @editor.buffer.getLastRow()
|
||||
else
|
||||
joinMarker = @editor.markBufferRange(selectedRange, invalidate: 'never')
|
||||
|
||||
rowCount = Math.max(1, selectedRange.getRowCount() - 1)
|
||||
for [0...rowCount]
|
||||
@cursor.setBufferPosition([selectedRange.start.row])
|
||||
@cursor.moveToEndOfLine()
|
||||
|
||||
# Remove trailing whitespace from the current line
|
||||
scanRange = @cursor.getCurrentLineBufferRange()
|
||||
trailingWhitespaceRange = null
|
||||
@editor.scanInBufferRange /[ \t]+$/, scanRange, ({range}) ->
|
||||
trailingWhitespaceRange = range
|
||||
if trailingWhitespaceRange?
|
||||
@setBufferRange(trailingWhitespaceRange)
|
||||
@deleteSelectedText()
|
||||
|
||||
currentRow = selectedRange.start.row
|
||||
nextRow = currentRow + 1
|
||||
insertSpace = nextRow <= @editor.buffer.getLastRow() and
|
||||
@editor.buffer.lineLengthForRow(nextRow) > 0 and
|
||||
@editor.buffer.lineLengthForRow(currentRow) > 0
|
||||
@insertText(' ') if insertSpace
|
||||
|
||||
@cursor.moveToEndOfLine()
|
||||
|
||||
# Remove leading whitespace from the line below
|
||||
@modifySelection =>
|
||||
@cursor.moveRight()
|
||||
@cursor.moveToFirstCharacterOfLine()
|
||||
@deleteSelectedText()
|
||||
|
||||
@cursor.moveLeft() if insertSpace
|
||||
|
||||
if joinMarker?
|
||||
newSelectedRange = joinMarker.getBufferRange()
|
||||
@setBufferRange(newSelectedRange)
|
||||
joinMarker.destroy()
|
||||
|
||||
# Public: Removes one level of indent from the currently selected rows.
|
||||
outdentSelectedRows: ->
|
||||
[start, end] = @getBufferRowRange()
|
||||
buffer = @editor.buffer
|
||||
leadingTabRegex = new RegExp("^( {1,#{@editor.getTabLength()}}|\t)")
|
||||
for row in [start..end]
|
||||
if matchLength = buffer.lineForRow(row).match(leadingTabRegex)?[0].length
|
||||
buffer.delete [[row, 0], [row, matchLength]]
|
||||
return
|
||||
|
||||
# Public: Sets the indentation level of all selected rows to values suggested
|
||||
# by the relevant grammars.
|
||||
autoIndentSelectedRows: ->
|
||||
[start, end] = @getBufferRowRange()
|
||||
@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.
|
||||
toggleLineComments: ->
|
||||
@editor.toggleLineCommentsForBufferRows(@getBufferRowRange()...)
|
||||
|
||||
# Public: Cuts the selection until the end of the screen line.
|
||||
cutToEndOfLine: (maintainClipboard) ->
|
||||
@selectToEndOfLine() if @isEmpty()
|
||||
@cut(maintainClipboard)
|
||||
|
||||
# Public: Cuts the selection until the end of the buffer line.
|
||||
cutToEndOfBufferLine: (maintainClipboard) ->
|
||||
@selectToEndOfBufferLine() if @isEmpty()
|
||||
@cut(maintainClipboard)
|
||||
|
||||
# Public: Copies the selection to the clipboard and then deletes it.
|
||||
#
|
||||
# * `maintainClipboard` {Boolean} (default: false) See {::copy}
|
||||
# * `fullLine` {Boolean} (default: false) See {::copy}
|
||||
cut: (maintainClipboard=false, fullLine=false) ->
|
||||
@copy(maintainClipboard, fullLine)
|
||||
@delete()
|
||||
|
||||
# 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) ->
|
||||
return if @isEmpty()
|
||||
{start, end} = @getBufferRange()
|
||||
selectionText = @editor.getTextInRange([start, end])
|
||||
precedingText = @editor.getTextInRange([[start.row, 0], start])
|
||||
startLevel = @editor.indentLevelForLine(precedingText)
|
||||
|
||||
if maintainClipboard
|
||||
{text: clipboardText, metadata} = @editor.constructor.clipboard.readWithMetadata()
|
||||
metadata ?= {}
|
||||
unless metadata.selections?
|
||||
metadata.selections = [{
|
||||
text: clipboardText,
|
||||
indentBasis: metadata.indentBasis,
|
||||
fullLine: metadata.fullLine,
|
||||
}]
|
||||
metadata.selections.push({
|
||||
text: selectionText,
|
||||
indentBasis: startLevel,
|
||||
fullLine: fullLine
|
||||
})
|
||||
@editor.constructor.clipboard.write([clipboardText, selectionText].join("\n"), metadata)
|
||||
else
|
||||
@editor.constructor.clipboard.write(selectionText, {
|
||||
indentBasis: startLevel,
|
||||
fullLine: fullLine
|
||||
})
|
||||
|
||||
# Public: Creates a fold containing the current selection.
|
||||
fold: ->
|
||||
range = @getBufferRange()
|
||||
unless range.isEmpty()
|
||||
@editor.foldBufferRange(range)
|
||||
@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 line, i in lines
|
||||
if indentAdjustment is 0 or line is ''
|
||||
continue
|
||||
else if indentAdjustment > 0
|
||||
lines[i] = @editor.buildIndentString(indentAdjustment) + line
|
||||
else
|
||||
currentIndentLevel = @editor.indentLevelForLine(lines[i])
|
||||
indentLevel = Math.max(0, currentIndentLevel + indentAdjustment)
|
||||
lines[i] = line.replace(/^[\t ]+/, @editor.buildIndentString(indentLevel))
|
||||
return
|
||||
|
||||
# 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.
|
||||
indent: ({autoIndent}={}) ->
|
||||
{row} = @cursor.getBufferPosition()
|
||||
|
||||
if @isEmpty()
|
||||
@cursor.skipLeadingWhitespace()
|
||||
desiredIndent = @editor.suggestedIndentForBufferRow(row)
|
||||
delta = desiredIndent - @cursor.getIndentLevel()
|
||||
|
||||
if autoIndent and delta > 0
|
||||
delta = Math.max(delta, 1) unless @editor.getSoftTabs()
|
||||
@insertText(@editor.buildIndentString(delta))
|
||||
else
|
||||
@insertText(@editor.buildIndentString(1, @cursor.getBufferColumn()))
|
||||
else
|
||||
@indentSelectedRows()
|
||||
|
||||
# Public: If the selection spans multiple rows, indent all of them.
|
||||
indentSelectedRows: ->
|
||||
[start, end] = @getBufferRowRange()
|
||||
for row in [start..end]
|
||||
@editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) is 0
|
||||
return
|
||||
|
||||
###
|
||||
Section: Managing multiple selections
|
||||
###
|
||||
|
||||
# Public: Moves the selection down one row.
|
||||
addSelectionBelow: ->
|
||||
range = @getGoalScreenRange().copy()
|
||||
nextRow = range.end.row + 1
|
||||
|
||||
for row in [nextRow..@editor.getLastScreenRow()]
|
||||
range.start.row = row
|
||||
range.end.row = row
|
||||
clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true)
|
||||
|
||||
if range.isEmpty()
|
||||
continue if range.end.column > 0 and clippedRange.end.column is 0
|
||||
else
|
||||
continue if clippedRange.isEmpty()
|
||||
|
||||
selection = @editor.addSelectionForScreenRange(clippedRange)
|
||||
selection.setGoalScreenRange(range)
|
||||
break
|
||||
|
||||
return
|
||||
|
||||
# Public: Moves the selection up one row.
|
||||
addSelectionAbove: ->
|
||||
range = @getGoalScreenRange().copy()
|
||||
previousRow = range.end.row - 1
|
||||
|
||||
for row in [previousRow..0]
|
||||
range.start.row = row
|
||||
range.end.row = row
|
||||
clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true)
|
||||
|
||||
if range.isEmpty()
|
||||
continue if range.end.column > 0 and clippedRange.end.column is 0
|
||||
else
|
||||
continue if clippedRange.isEmpty()
|
||||
|
||||
selection = @editor.addSelectionForScreenRange(clippedRange)
|
||||
selection.setGoalScreenRange(range)
|
||||
break
|
||||
|
||||
return
|
||||
|
||||
# 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 = {}) ->
|
||||
myGoalScreenRange = @getGoalScreenRange()
|
||||
otherGoalScreenRange = otherSelection.getGoalScreenRange()
|
||||
|
||||
if myGoalScreenRange? and otherGoalScreenRange?
|
||||
options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange)
|
||||
else
|
||||
options.goalScreenRange = myGoalScreenRange ? otherGoalScreenRange
|
||||
|
||||
@setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), 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) ->
|
||||
@marker.compare(otherSelection.marker)
|
||||
|
||||
###
|
||||
Section: Private Utilities
|
||||
###
|
||||
|
||||
setGoalScreenRange: (range) ->
|
||||
@goalScreenRange = Range.fromObject(range)
|
||||
|
||||
getGoalScreenRange: ->
|
||||
@goalScreenRange ? @getScreenRange()
|
||||
|
||||
markerDidChange: (e) ->
|
||||
{oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition} = e
|
||||
{oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e
|
||||
{textChanged} = e
|
||||
|
||||
unless oldHeadScreenPosition.isEqual(newHeadScreenPosition)
|
||||
@cursor.goalColumn = null
|
||||
cursorMovedEvent = {
|
||||
oldBufferPosition: oldHeadBufferPosition
|
||||
oldScreenPosition: oldHeadScreenPosition
|
||||
newBufferPosition: newHeadBufferPosition
|
||||
newScreenPosition: newHeadScreenPosition
|
||||
textChanged: textChanged
|
||||
cursor: @cursor
|
||||
}
|
||||
@cursor.emitter.emit('did-change-position', cursorMovedEvent)
|
||||
@editor.cursorMoved(cursorMovedEvent)
|
||||
|
||||
@emitter.emit 'did-change-range'
|
||||
@editor.selectionRangeChanged(
|
||||
oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition)
|
||||
oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition)
|
||||
newBufferRange: @getBufferRange()
|
||||
newScreenRange: @getScreenRange()
|
||||
selection: this
|
||||
)
|
||||
|
||||
markerDidDestroy: ->
|
||||
return if @editor.isDestroyed()
|
||||
|
||||
@destroyed = true
|
||||
@cursor.destroyed = true
|
||||
|
||||
@editor.removeSelection(this)
|
||||
|
||||
@cursor.emitter.emit 'did-destroy'
|
||||
@emitter.emit 'did-destroy'
|
||||
|
||||
@cursor.emitter.dispose()
|
||||
@emitter.dispose()
|
||||
|
||||
finalize: ->
|
||||
@initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange())
|
||||
if @isEmpty()
|
||||
@wordwise = false
|
||||
@linewise = false
|
||||
|
||||
autoscroll: (options) ->
|
||||
if @marker.hasTail()
|
||||
@editor.scrollToScreenRange(@getScreenRange(), Object.assign({reversed: @isReversed()}, options))
|
||||
else
|
||||
@cursor.autoscroll(options)
|
||||
|
||||
clearAutoscroll: ->
|
||||
|
||||
modifySelection: (fn) ->
|
||||
@retainSelection = true
|
||||
@plantTail()
|
||||
fn()
|
||||
@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: ->
|
||||
@marker.plantTail()
|
||||
977
src/selection.js
Normal file
977
src/selection.js
Normal file
@@ -0,0 +1,977 @@
|
||||
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()
|
||||
}
|
||||
|
||||
/*
|
||||
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` If `skip`, skips the undo stack for this operation.
|
||||
insertText (text, options = {}) {
|
||||
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
|
||||
desiredIndentLevel = this.editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
|
||||
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.
|
||||
backspace () {
|
||||
if (this.isEmpty()) this.selectLeft()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes the selection or, if nothing is selected, then all
|
||||
// characters from the start of the selection back to the previous word
|
||||
// boundary.
|
||||
deleteToPreviousWordBoundary () {
|
||||
if (this.isEmpty()) this.selectToPreviousWordBoundary()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes the selection or, if nothing is selected, then all
|
||||
// characters from the start of the selection up to the next word
|
||||
// boundary.
|
||||
deleteToNextWordBoundary () {
|
||||
if (this.isEmpty()) this.selectToNextWordBoundary()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// 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.
|
||||
deleteToBeginningOfWord () {
|
||||
if (this.isEmpty()) this.selectToBeginningOfWord()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes from the beginning of the line which the selection begins on
|
||||
// all the way through to the end of the selection.
|
||||
deleteToBeginningOfLine () {
|
||||
if (this.isEmpty() && this.cursor.isAtBeginningOfLine()) {
|
||||
this.selectLeft()
|
||||
} else {
|
||||
this.selectToBeginningOfLine()
|
||||
}
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes the selection or the next character after the start of the
|
||||
// selection if the selection is empty.
|
||||
delete () {
|
||||
if (this.isEmpty()) this.selectRight()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// 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.
|
||||
deleteToEndOfLine () {
|
||||
if (this.isEmpty()) {
|
||||
if (this.cursor.isAtEndOfLine()) {
|
||||
this.delete()
|
||||
return
|
||||
}
|
||||
this.selectToEndOfLine()
|
||||
}
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes the selection or all characters from the start of the
|
||||
// selection to the end of the current word if nothing is selected.
|
||||
deleteToEndOfWord () {
|
||||
if (this.isEmpty()) this.selectToEndOfWord()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes the selection or all characters from the start of the
|
||||
// selection to the end of the current word if nothing is selected.
|
||||
deleteToBeginningOfSubword () {
|
||||
if (this.isEmpty()) this.selectToPreviousSubwordBoundary()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes the selection or all characters from the start of the
|
||||
// selection to the end of the current word if nothing is selected.
|
||||
deleteToEndOfSubword () {
|
||||
if (this.isEmpty()) this.selectToNextSubwordBoundary()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes only the selected text.
|
||||
deleteSelectedText () {
|
||||
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.
|
||||
deleteLine () {
|
||||
if (this.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 range = this.getBufferRange()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
joinLines () {
|
||||
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()
|
||||
}
|
||||
|
||||
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(' ')
|
||||
|
||||
this.cursor.moveToEndOfLine()
|
||||
|
||||
// Remove leading whitespace from the line below
|
||||
this.modifySelection(() => {
|
||||
this.cursor.moveRight()
|
||||
this.cursor.moveToFirstCharacterOfLine()
|
||||
})
|
||||
this.deleteSelectedText()
|
||||
|
||||
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.
|
||||
outdentSelectedRows () {
|
||||
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.
|
||||
autoIndentSelectedRows () {
|
||||
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.
|
||||
toggleLineComments () {
|
||||
this.editor.toggleLineCommentsForBufferRows(...(this.getBufferRowRange() || []))
|
||||
}
|
||||
|
||||
// Public: Cuts the selection until the end of the screen line.
|
||||
cutToEndOfLine (maintainClipboard) {
|
||||
if (this.isEmpty()) this.selectToEndOfLine()
|
||||
return this.cut(maintainClipboard)
|
||||
}
|
||||
|
||||
// Public: Cuts the selection until the end of the buffer line.
|
||||
cutToEndOfBufferLine (maintainClipboard) {
|
||||
if (this.isEmpty()) this.selectToEndOfBufferLine()
|
||||
this.cut(maintainClipboard)
|
||||
}
|
||||
|
||||
// Public: Copies the selection to the clipboard and then deletes it.
|
||||
//
|
||||
// * `maintainClipboard` {Boolean} (default: false) See {::copy}
|
||||
// * `fullLine` {Boolean} (default: false) See {::copy}
|
||||
cut (maintainClipboard = false, fullLine = false) {
|
||||
this.copy(maintainClipboard, fullLine)
|
||||
this.delete()
|
||||
}
|
||||
|
||||
// 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.
|
||||
indent ({autoIndent} = {}) {
|
||||
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))
|
||||
} else {
|
||||
this.insertText(this.editor.buildIndentString(1, this.cursor.getBufferColumn()))
|
||||
}
|
||||
} else {
|
||||
this.indentSelectedRows()
|
||||
}
|
||||
}
|
||||
|
||||
// Public: If the selection spans multiple rows, indent all of them.
|
||||
indentSelectedRows () {
|
||||
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 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 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user