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