From 08ca84e58732e10bece209dd2d677d33b18838d7 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 20 Mar 2018 09:37:47 -0400 Subject: [PATCH 01/11] :memo: Convert code samples to js --- src/text-editor.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/text-editor.js b/src/text-editor.js index 9bfa8ff3e..c8a308bf4 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -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 @@ -1957,11 +1958,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. @@ -1986,11 +1987,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. From 427460552d0fd99a2080d15620dd4a8324b8dd3b Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 20 Mar 2018 10:19:56 -0400 Subject: [PATCH 02/11] Don't register modifying commands for readonly TextEditors --- src/register-default-commands.coffee | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index a367e6188..7f1503b73 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -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, { From 0f30f8d569074329b77bea2c6d224380c9deedfa Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 20 Mar 2018 15:34:17 -0400 Subject: [PATCH 03/11] Specs for calling buffer modification methods on read-only editors --- spec/text-editor-spec.js | 189 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 69be6be32..9711de7ff 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -5383,6 +5383,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() + } + }, + { + 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() + } + }, + { + name: 'autoIndentSelectedRows', + op: (opts = {}) => { + editor.setCursorBufferPosition([2, 0]) + editor.insertText('function() {\ninside=true\n}\n i=1\n') + editor.getLastSelection().setBufferRange([[2, 0], [6, 0]]) + editor.autoIndentSelectedRows(opts) + } + }, + { + name: 'undo/redo', + op: (opts = {}) => { + editor.insertText('foo') + 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}`, () => { + expect(() => op({bypassReadOnly: true})).not.toThrow() + }) + } + }) + }) }) describe('reading text', () => { From 1f866bd592d4652d482e8e0ed63e42881ae2495a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Tue, 20 Mar 2018 16:38:15 -0400 Subject: [PATCH 04/11] Add options.bypassReadOnly on methods that modify the underlying Buffer --- src/text-editor.js | 262 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 206 insertions(+), 56 deletions(-) diff --git a/src/text-editor.js b/src/text-editor.js index c8a308bf4..4956b9514 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -1307,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 = {}) { + this.ensureWritable('setText', options) + return this.buffer.setText(text) + } // Essential: Set the text in the given {Range} in buffer coordinates. // @@ -1316,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 = {}) { + this.ensureWritable('setTextInBufferRange', options) return this.getBuffer().setTextInRange(range, text, options) } @@ -1327,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 = {}) { + this.ensureWritable('insertText', options) if (!this.emitWillInsertTextEvent(text)) return false let groupLastChanges = false @@ -1353,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 = {}) { + this.ensureWritable('delete', options) + 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 = {}) { + this.ensureWritable('backspace', options) + return this.mutateSelectedText(selection => selection.backspace(options)) } // Extended: Mutate the text of all the selections in a single transaction. @@ -1387,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 = {}) { + this.ensureWritable('moveLineUp', options) + const selections = this.getSelectedBufferRanges().sort((a, b) => a.compare(b)) if (selections[0].start.row === 0) return @@ -1455,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 = {}) { + this.ensureWritable('moveLineDown', options) + const selections = this.getSelectedBufferRanges() selections.sort((a, b) => b.compare(a)) @@ -1527,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 = {}) { + this.ensureWritable('moveSelectionLeft', options) const selections = this.getSelectedBufferRanges() const noSelectionAtStartOfLine = selections.every(selection => selection.start.column !== 0) @@ -1551,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 = {}) { + this.ensureWritable('moveSelectionRight', options) const selections = this.getSelectedBufferRanges() const noSelectionAtEndOfLine = selections.every(selection => { return selection.end.column !== this.buffer.lineLengthForRow(selection.end.row) @@ -1576,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 = {}) { + this.ensureWritable('duplicateLines', options) this.transact(() => { const selections = this.getSelectionsOrderedByBufferPosition() const previousSelectionRanges = [] @@ -1663,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 = {}) { + this.ensureWritable('transpose', options) this.mutateSelectedText(selection => { if (selection.isEmpty()) { selection.selectRight() @@ -1681,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 = {}) { + this.ensureWritable('upperCase', options) + 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 = {}) { + this.ensureWritable('lowerCase', options) + 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 = {}) { + this.ensureWritable('toggleLineCommentsInSelection', options) + this.mutateSelectedText(selection => selection.toggleLineComments(options)) } // Convert multiple lines to a single line. @@ -1708,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 = {}) { + this.ensureWritable('joinLines', options) 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 = {}) { + this.ensureWritable('insertNewlineBelow', options) 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 = {}) { + this.ensureWritable('insertNewlineAbove', options) this.transact(() => { const bufferRow = this.getCursorBufferPosition().row const indentLevel = this.indentationForBufferRow(bufferRow) @@ -1729,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) @@ -1745,62 +1814,111 @@ 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 = {}) { + this.ensureWritable('deleteToBeginningOfWord', options) + 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 = {}) { + this.ensureWritable('deleteToPreviousWordBoundary', options) + 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 = {}) { + this.ensureWritable('deleteToNextWordBoundary', options) + 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 = {}) { + this.ensureWritable('deleteToBeginningOfSubword', options) + 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 = {}) { + this.ensureWritable('deleteToEndOfSubword', options) + 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 = {}) { + this.ensureWritable('deleteToBeginningOfLine', options) + 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 = {}) { + this.ensureWritable('deleteToEndOfLine', options) + 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 = {}) { + this.ensureWritable('deleteToEndOfWord', options) + 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 = {}) { + this.ensureWritable('deleteLine', options) 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()) { + 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 + } } /* @@ -1808,13 +1926,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 = {}) { + this.ensureWritable('undo', options) this.avoidMergingSelections(() => this.buffer.undo()) 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 = {}) { + this.ensureWritable('redo', options) this.avoidMergingSelections(() => this.buffer.redo()) this.getLastSelection().autoscroll() } @@ -3549,13 +3675,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 = {}) { + this.ensureWritable('indentSelectedRows', options) + 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 = {}) { + this.ensureWritable('outdentSelectedRows', options) + return this.mutateSelectedText(selection => selection.outdentSelectedRows(options)) } // Extended: Get the indentation level of the given line of text. @@ -3586,8 +3720,11 @@ 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 = {}) { + return this.mutateSelectedText(selection => selection.autoIndentSelectedRows(options)) } // Indent all lines intersecting selections. See {Selection::indent} for more @@ -3730,7 +3867,11 @@ 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 = {}) { + this.ensureWritable('cutSelectedText', options) let maintainClipboard = false this.mutateSelectedText(selection => { if (selection.isEmpty()) { @@ -3751,7 +3892,8 @@ class TextEditor { // corresponding clipboard selection text. // // * `options` (optional) See {Selection::insertText}. - pasteText (options) { + pasteText (options = {}) { + this.ensureWritable('parseText', options) options = Object.assign({}, options) let {text: clipboardText, metadata} = this.constructor.clipboard.readWithMetadata() if (!this.emitWillInsertTextEvent(clipboardText)) return false @@ -3792,7 +3934,11 @@ 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 = {}) { + this.ensureWritable('cutToEndOfLine', options) let maintainClipboard = false this.mutateSelectedText(selection => { selection.cutToEndOfLine(maintainClipboard) @@ -3803,7 +3949,11 @@ 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 = {}) { + this.ensureWritable('cutToEndOfBufferLine', options) let maintainClipboard = false this.mutateSelectedText(selection => { selection.cutToEndOfBufferLine(maintainClipboard) From 56a3f1bc4169bddc29d293b85d9e9cbda90d7f34 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Mar 2018 09:29:36 -0400 Subject: [PATCH 05/11] Fill in missing ensureWritable() calls caught by the specs --- spec/text-editor-spec.js | 10 +++++----- src/text-editor.js | 9 +++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 9711de7ff..c2bb911b5 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -5512,7 +5512,7 @@ describe('TextEditor', () => { name: 'cutToEndOfBufferLine', op: (opts = {}) => { editor.setCursorBufferPosition([2, 20]) - editor.cutToEndOfBufferLine() + editor.cutToEndOfBufferLine(opts) } }, { @@ -5534,14 +5534,14 @@ describe('TextEditor', () => { name: 'outdentSelectedRows', op: (opts = {}) => { editor.setSelectedBufferRange([[1, 3], [1, 3]]) - editor.outdentSelectedRows() + editor.outdentSelectedRows(opts) } }, { name: 'autoIndentSelectedRows', op: (opts = {}) => { editor.setCursorBufferPosition([2, 0]) - editor.insertText('function() {\ninside=true\n}\n i=1\n') + editor.insertText('function() {\ninside=true\n}\n i=1\n', opts) editor.getLastSelection().setBufferRange([[2, 0], [6, 0]]) editor.autoIndentSelectedRows(opts) } @@ -5549,7 +5549,7 @@ describe('TextEditor', () => { { name: 'undo/redo', op: (opts = {}) => { - editor.insertText('foo') + editor.insertText('foo', opts) editor.undo(opts) editor.redo(opts) } @@ -5567,7 +5567,7 @@ describe('TextEditor', () => { describe('with bypassReadOnly', () => { for (const {name, op} of modifications) { it(`permits ${name}`, () => { - expect(() => op({bypassReadOnly: true})).not.toThrow() + op({bypassReadOnly: true}) }) } }) diff --git a/src/text-editor.js b/src/text-editor.js index 4956b9514..8424edf45 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -3724,12 +3724,17 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. autoIndentSelectedRows (options = {}) { + this.ensureWritable('autoIndentSelectedRows', options) 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 = {}) { + this.ensureWritable('indent', options) if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndent() this.mutateSelectedText(selection => selection.indent(options)) } @@ -3941,7 +3946,7 @@ class TextEditor { this.ensureWritable('cutToEndOfLine', options) let maintainClipboard = false this.mutateSelectedText(selection => { - selection.cutToEndOfLine(maintainClipboard) + selection.cutToEndOfLine(maintainClipboard, options) maintainClipboard = true }) } @@ -3956,7 +3961,7 @@ class TextEditor { this.ensureWritable('cutToEndOfBufferLine', options) let maintainClipboard = false this.mutateSelectedText(selection => { - selection.cutToEndOfBufferLine(maintainClipboard) + selection.cutToEndOfBufferLine(maintainClipboard, options) maintainClipboard = true }) } From cf576a0a7e2881035218a87eedb4db7c66b0ba47 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Mar 2018 11:52:47 -0400 Subject: [PATCH 06/11] Verify Buffer-modifying methods on Selection --- spec/selection-spec.js | 114 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/spec/selection-spec.js b/spec/selection-spec.js index cb586da26..8afc67575 100644 --- a/spec/selection-spec.js +++ b/spec/selection-spec.js @@ -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}) + }) + } + }) + }) }) From 71d12f3f2ce22b2af8285d9c66a40094aad3ecb4 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Mar 2018 11:53:15 -0400 Subject: [PATCH 07/11] Guard Selection methods against read-only TextEditor modification --- src/selection.js | 180 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 139 insertions(+), 41 deletions(-) diff --git a/src/selection.js b/src/selection.js index 2c64fa126..e41358fa9 100644 --- a/src/selection.js +++ b/src/selection.js @@ -407,6 +407,19 @@ 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()) { + 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 + } + } + /* Section: Modifying the selected text */ @@ -428,7 +441,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 = {}) { + this.ensureWritable('insertText', options) + let desiredIndentLevel, indentAdjustment const oldBufferRange = this.getBufferRange() const wasReversed = this.isReversed() @@ -492,90 +508,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 = {}) { + this.ensureWritable('backspace', options) 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 = {}) { + this.ensureWritable('deleteToPreviousWordBoundary', options) 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 = {}) { + this.ensureWritable('deleteToNextWordBoundary', options) 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 = {}) { + this.ensureWritable('deleteToBeginningOfWord', options) 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 = {}) { + this.ensureWritable('deleteToBeginningOfLine', options) 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 = {}) { + this.ensureWritable('delete', options) 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 = {}) { + this.ensureWritable('deleteToEndOfLine', options) 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 = {}) { + this.ensureWritable('deleteToEndOfWord', options) 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 = {}) { + this.ensureWritable('deleteToBeginningOfSubword', options) 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 = {}) { + this.ensureWritable('deleteToEndOfSubword', options) 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 = {}) { + this.ensureWritable('deleteSelectedText', options) const bufferRange = this.getBufferRange() if (!bufferRange.isEmpty()) this.editor.buffer.delete(bufferRange) if (this.cursor) this.cursor.setBufferPosition(bufferRange.start) @@ -584,7 +644,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 = {}) { + this.ensureWritable('deleteLine', options) const range = this.getBufferRange() if (range.isEmpty()) { const start = this.cursor.getScreenRow() @@ -607,7 +671,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 = {}) { + this.ensureWritable('joinLines', options) let joinMarker const selectedRange = this.getBufferRange() if (selectedRange.isEmpty()) { @@ -629,7 +697,7 @@ class Selection { }) if (trailingWhitespaceRange) { this.setBufferRange(trailingWhitespaceRange) - this.deleteSelectedText() + this.deleteSelectedText(options) } const currentRow = selectedRange.start.row @@ -638,7 +706,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 +715,7 @@ class Selection { this.cursor.moveRight() this.cursor.moveToFirstCharacterOfLine() }) - this.deleteSelectedText() + this.deleteSelectedText(options) if (insertSpace) this.cursor.moveLeft() } @@ -660,7 +728,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 = {}) { + this.ensureWritable('outdentSelectedRows', options) const [start, end] = this.getBufferRowRange() const {buffer} = this.editor const leadingTabRegex = new RegExp(`^( {1,${this.editor.getTabLength()}}|\t)`) @@ -674,7 +746,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 = {}) { + this.ensureWritable('autoIndentSelectedRows', options) const [start, end] = this.getBufferRowRange() return this.editor.autoIndentBufferRows(start, end) } @@ -683,29 +759,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 = {}) { + this.ensureWritable('toggleLineComments', options) 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 = {}) { + this.ensureWritable('cutToEndOfLine', options) 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 = {}) { + this.ensureWritable('cutToEndOfBufferLine', options) 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) { + this.ensureWritable('cut', {bypassReadOnly}) this.copy(maintainClipboard, fullLine) - this.delete() + this.delete({bypassReadOnly}) } // Public: Copies the current selection to the clipboard. @@ -783,7 +875,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} = {}) { + this.ensureWritable('indent', {bypassReadOnly}) const {row} = this.cursor.getBufferPosition() if (this.isEmpty()) { @@ -793,17 +887,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 = {}) { + this.ensureWritable('indentSelectedRows', options) const [start, end] = this.getBufferRowRange() for (let row = start; row <= end; row++) { if (this.editor.buffer.lineLengthForRow(row) !== 0) { From d405039581396d3b7f0b845f0481ada15c9a8055 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Mar 2018 11:53:36 -0400 Subject: [PATCH 08/11] Pass {bypassReadOnly} to selection.cut() --- src/text-editor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/text-editor.js b/src/text-editor.js index 8424edf45..5c8749cc1 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -3881,9 +3881,9 @@ class TextEditor { 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 }) From 82575f5b07de227c31cfbfb5dec901910bf66ed2 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Mar 2018 12:29:52 -0400 Subject: [PATCH 09/11] :shirt: --- src/selection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/selection.js b/src/selection.js index e41358fa9..267c87d90 100644 --- a/src/selection.js +++ b/src/selection.js @@ -414,7 +414,7 @@ class Selection { 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 ' + ' read-only. Pass {bypassReadOnly: true} to modify it anyway, or test editors with .isReadOnly() before ' + ' attempting modifications.' throw e } From ebb76479033a0ef782e987b0f7165d682996b905 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Mar 2018 14:25:50 -0400 Subject: [PATCH 10/11] Mutation methods are no-ops in production. Throw in dev or spec mode --- src/text-editor.js | 90 ++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/src/text-editor.js b/src/text-editor.js index d817d9713..5e0984802 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -1310,7 +1310,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. setText (text, options = {}) { - this.ensureWritable('setText', options) + if (!this.ensureWritable('setText', options)) return return this.buffer.setText(text) } @@ -1325,7 +1325,7 @@ class TextEditor { // // Returns the {Range} of the newly-inserted text. setTextInBufferRange (range, text, options = {}) { - this.ensureWritable('setTextInBufferRange', options) + if (!this.ensureWritable('setTextInBufferRange', options)) return return this.getBuffer().setTextInRange(range, text, options) } @@ -1336,7 +1336,7 @@ class TextEditor { // // Returns a {Range} when the text has been inserted. Returns a {Boolean} `false` when the text has not been inserted. insertText (text, options = {}) { - this.ensureWritable('insertText', options) + if (!this.ensureWritable('insertText', options)) return if (!this.emitWillInsertTextEvent(text)) return false let groupLastChanges = false @@ -1373,7 +1373,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) delete (options = {}) { - this.ensureWritable('delete', options) + if (!this.ensureWritable('delete', options)) return return this.mutateSelectedText(selection => selection.delete(options)) } @@ -1383,7 +1383,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) backspace (options = {}) { - this.ensureWritable('backspace', options) + if (!this.ensureWritable('backspace', options)) return return this.mutateSelectedText(selection => selection.backspace(options)) } @@ -1409,7 +1409,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) moveLineUp (options = {}) { - this.ensureWritable('moveLineUp', options) + if (!this.ensureWritable('moveLineUp', options)) return const selections = this.getSelectedBufferRanges().sort((a, b) => a.compare(b)) @@ -1482,7 +1482,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) moveLineDown (options = {}) { - this.ensureWritable('moveLineDown', options) + if (!this.ensureWritable('moveLineDown', options)) return const selections = this.getSelectedBufferRanges() selections.sort((a, b) => b.compare(a)) @@ -1559,7 +1559,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) moveSelectionLeft (options = {}) { - this.ensureWritable('moveSelectionLeft', options) + if (!this.ensureWritable('moveSelectionLeft', options)) return const selections = this.getSelectedBufferRanges() const noSelectionAtStartOfLine = selections.every(selection => selection.start.column !== 0) @@ -1587,7 +1587,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) moveSelectionRight (options = {}) { - this.ensureWritable('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) @@ -1617,7 +1617,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) duplicateLines (options = {}) { - this.ensureWritable('duplicateLines', options) + if (!this.ensureWritable('duplicateLines', options)) return this.transact(() => { const selections = this.getSelectionsOrderedByBufferPosition() const previousSelectionRanges = [] @@ -1708,7 +1708,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) transpose (options = {}) { - this.ensureWritable('transpose', options) + if (!this.ensureWritable('transpose', options)) return this.mutateSelectedText(selection => { if (selection.isEmpty()) { selection.selectRight() @@ -1730,7 +1730,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) upperCase (options = {}) { - this.ensureWritable('upperCase', options) + if (!this.ensureWritable('upperCase', options)) return this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toUpperCase(options)) } @@ -1742,7 +1742,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) lowerCase (options = {}) { - this.ensureWritable('lowerCase', options) + if (!this.ensureWritable('lowerCase', options)) return this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toLowerCase(options)) } @@ -1753,7 +1753,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) toggleLineCommentsInSelection (options = {}) { - this.ensureWritable('toggleLineCommentsInSelection', options) + if (!this.ensureWritable('toggleLineCommentsInSelection', options)) return this.mutateSelectedText(selection => selection.toggleLineComments(options)) } @@ -1769,7 +1769,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) joinLines (options = {}) { - this.ensureWritable('joinLines', options) + if (!this.ensureWritable('joinLines', options)) return this.mutateSelectedText(selection => selection.joinLines()) } @@ -1778,7 +1778,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) insertNewlineBelow (options = {}) { - this.ensureWritable('insertNewlineBelow', options) + if (!this.ensureWritable('insertNewlineBelow', options)) return this.transact(() => { this.moveToEndOfLine() this.insertNewline(options) @@ -1790,7 +1790,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) insertNewlineAbove (options = {}) { - this.ensureWritable('insertNewlineAbove', options) + if (!this.ensureWritable('insertNewlineAbove', options)) return this.transact(() => { const bufferRow = this.getCursorBufferPosition().row const indentLevel = this.indentationForBufferRow(bufferRow) @@ -1818,7 +1818,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToBeginningOfWord (options = {}) { - this.ensureWritable('deleteToBeginningOfWord', options) + if (!this.ensureWritable('deleteToBeginningOfWord', options)) return this.mutateSelectedText(selection => selection.deleteToBeginningOfWord(options)) } @@ -1828,7 +1828,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToPreviousWordBoundary (options = {}) { - this.ensureWritable('deleteToPreviousWordBoundary', options) + if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary(options)) } @@ -1838,7 +1838,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToNextWordBoundary (options = {}) { - this.ensureWritable('deleteToNextWordBoundary', options) + if (!this.ensureWritable('deleteToNextWordBoundary', options)) return this.mutateSelectedText(selection => selection.deleteToNextWordBoundary(options)) } @@ -1849,7 +1849,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToBeginningOfSubword (options = {}) { - this.ensureWritable('deleteToBeginningOfSubword', options) + if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return this.mutateSelectedText(selection => selection.deleteToBeginningOfSubword(options)) } @@ -1860,7 +1860,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToEndOfSubword (options = {}) { - this.ensureWritable('deleteToEndOfSubword', options) + if (!this.ensureWritable('deleteToEndOfSubword', options)) return this.mutateSelectedText(selection => selection.deleteToEndOfSubword(options)) } @@ -1871,7 +1871,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToBeginningOfLine (options = {}) { - this.ensureWritable('deleteToBeginningOfLine', options) + if (!this.ensureWritable('deleteToBeginningOfLine', options)) return this.mutateSelectedText(selection => selection.deleteToBeginningOfLine(options)) } @@ -1883,7 +1883,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToEndOfLine (options = {}) { - this.ensureWritable('deleteToEndOfLine', options) + if (!this.ensureWritable('deleteToEndOfLine', options)) return this.mutateSelectedText(selection => selection.deleteToEndOfLine(options)) } @@ -1894,7 +1894,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteToEndOfWord (options = {}) { - this.ensureWritable('deleteToEndOfWord', options) + if (!this.ensureWritable('deleteToEndOfWord', options)) return this.mutateSelectedText(selection => selection.deleteToEndOfWord(options)) } @@ -1903,7 +1903,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) deleteLine (options = {}) { - this.ensureWritable('deleteLine', options) + if (!this.ensureWritable('deleteLine', options)) return this.mergeSelectionsOnSameRows() this.mutateSelectedText(selection => selection.deleteLine(options)) } @@ -1912,13 +1912,19 @@ class TextEditor { // 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()) { - 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 + 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 } /* @@ -1930,7 +1936,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) undo (options = {}) { - this.ensureWritable('undo', options) + if (!this.ensureWritable('undo', options)) return this.avoidMergingSelections(() => this.buffer.undo({selectionsMarkerLayer: this.selectionsMarkerLayer})) this.getLastSelection().autoscroll() } @@ -1940,7 +1946,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false) redo (options = {}) { - this.ensureWritable('redo', options) + if (!this.ensureWritable('redo', options)) return this.avoidMergingSelections(() => this.buffer.redo({selectionsMarkerLayer: this.selectionsMarkerLayer})) this.getLastSelection().autoscroll() } @@ -3689,7 +3695,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. indentSelectedRows (options = {}) { - this.ensureWritable('indentSelectedRows', options) + if (!this.ensureWritable('indentSelectedRows', options)) return return this.mutateSelectedText(selection => selection.indentSelectedRows(options)) } @@ -3698,7 +3704,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. outdentSelectedRows (options = {}) { - this.ensureWritable('outdentSelectedRows', options) + if (!this.ensureWritable('outdentSelectedRows', options)) return return this.mutateSelectedText(selection => selection.outdentSelectedRows(options)) } @@ -3734,7 +3740,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. autoIndentSelectedRows (options = {}) { - this.ensureWritable('autoIndentSelectedRows', options) + if (!this.ensureWritable('autoIndentSelectedRows', options)) return return this.mutateSelectedText(selection => selection.autoIndentSelectedRows(options)) } @@ -3744,7 +3750,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. indent (options = {}) { - this.ensureWritable('indent', options) + if (!this.ensureWritable('indent', options)) return if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndent() this.mutateSelectedText(selection => selection.indent(options)) } @@ -3886,7 +3892,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. cutSelectedText (options = {}) { - this.ensureWritable('cutSelectedText', options) + if (!this.ensureWritable('cutSelectedText', options)) return let maintainClipboard = false this.mutateSelectedText(selection => { if (selection.isEmpty()) { @@ -3908,7 +3914,7 @@ class TextEditor { // // * `options` (optional) See {Selection::insertText}. pasteText (options = {}) { - this.ensureWritable('parseText', 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 @@ -3953,7 +3959,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. cutToEndOfLine (options = {}) { - this.ensureWritable('cutToEndOfLine', options) + if (!this.ensureWritable('cutToEndOfLine', options)) return let maintainClipboard = false this.mutateSelectedText(selection => { selection.cutToEndOfLine(maintainClipboard, options) @@ -3968,7 +3974,7 @@ class TextEditor { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. cutToEndOfBufferLine (options = {}) { - this.ensureWritable('cutToEndOfBufferLine', options) + if (!this.ensureWritable('cutToEndOfBufferLine', options)) return let maintainClipboard = false this.mutateSelectedText(selection => { selection.cutToEndOfBufferLine(maintainClipboard, options) From 9de813173137070ea509a224e38305d014030bcb Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Wed, 21 Mar 2018 14:30:18 -0400 Subject: [PATCH 11/11] Ditto for Selection methods --- src/selection.js | 62 ++++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/src/selection.js b/src/selection.js index 267c87d90..b41095e26 100644 --- a/src/selection.js +++ b/src/selection.js @@ -411,13 +411,19 @@ class Selection { // 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()) { - 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 + 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 } /* @@ -443,7 +449,7 @@ class Selection { // * `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 = {}) { - this.ensureWritable('insertText', options) + if (!this.ensureWritable('insertText', options)) return let desiredIndentLevel, indentAdjustment const oldBufferRange = this.getBufferRange() @@ -512,7 +518,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) backspace (options = {}) { - this.ensureWritable('backspace', options) + if (!this.ensureWritable('backspace', options)) return if (this.isEmpty()) this.selectLeft() this.deleteSelectedText(options) } @@ -524,7 +530,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToPreviousWordBoundary (options = {}) { - this.ensureWritable('deleteToPreviousWordBoundary', options) + if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return if (this.isEmpty()) this.selectToPreviousWordBoundary() this.deleteSelectedText(options) } @@ -536,7 +542,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToNextWordBoundary (options = {}) { - this.ensureWritable('deleteToNextWordBoundary', options) + if (!this.ensureWritable('deleteToNextWordBoundary', options)) return if (this.isEmpty()) this.selectToNextWordBoundary() this.deleteSelectedText(options) } @@ -547,7 +553,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToBeginningOfWord (options = {}) { - this.ensureWritable('deleteToBeginningOfWord', options) + if (!this.ensureWritable('deleteToBeginningOfWord', options)) return if (this.isEmpty()) this.selectToBeginningOfWord() this.deleteSelectedText(options) } @@ -558,7 +564,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToBeginningOfLine (options = {}) { - this.ensureWritable('deleteToBeginningOfLine', options) + if (!this.ensureWritable('deleteToBeginningOfLine', options)) return if (this.isEmpty() && this.cursor.isAtBeginningOfLine()) { this.selectLeft() } else { @@ -573,7 +579,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) delete (options = {}) { - this.ensureWritable('delete', options) + if (!this.ensureWritable('delete', options)) return if (this.isEmpty()) this.selectRight() this.deleteSelectedText(options) } @@ -586,7 +592,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToEndOfLine (options = {}) { - this.ensureWritable('deleteToEndOfLine', options) + if (!this.ensureWritable('deleteToEndOfLine', options)) return if (this.isEmpty()) { if (this.cursor.isAtEndOfLine()) { this.delete(options) @@ -603,7 +609,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToEndOfWord (options = {}) { - this.ensureWritable('deleteToEndOfWord', options) + if (!this.ensureWritable('deleteToEndOfWord', options)) return if (this.isEmpty()) this.selectToEndOfWord() this.deleteSelectedText(options) } @@ -614,7 +620,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToBeginningOfSubword (options = {}) { - this.ensureWritable('deleteToBeginningOfSubword', options) + if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return if (this.isEmpty()) this.selectToPreviousSubwordBoundary() this.deleteSelectedText(options) } @@ -625,7 +631,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteToEndOfSubword (options = {}) { - this.ensureWritable('deleteToEndOfSubword', options) + if (!this.ensureWritable('deleteToEndOfSubword', options)) return if (this.isEmpty()) this.selectToNextSubwordBoundary() this.deleteSelectedText(options) } @@ -635,7 +641,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteSelectedText (options = {}) { - this.ensureWritable('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) @@ -648,7 +654,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) deleteLine (options = {}) { - this.ensureWritable('deleteLine', options) + if (!this.ensureWritable('deleteLine', options)) return const range = this.getBufferRange() if (range.isEmpty()) { const start = this.cursor.getScreenRow() @@ -675,7 +681,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) joinLines (options = {}) { - this.ensureWritable('joinLines', options) + if (!this.ensureWritable('joinLines', options)) return let joinMarker const selectedRange = this.getBufferRange() if (selectedRange.isEmpty()) { @@ -732,7 +738,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) outdentSelectedRows (options = {}) { - this.ensureWritable('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)`) @@ -750,7 +756,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) autoIndentSelectedRows (options = {}) { - this.ensureWritable('autoIndentSelectedRows', options) + if (!this.ensureWritable('autoIndentSelectedRows', options)) return const [start, end] = this.getBufferRowRange() return this.editor.autoIndentBufferRows(start, end) } @@ -763,7 +769,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) toggleLineComments (options = {}) { - this.ensureWritable('toggleLineComments', options) + if (!this.ensureWritable('toggleLineComments', options)) return this.editor.toggleLineCommentsForBufferRows(...(this.getBufferRowRange() || [])) } @@ -773,7 +779,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) cutToEndOfLine (maintainClipboard, options = {}) { - this.ensureWritable('cutToEndOfLine', options) + if (!this.ensureWritable('cutToEndOfLine', options)) return if (this.isEmpty()) this.selectToEndOfLine() return this.cut(maintainClipboard, false, options.bypassReadOnly) } @@ -784,7 +790,7 @@ class Selection { // * `options` (optional) {Object} // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) cutToEndOfBufferLine (maintainClipboard, options = {}) { - this.ensureWritable('cutToEndOfBufferLine', options) + if (!this.ensureWritable('cutToEndOfBufferLine', options)) return if (this.isEmpty()) this.selectToEndOfBufferLine() this.cut(maintainClipboard, false, options.bypassReadOnly) } @@ -795,7 +801,7 @@ class Selection { // * `fullLine` {Boolean} (default: false) See {::copy} // * `bypassReadOnly` {Boolean} (default: false) Must be `true` to modify text within a read-only editor. cut (maintainClipboard = false, fullLine = false, bypassReadOnly = false) { - this.ensureWritable('cut', {bypassReadOnly}) + if (!this.ensureWritable('cut', {bypassReadOnly})) return this.copy(maintainClipboard, fullLine) this.delete({bypassReadOnly}) } @@ -877,7 +883,7 @@ class Selection { // level. Otherwise, {TextEditor::getTabText} is inserted. // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) indent ({autoIndent, bypassReadOnly} = {}) { - this.ensureWritable('indent', {bypassReadOnly}) + if (!this.ensureWritable('indent', {bypassReadOnly})) return const {row} = this.cursor.getBufferPosition() if (this.isEmpty()) { @@ -901,7 +907,7 @@ class Selection { // * `options` (optional) {Object} with the keys: // * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false) indentSelectedRows (options = {}) { - this.ensureWritable('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) {