Merge pull request #16999 from atom/aw/no-really-read-only

Prevent default commands from modifying readonly TextEditors
This commit is contained in:
Ash Wilson
2018-03-21 16:03:02 -04:00
committed by GitHub
5 changed files with 702 additions and 114 deletions

View File

@@ -154,4 +154,118 @@ describe('Selection', () => {
expect(editor.isFoldedAtBufferRow(0)).toBe(false)
})
})
describe('within a read-only editor', () => {
beforeEach(() => {
editor.setReadOnly(true)
selection.setBufferRange([[0, 0], [0, 13]])
})
const modifications = [
{
name: 'insertText',
op: opts => selection.insertText('yes', opts)
},
{
name: 'backspace',
op: opts => selection.backspace(opts)
},
{
name: 'deleteToPreviousWordBoundary',
op: opts => selection.deleteToPreviousWordBoundary(opts)
},
{
name: 'deleteToNextWordBoundary',
op: opts => selection.deleteToNextWordBoundary(opts)
},
{
name: 'deleteToBeginningOfWord',
op: opts => selection.deleteToBeginningOfWord(opts)
},
{
name: 'deleteToBeginningOfLine',
op: opts => selection.deleteToBeginningOfLine(opts)
},
{
name: 'delete',
op: opts => selection.delete(opts)
},
{
name: 'deleteToEndOfLine',
op: opts => selection.deleteToEndOfLine(opts)
},
{
name: 'deleteToEndOfWord',
op: opts => selection.deleteToEndOfWord(opts)
},
{
name: 'deleteToBeginningOfSubword',
op: opts => selection.deleteToBeginningOfSubword(opts)
},
{
name: 'deleteToEndOfSubword',
op: opts => selection.deleteToEndOfSubword(opts)
},
{
name: 'deleteSelectedText',
op: opts => selection.deleteSelectedText(opts)
},
{
name: 'deleteLine',
op: opts => selection.deleteLine(opts)
},
{
name: 'joinLines',
op: opts => selection.joinLines(opts)
},
{
name: 'outdentSelectedRows',
op: opts => selection.outdentSelectedRows(opts)
},
{
name: 'autoIndentSelectedRows',
op: opts => selection.autoIndentSelectedRows(opts)
},
{
name: 'toggleLineComments',
op: opts => selection.toggleLineComments(opts)
},
{
name: 'cutToEndOfLine',
op: opts => selection.cutToEndOfLine(false, opts)
},
{
name: 'cutToEndOfBufferLine',
op: opts => selection.cutToEndOfBufferLine(false, opts)
},
{
name: 'cut',
op: opts => selection.cut(false, false, opts.bypassReadOnly)
},
{
name: 'indent',
op: opts => selection.indent(opts)
},
{
name: 'indentSelectedRows',
op: opts => selection.indentSelectedRows(opts)
},
]
describe('without bypassReadOnly', () => {
for (const {name, op} of modifications) {
it(`throws an error on ${name}`, () => {
expect(op).toThrow()
})
}
})
describe('with bypassReadOnly', () => {
for (const {name, op} of modifications) {
it(`permits ${name}`, () => {
op({bypassReadOnly: true})
})
}
})
})
})

View File

@@ -5488,6 +5488,195 @@ describe('TextEditor', () => {
})
})
})
describe('when readonly', () => {
beforeEach(() => {
editor.setReadOnly(true)
})
const modifications = [
{
name: 'moveLineUp',
op: (opts = {}) => {
editor.setCursorBufferPosition([1, 0])
editor.moveLineUp(opts)
}
},
{
name: 'moveLineDown',
op: (opts = {}) => {
editor.setCursorBufferPosition([0, 0])
editor.moveLineDown(opts)
}
},
{
name: 'insertText',
op: (opts = {}) => {
editor.setSelectedBufferRange([[1, 0], [1, 2]])
editor.insertText('xxx', opts)
}
},
{
name: 'insertNewline',
op: (opts = {}) => {
editor.setCursorScreenPosition({row: 1, column: 0})
editor.insertNewline(opts)
}
},
{
name: 'insertNewlineBelow',
op: (opts = {}) => {
editor.setCursorBufferPosition([0, 2])
editor.insertNewlineBelow(opts)
}
},
{
name: 'insertNewlineAbove',
op: (opts = {}) => {
editor.setCursorBufferPosition([0])
editor.insertNewlineAbove(opts)
}
},
{
name: 'backspace',
op: (opts = {}) => {
editor.setCursorScreenPosition({row: 1, column: 7})
editor.backspace(opts)
}
},
{
name: 'deleteToPreviousWordBoundary',
op: (opts = {}) => {
editor.setCursorBufferPosition([0, 16])
editor.deleteToPreviousWordBoundary(opts)
}
},
{
name: 'deleteToNextWordBoundary',
op: (opts = {}) => {
editor.setCursorBufferPosition([0, 15])
editor.deleteToNextWordBoundary(opts)
}
},
{
name: 'deleteToBeginningOfWord',
op: (opts = {}) => {
editor.setCursorBufferPosition([1, 24])
editor.deleteToBeginningOfWord(opts)
}
},
{
name: 'deleteToEndOfLine',
op: (opts = {}) => {
editor.setCursorBufferPosition([1, 24])
editor.deleteToEndOfLine(opts)
}
},
{
name: 'deleteToBeginningOfLine',
op: (opts = {}) => {
editor.setCursorBufferPosition([1, 24])
editor.deleteToBeginningOfLine(opts)
}
},
{
name: 'delete',
op: (opts = {}) => {
editor.setCursorScreenPosition([1, 6])
editor.delete(opts)
}
},
{
name: 'deleteToEndOfWord',
op: (opts = {}) => {
editor.setCursorBufferPosition([1, 24])
editor.deleteToEndOfWord(opts)
}
},
{
name: 'indent',
op: (opts = {}) => {
editor.indent(opts)
}
},
{
name: 'cutSelectedText',
op: (opts = {}) => {
editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]])
editor.cutSelectedText(opts)
}
},
{
name: 'cutToEndOfLine',
op: (opts = {}) => {
editor.setCursorBufferPosition([2, 20])
editor.cutToEndOfLine(opts)
}
},
{
name: 'cutToEndOfBufferLine',
op: (opts = {}) => {
editor.setCursorBufferPosition([2, 20])
editor.cutToEndOfBufferLine(opts)
}
},
{
name: 'pasteText',
op: (opts = {}) => {
editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]])
atom.clipboard.write('first')
editor.pasteText(opts)
}
},
{
name: 'indentSelectedRows',
op: (opts = {}) => {
editor.setSelectedBufferRange([[0, 3], [0, 3]])
editor.indentSelectedRows(opts)
}
},
{
name: 'outdentSelectedRows',
op: (opts = {}) => {
editor.setSelectedBufferRange([[1, 3], [1, 3]])
editor.outdentSelectedRows(opts)
}
},
{
name: 'autoIndentSelectedRows',
op: (opts = {}) => {
editor.setCursorBufferPosition([2, 0])
editor.insertText('function() {\ninside=true\n}\n i=1\n', opts)
editor.getLastSelection().setBufferRange([[2, 0], [6, 0]])
editor.autoIndentSelectedRows(opts)
}
},
{
name: 'undo/redo',
op: (opts = {}) => {
editor.insertText('foo', opts)
editor.undo(opts)
editor.redo(opts)
}
}
]
describe('without bypassReadOnly', () => {
for (const {name, op} of modifications) {
it(`throws an error on ${name}`, () => {
expect(op).toThrow()
})
}
})
describe('with bypassReadOnly', () => {
for (const {name, op} of modifications) {
it(`permits ${name}`, () => {
op({bypassReadOnly: true})
})
}
})
})
})
describe('reading text', () => {

View File

@@ -166,15 +166,35 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
false
)
commandRegistry.add(
'atom-text-editor:not([readonly])',
stopEventPropagation({
'core:undo': -> @undo()
'core:redo': -> @redo()
}),
false
)
commandRegistry.add(
'atom-text-editor',
stopEventPropagationAndGroupUndo(
config,
{
'core:copy': -> @copySelectedText()
'editor:copy-selection': -> @copyOnlySelectedText()
}
),
false
)
commandRegistry.add(
'atom-text-editor:not([readonly])',
stopEventPropagationAndGroupUndo(
config,
{
'core:backspace': -> @backspace()
'core:delete': -> @delete()
'core:cut': -> @cutSelectedText()
'core:copy': -> @copySelectedText()
'core:paste': -> @pasteText()
'editor:paste-without-reformatting': -> @pasteText({
normalizeLineEndings: false,
@@ -195,7 +215,6 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
'editor:transpose': -> @transpose()
'editor:upper-case': -> @upperCase()
'editor:lower-case': -> @lowerCase()
'editor:copy-selection': -> @copyOnlySelectedText()
}
),
false
@@ -266,7 +285,7 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
)
commandRegistry.add(
'atom-text-editor:not([mini])',
'atom-text-editor:not([mini]):not([readonly])',
stopEventPropagationAndGroupUndo(
config,
{

View File

@@ -407,6 +407,25 @@ class Selection {
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
*/
@@ -428,7 +447,10 @@ class Selection {
// level between the first lines and the trailing lines.
// * `normalizeLineEndings` (optional) {Boolean} (default: true)
// * `undo` If `skip`, skips the undo stack for this operation.
// * `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()
@@ -492,90 +514,134 @@ class Selection {
// Public: Removes the first character before the selection if the selection
// is empty otherwise it deletes the selection.
backspace () {
//
// * `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()
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.
deleteToPreviousWordBoundary () {
//
// * `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()
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.
deleteToNextWordBoundary () {
//
// * `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()
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.
deleteToBeginningOfWord () {
//
// * `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()
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.
deleteToBeginningOfLine () {
//
// * `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()
this.deleteSelectedText(options)
}
// Public: Removes the selection or the next character after the start of the
// selection if the selection is empty.
delete () {
//
// * `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()
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.
deleteToEndOfLine () {
//
// * `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()
this.delete(options)
return
}
this.selectToEndOfLine()
}
this.deleteSelectedText()
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.
deleteToEndOfWord () {
//
// * `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()
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.
deleteToBeginningOfSubword () {
//
// * `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()
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.
deleteToEndOfSubword () {
//
// * `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()
this.deleteSelectedText(options)
}
// Public: Removes only the selected text.
deleteSelectedText () {
//
// * `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)
@@ -584,7 +650,11 @@ class Selection {
// 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 () {
//
// * `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()
@@ -607,7 +677,11 @@ class Selection {
// be separated by a single space.
//
// If there selection spans more than one line, all the lines are joined together.
joinLines () {
//
// * `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()) {
@@ -629,7 +703,7 @@ class Selection {
})
if (trailingWhitespaceRange) {
this.setBufferRange(trailingWhitespaceRange)
this.deleteSelectedText()
this.deleteSelectedText(options)
}
const currentRow = selectedRange.start.row
@@ -638,7 +712,7 @@ class Selection {
(nextRow <= this.editor.buffer.getLastRow()) &&
(this.editor.buffer.lineLengthForRow(nextRow) > 0) &&
(this.editor.buffer.lineLengthForRow(currentRow) > 0)
if (insertSpace) this.insertText(' ')
if (insertSpace) this.insertText(' ', options)
this.cursor.moveToEndOfLine()
@@ -647,7 +721,7 @@ class Selection {
this.cursor.moveRight()
this.cursor.moveToFirstCharacterOfLine()
})
this.deleteSelectedText()
this.deleteSelectedText(options)
if (insertSpace) this.cursor.moveLeft()
}
@@ -660,7 +734,11 @@ class Selection {
}
// Public: Removes one level of indent from the currently selected rows.
outdentSelectedRows () {
//
// * `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)`)
@@ -674,7 +752,11 @@ class Selection {
// Public: Sets the indentation level of all selected rows to values suggested
// by the relevant grammars.
autoIndentSelectedRows () {
//
// * `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)
}
@@ -683,29 +765,45 @@ class Selection {
// of a comment.
//
// Removes the comment if they are currently wrapped in a comment.
toggleLineComments () {
//
// * `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.
cutToEndOfLine (maintainClipboard) {
//
// * `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)
return this.cut(maintainClipboard, false, options.bypassReadOnly)
}
// Public: Cuts the selection until the end of the buffer line.
cutToEndOfBufferLine (maintainClipboard) {
//
// * `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)
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}
cut (maintainClipboard = false, fullLine = false) {
// * `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()
this.delete({bypassReadOnly})
}
// Public: Copies the current selection to the clipboard.
@@ -783,7 +881,9 @@ class Selection {
// * `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} = {}) {
// * `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()) {
@@ -793,17 +893,21 @@ class Selection {
if (autoIndent && delta > 0) {
if (!this.editor.getSoftTabs()) delta = Math.max(delta, 1)
this.insertText(this.editor.buildIndentString(delta))
this.insertText(this.editor.buildIndentString(delta), {bypassReadOnly})
} else {
this.insertText(this.editor.buildIndentString(1, this.cursor.getBufferColumn()))
this.insertText(this.editor.buildIndentString(1, this.cursor.getBufferColumn()), {bypassReadOnly})
}
} else {
this.indentSelectedRows()
this.indentSelectedRows({bypassReadOnly})
}
}
// Public: If the selection spans multiple rows, indent all of them.
indentSelectedRows () {
//
// * `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) {

View File

@@ -42,9 +42,10 @@ const DEFAULT_NON_WORD_CHARACTERS = "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…"
// then be called with all current editor instances and also when any editor is
// created in the future.
//
// ```coffee
// atom.workspace.observeTextEditors (editor) ->
// ```js
// atom.workspace.observeTextEditors(editor => {
// editor.insertText('Hello World')
// })
// ```
//
// ## Buffer vs. Screen Coordinates
@@ -1306,7 +1307,12 @@ class TextEditor {
// Essential: Replaces the entire contents of the buffer with the given {String}.
//
// * `text` A {String} to replace with
setText (text) { return this.buffer.setText(text) }
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
setText (text, options = {}) {
if (!this.ensureWritable('setText', options)) return
return this.buffer.setText(text)
}
// Essential: Set the text in the given {Range} in buffer coordinates.
//
@@ -1315,9 +1321,11 @@ class TextEditor {
// * `options` (optional) {Object}
// * `normalizeLineEndings` (optional) {Boolean} (default: true)
// * `undo` (optional) {String} 'skip' will skip the undo system
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
//
// Returns the {Range} of the newly-inserted text.
setTextInBufferRange (range, text, options) {
setTextInBufferRange (range, text, options = {}) {
if (!this.ensureWritable('setTextInBufferRange', options)) return
return this.getBuffer().setTextInRange(range, text, options)
}
@@ -1326,9 +1334,9 @@ class TextEditor {
// * `text` A {String} representing the text to insert.
// * `options` (optional) See {Selection::insertText}.
//
// Returns a {Range} when the text has been inserted
// Returns a {Boolean} false when the text has not been inserted
// Returns a {Range} when the text has been inserted. Returns a {Boolean} `false` when the text has not been inserted.
insertText (text, options = {}) {
if (!this.ensureWritable('insertText', options)) return
if (!this.emitWillInsertTextEvent(text)) return false
let groupLastChanges = false
@@ -1352,20 +1360,31 @@ class TextEditor {
}
// Essential: For each selection, replace the selected text with a newline.
insertNewline (options) {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
insertNewline (options = {}) {
return this.insertText('\n', options)
}
// Essential: For each selection, if the selection is empty, delete the character
// following the cursor. Otherwise delete the selected text.
delete () {
return this.mutateSelectedText(selection => selection.delete())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
delete (options = {}) {
if (!this.ensureWritable('delete', options)) return
return this.mutateSelectedText(selection => selection.delete(options))
}
// Essential: For each selection, if the selection is empty, delete the character
// preceding the cursor. Otherwise delete the selected text.
backspace () {
return this.mutateSelectedText(selection => selection.backspace())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
backspace (options = {}) {
if (!this.ensureWritable('backspace', options)) return
return this.mutateSelectedText(selection => selection.backspace(options))
}
// Extended: Mutate the text of all the selections in a single transaction.
@@ -1386,7 +1405,12 @@ class TextEditor {
// Move lines intersecting the most recent selection or multiple selections
// up by one row in screen coordinates.
moveLineUp () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
moveLineUp (options = {}) {
if (!this.ensureWritable('moveLineUp', options)) return
const selections = this.getSelectedBufferRanges().sort((a, b) => a.compare(b))
if (selections[0].start.row === 0) return
@@ -1454,7 +1478,12 @@ class TextEditor {
// Move lines intersecting the most recent selection or multiple selections
// down by one row in screen coordinates.
moveLineDown () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
moveLineDown (options = {}) {
if (!this.ensureWritable('moveLineDown', options)) return
const selections = this.getSelectedBufferRanges()
selections.sort((a, b) => b.compare(a))
@@ -1526,7 +1555,11 @@ class TextEditor {
}
// Move any active selections one column to the left.
moveSelectionLeft () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
moveSelectionLeft (options = {}) {
if (!this.ensureWritable('moveSelectionLeft', options)) return
const selections = this.getSelectedBufferRanges()
const noSelectionAtStartOfLine = selections.every(selection => selection.start.column !== 0)
@@ -1550,7 +1583,11 @@ class TextEditor {
}
// Move any active selections one column to the right.
moveSelectionRight () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
moveSelectionRight (options = {}) {
if (!this.ensureWritable('moveSelectionRight', options)) return
const selections = this.getSelectedBufferRanges()
const noSelectionAtEndOfLine = selections.every(selection => {
return selection.end.column !== this.buffer.lineLengthForRow(selection.end.row)
@@ -1575,7 +1612,12 @@ class TextEditor {
}
}
duplicateLines () {
// Duplicate all lines containing active selections.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
duplicateLines (options = {}) {
if (!this.ensureWritable('duplicateLines', options)) return
this.transact(() => {
const selections = this.getSelectionsOrderedByBufferPosition()
const previousSelectionRanges = []
@@ -1662,7 +1704,11 @@ class TextEditor {
//
// If the selection is empty, the characters preceding and following the cursor
// are swapped. Otherwise, the selected characters are reversed.
transpose () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
transpose (options = {}) {
if (!this.ensureWritable('transpose', options)) return
this.mutateSelectedText(selection => {
if (selection.isEmpty()) {
selection.selectRight()
@@ -1680,23 +1726,35 @@ class TextEditor {
//
// For each selection, if the selection is empty, converts the containing word
// to upper case. Otherwise convert the selected text to upper case.
upperCase () {
this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toUpperCase())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
upperCase (options = {}) {
if (!this.ensureWritable('upperCase', options)) return
this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toUpperCase(options))
}
// Extended: Convert the selected text to lower case.
//
// For each selection, if the selection is empty, converts the containing word
// to upper case. Otherwise convert the selected text to upper case.
lowerCase () {
this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toLowerCase())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
lowerCase (options = {}) {
if (!this.ensureWritable('lowerCase', options)) return
this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toLowerCase(options))
}
// Extended: Toggle line comments for rows intersecting selections.
//
// If the current grammar doesn't support comments, does nothing.
toggleLineCommentsInSelection () {
this.mutateSelectedText(selection => selection.toggleLineComments())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
toggleLineCommentsInSelection (options = {}) {
if (!this.ensureWritable('toggleLineCommentsInSelection', options)) return
this.mutateSelectedText(selection => selection.toggleLineComments(options))
}
// Convert multiple lines to a single line.
@@ -1707,20 +1765,32 @@ class TextEditor {
//
// Joining a line means that multiple lines are converted to a single line with
// the contents of each of the original non-empty lines separated by a space.
joinLines () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
joinLines (options = {}) {
if (!this.ensureWritable('joinLines', options)) return
this.mutateSelectedText(selection => selection.joinLines())
}
// Extended: For each cursor, insert a newline at beginning the following line.
insertNewlineBelow () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
insertNewlineBelow (options = {}) {
if (!this.ensureWritable('insertNewlineBelow', options)) return
this.transact(() => {
this.moveToEndOfLine()
this.insertNewline()
this.insertNewline(options)
})
}
// Extended: For each cursor, insert a newline at the end of the preceding line.
insertNewlineAbove () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
insertNewlineAbove (options = {}) {
if (!this.ensureWritable('insertNewlineAbove', options)) return
this.transact(() => {
const bufferRow = this.getCursorBufferPosition().row
const indentLevel = this.indentationForBufferRow(bufferRow)
@@ -1728,7 +1798,7 @@ class TextEditor {
this.moveToBeginningOfLine()
this.moveLeft()
this.insertNewline()
this.insertNewline(options)
if (this.shouldAutoIndent() && (this.indentationForBufferRow(bufferRow) < indentLevel)) {
this.setIndentationForBufferRow(bufferRow, indentLevel)
@@ -1744,62 +1814,117 @@ class TextEditor {
// Extended: For each selection, if the selection is empty, delete all characters
// of the containing word that precede the cursor. Otherwise delete the
// selected text.
deleteToBeginningOfWord () {
this.mutateSelectedText(selection => selection.deleteToBeginningOfWord())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToBeginningOfWord (options = {}) {
if (!this.ensureWritable('deleteToBeginningOfWord', options)) return
this.mutateSelectedText(selection => selection.deleteToBeginningOfWord(options))
}
// Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the
// previous word boundary.
deleteToPreviousWordBoundary () {
this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToPreviousWordBoundary (options = {}) {
if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return
this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary(options))
}
// Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the
// next word boundary.
deleteToNextWordBoundary () {
this.mutateSelectedText(selection => selection.deleteToNextWordBoundary())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToNextWordBoundary (options = {}) {
if (!this.ensureWritable('deleteToNextWordBoundary', options)) return
this.mutateSelectedText(selection => selection.deleteToNextWordBoundary(options))
}
// Extended: For each selection, if the selection is empty, delete all characters
// of the containing subword following the cursor. Otherwise delete the selected
// text.
deleteToBeginningOfSubword () {
this.mutateSelectedText(selection => selection.deleteToBeginningOfSubword())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToBeginningOfSubword (options = {}) {
if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return
this.mutateSelectedText(selection => selection.deleteToBeginningOfSubword(options))
}
// Extended: For each selection, if the selection is empty, delete all characters
// of the containing subword following the cursor. Otherwise delete the selected
// text.
deleteToEndOfSubword () {
this.mutateSelectedText(selection => selection.deleteToEndOfSubword())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToEndOfSubword (options = {}) {
if (!this.ensureWritable('deleteToEndOfSubword', options)) return
this.mutateSelectedText(selection => selection.deleteToEndOfSubword(options))
}
// Extended: For each selection, if the selection is empty, delete all characters
// of the containing line that precede the cursor. Otherwise delete the
// selected text.
deleteToBeginningOfLine () {
this.mutateSelectedText(selection => selection.deleteToBeginningOfLine())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToBeginningOfLine (options = {}) {
if (!this.ensureWritable('deleteToBeginningOfLine', options)) return
this.mutateSelectedText(selection => selection.deleteToBeginningOfLine(options))
}
// Extended: For each selection, if the selection is not empty, deletes the
// selection; otherwise, deletes all characters of the containing line
// following the cursor. If the cursor is already at the end of the line,
// deletes the following newline.
deleteToEndOfLine () {
this.mutateSelectedText(selection => selection.deleteToEndOfLine())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToEndOfLine (options = {}) {
if (!this.ensureWritable('deleteToEndOfLine', options)) return
this.mutateSelectedText(selection => selection.deleteToEndOfLine(options))
}
// Extended: For each selection, if the selection is empty, delete all characters
// of the containing word following the cursor. Otherwise delete the selected
// text.
deleteToEndOfWord () {
this.mutateSelectedText(selection => selection.deleteToEndOfWord())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToEndOfWord (options = {}) {
if (!this.ensureWritable('deleteToEndOfWord', options)) return
this.mutateSelectedText(selection => selection.deleteToEndOfWord(options))
}
// Extended: Delete all lines intersecting selections.
deleteLine () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteLine (options = {}) {
if (!this.ensureWritable('deleteLine', options)) return
this.mergeSelectionsOnSameRows()
this.mutateSelectedText(selection => selection.deleteLine())
this.mutateSelectedText(selection => selection.deleteLine(options))
}
// Private: Ensure that this editor 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.isReadOnly()) {
if (atom.inDevMode() || atom.inSpecMode()) {
const e = new Error('Attempt to mutate a read-only TextEditor')
e.detail =
`Your package is attempting to call ${methodName} on 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
}
/*
@@ -1807,13 +1932,21 @@ class TextEditor {
*/
// Essential: Undo the last change.
undo () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
undo (options = {}) {
if (!this.ensureWritable('undo', options)) return
this.avoidMergingSelections(() => this.buffer.undo({selectionsMarkerLayer: this.selectionsMarkerLayer}))
this.getLastSelection().autoscroll()
}
// Essential: Redo the last change.
redo () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
redo (options = {}) {
if (!this.ensureWritable('redo', options)) return
this.avoidMergingSelections(() => this.buffer.redo({selectionsMarkerLayer: this.selectionsMarkerLayer}))
this.getLastSelection().autoscroll()
}
@@ -1967,11 +2100,11 @@ class TextEditor {
//
// ## Examples
//
// ```coffee
// editor.clipBufferPosition([-1, -1]) # -> `[0, 0]`
// ```js
// editor.clipBufferPosition([-1, -1]) // -> `[0, 0]`
//
// # When the line at buffer row 2 is 10 characters long
// editor.clipBufferPosition([2, Infinity]) # -> `[2, 10]`
// // When the line at buffer row 2 is 10 characters long
// editor.clipBufferPosition([2, Infinity]) // -> `[2, 10]`
// ```
//
// * `bufferPosition` The {Point} representing the position to clip.
@@ -1996,11 +2129,11 @@ class TextEditor {
//
// ## Examples
//
// ```coffee
// editor.clipScreenPosition([-1, -1]) # -> `[0, 0]`
// ```js
// editor.clipScreenPosition([-1, -1]) // -> `[0, 0]`
//
// # When the line at screen row 2 is 10 characters long
// editor.clipScreenPosition([2, Infinity]) # -> `[2, 10]`
// // When the line at screen row 2 is 10 characters long
// editor.clipScreenPosition([2, Infinity]) // -> `[2, 10]`
// ```
//
// * `screenPosition` The {Point} representing the position to clip.
@@ -3558,13 +3691,21 @@ class TextEditor {
}
// Extended: Indent rows intersecting selections by one level.
indentSelectedRows () {
return this.mutateSelectedText(selection => selection.indentSelectedRows())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
indentSelectedRows (options = {}) {
if (!this.ensureWritable('indentSelectedRows', options)) return
return this.mutateSelectedText(selection => selection.indentSelectedRows(options))
}
// Extended: Outdent rows intersecting selections by one level.
outdentSelectedRows () {
return this.mutateSelectedText(selection => selection.outdentSelectedRows())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
outdentSelectedRows (options = {}) {
if (!this.ensureWritable('outdentSelectedRows', options)) return
return this.mutateSelectedText(selection => selection.outdentSelectedRows(options))
}
// Extended: Get the indentation level of the given line of text.
@@ -3595,13 +3736,21 @@ class TextEditor {
// Extended: Indent rows intersecting selections based on the grammar's suggested
// indent level.
autoIndentSelectedRows () {
return this.mutateSelectedText(selection => selection.autoIndentSelectedRows())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
autoIndentSelectedRows (options = {}) {
if (!this.ensureWritable('autoIndentSelectedRows', options)) return
return this.mutateSelectedText(selection => selection.autoIndentSelectedRows(options))
}
// Indent all lines intersecting selections. See {Selection::indent} for more
// information.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
indent (options = {}) {
if (!this.ensureWritable('indent', options)) return
if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndent()
this.mutateSelectedText(selection => selection.indent(options))
}
@@ -3739,14 +3888,18 @@ class TextEditor {
}
// Essential: For each selection, cut the selected text.
cutSelectedText () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
cutSelectedText (options = {}) {
if (!this.ensureWritable('cutSelectedText', options)) return
let maintainClipboard = false
this.mutateSelectedText(selection => {
if (selection.isEmpty()) {
selection.selectLine()
selection.cut(maintainClipboard, true)
selection.cut(maintainClipboard, true, options.bypassReadOnly)
} else {
selection.cut(maintainClipboard, false)
selection.cut(maintainClipboard, false, options.bypassReadOnly)
}
maintainClipboard = true
})
@@ -3760,7 +3913,8 @@ class TextEditor {
// corresponding clipboard selection text.
//
// * `options` (optional) See {Selection::insertText}.
pasteText (options) {
pasteText (options = {}) {
if (!this.ensureWritable('parseText', options)) return
options = Object.assign({}, options)
let {text: clipboardText, metadata} = this.constructor.clipboard.readWithMetadata()
if (!this.emitWillInsertTextEvent(clipboardText)) return false
@@ -3801,10 +3955,14 @@ class TextEditor {
// Essential: For each selection, if the selection is empty, cut all characters
// of the containing screen line following the cursor. Otherwise cut the selected
// text.
cutToEndOfLine () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
cutToEndOfLine (options = {}) {
if (!this.ensureWritable('cutToEndOfLine', options)) return
let maintainClipboard = false
this.mutateSelectedText(selection => {
selection.cutToEndOfLine(maintainClipboard)
selection.cutToEndOfLine(maintainClipboard, options)
maintainClipboard = true
})
}
@@ -3812,10 +3970,14 @@ class TextEditor {
// Essential: For each selection, if the selection is empty, cut all characters
// of the containing buffer line following the cursor. Otherwise cut the
// selected text.
cutToEndOfBufferLine () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
cutToEndOfBufferLine (options = {}) {
if (!this.ensureWritable('cutToEndOfBufferLine', options)) return
let maintainClipboard = false
this.mutateSelectedText(selection => {
selection.cutToEndOfBufferLine(maintainClipboard)
selection.cutToEndOfBufferLine(maintainClipboard, options)
maintainClipboard = true
})
}