From e9e23a2d09832f578874020ed10deada2c954eb6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 15:19:52 -0700 Subject: [PATCH 1/6] Convert text-editor.coffee to JS Signed-off-by: Nathan Sobo --- src/selection.coffee | 2 +- src/text-editor-utils.js | 139 -- src/text-editor.coffee | 3928 -------------------------------- src/text-editor.js | 4587 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 4588 insertions(+), 4068 deletions(-) delete mode 100644 src/text-editor-utils.js delete mode 100644 src/text-editor.coffee create mode 100644 src/text-editor.js diff --git a/src/selection.coffee b/src/selection.coffee index cb45286b8..e55f17e88 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -735,7 +735,7 @@ class Selection extends Model # # * `otherSelection` A {Selection} to merge with. # * `options` (optional) {Object} options matching those found in {::setBufferRange}. - merge: (otherSelection, options) -> + merge: (otherSelection, options = {}) -> myGoalScreenRange = @getGoalScreenRange() otherGoalScreenRange = otherSelection.getGoalScreenRange() diff --git a/src/text-editor-utils.js b/src/text-editor-utils.js deleted file mode 100644 index ab1104144..000000000 --- a/src/text-editor-utils.js +++ /dev/null @@ -1,139 +0,0 @@ -// This file is temporary. We should gradually convert methods in `text-editor.coffee` -// from CoffeeScript to JavaScript and move them here, so that we can eventually convert -// the entire class to JavaScript. - -const {Point, Range} = require('text-buffer') - -const NON_WHITESPACE_REGEX = /\S/ - -module.exports = { - toggleLineCommentsForBufferRows (start, end) { - let { - commentStartString, - commentEndString - } = this.tokenizedBuffer.commentStringsForPosition(Point(start, 0)) - if (!commentStartString) return - commentStartString = commentStartString.trim() - - if (commentEndString) { - commentEndString = commentEndString.trim() - const startDelimiterColumnRange = columnRangeForStartDelimiter( - this.buffer.lineForRow(start), - commentStartString - ) - if (startDelimiterColumnRange) { - const endDelimiterColumnRange = columnRangeForEndDelimiter( - this.buffer.lineForRow(end), - commentEndString - ) - if (endDelimiterColumnRange) { - this.buffer.transact(() => { - this.buffer.delete([[end, endDelimiterColumnRange[0]], [end, endDelimiterColumnRange[1]]]) - this.buffer.delete([[start, startDelimiterColumnRange[0]], [start, startDelimiterColumnRange[1]]]) - }) - } - } else { - this.buffer.transact(() => { - const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length - this.buffer.insert([start, indentLength], commentStartString + ' ') - this.buffer.insert([end, this.buffer.lineLengthForRow(end)], ' ' + commentEndString) - }) - } - } else { - let hasCommentedLines = false - let hasUncommentedLines = false - for (let row = start; row <= end; row++) { - const line = this.buffer.lineForRow(row) - if (NON_WHITESPACE_REGEX.test(line)) { - if (columnRangeForStartDelimiter(line, commentStartString)) { - hasCommentedLines = true - } else { - hasUncommentedLines = true - } - } - } - - const shouldUncomment = hasCommentedLines && !hasUncommentedLines - - if (shouldUncomment) { - for (let row = start; row <= end; row++) { - const columnRange = columnRangeForStartDelimiter( - this.buffer.lineForRow(row), - commentStartString - ) - if (columnRange) this.buffer.delete([[row, columnRange[0]], [row, columnRange[1]]]) - } - } else { - let minIndentLevel = Infinity - let minBlankIndentLevel = Infinity - for (let row = start; row <= end; row++) { - const line = this.buffer.lineForRow(row) - const indentLevel = this.indentLevelForLine(line) - if (NON_WHITESPACE_REGEX.test(line)) { - if (indentLevel < minIndentLevel) minIndentLevel = indentLevel - } else { - if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel - } - } - minIndentLevel = Number.isFinite(minIndentLevel) - ? minIndentLevel - : Number.isFinite(minBlankIndentLevel) - ? minBlankIndentLevel - : 0 - - const tabLength = this.getTabLength() - const indentString = ' '.repeat(tabLength * minIndentLevel) - for (let row = start; row <= end; row++) { - const line = this.buffer.lineForRow(row) - if (NON_WHITESPACE_REGEX.test(line)) { - const indentColumn = columnForIndentLevel(line, minIndentLevel, this.getTabLength()) - this.buffer.insert(Point(row, indentColumn), commentStartString + ' ') - } else { - this.buffer.setTextInRange( - new Range(new Point(row, 0), new Point(row, Infinity)), - indentString + commentStartString + ' ' - ) - } - } - } - } - } -} - -function columnForIndentLevel (line, indentLevel, tabLength) { - let column = 0 - let indentLength = 0 - const goalIndentLength = indentLevel * tabLength - while (indentLength < goalIndentLength) { - const char = line[column] - if (char === '\t') { - indentLength += tabLength - (indentLength % tabLength) - } else if (char === ' ') { - indentLength++ - } else { - break - } - column++ - } - return column -} - -function columnRangeForStartDelimiter (line, delimiter) { - const startColumn = line.search(NON_WHITESPACE_REGEX) - if (startColumn === -1) return null - if (!line.startsWith(delimiter, startColumn)) return null - - let endColumn = startColumn + delimiter.length - if (line[endColumn] === ' ') endColumn++ - return [startColumn, endColumn] -} - -function columnRangeForEndDelimiter (line, delimiter) { - let startColumn = line.lastIndexOf(delimiter) - if (startColumn === -1) return null - - const endColumn = startColumn + delimiter.length - if (NON_WHITESPACE_REGEX.test(line.slice(endColumn))) return null - if (line[startColumn - 1] === ' ') startColumn-- - return [startColumn, endColumn] -} diff --git a/src/text-editor.coffee b/src/text-editor.coffee deleted file mode 100644 index 3bc5fa34e..000000000 --- a/src/text-editor.coffee +++ /dev/null @@ -1,3928 +0,0 @@ -_ = require 'underscore-plus' -path = require 'path' -fs = require 'fs-plus' -Grim = require 'grim' -{CompositeDisposable, Disposable, Emitter} = require 'event-kit' -{Point, Range} = TextBuffer = require 'text-buffer' -DecorationManager = require './decoration-manager' -TokenizedBuffer = require './tokenized-buffer' -Cursor = require './cursor' -Model = require './model' -Selection = require './selection' -TextEditorUtils = require './text-editor-utils' - -TextMateScopeSelector = require('first-mate').ScopeSelector -GutterContainer = require './gutter-container' -TextEditorComponent = null -TextEditorElement = null -{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' - -NON_WHITESPACE_REGEXP = /\S/ -ZERO_WIDTH_NBSP = '\ufeff' - -# Essential: This class represents all essential editing state for a single -# {TextBuffer}, including cursor and selection positions, folds, and soft wraps. -# If you're manipulating the state of an editor, use this class. -# -# A single {TextBuffer} can belong to multiple editors. For example, if the -# same file is open in two different panes, Atom creates a separate editor for -# each pane. If the buffer is manipulated the changes are reflected in both -# editors, but each maintains its own cursor position, folded lines, etc. -# -# ## Accessing TextEditor Instances -# -# The easiest way to get hold of `TextEditor` objects is by registering a callback -# with `::observeTextEditors` on the `atom.workspace` global. Your callback will -# then be called with all current editor instances and also when any editor is -# created in the future. -# -# ```coffee -# atom.workspace.observeTextEditors (editor) -> -# editor.insertText('Hello World') -# ``` -# -# ## Buffer vs. Screen Coordinates -# -# Because editors support folds and soft-wrapping, the lines on screen don't -# always match the lines in the buffer. For example, a long line that soft wraps -# twice renders as three lines on screen, but only represents one line in the -# buffer. Similarly, if rows 5-10 are folded, then row 6 on screen corresponds -# to row 11 in the buffer. -# -# Your choice of coordinates systems will depend on what you're trying to -# achieve. For example, if you're writing a command that jumps the cursor up or -# down by 10 lines, you'll want to use screen coordinates because the user -# probably wants to skip lines *on screen*. However, if you're writing a package -# that jumps between method definitions, you'll want to work in buffer -# coordinates. -# -# **When in doubt, just default to buffer coordinates**, then experiment with -# soft wraps and folds to ensure your code interacts with them correctly. -module.exports = -class TextEditor extends Model - @setClipboard: (clipboard) -> - @clipboard = clipboard - - @setScheduler: (scheduler) -> - TextEditorComponent ?= require './text-editor-component' - TextEditorComponent.setScheduler(scheduler) - - @didUpdateStyles: -> - TextEditorComponent ?= require './text-editor-component' - TextEditorComponent.didUpdateStyles() - - @didUpdateScrollbarStyles: -> - TextEditorComponent ?= require './text-editor-component' - TextEditorComponent.didUpdateScrollbarStyles() - - @viewForItem: (item) -> item.element ? item - - serializationVersion: 1 - - buffer: null - cursors: null - showCursorOnSelection: null - selections: null - suppressSelectionMerging: false - selectionFlashDuration: 500 - gutterContainer: null - editorElement: null - verticalScrollMargin: 2 - horizontalScrollMargin: 6 - softWrapped: null - editorWidthInChars: null - lineHeightInPixels: null - defaultCharWidth: null - height: null - width: null - registered: false - atomicSoftTabs: true - invisibles: null - - Object.defineProperty @prototype, "element", - get: -> @getElement() - - Object.defineProperty @prototype, "editorElement", - get: -> - Grim.deprecate(""" - `TextEditor.prototype.editorElement` has always been private, but now - it is gone. Reading the `editorElement` property still returns a - reference to the editor element but this field will be removed in a - later version of Atom, so we recommend using the `element` property instead. - """) - - @getElement() - - Object.defineProperty(@prototype, 'displayBuffer', get: -> - Grim.deprecate(""" - `TextEditor.prototype.displayBuffer` has always been private, but now - it is gone. Reading the `displayBuffer` property now returns a reference - to the containing `TextEditor`, which now provides *some* of the API of - the defunct `DisplayBuffer` class. - """) - this - ) - - Object.defineProperty(@prototype, 'languageMode', get: -> @tokenizedBuffer) - - Object.assign(@prototype, TextEditorUtils) - - @deserialize: (state, atomEnvironment) -> - # TODO: Return null on version mismatch when 1.8.0 has been out for a while - if state.version isnt @prototype.serializationVersion and state.displayBuffer? - state.tokenizedBuffer = state.displayBuffer.tokenizedBuffer - - try - tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) - return null unless tokenizedBuffer? - - state.tokenizedBuffer = tokenizedBuffer - state.tabLength = state.tokenizedBuffer.getTabLength() - catch error - if error.syscall is 'read' - return # Error reading the file, don't deserialize an editor for it - else - throw error - - state.buffer = state.tokenizedBuffer.buffer - state.assert = atomEnvironment.assert.bind(atomEnvironment) - editor = new this(state) - if state.registered - disposable = atomEnvironment.textEditors.add(editor) - editor.onDidDestroy -> disposable.dispose() - editor - - constructor: (params={}) -> - unless @constructor.clipboard? - throw new Error("Must call TextEditor.setClipboard at least once before creating TextEditor instances") - - super - - { - @softTabs, @initialScrollTopRow, @initialScrollLeftColumn, initialLine, initialColumn, tabLength, - @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation, - @mini, @placeholderText, lineNumberGutterVisible, @showLineNumbers, @largeFileMode, - @assert, grammar, @showInvisibles, @autoHeight, @autoWidth, @scrollPastEnd, @scrollSensitivity, @editorWidthInChars, - @tokenizedBuffer, @displayLayer, @invisibles, @showIndentGuide, - @softWrapped, @softWrapAtPreferredLineLength, @preferredLineLength, - @showCursorOnSelection, @maxScreenLineLength - } = params - - @assert ?= (condition) -> condition - @emitter = new Emitter - @disposables = new CompositeDisposable - @cursors = [] - @cursorsByMarkerId = new Map - @selections = [] - @hasTerminatedPendingState = false - - @mini ?= false - @scrollPastEnd ?= false - @scrollSensitivity ?= 40 - @showInvisibles ?= true - @softTabs ?= true - tabLength ?= 2 - @autoIndent ?= true - @autoIndentOnPaste ?= true - @showCursorOnSelection ?= true - @undoGroupingInterval ?= 300 - @nonWordCharacters ?= "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…" - @softWrapped ?= false - @softWrapAtPreferredLineLength ?= false - @preferredLineLength ?= 80 - @maxScreenLineLength ?= 500 - @showLineNumbers ?= true - - @buffer ?= new TextBuffer({ - shouldDestroyOnFileDelete: -> atom.config.get('core.closeDeletedFileTabs') - }) - @tokenizedBuffer ?= new TokenizedBuffer({ - grammar, tabLength, @buffer, @largeFileMode, @assert - }) - - unless @displayLayer? - displayLayerParams = { - invisibles: @getInvisibles(), - softWrapColumn: @getSoftWrapColumn(), - showIndentGuides: @doesShowIndentGuide(), - atomicSoftTabs: params.atomicSoftTabs ? true, - tabLength: tabLength, - ratioForCharacter: @ratioForCharacter.bind(this), - isWrapBoundary: isWrapBoundary, - foldCharacter: ZERO_WIDTH_NBSP, - softWrapHangingIndent: params.softWrapHangingIndentLength ? 0 - } - - if @displayLayer = @buffer.getDisplayLayer(params.displayLayerId) - @displayLayer.reset(displayLayerParams) - @selectionsMarkerLayer = @displayLayer.getMarkerLayer(params.selectionsMarkerLayerId) - else - @displayLayer = @buffer.addDisplayLayer(displayLayerParams) - - @backgroundWorkHandle = requestIdleCallback(@doBackgroundWork) - @disposables.add new Disposable => - cancelIdleCallback(@backgroundWorkHandle) if @backgroundWorkHandle? - - @displayLayer.setTextDecorationLayer(@tokenizedBuffer) - @defaultMarkerLayer = @displayLayer.addMarkerLayer() - @disposables.add(@defaultMarkerLayer.onDidDestroy => - @assert(false, "defaultMarkerLayer destroyed at an unexpected time") - ) - @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true) - @selectionsMarkerLayer.trackDestructionInOnDidCreateMarkerCallbacks = true - - @decorationManager = new DecorationManager(this) - @decorateMarkerLayer(@selectionsMarkerLayer, type: 'cursor') - @decorateCursorLine() unless @isMini() - - @decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'}) - - for marker in @selectionsMarkerLayer.getMarkers() - @addSelection(marker) - - @subscribeToBuffer() - @subscribeToDisplayLayer() - - if @cursors.length is 0 and not suppressCursorCreation - initialLine = Math.max(parseInt(initialLine) or 0, 0) - initialColumn = Math.max(parseInt(initialColumn) or 0, 0) - @addCursorAtBufferPosition([initialLine, initialColumn]) - - @gutterContainer = new GutterContainer(this) - @lineNumberGutter = @gutterContainer.addGutter - name: 'line-number' - priority: 0 - visible: lineNumberGutterVisible - - decorateCursorLine: -> - @cursorLineDecorations = [ - @decorateMarkerLayer(@selectionsMarkerLayer, type: 'line', class: 'cursor-line', onlyEmpty: true), - @decorateMarkerLayer(@selectionsMarkerLayer, type: 'line-number', class: 'cursor-line'), - @decorateMarkerLayer(@selectionsMarkerLayer, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true) - ] - - doBackgroundWork: (deadline) => - previousLongestRow = @getApproximateLongestScreenRow() - if @displayLayer.doBackgroundWork(deadline) - @backgroundWorkHandle = requestIdleCallback(@doBackgroundWork) - else - @backgroundWorkHandle = null - - if @getApproximateLongestScreenRow() isnt previousLongestRow - @component?.scheduleUpdate() - - update: (params) -> - displayLayerParams = {} - - for param in Object.keys(params) - value = params[param] - - switch param - when 'autoIndent' - @autoIndent = value - - when 'autoIndentOnPaste' - @autoIndentOnPaste = value - - when 'undoGroupingInterval' - @undoGroupingInterval = value - - when 'nonWordCharacters' - @nonWordCharacters = value - - when 'scrollSensitivity' - @scrollSensitivity = value - - when 'encoding' - @buffer.setEncoding(value) - - when 'softTabs' - if value isnt @softTabs - @softTabs = value - - when 'atomicSoftTabs' - if value isnt @displayLayer.atomicSoftTabs - displayLayerParams.atomicSoftTabs = value - - when 'tabLength' - if value? and value isnt @tokenizedBuffer.getTabLength() - @tokenizedBuffer.setTabLength(value) - displayLayerParams.tabLength = value - - when 'softWrapped' - if value isnt @softWrapped - @softWrapped = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - @emitter.emit 'did-change-soft-wrapped', @isSoftWrapped() - - when 'softWrapHangingIndentLength' - if value isnt @displayLayer.softWrapHangingIndent - displayLayerParams.softWrapHangingIndent = value - - when 'softWrapAtPreferredLineLength' - if value isnt @softWrapAtPreferredLineLength - @softWrapAtPreferredLineLength = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'preferredLineLength' - if value isnt @preferredLineLength - @preferredLineLength = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'maxScreenLineLength' - if value isnt @maxScreenLineLength - @maxScreenLineLength = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'mini' - if value isnt @mini - @mini = value - @emitter.emit 'did-change-mini', value - displayLayerParams.invisibles = @getInvisibles() - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - displayLayerParams.showIndentGuides = @doesShowIndentGuide() - if @mini - decoration.destroy() for decoration in @cursorLineDecorations - @cursorLineDecorations = null - else - @decorateCursorLine() - @component?.scheduleUpdate() - - when 'placeholderText' - if value isnt @placeholderText - @placeholderText = value - @emitter.emit 'did-change-placeholder-text', value - - when 'lineNumberGutterVisible' - if value isnt @lineNumberGutterVisible - if value - @lineNumberGutter.show() - else - @lineNumberGutter.hide() - @emitter.emit 'did-change-line-number-gutter-visible', @lineNumberGutter.isVisible() - - when 'showIndentGuide' - if value isnt @showIndentGuide - @showIndentGuide = value - displayLayerParams.showIndentGuides = @doesShowIndentGuide() - - when 'showLineNumbers' - if value isnt @showLineNumbers - @showLineNumbers = value - @component?.scheduleUpdate() - - when 'showInvisibles' - if value isnt @showInvisibles - @showInvisibles = value - displayLayerParams.invisibles = @getInvisibles() - - when 'invisibles' - if not _.isEqual(value, @invisibles) - @invisibles = value - displayLayerParams.invisibles = @getInvisibles() - - when 'editorWidthInChars' - if value > 0 and value isnt @editorWidthInChars - @editorWidthInChars = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'width' - if value isnt @width - @width = value - displayLayerParams.softWrapColumn = @getSoftWrapColumn() - - when 'scrollPastEnd' - if value isnt @scrollPastEnd - @scrollPastEnd = value - @component?.scheduleUpdate() - - when 'autoHeight' - if value isnt @autoHeight - @autoHeight = value - - when 'autoWidth' - if value isnt @autoWidth - @autoWidth = value - - when 'showCursorOnSelection' - if value isnt @showCursorOnSelection - @showCursorOnSelection = value - @component?.scheduleUpdate() - - else - if param isnt 'ref' and param isnt 'key' - throw new TypeError("Invalid TextEditor parameter: '#{param}'") - - @displayLayer.reset(displayLayerParams) - - if @component? - @component.getNextUpdatePromise() - else - Promise.resolve() - - scheduleComponentUpdate: -> - @component?.scheduleUpdate() - - serialize: -> - tokenizedBufferState = @tokenizedBuffer.serialize() - - { - deserializer: 'TextEditor' - version: @serializationVersion - - # TODO: Remove this forward-compatible fallback once 1.8 reaches stable. - displayBuffer: {tokenizedBuffer: tokenizedBufferState} - - tokenizedBuffer: tokenizedBufferState - displayLayerId: @displayLayer.id - selectionsMarkerLayerId: @selectionsMarkerLayer.id - - initialScrollTopRow: @getScrollTopRow() - initialScrollLeftColumn: @getScrollLeftColumn() - - atomicSoftTabs: @displayLayer.atomicSoftTabs - softWrapHangingIndentLength: @displayLayer.softWrapHangingIndent - - @id, @softTabs, @softWrapped, @softWrapAtPreferredLineLength, - @preferredLineLength, @mini, @editorWidthInChars, @width, @largeFileMode, @maxScreenLineLength, - @registered, @invisibles, @showInvisibles, @showIndentGuide, @autoHeight, @autoWidth - } - - subscribeToBuffer: -> - @buffer.retain() - @disposables.add @buffer.onDidChangePath => - @emitter.emit 'did-change-title', @getTitle() - @emitter.emit 'did-change-path', @getPath() - @disposables.add @buffer.onDidChangeEncoding => - @emitter.emit 'did-change-encoding', @getEncoding() - @disposables.add @buffer.onDidDestroy => @destroy() - @disposables.add @buffer.onDidChangeModified => - @terminatePendingState() if not @hasTerminatedPendingState and @buffer.isModified() - - terminatePendingState: -> - @emitter.emit 'did-terminate-pending-state' if not @hasTerminatedPendingState - @hasTerminatedPendingState = true - - onDidTerminatePendingState: (callback) -> - @emitter.on 'did-terminate-pending-state', callback - - subscribeToDisplayLayer: -> - @disposables.add @tokenizedBuffer.onDidChangeGrammar @handleGrammarChange.bind(this) - @disposables.add @displayLayer.onDidChange (changes) => - @mergeIntersectingSelections() - @component?.didChangeDisplayLayer(changes) - @emitter.emit 'did-change', changes.map (change) -> new ChangeEvent(change) - @disposables.add @displayLayer.onDidReset => - @mergeIntersectingSelections() - @component?.didResetDisplayLayer() - @emitter.emit 'did-change', {} - @disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this) - @disposables.add @selectionsMarkerLayer.onDidUpdate => @component?.didUpdateSelections() - - destroyed: -> - @disposables.dispose() - @displayLayer.destroy() - @tokenizedBuffer.destroy() - selection.destroy() for selection in @selections.slice() - @buffer.release() - @gutterContainer.destroy() - @emitter.emit 'did-destroy' - @emitter.clear() - @component?.element.component = null - @component = null - @lineNumberGutter.element = null - - ### - Section: Event Subscription - ### - - # Essential: Calls your `callback` when the buffer's title has changed. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeTitle: (callback) -> - @emitter.on 'did-change-title', callback - - # Essential: Calls your `callback` when the buffer's path, and therefore title, has changed. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangePath: (callback) -> - @emitter.on 'did-change-path', callback - - # Essential: Invoke the given callback synchronously when the content of the - # buffer changes. - # - # Because observers are invoked synchronously, it's important not to perform - # any expensive operations via this method. Consider {::onDidStopChanging} to - # delay expensive operations until after changes stop occurring. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChange: (callback) -> - @emitter.on 'did-change', callback - - # Essential: Invoke `callback` when the buffer's contents change. It is - # emit asynchronously 300ms after the last buffer change. This is a good place - # to handle changes to the buffer without compromising typing performance. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidStopChanging: (callback) -> - @getBuffer().onDidStopChanging(callback) - - # Essential: Calls your `callback` when a {Cursor} is moved. If there are - # multiple cursors, your callback will be called for each cursor. - # - # * `callback` {Function} - # * `event` {Object} - # * `oldBufferPosition` {Point} - # * `oldScreenPosition` {Point} - # * `newBufferPosition` {Point} - # * `newScreenPosition` {Point} - # * `textChanged` {Boolean} - # * `cursor` {Cursor} that triggered the event - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeCursorPosition: (callback) -> - @emitter.on 'did-change-cursor-position', callback - - # Essential: Calls your `callback` when a selection's screen range changes. - # - # * `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. - onDidChangeSelectionRange: (callback) -> - @emitter.on 'did-change-selection-range', callback - - # Extended: Calls your `callback` when soft wrap was enabled or disabled. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeSoftWrapped: (callback) -> - @emitter.on 'did-change-soft-wrapped', callback - - # Extended: Calls your `callback` when the buffer's encoding has changed. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeEncoding: (callback) -> - @emitter.on 'did-change-encoding', callback - - # Extended: Calls your `callback` when the grammar that interprets and - # colorizes the text has been changed. Immediately calls your callback with - # the current grammar. - # - # * `callback` {Function} - # * `grammar` {Grammar} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeGrammar: (callback) -> - callback(@getGrammar()) - @onDidChangeGrammar(callback) - - # Extended: Calls your `callback` when the grammar that interprets and - # colorizes the text has been changed. - # - # * `callback` {Function} - # * `grammar` {Grammar} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeGrammar: (callback) -> - @emitter.on 'did-change-grammar', callback - - # Extended: Calls your `callback` when the result of {::isModified} changes. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeModified: (callback) -> - @getBuffer().onDidChangeModified(callback) - - # Extended: Calls your `callback` when the buffer's underlying file changes on - # disk at a moment when the result of {::isModified} is true. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidConflict: (callback) -> - @getBuffer().onDidConflict(callback) - - # Extended: Calls your `callback` before text has been inserted. - # - # * `callback` {Function} - # * `event` event {Object} - # * `text` {String} text to be inserted - # * `cancel` {Function} Call to prevent the text from being inserted - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onWillInsertText: (callback) -> - @emitter.on 'will-insert-text', callback - - # Extended: Calls your `callback` after text has been inserted. - # - # * `callback` {Function} - # * `event` event {Object} - # * `text` {String} text to be inserted - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidInsertText: (callback) -> - @emitter.on 'did-insert-text', callback - - # Essential: Invoke the given callback after the buffer is saved to disk. - # - # * `callback` {Function} to be called after the buffer is saved. - # * `event` {Object} with the following keys: - # * `path` The path to which the buffer was saved. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidSave: (callback) -> - @getBuffer().onDidSave(callback) - - # Essential: Invoke the given callback when the editor is destroyed. - # - # * `callback` {Function} to be called when the editor is destroyed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.once 'did-destroy', callback - - # Extended: Calls your `callback` when a {Cursor} is added to the editor. - # Immediately calls your callback for each existing cursor. - # - # * `callback` {Function} - # * `cursor` {Cursor} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeCursors: (callback) -> - callback(cursor) for cursor in @getCursors() - @onDidAddCursor(callback) - - # Extended: Calls your `callback` when a {Cursor} is added to the editor. - # - # * `callback` {Function} - # * `cursor` {Cursor} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddCursor: (callback) -> - @emitter.on 'did-add-cursor', callback - - # Extended: Calls your `callback` when a {Cursor} is removed from the editor. - # - # * `callback` {Function} - # * `cursor` {Cursor} that was removed - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveCursor: (callback) -> - @emitter.on 'did-remove-cursor', callback - - # Extended: Calls your `callback` when a {Selection} is added to the editor. - # Immediately calls your callback for each existing selection. - # - # * `callback` {Function} - # * `selection` {Selection} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeSelections: (callback) -> - callback(selection) for selection in @getSelections() - @onDidAddSelection(callback) - - # Extended: Calls your `callback` when a {Selection} is added to the editor. - # - # * `callback` {Function} - # * `selection` {Selection} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddSelection: (callback) -> - @emitter.on 'did-add-selection', callback - - # Extended: Calls your `callback` when a {Selection} is removed from the editor. - # - # * `callback` {Function} - # * `selection` {Selection} that was removed - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveSelection: (callback) -> - @emitter.on 'did-remove-selection', callback - - # Extended: Calls your `callback` with each {Decoration} added to the editor. - # Calls your `callback` immediately for any existing decorations. - # - # * `callback` {Function} - # * `decoration` {Decoration} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeDecorations: (callback) -> - @decorationManager.observeDecorations(callback) - - # Extended: Calls your `callback` when a {Decoration} is added to the editor. - # - # * `callback` {Function} - # * `decoration` {Decoration} that was added - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddDecoration: (callback) -> - @decorationManager.onDidAddDecoration(callback) - - # Extended: Calls your `callback` when a {Decoration} is removed from the editor. - # - # * `callback` {Function} - # * `decoration` {Decoration} that was removed - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveDecoration: (callback) -> - @decorationManager.onDidRemoveDecoration(callback) - - # Called by DecorationManager when a decoration is added. - didAddDecoration: (decoration) -> - if decoration.isType('block') - @component?.addBlockDecoration(decoration) - - # Extended: Calls your `callback` when the placeholder text is changed. - # - # * `callback` {Function} - # * `placeholderText` {String} new text - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangePlaceholderText: (callback) -> - @emitter.on 'did-change-placeholder-text', callback - - onDidChangeScrollTop: (callback) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.") - - @getElement().onDidChangeScrollTop(callback) - - onDidChangeScrollLeft: (callback) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.") - - @getElement().onDidChangeScrollLeft(callback) - - onDidRequestAutoscroll: (callback) -> - @emitter.on 'did-request-autoscroll', callback - - # TODO Remove once the tabs package no longer uses .on subscriptions - onDidChangeIcon: (callback) -> - @emitter.on 'did-change-icon', callback - - onDidUpdateDecorations: (callback) -> - @decorationManager.onDidUpdateDecorations(callback) - - # Essential: Retrieves the current {TextBuffer}. - getBuffer: -> @buffer - - # Retrieves the current buffer's URI. - getURI: -> @buffer.getUri() - - # Create an {TextEditor} with its initial state based on this object - copy: -> - displayLayer = @displayLayer.copy() - selectionsMarkerLayer = displayLayer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id) - softTabs = @getSoftTabs() - new TextEditor({ - @buffer, selectionsMarkerLayer, softTabs, - suppressCursorCreation: true, - tabLength: @tokenizedBuffer.getTabLength(), - initialScrollTopRow: @getScrollTopRow(), - initialScrollLeftColumn: @getScrollLeftColumn(), - @assert, displayLayer, grammar: @getGrammar(), - @autoWidth, @autoHeight, @showCursorOnSelection - }) - - # Controls visibility based on the given {Boolean}. - setVisible: (visible) -> @tokenizedBuffer.setVisible(visible) - - setMini: (mini) -> - @update({mini}) - @mini - - isMini: -> @mini - - onDidChangeMini: (callback) -> - @emitter.on 'did-change-mini', callback - - setLineNumberGutterVisible: (lineNumberGutterVisible) -> @update({lineNumberGutterVisible}) - - isLineNumberGutterVisible: -> @lineNumberGutter.isVisible() - - onDidChangeLineNumberGutterVisible: (callback) -> - @emitter.on 'did-change-line-number-gutter-visible', callback - - # Essential: Calls your `callback` when a {Gutter} is added to the editor. - # Immediately calls your callback for each existing gutter. - # - # * `callback` {Function} - # * `gutter` {Gutter} that currently exists/was added. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeGutters: (callback) -> - @gutterContainer.observeGutters callback - - # Essential: Calls your `callback` when a {Gutter} is added to the editor. - # - # * `callback` {Function} - # * `gutter` {Gutter} that was added. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddGutter: (callback) -> - @gutterContainer.onDidAddGutter callback - - # Essential: Calls your `callback` when a {Gutter} is removed from the editor. - # - # * `callback` {Function} - # * `name` The name of the {Gutter} that was removed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidRemoveGutter: (callback) -> - @gutterContainer.onDidRemoveGutter callback - - # Set the number of characters that can be displayed horizontally in the - # editor. - # - # * `editorWidthInChars` A {Number} representing the width of the - # {TextEditorElement} in characters. - setEditorWidthInChars: (editorWidthInChars) -> @update({editorWidthInChars}) - - # Returns the editor width in characters. - getEditorWidthInChars: -> - if @width? and @defaultCharWidth > 0 - Math.max(0, Math.floor(@width / @defaultCharWidth)) - else - @editorWidthInChars - - ### - Section: File Details - ### - - # Essential: Get the editor's title for display in other parts of the - # UI such as the tabs. - # - # If the editor's buffer is saved, its title is the file name. If it is - # unsaved, its title is "untitled". - # - # Returns a {String}. - getTitle: -> - @getFileName() ? 'untitled' - - # Essential: Get unique title for display in other parts of the UI, such as - # the window title. - # - # If the editor's buffer is unsaved, its title is "untitled" - # If the editor's buffer is saved, its unique title is formatted as one - # of the following, - # * "" when it is the only editing buffer with this file name. - # * "" when other buffers have this file name. - # - # Returns a {String} - getLongTitle: -> - if @getPath() - fileName = @getFileName() - - allPathSegments = [] - for textEditor in atom.workspace.getTextEditors() when textEditor isnt this - if textEditor.getFileName() is fileName - directoryPath = fs.tildify(textEditor.getDirectoryPath()) - allPathSegments.push(directoryPath.split(path.sep)) - - if allPathSegments.length is 0 - return fileName - - ourPathSegments = fs.tildify(@getDirectoryPath()).split(path.sep) - allPathSegments.push ourPathSegments - - loop - firstSegment = ourPathSegments[0] - - commonBase = _.all(allPathSegments, (pathSegments) -> pathSegments.length > 1 and pathSegments[0] is firstSegment) - if commonBase - pathSegments.shift() for pathSegments in allPathSegments - else - break - - "#{fileName} \u2014 #{path.join(pathSegments...)}" - else - 'untitled' - - # Essential: Returns the {String} path of this editor's text buffer. - getPath: -> @buffer.getPath() - - getFileName: -> - if fullPath = @getPath() - path.basename(fullPath) - else - null - - getDirectoryPath: -> - if fullPath = @getPath() - path.dirname(fullPath) - else - null - - # Extended: Returns the {String} character set encoding of this editor's text - # buffer. - getEncoding: -> @buffer.getEncoding() - - # Extended: Set the character set encoding to use in this editor's text - # buffer. - # - # * `encoding` The {String} character set encoding name such as 'utf8' - setEncoding: (encoding) -> @buffer.setEncoding(encoding) - - # Essential: Returns {Boolean} `true` if this editor has been modified. - isModified: -> @buffer.isModified() - - # Essential: Returns {Boolean} `true` if this editor has no content. - isEmpty: -> @buffer.isEmpty() - - ### - Section: File Operations - ### - - # Essential: Saves the editor's text buffer. - # - # See {TextBuffer::save} for more details. - save: -> @buffer.save() - - # Essential: Saves the editor's text buffer as the given path. - # - # See {TextBuffer::saveAs} for more details. - # - # * `filePath` A {String} path. - saveAs: (filePath) -> @buffer.saveAs(filePath) - - # Determine whether the user should be prompted to save before closing - # this editor. - shouldPromptToSave: ({windowCloseRequested, projectHasPaths}={}) -> - if windowCloseRequested and projectHasPaths and atom.stateStore.isConnected() - @buffer.isInConflict() - else - @isModified() and not @buffer.hasMultipleEditors() - - # Returns an {Object} to configure dialog shown when this editor is saved - # via {Pane::saveItemAs}. - getSaveDialogOptions: -> {} - - ### - Section: Reading Text - ### - - # Essential: Returns a {String} representing the entire contents of the editor. - getText: -> @buffer.getText() - - # Essential: Get the text in the given {Range} in buffer coordinates. - # - # * `range` A {Range} or range-compatible {Array}. - # - # Returns a {String}. - getTextInBufferRange: (range) -> - @buffer.getTextInRange(range) - - # Essential: Returns a {Number} representing the number of lines in the buffer. - getLineCount: -> @buffer.getLineCount() - - # Essential: Returns a {Number} representing the number of screen lines in the - # editor. This accounts for folds. - getScreenLineCount: -> @displayLayer.getScreenLineCount() - - getApproximateScreenLineCount: -> @displayLayer.getApproximateScreenLineCount() - - # Essential: Returns a {Number} representing the last zero-indexed buffer row - # number of the editor. - getLastBufferRow: -> @buffer.getLastRow() - - # Essential: Returns a {Number} representing the last zero-indexed screen row - # number of the editor. - getLastScreenRow: -> @getScreenLineCount() - 1 - - # Essential: Returns a {String} representing the contents of the line at the - # given buffer row. - # - # * `bufferRow` A {Number} representing a zero-indexed buffer row. - lineTextForBufferRow: (bufferRow) -> @buffer.lineForRow(bufferRow) - - # Essential: Returns a {String} representing the contents of the line at the - # given screen row. - # - # * `screenRow` A {Number} representing a zero-indexed screen row. - lineTextForScreenRow: (screenRow) -> - @screenLineForScreenRow(screenRow)?.lineText - - logScreenLines: (start=0, end=@getLastScreenRow()) -> - for row in [start..end] - line = @lineTextForScreenRow(row) - console.log row, @bufferRowForScreenRow(row), line, line.length - return - - tokensForScreenRow: (screenRow) -> - tokens = [] - lineTextIndex = 0 - currentTokenScopes = [] - {lineText, tags} = @screenLineForScreenRow(screenRow) - for tag in tags - if @displayLayer.isOpenTag(tag) - currentTokenScopes.push(@displayLayer.classNameForTag(tag)) - else if @displayLayer.isCloseTag(tag) - currentTokenScopes.pop() - else - tokens.push({ - text: lineText.substr(lineTextIndex, tag) - scopes: currentTokenScopes.slice() - }) - lineTextIndex += tag - tokens - - screenLineForScreenRow: (screenRow) -> - @displayLayer.getScreenLine(screenRow) - - bufferRowForScreenRow: (screenRow) -> - @displayLayer.translateScreenPosition(Point(screenRow, 0)).row - - bufferRowsForScreenRows: (startScreenRow, endScreenRow) -> - @displayLayer.bufferRowsForScreenRows(startScreenRow, endScreenRow + 1) - - screenRowForBufferRow: (row) -> - @displayLayer.translateBufferPosition(Point(row, 0)).row - - getRightmostScreenPosition: -> @displayLayer.getRightmostScreenPosition() - - getApproximateRightmostScreenPosition: -> @displayLayer.getApproximateRightmostScreenPosition() - - getMaxScreenLineLength: -> @getRightmostScreenPosition().column - - getLongestScreenRow: -> @getRightmostScreenPosition().row - - getApproximateLongestScreenRow: -> @getApproximateRightmostScreenPosition().row - - lineLengthForScreenRow: (screenRow) -> @displayLayer.lineLengthForScreenRow(screenRow) - - # Returns the range for the given buffer row. - # - # * `row` A row {Number}. - # * `options` (optional) An options hash with an `includeNewline` key. - # - # Returns a {Range}. - bufferRangeForBufferRow: (row, {includeNewline}={}) -> @buffer.rangeForRow(row, includeNewline) - - # Get the text in the given {Range}. - # - # Returns a {String}. - getTextInRange: (range) -> @buffer.getTextInRange(range) - - # {Delegates to: TextBuffer.isRowBlank} - isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow) - - # {Delegates to: TextBuffer.nextNonBlankRow} - nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow) - - # {Delegates to: TextBuffer.getEndPosition} - getEofBufferPosition: -> @buffer.getEndPosition() - - # Essential: Get the {Range} of the paragraph surrounding the most recently added - # cursor. - # - # Returns a {Range}. - getCurrentParagraphBufferRange: -> - @getLastCursor().getCurrentParagraphBufferRange() - - - ### - Section: Mutating Text - ### - - # Essential: Replaces the entire contents of the buffer with the given {String}. - # - # * `text` A {String} to replace with - setText: (text) -> @buffer.setText(text) - - # Essential: Set the text in the given {Range} in buffer coordinates. - # - # * `range` A {Range} or range-compatible {Array}. - # * `text` A {String} - # * `options` (optional) {Object} - # * `normalizeLineEndings` (optional) {Boolean} (default: true) - # * `undo` (optional) {String} 'skip' will skip the undo system - # - # Returns the {Range} of the newly-inserted text. - setTextInBufferRange: (range, text, options) -> @getBuffer().setTextInRange(range, text, options) - - # Essential: For each selection, replace the selected text with the given text. - # - # * `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 - insertText: (text, options={}) -> - return false unless @emitWillInsertTextEvent(text) - - groupingInterval = if options.groupUndo - @undoGroupingInterval - else - 0 - - options.autoIndentNewline ?= @shouldAutoIndent() - options.autoDecreaseIndent ?= @shouldAutoIndent() - @mutateSelectedText( - (selection) => - range = selection.insertText(text, options) - didInsertEvent = {text, range} - @emitter.emit 'did-insert-text', didInsertEvent - range - , groupingInterval - ) - - # Essential: For each selection, replace the selected text with a newline. - insertNewline: (options) -> - @insertText('\n', options) - - # Essential: For each selection, if the selection is empty, delete the character - # following the cursor. Otherwise delete the selected text. - delete: -> - @mutateSelectedText (selection) -> selection.delete() - - # Essential: For each selection, if the selection is empty, delete the character - # preceding the cursor. Otherwise delete the selected text. - backspace: -> - @mutateSelectedText (selection) -> selection.backspace() - - # Extended: Mutate the text of all the selections in a single transaction. - # - # All the changes made inside the given {Function} can be reverted with a - # single call to {::undo}. - # - # * `fn` A {Function} that will be called once for each {Selection}. The first - # argument will be a {Selection} and the second argument will be the - # {Number} index of that selection. - mutateSelectedText: (fn, groupingInterval=0) -> - @mergeIntersectingSelections => - @transact groupingInterval, => - fn(selection, index) for selection, index in @getSelectionsOrderedByBufferPosition() - - # Move lines intersecting the most recent selection or multiple selections - # up by one row in screen coordinates. - moveLineUp: -> - selections = @getSelectedBufferRanges().sort((a, b) -> a.compare(b)) - - if selections[0].start.row is 0 - return - - if selections[selections.length - 1].start.row is @getLastBufferRow() and @buffer.getLastLine() is '' - return - - @transact => - newSelectionRanges = [] - - while selections.length > 0 - # Find selections spanning a contiguous set of lines - selection = selections.shift() - selectionsToMove = [selection] - - while selection.end.row is selections[0]?.start.row - selectionsToMove.push(selections[0]) - selection.end.row = selections[0].end.row - selections.shift() - - # Compute the buffer range spanned by all these selections, expanding it - # so that it includes any folded region that intersects them. - startRow = selection.start.row - endRow = selection.end.row - if selection.end.row > selection.start.row and selection.end.column is 0 - # Don't move the last line of a multi-line selection if the selection ends at column 0 - endRow-- - - startRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow) - endRow = @displayLayer.findBoundaryFollowingBufferRow(endRow + 1) - linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) - - # If selected line range is preceded by a fold, one line above on screen - # could be multiple lines in the buffer. - precedingRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow - 1) - insertDelta = linesRange.start.row - precedingRow - - # Any folds in the text that is moved will need to be re-created. - # It includes the folds that were intersecting with the selection. - rangesToRefold = @displayLayer - .destroyFoldsIntersectingBufferRange(linesRange) - .map((range) -> range.translate([-insertDelta, 0])) - - # Delete lines spanned by selection and insert them on the preceding buffer row - lines = @buffer.getTextInRange(linesRange) - lines += @buffer.lineEndingForRow(linesRange.end.row - 2) unless lines[lines.length - 1] is '\n' - @buffer.delete(linesRange) - @buffer.insert([precedingRow, 0], lines) - - # Restore folds that existed before the lines were moved - for rangeToRefold in rangesToRefold - @displayLayer.foldBufferRange(rangeToRefold) - - for selection in selectionsToMove - newSelectionRanges.push(selection.translate([-insertDelta, 0])) - - @setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) - @autoIndentSelectedRows() if @shouldAutoIndent() - @scrollToBufferPosition([newSelectionRanges[0].start.row, 0]) - - # Move lines intersecting the most recent selection or multiple selections - # down by one row in screen coordinates. - moveLineDown: -> - selections = @getSelectedBufferRanges() - selections.sort (a, b) -> a.compare(b) - selections = selections.reverse() - - @transact => - @consolidateSelections() - newSelectionRanges = [] - - while selections.length > 0 - # Find selections spanning a contiguous set of lines - selection = selections.shift() - selectionsToMove = [selection] - - # if the current selection start row matches the next selections' end row - make them one selection - while selection.start.row is selections[0]?.end.row - selectionsToMove.push(selections[0]) - selection.start.row = selections[0].start.row - selections.shift() - - # Compute the buffer range spanned by all these selections, expanding it - # so that it includes any folded region that intersects them. - startRow = selection.start.row - endRow = selection.end.row - if selection.end.row > selection.start.row and selection.end.column is 0 - # Don't move the last line of a multi-line selection if the selection ends at column 0 - endRow-- - - startRow = @displayLayer.findBoundaryPrecedingBufferRow(startRow) - endRow = @displayLayer.findBoundaryFollowingBufferRow(endRow + 1) - linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) - - # If selected line range is followed by a fold, one line below on screen - # could be multiple lines in the buffer. But at the same time, if the - # next buffer row is wrapped, one line in the buffer can represent many - # screen rows. - followingRow = Math.min(@buffer.getLineCount(), @displayLayer.findBoundaryFollowingBufferRow(endRow + 1)) - insertDelta = followingRow - linesRange.end.row - - # Any folds in the text that is moved will need to be re-created. - # It includes the folds that were intersecting with the selection. - rangesToRefold = @displayLayer - .destroyFoldsIntersectingBufferRange(linesRange) - .map((range) -> range.translate([insertDelta, 0])) - - # Delete lines spanned by selection and insert them on the following correct buffer row - lines = @buffer.getTextInRange(linesRange) - if followingRow - 1 is @buffer.getLastRow() - lines = "\n#{lines}" - - @buffer.insert([followingRow, 0], lines) - @buffer.delete(linesRange) - - # Restore folds that existed before the lines were moved - for rangeToRefold in rangesToRefold - @displayLayer.foldBufferRange(rangeToRefold) - - for selection in selectionsToMove - newSelectionRanges.push(selection.translate([insertDelta, 0])) - - @setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) - @autoIndentSelectedRows() if @shouldAutoIndent() - @scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0]) - - # Move any active selections one column to the left. - moveSelectionLeft: -> - selections = @getSelectedBufferRanges() - noSelectionAtStartOfLine = selections.every((selection) -> - selection.start.column isnt 0 - ) - - translationDelta = [0, -1] - translatedRanges = [] - - if noSelectionAtStartOfLine - @transact => - for selection in selections - charToLeftOfSelection = new Range(selection.start.translate(translationDelta), selection.start) - charTextToLeftOfSelection = @buffer.getTextInRange(charToLeftOfSelection) - - @buffer.insert(selection.end, charTextToLeftOfSelection) - @buffer.delete(charToLeftOfSelection) - translatedRanges.push(selection.translate(translationDelta)) - - @setSelectedBufferRanges(translatedRanges) - - # Move any active selections one column to the right. - moveSelectionRight: -> - selections = @getSelectedBufferRanges() - noSelectionAtEndOfLine = selections.every((selection) => - selection.end.column isnt @buffer.lineLengthForRow(selection.end.row) - ) - - translationDelta = [0, 1] - translatedRanges = [] - - if noSelectionAtEndOfLine - @transact => - for selection in selections - charToRightOfSelection = new Range(selection.end, selection.end.translate(translationDelta)) - charTextToRightOfSelection = @buffer.getTextInRange(charToRightOfSelection) - - @buffer.delete(charToRightOfSelection) - @buffer.insert(selection.start, charTextToRightOfSelection) - translatedRanges.push(selection.translate(translationDelta)) - - @setSelectedBufferRanges(translatedRanges) - - duplicateLines: -> - @transact => - selections = @getSelectionsOrderedByBufferPosition() - previousSelectionRanges = [] - - i = selections.length - 1 - while i >= 0 - j = i - previousSelectionRanges[i] = selections[i].getBufferRange() - if selections[i].isEmpty() - {start} = selections[i].getScreenRange() - selections[i].setScreenRange([[start.row, 0], [start.row + 1, 0]], preserveFolds: true) - [startRow, endRow] = selections[i].getBufferRowRange() - endRow++ - while i > 0 - [previousSelectionStartRow, previousSelectionEndRow] = selections[i - 1].getBufferRowRange() - if previousSelectionEndRow is startRow - startRow = previousSelectionStartRow - previousSelectionRanges[i - 1] = selections[i - 1].getBufferRange() - i-- - else - break - - intersectingFolds = @displayLayer.foldsIntersectingBufferRange([[startRow, 0], [endRow, 0]]) - textToDuplicate = @getTextInBufferRange([[startRow, 0], [endRow, 0]]) - textToDuplicate = '\n' + textToDuplicate if endRow > @getLastBufferRow() - @buffer.insert([endRow, 0], textToDuplicate) - - insertedRowCount = endRow - startRow - - for k in [i..j] by 1 - selections[k].setBufferRange(previousSelectionRanges[k].translate([insertedRowCount, 0])) - - for fold in intersectingFolds - foldRange = @displayLayer.bufferRangeForFold(fold) - @displayLayer.foldBufferRange(foldRange.translate([insertedRowCount, 0])) - - i-- - - replaceSelectedText: (options={}, fn) -> - {selectWordIfEmpty} = options - @mutateSelectedText (selection) -> - selection.getBufferRange() - if selectWordIfEmpty and selection.isEmpty() - selection.selectWord() - text = selection.getText() - selection.deleteSelectedText() - range = selection.insertText(fn(text)) - selection.setBufferRange(range) - - # Split multi-line selections into one selection per line. - # - # Operates on all selections. This method breaks apart all multi-line - # selections to create multiple single-line selections that cumulatively cover - # the same original area. - splitSelectionsIntoLines: -> - @mergeIntersectingSelections => - for selection in @getSelections() - range = selection.getBufferRange() - continue if range.isSingleLine() - - {start, end} = range - @addSelectionForBufferRange([start, [start.row, Infinity]]) - {row} = start - while ++row < end.row - @addSelectionForBufferRange([[row, 0], [row, Infinity]]) - @addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) unless end.column is 0 - selection.destroy() - return - - # Extended: For each selection, transpose the selected text. - # - # If the selection is empty, the characters preceding and following the cursor - # are swapped. Otherwise, the selected characters are reversed. - transpose: -> - @mutateSelectedText (selection) -> - if selection.isEmpty() - selection.selectRight() - text = selection.getText() - selection.delete() - selection.cursor.moveLeft() - selection.insertText text - else - selection.insertText selection.getText().split('').reverse().join('') - - # Extended: Convert the selected text to upper case. - # - # For each selection, if the selection is empty, converts the containing word - # to upper case. Otherwise convert the selected text to upper case. - upperCase: -> - @replaceSelectedText selectWordIfEmpty: true, (text) -> text.toUpperCase() - - # 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: -> - @replaceSelectedText selectWordIfEmpty: true, (text) -> text.toLowerCase() - - # Extended: Toggle line comments for rows intersecting selections. - # - # If the current grammar doesn't support comments, does nothing. - toggleLineCommentsInSelection: -> - @mutateSelectedText (selection) -> selection.toggleLineComments() - - # Convert multiple lines to a single line. - # - # Operates on all selections. If the selection is empty, joins the current - # line with the next line. Otherwise it joins all lines that intersect the - # selection. - # - # 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: -> - @mutateSelectedText (selection) -> selection.joinLines() - - # Extended: For each cursor, insert a newline at beginning the following line. - insertNewlineBelow: -> - @transact => - @moveToEndOfLine() - @insertNewline() - - # Extended: For each cursor, insert a newline at the end of the preceding line. - insertNewlineAbove: -> - @transact => - bufferRow = @getCursorBufferPosition().row - indentLevel = @indentationForBufferRow(bufferRow) - onFirstLine = bufferRow is 0 - - @moveToBeginningOfLine() - @moveLeft() - @insertNewline() - - if @shouldAutoIndent() and @indentationForBufferRow(bufferRow) < indentLevel - @setIndentationForBufferRow(bufferRow, indentLevel) - - if onFirstLine - @moveUp() - @moveToEndOfLine() - - # 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: -> - @mutateSelectedText (selection) -> selection.deleteToBeginningOfWord() - - # Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the - # previous word boundary. - deleteToPreviousWordBoundary: -> - @mutateSelectedText (selection) -> selection.deleteToPreviousWordBoundary() - - # Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the - # next word boundary. - deleteToNextWordBoundary: -> - @mutateSelectedText (selection) -> selection.deleteToNextWordBoundary() - - # 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: -> - @mutateSelectedText (selection) -> selection.deleteToBeginningOfSubword() - - # 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: -> - @mutateSelectedText (selection) -> selection.deleteToEndOfSubword() - - # 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: -> - @mutateSelectedText (selection) -> selection.deleteToBeginningOfLine() - - # 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: -> - @mutateSelectedText (selection) -> selection.deleteToEndOfLine() - - # 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: -> - @mutateSelectedText (selection) -> selection.deleteToEndOfWord() - - # Extended: Delete all lines intersecting selections. - deleteLine: -> - @mergeSelectionsOnSameRows() - @mutateSelectedText (selection) -> selection.deleteLine() - - ### - Section: History - ### - - # Essential: Undo the last change. - undo: -> - @avoidMergingSelections => @buffer.undo() - @getLastSelection().autoscroll() - - # Essential: Redo the last change. - redo: -> - @avoidMergingSelections => @buffer.redo() - @getLastSelection().autoscroll() - - # Extended: Batch multiple operations as a single undo/redo step. - # - # Any group of operations that are logically grouped from the perspective of - # undoing and redoing should be performed in a transaction. If you want to - # abort the transaction, call {::abortTransaction} to terminate the function's - # execution and revert any changes performed up to the abortion. - # - # * `groupingInterval` (optional) The {Number} of milliseconds for which this - # transaction should be considered 'groupable' after it begins. If a transaction - # with a positive `groupingInterval` is committed while the previous transaction is - # still 'groupable', the two transactions are merged with respect to undo and redo. - # * `fn` A {Function} to call inside the transaction. - transact: (groupingInterval, fn) -> - @buffer.transact(groupingInterval, fn) - - # Deprecated: Start an open-ended transaction. - beginTransaction: (groupingInterval) -> - Grim.deprecate('Transactions should be performed via TextEditor::transact only') - @buffer.beginTransaction(groupingInterval) - - # Deprecated: Commit an open-ended transaction started with {::beginTransaction}. - commitTransaction: -> - Grim.deprecate('Transactions should be performed via TextEditor::transact only') - @buffer.commitTransaction() - - # Extended: Abort an open transaction, undoing any operations performed so far - # within the transaction. - abortTransaction: -> @buffer.abortTransaction() - - # Extended: Create a pointer to the current state of the buffer for use - # with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}. - # - # Returns a checkpoint value. - createCheckpoint: -> @buffer.createCheckpoint() - - # Extended: Revert the buffer to the state it was in when the given - # checkpoint was created. - # - # The redo stack will be empty following this operation, so changes since the - # checkpoint will be lost. If the given checkpoint is no longer present in the - # undo history, no changes will be made to the buffer and this method will - # return `false`. - # - # * `checkpoint` The checkpoint to revert to. - # - # Returns a {Boolean} indicating whether the operation succeeded. - revertToCheckpoint: (checkpoint) -> @buffer.revertToCheckpoint(checkpoint) - - # Extended: Group all changes since the given checkpoint into a single - # transaction for purposes of undo/redo. - # - # If the given checkpoint is no longer present in the undo history, no - # grouping will be performed and this method will return `false`. - # - # * `checkpoint` The checkpoint from which to group changes. - # - # Returns a {Boolean} indicating whether the operation succeeded. - groupChangesSinceCheckpoint: (checkpoint) -> @buffer.groupChangesSinceCheckpoint(checkpoint) - - ### - Section: TextEditor Coordinates - ### - - # Essential: Convert a position in buffer-coordinates to screen-coordinates. - # - # The position is clipped via {::clipBufferPosition} prior to the conversion. - # The position is also clipped via {::clipScreenPosition} following the - # conversion, which only makes a difference when `options` are supplied. - # - # * `bufferPosition` A {Point} or {Array} of [row, column]. - # * `options` (optional) An options hash for {::clipScreenPosition}. - # - # Returns a {Point}. - screenPositionForBufferPosition: (bufferPosition, options) -> - if options?.clip? - Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") - options.clipDirection ?= options.clip - if options?.wrapAtSoftNewlines? - Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' - if options?.wrapBeyondNewlines? - Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' - - @displayLayer.translateBufferPosition(bufferPosition, options) - - # Essential: Convert a position in screen-coordinates to buffer-coordinates. - # - # The position is clipped via {::clipScreenPosition} prior to the conversion. - # - # * `bufferPosition` A {Point} or {Array} of [row, column]. - # * `options` (optional) An options hash for {::clipScreenPosition}. - # - # Returns a {Point}. - bufferPositionForScreenPosition: (screenPosition, options) -> - if options?.clip? - Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") - options.clipDirection ?= options.clip - if options?.wrapAtSoftNewlines? - Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' - if options?.wrapBeyondNewlines? - Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' - - @displayLayer.translateScreenPosition(screenPosition, options) - - # Essential: Convert a range in buffer-coordinates to screen-coordinates. - # - # * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates. - # - # Returns a {Range}. - screenRangeForBufferRange: (bufferRange, options) -> - bufferRange = Range.fromObject(bufferRange) - start = @screenPositionForBufferPosition(bufferRange.start, options) - end = @screenPositionForBufferPosition(bufferRange.end, options) - new Range(start, end) - - # Essential: Convert a range in screen-coordinates to buffer-coordinates. - # - # * `screenRange` {Range} in screen coordinates to translate into buffer coordinates. - # - # Returns a {Range}. - bufferRangeForScreenRange: (screenRange) -> - screenRange = Range.fromObject(screenRange) - start = @bufferPositionForScreenPosition(screenRange.start) - end = @bufferPositionForScreenPosition(screenRange.end) - new Range(start, end) - - # Extended: Clip the given {Point} to a valid position in the buffer. - # - # If the given {Point} describes a position that is actually reachable by the - # cursor based on the current contents of the buffer, it is returned - # unchanged. If the {Point} does not describe a valid position, the closest - # valid position is returned instead. - # - # ## Examples - # - # ```coffee - # editor.clipBufferPosition([-1, -1]) # -> `[0, 0]` - # - # # 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. - # - # Returns a {Point}. - clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) - - # Extended: Clip the start and end of the given range to valid positions in the - # buffer. See {::clipBufferPosition} for more information. - # - # * `range` The {Range} to clip. - # - # Returns a {Range}. - clipBufferRange: (range) -> @buffer.clipRange(range) - - # Extended: Clip the given {Point} to a valid position on screen. - # - # If the given {Point} describes a position that is actually reachable by the - # cursor based on the current contents of the screen, it is returned - # unchanged. If the {Point} does not describe a valid position, the closest - # valid position is returned instead. - # - # ## Examples - # - # ```coffee - # editor.clipScreenPosition([-1, -1]) # -> `[0, 0]` - # - # # 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. - # * `options` (optional) {Object} - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. - # - # Returns a {Point}. - clipScreenPosition: (screenPosition, options) -> - if options?.clip? - Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") - options.clipDirection ?= options.clip - if options?.wrapAtSoftNewlines? - Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' - if options?.wrapBeyondNewlines? - Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' - - @displayLayer.clipScreenPosition(screenPosition, options) - - # Extended: Clip the start and end of the given range to valid positions on screen. - # See {::clipScreenPosition} for more information. - # - # * `range` The {Range} to clip. - # * `options` (optional) See {::clipScreenPosition} `options`. - # - # Returns a {Range}. - clipScreenRange: (screenRange, options) -> - screenRange = Range.fromObject(screenRange) - start = @displayLayer.clipScreenPosition(screenRange.start, options) - end = @displayLayer.clipScreenPosition(screenRange.end, options) - Range(start, end) - - ### - Section: Decorations - ### - - # Essential: Add a decoration that tracks a {DisplayMarker}. When the - # marker moves, is invalidated, or is destroyed, the decoration will be - # updated to reflect the marker's state. - # - # The following are the supported decorations types: - # - # * __line__: Adds your CSS `class` to the line nodes within the range - # marked by the marker - # * __line-number__: Adds your CSS `class` to the line number nodes within the - # range marked by the marker - # * __highlight__: Adds a new highlight div to the editor surrounding the - # range marked by the marker. When the user selects text, the selection is - # visualized with a highlight decoration internally. The structure of this - # highlight will be - # ```html - #
- # - #
- #
- # ``` - # * __overlay__: Positions the view associated with the given item at the head - # or tail of the given `DisplayMarker`. - # * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter - # decorations are created by calling {Gutter::decorateMarker} on the - # desired `Gutter` instance. - # * __block__: Positions the view associated with the given item before or - # after the row of the given `TextEditorMarker`. - # - # ## Arguments - # - # * `marker` A {DisplayMarker} you want this decoration to follow. - # * `decorationParams` An {Object} representing the decoration e.g. - # `{type: 'line-number', class: 'linter-error'}` - # * `type` There are several supported decoration types. The behavior of the - # types are as follows: - # * `line` Adds the given `class` to the lines overlapping the rows - # spanned by the `DisplayMarker`. - # * `line-number` Adds the given `class` to the line numbers overlapping - # the rows spanned by the `DisplayMarker`. - # * `text` Injects spans into all text overlapping the marked range, - # then adds the given `class` or `style` properties to these spans. - # Use this to manipulate the foreground color or styling of text in - # a given range. - # * `highlight` Creates an absolutely-positioned `.highlight` div - # containing nested divs to cover the marked region. For example, this - # is used to implement selections. - # * `overlay` Positions the view associated with the given item at the - # head or tail of the given `DisplayMarker`, depending on the `position` - # property. - # * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling - # {Gutter::decorateMarker} on the desired `Gutter` instance. - # * `block` Positions the view associated with the given item before or - # after the row of the given `TextEditorMarker`, depending on the `position` - # property. - # * `cursor` Renders a cursor at the head of the given marker. If multiple - # decorations are created for the same marker, their class strings and - # style objects are combined into a single cursor. You can use this - # decoration type to style existing cursors by passing in their markers - # or render artificial cursors that don't actually exist in the model - # by passing a marker that isn't actually associated with a cursor. - # * `class` This CSS class will be applied to the decorated line number, - # line, text spans, highlight regions, cursors, or overlay. - # * `style` An {Object} containing CSS style properties to apply to the - # relevant DOM node. Currently this only works with a `type` of `cursor` - # or `text`. - # * `item` (optional) An {HTMLElement} or a model {Object} with a - # corresponding view registered. Only applicable to the `gutter`, - # `overlay` and `block` decoration types. - # * `onlyHead` (optional) If `true`, the decoration will only be applied to - # the head of the `DisplayMarker`. Only applicable to the `line` and - # `line-number` decoration types. - # * `onlyEmpty` (optional) If `true`, the decoration will only be applied if - # the associated `DisplayMarker` is empty. Only applicable to the `gutter`, - # `line`, and `line-number` decoration types. - # * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied - # if the associated `DisplayMarker` is non-empty. Only applicable to the - # `gutter`, `line`, and `line-number` decoration types. - # * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied - # to the last row of a non-empty range, even if it ends at column 0. - # Defaults to `true`. Only applicable to the `gutter`, `line`, and - # `line-number` decoration types. - # * `position` (optional) Only applicable to decorations of type `overlay` and `block`. - # Controls where the view is positioned relative to the `TextEditorMarker`. - # Values can be `'head'` (the default) or `'tail'` for overlay decorations, and - # `'before'` (the default) or `'after'` for block decorations. - # * `avoidOverflow` (optional) Only applicable to decorations of type - # `overlay`. Determines whether the decoration adjusts its horizontal or - # vertical position to remain fully visible when it would otherwise - # overflow the editor. Defaults to `true`. - # - # Returns a {Decoration} object - decorateMarker: (marker, decorationParams) -> - @decorationManager.decorateMarker(marker, decorationParams) - - # Essential: Add a decoration to every marker in the given marker layer. Can - # be used to decorate a large number of markers without having to create and - # manage many individual decorations. - # - # * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate. - # * `decorationParams` The same parameters that are passed to - # {TextEditor::decorateMarker}, except the `type` cannot be `overlay` or `gutter`. - # - # Returns a {LayerDecoration}. - decorateMarkerLayer: (markerLayer, decorationParams) -> - @decorationManager.decorateMarkerLayer(markerLayer, decorationParams) - - # Deprecated: Get all the decorations within a screen row range on the default - # layer. - # - # * `startScreenRow` the {Number} beginning screen row - # * `endScreenRow` the {Number} end screen row (inclusive) - # - # Returns an {Object} of decorations in the form - # `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}` - # where the keys are {DisplayMarker} IDs, and the values are an array of decoration - # params objects attached to the marker. - # Returns an empty object when no decorations are found - decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> - @decorationManager.decorationsForScreenRowRange(startScreenRow, endScreenRow) - - decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) -> - @decorationManager.decorationsStateForScreenRowRange(startScreenRow, endScreenRow) - - # Extended: Get all decorations. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getDecorations: (propertyFilter) -> - @decorationManager.getDecorations(propertyFilter) - - # Extended: Get all decorations of type 'line'. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getLineDecorations: (propertyFilter) -> - @decorationManager.getLineDecorations(propertyFilter) - - # Extended: Get all decorations of type 'line-number'. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getLineNumberDecorations: (propertyFilter) -> - @decorationManager.getLineNumberDecorations(propertyFilter) - - # Extended: Get all decorations of type 'highlight'. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getHighlightDecorations: (propertyFilter) -> - @decorationManager.getHighlightDecorations(propertyFilter) - - # Extended: Get all decorations of type 'overlay'. - # - # * `propertyFilter` (optional) An {Object} containing key value pairs that - # the returned decorations' properties must match. - # - # Returns an {Array} of {Decoration}s. - getOverlayDecorations: (propertyFilter) -> - @decorationManager.getOverlayDecorations(propertyFilter) - - ### - Section: Markers - ### - - # Essential: Create a marker on the default marker layer with the given range - # in buffer coordinates. This marker will maintain its logical location as the - # buffer is changed, so if you mark a particular word, the marker will remain - # over that word even if the word's location in the buffer changes. - # - # * `range` A {Range} or range-compatible {Array} - # * `properties` A hash of key-value pairs to associate with the marker. There - # are also reserved property names that have marker-specific meaning. - # * `maintainHistory` (optional) {Boolean} Whether to store this marker's - # range before and after each change in the undo history. This allows the - # marker's position to be restored more accurately for certain undo/redo - # operations, but uses more time and memory. (default: false) - # * `reversed` (optional) {Boolean} Creates the marker in a reversed - # orientation. (default: false) - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # - # Returns a {DisplayMarker}. - markBufferRange: (bufferRange, options) -> - @defaultMarkerLayer.markBufferRange(bufferRange, options) - - # Essential: Create a marker on the default marker layer with the given range - # in screen coordinates. This marker will maintain its logical location as the - # buffer is changed, so if you mark a particular word, the marker will remain - # over that word even if the word's location in the buffer changes. - # - # * `range` A {Range} or range-compatible {Array} - # * `properties` A hash of key-value pairs to associate with the marker. There - # are also reserved property names that have marker-specific meaning. - # * `maintainHistory` (optional) {Boolean} Whether to store this marker's - # range before and after each change in the undo history. This allows the - # marker's position to be restored more accurately for certain undo/redo - # operations, but uses more time and memory. (default: false) - # * `reversed` (optional) {Boolean} Creates the marker in a reversed - # orientation. (default: false) - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # - # Returns a {DisplayMarker}. - markScreenRange: (screenRange, options) -> - @defaultMarkerLayer.markScreenRange(screenRange, options) - - # Essential: Create a marker on the default marker layer with the given buffer - # position and no tail. To group multiple markers together in their own - # private layer, see {::addMarkerLayer}. - # - # * `bufferPosition` A {Point} or point-compatible {Array} - # * `options` (optional) An {Object} with the following keys: - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # - # Returns a {DisplayMarker}. - markBufferPosition: (bufferPosition, options) -> - @defaultMarkerLayer.markBufferPosition(bufferPosition, options) - - # Essential: Create a marker on the default marker layer with the given screen - # position and no tail. To group multiple markers together in their own - # private layer, see {::addMarkerLayer}. - # - # * `screenPosition` A {Point} or point-compatible {Array} - # * `options` (optional) An {Object} with the following keys: - # * `invalidate` (optional) {String} Determines the rules by which changes - # to the buffer *invalidate* the marker. (default: 'overlap') It can be - # any of the following strategies, in order of fragility: - # * __never__: The marker is never marked as invalid. This is a good choice for - # markers representing selections in an editor. - # * __surround__: The marker is invalidated by changes that completely surround it. - # * __overlap__: The marker is invalidated by changes that surround the - # start or end of the marker. This is the default. - # * __inside__: The marker is invalidated by changes that extend into the - # inside of the marker. Changes that end at the marker's start or - # start at the marker's end do not invalidate the marker. - # * __touch__: The marker is invalidated by a change that touches the marked - # region in any way, including changes that end at the marker's - # start or start at the marker's end. This is the most fragile strategy. - # * `clipDirection` {String} If `'backward'`, returns the first valid - # position preceding an invalid position. If `'forward'`, returns the - # first valid position following an invalid position. If `'closest'`, - # returns the first valid position closest to an invalid position. - # Defaults to `'closest'`. - # - # Returns a {DisplayMarker}. - markScreenPosition: (screenPosition, options) -> - @defaultMarkerLayer.markScreenPosition(screenPosition, options) - - # Essential: Find all {DisplayMarker}s on the default marker layer that - # match the given properties. - # - # This method finds markers based on the given properties. Markers can be - # associated with custom properties that will be compared with basic equality. - # In addition, there are several special properties that will be compared - # with the range of the markers rather than their properties. - # - # * `properties` An {Object} containing properties that each returned marker - # must satisfy. Markers can be associated with custom properties, which are - # compared with basic equality. In addition, several reserved properties - # can be used to filter markers based on their current range: - # * `startBufferRow` Only include markers starting at this row in buffer - # coordinates. - # * `endBufferRow` Only include markers ending at this row in buffer - # coordinates. - # * `containsBufferRange` Only include markers containing this {Range} or - # in range-compatible {Array} in buffer coordinates. - # * `containsBufferPosition` Only include markers containing this {Point} - # or {Array} of `[row, column]` in buffer coordinates. - # - # Returns an {Array} of {DisplayMarker}s - findMarkers: (params) -> - @defaultMarkerLayer.findMarkers(params) - - # Extended: Get the {DisplayMarker} on the default layer for the given - # marker id. - # - # * `id` {Number} id of the marker - getMarker: (id) -> - @defaultMarkerLayer.getMarker(id) - - # Extended: Get all {DisplayMarker}s on the default marker layer. Consider - # using {::findMarkers} - getMarkers: -> - @defaultMarkerLayer.getMarkers() - - # Extended: Get the number of markers in the default marker layer. - # - # Returns a {Number}. - getMarkerCount: -> - @defaultMarkerLayer.getMarkerCount() - - destroyMarker: (id) -> - @getMarker(id)?.destroy() - - # Essential: Create a marker layer to group related markers. - # - # * `options` An {Object} containing the following keys: - # * `maintainHistory` A {Boolean} indicating whether marker state should be - # restored on undo/redo. Defaults to `false`. - # * `persistent` A {Boolean} indicating whether or not this marker layer - # should be serialized and deserialized along with the rest of the - # buffer. Defaults to `false`. If `true`, the marker layer's id will be - # maintained across the serialization boundary, allowing you to retrieve - # it via {::getMarkerLayer}. - # - # Returns a {DisplayMarkerLayer}. - addMarkerLayer: (options) -> - @displayLayer.addMarkerLayer(options) - - # Essential: Get a {DisplayMarkerLayer} by id. - # - # * `id` The id of the marker layer to retrieve. - # - # Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the - # given id. - getMarkerLayer: (id) -> - @displayLayer.getMarkerLayer(id) - - # Essential: Get the default {DisplayMarkerLayer}. - # - # All marker APIs not tied to an explicit layer interact with this default - # layer. - # - # Returns a {DisplayMarkerLayer}. - getDefaultMarkerLayer: -> - @defaultMarkerLayer - - ### - Section: Cursors - ### - - # Essential: Get the position of the most recently added cursor in buffer - # coordinates. - # - # Returns a {Point} - getCursorBufferPosition: -> - @getLastCursor().getBufferPosition() - - # Essential: Get the position of all the cursor positions in buffer coordinates. - # - # Returns {Array} of {Point}s in the order they were added - getCursorBufferPositions: -> - cursor.getBufferPosition() for cursor in @getCursors() - - # Essential: Move the cursor to the given position in buffer coordinates. - # - # If there are multiple cursors, they will be consolidated to a single cursor. - # - # * `position` A {Point} or {Array} of `[row, column]` - # * `options` (optional) An {Object} containing the following keys: - # * `autoscroll` Determines whether the editor scrolls to the new cursor's - # position. Defaults to true. - setCursorBufferPosition: (position, options) -> - @moveCursors (cursor) -> cursor.setBufferPosition(position, options) - - # Essential: Get a {Cursor} at given screen coordinates {Point} - # - # * `position` A {Point} or {Array} of `[row, column]` - # - # Returns the first matched {Cursor} or undefined - getCursorAtScreenPosition: (position) -> - if selection = @getSelectionAtScreenPosition(position) - if selection.getHeadScreenPosition().isEqual(position) - selection.cursor - - # Essential: Get the position of the most recently added cursor in screen - # coordinates. - # - # Returns a {Point}. - getCursorScreenPosition: -> - @getLastCursor().getScreenPosition() - - # Essential: Get the position of all the cursor positions in screen coordinates. - # - # Returns {Array} of {Point}s in the order the cursors were added - getCursorScreenPositions: -> - cursor.getScreenPosition() for cursor in @getCursors() - - # Essential: Move the cursor to the given position in screen coordinates. - # - # If there are multiple cursors, they will be consolidated to a single cursor. - # - # * `position` A {Point} or {Array} of `[row, column]` - # * `options` (optional) An {Object} combining options for {::clipScreenPosition} with: - # * `autoscroll` Determines whether the editor scrolls to the new cursor's - # position. Defaults to true. - setCursorScreenPosition: (position, options) -> - if options?.clip? - Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.") - options.clipDirection ?= options.clip - if options?.wrapAtSoftNewlines? - Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward' - if options?.wrapBeyondNewlines? - Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") - options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward' - - @moveCursors (cursor) -> cursor.setScreenPosition(position, options) - - # Essential: Add a cursor at the given position in buffer coordinates. - # - # * `bufferPosition` A {Point} or {Array} of `[row, column]` - # - # Returns a {Cursor}. - addCursorAtBufferPosition: (bufferPosition, options) -> - @selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'}) - @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false - @getLastSelection().cursor - - # Essential: Add a cursor at the position in screen coordinates. - # - # * `screenPosition` A {Point} or {Array} of `[row, column]` - # - # Returns a {Cursor}. - addCursorAtScreenPosition: (screenPosition, options) -> - @selectionsMarkerLayer.markScreenPosition(screenPosition, {invalidate: 'never'}) - @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false - @getLastSelection().cursor - - # Essential: Returns {Boolean} indicating whether or not there are multiple cursors. - hasMultipleCursors: -> - @getCursors().length > 1 - - # Essential: Move every cursor up one row in screen coordinates. - # - # * `lineCount` (optional) {Number} number of lines to move - moveUp: (lineCount) -> - @moveCursors (cursor) -> cursor.moveUp(lineCount, moveToEndOfSelection: true) - - # Essential: Move every cursor down one row in screen coordinates. - # - # * `lineCount` (optional) {Number} number of lines to move - moveDown: (lineCount) -> - @moveCursors (cursor) -> cursor.moveDown(lineCount, moveToEndOfSelection: true) - - # Essential: Move every cursor left one column. - # - # * `columnCount` (optional) {Number} number of columns to move (default: 1) - moveLeft: (columnCount) -> - @moveCursors (cursor) -> cursor.moveLeft(columnCount, moveToEndOfSelection: true) - - # Essential: Move every cursor right one column. - # - # * `columnCount` (optional) {Number} number of columns to move (default: 1) - moveRight: (columnCount) -> - @moveCursors (cursor) -> cursor.moveRight(columnCount, moveToEndOfSelection: true) - - # Essential: Move every cursor to the beginning of its line in buffer coordinates. - moveToBeginningOfLine: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfLine() - - # Essential: Move every cursor to the beginning of its line in screen coordinates. - moveToBeginningOfScreenLine: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfScreenLine() - - # Essential: Move every cursor to the first non-whitespace character of its line. - moveToFirstCharacterOfLine: -> - @moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine() - - # Essential: Move every cursor to the end of its line in buffer coordinates. - moveToEndOfLine: -> - @moveCursors (cursor) -> cursor.moveToEndOfLine() - - # Essential: Move every cursor to the end of its line in screen coordinates. - moveToEndOfScreenLine: -> - @moveCursors (cursor) -> cursor.moveToEndOfScreenLine() - - # Essential: Move every cursor to the beginning of its surrounding word. - moveToBeginningOfWord: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfWord() - - # Essential: Move every cursor to the end of its surrounding word. - moveToEndOfWord: -> - @moveCursors (cursor) -> cursor.moveToEndOfWord() - - # Cursor Extended - - # Extended: Move every cursor to the top of the buffer. - # - # If there are multiple cursors, they will be merged into a single cursor. - moveToTop: -> - @moveCursors (cursor) -> cursor.moveToTop() - - # Extended: Move every cursor to the bottom of the buffer. - # - # If there are multiple cursors, they will be merged into a single cursor. - moveToBottom: -> - @moveCursors (cursor) -> cursor.moveToBottom() - - # Extended: Move every cursor to the beginning of the next word. - moveToBeginningOfNextWord: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfNextWord() - - # Extended: Move every cursor to the previous word boundary. - moveToPreviousWordBoundary: -> - @moveCursors (cursor) -> cursor.moveToPreviousWordBoundary() - - # Extended: Move every cursor to the next word boundary. - moveToNextWordBoundary: -> - @moveCursors (cursor) -> cursor.moveToNextWordBoundary() - - # Extended: Move every cursor to the previous subword boundary. - moveToPreviousSubwordBoundary: -> - @moveCursors (cursor) -> cursor.moveToPreviousSubwordBoundary() - - # Extended: Move every cursor to the next subword boundary. - moveToNextSubwordBoundary: -> - @moveCursors (cursor) -> cursor.moveToNextSubwordBoundary() - - # Extended: Move every cursor to the beginning of the next paragraph. - moveToBeginningOfNextParagraph: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfNextParagraph() - - # Extended: Move every cursor to the beginning of the previous paragraph. - moveToBeginningOfPreviousParagraph: -> - @moveCursors (cursor) -> cursor.moveToBeginningOfPreviousParagraph() - - # Extended: Returns the most recently added {Cursor} - getLastCursor: -> - @createLastSelectionIfNeeded() - _.last(@cursors) - - # Extended: Returns the word surrounding the most recently added cursor. - # - # * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}. - getWordUnderCursor: (options) -> - @getTextInBufferRange(@getLastCursor().getCurrentWordBufferRange(options)) - - # Extended: Get an Array of all {Cursor}s. - getCursors: -> - @createLastSelectionIfNeeded() - @cursors.slice() - - # Extended: Get all {Cursors}s, ordered by their position in the buffer - # instead of the order in which they were added. - # - # Returns an {Array} of {Selection}s. - getCursorsOrderedByBufferPosition: -> - @getCursors().sort (a, b) -> a.compare(b) - - cursorsForScreenRowRange: (startScreenRow, endScreenRow) -> - cursors = [] - for marker in @selectionsMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) - if cursor = @cursorsByMarkerId.get(marker.id) - cursors.push(cursor) - cursors - - # Add a cursor based on the given {DisplayMarker}. - addCursor: (marker) -> - cursor = new Cursor(editor: this, marker: marker, showCursorOnSelection: @showCursorOnSelection) - @cursors.push(cursor) - @cursorsByMarkerId.set(marker.id, cursor) - cursor - - moveCursors: (fn) -> - @transact => - fn(cursor) for cursor in @getCursors() - @mergeCursors() - - cursorMoved: (event) -> - @emitter.emit 'did-change-cursor-position', event - - # Merge cursors that have the same screen position - mergeCursors: -> - positions = {} - for cursor in @getCursors() - position = cursor.getBufferPosition().toString() - if positions.hasOwnProperty(position) - cursor.destroy() - else - positions[position] = true - return - - ### - Section: Selections - ### - - # Essential: Get the selected text of the most recently added selection. - # - # Returns a {String}. - getSelectedText: -> - @getLastSelection().getText() - - # Essential: Get the {Range} of the most recently added selection in buffer - # coordinates. - # - # Returns a {Range}. - getSelectedBufferRange: -> - @getLastSelection().getBufferRange() - - # Essential: Get the {Range}s of all selections in buffer coordinates. - # - # The ranges are sorted by when the selections were added. Most recent at the end. - # - # Returns an {Array} of {Range}s. - getSelectedBufferRanges: -> - selection.getBufferRange() for selection in @getSelections() - - # Essential: Set the selected range in buffer coordinates. If there are multiple - # selections, they are reduced to a single selection with the given range. - # - # * `bufferRange` A {Range} or range-compatible {Array}. - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the - # selection is set. - setSelectedBufferRange: (bufferRange, options) -> - @setSelectedBufferRanges([bufferRange], options) - - # Essential: Set the selected ranges in buffer coordinates. If there are multiple - # selections, they are replaced by new selections with the given ranges. - # - # * `bufferRanges` An {Array} of {Range}s or range-compatible {Array}s. - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the - # selection is set. - setSelectedBufferRanges: (bufferRanges, options={}) -> - throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length - - selections = @getSelections() - selection.destroy() for selection in selections[bufferRanges.length...] - - @mergeIntersectingSelections options, => - for bufferRange, i in bufferRanges - bufferRange = Range.fromObject(bufferRange) - if selections[i] - selections[i].setBufferRange(bufferRange, options) - else - @addSelectionForBufferRange(bufferRange, options) - return - - # Essential: Get the {Range} of the most recently added selection in screen - # coordinates. - # - # Returns a {Range}. - getSelectedScreenRange: -> - @getLastSelection().getScreenRange() - - # Essential: Get the {Range}s of all selections in screen coordinates. - # - # The ranges are sorted by when the selections were added. Most recent at the end. - # - # Returns an {Array} of {Range}s. - getSelectedScreenRanges: -> - selection.getScreenRange() for selection in @getSelections() - - # Essential: Set the selected range in screen coordinates. If there are multiple - # selections, they are reduced to a single selection with the given range. - # - # * `screenRange` A {Range} or range-compatible {Array}. - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - setSelectedScreenRange: (screenRange, options) -> - @setSelectedBufferRange(@bufferRangeForScreenRange(screenRange, options), options) - - # Essential: Set the selected ranges in screen coordinates. If there are multiple - # selections, they are replaced by new selections with the given ranges. - # - # * `screenRanges` An {Array} of {Range}s or range-compatible {Array}s. - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - setSelectedScreenRanges: (screenRanges, options={}) -> - throw new Error("Passed an empty array to setSelectedScreenRanges") unless screenRanges.length - - selections = @getSelections() - selection.destroy() for selection in selections[screenRanges.length...] - - @mergeIntersectingSelections options, => - for screenRange, i in screenRanges - screenRange = Range.fromObject(screenRange) - if selections[i] - selections[i].setScreenRange(screenRange, options) - else - @addSelectionForScreenRange(screenRange, options) - return - - # Essential: Add a selection for the given range in buffer coordinates. - # - # * `bufferRange` A {Range} - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the - # selection is set. - # - # Returns the added {Selection}. - addSelectionForBufferRange: (bufferRange, options={}) -> - bufferRange = Range.fromObject(bufferRange) - unless options.preserveFolds - @displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) - @selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed ? false}) - @getLastSelection().autoscroll() unless options.autoscroll is false - @getLastSelection() - - # Essential: Add a selection for the given range in screen coordinates. - # - # * `screenRange` A {Range} - # * `options` (optional) An options {Object}: - # * `reversed` A {Boolean} indicating whether to create the selection in a - # reversed orientation. - # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the - # selection is set. - # Returns the added {Selection}. - addSelectionForScreenRange: (screenRange, options={}) -> - @addSelectionForBufferRange(@bufferRangeForScreenRange(screenRange), options) - - # Essential: Select from the current cursor position to the given position in - # buffer coordinates. - # - # This method may merge selections that end up intersecting. - # - # * `position` An instance of {Point}, with a given `row` and `column`. - selectToBufferPosition: (position) -> - lastSelection = @getLastSelection() - lastSelection.selectToBufferPosition(position) - @mergeIntersectingSelections(reversed: lastSelection.isReversed()) - - # Essential: Select from the current cursor position to the given position in - # screen coordinates. - # - # This method may merge selections that end up intersecting. - # - # * `position` An instance of {Point}, with a given `row` and `column`. - selectToScreenPosition: (position, options) -> - lastSelection = @getLastSelection() - lastSelection.selectToScreenPosition(position, options) - unless options?.suppressSelectionMerge - @mergeIntersectingSelections(reversed: lastSelection.isReversed()) - - # Essential: Move the cursor of each selection one character upward while - # preserving the selection's tail position. - # - # * `rowCount` (optional) {Number} number of rows to select (default: 1) - # - # This method may merge selections that end up intersecting. - selectUp: (rowCount) -> - @expandSelectionsBackward (selection) -> selection.selectUp(rowCount) - - # Essential: Move the cursor of each selection one character downward while - # preserving the selection's tail position. - # - # * `rowCount` (optional) {Number} number of rows to select (default: 1) - # - # This method may merge selections that end up intersecting. - selectDown: (rowCount) -> - @expandSelectionsForward (selection) -> selection.selectDown(rowCount) - - # Essential: Move the cursor of each selection one character leftward while - # preserving the selection's tail position. - # - # * `columnCount` (optional) {Number} number of columns to select (default: 1) - # - # This method may merge selections that end up intersecting. - selectLeft: (columnCount) -> - @expandSelectionsBackward (selection) -> selection.selectLeft(columnCount) - - # Essential: Move the cursor of each selection one character rightward while - # preserving the selection's tail position. - # - # * `columnCount` (optional) {Number} number of columns to select (default: 1) - # - # This method may merge selections that end up intersecting. - selectRight: (columnCount) -> - @expandSelectionsForward (selection) -> selection.selectRight(columnCount) - - # Essential: Select from the top of the buffer to the end of the last selection - # in the buffer. - # - # This method merges multiple selections into a single selection. - selectToTop: -> - @expandSelectionsBackward (selection) -> selection.selectToTop() - - # Essential: Selects from the top of the first selection in the buffer to the end - # of the buffer. - # - # This method merges multiple selections into a single selection. - selectToBottom: -> - @expandSelectionsForward (selection) -> selection.selectToBottom() - - # Essential: Select all text in the buffer. - # - # This method merges multiple selections into a single selection. - selectAll: -> - @expandSelectionsForward (selection) -> selection.selectAll() - - # Essential: Move the cursor of each selection to the beginning of its line - # while preserving the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToBeginningOfLine: -> - @expandSelectionsBackward (selection) -> selection.selectToBeginningOfLine() - - # Essential: Move the cursor of each selection to the first non-whitespace - # character of its line while preserving the selection's tail position. If the - # cursor is already on the first character of the line, move it to the - # beginning of the line. - # - # This method may merge selections that end up intersecting. - selectToFirstCharacterOfLine: -> - @expandSelectionsBackward (selection) -> selection.selectToFirstCharacterOfLine() - - # Essential: Move the cursor of each selection to the end of its line while - # preserving the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToEndOfLine: -> - @expandSelectionsForward (selection) -> selection.selectToEndOfLine() - - # Essential: Expand selections to the beginning of their containing word. - # - # Operates on all selections. Moves the cursor to the beginning of the - # containing word while preserving the selection's tail position. - selectToBeginningOfWord: -> - @expandSelectionsBackward (selection) -> selection.selectToBeginningOfWord() - - # Essential: Expand selections to the end of their containing word. - # - # Operates on all selections. Moves the cursor to the end of the containing - # word while preserving the selection's tail position. - selectToEndOfWord: -> - @expandSelectionsForward (selection) -> selection.selectToEndOfWord() - - # Extended: For each selection, move its cursor to the preceding subword - # boundary while maintaining the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToPreviousSubwordBoundary: -> - @expandSelectionsBackward (selection) -> selection.selectToPreviousSubwordBoundary() - - # Extended: For each selection, move its cursor to the next subword boundary - # while maintaining the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToNextSubwordBoundary: -> - @expandSelectionsForward (selection) -> selection.selectToNextSubwordBoundary() - - # Essential: For each cursor, select the containing line. - # - # This method merges selections on successive lines. - selectLinesContainingCursors: -> - @expandSelectionsForward (selection) -> selection.selectLine() - - # Essential: Select the word surrounding each cursor. - selectWordsContainingCursors: -> - @expandSelectionsForward (selection) -> selection.selectWord() - - # Selection Extended - - # Extended: For each selection, move its cursor to the preceding word boundary - # while maintaining the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToPreviousWordBoundary: -> - @expandSelectionsBackward (selection) -> selection.selectToPreviousWordBoundary() - - # Extended: For each selection, move its cursor to the next word boundary while - # maintaining the selection's tail position. - # - # This method may merge selections that end up intersecting. - selectToNextWordBoundary: -> - @expandSelectionsForward (selection) -> selection.selectToNextWordBoundary() - - # Extended: Expand selections to the beginning of the next word. - # - # Operates on all selections. Moves the cursor to the beginning of the next - # word while preserving the selection's tail position. - selectToBeginningOfNextWord: -> - @expandSelectionsForward (selection) -> selection.selectToBeginningOfNextWord() - - # Extended: Expand selections to the beginning of the next paragraph. - # - # Operates on all selections. Moves the cursor to the beginning of the next - # paragraph while preserving the selection's tail position. - selectToBeginningOfNextParagraph: -> - @expandSelectionsForward (selection) -> selection.selectToBeginningOfNextParagraph() - - # Extended: Expand selections to the beginning of the next paragraph. - # - # Operates on all selections. Moves the cursor to the beginning of the next - # paragraph while preserving the selection's tail position. - selectToBeginningOfPreviousParagraph: -> - @expandSelectionsBackward (selection) -> selection.selectToBeginningOfPreviousParagraph() - - # Extended: Select the range of the given marker if it is valid. - # - # * `marker` A {DisplayMarker} - # - # Returns the selected {Range} or `undefined` if the marker is invalid. - selectMarker: (marker) -> - if marker.isValid() - range = marker.getBufferRange() - @setSelectedBufferRange(range) - range - - # Extended: Get the most recently added {Selection}. - # - # Returns a {Selection}. - getLastSelection: -> - @createLastSelectionIfNeeded() - _.last(@selections) - - getSelectionAtScreenPosition: (position) -> - markers = @selectionsMarkerLayer.findMarkers(containsScreenPosition: position) - if markers.length > 0 - @cursorsByMarkerId.get(markers[0].id).selection - - # Extended: Get current {Selection}s. - # - # Returns: An {Array} of {Selection}s. - getSelections: -> - @createLastSelectionIfNeeded() - @selections.slice() - - # Extended: Get all {Selection}s, ordered by their position in the buffer - # instead of the order in which they were added. - # - # Returns an {Array} of {Selection}s. - getSelectionsOrderedByBufferPosition: -> - @getSelections().sort (a, b) -> a.compare(b) - - # Extended: Determine if a given range in buffer coordinates intersects a - # selection. - # - # * `bufferRange` A {Range} or range-compatible {Array}. - # - # Returns a {Boolean}. - selectionIntersectsBufferRange: (bufferRange) -> - _.any @getSelections(), (selection) -> - selection.intersectsBufferRange(bufferRange) - - # Selections Private - - # Add a similarly-shaped selection to the next eligible line below - # each selection. - # - # Operates on all selections. If the selection is empty, adds an empty - # selection to the next following non-empty line as close to the current - # selection's column as possible. If the selection is non-empty, adds a - # selection to the next line that is long enough for a non-empty selection - # starting at the same column as the current selection to be added to it. - addSelectionBelow: -> - @expandSelectionsForward (selection) -> selection.addSelectionBelow() - - # Add a similarly-shaped selection to the next eligible line above - # each selection. - # - # Operates on all selections. If the selection is empty, adds an empty - # selection to the next preceding non-empty line as close to the current - # selection's column as possible. If the selection is non-empty, adds a - # selection to the next line that is long enough for a non-empty selection - # starting at the same column as the current selection to be added to it. - addSelectionAbove: -> - @expandSelectionsBackward (selection) -> selection.addSelectionAbove() - - # Calls the given function with each selection, then merges selections - expandSelectionsForward: (fn) -> - @mergeIntersectingSelections => - fn(selection) for selection in @getSelections() - return - - # Calls the given function with each selection, then merges selections in the - # reversed orientation - expandSelectionsBackward: (fn) -> - @mergeIntersectingSelections reversed: true, => - fn(selection) for selection in @getSelections() - return - - finalizeSelections: -> - selection.finalize() for selection in @getSelections() - return - - selectionsForScreenRows: (startRow, endRow) -> - @getSelections().filter (selection) -> selection.intersectsScreenRowRange(startRow, endRow) - - # Merges intersecting selections. If passed a function, it executes - # the function with merging suppressed, then merges intersecting selections - # afterward. - mergeIntersectingSelections: (args...) -> - @mergeSelections args..., (previousSelection, currentSelection) -> - exclusive = not currentSelection.isEmpty() and not previousSelection.isEmpty() - - previousSelection.intersectsWith(currentSelection, exclusive) - - mergeSelectionsOnSameRows: (args...) -> - @mergeSelections args..., (previousSelection, currentSelection) -> - screenRange = currentSelection.getScreenRange() - - previousSelection.intersectsScreenRowRange(screenRange.start.row, screenRange.end.row) - - avoidMergingSelections: (args...) -> - @mergeSelections args..., -> false - - mergeSelections: (args...) -> - mergePredicate = args.pop() - fn = args.pop() if _.isFunction(_.last(args)) - options = args.pop() ? {} - - return fn?() if @suppressSelectionMerging - - if fn? - @suppressSelectionMerging = true - result = fn() - @suppressSelectionMerging = false - - reducer = (disjointSelections, selection) -> - adjacentSelection = _.last(disjointSelections) - if mergePredicate(adjacentSelection, selection) - adjacentSelection.merge(selection, options) - disjointSelections - else - disjointSelections.concat([selection]) - - [head, tail...] = @getSelectionsOrderedByBufferPosition() - _.reduce(tail, reducer, [head]) - return result if fn? - - # Add a {Selection} based on the given {DisplayMarker}. - # - # * `marker` The {DisplayMarker} to highlight - # * `options` (optional) An {Object} that pertains to the {Selection} constructor. - # - # Returns the new {Selection}. - addSelection: (marker, options={}) -> - cursor = @addCursor(marker) - selection = new Selection(Object.assign({editor: this, marker, cursor}, options)) - @selections.push(selection) - selectionBufferRange = selection.getBufferRange() - @mergeIntersectingSelections(preserveFolds: options.preserveFolds) - - if selection.destroyed - for selection in @getSelections() - if selection.intersectsBufferRange(selectionBufferRange) - return selection - else - @emitter.emit 'did-add-cursor', cursor - @emitter.emit 'did-add-selection', selection - selection - - # Remove the given selection. - removeSelection: (selection) -> - _.remove(@cursors, selection.cursor) - _.remove(@selections, selection) - @cursorsByMarkerId.delete(selection.cursor.marker.id) - @emitter.emit 'did-remove-cursor', selection.cursor - @emitter.emit 'did-remove-selection', selection - - # Reduce one or more selections to a single empty selection based on the most - # recently added cursor. - clearSelections: (options) -> - @consolidateSelections() - @getLastSelection().clear(options) - - # Reduce multiple selections to the least recently added selection. - consolidateSelections: -> - selections = @getSelections() - if selections.length > 1 - selection.destroy() for selection in selections[1...(selections.length)] - selections[0].autoscroll(center: true) - true - else - false - - # Called by the selection - selectionRangeChanged: (event) -> - @component?.didChangeSelectionRange() - @emitter.emit 'did-change-selection-range', event - - createLastSelectionIfNeeded: -> - if @selections.length is 0 - @addSelectionForBufferRange([[0, 0], [0, 0]], autoscroll: false, preserveFolds: true) - - ### - Section: Searching and Replacing - ### - - # Essential: Scan regular expression matches in the entire buffer, calling the - # given iterator function on each match. - # - # `::scan` functions as the replace method as well via the `replace` - # - # If you're programmatically modifying the results, you may want to try - # {::backwardsScanInBufferRange} to avoid tripping over your own changes. - # - # * `regex` A {RegExp} to search for. - # * `options` (optional) {Object} - # * `leadingContextLineCount` {Number} default `0`; The number of lines - # before the matched line to include in the results object. - # * `trailingContextLineCount` {Number} default `0`; The number of lines - # after the matched line to include in the results object. - # * `iterator` A {Function} that's called on each match - # * `object` {Object} - # * `match` The current regular expression match. - # * `matchText` A {String} with the text of the match. - # * `range` The {Range} of the match. - # * `stop` Call this {Function} to terminate the scan. - # * `replace` Call this {Function} with a {String} to replace the match. - scan: (regex, options={}, iterator) -> - if _.isFunction(options) - iterator = options - options = {} - - @buffer.scan(regex, options, iterator) - - # Essential: Scan regular expression matches in a given range, calling the given - # iterator function on each match. - # - # * `regex` A {RegExp} to search for. - # * `range` A {Range} in which to search. - # * `iterator` A {Function} that's called on each match with an {Object} - # containing the following keys: - # * `match` The current regular expression match. - # * `matchText` A {String} with the text of the match. - # * `range` The {Range} of the match. - # * `stop` Call this {Function} to terminate the scan. - # * `replace` Call this {Function} with a {String} to replace the match. - scanInBufferRange: (regex, range, iterator) -> @buffer.scanInRange(regex, range, iterator) - - # Essential: Scan regular expression matches in a given range in reverse order, - # calling the given iterator function on each match. - # - # * `regex` A {RegExp} to search for. - # * `range` A {Range} in which to search. - # * `iterator` A {Function} that's called on each match with an {Object} - # containing the following keys: - # * `match` The current regular expression match. - # * `matchText` A {String} with the text of the match. - # * `range` The {Range} of the match. - # * `stop` Call this {Function} to terminate the scan. - # * `replace` Call this {Function} with a {String} to replace the match. - backwardsScanInBufferRange: (regex, range, iterator) -> @buffer.backwardsScanInRange(regex, range, iterator) - - ### - Section: Tab Behavior - ### - - # Essential: Returns a {Boolean} indicating whether softTabs are enabled for this - # editor. - getSoftTabs: -> @softTabs - - # Essential: Enable or disable soft tabs for this editor. - # - # * `softTabs` A {Boolean} - setSoftTabs: (@softTabs) -> @update({@softTabs}) - - # Returns a {Boolean} indicating whether atomic soft tabs are enabled for this editor. - hasAtomicSoftTabs: -> @displayLayer.atomicSoftTabs - - # Essential: Toggle soft tabs for this editor - toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs()) - - # Essential: Get the on-screen length of tab characters. - # - # Returns a {Number}. - getTabLength: -> @tokenizedBuffer.getTabLength() - - # Essential: Set the on-screen length of tab characters. Setting this to a - # {Number} This will override the `editor.tabLength` setting. - # - # * `tabLength` {Number} length of a single tab. Setting to `null` will - # fallback to using the `editor.tabLength` config setting - setTabLength: (tabLength) -> @update({tabLength}) - - # Returns an {Object} representing the current invisible character - # substitutions for this editor. See {::setInvisibles}. - getInvisibles: -> - if not @mini and @showInvisibles and @invisibles? - @invisibles - else - {} - - doesShowIndentGuide: -> @showIndentGuide and not @mini - - getSoftWrapHangingIndentLength: -> @displayLayer.softWrapHangingIndent - - # Extended: Determine if the buffer uses hard or soft tabs. - # - # Returns `true` if the first non-comment line with leading whitespace starts - # with a space character. Returns `false` if it starts with a hard tab (`\t`). - # - # Returns a {Boolean} or undefined if no non-comment lines had leading - # whitespace. - usesSoftTabs: -> - for bufferRow in [0..Math.min(1000, @buffer.getLastRow())] - continue if @tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() - - line = @buffer.lineForRow(bufferRow) - return true if line[0] is ' ' - return false if line[0] is '\t' - - undefined - - # Extended: Get the text representing a single level of indent. - # - # If soft tabs are enabled, the text is composed of N spaces, where N is the - # tab length. Otherwise the text is a tab character (`\t`). - # - # Returns a {String}. - getTabText: -> @buildIndentString(1) - - # If soft tabs are enabled, convert all hard tabs to soft tabs in the given - # {Range}. - normalizeTabsInBufferRange: (bufferRange) -> - return unless @getSoftTabs() - @scanInBufferRange /\t/g, bufferRange, ({replace}) => replace(@getTabText()) - - ### - Section: Soft Wrap Behavior - ### - - # Essential: Determine whether lines in this editor are soft-wrapped. - # - # Returns a {Boolean}. - isSoftWrapped: -> @softWrapped - - # Essential: Enable or disable soft wrapping for this editor. - # - # * `softWrapped` A {Boolean} - # - # Returns a {Boolean}. - setSoftWrapped: (softWrapped) -> - @update({softWrapped}) - @isSoftWrapped() - - getPreferredLineLength: -> @preferredLineLength - - # Essential: Toggle soft wrapping for this editor - # - # Returns a {Boolean}. - toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped()) - - # Essential: Gets the column at which column will soft wrap - getSoftWrapColumn: -> - if @isSoftWrapped() and not @mini - if @softWrapAtPreferredLineLength - Math.min(@getEditorWidthInChars(), @preferredLineLength) - else - @getEditorWidthInChars() - else - @maxScreenLineLength - - ### - Section: Indentation - ### - - # Essential: Get the indentation level of the given buffer row. - # - # Determines how deeply the given row is indented based on the soft tabs and - # tab length settings of this editor. Note that if soft tabs are enabled and - # the tab length is 2, a row with 4 leading spaces would have an indentation - # level of 2. - # - # * `bufferRow` A {Number} indicating the buffer row. - # - # Returns a {Number}. - indentationForBufferRow: (bufferRow) -> - @indentLevelForLine(@lineTextForBufferRow(bufferRow)) - - # Essential: Set the indentation level for the given buffer row. - # - # Inserts or removes hard tabs or spaces based on the soft tabs and tab length - # settings of this editor in order to bring it to the given indentation level. - # Note that if soft tabs are enabled and the tab length is 2, a row with 4 - # leading spaces would have an indentation level of 2. - # - # * `bufferRow` A {Number} indicating the buffer row. - # * `newLevel` A {Number} indicating the new indentation level. - # * `options` (optional) An {Object} with the following keys: - # * `preserveLeadingWhitespace` `true` to preserve any whitespace already at - # the beginning of the line (default: false). - setIndentationForBufferRow: (bufferRow, newLevel, {preserveLeadingWhitespace}={}) -> - if preserveLeadingWhitespace - endColumn = 0 - else - endColumn = @lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length - newIndentString = @buildIndentString(newLevel) - @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) - - # Extended: Indent rows intersecting selections by one level. - indentSelectedRows: -> - @mutateSelectedText (selection) -> selection.indentSelectedRows() - - # Extended: Outdent rows intersecting selections by one level. - outdentSelectedRows: -> - @mutateSelectedText (selection) -> selection.outdentSelectedRows() - - # Extended: Get the indentation level of the given line of text. - # - # Determines how deeply the given line is indented based on the soft tabs and - # tab length settings of this editor. Note that if soft tabs are enabled and - # the tab length is 2, a row with 4 leading spaces would have an indentation - # level of 2. - # - # * `line` A {String} representing a line of text. - # - # Returns a {Number}. - indentLevelForLine: (line) -> - @tokenizedBuffer.indentLevelForLine(line) - - # Extended: Indent rows intersecting selections based on the grammar's suggested - # indent level. - autoIndentSelectedRows: -> - @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() - - # Indent all lines intersecting selections. See {Selection::indent} for more - # information. - indent: (options={}) -> - options.autoIndent ?= @shouldAutoIndent() - @mutateSelectedText (selection) -> selection.indent(options) - - # Constructs the string used for indents. - buildIndentString: (level, column=0) -> - if @getSoftTabs() - tabStopViolation = column % @getTabLength() - _.multiplyString(" ", Math.floor(level * @getTabLength()) - tabStopViolation) - else - excessWhitespace = _.multiplyString(' ', Math.round((level - Math.floor(level)) * @getTabLength())) - _.multiplyString("\t", Math.floor(level)) + excessWhitespace - - ### - Section: Grammars - ### - - # Essential: Get the current {Grammar} of this editor. - getGrammar: -> - @tokenizedBuffer.grammar - - # Essential: Set the current {Grammar} of this editor. - # - # Assigning a grammar will cause the editor to re-tokenize based on the new - # grammar. - # - # * `grammar` {Grammar} - setGrammar: (grammar) -> - @tokenizedBuffer.setGrammar(grammar) - - # Reload the grammar based on the file name. - reloadGrammar: -> - @tokenizedBuffer.reloadGrammar() - - # Experimental: Get a notification when async tokenization is completed. - onDidTokenize: (callback) -> - @tokenizedBuffer.onDidTokenize(callback) - - ### - Section: Managing Syntax Scopes - ### - - # Essential: Returns a {ScopeDescriptor} that includes this editor's language. - # e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with - # {Config::get} to get language specific config values. - getRootScopeDescriptor: -> - @tokenizedBuffer.rootScopeDescriptor - - # Essential: Get the syntactic scopeDescriptor for the given position in buffer - # coordinates. Useful with {Config::get}. - # - # For example, if called with a position inside the parameter list of an - # anonymous CoffeeScript function, the method returns the following array: - # `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]` - # - # * `bufferPosition` A {Point} or {Array} of [row, column]. - # - # Returns a {ScopeDescriptor}. - scopeDescriptorForBufferPosition: (bufferPosition) -> - @tokenizedBuffer.scopeDescriptorForPosition(bufferPosition) - - # Extended: Get the range in buffer coordinates of all tokens surrounding the - # cursor that match the given scope selector. - # - # For example, if you wanted to find the string surrounding the cursor, you - # could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`. - # - # * `scopeSelector` {String} selector. e.g. `'.source.ruby'` - # - # Returns a {Range}. - bufferRangeForScopeAtCursor: (scopeSelector) -> - @bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition()) - - bufferRangeForScopeAtPosition: (scopeSelector, position) -> - @tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, position) - - # Extended: Determine if the given row is entirely a comment - isBufferRowCommented: (bufferRow) -> - if match = @lineTextForBufferRow(bufferRow).match(/\S/) - @commentScopeSelector ?= new TextMateScopeSelector('comment.*') - @commentScopeSelector.matches(@scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes) - - # Get the scope descriptor at the cursor. - getCursorScope: -> - @getLastCursor().getScopeDescriptor() - - tokenForBufferPosition: (bufferPosition) -> - @tokenizedBuffer.tokenForPosition(bufferPosition) - - ### - Section: Clipboard Operations - ### - - # Essential: For each selection, copy the selected text. - copySelectedText: -> - maintainClipboard = false - for selection in @getSelectionsOrderedByBufferPosition() - if selection.isEmpty() - previousRange = selection.getBufferRange() - selection.selectLine() - selection.copy(maintainClipboard, true) - selection.setBufferRange(previousRange) - else - selection.copy(maintainClipboard, false) - maintainClipboard = true - return - - # Private: For each selection, only copy highlighted text. - copyOnlySelectedText: -> - maintainClipboard = false - for selection in @getSelectionsOrderedByBufferPosition() - if not selection.isEmpty() - selection.copy(maintainClipboard, false) - maintainClipboard = true - return - - # Essential: For each selection, cut the selected text. - cutSelectedText: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - if selection.isEmpty() - selection.selectLine() - selection.cut(maintainClipboard, true) - else - selection.cut(maintainClipboard, false) - maintainClipboard = true - - # Essential: For each selection, replace the selected text with the contents of - # the clipboard. - # - # If the clipboard contains the same number of selections as the current - # editor, each selection will be replaced with the content of the - # corresponding clipboard selection text. - # - # * `options` (optional) See {Selection::insertText}. - pasteText: (options) -> - options = Object.assign({}, options) - {text: clipboardText, metadata} = @constructor.clipboard.readWithMetadata() - return false unless @emitWillInsertTextEvent(clipboardText) - - metadata ?= {} - options.autoIndent ?= @shouldAutoIndentOnPaste() - - @mutateSelectedText (selection, index) => - if metadata.selections?.length is @getSelections().length - {text, indentBasis, fullLine} = metadata.selections[index] - else - {indentBasis, fullLine} = metadata - text = clipboardText - - delete options.indentBasis - {cursor} = selection - if indentBasis? - containsNewlines = text.indexOf('\n') isnt -1 - if containsNewlines or not cursor.hasPrecedingCharactersOnLine() - options.indentBasis ?= indentBasis - - range = null - if fullLine and selection.isEmpty() - oldPosition = selection.getBufferRange().start - selection.setBufferRange([[oldPosition.row, 0], [oldPosition.row, 0]]) - range = selection.insertText(text, options) - newPosition = oldPosition.translate([1, 0]) - selection.setBufferRange([newPosition, newPosition]) - else - range = selection.insertText(text, options) - - didInsertEvent = {text, range} - @emitter.emit 'did-insert-text', didInsertEvent - - # 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: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - selection.cutToEndOfLine(maintainClipboard) - maintainClipboard = true - - # 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: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - selection.cutToEndOfBufferLine(maintainClipboard) - maintainClipboard = true - - ### - Section: Folds - ### - - # Essential: Fold the most recent cursor's row based on its indentation level. - # - # The fold will extend from the nearest preceding line with a lower - # indentation level up to the nearest following row with a lower indentation - # level. - foldCurrentRow: -> - {row} = @getCursorBufferPosition() - if range = @tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity)) - @displayLayer.foldBufferRange(range) - - # Essential: Unfold the most recent cursor's row by one level. - unfoldCurrentRow: -> - {row} = @getCursorBufferPosition() - @displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false) - - # Essential: Fold the given row in buffer coordinates based on its indentation - # level. - # - # If the given row is foldable, the fold will begin there. Otherwise, it will - # begin at the first foldable row preceding the given row. - # - # * `bufferRow` A {Number}. - foldBufferRow: (bufferRow) -> - position = Point(bufferRow, Infinity) - loop - foldableRange = @tokenizedBuffer.getFoldableRangeContainingPoint(position, @getTabLength()) - if foldableRange - existingFolds = @displayLayer.foldsIntersectingBufferRange(Range(foldableRange.start, foldableRange.start)) - if existingFolds.length is 0 - @displayLayer.foldBufferRange(foldableRange) - else - firstExistingFoldRange = @displayLayer.bufferRangeForFold(existingFolds[0]) - if firstExistingFoldRange.start.isLessThan(position) - position = Point(firstExistingFoldRange.start.row, 0) - continue - return - - # Essential: Unfold all folds containing the given row in buffer coordinates. - # - # * `bufferRow` A {Number} - unfoldBufferRow: (bufferRow) -> - position = Point(bufferRow, Infinity) - @displayLayer.destroyFoldsContainingBufferPositions([position]) - - # Extended: For each selection, fold the rows it intersects. - foldSelectedLines: -> - selection.fold() for selection in @getSelections() - return - - # Extended: Fold all foldable lines. - foldAll: -> - @displayLayer.destroyAllFolds() - for range in @tokenizedBuffer.getFoldableRanges(@getTabLength()) - @displayLayer.foldBufferRange(range) - return - - # Extended: Unfold all existing folds. - unfoldAll: -> - result = @displayLayer.destroyAllFolds() - @scrollToCursorPosition() - result - - # Extended: Fold all foldable lines at the given indent level. - # - # * `level` A {Number}. - foldAllAtIndentLevel: (level) -> - @displayLayer.destroyAllFolds() - for range in @tokenizedBuffer.getFoldableRangesAtIndentLevel(level, @getTabLength()) - @displayLayer.foldBufferRange(range) - return - - # Extended: Determine whether the given row in buffer coordinates is foldable. - # - # A *foldable* row is a row that *starts* a row range that can be folded. - # - # * `bufferRow` A {Number} - # - # Returns a {Boolean}. - isFoldableAtBufferRow: (bufferRow) -> - @tokenizedBuffer.isFoldableAtRow(bufferRow) - - # Extended: Determine whether the given row in screen coordinates is foldable. - # - # A *foldable* row is a row that *starts* a row range that can be folded. - # - # * `bufferRow` A {Number} - # - # Returns a {Boolean}. - isFoldableAtScreenRow: (screenRow) -> - @isFoldableAtBufferRow(@bufferRowForScreenRow(screenRow)) - - # Extended: Fold the given buffer row if it isn't currently folded, and unfold - # it otherwise. - toggleFoldAtBufferRow: (bufferRow) -> - if @isFoldedAtBufferRow(bufferRow) - @unfoldBufferRow(bufferRow) - else - @foldBufferRow(bufferRow) - - # Extended: Determine whether the most recently added cursor's row is folded. - # - # Returns a {Boolean}. - isFoldedAtCursorRow: -> - @isFoldedAtBufferRow(@getCursorBufferPosition().row) - - # Extended: Determine whether the given row in buffer coordinates is folded. - # - # * `bufferRow` A {Number} - # - # Returns a {Boolean}. - isFoldedAtBufferRow: (bufferRow) -> - range = Range( - Point(bufferRow, 0), - Point(bufferRow, @buffer.lineLengthForRow(bufferRow)) - ) - @displayLayer.foldsIntersectingBufferRange(range).length > 0 - - # Extended: Determine whether the given row in screen coordinates is folded. - # - # * `screenRow` A {Number} - # - # Returns a {Boolean}. - isFoldedAtScreenRow: (screenRow) -> - @isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow)) - - # Creates a new fold between two row numbers. - # - # startRow - The row {Number} to start folding at - # endRow - The row {Number} to end the fold - # - # Returns the new {Fold}. - foldBufferRowRange: (startRow, endRow) -> - @foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity))) - - foldBufferRange: (range) -> - @displayLayer.foldBufferRange(range) - - # Remove any {Fold}s found that intersect the given buffer range. - destroyFoldsIntersectingBufferRange: (bufferRange) -> - @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) - - # Remove any {Fold}s found that contain the given array of buffer positions. - destroyFoldsContainingBufferPositions: (bufferPositions, excludeEndpoints) -> - @displayLayer.destroyFoldsContainingBufferPositions(bufferPositions, excludeEndpoints) - - ### - Section: Gutters - ### - - # Essential: Add a custom {Gutter}. - # - # * `options` An {Object} with the following fields: - # * `name` (required) A unique {String} to identify this gutter. - # * `priority` (optional) A {Number} that determines stacking order between - # gutters. Lower priority items are forced closer to the edges of the - # window. (default: -100) - # * `visible` (optional) {Boolean} specifying whether the gutter is visible - # initially after being created. (default: true) - # - # Returns the newly-created {Gutter}. - addGutter: (options) -> - @gutterContainer.addGutter(options) - - # Essential: Get this editor's gutters. - # - # Returns an {Array} of {Gutter}s. - getGutters: -> - @gutterContainer.getGutters() - - getLineNumberGutter: -> - @lineNumberGutter - - # Essential: Get the gutter with the given name. - # - # Returns a {Gutter}, or `null` if no gutter exists for the given name. - gutterWithName: (name) -> - @gutterContainer.gutterWithName(name) - - ### - Section: Scrolling the TextEditor - ### - - # Essential: Scroll the editor to reveal the most recently added cursor if it is - # off-screen. - # - # * `options` (optional) {Object} - # * `center` Center the editor around the cursor if possible. (default: true) - scrollToCursorPosition: (options) -> - @getLastCursor().autoscroll(center: options?.center ? true) - - # Essential: Scrolls the editor to the given buffer position. - # - # * `bufferPosition` An object that represents a buffer position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # * `options` (optional) {Object} - # * `center` Center the editor around the position if possible. (default: false) - scrollToBufferPosition: (bufferPosition, options) -> - @scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition), options) - - # Essential: Scrolls the editor to the given screen position. - # - # * `screenPosition` An object that represents a screen position. It can be either - # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} - # * `options` (optional) {Object} - # * `center` Center the editor around the position if possible. (default: false) - scrollToScreenPosition: (screenPosition, options) -> - @scrollToScreenRange(new Range(screenPosition, screenPosition), options) - - scrollToTop: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") - - @getElement().scrollToTop() - - scrollToBottom: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") - - @getElement().scrollToBottom() - - scrollToScreenRange: (screenRange, options = {}) -> - screenRange = @clipScreenRange(screenRange) if options.clip isnt false - scrollEvent = {screenRange, options} - @component?.didRequestAutoscroll(scrollEvent) - @emitter.emit "did-request-autoscroll", scrollEvent - - getHorizontalScrollbarHeight: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.") - - @getElement().getHorizontalScrollbarHeight() - - getVerticalScrollbarWidth: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.") - - @getElement().getVerticalScrollbarWidth() - - pageUp: -> - @moveUp(@getRowsPerPage()) - - pageDown: -> - @moveDown(@getRowsPerPage()) - - selectPageUp: -> - @selectUp(@getRowsPerPage()) - - selectPageDown: -> - @selectDown(@getRowsPerPage()) - - # Returns the number of rows per page - getRowsPerPage: -> - if @component? - clientHeight = @component.getScrollContainerClientHeight() - lineHeight = @component.getLineHeight() - Math.max(1, Math.ceil(clientHeight / lineHeight)) - else - 1 - - Object.defineProperty(@prototype, 'rowsPerPage', { - get: -> @getRowsPerPage() - }) - - ### - Section: Config - ### - - # Experimental: Supply an object that will provide the editor with settings - # for specific syntactic scopes. See the `ScopedSettingsDelegate` in - # `text-editor-registry.js` for an example implementation. - setScopedSettingsDelegate: (@scopedSettingsDelegate) -> - @tokenizedBuffer.scopedSettingsDelegate = this.scopedSettingsDelegate - - # Experimental: Retrieve the {Object} that provides the editor with settings - # for specific syntactic scopes. - getScopedSettingsDelegate: -> @scopedSettingsDelegate - - # Experimental: Is auto-indentation enabled for this editor? - # - # Returns a {Boolean}. - shouldAutoIndent: -> @autoIndent - - # Experimental: Is auto-indentation on paste enabled for this editor? - # - # Returns a {Boolean}. - shouldAutoIndentOnPaste: -> @autoIndentOnPaste - - # Experimental: Does this editor allow scrolling past the last line? - # - # Returns a {Boolean}. - getScrollPastEnd: -> - if @getAutoHeight() - false - else - @scrollPastEnd - - # Experimental: How fast does the editor scroll in response to mouse wheel - # movements? - # - # Returns a positive {Number}. - getScrollSensitivity: -> @scrollSensitivity - - # Experimental: Does this editor show cursors while there is a selection? - # - # Returns a positive {Boolean}. - getShowCursorOnSelection: -> @showCursorOnSelection - - # Experimental: Are line numbers enabled for this editor? - # - # Returns a {Boolean} - doesShowLineNumbers: -> @showLineNumbers - - # Experimental: Get the time interval within which text editing operations - # are grouped together in the editor's undo history. - # - # Returns the time interval {Number} in milliseconds. - getUndoGroupingInterval: -> @undoGroupingInterval - - # Experimental: Get the characters that are *not* considered part of words, - # for the purpose of word-based cursor movements. - # - # Returns a {String} containing the non-word characters. - getNonWordCharacters: (scopes) -> - @scopedSettingsDelegate?.getNonWordCharacters?(scopes) ? @nonWordCharacters - - ### - Section: Event Handlers - ### - - handleGrammarChange: -> - @unfoldAll() - @emitter.emit 'did-change-grammar', @getGrammar() - - ### - Section: TextEditor Rendering - ### - - # Get the Element for the editor. - getElement: -> - if @component? - @component.element - else - TextEditorComponent ?= require('./text-editor-component') - TextEditorElement ?= require('./text-editor-element') - new TextEditorComponent({ - model: this, - updatedSynchronously: TextEditorElement.prototype.updatedSynchronously, - @initialScrollTopRow, @initialScrollLeftColumn - }) - @component.element - - getAllowedLocations: -> - ['center'] - - # Essential: Retrieves the greyed out placeholder of a mini editor. - # - # Returns a {String}. - getPlaceholderText: -> @placeholderText - - # Essential: Set the greyed out placeholder of a mini editor. Placeholder text - # will be displayed when the editor has no content. - # - # * `placeholderText` {String} text that is displayed when the editor has no content. - setPlaceholderText: (placeholderText) -> @update({placeholderText}) - - pixelPositionForBufferPosition: (bufferPosition) -> - Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead") - @getElement().pixelPositionForBufferPosition(bufferPosition) - - pixelPositionForScreenPosition: (screenPosition) -> - Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead") - @getElement().pixelPositionForScreenPosition(screenPosition) - - getVerticalScrollMargin: -> - maxScrollMargin = Math.floor(((@height / @getLineHeightInPixels()) - 1) / 2) - Math.min(@verticalScrollMargin, maxScrollMargin) - - setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin - - getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@width / @getDefaultCharWidth()) - 1) / 2)) - setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin - - getLineHeightInPixels: -> @lineHeightInPixels - setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels - - getKoreanCharWidth: -> @koreanCharWidth - getHalfWidthCharWidth: -> @halfWidthCharWidth - getDoubleWidthCharWidth: -> @doubleWidthCharWidth - getDefaultCharWidth: -> @defaultCharWidth - - ratioForCharacter: (character) -> - if isKoreanCharacter(character) - @getKoreanCharWidth() / @getDefaultCharWidth() - else if isHalfWidthCharacter(character) - @getHalfWidthCharWidth() / @getDefaultCharWidth() - else if isDoubleWidthCharacter(character) - @getDoubleWidthCharWidth() / @getDefaultCharWidth() - else - 1 - - setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) -> - doubleWidthCharWidth ?= defaultCharWidth - halfWidthCharWidth ?= defaultCharWidth - koreanCharWidth ?= defaultCharWidth - if defaultCharWidth isnt @defaultCharWidth or doubleWidthCharWidth isnt @doubleWidthCharWidth and halfWidthCharWidth isnt @halfWidthCharWidth and koreanCharWidth isnt @koreanCharWidth - @defaultCharWidth = defaultCharWidth - @doubleWidthCharWidth = doubleWidthCharWidth - @halfWidthCharWidth = halfWidthCharWidth - @koreanCharWidth = koreanCharWidth - if @isSoftWrapped() - @displayLayer.reset({ - softWrapColumn: @getSoftWrapColumn() - }) - defaultCharWidth - - setHeight: (height) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.") - @getElement().setHeight(height) - - getHeight: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.") - @getElement().getHeight() - - getAutoHeight: -> @autoHeight ? true - - getAutoWidth: -> @autoWidth ? false - - setWidth: (width) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.") - @getElement().setWidth(width) - - getWidth: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.") - @getElement().getWidth() - - # Use setScrollTopRow instead of this method - setFirstVisibleScreenRow: (screenRow) -> - @setScrollTopRow(screenRow) - - getFirstVisibleScreenRow: -> - @getElement().component.getFirstVisibleRow() - - getLastVisibleScreenRow: -> - @getElement().component.getLastVisibleRow() - - getVisibleRowRange: -> - [@getFirstVisibleScreenRow(), @getLastVisibleScreenRow()] - - # Use setScrollLeftColumn instead of this method - setFirstVisibleScreenColumn: (column) -> - @setScrollLeftColumn(column) - - getFirstVisibleScreenColumn: -> - @getElement().component.getFirstVisibleColumn() - - getScrollTop: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.") - - @getElement().getScrollTop() - - setScrollTop: (scrollTop) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollTop instead.") - - @getElement().setScrollTop(scrollTop) - - getScrollBottom: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollBottom instead.") - - @getElement().getScrollBottom() - - setScrollBottom: (scrollBottom) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollBottom instead.") - - @getElement().setScrollBottom(scrollBottom) - - getScrollLeft: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollLeft instead.") - - @getElement().getScrollLeft() - - setScrollLeft: (scrollLeft) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollLeft instead.") - - @getElement().setScrollLeft(scrollLeft) - - getScrollRight: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollRight instead.") - - @getElement().getScrollRight() - - setScrollRight: (scrollRight) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollRight instead.") - - @getElement().setScrollRight(scrollRight) - - getScrollHeight: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollHeight instead.") - - @getElement().getScrollHeight() - - getScrollWidth: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollWidth instead.") - - @getElement().getScrollWidth() - - getMaxScrollTop: -> - Grim.deprecate("This is now a view method. Call TextEditorElement::getMaxScrollTop instead.") - - @getElement().getMaxScrollTop() - - getScrollTopRow: -> - @getElement().component.getScrollTopRow() - - setScrollTopRow: (scrollTopRow) -> - @getElement().component.setScrollTopRow(scrollTopRow) - - getScrollLeftColumn: -> - @getElement().component.getScrollLeftColumn() - - setScrollLeftColumn: (scrollLeftColumn) -> - @getElement().component.setScrollLeftColumn(scrollLeftColumn) - - intersectsVisibleRowRange: (startRow, endRow) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.") - - @getElement().intersectsVisibleRowRange(startRow, endRow) - - selectionIntersectsVisibleRowRange: (selection) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.") - - @getElement().selectionIntersectsVisibleRowRange(selection) - - screenPositionForPixelPosition: (pixelPosition) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.") - - @getElement().screenPositionForPixelPosition(pixelPosition) - - pixelRectForScreenRange: (screenRange) -> - Grim.deprecate("This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.") - - @getElement().pixelRectForScreenRange(screenRange) - - ### - Section: Utility - ### - - inspect: -> - "" - - emitWillInsertTextEvent: (text) -> - result = true - cancel = -> result = false - willInsertEvent = {cancel, text} - @emitter.emit 'will-insert-text', willInsertEvent - result - - ### - Section: Language Mode Delegated Methods - ### - - suggestedIndentForBufferRow: (bufferRow, options) -> @tokenizedBuffer.suggestedIndentForBufferRow(bufferRow, options) - - # Given a buffer row, indent it. - # - # * bufferRow - The row {Number}. - # * options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}. - autoIndentBufferRow: (bufferRow, options) -> - indentLevel = @suggestedIndentForBufferRow(bufferRow, options) - @setIndentationForBufferRow(bufferRow, indentLevel, options) - - # Indents all the rows between two buffer row numbers. - # - # * startRow - The row {Number} to start at - # * endRow - The row {Number} to end at - autoIndentBufferRows: (startRow, endRow) -> - row = startRow - while row <= endRow - @autoIndentBufferRow(row) - row++ - return - - autoDecreaseIndentForBufferRow: (bufferRow) -> - indentLevel = @tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow) - @setIndentationForBufferRow(bufferRow, indentLevel) if indentLevel? - - toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) - - rowRangeForParagraphAtBufferRow: (bufferRow) -> - return unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(bufferRow)) - - isCommented = @tokenizedBuffer.isRowCommented(bufferRow) - - startRow = bufferRow - while startRow > 0 - break unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(startRow - 1)) - break if @tokenizedBuffer.isRowCommented(startRow - 1) isnt isCommented - startRow-- - - endRow = bufferRow - rowCount = @getLineCount() - while endRow < rowCount - break unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(endRow + 1)) - break if @tokenizedBuffer.isRowCommented(endRow + 1) isnt isCommented - endRow++ - - new Range(new Point(startRow, 0), new Point(endRow, @buffer.lineLengthForRow(endRow))) - -class ChangeEvent - constructor: ({@oldRange, @newRange}) -> - - Object.defineProperty @prototype, 'start', { - get: -> @oldRange.start - } - - Object.defineProperty @prototype, 'oldExtent', { - get: -> @oldRange.getExtent() - } - - Object.defineProperty @prototype, 'newExtent', { - get: -> @newRange.getExtent() - } diff --git a/src/text-editor.js b/src/text-editor.js new file mode 100644 index 000000000..4d7d94de0 --- /dev/null +++ b/src/text-editor.js @@ -0,0 +1,4587 @@ +const _ = require('underscore-plus') +const path = require('path') +const fs = require('fs-plus') +const Grim = require('grim') +const dedent = require('dedent') +const {CompositeDisposable, Disposable, Emitter} = require('event-kit') +const TextBuffer = require('text-buffer') +const {Point, Range} = TextBuffer +const DecorationManager = require('./decoration-manager') +const TokenizedBuffer = require('./tokenized-buffer') +const Cursor = require('./cursor') +const Selection = require('./selection') + +const TextMateScopeSelector = require('first-mate').ScopeSelector +const GutterContainer = require('./gutter-container') +let TextEditorComponent = null +let TextEditorElement = null +const {isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require('./text-utils') + +const SERIALIZATION_VERSION = 1 +const NON_WHITESPACE_REGEXP = /\S/ +const ZERO_WIDTH_NBSP = '\ufeff' +let nextId = 0 + +// Essential: This class represents all essential editing state for a single +// {TextBuffer}, including cursor and selection positions, folds, and soft wraps. +// If you're manipulating the state of an editor, use this class. +// +// A single {TextBuffer} can belong to multiple editors. For example, if the +// same file is open in two different panes, Atom creates a separate editor for +// each pane. If the buffer is manipulated the changes are reflected in both +// editors, but each maintains its own cursor position, folded lines, etc. +// +// ## Accessing TextEditor Instances +// +// The easiest way to get hold of `TextEditor` objects is by registering a callback +// with `::observeTextEditors` on the `atom.workspace` global. Your callback will +// then be called with all current editor instances and also when any editor is +// created in the future. +// +// ```coffee +// atom.workspace.observeTextEditors (editor) -> +// editor.insertText('Hello World') +// ``` +// +// ## Buffer vs. Screen Coordinates +// +// Because editors support folds and soft-wrapping, the lines on screen don't +// always match the lines in the buffer. For example, a long line that soft wraps +// twice renders as three lines on screen, but only represents one line in the +// buffer. Similarly, if rows 5-10 are folded, then row 6 on screen corresponds +// to row 11 in the buffer. +// +// Your choice of coordinates systems will depend on what you're trying to +// achieve. For example, if you're writing a command that jumps the cursor up or +// down by 10 lines, you'll want to use screen coordinates because the user +// probably wants to skip lines *on screen*. However, if you're writing a package +// that jumps between method definitions, you'll want to work in buffer +// coordinates. +// +// **When in doubt, just default to buffer coordinates**, then experiment with +// soft wraps and folds to ensure your code interacts with them correctly. +module.exports = +class TextEditor { + static setClipboard (clipboard) { + this.clipboard = clipboard + } + + static setScheduler (scheduler) { + if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component') } + return TextEditorComponent.setScheduler(scheduler) + } + + static didUpdateStyles () { + if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component') } + return TextEditorComponent.didUpdateStyles() + } + + static didUpdateScrollbarStyles () { + if (TextEditorComponent == null) { TextEditorComponent = require('./text-editor-component') } + return TextEditorComponent.didUpdateScrollbarStyles() + } + + static viewForItem (item) { return item.element || item } + + static deserialize (state, atomEnvironment) { + if (state.version !== SERIALIZATION_VERSION) return null + + try { + const tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment) + if (!tokenizedBuffer) return null + + state.tokenizedBuffer = tokenizedBuffer + state.tabLength = state.tokenizedBuffer.getTabLength() + } catch (error) { + if (error.syscall === 'read') { + return // Error reading the file, don't deserialize an editor for it + } else { + throw error + } + } + + state.buffer = state.tokenizedBuffer.buffer + state.assert = atomEnvironment.assert.bind(atomEnvironment) + const editor = new TextEditor(state) + if (state.registered) { + const disposable = atomEnvironment.textEditors.add(editor) + editor.onDidDestroy(() => disposable.dispose()) + } + return editor + } + + constructor (params = {}) { + if (this.constructor.clipboard == null) { + throw new Error('Must call TextEditor.setClipboard at least once before creating TextEditor instances') + } + + this.id = params.id != null ? params.id : nextId++ + this.initialScrollTopRow = params.initialScrollTopRow + this.initialScrollLeftColumn = params.initialScrollLeftColumn + this.decorationManager = params.decorationManager + this.selectionsMarkerLayer = params.selectionsMarkerLayer + this.mini = (params.mini != null) ? params.mini : false + this.placeholderText = params.placeholderText + this.showLineNumbers = params.showLineNumbers + this.largeFileMode = params.largeFileMode + this.assert = params.assert || (condition => condition) + this.showInvisibles = (params.showInvisibles != null) ? params.showInvisibles : true + this.autoHeight = params.autoHeight + this.autoWidth = params.autoWidth + this.scrollPastEnd = (params.scrollPastEnd != null) ? params.scrollPastEnd : false + this.scrollSensitivity = (params.scrollSensitivity != null) ? params.scrollSensitivity : 40 + this.editorWidthInChars = params.editorWidthInChars + this.invisibles = params.invisibles + this.showIndentGuide = params.showIndentGuide + this.softWrapped = params.softWrapped + this.softWrapAtPreferredLineLength = params.softWrapAtPreferredLineLength + this.preferredLineLength = params.preferredLineLength + this.showCursorOnSelection = (params.showCursorOnSelection != null) ? params.showCursorOnSelection : true + this.maxScreenLineLength = params.maxScreenLineLength + this.softTabs = (params.softTabs != null) ? params.softTabs : true + this.autoIndent = (params.autoIndent != null) ? params.autoIndent : true + this.autoIndentOnPaste = (params.autoIndentOnPaste != null) ? params.autoIndentOnPaste : true + this.undoGroupingInterval = (params.undoGroupingInterval != null) ? params.undoGroupingInterval : 300 + this.nonWordCharacters = (params.nonWordCharacters != null) ? params.nonWordCharacters : "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…" + this.softWrapped = (params.softWrapped != null) ? params.softWrapped : false + this.softWrapAtPreferredLineLength = (params.softWrapAtPreferredLineLength != null) ? params.softWrapAtPreferredLineLength : false + this.preferredLineLength = (params.preferredLineLength != null) ? params.preferredLineLength : 80 + this.maxScreenLineLength = (params.maxScreenLineLength != null) ? params.maxScreenLineLength : 500 + this.showLineNumbers = (params.showLineNumbers != null) ? params.showLineNumbers : true + const {tabLength = 2} = params + + this.alive = true + this.doBackgroundWork = this.doBackgroundWork.bind(this) + this.serializationVersion = 1 + this.suppressSelectionMerging = false + this.selectionFlashDuration = 500 + this.gutterContainer = null + this.verticalScrollMargin = 2 + this.horizontalScrollMargin = 6 + this.lineHeightInPixels = null + this.defaultCharWidth = null + this.height = null + this.width = null + this.registered = false + this.atomicSoftTabs = true + this.emitter = new Emitter() + this.disposables = new CompositeDisposable() + this.cursors = [] + this.cursorsByMarkerId = new Map() + this.selections = [] + this.hasTerminatedPendingState = false + + this.buffer = params.buffer || new TextBuffer({ + shouldDestroyOnFileDelete () { return atom.config.get('core.closeDeletedFileTabs') } + }) + + this.tokenizedBuffer = params.tokenizedBuffer || new TokenizedBuffer({ + grammar: params.grammar, + tabLength, + buffer: this.buffer, + largeFileMode: this.largeFileMode, + assert: this.assert + }) + + if (params.displayLayer) { + this.displayLayer = params.displayLayer + } else { + const displayLayerParams = { + invisibles: this.getInvisibles(), + softWrapColumn: this.getSoftWrapColumn(), + showIndentGuides: this.doesShowIndentGuide(), + atomicSoftTabs: params.atomicSoftTabs != null ? params.atomicSoftTabs : true, + tabLength, + ratioForCharacter: this.ratioForCharacter.bind(this), + isWrapBoundary, + foldCharacter: ZERO_WIDTH_NBSP, + softWrapHangingIndent: params.softWrapHangingIndentLength != null ? params.softWrapHangingIndentLength : 0 + } + + this.displayLayer = this.buffer.getDisplayLayer(params.displayLayerId) + if (this.displayLayer) { + this.displayLayer.reset(displayLayerParams) + this.selectionsMarkerLayer = this.displayLayer.getMarkerLayer(params.selectionsMarkerLayerId) + } else { + this.displayLayer = this.buffer.addDisplayLayer(displayLayerParams) + } + } + + this.backgroundWorkHandle = requestIdleCallback(this.doBackgroundWork) + this.disposables.add(new Disposable(() => { + if (this.backgroundWorkHandle != null) return cancelIdleCallback(this.backgroundWorkHandle) + })) + + this.defaultMarkerLayer = this.displayLayer.addMarkerLayer() + if (!this.selectionsMarkerLayer) { + this.selectionsMarkerLayer = this.addMarkerLayer({maintainHistory: true, persistent: true}) + } + + this.displayLayer.setTextDecorationLayer(this.tokenizedBuffer) + + this.decorationManager = new DecorationManager(this) + this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'cursor'}) + if (!this.isMini()) this.decorateCursorLine() + + this.decorateMarkerLayer(this.displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'}) + + for (let marker of this.selectionsMarkerLayer.getMarkers()) { + this.addSelection(marker) + } + + this.subscribeToBuffer() + this.subscribeToDisplayLayer() + + if (this.cursors.length === 0 && !params.suppressCursorCreation) { + const initialLine = Math.max(parseInt(params.initialLine) || 0, 0) + const initialColumn = Math.max(parseInt(params.initialColumn) || 0, 0) + this.addCursorAtBufferPosition([initialLine, initialColumn]) + } + + this.gutterContainer = new GutterContainer(this) + this.lineNumberGutter = this.gutterContainer.addGutter({ + name: 'line-number', + priority: 0, + visible: params.lineNumberGutterVisible + }) + } + + get element () { + return this.getElement() + } + + get editorElement () { + Grim.deprecate(dedent`\ + \`TextEditor.prototype.editorElement\` has always been private, but now + it is gone. Reading the \`editorElement\` property still returns a + reference to the editor element but this field will be removed in a + later version of Atom, so we recommend using the \`element\` property instead.\ + `) + + return this.getElement() + } + + get displayBuffer () { + Grim.deprecate(dedent`\ + \`TextEditor.prototype.displayBuffer\` has always been private, but now + it is gone. Reading the \`displayBuffer\` property now returns a reference + to the containing \`TextEditor\`, which now provides *some* of the API of + the defunct \`DisplayBuffer\` class.\ + `) + return this + } + + get languageMode () { + return this.tokenizedBuffer + } + + get rowsPerPage () { + return this.getRowsPerPage() + } + + decorateCursorLine () { + this.cursorLineDecorations = [ + this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'line', class: 'cursor-line', onlyEmpty: true}), + this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'line-number', class: 'cursor-line'}), + this.decorateMarkerLayer(this.selectionsMarkerLayer, {type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true}) + ] + } + + doBackgroundWork (deadline) { + const previousLongestRow = this.getApproximateLongestScreenRow() + if (this.displayLayer.doBackgroundWork(deadline)) { + this.backgroundWorkHandle = requestIdleCallback(this.doBackgroundWork) + } else { + this.backgroundWorkHandle = null + } + + if (this.component && this.getApproximateLongestScreenRow() !== previousLongestRow) { + this.component.scheduleUpdate() + } + } + + update (params) { + const displayLayerParams = {} + + for (let param of Object.keys(params)) { + const value = params[param] + + switch (param) { + case 'autoIndent': + this.autoIndent = value + break + + case 'autoIndentOnPaste': + this.autoIndentOnPaste = value + break + + case 'undoGroupingInterval': + this.undoGroupingInterval = value + break + + case 'nonWordCharacters': + this.nonWordCharacters = value + break + + case 'scrollSensitivity': + this.scrollSensitivity = value + break + + case 'encoding': + this.buffer.setEncoding(value) + break + + case 'softTabs': + if (value !== this.softTabs) { + this.softTabs = value + } + break + + case 'atomicSoftTabs': + if (value !== this.displayLayer.atomicSoftTabs) { + displayLayerParams.atomicSoftTabs = value + } + break + + case 'tabLength': + if (value > 0 && value !== this.tokenizedBuffer.getTabLength()) { + this.tokenizedBuffer.setTabLength(value) + displayLayerParams.tabLength = value + } + break + + case 'softWrapped': + if (value !== this.softWrapped) { + this.softWrapped = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + this.emitter.emit('did-change-soft-wrapped', this.isSoftWrapped()) + } + break + + case 'softWrapHangingIndentLength': + if (value !== this.displayLayer.softWrapHangingIndent) { + displayLayerParams.softWrapHangingIndent = value + } + break + + case 'softWrapAtPreferredLineLength': + if (value !== this.softWrapAtPreferredLineLength) { + this.softWrapAtPreferredLineLength = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'preferredLineLength': + if (value !== this.preferredLineLength) { + this.preferredLineLength = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'maxScreenLineLength': + if (value !== this.maxScreenLineLength) { + this.maxScreenLineLength = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'mini': + if (value !== this.mini) { + this.mini = value + this.emitter.emit('did-change-mini', value) + displayLayerParams.invisibles = this.getInvisibles() + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + displayLayerParams.showIndentGuides = this.doesShowIndentGuide() + if (this.mini) { + for (let decoration of this.cursorLineDecorations) { decoration.destroy() } + this.cursorLineDecorations = null + } else { + this.decorateCursorLine() + } + if (this.component != null) { + this.component.scheduleUpdate() + } + } + break + + case 'placeholderText': + if (value !== this.placeholderText) { + this.placeholderText = value + this.emitter.emit('did-change-placeholder-text', value) + } + break + + case 'lineNumberGutterVisible': + if (value !== this.lineNumberGutterVisible) { + if (value) { + this.lineNumberGutter.show() + } else { + this.lineNumberGutter.hide() + } + this.emitter.emit('did-change-line-number-gutter-visible', this.lineNumberGutter.isVisible()) + } + break + + case 'showIndentGuide': + if (value !== this.showIndentGuide) { + this.showIndentGuide = value + displayLayerParams.showIndentGuides = this.doesShowIndentGuide() + } + break + + case 'showLineNumbers': + if (value !== this.showLineNumbers) { + this.showLineNumbers = value + if (this.component != null) { + this.component.scheduleUpdate() + } + } + break + + case 'showInvisibles': + if (value !== this.showInvisibles) { + this.showInvisibles = value + displayLayerParams.invisibles = this.getInvisibles() + } + break + + case 'invisibles': + if (!_.isEqual(value, this.invisibles)) { + this.invisibles = value + displayLayerParams.invisibles = this.getInvisibles() + } + break + + case 'editorWidthInChars': + if (value > 0 && value !== this.editorWidthInChars) { + this.editorWidthInChars = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'width': + if (value !== this.width) { + this.width = value + displayLayerParams.softWrapColumn = this.getSoftWrapColumn() + } + break + + case 'scrollPastEnd': + if (value !== this.scrollPastEnd) { + this.scrollPastEnd = value + if (this.component) this.component.scheduleUpdate() + } + break + + case 'autoHeight': + if (value !== this.autoHeight) { + this.autoHeight = value + } + break + + case 'autoWidth': + if (value !== this.autoWidth) { + this.autoWidth = value + } + break + + case 'showCursorOnSelection': + if (value !== this.showCursorOnSelection) { + this.showCursorOnSelection = value + if (this.component) this.component.scheduleUpdate() + } + break + + default: + if (param !== 'ref' && param !== 'key') { + throw new TypeError(`Invalid TextEditor parameter: '${param}'`) + } + } + } + + this.displayLayer.reset(displayLayerParams) + + if (this.component) { + return this.component.getNextUpdatePromise() + } else { + return Promise.resolve() + } + } + + scheduleComponentUpdate () { + if (this.component) this.component.scheduleUpdate() + } + + serialize () { + const tokenizedBufferState = this.tokenizedBuffer.serialize() + + return { + deserializer: 'TextEditor', + version: SERIALIZATION_VERSION, + + // TODO: Remove this forward-compatible fallback once 1.8 reaches stable. + displayBuffer: {tokenizedBuffer: tokenizedBufferState}, + + tokenizedBuffer: tokenizedBufferState, + displayLayerId: this.displayLayer.id, + selectionsMarkerLayerId: this.selectionsMarkerLayer.id, + + initialScrollTopRow: this.getScrollTopRow(), + initialScrollLeftColumn: this.getScrollLeftColumn(), + + atomicSoftTabs: this.displayLayer.atomicSoftTabs, + softWrapHangingIndentLength: this.displayLayer.softWrapHangingIndent, + + id: this.id, + softTabs: this.softTabs, + softWrapped: this.softWrapped, + softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength, + preferredLineLength: this.preferredLineLength, + mini: this.mini, + editorWidthInChars: this.editorWidthInChars, + width: this.width, + largeFileMode: this.largeFileMode, + maxScreenLineLength: this.maxScreenLineLength, + registered: this.registered, + invisibles: this.invisibles, + showInvisibles: this.showInvisibles, + showIndentGuide: this.showIndentGuide, + autoHeight: this.autoHeight, + autoWidth: this.autoWidth + } + } + + subscribeToBuffer () { + this.buffer.retain() + this.disposables.add(this.buffer.onDidChangePath(() => { + this.emitter.emit('did-change-title', this.getTitle()) + this.emitter.emit('did-change-path', this.getPath()) + })) + this.disposables.add(this.buffer.onDidChangeEncoding(() => { + this.emitter.emit('did-change-encoding', this.getEncoding()) + })) + this.disposables.add(this.buffer.onDidDestroy(() => this.destroy())) + this.disposables.add(this.buffer.onDidChangeModified(() => { + if (!this.hasTerminatedPendingState && this.buffer.isModified()) this.terminatePendingState() + })) + } + + terminatePendingState () { + if (!this.hasTerminatedPendingState) this.emitter.emit('did-terminate-pending-state') + this.hasTerminatedPendingState = true + } + + onDidTerminatePendingState (callback) { + return this.emitter.on('did-terminate-pending-state', callback) + } + + subscribeToDisplayLayer () { + this.disposables.add(this.tokenizedBuffer.onDidChangeGrammar(this.handleGrammarChange.bind(this))) + this.disposables.add(this.displayLayer.onDidChange(changes => { + this.mergeIntersectingSelections() + if (this.component) this.component.didChangeDisplayLayer(changes) + this.emitter.emit('did-change', changes.map(change => new ChangeEvent(change))) + })) + this.disposables.add(this.displayLayer.onDidReset(() => { + this.mergeIntersectingSelections() + if (this.component) this.component.didResetDisplayLayer() + this.emitter.emit('did-change', {}) + })) + this.disposables.add(this.selectionsMarkerLayer.onDidCreateMarker(this.addSelection.bind(this))) + return this.disposables.add(this.selectionsMarkerLayer.onDidUpdate(() => (this.component != null ? this.component.didUpdateSelections() : undefined))) + } + + destroy () { + if (!this.alive) return + this.alive = false + this.disposables.dispose() + this.displayLayer.destroy() + this.tokenizedBuffer.destroy() + for (let selection of this.selections.slice()) { + selection.destroy() + } + this.buffer.release() + this.gutterContainer.destroy() + this.emitter.emit('did-destroy') + this.emitter.clear() + if (this.component) this.component.element.component = null + this.component = null + this.lineNumberGutter.element = null + } + + isAlive () { return this.alive } + + isDestroyed () { return !this.alive } + + /* + Section: Event Subscription + */ + + // Essential: Calls your `callback` when the buffer's title has changed. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeTitle (callback) { + return this.emitter.on('did-change-title', callback) + } + + // Essential: Calls your `callback` when the buffer's path, and therefore title, has changed. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangePath (callback) { + return this.emitter.on('did-change-path', callback) + } + + // Essential: Invoke the given callback synchronously when the content of the + // buffer changes. + // + // Because observers are invoked synchronously, it's important not to perform + // any expensive operations via this method. Consider {::onDidStopChanging} to + // delay expensive operations until after changes stop occurring. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChange (callback) { + return this.emitter.on('did-change', callback) + } + + // Essential: Invoke `callback` when the buffer's contents change. It is + // emit asynchronously 300ms after the last buffer change. This is a good place + // to handle changes to the buffer without compromising typing performance. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidStopChanging (callback) { + return this.getBuffer().onDidStopChanging(callback) + } + + // Essential: Calls your `callback` when a {Cursor} is moved. If there are + // multiple cursors, your callback will be called for each cursor. + // + // * `callback` {Function} + // * `event` {Object} + // * `oldBufferPosition` {Point} + // * `oldScreenPosition` {Point} + // * `newBufferPosition` {Point} + // * `newScreenPosition` {Point} + // * `textChanged` {Boolean} + // * `cursor` {Cursor} that triggered the event + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeCursorPosition (callback) { + return this.emitter.on('did-change-cursor-position', callback) + } + + // Essential: Calls your `callback` when a selection's screen range changes. + // + // * `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. + onDidChangeSelectionRange (callback) { + return this.emitter.on('did-change-selection-range', callback) + } + + // Extended: Calls your `callback` when soft wrap was enabled or disabled. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeSoftWrapped (callback) { + return this.emitter.on('did-change-soft-wrapped', callback) + } + + // Extended: Calls your `callback` when the buffer's encoding has changed. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeEncoding (callback) { + return this.emitter.on('did-change-encoding', callback) + } + + // Extended: Calls your `callback` when the grammar that interprets and + // colorizes the text has been changed. Immediately calls your callback with + // the current grammar. + // + // * `callback` {Function} + // * `grammar` {Grammar} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeGrammar (callback) { + callback(this.getGrammar()) + return this.onDidChangeGrammar(callback) + } + + // Extended: Calls your `callback` when the grammar that interprets and + // colorizes the text has been changed. + // + // * `callback` {Function} + // * `grammar` {Grammar} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeGrammar (callback) { + return this.emitter.on('did-change-grammar', callback) + } + + // Extended: Calls your `callback` when the result of {::isModified} changes. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeModified (callback) { + return this.getBuffer().onDidChangeModified(callback) + } + + // Extended: Calls your `callback` when the buffer's underlying file changes on + // disk at a moment when the result of {::isModified} is true. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidConflict (callback) { + return this.getBuffer().onDidConflict(callback) + } + + // Extended: Calls your `callback` before text has been inserted. + // + // * `callback` {Function} + // * `event` event {Object} + // * `text` {String} text to be inserted + // * `cancel` {Function} Call to prevent the text from being inserted + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillInsertText (callback) { + return this.emitter.on('will-insert-text', callback) + } + + // Extended: Calls your `callback` after text has been inserted. + // + // * `callback` {Function} + // * `event` event {Object} + // * `text` {String} text to be inserted + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidInsertText (callback) { + return this.emitter.on('did-insert-text', callback) + } + + // Essential: Invoke the given callback after the buffer is saved to disk. + // + // * `callback` {Function} to be called after the buffer is saved. + // * `event` {Object} with the following keys: + // * `path` The path to which the buffer was saved. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidSave (callback) { + return this.getBuffer().onDidSave(callback) + } + + // Essential: Invoke the given callback when the editor is destroyed. + // + // * `callback` {Function} to be called when the editor is destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy (callback) { + return this.emitter.once('did-destroy', callback) + } + + // Extended: Calls your `callback` when a {Cursor} is added to the editor. + // Immediately calls your callback for each existing cursor. + // + // * `callback` {Function} + // * `cursor` {Cursor} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeCursors (callback) { + this.getCursors().forEach(callback) + return this.onDidAddCursor(callback) + } + + // Extended: Calls your `callback` when a {Cursor} is added to the editor. + // + // * `callback` {Function} + // * `cursor` {Cursor} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddCursor (callback) { + return this.emitter.on('did-add-cursor', callback) + } + + // Extended: Calls your `callback` when a {Cursor} is removed from the editor. + // + // * `callback` {Function} + // * `cursor` {Cursor} that was removed + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveCursor (callback) { + return this.emitter.on('did-remove-cursor', callback) + } + + // Extended: Calls your `callback` when a {Selection} is added to the editor. + // Immediately calls your callback for each existing selection. + // + // * `callback` {Function} + // * `selection` {Selection} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeSelections (callback) { + this.getSelections().forEach(callback) + return this.onDidAddSelection(callback) + } + + // Extended: Calls your `callback` when a {Selection} is added to the editor. + // + // * `callback` {Function} + // * `selection` {Selection} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddSelection (callback) { + return this.emitter.on('did-add-selection', callback) + } + + // Extended: Calls your `callback` when a {Selection} is removed from the editor. + // + // * `callback` {Function} + // * `selection` {Selection} that was removed + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveSelection (callback) { + return this.emitter.on('did-remove-selection', callback) + } + + // Extended: Calls your `callback` with each {Decoration} added to the editor. + // Calls your `callback` immediately for any existing decorations. + // + // * `callback` {Function} + // * `decoration` {Decoration} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeDecorations (callback) { + return this.decorationManager.observeDecorations(callback) + } + + // Extended: Calls your `callback` when a {Decoration} is added to the editor. + // + // * `callback` {Function} + // * `decoration` {Decoration} that was added + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddDecoration (callback) { + return this.decorationManager.onDidAddDecoration(callback) + } + + // Extended: Calls your `callback` when a {Decoration} is removed from the editor. + // + // * `callback` {Function} + // * `decoration` {Decoration} that was removed + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveDecoration (callback) { + return this.decorationManager.onDidRemoveDecoration(callback) + } + + // Called by DecorationManager when a decoration is added. + didAddDecoration (decoration) { + if (this.component && decoration.isType('block')) { + this.component.addBlockDecoration(decoration) + } + } + + // Extended: Calls your `callback` when the placeholder text is changed. + // + // * `callback` {Function} + // * `placeholderText` {String} new text + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangePlaceholderText (callback) { + return this.emitter.on('did-change-placeholder-text', callback) + } + + onDidChangeScrollTop (callback) { + Grim.deprecate('This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.') + return this.getElement().onDidChangeScrollTop(callback) + } + + onDidChangeScrollLeft (callback) { + Grim.deprecate('This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.') + return this.getElement().onDidChangeScrollLeft(callback) + } + + onDidRequestAutoscroll (callback) { + return this.emitter.on('did-request-autoscroll', callback) + } + + // TODO Remove once the tabs package no longer uses .on subscriptions + onDidChangeIcon (callback) { + return this.emitter.on('did-change-icon', callback) + } + + onDidUpdateDecorations (callback) { + return this.decorationManager.onDidUpdateDecorations(callback) + } + + // Essential: Retrieves the current {TextBuffer}. + getBuffer () { return this.buffer } + + // Retrieves the current buffer's URI. + getURI () { return this.buffer.getUri() } + + // Create an {TextEditor} with its initial state based on this object + copy () { + const displayLayer = this.displayLayer.copy() + const selectionsMarkerLayer = displayLayer.getMarkerLayer(this.buffer.getMarkerLayer(this.selectionsMarkerLayer.id).copy().id) + const softTabs = this.getSoftTabs() + return new TextEditor({ + buffer: this.buffer, + selectionsMarkerLayer, + softTabs, + suppressCursorCreation: true, + tabLength: this.tokenizedBuffer.getTabLength(), + initialScrollTopRow: this.getScrollTopRow(), + initialScrollLeftColumn: this.getScrollLeftColumn(), + assert: this.assert, + displayLayer, + grammar: this.getGrammar(), + autoWidth: this.autoWidth, + autoHeight: this.autoHeight, + showCursorOnSelection: this.showCursorOnSelection + }) + } + + // Controls visibility based on the given {Boolean}. + setVisible (visible) { this.tokenizedBuffer.setVisible(visible) } + + setMini (mini) { + this.update({mini}) + } + + isMini () { return this.mini } + + onDidChangeMini (callback) { + return this.emitter.on('did-change-mini', callback) + } + + setLineNumberGutterVisible (lineNumberGutterVisible) { this.update({lineNumberGutterVisible}) } + + isLineNumberGutterVisible () { return this.lineNumberGutter.isVisible() } + + onDidChangeLineNumberGutterVisible (callback) { + return this.emitter.on('did-change-line-number-gutter-visible', callback) + } + + // Essential: Calls your `callback` when a {Gutter} is added to the editor. + // Immediately calls your callback for each existing gutter. + // + // * `callback` {Function} + // * `gutter` {Gutter} that currently exists/was added. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeGutters (callback) { + return this.gutterContainer.observeGutters(callback) + } + + // Essential: Calls your `callback` when a {Gutter} is added to the editor. + // + // * `callback` {Function} + // * `gutter` {Gutter} that was added. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddGutter (callback) { + return this.gutterContainer.onDidAddGutter(callback) + } + + // Essential: Calls your `callback` when a {Gutter} is removed from the editor. + // + // * `callback` {Function} + // * `name` The name of the {Gutter} that was removed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidRemoveGutter (callback) { + return this.gutterContainer.onDidRemoveGutter(callback) + } + + // Set the number of characters that can be displayed horizontally in the + // editor. + // + // * `editorWidthInChars` A {Number} representing the width of the + // {TextEditorElement} in characters. + setEditorWidthInChars (editorWidthInChars) { this.update({editorWidthInChars}) } + + // Returns the editor width in characters. + getEditorWidthInChars () { + if (this.width != null && this.defaultCharWidth > 0) { + return Math.max(0, Math.floor(this.width / this.defaultCharWidth)) + } else { + return this.editorWidthInChars + } + } + + /* + Section: File Details + */ + + // Essential: Get the editor's title for display in other parts of the + // UI such as the tabs. + // + // If the editor's buffer is saved, its title is the file name. If it is + // unsaved, its title is "untitled". + // + // Returns a {String}. + getTitle () { + return this.getFileName() || 'untitled' + } + + // Essential: Get unique title for display in other parts of the UI, such as + // the window title. + // + // If the editor's buffer is unsaved, its title is "untitled" + // If the editor's buffer is saved, its unique title is formatted as one + // of the following, + // * "" when it is the only editing buffer with this file name. + // * "" when other buffers have this file name. + // + // Returns a {String} + getLongTitle () { + if (this.getPath()) { + const fileName = this.getFileName() + + let myPathSegments + const openEditorPathSegmentsWithSameFilename = [] + for (const textEditor of atom.workspace.getTextEditors()) { + const pathSegments = fs.tildify(textEditor.getDirectoryPath()).split(path.sep) + if (textEditor.getFileName() === fileName) { + openEditorPathSegmentsWithSameFilename.push(pathSegments) + } + if (textEditor === this) myPathSegments = pathSegments + } + + if (openEditorPathSegmentsWithSameFilename.length === 1) return fileName + + let commonPathSegmentCount + for (let i = 0, {length} = myPathSegments; i < length; i++) { + const myPathSegment = myPathSegments[i] + if (openEditorPathSegmentsWithSameFilename.some(segments => (segments.length === i + 1) || (segments[i] !== myPathSegment))) { + commonPathSegmentCount = i + break + } + } + + return `${fileName} \u2014 ${path.join(...myPathSegments.slice(commonPathSegmentCount))}` + } else { + return 'untitled' + } + } + + // Essential: Returns the {String} path of this editor's text buffer. + getPath () { + return this.buffer.getPath() + } + + getFileName () { + const fullPath = this.getPath() + if (fullPath) return path.basename(fullPath) + } + + getDirectoryPath () { + const fullPath = this.getPath() + if (fullPath) return path.dirname(fullPath) + } + + // Extended: Returns the {String} character set encoding of this editor's text + // buffer. + getEncoding () { return this.buffer.getEncoding() } + + // Extended: Set the character set encoding to use in this editor's text + // buffer. + // + // * `encoding` The {String} character set encoding name such as 'utf8' + setEncoding (encoding) { this.buffer.setEncoding(encoding) } + + // Essential: Returns {Boolean} `true` if this editor has been modified. + isModified () { return this.buffer.isModified() } + + // Essential: Returns {Boolean} `true` if this editor has no content. + isEmpty () { return this.buffer.isEmpty() } + + /* + Section: File Operations + */ + + // Essential: Saves the editor's text buffer. + // + // See {TextBuffer::save} for more details. + save () { return this.buffer.save() } + + // Essential: Saves the editor's text buffer as the given path. + // + // See {TextBuffer::saveAs} for more details. + // + // * `filePath` A {String} path. + saveAs (filePath) { return this.buffer.saveAs(filePath) } + + // Determine whether the user should be prompted to save before closing + // this editor. + shouldPromptToSave ({windowCloseRequested, projectHasPaths} = {}) { + if (windowCloseRequested && projectHasPaths && atom.stateStore.isConnected()) { + return this.buffer.isInConflict() + } else { + return this.isModified() && !this.buffer.hasMultipleEditors() + } + } + + // Returns an {Object} to configure dialog shown when this editor is saved + // via {Pane::saveItemAs}. + getSaveDialogOptions () { return {} } + + /* + Section: Reading Text + */ + + // Essential: Returns a {String} representing the entire contents of the editor. + getText () { return this.buffer.getText() } + + // Essential: Get the text in the given {Range} in buffer coordinates. + // + // * `range` A {Range} or range-compatible {Array}. + // + // Returns a {String}. + getTextInBufferRange (range) { + return this.buffer.getTextInRange(range) + } + + // Essential: Returns a {Number} representing the number of lines in the buffer. + getLineCount () { return this.buffer.getLineCount() } + + // Essential: Returns a {Number} representing the number of screen lines in the + // editor. This accounts for folds. + getScreenLineCount () { return this.displayLayer.getScreenLineCount() } + + getApproximateScreenLineCount () { return this.displayLayer.getApproximateScreenLineCount() } + + // Essential: Returns a {Number} representing the last zero-indexed buffer row + // number of the editor. + getLastBufferRow () { return this.buffer.getLastRow() } + + // Essential: Returns a {Number} representing the last zero-indexed screen row + // number of the editor. + getLastScreenRow () { return this.getScreenLineCount() - 1 } + + // Essential: Returns a {String} representing the contents of the line at the + // given buffer row. + // + // * `bufferRow` A {Number} representing a zero-indexed buffer row. + lineTextForBufferRow (bufferRow) { return this.buffer.lineForRow(bufferRow) } + + // Essential: Returns a {String} representing the contents of the line at the + // given screen row. + // + // * `screenRow` A {Number} representing a zero-indexed screen row. + lineTextForScreenRow (screenRow) { + const screenLine = this.screenLineForScreenRow(screenRow) + if (screenLine) return screenLine.lineText + } + + logScreenLines (start = 0, end = this.getLastScreenRow()) { + for (let row = start; row <= end; row++) { + const line = this.lineTextForScreenRow(row) + console.log(row, this.bufferRowForScreenRow(row), line, line.length) + } + } + + tokensForScreenRow (screenRow) { + const tokens = [] + let lineTextIndex = 0 + const currentTokenScopes = [] + const {lineText, tags} = this.screenLineForScreenRow(screenRow) + for (const tag of tags) { + if (this.displayLayer.isOpenTag(tag)) { + currentTokenScopes.push(this.displayLayer.classNameForTag(tag)) + } else if (this.displayLayer.isCloseTag(tag)) { + currentTokenScopes.pop() + } else { + tokens.push({ + text: lineText.substr(lineTextIndex, tag), + scopes: currentTokenScopes.slice() + }) + lineTextIndex += tag + } + } + return tokens + } + + screenLineForScreenRow (screenRow) { + return this.displayLayer.getScreenLine(screenRow) + } + + bufferRowForScreenRow (screenRow) { + return this.displayLayer.translateScreenPosition(Point(screenRow, 0)).row + } + + bufferRowsForScreenRows (startScreenRow, endScreenRow) { + return this.displayLayer.bufferRowsForScreenRows(startScreenRow, endScreenRow + 1) + } + + screenRowForBufferRow (row) { + return this.displayLayer.translateBufferPosition(Point(row, 0)).row + } + + getRightmostScreenPosition () { return this.displayLayer.getRightmostScreenPosition() } + + getApproximateRightmostScreenPosition () { return this.displayLayer.getApproximateRightmostScreenPosition() } + + getMaxScreenLineLength () { return this.getRightmostScreenPosition().column } + + getLongestScreenRow () { return this.getRightmostScreenPosition().row } + + getApproximateLongestScreenRow () { return this.getApproximateRightmostScreenPosition().row } + + lineLengthForScreenRow (screenRow) { return this.displayLayer.lineLengthForScreenRow(screenRow) } + + // Returns the range for the given buffer row. + // + // * `row` A row {Number}. + // * `options` (optional) An options hash with an `includeNewline` key. + // + // Returns a {Range}. + bufferRangeForBufferRow (row, options) { + return this.buffer.rangeForRow(row, options && options.includeNewline) + } + + // Get the text in the given {Range}. + // + // Returns a {String}. + getTextInRange (range) { return this.buffer.getTextInRange(range) } + + // {Delegates to: TextBuffer.isRowBlank} + isBufferRowBlank (bufferRow) { return this.buffer.isRowBlank(bufferRow) } + + // {Delegates to: TextBuffer.nextNonBlankRow} + nextNonBlankBufferRow (bufferRow) { return this.buffer.nextNonBlankRow(bufferRow) } + + // {Delegates to: TextBuffer.getEndPosition} + getEofBufferPosition () { return this.buffer.getEndPosition() } + + // Essential: Get the {Range} of the paragraph surrounding the most recently added + // cursor. + // + // Returns a {Range}. + getCurrentParagraphBufferRange () { + return this.getLastCursor().getCurrentParagraphBufferRange() + } + + /* + Section: Mutating Text + */ + + // 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) } + + // Essential: Set the text in the given {Range} in buffer coordinates. + // + // * `range` A {Range} or range-compatible {Array}. + // * `text` A {String} + // * `options` (optional) {Object} + // * `normalizeLineEndings` (optional) {Boolean} (default: true) + // * `undo` (optional) {String} 'skip' will skip the undo system + // + // Returns the {Range} of the newly-inserted text. + setTextInBufferRange (range, text, options) { + return this.getBuffer().setTextInRange(range, text, options) + } + + // Essential: For each selection, replace the selected text with the given text. + // + // * `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 + insertText (text, options = {}) { + if (!this.emitWillInsertTextEvent(text)) return false + + const groupingInterval = options.groupUndo ? this.undoGroupingInterval : 0 + if (options.autoIndentNewline == null) options.autoIndentNewline = this.shouldAutoIndent() + if (options.autoDecreaseIndent == null) options.autoDecreaseIndent = this.shouldAutoIndent() + return this.mutateSelectedText(selection => { + const range = selection.insertText(text, options) + const didInsertEvent = {text, range} + this.emitter.emit('did-insert-text', didInsertEvent) + return range + }, groupingInterval) + } + + // Essential: For each selection, replace the selected text with a newline. + 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()) + } + + // 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()) + } + + // Extended: Mutate the text of all the selections in a single transaction. + // + // All the changes made inside the given {Function} can be reverted with a + // single call to {::undo}. + // + // * `fn` A {Function} that will be called once for each {Selection}. The first + // argument will be a {Selection} and the second argument will be the + // {Number} index of that selection. + mutateSelectedText (fn, groupingInterval = 0) { + return this.mergeIntersectingSelections(() => { + return this.transact(groupingInterval, () => { + return this.getSelectionsOrderedByBufferPosition().map((selection, index) => fn(selection, index)) + }) + }) + } + + // Move lines intersecting the most recent selection or multiple selections + // up by one row in screen coordinates. + moveLineUp () { + const selections = this.getSelectedBufferRanges().sort((a, b) => a.compare(b)) + + if (selections[0].start.row === 0) return + if (selections[selections.length - 1].start.row === this.getLastBufferRow() && this.buffer.getLastLine() === '') return + + this.transact(() => { + const newSelectionRanges = [] + + while (selections.length > 0) { + // Find selections spanning a contiguous set of lines + const selection = selections.shift() + const selectionsToMove = [selection] + + while (selection.end.row === (selections[0] != null ? selections[0].start.row : undefined)) { + selectionsToMove.push(selections[0]) + selection.end.row = selections[0].end.row + selections.shift() + } + + // Compute the buffer range spanned by all these selections, expanding it + // so that it includes any folded region that intersects them. + let startRow = selection.start.row + let endRow = selection.end.row + if (selection.end.row > selection.start.row && selection.end.column === 0) { + // Don't move the last line of a multi-line selection if the selection ends at column 0 + endRow-- + } + + startRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow) + endRow = this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1) + const linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) + + // If selected line range is preceded by a fold, one line above on screen + // could be multiple lines in the buffer. + const precedingRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow - 1) + const insertDelta = linesRange.start.row - precedingRow + + // Any folds in the text that is moved will need to be re-created. + // It includes the folds that were intersecting with the selection. + const rangesToRefold = this.displayLayer + .destroyFoldsIntersectingBufferRange(linesRange) + .map(range => range.translate([-insertDelta, 0])) + + // Delete lines spanned by selection and insert them on the preceding buffer row + let lines = this.buffer.getTextInRange(linesRange) + if (lines[lines.length - 1] !== '\n') { lines += this.buffer.lineEndingForRow(linesRange.end.row - 2) } + this.buffer.delete(linesRange) + this.buffer.insert([precedingRow, 0], lines) + + // Restore folds that existed before the lines were moved + for (let rangeToRefold of rangesToRefold) { + this.displayLayer.foldBufferRange(rangeToRefold) + } + + for (const selectionToMove of selectionsToMove) { + newSelectionRanges.push(selectionToMove.translate([-insertDelta, 0])) + } + } + + this.setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) + if (this.shouldAutoIndent()) this.autoIndentSelectedRows() + this.scrollToBufferPosition([newSelectionRanges[0].start.row, 0]) + }) + } + + // Move lines intersecting the most recent selection or multiple selections + // down by one row in screen coordinates. + moveLineDown () { + const selections = this.getSelectedBufferRanges() + selections.sort((a, b) => b.compare(a)) + + this.transact(() => { + this.consolidateSelections() + const newSelectionRanges = [] + + while (selections.length > 0) { + // Find selections spanning a contiguous set of lines + const selection = selections.shift() + const selectionsToMove = [selection] + + // if the current selection start row matches the next selections' end row - make them one selection + while (selection.start.row === (selections[0] != null ? selections[0].end.row : undefined)) { + selectionsToMove.push(selections[0]) + selection.start.row = selections[0].start.row + selections.shift() + } + + // Compute the buffer range spanned by all these selections, expanding it + // so that it includes any folded region that intersects them. + let startRow = selection.start.row + let endRow = selection.end.row + if (selection.end.row > selection.start.row && selection.end.column === 0) { + // Don't move the last line of a multi-line selection if the selection ends at column 0 + endRow-- + } + + startRow = this.displayLayer.findBoundaryPrecedingBufferRow(startRow) + endRow = this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1) + const linesRange = new Range(Point(startRow, 0), Point(endRow, 0)) + + // If selected line range is followed by a fold, one line below on screen + // could be multiple lines in the buffer. But at the same time, if the + // next buffer row is wrapped, one line in the buffer can represent many + // screen rows. + const followingRow = Math.min(this.buffer.getLineCount(), this.displayLayer.findBoundaryFollowingBufferRow(endRow + 1)) + const insertDelta = followingRow - linesRange.end.row + + // Any folds in the text that is moved will need to be re-created. + // It includes the folds that were intersecting with the selection. + const rangesToRefold = this.displayLayer + .destroyFoldsIntersectingBufferRange(linesRange) + .map(range => range.translate([insertDelta, 0])) + + // Delete lines spanned by selection and insert them on the following correct buffer row + let lines = this.buffer.getTextInRange(linesRange) + if (followingRow - 1 === this.buffer.getLastRow()) { + lines = `\n${lines}` + } + + this.buffer.insert([followingRow, 0], lines) + this.buffer.delete(linesRange) + + // Restore folds that existed before the lines were moved + for (let rangeToRefold of rangesToRefold) { + this.displayLayer.foldBufferRange(rangeToRefold) + } + + for (const selectionToMove of selectionsToMove) { + newSelectionRanges.push(selectionToMove.translate([insertDelta, 0])) + } + } + + this.setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) + if (this.shouldAutoIndent()) this.autoIndentSelectedRows() + this.scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0]) + }) + } + + // Move any active selections one column to the left. + moveSelectionLeft () { + const selections = this.getSelectedBufferRanges() + const noSelectionAtStartOfLine = selections.every(selection => selection.start.column !== 0) + + const translationDelta = [0, -1] + const translatedRanges = [] + + if (noSelectionAtStartOfLine) { + this.transact(() => { + for (let selection of selections) { + const charToLeftOfSelection = new Range(selection.start.translate(translationDelta), selection.start) + const charTextToLeftOfSelection = this.buffer.getTextInRange(charToLeftOfSelection) + + this.buffer.insert(selection.end, charTextToLeftOfSelection) + this.buffer.delete(charToLeftOfSelection) + translatedRanges.push(selection.translate(translationDelta)) + } + + this.setSelectedBufferRanges(translatedRanges) + }) + } + } + + // Move any active selections one column to the right. + moveSelectionRight () { + const selections = this.getSelectedBufferRanges() + const noSelectionAtEndOfLine = selections.every(selection => { + return selection.end.column !== this.buffer.lineLengthForRow(selection.end.row) + }) + + const translationDelta = [0, 1] + const translatedRanges = [] + + if (noSelectionAtEndOfLine) { + this.transact(() => { + for (let selection of selections) { + const charToRightOfSelection = new Range(selection.end, selection.end.translate(translationDelta)) + const charTextToRightOfSelection = this.buffer.getTextInRange(charToRightOfSelection) + + this.buffer.delete(charToRightOfSelection) + this.buffer.insert(selection.start, charTextToRightOfSelection) + translatedRanges.push(selection.translate(translationDelta)) + } + + this.setSelectedBufferRanges(translatedRanges) + }) + } + } + + duplicateLines () { + this.transact(() => { + const selections = this.getSelectionsOrderedByBufferPosition() + const previousSelectionRanges = [] + + let i = selections.length - 1 + while (i >= 0) { + const j = i + previousSelectionRanges[i] = selections[i].getBufferRange() + if (selections[i].isEmpty()) { + const {start} = selections[i].getScreenRange() + selections[i].setScreenRange([[start.row, 0], [start.row + 1, 0]], {preserveFolds: true}) + } + let [startRow, endRow] = selections[i].getBufferRowRange() + endRow++ + while (i > 0) { + const [previousSelectionStartRow, previousSelectionEndRow] = selections[i - 1].getBufferRowRange() + if (previousSelectionEndRow === startRow) { + startRow = previousSelectionStartRow + previousSelectionRanges[i - 1] = selections[i - 1].getBufferRange() + i-- + } else { + break + } + } + + const intersectingFolds = this.displayLayer.foldsIntersectingBufferRange([[startRow, 0], [endRow, 0]]) + let textToDuplicate = this.getTextInBufferRange([[startRow, 0], [endRow, 0]]) + if (endRow > this.getLastBufferRow()) textToDuplicate = `\n${textToDuplicate}` + this.buffer.insert([endRow, 0], textToDuplicate) + + const insertedRowCount = endRow - startRow + + for (let k = i; k <= j; k++) { + selections[k].setBufferRange(previousSelectionRanges[k].translate([insertedRowCount, 0])) + } + + for (const fold of intersectingFolds) { + const foldRange = this.displayLayer.bufferRangeForFold(fold) + this.displayLayer.foldBufferRange(foldRange.translate([insertedRowCount, 0])) + } + + i-- + } + }) + } + + replaceSelectedText (options, fn) { + this.mutateSelectedText((selection) => { + selection.getBufferRange() + if (options && options.selectWordIfEmpty && selection.isEmpty()) { + selection.selectWord() + } + const text = selection.getText() + selection.deleteSelectedText() + const range = selection.insertText(fn(text)) + selection.setBufferRange(range) + }) + } + + // Split multi-line selections into one selection per line. + // + // Operates on all selections. This method breaks apart all multi-line + // selections to create multiple single-line selections that cumulatively cover + // the same original area. + splitSelectionsIntoLines () { + this.mergeIntersectingSelections(() => { + for (const selection of this.getSelections()) { + const range = selection.getBufferRange() + if (range.isSingleLine()) continue + + const {start, end} = range + this.addSelectionForBufferRange([start, [start.row, Infinity]]) + let {row} = start + while (++row < end.row) { + this.addSelectionForBufferRange([[row, 0], [row, Infinity]]) + } + if (end.column !== 0) this.addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) + selection.destroy() + } + }) + } + + // Extended: For each selection, transpose the selected text. + // + // If the selection is empty, the characters preceding and following the cursor + // are swapped. Otherwise, the selected characters are reversed. + transpose () { + this.mutateSelectedText(selection => { + if (selection.isEmpty()) { + selection.selectRight() + const text = selection.getText() + selection.delete() + selection.cursor.moveLeft() + selection.insertText(text) + } else { + selection.insertText(selection.getText().split('').reverse().join('')) + } + }) + } + + // Extended: Convert the selected text to upper case. + // + // 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()) + } + + // 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()) + } + + // Extended: Toggle line comments for rows intersecting selections. + // + // If the current grammar doesn't support comments, does nothing. + toggleLineCommentsInSelection () { + this.mutateSelectedText(selection => selection.toggleLineComments()) + } + + // Convert multiple lines to a single line. + // + // Operates on all selections. If the selection is empty, joins the current + // line with the next line. Otherwise it joins all lines that intersect the + // selection. + // + // 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 () { + this.mutateSelectedText(selection => selection.joinLines()) + } + + // Extended: For each cursor, insert a newline at beginning the following line. + insertNewlineBelow () { + this.transact(() => { + this.moveToEndOfLine() + this.insertNewline() + }) + } + + // Extended: For each cursor, insert a newline at the end of the preceding line. + insertNewlineAbove () { + this.transact(() => { + const bufferRow = this.getCursorBufferPosition().row + const indentLevel = this.indentationForBufferRow(bufferRow) + const onFirstLine = bufferRow === 0 + + this.moveToBeginningOfLine() + this.moveLeft() + this.insertNewline() + + if (this.shouldAutoIndent() && (this.indentationForBufferRow(bufferRow) < indentLevel)) { + this.setIndentationForBufferRow(bufferRow, indentLevel) + } + + if (onFirstLine) { + this.moveUp() + this.moveToEndOfLine() + } + }) + } + + // 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()) + } + + // Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the + // previous word boundary. + deleteToPreviousWordBoundary () { + this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary()) + } + + // Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the + // next word boundary. + deleteToNextWordBoundary () { + this.mutateSelectedText(selection => selection.deleteToNextWordBoundary()) + } + + // 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()) + } + + // 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()) + } + + // 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()) + } + + // 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()) + } + + // 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()) + } + + // Extended: Delete all lines intersecting selections. + deleteLine () { + this.mergeSelectionsOnSameRows() + this.mutateSelectedText(selection => selection.deleteLine()) + } + + /* + Section: History + */ + + // Essential: Undo the last change. + undo () { + this.avoidMergingSelections(() => this.buffer.undo()) + this.getLastSelection().autoscroll() + } + + // Essential: Redo the last change. + redo () { + this.avoidMergingSelections(() => this.buffer.redo()) + this.getLastSelection().autoscroll() + } + + // Extended: Batch multiple operations as a single undo/redo step. + // + // Any group of operations that are logically grouped from the perspective of + // undoing and redoing should be performed in a transaction. If you want to + // abort the transaction, call {::abortTransaction} to terminate the function's + // execution and revert any changes performed up to the abortion. + // + // * `groupingInterval` (optional) The {Number} of milliseconds for which this + // transaction should be considered 'groupable' after it begins. If a transaction + // with a positive `groupingInterval` is committed while the previous transaction is + // still 'groupable', the two transactions are merged with respect to undo and redo. + // * `fn` A {Function} to call inside the transaction. + transact (groupingInterval, fn) { + return this.buffer.transact(groupingInterval, fn) + } + + // Extended: Abort an open transaction, undoing any operations performed so far + // within the transaction. + abortTransaction () { return this.buffer.abortTransaction() } + + // Extended: Create a pointer to the current state of the buffer for use + // with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}. + // + // Returns a checkpoint value. + createCheckpoint () { return this.buffer.createCheckpoint() } + + // Extended: Revert the buffer to the state it was in when the given + // checkpoint was created. + // + // The redo stack will be empty following this operation, so changes since the + // checkpoint will be lost. If the given checkpoint is no longer present in the + // undo history, no changes will be made to the buffer and this method will + // return `false`. + // + // * `checkpoint` The checkpoint to revert to. + // + // Returns a {Boolean} indicating whether the operation succeeded. + revertToCheckpoint (checkpoint) { return this.buffer.revertToCheckpoint(checkpoint) } + + // Extended: Group all changes since the given checkpoint into a single + // transaction for purposes of undo/redo. + // + // If the given checkpoint is no longer present in the undo history, no + // grouping will be performed and this method will return `false`. + // + // * `checkpoint` The checkpoint from which to group changes. + // + // Returns a {Boolean} indicating whether the operation succeeded. + groupChangesSinceCheckpoint (checkpoint) { return this.buffer.groupChangesSinceCheckpoint(checkpoint) } + + /* + Section: TextEditor Coordinates + */ + + // Essential: Convert a position in buffer-coordinates to screen-coordinates. + // + // The position is clipped via {::clipBufferPosition} prior to the conversion. + // The position is also clipped via {::clipScreenPosition} following the + // conversion, which only makes a difference when `options` are supplied. + // + // * `bufferPosition` A {Point} or {Array} of [row, column]. + // * `options` (optional) An options hash for {::clipScreenPosition}. + // + // Returns a {Point}. + screenPositionForBufferPosition (bufferPosition, options) { + if (options && options.clip) { + Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.') + if (options.clipDirection) options.clipDirection = options.clip + } + if (options && options.wrapAtSoftNewlines != null) { + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward' + } + if (options && options.wrapBeyondNewlines != null) { + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward' + } + + return this.displayLayer.translateBufferPosition(bufferPosition, options) + } + + // Essential: Convert a position in screen-coordinates to buffer-coordinates. + // + // The position is clipped via {::clipScreenPosition} prior to the conversion. + // + // * `bufferPosition` A {Point} or {Array} of [row, column]. + // * `options` (optional) An options hash for {::clipScreenPosition}. + // + // Returns a {Point}. + bufferPositionForScreenPosition (screenPosition, options) { + if (options && options.clip) { + Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.') + if (options.clipDirection) options.clipDirection = options.clip + } + if (options && options.wrapAtSoftNewlines != null) { + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward' + } + if (options && options.wrapBeyondNewlines != null) { + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward' + } + + return this.displayLayer.translateScreenPosition(screenPosition, options) + } + + // Essential: Convert a range in buffer-coordinates to screen-coordinates. + // + // * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates. + // + // Returns a {Range}. + screenRangeForBufferRange (bufferRange, options) { + bufferRange = Range.fromObject(bufferRange) + const start = this.screenPositionForBufferPosition(bufferRange.start, options) + const end = this.screenPositionForBufferPosition(bufferRange.end, options) + return new Range(start, end) + } + + // Essential: Convert a range in screen-coordinates to buffer-coordinates. + // + // * `screenRange` {Range} in screen coordinates to translate into buffer coordinates. + // + // Returns a {Range}. + bufferRangeForScreenRange (screenRange) { + screenRange = Range.fromObject(screenRange) + const start = this.bufferPositionForScreenPosition(screenRange.start) + const end = this.bufferPositionForScreenPosition(screenRange.end) + return new Range(start, end) + } + + // Extended: Clip the given {Point} to a valid position in the buffer. + // + // If the given {Point} describes a position that is actually reachable by the + // cursor based on the current contents of the buffer, it is returned + // unchanged. If the {Point} does not describe a valid position, the closest + // valid position is returned instead. + // + // ## Examples + // + // ```coffee + // editor.clipBufferPosition([-1, -1]) # -> `[0, 0]` + // + // # 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. + // + // Returns a {Point}. + clipBufferPosition (bufferPosition) { return this.buffer.clipPosition(bufferPosition) } + + // Extended: Clip the start and end of the given range to valid positions in the + // buffer. See {::clipBufferPosition} for more information. + // + // * `range` The {Range} to clip. + // + // Returns a {Range}. + clipBufferRange (range) { return this.buffer.clipRange(range) } + + // Extended: Clip the given {Point} to a valid position on screen. + // + // If the given {Point} describes a position that is actually reachable by the + // cursor based on the current contents of the screen, it is returned + // unchanged. If the {Point} does not describe a valid position, the closest + // valid position is returned instead. + // + // ## Examples + // + // ```coffee + // editor.clipScreenPosition([-1, -1]) # -> `[0, 0]` + // + // # 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. + // * `options` (optional) {Object} + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. + // + // Returns a {Point}. + clipScreenPosition (screenPosition, options) { + if (options && options.clip) { + Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.') + if (options.clipDirection) options.clipDirection = options.clip + } + if (options && options.wrapAtSoftNewlines != null) { + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward' + } + if (options && options.wrapBeyondNewlines != null) { + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward' + } + + return this.displayLayer.clipScreenPosition(screenPosition, options) + } + + // Extended: Clip the start and end of the given range to valid positions on screen. + // See {::clipScreenPosition} for more information. + // + // * `range` The {Range} to clip. + // * `options` (optional) See {::clipScreenPosition} `options`. + // + // Returns a {Range}. + clipScreenRange (screenRange, options) { + screenRange = Range.fromObject(screenRange) + const start = this.displayLayer.clipScreenPosition(screenRange.start, options) + const end = this.displayLayer.clipScreenPosition(screenRange.end, options) + return Range(start, end) + } + + /* + Section: Decorations + */ + + // Essential: Add a decoration that tracks a {DisplayMarker}. When the + // marker moves, is invalidated, or is destroyed, the decoration will be + // updated to reflect the marker's state. + // + // The following are the supported decorations types: + // + // * __line__: Adds your CSS `class` to the line nodes within the range + // marked by the marker + // * __line-number__: Adds your CSS `class` to the line number nodes within the + // range marked by the marker + // * __highlight__: Adds a new highlight div to the editor surrounding the + // range marked by the marker. When the user selects text, the selection is + // visualized with a highlight decoration internally. The structure of this + // highlight will be + // ```html + //
+ // + //
+ //
+ // ``` + // * __overlay__: Positions the view associated with the given item at the head + // or tail of the given `DisplayMarker`. + // * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter + // decorations are created by calling {Gutter::decorateMarker} on the + // desired `Gutter` instance. + // * __block__: Positions the view associated with the given item before or + // after the row of the given `TextEditorMarker`. + // + // ## Arguments + // + // * `marker` A {DisplayMarker} you want this decoration to follow. + // * `decorationParams` An {Object} representing the decoration e.g. + // `{type: 'line-number', class: 'linter-error'}` + // * `type` There are several supported decoration types. The behavior of the + // types are as follows: + // * `line` Adds the given `class` to the lines overlapping the rows + // spanned by the `DisplayMarker`. + // * `line-number` Adds the given `class` to the line numbers overlapping + // the rows spanned by the `DisplayMarker`. + // * `text` Injects spans into all text overlapping the marked range, + // then adds the given `class` or `style` properties to these spans. + // Use this to manipulate the foreground color or styling of text in + // a given range. + // * `highlight` Creates an absolutely-positioned `.highlight` div + // containing nested divs to cover the marked region. For example, this + // is used to implement selections. + // * `overlay` Positions the view associated with the given item at the + // head or tail of the given `DisplayMarker`, depending on the `position` + // property. + // * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling + // {Gutter::decorateMarker} on the desired `Gutter` instance. + // * `block` Positions the view associated with the given item before or + // after the row of the given `TextEditorMarker`, depending on the `position` + // property. + // * `cursor` Renders a cursor at the head of the given marker. If multiple + // decorations are created for the same marker, their class strings and + // style objects are combined into a single cursor. You can use this + // decoration type to style existing cursors by passing in their markers + // or render artificial cursors that don't actually exist in the model + // by passing a marker that isn't actually associated with a cursor. + // * `class` This CSS class will be applied to the decorated line number, + // line, text spans, highlight regions, cursors, or overlay. + // * `style` An {Object} containing CSS style properties to apply to the + // relevant DOM node. Currently this only works with a `type` of `cursor` + // or `text`. + // * `item` (optional) An {HTMLElement} or a model {Object} with a + // corresponding view registered. Only applicable to the `gutter`, + // `overlay` and `block` decoration types. + // * `onlyHead` (optional) If `true`, the decoration will only be applied to + // the head of the `DisplayMarker`. Only applicable to the `line` and + // `line-number` decoration types. + // * `onlyEmpty` (optional) If `true`, the decoration will only be applied if + // the associated `DisplayMarker` is empty. Only applicable to the `gutter`, + // `line`, and `line-number` decoration types. + // * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied + // if the associated `DisplayMarker` is non-empty. Only applicable to the + // `gutter`, `line`, and `line-number` decoration types. + // * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied + // to the last row of a non-empty range, even if it ends at column 0. + // Defaults to `true`. Only applicable to the `gutter`, `line`, and + // `line-number` decoration types. + // * `position` (optional) Only applicable to decorations of type `overlay` and `block`. + // Controls where the view is positioned relative to the `TextEditorMarker`. + // Values can be `'head'` (the default) or `'tail'` for overlay decorations, and + // `'before'` (the default) or `'after'` for block decorations. + // * `avoidOverflow` (optional) Only applicable to decorations of type + // `overlay`. Determines whether the decoration adjusts its horizontal or + // vertical position to remain fully visible when it would otherwise + // overflow the editor. Defaults to `true`. + // + // Returns a {Decoration} object + decorateMarker (marker, decorationParams) { + return this.decorationManager.decorateMarker(marker, decorationParams) + } + + // Essential: Add a decoration to every marker in the given marker layer. Can + // be used to decorate a large number of markers without having to create and + // manage many individual decorations. + // + // * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate. + // * `decorationParams` The same parameters that are passed to + // {TextEditor::decorateMarker}, except the `type` cannot be `overlay` or `gutter`. + // + // Returns a {LayerDecoration}. + decorateMarkerLayer (markerLayer, decorationParams) { + return this.decorationManager.decorateMarkerLayer(markerLayer, decorationParams) + } + + // Deprecated: Get all the decorations within a screen row range on the default + // layer. + // + // * `startScreenRow` the {Number} beginning screen row + // * `endScreenRow` the {Number} end screen row (inclusive) + // + // Returns an {Object} of decorations in the form + // `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}` + // where the keys are {DisplayMarker} IDs, and the values are an array of decoration + // params objects attached to the marker. + // Returns an empty object when no decorations are found + decorationsForScreenRowRange (startScreenRow, endScreenRow) { + return this.decorationManager.decorationsForScreenRowRange(startScreenRow, endScreenRow) + } + + decorationsStateForScreenRowRange (startScreenRow, endScreenRow) { + return this.decorationManager.decorationsStateForScreenRowRange(startScreenRow, endScreenRow) + } + + // Extended: Get all decorations. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getDecorations (propertyFilter) { + return this.decorationManager.getDecorations(propertyFilter) + } + + // Extended: Get all decorations of type 'line'. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getLineDecorations (propertyFilter) { + return this.decorationManager.getLineDecorations(propertyFilter) + } + + // Extended: Get all decorations of type 'line-number'. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getLineNumberDecorations (propertyFilter) { + return this.decorationManager.getLineNumberDecorations(propertyFilter) + } + + // Extended: Get all decorations of type 'highlight'. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getHighlightDecorations (propertyFilter) { + return this.decorationManager.getHighlightDecorations(propertyFilter) + } + + // Extended: Get all decorations of type 'overlay'. + // + // * `propertyFilter` (optional) An {Object} containing key value pairs that + // the returned decorations' properties must match. + // + // Returns an {Array} of {Decoration}s. + getOverlayDecorations (propertyFilter) { + return this.decorationManager.getOverlayDecorations(propertyFilter) + } + + /* + Section: Markers + */ + + // Essential: Create a marker on the default marker layer with the given range + // in buffer coordinates. This marker will maintain its logical location as the + // buffer is changed, so if you mark a particular word, the marker will remain + // over that word even if the word's location in the buffer changes. + // + // * `range` A {Range} or range-compatible {Array} + // * `properties` A hash of key-value pairs to associate with the marker. There + // are also reserved property names that have marker-specific meaning. + // * `maintainHistory` (optional) {Boolean} Whether to store this marker's + // range before and after each change in the undo history. This allows the + // marker's position to be restored more accurately for certain undo/redo + // operations, but uses more time and memory. (default: false) + // * `reversed` (optional) {Boolean} Creates the marker in a reversed + // orientation. (default: false) + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // + // Returns a {DisplayMarker}. + markBufferRange (bufferRange, options) { + return this.defaultMarkerLayer.markBufferRange(bufferRange, options) + } + + // Essential: Create a marker on the default marker layer with the given range + // in screen coordinates. This marker will maintain its logical location as the + // buffer is changed, so if you mark a particular word, the marker will remain + // over that word even if the word's location in the buffer changes. + // + // * `range` A {Range} or range-compatible {Array} + // * `properties` A hash of key-value pairs to associate with the marker. There + // are also reserved property names that have marker-specific meaning. + // * `maintainHistory` (optional) {Boolean} Whether to store this marker's + // range before and after each change in the undo history. This allows the + // marker's position to be restored more accurately for certain undo/redo + // operations, but uses more time and memory. (default: false) + // * `reversed` (optional) {Boolean} Creates the marker in a reversed + // orientation. (default: false) + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // + // Returns a {DisplayMarker}. + markScreenRange (screenRange, options) { + return this.defaultMarkerLayer.markScreenRange(screenRange, options) + } + + // Essential: Create a marker on the default marker layer with the given buffer + // position and no tail. To group multiple markers together in their own + // private layer, see {::addMarkerLayer}. + // + // * `bufferPosition` A {Point} or point-compatible {Array} + // * `options` (optional) An {Object} with the following keys: + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // + // Returns a {DisplayMarker}. + markBufferPosition (bufferPosition, options) { + return this.defaultMarkerLayer.markBufferPosition(bufferPosition, options) + } + + // Essential: Create a marker on the default marker layer with the given screen + // position and no tail. To group multiple markers together in their own + // private layer, see {::addMarkerLayer}. + // + // * `screenPosition` A {Point} or point-compatible {Array} + // * `options` (optional) An {Object} with the following keys: + // * `invalidate` (optional) {String} Determines the rules by which changes + // to the buffer *invalidate* the marker. (default: 'overlap') It can be + // any of the following strategies, in order of fragility: + // * __never__: The marker is never marked as invalid. This is a good choice for + // markers representing selections in an editor. + // * __surround__: The marker is invalidated by changes that completely surround it. + // * __overlap__: The marker is invalidated by changes that surround the + // start or end of the marker. This is the default. + // * __inside__: The marker is invalidated by changes that extend into the + // inside of the marker. Changes that end at the marker's start or + // start at the marker's end do not invalidate the marker. + // * __touch__: The marker is invalidated by a change that touches the marked + // region in any way, including changes that end at the marker's + // start or start at the marker's end. This is the most fragile strategy. + // * `clipDirection` {String} If `'backward'`, returns the first valid + // position preceding an invalid position. If `'forward'`, returns the + // first valid position following an invalid position. If `'closest'`, + // returns the first valid position closest to an invalid position. + // Defaults to `'closest'`. + // + // Returns a {DisplayMarker}. + markScreenPosition (screenPosition, options) { + return this.defaultMarkerLayer.markScreenPosition(screenPosition, options) + } + + // Essential: Find all {DisplayMarker}s on the default marker layer that + // match the given properties. + // + // This method finds markers based on the given properties. Markers can be + // associated with custom properties that will be compared with basic equality. + // In addition, there are several special properties that will be compared + // with the range of the markers rather than their properties. + // + // * `properties` An {Object} containing properties that each returned marker + // must satisfy. Markers can be associated with custom properties, which are + // compared with basic equality. In addition, several reserved properties + // can be used to filter markers based on their current range: + // * `startBufferRow` Only include markers starting at this row in buffer + // coordinates. + // * `endBufferRow` Only include markers ending at this row in buffer + // coordinates. + // * `containsBufferRange` Only include markers containing this {Range} or + // in range-compatible {Array} in buffer coordinates. + // * `containsBufferPosition` Only include markers containing this {Point} + // or {Array} of `[row, column]` in buffer coordinates. + // + // Returns an {Array} of {DisplayMarker}s + findMarkers (params) { + return this.defaultMarkerLayer.findMarkers(params) + } + + // Extended: Get the {DisplayMarker} on the default layer for the given + // marker id. + // + // * `id` {Number} id of the marker + getMarker (id) { + return this.defaultMarkerLayer.getMarker(id) + } + + // Extended: Get all {DisplayMarker}s on the default marker layer. Consider + // using {::findMarkers} + getMarkers () { + return this.defaultMarkerLayer.getMarkers() + } + + // Extended: Get the number of markers in the default marker layer. + // + // Returns a {Number}. + getMarkerCount () { + return this.defaultMarkerLayer.getMarkerCount() + } + + destroyMarker (id) { + const marker = this.getMarker(id) + if (marker) marker.destroy() + } + + // Essential: Create a marker layer to group related markers. + // + // * `options` An {Object} containing the following keys: + // * `maintainHistory` A {Boolean} indicating whether marker state should be + // restored on undo/redo. Defaults to `false`. + // * `persistent` A {Boolean} indicating whether or not this marker layer + // should be serialized and deserialized along with the rest of the + // buffer. Defaults to `false`. If `true`, the marker layer's id will be + // maintained across the serialization boundary, allowing you to retrieve + // it via {::getMarkerLayer}. + // + // Returns a {DisplayMarkerLayer}. + addMarkerLayer (options) { + return this.displayLayer.addMarkerLayer(options) + } + + // Essential: Get a {DisplayMarkerLayer} by id. + // + // * `id` The id of the marker layer to retrieve. + // + // Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the + // given id. + getMarkerLayer (id) { + return this.displayLayer.getMarkerLayer(id) + } + + // Essential: Get the default {DisplayMarkerLayer}. + // + // All marker APIs not tied to an explicit layer interact with this default + // layer. + // + // Returns a {DisplayMarkerLayer}. + getDefaultMarkerLayer () { + return this.defaultMarkerLayer + } + + /* + Section: Cursors + */ + + // Essential: Get the position of the most recently added cursor in buffer + // coordinates. + // + // Returns a {Point} + getCursorBufferPosition () { + return this.getLastCursor().getBufferPosition() + } + + // Essential: Get the position of all the cursor positions in buffer coordinates. + // + // Returns {Array} of {Point}s in the order they were added + getCursorBufferPositions () { + return this.getCursors().map((cursor) => cursor.getBufferPosition()) + } + + // Essential: Move the cursor to the given position in buffer coordinates. + // + // If there are multiple cursors, they will be consolidated to a single cursor. + // + // * `position` A {Point} or {Array} of `[row, column]` + // * `options` (optional) An {Object} containing the following keys: + // * `autoscroll` Determines whether the editor scrolls to the new cursor's + // position. Defaults to true. + setCursorBufferPosition (position, options) { + return this.moveCursors(cursor => cursor.setBufferPosition(position, options)) + } + + // Essential: Get a {Cursor} at given screen coordinates {Point} + // + // * `position` A {Point} or {Array} of `[row, column]` + // + // Returns the first matched {Cursor} or undefined + getCursorAtScreenPosition (position) { + const selection = this.getSelectionAtScreenPosition(position) + if (selection && selection.getHeadScreenPosition().isEqual(position)) { + return selection.cursor + } + } + + // Essential: Get the position of the most recently added cursor in screen + // coordinates. + // + // Returns a {Point}. + getCursorScreenPosition () { + return this.getLastCursor().getScreenPosition() + } + + // Essential: Get the position of all the cursor positions in screen coordinates. + // + // Returns {Array} of {Point}s in the order the cursors were added + getCursorScreenPositions () { + return this.getCursors().map((cursor) => cursor.getScreenPosition()) + } + + // Essential: Move the cursor to the given position in screen coordinates. + // + // If there are multiple cursors, they will be consolidated to a single cursor. + // + // * `position` A {Point} or {Array} of `[row, column]` + // * `options` (optional) An {Object} combining options for {::clipScreenPosition} with: + // * `autoscroll` Determines whether the editor scrolls to the new cursor's + // position. Defaults to true. + setCursorScreenPosition (position, options) { + if (options && options.clip) { + Grim.deprecate('The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.') + if (options.clipDirection) options.clipDirection = options.clip + } + if (options && options.wrapAtSoftNewlines != null) { + Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapAtSoftNewlines ? 'forward' : 'backward' + } + if (options && options.wrapBeyondNewlines != null) { + Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.") + if (options.clipDirection) options.clipDirection = options.wrapBeyondNewlines ? 'forward' : 'backward' + } + + return this.moveCursors(cursor => cursor.setScreenPosition(position, options)) + } + + // Essential: Add a cursor at the given position in buffer coordinates. + // + // * `bufferPosition` A {Point} or {Array} of `[row, column]` + // + // Returns a {Cursor}. + addCursorAtBufferPosition (bufferPosition, options) { + this.selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'}) + if (!options || options.autoscroll !== false) this.getLastSelection().cursor.autoscroll() + return this.getLastSelection().cursor + } + + // Essential: Add a cursor at the position in screen coordinates. + // + // * `screenPosition` A {Point} or {Array} of `[row, column]` + // + // Returns a {Cursor}. + addCursorAtScreenPosition (screenPosition, options) { + this.selectionsMarkerLayer.markScreenPosition(screenPosition, {invalidate: 'never'}) + if (!options || options.autoscroll !== false) this.getLastSelection().cursor.autoscroll() + return this.getLastSelection().cursor + } + + // Essential: Returns {Boolean} indicating whether or not there are multiple cursors. + hasMultipleCursors () { + return this.getCursors().length > 1 + } + + // Essential: Move every cursor up one row in screen coordinates. + // + // * `lineCount` (optional) {Number} number of lines to move + moveUp (lineCount) { + return this.moveCursors(cursor => cursor.moveUp(lineCount, {moveToEndOfSelection: true})) + } + + // Essential: Move every cursor down one row in screen coordinates. + // + // * `lineCount` (optional) {Number} number of lines to move + moveDown (lineCount) { + return this.moveCursors(cursor => cursor.moveDown(lineCount, {moveToEndOfSelection: true})) + } + + // Essential: Move every cursor left one column. + // + // * `columnCount` (optional) {Number} number of columns to move (default: 1) + moveLeft (columnCount) { + return this.moveCursors(cursor => cursor.moveLeft(columnCount, {moveToEndOfSelection: true})) + } + + // Essential: Move every cursor right one column. + // + // * `columnCount` (optional) {Number} number of columns to move (default: 1) + moveRight (columnCount) { + return this.moveCursors(cursor => cursor.moveRight(columnCount, {moveToEndOfSelection: true})) + } + + // Essential: Move every cursor to the beginning of its line in buffer coordinates. + moveToBeginningOfLine () { + return this.moveCursors(cursor => cursor.moveToBeginningOfLine()) + } + + // Essential: Move every cursor to the beginning of its line in screen coordinates. + moveToBeginningOfScreenLine () { + return this.moveCursors(cursor => cursor.moveToBeginningOfScreenLine()) + } + + // Essential: Move every cursor to the first non-whitespace character of its line. + moveToFirstCharacterOfLine () { + return this.moveCursors(cursor => cursor.moveToFirstCharacterOfLine()) + } + + // Essential: Move every cursor to the end of its line in buffer coordinates. + moveToEndOfLine () { + return this.moveCursors(cursor => cursor.moveToEndOfLine()) + } + + // Essential: Move every cursor to the end of its line in screen coordinates. + moveToEndOfScreenLine () { + return this.moveCursors(cursor => cursor.moveToEndOfScreenLine()) + } + + // Essential: Move every cursor to the beginning of its surrounding word. + moveToBeginningOfWord () { + return this.moveCursors(cursor => cursor.moveToBeginningOfWord()) + } + + // Essential: Move every cursor to the end of its surrounding word. + moveToEndOfWord () { + return this.moveCursors(cursor => cursor.moveToEndOfWord()) + } + + // Cursor Extended + + // Extended: Move every cursor to the top of the buffer. + // + // If there are multiple cursors, they will be merged into a single cursor. + moveToTop () { + return this.moveCursors(cursor => cursor.moveToTop()) + } + + // Extended: Move every cursor to the bottom of the buffer. + // + // If there are multiple cursors, they will be merged into a single cursor. + moveToBottom () { + return this.moveCursors(cursor => cursor.moveToBottom()) + } + + // Extended: Move every cursor to the beginning of the next word. + moveToBeginningOfNextWord () { + return this.moveCursors(cursor => cursor.moveToBeginningOfNextWord()) + } + + // Extended: Move every cursor to the previous word boundary. + moveToPreviousWordBoundary () { + return this.moveCursors(cursor => cursor.moveToPreviousWordBoundary()) + } + + // Extended: Move every cursor to the next word boundary. + moveToNextWordBoundary () { + return this.moveCursors(cursor => cursor.moveToNextWordBoundary()) + } + + // Extended: Move every cursor to the previous subword boundary. + moveToPreviousSubwordBoundary () { + return this.moveCursors(cursor => cursor.moveToPreviousSubwordBoundary()) + } + + // Extended: Move every cursor to the next subword boundary. + moveToNextSubwordBoundary () { + return this.moveCursors(cursor => cursor.moveToNextSubwordBoundary()) + } + + // Extended: Move every cursor to the beginning of the next paragraph. + moveToBeginningOfNextParagraph () { + return this.moveCursors(cursor => cursor.moveToBeginningOfNextParagraph()) + } + + // Extended: Move every cursor to the beginning of the previous paragraph. + moveToBeginningOfPreviousParagraph () { + return this.moveCursors(cursor => cursor.moveToBeginningOfPreviousParagraph()) + } + + // Extended: Returns the most recently added {Cursor} + getLastCursor () { + this.createLastSelectionIfNeeded() + return _.last(this.cursors) + } + + // Extended: Returns the word surrounding the most recently added cursor. + // + // * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}. + getWordUnderCursor (options) { + return this.getTextInBufferRange(this.getLastCursor().getCurrentWordBufferRange(options)) + } + + // Extended: Get an Array of all {Cursor}s. + getCursors () { + this.createLastSelectionIfNeeded() + return this.cursors.slice() + } + + // Extended: Get all {Cursors}s, ordered by their position in the buffer + // instead of the order in which they were added. + // + // Returns an {Array} of {Selection}s. + getCursorsOrderedByBufferPosition () { + return this.getCursors().sort((a, b) => a.compare(b)) + } + + cursorsForScreenRowRange (startScreenRow, endScreenRow) { + const cursors = [] + for (let marker of this.selectionsMarkerLayer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) { + const cursor = this.cursorsByMarkerId.get(marker.id) + if (cursor) cursors.push(cursor) + } + return cursors + } + + // Add a cursor based on the given {DisplayMarker}. + addCursor (marker) { + const cursor = new Cursor({editor: this, marker, showCursorOnSelection: this.showCursorOnSelection}) + this.cursors.push(cursor) + this.cursorsByMarkerId.set(marker.id, cursor) + return cursor + } + + moveCursors (fn) { + return this.transact(() => { + this.getCursors().forEach(fn) + return this.mergeCursors() + }) + } + + cursorMoved (event) { + return this.emitter.emit('did-change-cursor-position', event) + } + + // Merge cursors that have the same screen position + mergeCursors () { + const positions = {} + for (let cursor of this.getCursors()) { + const position = cursor.getBufferPosition().toString() + if (positions.hasOwnProperty(position)) { + cursor.destroy() + } else { + positions[position] = true + } + } + } + + /* + Section: Selections + */ + + // Essential: Get the selected text of the most recently added selection. + // + // Returns a {String}. + getSelectedText () { + return this.getLastSelection().getText() + } + + // Essential: Get the {Range} of the most recently added selection in buffer + // coordinates. + // + // Returns a {Range}. + getSelectedBufferRange () { + return this.getLastSelection().getBufferRange() + } + + // Essential: Get the {Range}s of all selections in buffer coordinates. + // + // The ranges are sorted by when the selections were added. Most recent at the end. + // + // Returns an {Array} of {Range}s. + getSelectedBufferRanges () { + return this.getSelections().map((selection) => selection.getBufferRange()) + } + + // Essential: Set the selected range in buffer coordinates. If there are multiple + // selections, they are reduced to a single selection with the given range. + // + // * `bufferRange` A {Range} or range-compatible {Array}. + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + // selection is set. + setSelectedBufferRange (bufferRange, options) { + return this.setSelectedBufferRanges([bufferRange], options) + } + + // Essential: Set the selected ranges in buffer coordinates. If there are multiple + // selections, they are replaced by new selections with the given ranges. + // + // * `bufferRanges` An {Array} of {Range}s or range-compatible {Array}s. + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + // selection is set. + setSelectedBufferRanges (bufferRanges, options = {}) { + if (!bufferRanges.length) throw new Error('Passed an empty array to setSelectedBufferRanges') + + const selections = this.getSelections() + for (let selection of selections.slice(bufferRanges.length)) { + selection.destroy() + } + + this.mergeIntersectingSelections(options, () => { + for (let i = 0; i < bufferRanges.length; i++) { + let bufferRange = bufferRanges[i] + bufferRange = Range.fromObject(bufferRange) + if (selections[i]) { + selections[i].setBufferRange(bufferRange, options) + } else { + this.addSelectionForBufferRange(bufferRange, options) + } + } + }) + } + + // Essential: Get the {Range} of the most recently added selection in screen + // coordinates. + // + // Returns a {Range}. + getSelectedScreenRange () { + return this.getLastSelection().getScreenRange() + } + + // Essential: Get the {Range}s of all selections in screen coordinates. + // + // The ranges are sorted by when the selections were added. Most recent at the end. + // + // Returns an {Array} of {Range}s. + getSelectedScreenRanges () { + return this.getSelections().map((selection) => selection.getScreenRange()) + } + + // Essential: Set the selected range in screen coordinates. If there are multiple + // selections, they are reduced to a single selection with the given range. + // + // * `screenRange` A {Range} or range-compatible {Array}. + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + setSelectedScreenRange (screenRange, options) { + return this.setSelectedBufferRange(this.bufferRangeForScreenRange(screenRange, options), options) + } + + // Essential: Set the selected ranges in screen coordinates. If there are multiple + // selections, they are replaced by new selections with the given ranges. + // + // * `screenRanges` An {Array} of {Range}s or range-compatible {Array}s. + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + setSelectedScreenRanges (screenRanges, options = {}) { + if (!screenRanges.length) throw new Error('Passed an empty array to setSelectedScreenRanges') + + const selections = this.getSelections() + for (let selection of selections.slice(screenRanges.length)) { + selection.destroy() + } + + this.mergeIntersectingSelections(options, () => { + for (let i = 0; i < screenRanges.length; i++) { + let screenRange = screenRanges[i] + screenRange = Range.fromObject(screenRange) + if (selections[i]) { + selections[i].setScreenRange(screenRange, options) + } else { + this.addSelectionForScreenRange(screenRange, options) + } + } + }) + } + + // Essential: Add a selection for the given range in buffer coordinates. + // + // * `bufferRange` A {Range} + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + // selection is set. + // + // Returns the added {Selection}. + addSelectionForBufferRange (bufferRange, options = {}) { + bufferRange = Range.fromObject(bufferRange) + if (!options.preserveFolds) { + this.displayLayer.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true) + } + this.selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed != null ? options.reversed : false}) + if (options.autoscroll !== false) this.getLastSelection().autoscroll() + return this.getLastSelection() + } + + // Essential: Add a selection for the given range in screen coordinates. + // + // * `screenRange` A {Range} + // * `options` (optional) An options {Object}: + // * `reversed` A {Boolean} indicating whether to create the selection in a + // reversed orientation. + // * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the + // selection is set. + // Returns the added {Selection}. + addSelectionForScreenRange (screenRange, options = {}) { + return this.addSelectionForBufferRange(this.bufferRangeForScreenRange(screenRange), options) + } + + // Essential: Select from the current cursor position to the given position in + // buffer coordinates. + // + // This method may merge selections that end up intersecting. + // + // * `position` An instance of {Point}, with a given `row` and `column`. + selectToBufferPosition (position) { + const lastSelection = this.getLastSelection() + lastSelection.selectToBufferPosition(position) + return this.mergeIntersectingSelections({reversed: lastSelection.isReversed()}) + } + + // Essential: Select from the current cursor position to the given position in + // screen coordinates. + // + // This method may merge selections that end up intersecting. + // + // * `position` An instance of {Point}, with a given `row` and `column`. + selectToScreenPosition (position, options) { + const lastSelection = this.getLastSelection() + lastSelection.selectToScreenPosition(position, options) + if (!options || !options.suppressSelectionMerge) { + return this.mergeIntersectingSelections({reversed: lastSelection.isReversed()}) + } + } + + // Essential: Move the cursor of each selection one character upward while + // preserving the selection's tail position. + // + // * `rowCount` (optional) {Number} number of rows to select (default: 1) + // + // This method may merge selections that end up intersecting. + selectUp (rowCount) { + return this.expandSelectionsBackward(selection => selection.selectUp(rowCount)) + } + + // Essential: Move the cursor of each selection one character downward while + // preserving the selection's tail position. + // + // * `rowCount` (optional) {Number} number of rows to select (default: 1) + // + // This method may merge selections that end up intersecting. + selectDown (rowCount) { + return this.expandSelectionsForward(selection => selection.selectDown(rowCount)) + } + + // Essential: Move the cursor of each selection one character leftward while + // preserving the selection's tail position. + // + // * `columnCount` (optional) {Number} number of columns to select (default: 1) + // + // This method may merge selections that end up intersecting. + selectLeft (columnCount) { + return this.expandSelectionsBackward(selection => selection.selectLeft(columnCount)) + } + + // Essential: Move the cursor of each selection one character rightward while + // preserving the selection's tail position. + // + // * `columnCount` (optional) {Number} number of columns to select (default: 1) + // + // This method may merge selections that end up intersecting. + selectRight (columnCount) { + return this.expandSelectionsForward(selection => selection.selectRight(columnCount)) + } + + // Essential: Select from the top of the buffer to the end of the last selection + // in the buffer. + // + // This method merges multiple selections into a single selection. + selectToTop () { + return this.expandSelectionsBackward(selection => selection.selectToTop()) + } + + // Essential: Selects from the top of the first selection in the buffer to the end + // of the buffer. + // + // This method merges multiple selections into a single selection. + selectToBottom () { + return this.expandSelectionsForward(selection => selection.selectToBottom()) + } + + // Essential: Select all text in the buffer. + // + // This method merges multiple selections into a single selection. + selectAll () { + return this.expandSelectionsForward(selection => selection.selectAll()) + } + + // Essential: Move the cursor of each selection to the beginning of its line + // while preserving the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToBeginningOfLine () { + return this.expandSelectionsBackward(selection => selection.selectToBeginningOfLine()) + } + + // Essential: Move the cursor of each selection to the first non-whitespace + // character of its line while preserving the selection's tail position. If the + // cursor is already on the first character of the line, move it to the + // beginning of the line. + // + // This method may merge selections that end up intersecting. + selectToFirstCharacterOfLine () { + return this.expandSelectionsBackward(selection => selection.selectToFirstCharacterOfLine()) + } + + // Essential: Move the cursor of each selection to the end of its line while + // preserving the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToEndOfLine () { + return this.expandSelectionsForward(selection => selection.selectToEndOfLine()) + } + + // Essential: Expand selections to the beginning of their containing word. + // + // Operates on all selections. Moves the cursor to the beginning of the + // containing word while preserving the selection's tail position. + selectToBeginningOfWord () { + return this.expandSelectionsBackward(selection => selection.selectToBeginningOfWord()) + } + + // Essential: Expand selections to the end of their containing word. + // + // Operates on all selections. Moves the cursor to the end of the containing + // word while preserving the selection's tail position. + selectToEndOfWord () { + return this.expandSelectionsForward(selection => selection.selectToEndOfWord()) + } + + // Extended: For each selection, move its cursor to the preceding subword + // boundary while maintaining the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToPreviousSubwordBoundary () { + return this.expandSelectionsBackward(selection => selection.selectToPreviousSubwordBoundary()) + } + + // Extended: For each selection, move its cursor to the next subword boundary + // while maintaining the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToNextSubwordBoundary () { + return this.expandSelectionsForward(selection => selection.selectToNextSubwordBoundary()) + } + + // Essential: For each cursor, select the containing line. + // + // This method merges selections on successive lines. + selectLinesContainingCursors () { + return this.expandSelectionsForward(selection => selection.selectLine()) + } + + // Essential: Select the word surrounding each cursor. + selectWordsContainingCursors () { + return this.expandSelectionsForward(selection => selection.selectWord()) + } + + // Selection Extended + + // Extended: For each selection, move its cursor to the preceding word boundary + // while maintaining the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToPreviousWordBoundary () { + return this.expandSelectionsBackward(selection => selection.selectToPreviousWordBoundary()) + } + + // Extended: For each selection, move its cursor to the next word boundary while + // maintaining the selection's tail position. + // + // This method may merge selections that end up intersecting. + selectToNextWordBoundary () { + return this.expandSelectionsForward(selection => selection.selectToNextWordBoundary()) + } + + // Extended: Expand selections to the beginning of the next word. + // + // Operates on all selections. Moves the cursor to the beginning of the next + // word while preserving the selection's tail position. + selectToBeginningOfNextWord () { + return this.expandSelectionsForward(selection => selection.selectToBeginningOfNextWord()) + } + + // Extended: Expand selections to the beginning of the next paragraph. + // + // Operates on all selections. Moves the cursor to the beginning of the next + // paragraph while preserving the selection's tail position. + selectToBeginningOfNextParagraph () { + return this.expandSelectionsForward(selection => selection.selectToBeginningOfNextParagraph()) + } + + // Extended: Expand selections to the beginning of the next paragraph. + // + // Operates on all selections. Moves the cursor to the beginning of the next + // paragraph while preserving the selection's tail position. + selectToBeginningOfPreviousParagraph () { + return this.expandSelectionsBackward(selection => selection.selectToBeginningOfPreviousParagraph()) + } + + // Extended: Select the range of the given marker if it is valid. + // + // * `marker` A {DisplayMarker} + // + // Returns the selected {Range} or `undefined` if the marker is invalid. + selectMarker (marker) { + if (marker.isValid()) { + const range = marker.getBufferRange() + this.setSelectedBufferRange(range) + return range + } + } + + // Extended: Get the most recently added {Selection}. + // + // Returns a {Selection}. + getLastSelection () { + this.createLastSelectionIfNeeded() + return _.last(this.selections) + } + + getSelectionAtScreenPosition (position) { + const markers = this.selectionsMarkerLayer.findMarkers({containsScreenPosition: position}) + if (markers.length > 0) return this.cursorsByMarkerId.get(markers[0].id).selection + } + + // Extended: Get current {Selection}s. + // + // Returns: An {Array} of {Selection}s. + getSelections () { + this.createLastSelectionIfNeeded() + return this.selections.slice() + } + + // Extended: Get all {Selection}s, ordered by their position in the buffer + // instead of the order in which they were added. + // + // Returns an {Array} of {Selection}s. + getSelectionsOrderedByBufferPosition () { + return this.getSelections().sort((a, b) => a.compare(b)) + } + + // Extended: Determine if a given range in buffer coordinates intersects a + // selection. + // + // * `bufferRange` A {Range} or range-compatible {Array}. + // + // Returns a {Boolean}. + selectionIntersectsBufferRange (bufferRange) { + return this.getSelections().some(selection => selection.intersectsBufferRange(bufferRange)) + } + + // Selections Private + + // Add a similarly-shaped selection to the next eligible line below + // each selection. + // + // Operates on all selections. If the selection is empty, adds an empty + // selection to the next following non-empty line as close to the current + // selection's column as possible. If the selection is non-empty, adds a + // selection to the next line that is long enough for a non-empty selection + // starting at the same column as the current selection to be added to it. + addSelectionBelow () { + return this.expandSelectionsForward(selection => selection.addSelectionBelow()) + } + + // Add a similarly-shaped selection to the next eligible line above + // each selection. + // + // Operates on all selections. If the selection is empty, adds an empty + // selection to the next preceding non-empty line as close to the current + // selection's column as possible. If the selection is non-empty, adds a + // selection to the next line that is long enough for a non-empty selection + // starting at the same column as the current selection to be added to it. + addSelectionAbove () { + return this.expandSelectionsBackward(selection => selection.addSelectionAbove()) + } + + // Calls the given function with each selection, then merges selections + expandSelectionsForward (fn) { + this.mergeIntersectingSelections(() => this.getSelections().forEach(fn)) + } + + // Calls the given function with each selection, then merges selections in the + // reversed orientation + expandSelectionsBackward (fn) { + this.mergeIntersectingSelections({reversed: true}, () => this.getSelections().forEach(fn)) + } + + finalizeSelections () { + for (let selection of this.getSelections()) { selection.finalize() } + } + + selectionsForScreenRows (startRow, endRow) { + return this.getSelections().filter(selection => selection.intersectsScreenRowRange(startRow, endRow)) + } + + // Merges intersecting selections. If passed a function, it executes + // the function with merging suppressed, then merges intersecting selections + // afterward. + mergeIntersectingSelections (...args) { + return this.mergeSelections(...args, (previousSelection, currentSelection) => { + const exclusive = !currentSelection.isEmpty() && !previousSelection.isEmpty() + return previousSelection.intersectsWith(currentSelection, exclusive) + }) + } + + mergeSelectionsOnSameRows (...args) { + return this.mergeSelections(...args, (previousSelection, currentSelection) => { + const screenRange = currentSelection.getScreenRange() + return previousSelection.intersectsScreenRowRange(screenRange.start.row, screenRange.end.row) + }) + } + + avoidMergingSelections (...args) { + return this.mergeSelections(...args, () => false) + } + + mergeSelections (...args) { + const mergePredicate = args.pop() + let fn = args.pop() + let options = args.pop() + if (typeof fn !== 'function') { + options = fn + fn = () => {} + } + + if (this.suppressSelectionMerging) return fn() + + this.suppressSelectionMerging = true + const result = fn() + this.suppressSelectionMerging = false + + const selections = this.getSelectionsOrderedByBufferPosition() + let lastSelection = selections.shift() + for (const selection of selections) { + if (mergePredicate(lastSelection, selection)) { + lastSelection.merge(selection, options) + } else { + lastSelection = selection + } + } + + return result + } + + // Add a {Selection} based on the given {DisplayMarker}. + // + // * `marker` The {DisplayMarker} to highlight + // * `options` (optional) An {Object} that pertains to the {Selection} constructor. + // + // Returns the new {Selection}. + addSelection (marker, options = {}) { + const cursor = this.addCursor(marker) + let selection = new Selection(Object.assign({editor: this, marker, cursor}, options)) + this.selections.push(selection) + const selectionBufferRange = selection.getBufferRange() + this.mergeIntersectingSelections({preserveFolds: options.preserveFolds}) + + if (selection.destroyed) { + for (selection of this.getSelections()) { + if (selection.intersectsBufferRange(selectionBufferRange)) return selection + } + } else { + this.emitter.emit('did-add-cursor', cursor) + this.emitter.emit('did-add-selection', selection) + return selection + } + } + + // Remove the given selection. + removeSelection (selection) { + _.remove(this.cursors, selection.cursor) + _.remove(this.selections, selection) + this.cursorsByMarkerId.delete(selection.cursor.marker.id) + this.emitter.emit('did-remove-cursor', selection.cursor) + return this.emitter.emit('did-remove-selection', selection) + } + + // Reduce one or more selections to a single empty selection based on the most + // recently added cursor. + clearSelections (options) { + this.consolidateSelections() + this.getLastSelection().clear(options) + } + + // Reduce multiple selections to the least recently added selection. + consolidateSelections () { + const selections = this.getSelections() + if (selections.length > 1) { + for (let selection of selections.slice(1, (selections.length))) { selection.destroy() } + selections[0].autoscroll({center: true}) + return true + } else { + return false + } + } + + // Called by the selection + selectionRangeChanged (event) { + if (this.component) this.component.didChangeSelectionRange() + this.emitter.emit('did-change-selection-range', event) + } + + createLastSelectionIfNeeded () { + if (this.selections.length === 0) { + this.addSelectionForBufferRange([[0, 0], [0, 0]], {autoscroll: false, preserveFolds: true}) + } + } + + /* + Section: Searching and Replacing + */ + + // Essential: Scan regular expression matches in the entire buffer, calling the + // given iterator function on each match. + // + // `::scan` functions as the replace method as well via the `replace` + // + // If you're programmatically modifying the results, you may want to try + // {::backwardsScanInBufferRange} to avoid tripping over your own changes. + // + // * `regex` A {RegExp} to search for. + // * `options` (optional) {Object} + // * `leadingContextLineCount` {Number} default `0`; The number of lines + // before the matched line to include in the results object. + // * `trailingContextLineCount` {Number} default `0`; The number of lines + // after the matched line to include in the results object. + // * `iterator` A {Function} that's called on each match + // * `object` {Object} + // * `match` The current regular expression match. + // * `matchText` A {String} with the text of the match. + // * `range` The {Range} of the match. + // * `stop` Call this {Function} to terminate the scan. + // * `replace` Call this {Function} with a {String} to replace the match. + scan (regex, options = {}, iterator) { + if (_.isFunction(options)) { + iterator = options + options = {} + } + + return this.buffer.scan(regex, options, iterator) + } + + // Essential: Scan regular expression matches in a given range, calling the given + // iterator function on each match. + // + // * `regex` A {RegExp} to search for. + // * `range` A {Range} in which to search. + // * `iterator` A {Function} that's called on each match with an {Object} + // containing the following keys: + // * `match` The current regular expression match. + // * `matchText` A {String} with the text of the match. + // * `range` The {Range} of the match. + // * `stop` Call this {Function} to terminate the scan. + // * `replace` Call this {Function} with a {String} to replace the match. + scanInBufferRange (regex, range, iterator) { return this.buffer.scanInRange(regex, range, iterator) } + + // Essential: Scan regular expression matches in a given range in reverse order, + // calling the given iterator function on each match. + // + // * `regex` A {RegExp} to search for. + // * `range` A {Range} in which to search. + // * `iterator` A {Function} that's called on each match with an {Object} + // containing the following keys: + // * `match` The current regular expression match. + // * `matchText` A {String} with the text of the match. + // * `range` The {Range} of the match. + // * `stop` Call this {Function} to terminate the scan. + // * `replace` Call this {Function} with a {String} to replace the match. + backwardsScanInBufferRange (regex, range, iterator) { return this.buffer.backwardsScanInRange(regex, range, iterator) } + + /* + Section: Tab Behavior + */ + + // Essential: Returns a {Boolean} indicating whether softTabs are enabled for this + // editor. + getSoftTabs () { return this.softTabs } + + // Essential: Enable or disable soft tabs for this editor. + // + // * `softTabs` A {Boolean} + setSoftTabs (softTabs) { + this.softTabs = softTabs + this.update({softTabs: this.softTabs}) + } + + // Returns a {Boolean} indicating whether atomic soft tabs are enabled for this editor. + hasAtomicSoftTabs () { return this.displayLayer.atomicSoftTabs } + + // Essential: Toggle soft tabs for this editor + toggleSoftTabs () { this.setSoftTabs(!this.getSoftTabs()) } + + // Essential: Get the on-screen length of tab characters. + // + // Returns a {Number}. + getTabLength () { return this.tokenizedBuffer.getTabLength() } + + // Essential: Set the on-screen length of tab characters. Setting this to a + // {Number} This will override the `editor.tabLength` setting. + // + // * `tabLength` {Number} length of a single tab. Setting to `null` will + // fallback to using the `editor.tabLength` config setting + setTabLength (tabLength) { this.update({tabLength}) } + + // Returns an {Object} representing the current invisible character + // substitutions for this editor. See {::setInvisibles}. + getInvisibles () { + if (!this.mini && this.showInvisibles && (this.invisibles != null)) { + return this.invisibles + } else { + return {} + } + } + + doesShowIndentGuide () { return this.showIndentGuide && !this.mini } + + getSoftWrapHangingIndentLength () { return this.displayLayer.softWrapHangingIndent } + + // Extended: Determine if the buffer uses hard or soft tabs. + // + // Returns `true` if the first non-comment line with leading whitespace starts + // with a space character. Returns `false` if it starts with a hard tab (`\t`). + // + // Returns a {Boolean} or undefined if no non-comment lines had leading + // whitespace. + usesSoftTabs () { + for (let bufferRow = 0, end = Math.min(1000, this.buffer.getLastRow()); bufferRow <= end; bufferRow++) { + const tokenizedLine = this.tokenizedBuffer.tokenizedLines[bufferRow] + if (tokenizedLine && tokenizedLine.isComment()) continue + const line = this.buffer.lineForRow(bufferRow) + if (line[0] === ' ') return true + if (line[0] === '\t') return false + } + } + + // Extended: Get the text representing a single level of indent. + // + // If soft tabs are enabled, the text is composed of N spaces, where N is the + // tab length. Otherwise the text is a tab character (`\t`). + // + // Returns a {String}. + getTabText () { return this.buildIndentString(1) } + + // If soft tabs are enabled, convert all hard tabs to soft tabs in the given + // {Range}. + normalizeTabsInBufferRange (bufferRange) { + if (!this.getSoftTabs()) { return } + return this.scanInBufferRange(/\t/g, bufferRange, ({replace}) => replace(this.getTabText())) + } + + /* + Section: Soft Wrap Behavior + */ + + // Essential: Determine whether lines in this editor are soft-wrapped. + // + // Returns a {Boolean}. + isSoftWrapped () { return this.softWrapped } + + // Essential: Enable or disable soft wrapping for this editor. + // + // * `softWrapped` A {Boolean} + // + // Returns a {Boolean}. + setSoftWrapped (softWrapped) { + this.update({softWrapped}) + return this.isSoftWrapped() + } + + getPreferredLineLength () { return this.preferredLineLength } + + // Essential: Toggle soft wrapping for this editor + // + // Returns a {Boolean}. + toggleSoftWrapped () { return this.setSoftWrapped(!this.isSoftWrapped()) } + + // Essential: Gets the column at which column will soft wrap + getSoftWrapColumn () { + if (this.isSoftWrapped() && !this.mini) { + if (this.softWrapAtPreferredLineLength) { + return Math.min(this.getEditorWidthInChars(), this.preferredLineLength) + } else { + return this.getEditorWidthInChars() + } + } else { + return this.maxScreenLineLength + } + } + + /* + Section: Indentation + */ + + // Essential: Get the indentation level of the given buffer row. + // + // Determines how deeply the given row is indented based on the soft tabs and + // tab length settings of this editor. Note that if soft tabs are enabled and + // the tab length is 2, a row with 4 leading spaces would have an indentation + // level of 2. + // + // * `bufferRow` A {Number} indicating the buffer row. + // + // Returns a {Number}. + indentationForBufferRow (bufferRow) { + return this.indentLevelForLine(this.lineTextForBufferRow(bufferRow)) + } + + // Essential: Set the indentation level for the given buffer row. + // + // Inserts or removes hard tabs or spaces based on the soft tabs and tab length + // settings of this editor in order to bring it to the given indentation level. + // Note that if soft tabs are enabled and the tab length is 2, a row with 4 + // leading spaces would have an indentation level of 2. + // + // * `bufferRow` A {Number} indicating the buffer row. + // * `newLevel` A {Number} indicating the new indentation level. + // * `options` (optional) An {Object} with the following keys: + // * `preserveLeadingWhitespace` `true` to preserve any whitespace already at + // the beginning of the line (default: false). + setIndentationForBufferRow (bufferRow, newLevel, {preserveLeadingWhitespace} = {}) { + let endColumn + if (preserveLeadingWhitespace) { + endColumn = 0 + } else { + endColumn = this.lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length + } + const newIndentString = this.buildIndentString(newLevel) + return this.buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) + } + + // Extended: Indent rows intersecting selections by one level. + indentSelectedRows () { + return this.mutateSelectedText(selection => selection.indentSelectedRows()) + } + + // Extended: Outdent rows intersecting selections by one level. + outdentSelectedRows () { + return this.mutateSelectedText(selection => selection.outdentSelectedRows()) + } + + // Extended: Get the indentation level of the given line of text. + // + // Determines how deeply the given line is indented based on the soft tabs and + // tab length settings of this editor. Note that if soft tabs are enabled and + // the tab length is 2, a row with 4 leading spaces would have an indentation + // level of 2. + // + // * `line` A {String} representing a line of text. + // + // Returns a {Number}. + indentLevelForLine (line) { + return this.tokenizedBuffer.indentLevelForLine(line) + } + + // Extended: Indent rows intersecting selections based on the grammar's suggested + // indent level. + autoIndentSelectedRows () { + return this.mutateSelectedText(selection => selection.autoIndentSelectedRows()) + } + + // Indent all lines intersecting selections. See {Selection::indent} for more + // information. + indent (options = {}) { + if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndent() + this.mutateSelectedText(selection => selection.indent(options)) + } + + // Constructs the string used for indents. + buildIndentString (level, column = 0) { + if (this.getSoftTabs()) { + const tabStopViolation = column % this.getTabLength() + return _.multiplyString(' ', Math.floor(level * this.getTabLength()) - tabStopViolation) + } else { + const excessWhitespace = _.multiplyString(' ', Math.round((level - Math.floor(level)) * this.getTabLength())) + return _.multiplyString('\t', Math.floor(level)) + excessWhitespace + } + } + + /* + Section: Grammars + */ + + // Essential: Get the current {Grammar} of this editor. + getGrammar () { + return this.tokenizedBuffer.grammar + } + + // Essential: Set the current {Grammar} of this editor. + // + // Assigning a grammar will cause the editor to re-tokenize based on the new + // grammar. + // + // * `grammar` {Grammar} + setGrammar (grammar) { + return this.tokenizedBuffer.setGrammar(grammar) + } + + // Reload the grammar based on the file name. + reloadGrammar () { + return this.tokenizedBuffer.reloadGrammar() + } + + // Experimental: Get a notification when async tokenization is completed. + onDidTokenize (callback) { + return this.tokenizedBuffer.onDidTokenize(callback) + } + + /* + Section: Managing Syntax Scopes + */ + + // Essential: Returns a {ScopeDescriptor} that includes this editor's language. + // e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with + // {Config::get} to get language specific config values. + getRootScopeDescriptor () { + return this.tokenizedBuffer.rootScopeDescriptor + } + + // Essential: Get the syntactic scopeDescriptor for the given position in buffer + // coordinates. Useful with {Config::get}. + // + // For example, if called with a position inside the parameter list of an + // anonymous CoffeeScript function, the method returns the following array: + // `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]` + // + // * `bufferPosition` A {Point} or {Array} of [row, column]. + // + // Returns a {ScopeDescriptor}. + scopeDescriptorForBufferPosition (bufferPosition) { + return this.tokenizedBuffer.scopeDescriptorForPosition(bufferPosition) + } + + // Extended: Get the range in buffer coordinates of all tokens surrounding the + // cursor that match the given scope selector. + // + // For example, if you wanted to find the string surrounding the cursor, you + // could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`. + // + // * `scopeSelector` {String} selector. e.g. `'.source.ruby'` + // + // Returns a {Range}. + bufferRangeForScopeAtCursor (scopeSelector) { + return this.bufferRangeForScopeAtPosition(scopeSelector, this.getCursorBufferPosition()) + } + + bufferRangeForScopeAtPosition (scopeSelector, position) { + return this.tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, position) + } + + // Extended: Determine if the given row is entirely a comment + isBufferRowCommented (bufferRow) { + const match = this.lineTextForBufferRow(bufferRow).match(/\S/) + if (match) { + if (!this.commentScopeSelector) this.commentScopeSelector = new TextMateScopeSelector('comment.*') + return this.commentScopeSelector.matches(this.scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes) + } + } + + // Get the scope descriptor at the cursor. + getCursorScope () { + return this.getLastCursor().getScopeDescriptor() + } + + tokenForBufferPosition (bufferPosition) { + return this.tokenizedBuffer.tokenForPosition(bufferPosition) + } + + /* + Section: Clipboard Operations + */ + + // Essential: For each selection, copy the selected text. + copySelectedText () { + let maintainClipboard = false + for (let selection of this.getSelectionsOrderedByBufferPosition()) { + if (selection.isEmpty()) { + const previousRange = selection.getBufferRange() + selection.selectLine() + selection.copy(maintainClipboard, true) + selection.setBufferRange(previousRange) + } else { + selection.copy(maintainClipboard, false) + } + maintainClipboard = true + } + } + + // Private: For each selection, only copy highlighted text. + copyOnlySelectedText () { + let maintainClipboard = false + for (let selection of this.getSelectionsOrderedByBufferPosition()) { + if (!selection.isEmpty()) { + selection.copy(maintainClipboard, false) + maintainClipboard = true + } + } + } + + // Essential: For each selection, cut the selected text. + cutSelectedText () { + let maintainClipboard = false + this.mutateSelectedText(selection => { + if (selection.isEmpty()) { + selection.selectLine() + selection.cut(maintainClipboard, true) + } else { + selection.cut(maintainClipboard, false) + } + maintainClipboard = true + }) + } + + // Essential: For each selection, replace the selected text with the contents of + // the clipboard. + // + // If the clipboard contains the same number of selections as the current + // editor, each selection will be replaced with the content of the + // corresponding clipboard selection text. + // + // * `options` (optional) See {Selection::insertText}. + pasteText (options) { + options = Object.assign({}, options) + let {text: clipboardText, metadata} = this.constructor.clipboard.readWithMetadata() + if (!this.emitWillInsertTextEvent(clipboardText)) return false + + if (!metadata) metadata = {} + if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndentOnPaste() + + this.mutateSelectedText((selection, index) => { + let fullLine, indentBasis, text + if (metadata.selections && metadata.selections.length === this.getSelections().length) { + ({text, indentBasis, fullLine} = metadata.selections[index]) + } else { + ({indentBasis, fullLine} = metadata) + text = clipboardText + } + + if (indentBasis != null && (text.includes('\n') || !selection.cursor.hasPrecedingCharactersOnLine())) { + options.indentBasis = indentBasis + } else { + options.indentBasis = null + } + + let range + if (fullLine && selection.isEmpty()) { + const oldPosition = selection.getBufferRange().start + selection.setBufferRange([[oldPosition.row, 0], [oldPosition.row, 0]]) + range = selection.insertText(text, options) + const newPosition = oldPosition.translate([1, 0]) + selection.setBufferRange([newPosition, newPosition]) + } else { + range = selection.insertText(text, options) + } + + this.emitter.emit('did-insert-text', {text, range}) + }) + } + + // 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 () { + let maintainClipboard = false + this.mutateSelectedText(selection => { + selection.cutToEndOfLine(maintainClipboard) + maintainClipboard = true + }) + } + + // 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 () { + let maintainClipboard = false + this.mutateSelectedText(selection => { + selection.cutToEndOfBufferLine(maintainClipboard) + maintainClipboard = true + }) + } + + /* + Section: Folds + */ + + // Essential: Fold the most recent cursor's row based on its indentation level. + // + // The fold will extend from the nearest preceding line with a lower + // indentation level up to the nearest following row with a lower indentation + // level. + foldCurrentRow () { + const {row} = this.getCursorBufferPosition() + const range = this.tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity)) + if (range) return this.displayLayer.foldBufferRange(range) + } + + // Essential: Unfold the most recent cursor's row by one level. + unfoldCurrentRow () { + const {row} = this.getCursorBufferPosition() + return this.displayLayer.destroyFoldsContainingBufferPositions([Point(row, Infinity)], false) + } + + // Essential: Fold the given row in buffer coordinates based on its indentation + // level. + // + // If the given row is foldable, the fold will begin there. Otherwise, it will + // begin at the first foldable row preceding the given row. + // + // * `bufferRow` A {Number}. + foldBufferRow (bufferRow) { + let position = Point(bufferRow, Infinity) + while (true) { + const foldableRange = this.tokenizedBuffer.getFoldableRangeContainingPoint(position, this.getTabLength()) + if (foldableRange) { + const existingFolds = this.displayLayer.foldsIntersectingBufferRange(Range(foldableRange.start, foldableRange.start)) + if (existingFolds.length === 0) { + this.displayLayer.foldBufferRange(foldableRange) + } else { + const firstExistingFoldRange = this.displayLayer.bufferRangeForFold(existingFolds[0]) + if (firstExistingFoldRange.start.isLessThan(position)) { + position = Point(firstExistingFoldRange.start.row, 0) + continue + } + } + } + break + } + } + + // Essential: Unfold all folds containing the given row in buffer coordinates. + // + // * `bufferRow` A {Number} + unfoldBufferRow (bufferRow) { + const position = Point(bufferRow, Infinity) + return this.displayLayer.destroyFoldsContainingBufferPositions([position]) + } + + // Extended: For each selection, fold the rows it intersects. + foldSelectedLines () { + for (let selection of this.selections) { + selection.fold() + } + } + + // Extended: Fold all foldable lines. + foldAll () { + this.displayLayer.destroyAllFolds() + for (let range of this.tokenizedBuffer.getFoldableRanges(this.getTabLength())) { + this.displayLayer.foldBufferRange(range) + } + } + + // Extended: Unfold all existing folds. + unfoldAll () { + const result = this.displayLayer.destroyAllFolds() + this.scrollToCursorPosition() + return result + } + + // Extended: Fold all foldable lines at the given indent level. + // + // * `level` A {Number}. + foldAllAtIndentLevel (level) { + this.displayLayer.destroyAllFolds() + for (let range of this.tokenizedBuffer.getFoldableRangesAtIndentLevel(level, this.getTabLength())) { + this.displayLayer.foldBufferRange(range) + } + } + + // Extended: Determine whether the given row in buffer coordinates is foldable. + // + // A *foldable* row is a row that *starts* a row range that can be folded. + // + // * `bufferRow` A {Number} + // + // Returns a {Boolean}. + isFoldableAtBufferRow (bufferRow) { + return this.tokenizedBuffer.isFoldableAtRow(bufferRow) + } + + // Extended: Determine whether the given row in screen coordinates is foldable. + // + // A *foldable* row is a row that *starts* a row range that can be folded. + // + // * `bufferRow` A {Number} + // + // Returns a {Boolean}. + isFoldableAtScreenRow (screenRow) { + return this.isFoldableAtBufferRow(this.bufferRowForScreenRow(screenRow)) + } + + // Extended: Fold the given buffer row if it isn't currently folded, and unfold + // it otherwise. + toggleFoldAtBufferRow (bufferRow) { + if (this.isFoldedAtBufferRow(bufferRow)) { + return this.unfoldBufferRow(bufferRow) + } else { + return this.foldBufferRow(bufferRow) + } + } + + // Extended: Determine whether the most recently added cursor's row is folded. + // + // Returns a {Boolean}. + isFoldedAtCursorRow () { + return this.isFoldedAtBufferRow(this.getCursorBufferPosition().row) + } + + // Extended: Determine whether the given row in buffer coordinates is folded. + // + // * `bufferRow` A {Number} + // + // Returns a {Boolean}. + isFoldedAtBufferRow (bufferRow) { + const range = Range( + Point(bufferRow, 0), + Point(bufferRow, this.buffer.lineLengthForRow(bufferRow)) + ) + return this.displayLayer.foldsIntersectingBufferRange(range).length > 0 + } + + // Extended: Determine whether the given row in screen coordinates is folded. + // + // * `screenRow` A {Number} + // + // Returns a {Boolean}. + isFoldedAtScreenRow (screenRow) { + return this.isFoldedAtBufferRow(this.bufferRowForScreenRow(screenRow)) + } + + // Creates a new fold between two row numbers. + // + // startRow - The row {Number} to start folding at + // endRow - The row {Number} to end the fold + // + // Returns the new {Fold}. + foldBufferRowRange (startRow, endRow) { + return this.foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity))) + } + + foldBufferRange (range) { + return this.displayLayer.foldBufferRange(range) + } + + // Remove any {Fold}s found that intersect the given buffer range. + destroyFoldsIntersectingBufferRange (bufferRange) { + return this.displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) + } + + // Remove any {Fold}s found that contain the given array of buffer positions. + destroyFoldsContainingBufferPositions (bufferPositions, excludeEndpoints) { + return this.displayLayer.destroyFoldsContainingBufferPositions(bufferPositions, excludeEndpoints) + } + + /* + Section: Gutters + */ + + // Essential: Add a custom {Gutter}. + // + // * `options` An {Object} with the following fields: + // * `name` (required) A unique {String} to identify this gutter. + // * `priority` (optional) A {Number} that determines stacking order between + // gutters. Lower priority items are forced closer to the edges of the + // window. (default: -100) + // * `visible` (optional) {Boolean} specifying whether the gutter is visible + // initially after being created. (default: true) + // + // Returns the newly-created {Gutter}. + addGutter (options) { + return this.gutterContainer.addGutter(options) + } + + // Essential: Get this editor's gutters. + // + // Returns an {Array} of {Gutter}s. + getGutters () { + return this.gutterContainer.getGutters() + } + + getLineNumberGutter () { + return this.lineNumberGutter + } + + // Essential: Get the gutter with the given name. + // + // Returns a {Gutter}, or `null` if no gutter exists for the given name. + gutterWithName (name) { + return this.gutterContainer.gutterWithName(name) + } + + /* + Section: Scrolling the TextEditor + */ + + // Essential: Scroll the editor to reveal the most recently added cursor if it is + // off-screen. + // + // * `options` (optional) {Object} + // * `center` Center the editor around the cursor if possible. (default: true) + scrollToCursorPosition (options) { + this.getLastCursor().autoscroll({center: options && options.center !== false}) + } + + // Essential: Scrolls the editor to the given buffer position. + // + // * `bufferPosition` An object that represents a buffer position. It can be either + // an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + // * `options` (optional) {Object} + // * `center` Center the editor around the position if possible. (default: false) + scrollToBufferPosition (bufferPosition, options) { + return this.scrollToScreenPosition(this.screenPositionForBufferPosition(bufferPosition), options) + } + + // Essential: Scrolls the editor to the given screen position. + // + // * `screenPosition` An object that represents a screen position. It can be either + // an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + // * `options` (optional) {Object} + // * `center` Center the editor around the position if possible. (default: false) + scrollToScreenPosition (screenPosition, options) { + this.scrollToScreenRange(new Range(screenPosition, screenPosition), options) + } + + scrollToTop () { + Grim.deprecate('This is now a view method. Call TextEditorElement::scrollToTop instead.') + this.getElement().scrollToTop() + } + + scrollToBottom () { + Grim.deprecate('This is now a view method. Call TextEditorElement::scrollToTop instead.') + this.getElement().scrollToBottom() + } + + scrollToScreenRange (screenRange, options = {}) { + if (options.clip !== false) screenRange = this.clipScreenRange(screenRange) + const scrollEvent = {screenRange, options} + if (this.component) this.component.didRequestAutoscroll(scrollEvent) + this.emitter.emit('did-request-autoscroll', scrollEvent) + } + + getHorizontalScrollbarHeight () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.') + return this.getElement().getHorizontalScrollbarHeight() + } + + getVerticalScrollbarWidth () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.') + return this.getElement().getVerticalScrollbarWidth() + } + + pageUp () { + this.moveUp(this.getRowsPerPage()) + } + + pageDown () { + this.moveDown(this.getRowsPerPage()) + } + + selectPageUp () { + this.selectUp(this.getRowsPerPage()) + } + + selectPageDown () { + this.selectDown(this.getRowsPerPage()) + } + + // Returns the number of rows per page + getRowsPerPage () { + if (this.component) { + const clientHeight = this.component.getScrollContainerClientHeight() + const lineHeight = this.component.getLineHeight() + return Math.max(1, Math.ceil(clientHeight / lineHeight)) + } else { + return 1 + } + } + + /* + Section: Config + */ + + // Experimental: Supply an object that will provide the editor with settings + // for specific syntactic scopes. See the `ScopedSettingsDelegate` in + // `text-editor-registry.js` for an example implementation. + setScopedSettingsDelegate (scopedSettingsDelegate) { + this.scopedSettingsDelegate = scopedSettingsDelegate + this.tokenizedBuffer.scopedSettingsDelegate = this.scopedSettingsDelegate + } + + // Experimental: Retrieve the {Object} that provides the editor with settings + // for specific syntactic scopes. + getScopedSettingsDelegate () { return this.scopedSettingsDelegate } + + // Experimental: Is auto-indentation enabled for this editor? + // + // Returns a {Boolean}. + shouldAutoIndent () { return this.autoIndent } + + // Experimental: Is auto-indentation on paste enabled for this editor? + // + // Returns a {Boolean}. + shouldAutoIndentOnPaste () { return this.autoIndentOnPaste } + + // Experimental: Does this editor allow scrolling past the last line? + // + // Returns a {Boolean}. + getScrollPastEnd () { + if (this.getAutoHeight()) { + return false + } else { + return this.scrollPastEnd + } + } + + // Experimental: How fast does the editor scroll in response to mouse wheel + // movements? + // + // Returns a positive {Number}. + getScrollSensitivity () { return this.scrollSensitivity } + + // Experimental: Does this editor show cursors while there is a selection? + // + // Returns a positive {Boolean}. + getShowCursorOnSelection () { return this.showCursorOnSelection } + + // Experimental: Are line numbers enabled for this editor? + // + // Returns a {Boolean} + doesShowLineNumbers () { return this.showLineNumbers } + + // Experimental: Get the time interval within which text editing operations + // are grouped together in the editor's undo history. + // + // Returns the time interval {Number} in milliseconds. + getUndoGroupingInterval () { return this.undoGroupingInterval } + + // Experimental: Get the characters that are *not* considered part of words, + // for the purpose of word-based cursor movements. + // + // Returns a {String} containing the non-word characters. + getNonWordCharacters (scopes) { + if (this.scopedSettingsDelegate && this.scopedSettingsDelegate.getNonWordCharacters) { + return this.scopedSettingsDelegate.getNonWordCharacters(scopes) || this.nonWordCharacters + } else { + return this.nonWordCharacters + } + } + + /* + Section: Event Handlers + */ + + handleGrammarChange () { + this.unfoldAll() + return this.emitter.emit('did-change-grammar', this.getGrammar()) + } + + /* + Section: TextEditor Rendering + */ + + // Get the Element for the editor. + getElement () { + if (!this.component) { + if (!TextEditorComponent) TextEditorComponent = require('./text-editor-component') + if (!TextEditorElement) TextEditorElement = require('./text-editor-element') + this.component = new TextEditorComponent({ + model: this, + updatedSynchronously: TextEditorElement.prototype.updatedSynchronously, + initialScrollTopRow: this.initialScrollTopRow, + initialScrollLeftColumn: this.initialScrollLeftColumn + }) + } + return this.component.element + } + + getAllowedLocations () { + return ['center'] + } + + // Essential: Retrieves the greyed out placeholder of a mini editor. + // + // Returns a {String}. + getPlaceholderText () { return this.placeholderText } + + // Essential: Set the greyed out placeholder of a mini editor. Placeholder text + // will be displayed when the editor has no content. + // + // * `placeholderText` {String} text that is displayed when the editor has no content. + setPlaceholderText (placeholderText) { this.update({placeholderText}) } + + pixelPositionForBufferPosition (bufferPosition) { + Grim.deprecate('This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead') + return this.getElement().pixelPositionForBufferPosition(bufferPosition) + } + + pixelPositionForScreenPosition (screenPosition) { + Grim.deprecate('This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead') + return this.getElement().pixelPositionForScreenPosition(screenPosition) + } + + getVerticalScrollMargin () { + const maxScrollMargin = Math.floor(((this.height / this.getLineHeightInPixels()) - 1) / 2) + return Math.min(this.verticalScrollMargin, maxScrollMargin) + } + + setVerticalScrollMargin (verticalScrollMargin) { + this.verticalScrollMargin = verticalScrollMargin + return this.verticalScrollMargin + } + + getHorizontalScrollMargin () { + return Math.min(this.horizontalScrollMargin, Math.floor(((this.width / this.getDefaultCharWidth()) - 1) / 2)) + } + setHorizontalScrollMargin (horizontalScrollMargin) { + this.horizontalScrollMargin = horizontalScrollMargin + return this.horizontalScrollMargin + } + + getLineHeightInPixels () { return this.lineHeightInPixels } + setLineHeightInPixels (lineHeightInPixels) { + this.lineHeightInPixels = lineHeightInPixels + return this.lineHeightInPixels + } + + getKoreanCharWidth () { return this.koreanCharWidth } + getHalfWidthCharWidth () { return this.halfWidthCharWidth } + getDoubleWidthCharWidth () { return this.doubleWidthCharWidth } + getDefaultCharWidth () { return this.defaultCharWidth } + + ratioForCharacter (character) { + if (isKoreanCharacter(character)) { + return this.getKoreanCharWidth() / this.getDefaultCharWidth() + } else if (isHalfWidthCharacter(character)) { + return this.getHalfWidthCharWidth() / this.getDefaultCharWidth() + } else if (isDoubleWidthCharacter(character)) { + return this.getDoubleWidthCharWidth() / this.getDefaultCharWidth() + } else { + return 1 + } + } + + setDefaultCharWidth (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) { + if (doubleWidthCharWidth == null) { doubleWidthCharWidth = defaultCharWidth } + if (halfWidthCharWidth == null) { halfWidthCharWidth = defaultCharWidth } + if (koreanCharWidth == null) { koreanCharWidth = defaultCharWidth } + if (defaultCharWidth !== this.defaultCharWidth || + (doubleWidthCharWidth !== this.doubleWidthCharWidth && + halfWidthCharWidth !== this.halfWidthCharWidth && + koreanCharWidth !== this.koreanCharWidth)) { + this.defaultCharWidth = defaultCharWidth + this.doubleWidthCharWidth = doubleWidthCharWidth + this.halfWidthCharWidth = halfWidthCharWidth + this.koreanCharWidth = koreanCharWidth + if (this.isSoftWrapped()) { + this.displayLayer.reset({ + softWrapColumn: this.getSoftWrapColumn() + }) + } + } + return defaultCharWidth + } + + setHeight (height) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setHeight instead.') + this.getElement().setHeight(height) + } + + getHeight () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getHeight instead.') + return this.getElement().getHeight() + } + + getAutoHeight () { return this.autoHeight != null ? this.autoHeight : true } + + getAutoWidth () { return this.autoWidth != null ? this.autoWidth : false } + + setWidth (width) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setWidth instead.') + this.getElement().setWidth(width) + } + + getWidth () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getWidth instead.') + return this.getElement().getWidth() + } + + // Use setScrollTopRow instead of this method + setFirstVisibleScreenRow (screenRow) { + this.setScrollTopRow(screenRow) + } + + getFirstVisibleScreenRow () { + return this.getElement().component.getFirstVisibleRow() + } + + getLastVisibleScreenRow () { + return this.getElement().component.getLastVisibleRow() + } + + getVisibleRowRange () { + return [this.getFirstVisibleScreenRow(), this.getLastVisibleScreenRow()] + } + + // Use setScrollLeftColumn instead of this method + setFirstVisibleScreenColumn (column) { + return this.setScrollLeftColumn(column) + } + + getFirstVisibleScreenColumn () { + return this.getElement().component.getFirstVisibleColumn() + } + + getScrollTop () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollTop instead.') + return this.getElement().getScrollTop() + } + + setScrollTop (scrollTop) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollTop instead.') + this.getElement().setScrollTop(scrollTop) + } + + getScrollBottom () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollBottom instead.') + return this.getElement().getScrollBottom() + } + + setScrollBottom (scrollBottom) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollBottom instead.') + this.getElement().setScrollBottom(scrollBottom) + } + + getScrollLeft () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollLeft instead.') + return this.getElement().getScrollLeft() + } + + setScrollLeft (scrollLeft) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollLeft instead.') + this.getElement().setScrollLeft(scrollLeft) + } + + getScrollRight () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollRight instead.') + return this.getElement().getScrollRight() + } + + setScrollRight (scrollRight) { + Grim.deprecate('This is now a view method. Call TextEditorElement::setScrollRight instead.') + this.getElement().setScrollRight(scrollRight) + } + + getScrollHeight () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollHeight instead.') + return this.getElement().getScrollHeight() + } + + getScrollWidth () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getScrollWidth instead.') + return this.getElement().getScrollWidth() + } + + getMaxScrollTop () { + Grim.deprecate('This is now a view method. Call TextEditorElement::getMaxScrollTop instead.') + return this.getElement().getMaxScrollTop() + } + + getScrollTopRow () { + return this.getElement().component.getScrollTopRow() + } + + setScrollTopRow (scrollTopRow) { + this.getElement().component.setScrollTopRow(scrollTopRow) + } + + getScrollLeftColumn () { + return this.getElement().component.getScrollLeftColumn() + } + + setScrollLeftColumn (scrollLeftColumn) { + this.getElement().component.setScrollLeftColumn(scrollLeftColumn) + } + + intersectsVisibleRowRange (startRow, endRow) { + Grim.deprecate('This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.') + return this.getElement().intersectsVisibleRowRange(startRow, endRow) + } + + selectionIntersectsVisibleRowRange (selection) { + Grim.deprecate('This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.') + return this.getElement().selectionIntersectsVisibleRowRange(selection) + } + + screenPositionForPixelPosition (pixelPosition) { + Grim.deprecate('This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.') + return this.getElement().screenPositionForPixelPosition(pixelPosition) + } + + pixelRectForScreenRange (screenRange) { + Grim.deprecate('This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.') + return this.getElement().pixelRectForScreenRange(screenRange) + } + + /* + Section: Utility + */ + + inspect () { + return `` + } + + emitWillInsertTextEvent (text) { + let result = true + const cancel = () => { result = false } + this.emitter.emit('will-insert-text', {cancel, text}) + return result + } + + /* + Section: Language Mode Delegated Methods + */ + + suggestedIndentForBufferRow (bufferRow, options) { + return this.tokenizedBuffer.suggestedIndentForBufferRow(bufferRow, options) + } + + // Given a buffer row, indent it. + // + // * bufferRow - The row {Number}. + // * options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}. + autoIndentBufferRow (bufferRow, options) { + const indentLevel = this.suggestedIndentForBufferRow(bufferRow, options) + return this.setIndentationForBufferRow(bufferRow, indentLevel, options) + } + + // Indents all the rows between two buffer row numbers. + // + // * startRow - The row {Number} to start at + // * endRow - The row {Number} to end at + autoIndentBufferRows (startRow, endRow) { + let row = startRow + while (row <= endRow) { + this.autoIndentBufferRow(row) + row++ + } + } + + autoDecreaseIndentForBufferRow (bufferRow) { + const indentLevel = this.tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow) + if (indentLevel != null) this.setIndentationForBufferRow(bufferRow, indentLevel) + } + + toggleLineCommentForBufferRow (row) { this.toggleLineCommentsForBufferRows(row, row) } + + toggleLineCommentsForBufferRows (start, end) { + let { + commentStartString, + commentEndString + } = this.tokenizedBuffer.commentStringsForPosition(Point(start, 0)) + if (!commentStartString) return + commentStartString = commentStartString.trim() + + if (commentEndString) { + commentEndString = commentEndString.trim() + const startDelimiterColumnRange = columnRangeForStartDelimiter( + this.buffer.lineForRow(start), + commentStartString + ) + if (startDelimiterColumnRange) { + const endDelimiterColumnRange = columnRangeForEndDelimiter( + this.buffer.lineForRow(end), + commentEndString + ) + if (endDelimiterColumnRange) { + this.buffer.transact(() => { + this.buffer.delete([[end, endDelimiterColumnRange[0]], [end, endDelimiterColumnRange[1]]]) + this.buffer.delete([[start, startDelimiterColumnRange[0]], [start, startDelimiterColumnRange[1]]]) + }) + } + } else { + this.buffer.transact(() => { + const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length + this.buffer.insert([start, indentLength], commentStartString + ' ') + this.buffer.insert([end, this.buffer.lineLengthForRow(end)], ' ' + commentEndString) + }) + } + } else { + let hasCommentedLines = false + let hasUncommentedLines = false + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + if (NON_WHITESPACE_REGEXP.test(line)) { + if (columnRangeForStartDelimiter(line, commentStartString)) { + hasCommentedLines = true + } else { + hasUncommentedLines = true + } + } + } + + const shouldUncomment = hasCommentedLines && !hasUncommentedLines + + if (shouldUncomment) { + for (let row = start; row <= end; row++) { + const columnRange = columnRangeForStartDelimiter( + this.buffer.lineForRow(row), + commentStartString + ) + if (columnRange) this.buffer.delete([[row, columnRange[0]], [row, columnRange[1]]]) + } + } else { + let minIndentLevel = Infinity + let minBlankIndentLevel = Infinity + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + const indentLevel = this.indentLevelForLine(line) + if (NON_WHITESPACE_REGEXP.test(line)) { + if (indentLevel < minIndentLevel) minIndentLevel = indentLevel + } else { + if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel + } + } + minIndentLevel = Number.isFinite(minIndentLevel) + ? minIndentLevel + : Number.isFinite(minBlankIndentLevel) + ? minBlankIndentLevel + : 0 + + const tabLength = this.getTabLength() + const indentString = ' '.repeat(tabLength * minIndentLevel) + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + if (NON_WHITESPACE_REGEXP.test(line)) { + const indentColumn = columnForIndentLevel(line, minIndentLevel, this.getTabLength()) + this.buffer.insert(Point(row, indentColumn), commentStartString + ' ') + } else { + this.buffer.setTextInRange( + new Range(new Point(row, 0), new Point(row, Infinity)), + indentString + commentStartString + ' ' + ) + } + } + } + } + } + + rowRangeForParagraphAtBufferRow (bufferRow) { + if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(bufferRow))) return + + const isCommented = this.tokenizedBuffer.isRowCommented(bufferRow) + + let startRow = bufferRow + while (startRow > 0) { + if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(startRow - 1))) break + if (this.tokenizedBuffer.isRowCommented(startRow - 1) !== isCommented) break + startRow-- + } + + let endRow = bufferRow + const rowCount = this.getLineCount() + while (endRow < rowCount) { + if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(endRow + 1))) break + if (this.tokenizedBuffer.isRowCommented(endRow + 1) !== isCommented) break + endRow++ + } + + return new Range(new Point(startRow, 0), new Point(endRow, this.buffer.lineLengthForRow(endRow))) + } +} + +function columnForIndentLevel (line, indentLevel, tabLength) { + let column = 0 + let indentLength = 0 + const goalIndentLength = indentLevel * tabLength + while (indentLength < goalIndentLength) { + const char = line[column] + if (char === '\t') { + indentLength += tabLength - (indentLength % tabLength) + } else if (char === ' ') { + indentLength++ + } else { + break + } + column++ + } + return column +} + +function columnRangeForStartDelimiter (line, delimiter) { + const startColumn = line.search(NON_WHITESPACE_REGEXP) + if (startColumn === -1) return null + if (!line.startsWith(delimiter, startColumn)) return null + + let endColumn = startColumn + delimiter.length + if (line[endColumn] === ' ') endColumn++ + return [startColumn, endColumn] +} + +function columnRangeForEndDelimiter (line, delimiter) { + let startColumn = line.lastIndexOf(delimiter) + if (startColumn === -1) return null + + const endColumn = startColumn + delimiter.length + if (NON_WHITESPACE_REGEXP.test(line.slice(endColumn))) return null + if (line[startColumn - 1] === ' ') startColumn-- + return [startColumn, endColumn] +} + +class ChangeEvent { + constructor ({oldRange, newRange}) { + this.oldRange = oldRange + this.newRange = newRange + } + + get start () { + return this.newRange.start + } + + get oldExtent () { + return this.oldRange.getExtent() + } + + get newExtent () { + return this.newRange.getExtent() + } +} From 616ebe71d940e28ecdee1522347e21444155f9ef Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 15:51:14 -0700 Subject: [PATCH 2/6] Convert text-editor-spec.coffee to JavaScript --- spec/text-editor-spec.coffee | 5873 ------------------------------ spec/text-editor-spec.js | 6656 +++++++++++++++++++++++++++++++++- 2 files changed, 6653 insertions(+), 5876 deletions(-) delete mode 100644 spec/text-editor-spec.coffee diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee deleted file mode 100644 index b8d4bdcf9..000000000 --- a/spec/text-editor-spec.coffee +++ /dev/null @@ -1,5873 +0,0 @@ -path = require 'path' -clipboard = require '../src/safe-clipboard' -TextEditor = require '../src/text-editor' -TextBuffer = require 'text-buffer' - -describe "TextEditor", -> - [buffer, editor, lineLengths] = [] - - convertToHardTabs = (buffer) -> - buffer.setText(buffer.getText().replace(/[ ]{2}/g, "\t")) - - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js', {autoIndent: false}).then (o) -> editor = o - - runs -> - buffer = editor.buffer - editor.update({autoIndent: false}) - lineLengths = buffer.getLines().map (line) -> line.length - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - describe "when the editor is deserialized", -> - it "restores selections and folds based on markers in the buffer", -> - editor.setSelectedBufferRange([[1, 2], [3, 4]]) - editor.addSelectionForBufferRange([[5, 6], [7, 5]], reversed: true) - editor.foldBufferRow(4) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - - waitsForPromise -> - TextBuffer.deserialize(editor.buffer.serialize()).then (buffer2) -> - editor2 = TextEditor.deserialize(editor.serialize(), { - assert: atom.assert, - textEditors: atom.textEditors, - project: {bufferForIdSync: -> buffer2} - }) - - expect(editor2.id).toBe editor.id - expect(editor2.getBuffer().getPath()).toBe editor.getBuffer().getPath() - expect(editor2.getSelectedBufferRanges()).toEqual [[[1, 2], [3, 4]], [[5, 6], [7, 5]]] - expect(editor2.getSelections()[1].isReversed()).toBeTruthy() - expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() - editor2.destroy() - - it "restores the editor's layout configuration", -> - editor.update({ - softTabs: true - atomicSoftTabs: false - tabLength: 12 - softWrapped: true - softWrapAtPreferredLineLength: true - softWrapHangingIndentLength: 8 - invisibles: {space: 'S'} - showInvisibles: true - editorWidthInChars: 120 - }) - - # Force buffer and display layer to be deserialized as well, rather than - # reusing the same buffer instance - waitsForPromise -> - TextBuffer.deserialize(editor.buffer.serialize()).then (buffer2) -> - editor2 = TextEditor.deserialize(editor.serialize(), { - assert: atom.assert, - textEditors: atom.textEditors, - project: {bufferForIdSync: -> buffer2} - }) - - expect(editor2.getSoftTabs()).toBe(editor.getSoftTabs()) - expect(editor2.hasAtomicSoftTabs()).toBe(editor.hasAtomicSoftTabs()) - expect(editor2.getTabLength()).toBe(editor.getTabLength()) - expect(editor2.getSoftWrapColumn()).toBe(editor.getSoftWrapColumn()) - expect(editor2.getSoftWrapHangingIndentLength()).toBe(editor.getSoftWrapHangingIndentLength()) - expect(editor2.getInvisibles()).toEqual(editor.getInvisibles()) - expect(editor2.getEditorWidthInChars()).toBe(editor.getEditorWidthInChars()) - expect(editor2.displayLayer.tabLength).toBe(editor2.getTabLength()) - expect(editor2.displayLayer.softWrapColumn).toBe(editor2.getSoftWrapColumn()) - - it "ignores buffers with retired IDs", -> - editor2 = TextEditor.deserialize(editor.serialize(), { - assert: atom.assert, - textEditors: atom.textEditors, - project: {bufferForIdSync: -> null} - }) - - expect(editor2).toBeNull() - - describe "when the editor is constructed with the largeFileMode option set to true", -> - it "loads the editor but doesn't tokenize", -> - editor = null - - waitsForPromise -> - atom.workspace.openTextFile('sample.js', largeFileMode: true).then (o) -> editor = o - - runs -> - buffer = editor.getBuffer() - expect(editor.lineTextForScreenRow(0)).toBe buffer.lineForRow(0) - expect(editor.tokensForScreenRow(0).length).toBe 1 - expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab - expect(editor.lineTextForScreenRow(12)).toBe buffer.lineForRow(12) - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - editor.insertText('hey"') - expect(editor.tokensForScreenRow(0).length).toBe 1 - expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab - - describe ".copy()", -> - it "returns a different editor with the same initial state", -> - expect(editor.getAutoHeight()).toBeFalsy() - expect(editor.getAutoWidth()).toBeFalsy() - expect(editor.getShowCursorOnSelection()).toBeTruthy() - - element = editor.getElement() - element.setHeight(100) - element.setWidth(100) - jasmine.attachToDOM(element) - - editor.update({showCursorOnSelection: false}) - editor.setSelectedBufferRange([[1, 2], [3, 4]]) - editor.addSelectionForBufferRange([[5, 6], [7, 8]], reversed: true) - editor.setScrollTopRow(3) - expect(editor.getScrollTopRow()).toBe(3) - editor.setScrollLeftColumn(4) - expect(editor.getScrollLeftColumn()).toBe(4) - editor.foldBufferRow(4) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - - editor2 = editor.copy() - element2 = editor2.getElement() - element2.setHeight(100) - element2.setWidth(100) - jasmine.attachToDOM(element2) - expect(editor2.id).not.toBe editor.id - expect(editor2.getSelectedBufferRanges()).toEqual editor.getSelectedBufferRanges() - expect(editor2.getSelections()[1].isReversed()).toBeTruthy() - expect(editor2.getScrollTopRow()).toBe(3) - expect(editor2.getScrollLeftColumn()).toBe(4) - expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor2.getAutoWidth()).toBe(false) - expect(editor2.getAutoHeight()).toBe(false) - expect(editor2.getShowCursorOnSelection()).toBeFalsy() - - # editor2 can now diverge from its origin edit session - editor2.getLastSelection().setBufferRange([[2, 1], [4, 3]]) - expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges() - editor2.unfoldBufferRow(4) - expect(editor2.isFoldedAtBufferRow(4)).not.toBe editor.isFoldedAtBufferRow(4) - - describe ".update()", -> - it "updates the editor with the supplied config parameters", -> - element = editor.element # force element initialization - element.setUpdatedSynchronously(false) - editor.update({showInvisibles: true}) - editor.onDidChange(changeSpy = jasmine.createSpy('onDidChange')) - - returnedPromise = editor.update({ - tabLength: 6, softTabs: false, softWrapped: true, editorWidthInChars: 40, - showInvisibles: false, mini: false, lineNumberGutterVisible: false, scrollPastEnd: true, - autoHeight: false, maxScreenLineLength: 1000 - }) - - expect(returnedPromise).toBe(element.component.getNextUpdatePromise()) - expect(changeSpy.callCount).toBe(1) - expect(editor.getTabLength()).toBe(6) - expect(editor.getSoftTabs()).toBe(false) - expect(editor.isSoftWrapped()).toBe(true) - expect(editor.getEditorWidthInChars()).toBe(40) - expect(editor.getInvisibles()).toEqual({}) - expect(editor.isMini()).toBe(false) - expect(editor.isLineNumberGutterVisible()).toBe(false) - expect(editor.getScrollPastEnd()).toBe(true) - expect(editor.getAutoHeight()).toBe(false) - - describe "title", -> - describe ".getTitle()", -> - it "uses the basename of the buffer's path as its title, or 'untitled' if the path is undefined", -> - expect(editor.getTitle()).toBe 'sample.js' - buffer.setPath(undefined) - expect(editor.getTitle()).toBe 'untitled' - - describe ".getLongTitle()", -> - it "returns file name when there is no opened file with identical name", -> - expect(editor.getLongTitle()).toBe 'sample.js' - buffer.setPath(undefined) - expect(editor.getLongTitle()).toBe 'untitled' - - it "returns '' when opened files have identical file names", -> - editor1 = null - editor2 = null - waitsForPromise -> - atom.workspace.open(path.join('sample-theme-1', 'readme')).then (o) -> - editor1 = o - atom.workspace.open(path.join('sample-theme-2', 'readme')).then (o) -> - editor2 = o - runs -> - expect(editor1.getLongTitle()).toBe "readme \u2014 sample-theme-1" - expect(editor2.getLongTitle()).toBe "readme \u2014 sample-theme-2" - - it "returns '' when opened files have identical file names in subdirectories", -> - editor1 = null - editor2 = null - path1 = path.join('sample-theme-1', 'src', 'js') - path2 = path.join('sample-theme-2', 'src', 'js') - waitsForPromise -> - atom.workspace.open(path.join(path1, 'main.js')).then (o) -> - editor1 = o - atom.workspace.open(path.join(path2, 'main.js')).then (o) -> - editor2 = o - runs -> - expect(editor1.getLongTitle()).toBe "main.js \u2014 #{path1}" - expect(editor2.getLongTitle()).toBe "main.js \u2014 #{path2}" - - it "returns '' when opened files have identical file and same parent dir name", -> - editor1 = null - editor2 = null - waitsForPromise -> - atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) -> - editor1 = o - atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'plugin', 'main.js')).then (o) -> - editor2 = o - runs -> - expect(editor1.getLongTitle()).toBe "main.js \u2014 js" - expect(editor2.getLongTitle()).toBe "main.js \u2014 " + path.join('js', 'plugin') - - it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", -> - observed = [] - editor.onDidChangeTitle (title) -> observed.push(title) - - buffer.setPath('/foo/bar/baz.txt') - buffer.setPath(undefined) - - expect(observed).toEqual ['baz.txt', 'untitled'] - - describe "path", -> - it "notifies ::onDidChangePath observers when the underlying buffer path changes", -> - observed = [] - editor.onDidChangePath (filePath) -> observed.push(filePath) - - buffer.setPath(__filename) - buffer.setPath(undefined) - - expect(observed).toEqual [__filename, undefined] - - describe "encoding", -> - it "notifies ::onDidChangeEncoding observers when the editor encoding changes", -> - observed = [] - editor.onDidChangeEncoding (encoding) -> observed.push(encoding) - - editor.setEncoding('utf16le') - editor.setEncoding('utf16le') - editor.setEncoding('utf16be') - editor.setEncoding() - editor.setEncoding() - - expect(observed).toEqual ['utf16le', 'utf16be', 'utf8'] - - describe "cursor", -> - describe ".getLastCursor()", -> - it "returns the most recently created cursor", -> - editor.addCursorAtScreenPosition([1, 0]) - lastCursor = editor.addCursorAtScreenPosition([2, 0]) - expect(editor.getLastCursor()).toBe lastCursor - - it "creates a new cursor at (0, 0) if the last cursor has been destroyed", -> - editor.getLastCursor().destroy() - expect(editor.getLastCursor().getBufferPosition()).toEqual([0, 0]) - - describe ".getCursors()", -> - it "creates a new cursor at (0, 0) if the last cursor has been destroyed", -> - editor.getLastCursor().destroy() - expect(editor.getCursors()[0].getBufferPosition()).toEqual([0, 0]) - - describe "when the cursor moves", -> - it "clears a goal column established by vertical movement", -> - editor.setText('b') - editor.setCursorBufferPosition([0, 0]) - editor.insertNewline() - editor.moveUp() - editor.insertText('a') - editor.moveDown() - expect(editor.getCursorBufferPosition()).toEqual [1, 1] - - it "emits an event with the old position, new position, and the cursor that moved", -> - cursorCallback = jasmine.createSpy('cursor-changed-position') - editorCallback = jasmine.createSpy('editor-changed-cursor-position') - - editor.getLastCursor().onDidChangePosition(cursorCallback) - editor.onDidChangeCursorPosition(editorCallback) - - editor.setCursorBufferPosition([2, 4]) - - expect(editorCallback).toHaveBeenCalled() - expect(cursorCallback).toHaveBeenCalled() - eventObject = editorCallback.mostRecentCall.args[0] - expect(cursorCallback.mostRecentCall.args[0]).toEqual(eventObject) - - expect(eventObject.oldBufferPosition).toEqual [0, 0] - expect(eventObject.oldScreenPosition).toEqual [0, 0] - expect(eventObject.newBufferPosition).toEqual [2, 4] - expect(eventObject.newScreenPosition).toEqual [2, 4] - expect(eventObject.cursor).toBe editor.getLastCursor() - - describe ".setCursorScreenPosition(screenPosition)", -> - it "clears a goal column established by vertical movement", -> - # set a goal column by moving down - editor.setCursorScreenPosition(row: 3, column: lineLengths[3]) - editor.moveDown() - expect(editor.getCursorScreenPosition().column).not.toBe 6 - - # clear the goal column by explicitly setting the cursor position - editor.setCursorScreenPosition([4, 6]) - expect(editor.getCursorScreenPosition().column).toBe 6 - - editor.moveDown() - expect(editor.getCursorScreenPosition().column).toBe 6 - - it "merges multiple cursors", -> - editor.setCursorScreenPosition([0, 0]) - editor.addCursorAtScreenPosition([0, 1]) - [cursor1, cursor2] = editor.getCursors() - editor.setCursorScreenPosition([4, 7]) - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursors()).toEqual [cursor1] - expect(editor.getCursorScreenPosition()).toEqual [4, 7] - - describe "when soft-wrap is enabled and code is folded", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - editor.foldBufferRowRange(2, 3) - - it "positions the cursor at the buffer position that corresponds to the given screen position", -> - editor.setCursorScreenPosition([9, 0]) - expect(editor.getCursorBufferPosition()).toEqual [8, 11] - - describe ".moveUp()", -> - it "moves the cursor up", -> - editor.setCursorScreenPosition([2, 2]) - editor.moveUp() - expect(editor.getCursorScreenPosition()).toEqual [1, 2] - - it "retains the goal column across lines of differing length", -> - expect(lineLengths[6]).toBeGreaterThan(32) - editor.setCursorScreenPosition(row: 6, column: 32) - - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[5] - - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[4] - - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe 32 - - describe "when the cursor is on the first line", -> - it "moves the cursor to the beginning of the line, but retains the goal column", -> - editor.setCursorScreenPosition([0, 4]) - editor.moveUp() - expect(editor.getCursorScreenPosition()).toEqual([0, 0]) - - editor.moveDown() - expect(editor.getCursorScreenPosition()).toEqual([1, 4]) - - describe "when there is a selection", -> - beforeEach -> - editor.setSelectedBufferRange([[4, 9], [5, 10]]) - - it "moves above the selection", -> - cursor = editor.getLastCursor() - editor.moveUp() - expect(cursor.getBufferPosition()).toEqual [3, 9] - - it "merges cursors when they overlap", -> - editor.addCursorAtScreenPosition([1, 0]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveUp() - expect(editor.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [0, 0] - - describe "when the cursor was moved down from the beginning of an indented soft-wrapped line", -> - it "moves to the beginning of the previous line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - - editor.setCursorScreenPosition([3, 0]) - editor.moveDown() - editor.moveDown() - editor.moveUp() - expect(editor.getCursorScreenPosition()).toEqual [4, 4] - - describe ".moveDown()", -> - it "moves the cursor down", -> - editor.setCursorScreenPosition([2, 2]) - editor.moveDown() - expect(editor.getCursorScreenPosition()).toEqual [3, 2] - - it "retains the goal column across lines of differing length", -> - editor.setCursorScreenPosition(row: 3, column: lineLengths[3]) - - editor.moveDown() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[4] - - editor.moveDown() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[5] - - editor.moveDown() - expect(editor.getCursorScreenPosition().column).toBe lineLengths[3] - - describe "when the cursor is on the last line", -> - it "moves the cursor to the end of line, but retains the goal column when moving back up", -> - lastLineIndex = buffer.getLines().length - 1 - lastLine = buffer.lineForRow(lastLineIndex) - expect(lastLine.length).toBeGreaterThan(0) - - editor.setCursorScreenPosition(row: lastLineIndex, column: editor.getTabLength()) - editor.moveDown() - expect(editor.getCursorScreenPosition()).toEqual(row: lastLineIndex, column: lastLine.length) - - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe editor.getTabLength() - - it "retains a goal column of 0 when moving back up", -> - lastLineIndex = buffer.getLines().length - 1 - lastLine = buffer.lineForRow(lastLineIndex) - expect(lastLine.length).toBeGreaterThan(0) - - editor.setCursorScreenPosition(row: lastLineIndex, column: 0) - editor.moveDown() - editor.moveUp() - expect(editor.getCursorScreenPosition().column).toBe 0 - - describe "when the cursor is at the beginning of an indented soft-wrapped line", -> - it "moves to the beginning of the line's continuation on the next screen row", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - - editor.setCursorScreenPosition([3, 0]) - editor.moveDown() - expect(editor.getCursorScreenPosition()).toEqual [4, 4] - - - describe "when there is a selection", -> - beforeEach -> - editor.setSelectedBufferRange([[4, 9], [5, 10]]) - - it "moves below the selection", -> - cursor = editor.getLastCursor() - editor.moveDown() - expect(cursor.getBufferPosition()).toEqual [6, 10] - - it "merges cursors when they overlap", -> - editor.setCursorScreenPosition([12, 2]) - editor.addCursorAtScreenPosition([11, 2]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveDown() - expect(editor.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [12, 2] - - describe ".moveLeft()", -> - it "moves the cursor by one column to the left", -> - editor.setCursorScreenPosition([1, 8]) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual [1, 7] - - it "moves the cursor by n columns to the left", -> - editor.setCursorScreenPosition([1, 8]) - editor.moveLeft(4) - expect(editor.getCursorScreenPosition()).toEqual [1, 4] - - it "moves the cursor by two rows up when the columnCount is longer than an entire line", -> - editor.setCursorScreenPosition([2, 2]) - editor.moveLeft(34) - expect(editor.getCursorScreenPosition()).toEqual [0, 29] - - it "moves the cursor to the beginning columnCount is longer than the position in the buffer", -> - editor.setCursorScreenPosition([1, 0]) - editor.moveLeft(100) - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - - describe "when the cursor is in the first column", -> - describe "when there is a previous line", -> - it "wraps to the end of the previous line", -> - editor.setCursorScreenPosition(row: 1, column: 0) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: buffer.lineForRow(0).length) - - it "moves the cursor by one row up and n columns to the left", -> - editor.setCursorScreenPosition([1, 0]) - editor.moveLeft(4) - expect(editor.getCursorScreenPosition()).toEqual [0, 26] - - describe "when the next line is empty", -> - it "wraps to the beginning of the previous line", -> - editor.setCursorScreenPosition([11, 0]) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual [10, 0] - - describe "when line is wrapped and follow previous line indentation", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(50) - - it "wraps to the end of the previous line", -> - editor.setCursorScreenPosition([4, 4]) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual [3, 46] - - describe "when the cursor is on the first line", -> - it "remains in the same position (0,0)", -> - editor.setCursorScreenPosition(row: 0, column: 0) - editor.moveLeft() - expect(editor.getCursorScreenPosition()).toEqual(row: 0, column: 0) - - it "remains in the same position (0,0) when columnCount is specified", -> - editor.setCursorScreenPosition([0, 0]) - editor.moveLeft(4) - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - - describe "when softTabs is enabled and the cursor is preceded by leading whitespace", -> - it "skips tabLength worth of whitespace at a time", -> - editor.setCursorBufferPosition([5, 6]) - - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [5, 4] - - describe "when there is a selection", -> - beforeEach -> - editor.setSelectedBufferRange([[5, 22], [5, 27]]) - - it "moves to the left of the selection", -> - cursor = editor.getLastCursor() - editor.moveLeft() - expect(cursor.getBufferPosition()).toEqual [5, 22] - - editor.moveLeft() - expect(cursor.getBufferPosition()).toEqual [5, 21] - - it "merges cursors when they overlap", -> - editor.setCursorScreenPosition([0, 0]) - editor.addCursorAtScreenPosition([0, 1]) - - [cursor1, cursor2] = editor.getCursors() - editor.moveLeft() - expect(editor.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [0, 0] - - describe ".moveRight()", -> - it "moves the cursor by one column to the right", -> - editor.setCursorScreenPosition([3, 3]) - editor.moveRight() - expect(editor.getCursorScreenPosition()).toEqual [3, 4] - - it "moves the cursor by n columns to the right", -> - editor.setCursorScreenPosition([3, 7]) - editor.moveRight(4) - expect(editor.getCursorScreenPosition()).toEqual [3, 11] - - it "moves the cursor by two rows down when the columnCount is longer than an entire line", -> - editor.setCursorScreenPosition([0, 29]) - editor.moveRight(34) - expect(editor.getCursorScreenPosition()).toEqual [2, 2] - - it "moves the cursor to the end of the buffer when columnCount is longer than the number of characters following the cursor position", -> - editor.setCursorScreenPosition([11, 5]) - editor.moveRight(100) - expect(editor.getCursorScreenPosition()).toEqual [12, 2] - - describe "when the cursor is on the last column of a line", -> - describe "when there is a subsequent line", -> - it "wraps to the beginning of the next line", -> - editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) - editor.moveRight() - expect(editor.getCursorScreenPosition()).toEqual [1, 0] - - it "moves the cursor by one row down and n columns to the right", -> - editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) - editor.moveRight(4) - expect(editor.getCursorScreenPosition()).toEqual [1, 3] - - describe "when the next line is empty", -> - it "wraps to the beginning of the next line", -> - editor.setCursorScreenPosition([9, 4]) - editor.moveRight() - expect(editor.getCursorScreenPosition()).toEqual [10, 0] - - describe "when the cursor is on the last line", -> - it "remains in the same position", -> - lastLineIndex = buffer.getLines().length - 1 - lastLine = buffer.lineForRow(lastLineIndex) - expect(lastLine.length).toBeGreaterThan(0) - - lastPosition = {row: lastLineIndex, column: lastLine.length} - editor.setCursorScreenPosition(lastPosition) - editor.moveRight() - - expect(editor.getCursorScreenPosition()).toEqual(lastPosition) - - describe "when there is a selection", -> - beforeEach -> - editor.setSelectedBufferRange([[5, 22], [5, 27]]) - - it "moves to the left of the selection", -> - cursor = editor.getLastCursor() - editor.moveRight() - expect(cursor.getBufferPosition()).toEqual [5, 27] - - editor.moveRight() - expect(cursor.getBufferPosition()).toEqual [5, 28] - - it "merges cursors when they overlap", -> - editor.setCursorScreenPosition([12, 2]) - editor.addCursorAtScreenPosition([12, 1]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveRight() - expect(editor.getCursors()).toEqual [cursor1] - expect(cursor1.getBufferPosition()).toEqual [12, 2] - - describe ".moveToTop()", -> - it "moves the cursor to the top of the buffer", -> - editor.setCursorScreenPosition [11, 1] - editor.addCursorAtScreenPosition [12, 0] - editor.moveToTop() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe ".moveToBottom()", -> - it "moves the cursor to the bottom of the buffer", -> - editor.setCursorScreenPosition [0, 0] - editor.addCursorAtScreenPosition [1, 0] - editor.moveToBottom() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursorBufferPosition()).toEqual [12, 2] - - describe ".moveToBeginningOfScreenLine()", -> - describe "when soft wrap is on", -> - it "moves cursor to the beginning of the screen line", -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([1, 2]) - editor.moveToBeginningOfScreenLine() - cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [1, 0] - - describe "when soft wrap is off", -> - it "moves cursor to the beginning of the line", -> - editor.setCursorScreenPosition [0, 5] - editor.addCursorAtScreenPosition [1, 7] - editor.moveToBeginningOfScreenLine() - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor2.getBufferPosition()).toEqual [1, 0] - - describe ".moveToEndOfScreenLine()", -> - describe "when soft wrap is on", -> - it "moves cursor to the beginning of the screen line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([1, 2]) - editor.moveToEndOfScreenLine() - cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [1, 9] - - describe "when soft wrap is off", -> - it "moves cursor to the end of line", -> - editor.setCursorScreenPosition [0, 0] - editor.addCursorAtScreenPosition [1, 0] - editor.moveToEndOfScreenLine() - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 29] - expect(cursor2.getBufferPosition()).toEqual [1, 30] - - describe ".moveToBeginningOfLine()", -> - it "moves cursor to the beginning of the buffer line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([1, 2]) - editor.moveToBeginningOfLine() - cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [0, 0] - - describe ".moveToEndOfLine()", -> - it "moves cursor to the end of the buffer line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition([0, 2]) - editor.moveToEndOfLine() - cursor = editor.getLastCursor() - expect(cursor.getScreenPosition()).toEqual [4, 4] - - describe ".moveToFirstCharacterOfLine()", -> - describe "when soft wrap is on", -> - it "moves to the first character of the current screen line or the beginning of the screen line if it's already on the first character", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(10) - editor.setCursorScreenPosition [2, 5] - editor.addCursorAtScreenPosition [8, 7] - - editor.moveToFirstCharacterOfLine() - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getScreenPosition()).toEqual [2, 0] - expect(cursor2.getScreenPosition()).toEqual [8, 2] - - editor.moveToFirstCharacterOfLine() - expect(cursor1.getScreenPosition()).toEqual [2, 0] - expect(cursor2.getScreenPosition()).toEqual [8, 2] - - describe "when soft wrap is off", -> - it "moves to the first character of the current line or the beginning of the line if it's already on the first character", -> - editor.setCursorScreenPosition [0, 5] - editor.addCursorAtScreenPosition [1, 7] - - editor.moveToFirstCharacterOfLine() - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor2.getBufferPosition()).toEqual [1, 2] - - editor.moveToFirstCharacterOfLine() - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor2.getBufferPosition()).toEqual [1, 0] - - it "moves to the beginning of the line if it only contains whitespace ", -> - editor.setText("first\n \nthird") - editor.setCursorScreenPosition [1, 2] - editor.moveToFirstCharacterOfLine() - cursor = editor.getLastCursor() - expect(cursor.getBufferPosition()).toEqual [1, 0] - - describe "when invisible characters are enabled with soft tabs", -> - it "moves to the first character of the current line without being confused by the invisible characters", -> - editor.update({showInvisibles: true}) - editor.setCursorScreenPosition [1, 7] - editor.moveToFirstCharacterOfLine() - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - editor.moveToFirstCharacterOfLine() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - - describe "when invisible characters are enabled with hard tabs", -> - it "moves to the first character of the current line without being confused by the invisible characters", -> - editor.update({showInvisibles: true}) - buffer.setTextInRange([[1, 0], [1, Infinity]], '\t\t\ta', normalizeLineEndings: false) - - editor.setCursorScreenPosition [1, 7] - editor.moveToFirstCharacterOfLine() - expect(editor.getCursorBufferPosition()).toEqual [1, 3] - editor.moveToFirstCharacterOfLine() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - - describe ".moveToBeginningOfWord()", -> - it "moves the cursor to the beginning of the word", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [1, 12] - editor.addCursorAtBufferPosition [3, 0] - [cursor1, cursor2, cursor3] = editor.getCursors() - - editor.moveToBeginningOfWord() - - expect(cursor1.getBufferPosition()).toEqual [0, 4] - expect(cursor2.getBufferPosition()).toEqual [1, 11] - expect(cursor3.getBufferPosition()).toEqual [2, 39] - - it "does not fail at position [0, 0]", -> - editor.setCursorBufferPosition([0, 0]) - editor.moveToBeginningOfWord() - - it "treats lines with only whitespace as a word", -> - editor.setCursorBufferPosition([11, 0]) - editor.moveToBeginningOfWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "treats lines with only whitespace as a word (CRLF line ending)", -> - editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) - editor.setCursorBufferPosition([11, 0]) - editor.moveToBeginningOfWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "works when the current line is blank", -> - editor.setCursorBufferPosition([10, 0]) - editor.moveToBeginningOfWord() - expect(editor.getCursorBufferPosition()).toEqual [9, 2] - - it "works when the current line is blank (CRLF line ending)", -> - editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) - editor.setCursorBufferPosition([10, 0]) - editor.moveToBeginningOfWord() - expect(editor.getCursorBufferPosition()).toEqual [9, 2] - editor.buffer.setText(buffer.getText().replace(/\r\n/g, "\n")) - - describe ".moveToPreviousWordBoundary()", -> - it "moves the cursor to the previous word boundary", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [2, 0] - editor.addCursorAtBufferPosition [2, 4] - editor.addCursorAtBufferPosition [3, 14] - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.moveToPreviousWordBoundary() - - expect(cursor1.getBufferPosition()).toEqual [0, 4] - expect(cursor2.getBufferPosition()).toEqual [1, 30] - expect(cursor3.getBufferPosition()).toEqual [2, 0] - expect(cursor4.getBufferPosition()).toEqual [3, 13] - - describe ".moveToNextWordBoundary()", -> - it "moves the cursor to the previous word boundary", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [2, 40] - editor.addCursorAtBufferPosition [3, 0] - editor.addCursorAtBufferPosition [3, 30] - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.moveToNextWordBoundary() - - expect(cursor1.getBufferPosition()).toEqual [0, 13] - expect(cursor2.getBufferPosition()).toEqual [3, 0] - expect(cursor3.getBufferPosition()).toEqual [3, 4] - expect(cursor4.getBufferPosition()).toEqual [3, 31] - - describe ".moveToEndOfWord()", -> - it "moves the cursor to the end of the word", -> - editor.setCursorBufferPosition [0, 6] - editor.addCursorAtBufferPosition [1, 10] - editor.addCursorAtBufferPosition [2, 40] - [cursor1, cursor2, cursor3] = editor.getCursors() - - editor.moveToEndOfWord() - - expect(cursor1.getBufferPosition()).toEqual [0, 13] - expect(cursor2.getBufferPosition()).toEqual [1, 12] - expect(cursor3.getBufferPosition()).toEqual [3, 7] - - it "does not blow up when there is no next word", -> - editor.setCursorBufferPosition [Infinity, Infinity] - endPosition = editor.getCursorBufferPosition() - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual endPosition - - it "treats lines with only whitespace as a word", -> - editor.setCursorBufferPosition([9, 4]) - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "treats lines with only whitespace as a word (CRLF line ending)", -> - editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) - editor.setCursorBufferPosition([9, 4]) - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "works when the current line is blank", -> - editor.setCursorBufferPosition([10, 0]) - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual [11, 8] - - it "works when the current line is blank (CRLF line ending)", -> - editor.buffer.setText(buffer.getText().replace(/\n/g, "\r\n")) - editor.setCursorBufferPosition([10, 0]) - editor.moveToEndOfWord() - expect(editor.getCursorBufferPosition()).toEqual [11, 8] - - describe ".moveToBeginningOfNextWord()", -> - it "moves the cursor before the first character of the next word", -> - editor.setCursorBufferPosition [0, 6] - editor.addCursorAtBufferPosition [1, 11] - editor.addCursorAtBufferPosition [2, 0] - [cursor1, cursor2, cursor3] = editor.getCursors() - - editor.moveToBeginningOfNextWord() - - expect(cursor1.getBufferPosition()).toEqual [0, 14] - expect(cursor2.getBufferPosition()).toEqual [1, 13] - expect(cursor3.getBufferPosition()).toEqual [2, 4] - - # When the cursor is on whitespace - editor.setText("ab cde- ") - editor.setCursorBufferPosition [0, 2] - cursor = editor.getLastCursor() - editor.moveToBeginningOfNextWord() - - expect(cursor.getBufferPosition()).toEqual [0, 3] - - it "does not blow up when there is no next word", -> - editor.setCursorBufferPosition [Infinity, Infinity] - endPosition = editor.getCursorBufferPosition() - editor.moveToBeginningOfNextWord() - expect(editor.getCursorBufferPosition()).toEqual endPosition - - it "treats lines with only whitespace as a word", -> - editor.setCursorBufferPosition([9, 4]) - editor.moveToBeginningOfNextWord() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - it "works when the current line is blank", -> - editor.setCursorBufferPosition([10, 0]) - editor.moveToBeginningOfNextWord() - expect(editor.getCursorBufferPosition()).toEqual [11, 9] - - describe ".moveToPreviousSubwordBoundary", -> - it "does not move the cursor when there is no previous subword boundary", -> - editor.setText('') - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - it "stops at word and underscore boundaries", -> - editor.setText("sub_word \n") - editor.setCursorBufferPosition([0, 9]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 8]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - editor.setText(" word\n") - editor.setCursorBufferPosition([0, 3]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "stops at camelCase boundaries", -> - editor.setText(" getPreviousWord\n") - editor.setCursorBufferPosition([0, 16]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 12]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "skips consecutive non-word characters", -> - editor.setText("e, => \n") - editor.setCursorBufferPosition([0, 6]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "skips consecutive uppercase characters", -> - editor.setText(" AAADF \n") - editor.setCursorBufferPosition([0, 7]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 6]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.setText("ALPhA\n") - editor.setCursorBufferPosition([0, 4]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 2]) - - it "skips consecutive numbers", -> - editor.setText(" 88 \n") - editor.setCursorBufferPosition([0, 4]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - it "works with multiple cursors", -> - editor.setText("curOp\ncursorOptions\n") - editor.setCursorBufferPosition([0, 8]) - editor.addCursorAtBufferPosition([1, 13]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveToPreviousSubwordBoundary() - - expect(cursor1.getBufferPosition()).toEqual([0, 3]) - expect(cursor2.getBufferPosition()).toEqual([1, 6]) - - it "works with non-English characters", -> - editor.setText("supåTøåst \n") - editor.setCursorBufferPosition([0, 9]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.setText("supaÖast \n") - editor.setCursorBufferPosition([0, 8]) - editor.moveToPreviousSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - describe ".moveToNextSubwordBoundary", -> - it "does not move the cursor when there is no next subword boundary", -> - editor.setText('') - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 0]) - - it "stops at word and underscore boundaries", -> - editor.setText(" sub_word \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 9]) - - editor.setText("word \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - it "stops at camelCase boundaries", -> - editor.setText("getPreviousWord \n") - editor.setCursorBufferPosition([0, 0]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 11]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 15]) - - it "skips consecutive non-word characters", -> - editor.setText(", => \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - it "skips consecutive uppercase characters", -> - editor.setText(" AAADF \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 6]) - - editor.setText("ALPhA\n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 2]) - - it "skips consecutive numbers", -> - editor.setText(" 88 \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 1]) - - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 3]) - - it "works with multiple cursors", -> - editor.setText("curOp\ncursorOptions\n") - editor.setCursorBufferPosition([0, 0]) - editor.addCursorAtBufferPosition([1, 0]) - [cursor1, cursor2] = editor.getCursors() - - editor.moveToNextSubwordBoundary() - expect(cursor1.getBufferPosition()).toEqual([0, 3]) - expect(cursor2.getBufferPosition()).toEqual([1, 6]) - - it "works with non-English characters", -> - editor.setText("supåTøåst \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - editor.setText("supaÖast \n") - editor.setCursorBufferPosition([0, 0]) - editor.moveToNextSubwordBoundary() - expect(editor.getCursorBufferPosition()).toEqual([0, 4]) - - describe ".moveToBeginningOfNextParagraph()", -> - it "moves the cursor before the first line of the next paragraph", -> - editor.setCursorBufferPosition [0, 6] - editor.foldBufferRow(4) - - editor.moveToBeginningOfNextParagraph() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - editor.setText("") - editor.setCursorBufferPosition [0, 0] - editor.moveToBeginningOfNextParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - it "moves the cursor before the first line of the next paragraph (CRLF line endings)", -> - editor.setText(editor.getText().replace(/\n/g, '\r\n')) - - editor.setCursorBufferPosition [0, 6] - editor.foldBufferRow(4) - - editor.moveToBeginningOfNextParagraph() - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - editor.setText("") - editor.setCursorBufferPosition [0, 0] - editor.moveToBeginningOfNextParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe ".moveToBeginningOfPreviousParagraph()", -> - it "moves the cursor before the first line of the previous paragraph", -> - editor.setCursorBufferPosition [10, 0] - editor.foldBufferRow(4) - - editor.moveToBeginningOfPreviousParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - editor.setText("") - editor.setCursorBufferPosition [0, 0] - editor.moveToBeginningOfPreviousParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - it "moves the cursor before the first line of the previous paragraph (CRLF line endings)", -> - editor.setText(editor.getText().replace(/\n/g, '\r\n')) - - editor.setCursorBufferPosition [10, 0] - editor.foldBufferRow(4) - - editor.moveToBeginningOfPreviousParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - editor.setText("") - editor.setCursorBufferPosition [0, 0] - editor.moveToBeginningOfPreviousParagraph() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe ".getCurrentParagraphBufferRange()", -> - it "returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file", -> - buffer.setText """ - I am the first paragraph, - bordered by the beginning of - the file - #{' '} - - I am the second paragraph - with blank lines above and below - me. - - I am the last paragraph, - bordered by the end of the file. - """ - - # in a paragraph - editor.setCursorBufferPosition([1, 7]) - expect(editor.getCurrentParagraphBufferRange()).toEqual [[0, 0], [2, 8]] - - editor.setCursorBufferPosition([7, 1]) - expect(editor.getCurrentParagraphBufferRange()).toEqual [[5, 0], [7, 3]] - - editor.setCursorBufferPosition([9, 10]) - expect(editor.getCurrentParagraphBufferRange()).toEqual [[9, 0], [10, 32]] - - # between paragraphs - editor.setCursorBufferPosition([3, 1]) - expect(editor.getCurrentParagraphBufferRange()).toBeUndefined() - - it 'will limit paragraph range to comments', -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - runs -> - editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) - editor.setText(""" - var quicksort = function () { - /* Single line comment block */ - var sort = function(items) {}; - - /* - A multiline - comment is here - */ - var sort = function(items) {}; - - // A comment - // - // Multiple comment - // lines - var sort = function(items) {}; - // comment line after fn - - var nosort = function(items) { - item; - } - - }; - """) - - paragraphBufferRangeForRow = (row) -> - editor.setCursorBufferPosition([row, 0]) - editor.getLastCursor().getCurrentParagraphBufferRange() - - expect(paragraphBufferRangeForRow(0)).toEqual([[0, 0], [0, 29]]) - expect(paragraphBufferRangeForRow(1)).toEqual([[1, 0], [1, 33]]) - expect(paragraphBufferRangeForRow(2)).toEqual([[2, 0], [2, 32]]) - expect(paragraphBufferRangeForRow(3)).toBeFalsy() - expect(paragraphBufferRangeForRow(4)).toEqual([[4, 0], [7, 4]]) - expect(paragraphBufferRangeForRow(5)).toEqual([[4, 0], [7, 4]]) - expect(paragraphBufferRangeForRow(6)).toEqual([[4, 0], [7, 4]]) - expect(paragraphBufferRangeForRow(7)).toEqual([[4, 0], [7, 4]]) - expect(paragraphBufferRangeForRow(8)).toEqual([[8, 0], [8, 32]]) - expect(paragraphBufferRangeForRow(9)).toBeFalsy() - expect(paragraphBufferRangeForRow(10)).toEqual([[10, 0], [13, 10]]) - expect(paragraphBufferRangeForRow(11)).toEqual([[10, 0], [13, 10]]) - expect(paragraphBufferRangeForRow(12)).toEqual([[10, 0], [13, 10]]) - expect(paragraphBufferRangeForRow(14)).toEqual([[14, 0], [14, 32]]) - expect(paragraphBufferRangeForRow(15)).toEqual([[15, 0], [15, 26]]) - expect(paragraphBufferRangeForRow(18)).toEqual([[17, 0], [19, 3]]) - - describe "getCursorAtScreenPosition(screenPosition)", -> - it "returns the cursor at the given screenPosition", -> - cursor1 = editor.addCursorAtScreenPosition([0, 2]) - cursor2 = editor.getCursorAtScreenPosition(cursor1.getScreenPosition()) - expect(cursor2).toBe cursor1 - - describe "::getCursorScreenPositions()", -> - it "returns the cursor positions in the order they were added", -> - editor.foldBufferRow(4) - cursor1 = editor.addCursorAtBufferPosition([8, 5]) - cursor2 = editor.addCursorAtBufferPosition([3, 5]) - expect(editor.getCursorScreenPositions()).toEqual [[0, 0], [5, 5], [3, 5]] - - describe "::getCursorsOrderedByBufferPosition()", -> - it "returns all cursors ordered by buffer positions", -> - originalCursor = editor.getLastCursor() - cursor1 = editor.addCursorAtBufferPosition([8, 5]) - cursor2 = editor.addCursorAtBufferPosition([4, 5]) - expect(editor.getCursorsOrderedByBufferPosition()).toEqual [originalCursor, cursor2, cursor1] - - describe "addCursorAtScreenPosition(screenPosition)", -> - describe "when a cursor already exists at the position", -> - it "returns the existing cursor", -> - cursor1 = editor.addCursorAtScreenPosition([0, 2]) - cursor2 = editor.addCursorAtScreenPosition([0, 2]) - expect(cursor2).toBe cursor1 - - describe "addCursorAtBufferPosition(bufferPosition)", -> - describe "when a cursor already exists at the position", -> - it "returns the existing cursor", -> - cursor1 = editor.addCursorAtBufferPosition([1, 4]) - cursor2 = editor.addCursorAtBufferPosition([1, 4]) - expect(cursor2.marker).toBe cursor1.marker - - describe '.getCursorScope()', -> - it 'returns the current scope', -> - descriptor = editor.getCursorScope() - expect(descriptor.scopes).toContain('source.js') - - describe "selection", -> - selection = null - - beforeEach -> - selection = editor.getLastSelection() - - describe ".getLastSelection()", -> - it "creates a new selection at (0, 0) if the last selection has been destroyed", -> - editor.getLastSelection().destroy() - expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) - - it "doesn't get stuck in a infinite loop when called from ::onDidAddCursor after the last selection has been destroyed (regression)", -> - callCount = 0 - editor.getLastSelection().destroy() - editor.onDidAddCursor (cursor) -> - callCount++ - editor.getLastSelection() - expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) - expect(callCount).toBe(1) - - describe ".getSelections()", -> - it "creates a new selection at (0, 0) if the last selection has been destroyed", -> - editor.getLastSelection().destroy() - expect(editor.getSelections()[0].getBufferRange()).toEqual([[0, 0], [0, 0]]) - - describe "when the selection range changes", -> - it "emits an event with the old range, new range, and the selection that moved", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - - editor.onDidChangeSelectionRange rangeChangedHandler = jasmine.createSpy() - editor.selectToBufferPosition([6, 2]) - - expect(rangeChangedHandler).toHaveBeenCalled() - eventObject = rangeChangedHandler.mostRecentCall.args[0] - - expect(eventObject.oldBufferRange).toEqual [[3, 0], [4, 5]] - expect(eventObject.oldScreenRange).toEqual [[3, 0], [4, 5]] - expect(eventObject.newBufferRange).toEqual [[3, 0], [6, 2]] - expect(eventObject.newScreenRange).toEqual [[3, 0], [6, 2]] - expect(eventObject.selection).toBe selection - - describe ".selectUp/Down/Left/Right()", -> - it "expands each selection to its cursor's new location", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]) - [selection1, selection2] = editor.getSelections() - - editor.selectRight() - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 14]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 22]] - - editor.selectLeft() - editor.selectLeft() - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 20]] - - editor.selectDown() - expect(selection1.getBufferRange()).toEqual [[0, 9], [1, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [4, 20]] - - editor.selectUp() - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 20]] - - it "merges selections when they intersect when moving down", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[1, 10], [1, 20]], [[2, 15], [3, 25]]]) - [selection1, selection2, selection3] = editor.getSelections() - - editor.selectDown() - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 9], [4, 25]]) - expect(selection1.isReversed()).toBeFalsy() - - it "merges selections when they intersect when moving up", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[1, 10], [1, 20]]], reversed: true) - [selection1, selection2] = editor.getSelections() - - editor.selectUp() - expect(editor.getSelections().length).toBe 1 - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 0], [1, 20]]) - expect(selection1.isReversed()).toBeTruthy() - - it "merges selections when they intersect when moving left", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[0, 13], [1, 20]]], reversed: true) - [selection1, selection2] = editor.getSelections() - - editor.selectLeft() - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 8], [1, 20]]) - expect(selection1.isReversed()).toBeTruthy() - - it "merges selections when they intersect when moving right", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 14]], [[0, 14], [1, 20]]]) - [selection1, selection2] = editor.getSelections() - - editor.selectRight() - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.getScreenRange()).toEqual([[0, 9], [1, 21]]) - expect(selection1.isReversed()).toBeFalsy() - - describe "when counts are passed into the selection functions", -> - it "expands each selection to its cursor's new location", -> - editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]) - [selection1, selection2] = editor.getSelections() - - editor.selectRight(2) - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 15]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 23]] - - editor.selectLeft(3) - expect(selection1.getBufferRange()).toEqual [[0, 9], [0, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [3, 20]] - - editor.selectDown(3) - expect(selection1.getBufferRange()).toEqual [[0, 9], [3, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [6, 20]] - - editor.selectUp(2) - expect(selection1.getBufferRange()).toEqual [[0, 9], [1, 12]] - expect(selection2.getBufferRange()).toEqual [[3, 16], [4, 20]] - - describe ".selectToBufferPosition(bufferPosition)", -> - it "expands the last selection to the given position", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - editor.addCursorAtBufferPosition([5, 6]) - editor.selectToBufferPosition([6, 2]) - - selections = editor.getSelections() - expect(selections.length).toBe 2 - [selection1, selection2] = selections - expect(selection1.getBufferRange()).toEqual [[3, 0], [4, 5]] - expect(selection2.getBufferRange()).toEqual [[5, 6], [6, 2]] - - describe ".selectToScreenPosition(screenPosition)", -> - it "expands the last selection to the given position", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - editor.addCursorAtScreenPosition([5, 6]) - editor.selectToScreenPosition([6, 2]) - - selections = editor.getSelections() - expect(selections.length).toBe 2 - [selection1, selection2] = selections - expect(selection1.getScreenRange()).toEqual [[3, 0], [4, 5]] - expect(selection2.getScreenRange()).toEqual [[5, 6], [6, 2]] - - describe "when selecting with an initial screen range", -> - it "switches the direction of the selection when selecting to positions before/after the start of the initial range", -> - editor.setCursorScreenPosition([5, 10]) - editor.selectWordsContainingCursors() - editor.selectToScreenPosition([3, 0]) - expect(editor.getLastSelection().isReversed()).toBe true - editor.selectToScreenPosition([9, 0]) - expect(editor.getLastSelection().isReversed()).toBe false - - describe ".selectToBeginningOfNextParagraph()", -> - it "selects from the cursor to first line of the next paragraph", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - editor.addCursorAtScreenPosition([5, 6]) - editor.selectToScreenPosition([6, 2]) - - editor.selectToBeginningOfNextParagraph() - - selections = editor.getSelections() - expect(selections.length).toBe 1 - expect(selections[0].getScreenRange()).toEqual [[3, 0], [10, 0]] - - describe ".selectToBeginningOfPreviousParagraph()", -> - it "selects from the cursor to the first line of the previous paragraph", -> - editor.setSelectedBufferRange([[3, 0], [4, 5]]) - editor.addCursorAtScreenPosition([5, 6]) - editor.selectToScreenPosition([6, 2]) - - editor.selectToBeginningOfPreviousParagraph() - - selections = editor.getSelections() - expect(selections.length).toBe 1 - expect(selections[0].getScreenRange()).toEqual [[0, 0], [5, 6]] - - it "merges selections if they intersect, maintaining the directionality of the last selection", -> - editor.setCursorScreenPosition([4, 10]) - editor.selectToScreenPosition([5, 27]) - editor.addCursorAtScreenPosition([3, 10]) - editor.selectToScreenPosition([6, 27]) - - selections = editor.getSelections() - expect(selections.length).toBe 1 - [selection1] = selections - expect(selection1.getScreenRange()).toEqual [[3, 10], [6, 27]] - expect(selection1.isReversed()).toBeFalsy() - - editor.addCursorAtScreenPosition([7, 4]) - editor.selectToScreenPosition([4, 11]) - - selections = editor.getSelections() - expect(selections.length).toBe 1 - [selection1] = selections - expect(selection1.getScreenRange()).toEqual [[3, 10], [7, 4]] - expect(selection1.isReversed()).toBeTruthy() - - describe ".selectToTop()", -> - it "selects text from cursor position to the top of the buffer", -> - editor.setCursorScreenPosition [11, 2] - editor.addCursorAtScreenPosition [10, 0] - editor.selectToTop() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - expect(editor.getLastSelection().getBufferRange()).toEqual [[0, 0], [11, 2]] - expect(editor.getLastSelection().isReversed()).toBeTruthy() - - describe ".selectToBottom()", -> - it "selects text from cursor position to the bottom of the buffer", -> - editor.setCursorScreenPosition [10, 0] - editor.addCursorAtScreenPosition [9, 3] - editor.selectToBottom() - expect(editor.getCursors().length).toBe 1 - expect(editor.getCursorBufferPosition()).toEqual [12, 2] - expect(editor.getLastSelection().getBufferRange()).toEqual [[9, 3], [12, 2]] - expect(editor.getLastSelection().isReversed()).toBeFalsy() - - describe ".selectAll()", -> - it "selects the entire buffer", -> - editor.selectAll() - expect(editor.getLastSelection().getBufferRange()).toEqual buffer.getRange() - - describe ".selectToBeginningOfLine()", -> - it "selects text from cursor position to beginning of line", -> - editor.setCursorScreenPosition [12, 2] - editor.addCursorAtScreenPosition [11, 3] - - editor.selectToBeginningOfLine() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [12, 0] - expect(cursor2.getBufferPosition()).toEqual [11, 0] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[12, 0], [12, 2]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[11, 0], [11, 3]] - expect(selection2.isReversed()).toBeTruthy() - - describe ".selectToEndOfLine()", -> - it "selects text from cursor position to end of line", -> - editor.setCursorScreenPosition [12, 0] - editor.addCursorAtScreenPosition [11, 3] - - editor.selectToEndOfLine() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [12, 2] - expect(cursor2.getBufferPosition()).toEqual [11, 44] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[12, 0], [12, 2]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[11, 3], [11, 44]] - expect(selection2.isReversed()).toBeFalsy() - - describe ".selectLinesContainingCursors()", -> - it "selects to the entire line (including newlines) at given row", -> - editor.setCursorScreenPosition([1, 2]) - editor.selectLinesContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [2, 0]] - expect(editor.getSelectedText()).toBe " var sort = function(items) {\n" - - editor.setCursorScreenPosition([12, 2]) - editor.selectLinesContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[12, 0], [12, 2]] - - editor.setCursorBufferPosition([0, 2]) - editor.selectLinesContainingCursors() - editor.selectLinesContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [2, 0]] - - describe "when the selection spans multiple row", -> - it "selects from the beginning of the first line to the last line", -> - selection = editor.getLastSelection() - selection.setBufferRange [[1, 10], [3, 20]] - editor.selectLinesContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [4, 0]] - - describe ".selectToBeginningOfWord()", -> - it "selects text from cursor position to beginning of word", -> - editor.setCursorScreenPosition [0, 13] - editor.addCursorAtScreenPosition [3, 49] - - editor.selectToBeginningOfWord() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 4] - expect(cursor2.getBufferPosition()).toEqual [3, 47] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 4], [0, 13]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[3, 47], [3, 49]] - expect(selection2.isReversed()).toBeTruthy() - - describe ".selectToEndOfWord()", -> - it "selects text from cursor position to end of word", -> - editor.setCursorScreenPosition [0, 4] - editor.addCursorAtScreenPosition [3, 48] - - editor.selectToEndOfWord() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 13] - expect(cursor2.getBufferPosition()).toEqual [3, 50] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 4], [0, 13]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[3, 48], [3, 50]] - expect(selection2.isReversed()).toBeFalsy() - - describe ".selectToBeginningOfNextWord()", -> - it "selects text from cursor position to beginning of next word", -> - editor.setCursorScreenPosition [0, 4] - editor.addCursorAtScreenPosition [3, 48] - - editor.selectToBeginningOfNextWord() - - expect(editor.getCursors().length).toBe 2 - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 14] - expect(cursor2.getBufferPosition()).toEqual [3, 51] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 4], [0, 14]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[3, 48], [3, 51]] - expect(selection2.isReversed()).toBeFalsy() - - describe ".selectToPreviousWordBoundary()", -> - it "select to the previous word boundary", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [2, 0] - editor.addCursorAtBufferPosition [3, 4] - editor.addCursorAtBufferPosition [3, 14] - - editor.selectToPreviousWordBoundary() - - expect(editor.getSelections().length).toBe 4 - [selection1, selection2, selection3, selection4] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 8], [0, 4]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[2, 0], [1, 30]] - expect(selection2.isReversed()).toBeTruthy() - expect(selection3.getBufferRange()).toEqual [[3, 4], [3, 0]] - expect(selection3.isReversed()).toBeTruthy() - expect(selection4.getBufferRange()).toEqual [[3, 14], [3, 13]] - expect(selection4.isReversed()).toBeTruthy() - - describe ".selectToNextWordBoundary()", -> - it "select to the next word boundary", -> - editor.setCursorBufferPosition [0, 8] - editor.addCursorAtBufferPosition [2, 40] - editor.addCursorAtBufferPosition [4, 0] - editor.addCursorAtBufferPosition [3, 30] - - editor.selectToNextWordBoundary() - - expect(editor.getSelections().length).toBe 4 - [selection1, selection2, selection3, selection4] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 8], [0, 13]] - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual [[2, 40], [3, 0]] - expect(selection2.isReversed()).toBeFalsy() - expect(selection3.getBufferRange()).toEqual [[4, 0], [4, 4]] - expect(selection3.isReversed()).toBeFalsy() - expect(selection4.getBufferRange()).toEqual [[3, 30], [3, 31]] - expect(selection4.isReversed()).toBeFalsy() - - describe ".selectToPreviousSubwordBoundary", -> - it "selects subwords", -> - editor.setText("") - editor.insertText("_word\n") - editor.insertText(" getPreviousWord\n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 5]) - editor.addCursorAtBufferPosition([1, 7]) - editor.addCursorAtBufferPosition([2, 5]) - editor.addCursorAtBufferPosition([3, 3]) - [selection1, selection2, selection3, selection4] = editor.getSelections() - - editor.selectToPreviousSubwordBoundary() - expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 5]]) - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual([[1, 4], [1, 7]]) - expect(selection2.isReversed()).toBeTruthy() - expect(selection3.getBufferRange()).toEqual([[2, 3], [2, 5]]) - expect(selection3.isReversed()).toBeTruthy() - expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) - expect(selection4.isReversed()).toBeTruthy() - - describe ".selectToNextSubwordBoundary", -> - it "selects subwords", -> - editor.setText("") - editor.insertText("word_\n") - editor.insertText("getPreviousWord\n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 1]) - editor.addCursorAtBufferPosition([1, 7]) - editor.addCursorAtBufferPosition([2, 2]) - editor.addCursorAtBufferPosition([3, 1]) - [selection1, selection2, selection3, selection4] = editor.getSelections() - - editor.selectToNextSubwordBoundary() - expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 4]]) - expect(selection1.isReversed()).toBeFalsy() - expect(selection2.getBufferRange()).toEqual([[1, 7], [1, 11]]) - expect(selection2.isReversed()).toBeFalsy() - expect(selection3.getBufferRange()).toEqual([[2, 2], [2, 5]]) - expect(selection3.isReversed()).toBeFalsy() - expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) - expect(selection4.isReversed()).toBeFalsy() - - describe ".deleteToBeginningOfSubword", -> - it "deletes subwords", -> - editor.setText("") - editor.insertText("_word\n") - editor.insertText(" getPreviousWord\n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 5]) - editor.addCursorAtBufferPosition([1, 7]) - editor.addCursorAtBufferPosition([2, 5]) - editor.addCursorAtBufferPosition([3, 3]) - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.deleteToBeginningOfSubword() - expect(buffer.lineForRow(0)).toBe('_') - expect(buffer.lineForRow(1)).toBe(' getviousWord') - expect(buffer.lineForRow(2)).toBe('e, ') - expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0, 1]) - expect(cursor2.getBufferPosition()).toEqual([1, 4]) - expect(cursor3.getBufferPosition()).toEqual([2, 3]) - expect(cursor4.getBufferPosition()).toEqual([3, 1]) - - editor.deleteToBeginningOfSubword() - expect(buffer.lineForRow(0)).toBe('') - expect(buffer.lineForRow(1)).toBe(' viousWord') - expect(buffer.lineForRow(2)).toBe('e ') - expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 1]) - expect(cursor3.getBufferPosition()).toEqual([2, 1]) - expect(cursor4.getBufferPosition()).toEqual([3, 0]) - - editor.deleteToBeginningOfSubword() - expect(buffer.lineForRow(0)).toBe('') - expect(buffer.lineForRow(1)).toBe('viousWord') - expect(buffer.lineForRow(2)).toBe(' ') - expect(buffer.lineForRow(3)).toBe('') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 0]) - expect(cursor3.getBufferPosition()).toEqual([2, 0]) - expect(cursor4.getBufferPosition()).toEqual([2, 1]) - - describe ".deleteToEndOfSubword", -> - it "deletes subwords", -> - editor.setText("") - editor.insertText("word_\n") - editor.insertText("getPreviousWord \n") - editor.insertText("e, => \n") - editor.insertText(" 88 \n") - editor.setCursorBufferPosition([0, 0]) - editor.addCursorAtBufferPosition([1, 0]) - editor.addCursorAtBufferPosition([2, 2]) - editor.addCursorAtBufferPosition([3, 0]) - [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() - - editor.deleteToEndOfSubword() - expect(buffer.lineForRow(0)).toBe('_') - expect(buffer.lineForRow(1)).toBe('PreviousWord ') - expect(buffer.lineForRow(2)).toBe('e, ') - expect(buffer.lineForRow(3)).toBe('88 ') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 0]) - expect(cursor3.getBufferPosition()).toEqual([2, 2]) - expect(cursor4.getBufferPosition()).toEqual([3, 0]) - - editor.deleteToEndOfSubword() - expect(buffer.lineForRow(0)).toBe('') - expect(buffer.lineForRow(1)).toBe('Word ') - expect(buffer.lineForRow(2)).toBe('e,') - expect(buffer.lineForRow(3)).toBe(' ') - expect(cursor1.getBufferPosition()).toEqual([0, 0]) - expect(cursor2.getBufferPosition()).toEqual([1, 0]) - expect(cursor3.getBufferPosition()).toEqual([2, 2]) - expect(cursor4.getBufferPosition()).toEqual([3, 0]) - - describe ".selectWordsContainingCursors()", -> - describe "when the cursor is inside a word", -> - it "selects the entire word", -> - editor.setCursorScreenPosition([0, 8]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedText()).toBe 'quicksort' - - describe "when the cursor is between two words", -> - it "selects the word the cursor is on", -> - editor.setCursorScreenPosition([0, 4]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedText()).toBe 'quicksort' - - editor.setCursorScreenPosition([0, 3]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedText()).toBe 'var' - - describe "when the cursor is inside a region of whitespace", -> - it "selects the whitespace region", -> - editor.setCursorScreenPosition([5, 2]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] - - editor.setCursorScreenPosition([5, 0]) - editor.selectWordsContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[5, 0], [5, 6]] - - describe "when the cursor is at the end of the text", -> - it "select the previous word", -> - editor.buffer.append 'word' - editor.moveToBottom() - editor.selectWordsContainingCursors() - expect(editor.getSelectedBufferRange()).toEqual [[12, 2], [12, 6]] - - it "selects words based on the non-word characters configured at the cursor's current scope", -> - editor.setText("one-one; 'two-two'; three-three") - - editor.setCursorBufferPosition([0, 1]) - editor.addCursorAtBufferPosition([0, 12]) - - scopeDescriptors = editor.getCursors().map (c) -> c.getScopeDescriptor() - expect(scopeDescriptors[0].getScopesArray()).toEqual(['source.js']) - expect(scopeDescriptors[1].getScopesArray()).toEqual(['source.js', 'string.quoted.single.js']) - - editor.setScopedSettingsDelegate({ - getNonWordCharacters: (scopes) -> - result = '/\()"\':,.;<>~!@#$%^&*|+=[]{}`?' - if (scopes.some (scope) -> scope.startsWith('string')) - result - else - result + '-' - }) - - editor.selectWordsContainingCursors() - - expect(editor.getSelections()[0].getText()).toBe('one') - expect(editor.getSelections()[1].getText()).toBe('two-two') - - describe ".selectToFirstCharacterOfLine()", -> - it "moves to the first character of the current line or the beginning of the line if it's already on the first character", -> - editor.setCursorScreenPosition [0, 5] - editor.addCursorAtScreenPosition [1, 7] - - editor.selectToFirstCharacterOfLine() - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor2.getBufferPosition()).toEqual [1, 2] - - expect(editor.getSelections().length).toBe 2 - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 0], [0, 5]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[1, 2], [1, 7]] - expect(selection2.isReversed()).toBeTruthy() - - editor.selectToFirstCharacterOfLine() - [selection1, selection2] = editor.getSelections() - expect(selection1.getBufferRange()).toEqual [[0, 0], [0, 5]] - expect(selection1.isReversed()).toBeTruthy() - expect(selection2.getBufferRange()).toEqual [[1, 0], [1, 7]] - expect(selection2.isReversed()).toBeTruthy() - - describe ".setSelectedBufferRanges(ranges)", -> - it "clears existing selections and creates selections for each of the given ranges", -> - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [3, 3]], [[4, 4], [5, 5]]] - - editor.setSelectedBufferRanges([[[5, 5], [6, 6]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[5, 5], [6, 6]]] - - it "merges intersecting selections", -> - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] - - it "does not merge non-empty adjacent selections", -> - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 3], [5, 5]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [3, 3]], [[3, 3], [5, 5]]] - - it "recycles existing selection instances", -> - selection = editor.getLastSelection() - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) - - [selection1, selection2] = editor.getSelections() - expect(selection1).toBe selection - expect(selection1.getBufferRange()).toEqual [[2, 2], [3, 3]] - - describe "when the 'preserveFolds' option is false (the default)", -> - it "removes folds that contain one or both of the selection's end points", -> - editor.setSelectedBufferRange([[0, 0], [0, 0]]) - editor.foldBufferRowRange(1, 4) - editor.foldBufferRowRange(2, 3) - editor.foldBufferRowRange(6, 8) - editor.foldBufferRowRange(10, 11) - - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) - expect(editor.isFoldedAtScreenRow(1)).toBeFalsy() - expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() - expect(editor.isFoldedAtScreenRow(6)).toBeFalsy() - expect(editor.isFoldedAtScreenRow(10)).toBeTruthy() - - editor.setSelectedBufferRange([[10, 0], [12, 0]]) - expect(editor.isFoldedAtScreenRow(10)).toBeTruthy() - - describe "when the 'preserveFolds' option is true", -> - it "does not remove folds that contain the selections", -> - editor.setSelectedBufferRange([[0, 0], [0, 0]]) - editor.foldBufferRowRange(1, 4) - editor.foldBufferRowRange(6, 8) - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], preserveFolds: true) - expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - - describe ".setSelectedScreenRanges(ranges)", -> - beforeEach -> - editor.foldBufferRow(4) - - it "clears existing selections and creates selections for each of the given ranges", -> - editor.setSelectedScreenRanges([[[3, 4], [3, 7]], [[5, 4], [5, 7]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[3, 4], [3, 7]], [[8, 4], [8, 7]]] - - editor.setSelectedScreenRanges([[[6, 2], [6, 4]]]) - expect(editor.getSelectedScreenRanges()).toEqual [[[6, 2], [6, 4]]] - - it "merges intersecting selections and unfolds the fold which contain them", -> - editor.foldBufferRow(0) - - # Use buffer ranges because only the first line is on screen - editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]] - - it "recycles existing selection instances", -> - selection = editor.getLastSelection() - editor.setSelectedScreenRanges([[[2, 2], [3, 4]], [[4, 4], [5, 5]]]) - - [selection1, selection2] = editor.getSelections() - expect(selection1).toBe selection - expect(selection1.getScreenRange()).toEqual [[2, 2], [3, 4]] - - describe ".selectMarker(marker)", -> - describe "if the marker is valid", -> - it "selects the marker's range and returns the selected range", -> - marker = editor.markBufferRange([[0, 1], [3, 3]]) - expect(editor.selectMarker(marker)).toEqual [[0, 1], [3, 3]] - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [3, 3]] - - describe "if the marker is invalid", -> - it "does not change the selection and returns a falsy value", -> - marker = editor.markBufferRange([[0, 1], [3, 3]]) - marker.destroy() - expect(editor.selectMarker(marker)).toBeFalsy() - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 0]] - - describe ".addSelectionForBufferRange(bufferRange)", -> - it "adds a selection for the specified buffer range", -> - editor.addSelectionForBufferRange([[3, 4], [5, 6]]) - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 0]], [[3, 4], [5, 6]]] - - describe ".addSelectionBelow()", -> - describe "when the selection is non-empty", -> - it "selects the same region of the line below current selections if possible", -> - editor.setSelectedBufferRange([[3, 16], [3, 21]]) - editor.addSelectionForBufferRange([[3, 25], [3, 34]]) - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 16], [3, 21]] - [[3, 25], [3, 34]] - [[4, 16], [4, 21]] - [[4, 25], [4, 29]] - ] - - it "skips lines that are too short to create a non-empty selection", -> - editor.setSelectedBufferRange([[3, 31], [3, 38]]) - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 31], [3, 38]] - [[6, 31], [6, 38]] - ] - - it "honors the original selection's range (goal range) when adding across shorter lines", -> - editor.setSelectedBufferRange([[3, 22], [3, 38]]) - editor.addSelectionBelow() - editor.addSelectionBelow() - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 38]] - [[4, 22], [4, 29]] - [[5, 22], [5, 30]] - [[6, 22], [6, 38]] - ] - - it "clears selection goal ranges when the selection changes", -> - editor.setSelectedBufferRange([[3, 22], [3, 38]]) - editor.addSelectionBelow() - editor.selectLeft() - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 37]] - [[4, 22], [4, 29]] - [[5, 22], [5, 28]] - ] - - # goal range from previous add selection is honored next time - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 22], [3, 37]] - [[4, 22], [4, 29]] - [[5, 22], [5, 30]] # select to end of line 5 because line 4's goal range was reset by line 3 previously - [[6, 22], [6, 28]] - ] - - it "can add selections to soft-wrapped line segments", -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(40) - editor.setDefaultCharWidth(1) - - editor.setSelectedScreenRange([[3, 10], [3, 15]]) - editor.addSelectionBelow() - expect(editor.getSelectedScreenRanges()).toEqual [ - [[3, 10], [3, 15]] - [[4, 10], [4, 15]] - ] - - it "takes atomic tokens into account", -> - waitsForPromise -> - atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o - - runs -> - editor.setSelectedBufferRange([[2, 1], [2, 3]]) - editor.addSelectionBelow() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[2, 1], [2, 3]] - [[3, 1], [3, 2]] - ] - - describe "when the selection is empty", -> - describe "when lines are soft-wrapped", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(40) - - it "skips soft-wrap indentation tokens", -> - editor.setCursorScreenPosition([3, 0]) - editor.addSelectionBelow() - - expect(editor.getSelectedScreenRanges()).toEqual [ - [[3, 0], [3, 0]] - [[4, 4], [4, 4]] - ] - - it "does not skip them if they're shorter than the current column", -> - editor.setCursorScreenPosition([3, 37]) - editor.addSelectionBelow() - - expect(editor.getSelectedScreenRanges()).toEqual [ - [[3, 37], [3, 37]] - [[4, 26], [4, 26]] - ] - - it "does not skip lines that are shorter than the current column", -> - editor.setCursorBufferPosition([3, 36]) - editor.addSelectionBelow() - editor.addSelectionBelow() - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 36], [3, 36]] - [[4, 29], [4, 29]] - [[5, 30], [5, 30]] - [[6, 36], [6, 36]] - ] - - it "skips empty lines when the column is non-zero", -> - editor.setCursorBufferPosition([9, 4]) - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[9, 4], [9, 4]] - [[11, 4], [11, 4]] - ] - - it "does not skip empty lines when the column is zero", -> - editor.setCursorBufferPosition([9, 0]) - editor.addSelectionBelow() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[9, 0], [9, 0]] - [[10, 0], [10, 0]] - ] - - describe ".addSelectionAbove()", -> - describe "when the selection is non-empty", -> - it "selects the same region of the line above current selections if possible", -> - editor.setSelectedBufferRange([[3, 16], [3, 21]]) - editor.addSelectionForBufferRange([[3, 37], [3, 44]]) - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 16], [3, 21]] - [[3, 37], [3, 44]] - [[2, 16], [2, 21]] - [[2, 37], [2, 40]] - ] - - it "skips lines that are too short to create a non-empty selection", -> - editor.setSelectedBufferRange([[6, 31], [6, 38]]) - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[6, 31], [6, 38]] - [[3, 31], [3, 38]] - ] - - it "honors the original selection's range (goal range) when adding across shorter lines", -> - editor.setSelectedBufferRange([[6, 22], [6, 38]]) - editor.addSelectionAbove() - editor.addSelectionAbove() - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[6, 22], [6, 38]] - [[5, 22], [5, 30]] - [[4, 22], [4, 29]] - [[3, 22], [3, 38]] - ] - - it "can add selections to soft-wrapped line segments", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(40) - - editor.setSelectedScreenRange([[4, 10], [4, 15]]) - editor.addSelectionAbove() - expect(editor.getSelectedScreenRanges()).toEqual [ - [[4, 10], [4, 15]] - [[3, 10], [3, 15]] - ] - - it "takes atomic tokens into account", -> - waitsForPromise -> - atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', autoIndent: false).then (o) -> editor = o - - runs -> - editor.setSelectedBufferRange([[3, 1], [3, 2]]) - editor.addSelectionAbove() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[3, 1], [3, 2]] - [[2, 1], [2, 3]] - ] - - describe "when the selection is empty", -> - describe "when lines are soft-wrapped", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(40) - - it "skips soft-wrap indentation tokens", -> - editor.setCursorScreenPosition([5, 0]) - editor.addSelectionAbove() - - expect(editor.getSelectedScreenRanges()).toEqual [ - [[5, 0], [5, 0]] - [[4, 4], [4, 4]] - ] - - it "does not skip them if they're shorter than the current column", -> - editor.setCursorScreenPosition([5, 29]) - editor.addSelectionAbove() - - expect(editor.getSelectedScreenRanges()).toEqual [ - [[5, 29], [5, 29]] - [[4, 26], [4, 26]] - ] - - it "does not skip lines that are shorter than the current column", -> - editor.setCursorBufferPosition([6, 36]) - editor.addSelectionAbove() - editor.addSelectionAbove() - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[6, 36], [6, 36]] - [[5, 30], [5, 30]] - [[4, 29], [4, 29]] - [[3, 36], [3, 36]] - ] - - it "skips empty lines when the column is non-zero", -> - editor.setCursorBufferPosition([11, 4]) - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[11, 4], [11, 4]] - [[9, 4], [9, 4]] - ] - - it "does not skip empty lines when the column is zero", -> - editor.setCursorBufferPosition([10, 0]) - editor.addSelectionAbove() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[10, 0], [10, 0]] - [[9, 0], [9, 0]] - ] - - describe ".splitSelectionsIntoLines()", -> - it "splits all multi-line selections into one selection per line", -> - editor.setSelectedBufferRange([[0, 3], [2, 4]]) - editor.splitSelectionsIntoLines() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 3], [0, 29]] - [[1, 0], [1, 30]] - [[2, 0], [2, 4]] - ] - - editor.setSelectedBufferRange([[0, 3], [1, 10]]) - editor.splitSelectionsIntoLines() - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 3], [0, 29]] - [[1, 0], [1, 10]] - ] - - editor.setSelectedBufferRange([[0, 0], [0, 3]]) - editor.splitSelectionsIntoLines() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]]] - - describe "::consolidateSelections()", -> - makeMultipleSelections = -> - selection.setBufferRange [[3, 16], [3, 21]] - selection2 = editor.addSelectionForBufferRange([[3, 25], [3, 34]]) - selection3 = editor.addSelectionForBufferRange([[8, 4], [8, 10]]) - selection4 = editor.addSelectionForBufferRange([[1, 6], [1, 10]]) - expect(editor.getSelections()).toEqual [selection, selection2, selection3, selection4] - [selection, selection2, selection3, selection4] - - it "destroys all selections but the oldest selection and autoscrolls to it, returning true if any selections were destroyed", -> - [selection1] = makeMultipleSelections() - - autoscrollEvents = [] - editor.onDidRequestAutoscroll (event) -> autoscrollEvents.push(event) - - expect(editor.consolidateSelections()).toBeTruthy() - expect(editor.getSelections()).toEqual [selection1] - expect(selection1.isEmpty()).toBeFalsy() - expect(editor.consolidateSelections()).toBeFalsy() - expect(editor.getSelections()).toEqual [selection1] - - expect(autoscrollEvents).toEqual([ - {screenRange: selection1.getScreenRange(), options: {center: true, reversed: false}} - ]) - - describe "when the cursor is moved while there is a selection", -> - makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]] - - it "clears the selection", -> - makeSelection() - editor.moveDown() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editor.moveUp() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editor.moveLeft() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editor.moveRight() - expect(selection.isEmpty()).toBeTruthy() - - makeSelection() - editor.setCursorScreenPosition([3, 3]) - expect(selection.isEmpty()).toBeTruthy() - - it "does not share selections between different edit sessions for the same buffer", -> - editor2 = null - waitsForPromise -> - atom.workspace.getActivePane().splitRight() - atom.workspace.open(editor.getPath()).then (o) -> editor2 = o - - runs -> - expect(editor2.getText()).toBe(editor.getText()) - editor.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) - editor2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]]) - expect(editor2.getSelectedBufferRanges()).not.toEqual editor.getSelectedBufferRanges() - - describe "buffer manipulation", -> - describe ".moveLineUp", -> - it "moves the line under the cursor up", -> - editor.setCursorBufferPosition([1, 0]) - editor.moveLineUp() - expect(editor.getTextInBufferRange([[0, 0], [0, 30]])).toBe " var sort = function(items) {" - expect(editor.indentationForBufferRow(0)).toBe 1 - expect(editor.indentationForBufferRow(1)).toBe 0 - - it "updates the line's indentation when the the autoIndent setting is true", -> - editor.update({autoIndent: true}) - editor.setCursorBufferPosition([1, 0]) - editor.moveLineUp() - expect(editor.indentationForBufferRow(0)).toBe 0 - expect(editor.indentationForBufferRow(1)).toBe 0 - - describe "when there is a single selection", -> - describe "when the selection spans a single line", -> - describe "when there is no fold in the preceeding row", -> - it "moves the line to the preceding row", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - editor.setSelectedBufferRange([[3, 2], [3, 9]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [2, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - - describe "when the cursor is at the beginning of a fold", -> - it "moves the line to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true) - expect(editor.getSelectedBufferRange()).toEqual [[4, 2], [4, 9]] - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [3, 9]] - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - - - describe "when the preceding row consists of folded code", -> - it "moves the line above the folded row and perseveres the correct folds", -> - expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(9)).toBe " };" - - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRange([[8, 0], [8, 4]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [4, 4]] - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when the selection spans multiple lines", -> - it "moves the lines spanned by the selection to the preceding row", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.setSelectedBufferRange([[3, 2], [4, 9]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(4)).toBe " if (items.length <= 1) return items;" - - describe "when the selection's end intersects a fold", -> - it "moves the lines to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(7)).toBe " if (items.length <= 1) return items;" - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - - describe "when the selection's start intersects a fold", -> - it "moves the lines to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [7, 9]] - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(7)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(8)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - - describe "when the selection spans multiple lines, but ends at column 0", -> - it "does not move the last line of the selection", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.setSelectedBufferRange([[3, 2], [4, 0]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 0]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - describe "when the preceeding row is a folded row", -> - it "moves the lines spanned by the selection to the preceeding row, but preserves the folded code", -> - expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(9)).toBe " };" - - editor.foldBufferRowRange(4, 7) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRange([[8, 0], [9, 2]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[4, 0], [5, 2]] - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(5)).toBe " };" - expect(editor.lineTextForBufferRow(6)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(5)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() - - describe "when there are multiple selections", -> - describe "when all the selections span different lines", -> - describe "when there is no folds", -> - it "moves all lines that are spanned by a selection to the preceding row", -> - editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [0, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]] - expect(editor.lineTextForBufferRow(0)).toBe " var sort = function(items) {" - expect(editor.lineTextForBufferRow(1)).toBe "var quicksort = function () {" - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " current = items.shift();" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - - describe "when one selection intersects a fold", -> - it "moves the lines to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRanges([ - [[2, 2], [2, 9]], - [[4, 2], [4, 9]] - ], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual([ - [[1, 2], [1, 9]], - [[3, 2], [3, 9]] - ]) - - expect(editor.lineTextForBufferRow(1)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(2)).toBe " var sort = function(items) {" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - describe "when there is a fold", -> - it "moves all lines that spanned by a selection to preceding row, preserving all folds", -> - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRanges([[[8, 0], [8, 3]], [[11, 0], [11, 5]]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[4, 0], [4, 3]], [[10, 0], [10, 5]]] - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(10)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe 'when there are many folds', -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample-with-many-folds.js', autoIndent: false).then (o) -> editor = o - - describe 'and many selections intersects folded rows', -> - it 'moves and preserves all the folds', -> - editor.foldBufferRowRange(2, 4) - editor.foldBufferRowRange(7, 9) - - editor.setSelectedBufferRanges([ - [[1, 0], [5, 4]], - [[7, 0], [7, 4]] - ], preserveFolds: true) - - editor.moveLineUp() - - expect(editor.lineTextForBufferRow(1)).toEqual "function f3() {" - expect(editor.lineTextForBufferRow(4)).toEqual "6;" - expect(editor.lineTextForBufferRow(5)).toEqual "1;" - expect(editor.lineTextForBufferRow(6)).toEqual "function f8() {" - expect(editor.lineTextForBufferRow(9)).toEqual "7;" - - expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() - - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when some of the selections span the same lines", -> - it "moves lines that contain multiple selections correctly", -> - editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [2, 9]], [[2, 12], [2, 13]]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when one of the selections spans line 0", -> - it "doesn't move any lines, since line 0 can't move", -> - editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]) - - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]] - expect(buffer.isModified()).toBe false - - describe "when one of the selections spans the last line, and it is empty", -> - it "doesn't move any lines, since the last line can't move", -> - buffer.append('\n') - editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]]) - - editor.moveLineUp() - - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]] - - describe ".moveLineDown", -> - it "moves the line under the cursor down", -> - editor.setCursorBufferPosition([0, 0]) - editor.moveLineDown() - expect(editor.getTextInBufferRange([[1, 0], [1, 31]])).toBe "var quicksort = function () {" - expect(editor.indentationForBufferRow(0)).toBe 1 - expect(editor.indentationForBufferRow(1)).toBe 0 - - it "updates the line's indentation when the editor.autoIndent setting is true", -> - editor.update({autoIndent: true}) - editor.setCursorBufferPosition([0, 0]) - editor.moveLineDown() - expect(editor.indentationForBufferRow(0)).toBe 1 - expect(editor.indentationForBufferRow(1)).toBe 2 - - describe "when there is a single selection", -> - describe "when the selection spans a single line", -> - describe "when there is no fold in the following row", -> - it "moves the line to the following row", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - editor.setSelectedBufferRange([[2, 2], [2, 9]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [3, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - - describe "when the cursor is at the beginning of a fold", -> - it "moves the line to the following row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[5, 2], [5, 9]] - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when the following row is a folded row", -> - it "moves the line below the folded row and preserves the fold", -> - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRange([[3, 0], [3, 4]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[7, 0], [7, 4]] - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - - - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when the selection spans multiple lines", -> - it "moves the lines spanned by the selection to the following row", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.setSelectedBufferRange([[2, 2], [3, 9]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [4, 9]] - expect(editor.lineTextForBufferRow(2)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when the selection spans multiple lines, but ends at column 0", -> - it "does not move the last line of the selection", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.setSelectedBufferRange([[2, 2], [3, 0]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [4, 0]] - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - describe "when the selection's end intersects a fold", -> - it "moves the lines to the following row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[4, 2], [5, 9]] - expect(editor.lineTextForBufferRow(3)).toBe " return sort(left).concat(pivot).concat(sort(right));" - expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - - expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when the selection's start intersects a fold", -> - it "moves the lines to the following row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[5, 2], [9, 9]] - expect(editor.lineTextForBufferRow(4)).toBe " };" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(9)).toBe " return sort(left).concat(pivot).concat(sort(right));" - - expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() - - describe "when the following row is a folded row", -> - it "moves the lines spanned by the selection to the following row, but preserves the folded code", -> - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - editor.foldBufferRowRange(4, 7) - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRange([[2, 0], [3, 2]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRange()).toEqual [[6, 0], [7, 2]] - expect(editor.lineTextForBufferRow(2)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() - expect(editor.lineTextForBufferRow(6)).toBe " if (items.length <= 1) return items;" - - describe "when the last line of selection does not end with a valid line ending", -> - it "appends line ending to last line and moves the lines spanned by the selection to the preceeding row", -> - expect(editor.lineTextForBufferRow(9)).toBe " };" - expect(editor.lineTextForBufferRow(10)).toBe "" - expect(editor.lineTextForBufferRow(11)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.lineTextForBufferRow(12)).toBe "};" - - editor.setSelectedBufferRange([[10, 0], [12, 2]]) - editor.moveLineUp() - - expect(editor.getSelectedBufferRange()).toEqual [[9, 0], [11, 2]] - expect(editor.lineTextForBufferRow(9)).toBe "" - expect(editor.lineTextForBufferRow(10)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.lineTextForBufferRow(11)).toBe "};" - expect(editor.lineTextForBufferRow(12)).toBe " };" - - describe "when there are multiple selections", -> - describe "when all the selections span different lines", -> - describe "when there is no folds", -> - it "moves all lines that are spanned by a selection to the following row", -> - editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual [[[6, 2], [6, 9]], [[4, 2], [4, 9]], [[2, 2], [2, 9]]] - expect(editor.lineTextForBufferRow(1)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(2)).toBe " var sort = function(items) {" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(5)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(editor.lineTextForBufferRow(6)).toBe " current = items.shift();" - - describe 'when there are many folds', -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample-with-many-folds.js', autoIndent: false).then (o) -> editor = o - - describe 'and many selections intersects folded rows', -> - it 'moves and preserves all the folds', -> - editor.foldBufferRowRange(2, 4) - editor.foldBufferRowRange(7, 9) - - editor.setSelectedBufferRanges([ - [[2, 0], [2, 4]], - [[6, 0], [10, 4]] - ], preserveFolds: true) - - editor.moveLineDown() - - expect(editor.lineTextForBufferRow(2)).toEqual "6;" - expect(editor.lineTextForBufferRow(3)).toEqual "function f3() {" - expect(editor.lineTextForBufferRow(6)).toEqual "12;" - expect(editor.lineTextForBufferRow(7)).toEqual "7;" - expect(editor.lineTextForBufferRow(8)).toEqual "function f8() {" - expect(editor.lineTextForBufferRow(11)).toEqual "11;" - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(10)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(11)).toBeFalsy() - - describe "when there is a fold below one of the selected row", -> - it "moves all lines spanned by a selection to the following row, preserving the fold", -> - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRanges([[[1, 2], [1, 6]], [[3, 0], [3, 4]], [[8, 0], [8, 3]]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual [[[9, 0], [9, 3]], [[7, 0], [7, 4]], [[2, 2], [2, 6]]] - expect(editor.lineTextForBufferRow(2)).toBe " var sort = function(items) {" - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(9)).toBe " return sort(left).concat(pivot).concat(sort(right));" - - describe "when there is a fold below a group of multiple selections without any lines with no selection in-between", -> - it "moves all the lines below the fold, preserving the fold", -> - editor.foldBufferRowRange(4, 7) - - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - - editor.setSelectedBufferRanges([[[2, 2], [2, 6]], [[3, 0], [3, 4]]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual [[[7, 0], [7, 4]], [[6, 2], [6, 6]]] - expect(editor.lineTextForBufferRow(2)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() - expect(editor.lineTextForBufferRow(6)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(7)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - describe "when one selection intersects a fold", -> - it "moves the lines to the previous row without breaking the fold", -> - expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {" - - editor.foldBufferRowRange(4, 7) - editor.setSelectedBufferRanges([ - [[2, 2], [2, 9]], - [[4, 2], [4, 9]] - ], preserveFolds: true) - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual([ - [[5, 2], [5, 9]] - [[3, 2], [3, 9]], - ]) - - expect(editor.lineTextForBufferRow(2)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " if (items.length <= 1) return items;" - expect(editor.lineTextForBufferRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));" - - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(9)).toBe " };" - - expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() - - describe "when some of the selections span the same lines", -> - it "moves lines that contain multiple selections correctly", -> - editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]]) - editor.moveLineDown() - - expect(editor.getSelectedBufferRanges()).toEqual [[[4, 12], [4, 13]], [[4, 2], [4, 9]]] - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - - describe "when the selections are above a wrapped line", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(80) - editor.setText(""" - 1 - 2 - Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. - 3 - 4 - """) - - it 'moves the lines past the soft wrapped line', -> - editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 0]]]) - - editor.moveLineDown() - - expect(editor.lineTextForBufferRow(0)).not.toBe "2" - expect(editor.lineTextForBufferRow(1)).toBe "1" - expect(editor.lineTextForBufferRow(2)).toBe "2" - - describe "when the line is the last buffer row", -> - it "doesn't move it", -> - editor.setText("abc\ndef") - editor.setCursorBufferPosition([1, 0]) - editor.moveLineDown() - expect(editor.getText()).toBe("abc\ndef") - - describe ".insertText(text)", -> - describe "when there is a single selection", -> - beforeEach -> - editor.setSelectedBufferRange([[1, 0], [1, 2]]) - - it "replaces the selection with the given text", -> - range = editor.insertText('xxx') - expect(range).toEqual [ [[1, 0], [1, 3]] ] - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - - describe "when there are multiple empty selections", -> - describe "when the cursors are on the same line", -> - it "inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", -> - editor.setCursorScreenPosition([1, 2]) - editor.addCursorAtScreenPosition([1, 5]) - - editor.insertText('xxx') - - expect(buffer.lineForRow(1)).toBe ' xxxvarxxx sort = function(items) {' - [cursor1, cursor2] = editor.getCursors() - - expect(cursor1.getBufferPosition()).toEqual [1, 5] - expect(cursor2.getBufferPosition()).toEqual [1, 11] - - describe "when the cursors are on different lines", -> - it "inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", -> - editor.setCursorScreenPosition([1, 2]) - editor.addCursorAtScreenPosition([2, 4]) - - editor.insertText('xxx') - - expect(buffer.lineForRow(1)).toBe ' xxxvar sort = function(items) {' - expect(buffer.lineForRow(2)).toBe ' xxxif (items.length <= 1) return items;' - [cursor1, cursor2] = editor.getCursors() - - expect(cursor1.getBufferPosition()).toEqual [1, 5] - expect(cursor2.getBufferPosition()).toEqual [2, 7] - - describe "when there are multiple non-empty selections", -> - describe "when the selections are on the same line", -> - it "replaces each selection range with the inserted characters", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 22], [0, 24]]]) - editor.insertText("x") - - [cursor1, cursor2] = editor.getCursors() - [selection1, selection2] = editor.getSelections() - - expect(cursor1.getScreenPosition()).toEqual [0, 5] - expect(cursor2.getScreenPosition()).toEqual [0, 15] - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - expect(editor.lineTextForBufferRow(0)).toBe "var x = functix () {" - - describe "when the selections are on different lines", -> - it "replaces each selection with the given text, clears the selections, and places the cursor at the end of each selection's inserted text", -> - editor.setSelectedBufferRanges([[[1, 0], [1, 2]], [[2, 0], [2, 4]]]) - - editor.insertText('xxx') - - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - expect(buffer.lineForRow(2)).toBe 'xxxif (items.length <= 1) return items;' - [selection1, selection2] = editor.getSelections() - - expect(selection1.isEmpty()).toBeTruthy() - expect(selection1.cursor.getBufferPosition()).toEqual [1, 3] - expect(selection2.isEmpty()).toBeTruthy() - expect(selection2.cursor.getBufferPosition()).toEqual [2, 3] - - describe "when there is a selection that ends on a folded line", -> - it "destroys the selection", -> - editor.foldBufferRowRange(2, 4) - editor.setSelectedBufferRange([[1, 0], [2, 0]]) - editor.insertText('holy cow') - expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() - - describe "when there are ::onWillInsertText and ::onDidInsertText observers", -> - beforeEach -> - editor.setSelectedBufferRange([[1, 0], [1, 2]]) - - it "notifies the observers when inserting text", -> - willInsertSpy = jasmine.createSpy().andCallFake -> - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' - - didInsertSpy = jasmine.createSpy().andCallFake -> - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - - editor.onWillInsertText(willInsertSpy) - editor.onDidInsertText(didInsertSpy) - - expect(editor.insertText('xxx')).toBeTruthy() - expect(buffer.lineForRow(1)).toBe 'xxxvar sort = function(items) {' - - expect(willInsertSpy).toHaveBeenCalled() - expect(didInsertSpy).toHaveBeenCalled() - - options = willInsertSpy.mostRecentCall.args[0] - expect(options.text).toBe 'xxx' - expect(options.cancel).toBeDefined() - - options = didInsertSpy.mostRecentCall.args[0] - expect(options.text).toBe 'xxx' - - it "cancels text insertion when an ::onWillInsertText observer calls cancel on an event", -> - willInsertSpy = jasmine.createSpy().andCallFake ({cancel}) -> - cancel() - - didInsertSpy = jasmine.createSpy() - - editor.onWillInsertText(willInsertSpy) - editor.onDidInsertText(didInsertSpy) - - expect(editor.insertText('xxx')).toBe false - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) {' - - expect(willInsertSpy).toHaveBeenCalled() - expect(didInsertSpy).not.toHaveBeenCalled() - - describe "when the undo option is set to 'skip'", -> - beforeEach -> - editor.setSelectedBufferRange([[1, 2], [1, 2]]) - - it "does not undo the skipped operation", -> - range = editor.insertText('x') - range = editor.insertText('y', undo: 'skip') - editor.undo() - expect(buffer.lineForRow(1)).toBe ' yvar sort = function(items) {' - - describe ".insertNewline()", -> - describe "when there is a single cursor", -> - describe "when the cursor is at the beginning of a line", -> - it "inserts an empty line before it", -> - editor.setCursorScreenPosition(row: 1, column: 0) - - editor.insertNewline() - - expect(buffer.lineForRow(1)).toBe '' - expect(editor.getCursorScreenPosition()).toEqual(row: 2, column: 0) - - describe "when the cursor is in the middle of a line", -> - it "splits the current line to form a new line", -> - editor.setCursorScreenPosition(row: 1, column: 6) - originalLine = buffer.lineForRow(1) - lineBelowOriginalLine = buffer.lineForRow(2) - - editor.insertNewline() - - expect(buffer.lineForRow(1)).toBe originalLine[0...6] - expect(buffer.lineForRow(2)).toBe originalLine[6..] - expect(buffer.lineForRow(3)).toBe lineBelowOriginalLine - expect(editor.getCursorScreenPosition()).toEqual(row: 2, column: 0) - - describe "when the cursor is on the end of a line", -> - it "inserts an empty line after it", -> - editor.setCursorScreenPosition(row: 1, column: buffer.lineForRow(1).length) - - editor.insertNewline() - - expect(buffer.lineForRow(2)).toBe '' - expect(editor.getCursorScreenPosition()).toEqual(row: 2, column: 0) - - describe "when there are multiple cursors", -> - describe "when the cursors are on the same line", -> - it "breaks the line at the cursor locations", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([3, 38]) - - editor.insertNewline() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivot" - expect(editor.lineTextForBufferRow(4)).toBe " = items.shift(), current" - expect(editor.lineTextForBufferRow(5)).toBe ", left = [], right = [];" - expect(editor.lineTextForBufferRow(6)).toBe " while(items.length > 0) {" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [4, 0] - expect(cursor2.getBufferPosition()).toEqual [5, 0] - - describe "when the cursors are on different lines", -> - it "inserts newlines at each cursor location", -> - editor.setCursorScreenPosition([3, 0]) - editor.addCursorAtScreenPosition([6, 0]) - - editor.insertText("\n") - expect(editor.lineTextForBufferRow(3)).toBe "" - expect(editor.lineTextForBufferRow(4)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(5)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(6)).toBe " current = items.shift();" - expect(editor.lineTextForBufferRow(7)).toBe "" - expect(editor.lineTextForBufferRow(8)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(editor.lineTextForBufferRow(9)).toBe " }" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [4, 0] - expect(cursor2.getBufferPosition()).toEqual [8, 0] - - describe ".insertNewlineBelow()", -> - describe "when the operation is undone", -> - it "places the cursor back at the previous location", -> - editor.setCursorBufferPosition([0, 2]) - editor.insertNewlineBelow() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - editor.undo() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - - it "inserts a newline below the cursor's current line, autoindents it, and moves the cursor to the end of the line", -> - editor.update({autoIndent: true}) - editor.insertNewlineBelow() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " " - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - - describe ".insertNewlineAbove()", -> - describe "when the cursor is on first line", -> - it "inserts a newline on the first line and moves the cursor to the first line", -> - editor.setCursorBufferPosition([0]) - editor.insertNewlineAbove() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - expect(editor.lineTextForBufferRow(0)).toBe '' - expect(editor.lineTextForBufferRow(1)).toBe 'var quicksort = function () {' - expect(editor.buffer.getLineCount()).toBe 14 - - describe "when the cursor is not on the first line", -> - it "inserts a newline above the current line and moves the cursor to the inserted line", -> - editor.setCursorBufferPosition([3, 4]) - editor.insertNewlineAbove() - expect(editor.getCursorBufferPosition()).toEqual [3, 0] - expect(editor.lineTextForBufferRow(3)).toBe '' - expect(editor.lineTextForBufferRow(4)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(editor.buffer.getLineCount()).toBe 14 - - editor.undo() - expect(editor.getCursorBufferPosition()).toEqual [3, 4] - - it "indents the new line to the correct level when editor.autoIndent is true", -> - editor.update({autoIndent: true}) - - editor.setText(' var test') - editor.setCursorBufferPosition([0, 2]) - editor.insertNewlineAbove() - - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - expect(editor.lineTextForBufferRow(0)).toBe ' ' - expect(editor.lineTextForBufferRow(1)).toBe ' var test' - - editor.setText('\n var test') - editor.setCursorBufferPosition([1, 2]) - editor.insertNewlineAbove() - - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - expect(editor.lineTextForBufferRow(0)).toBe '' - expect(editor.lineTextForBufferRow(1)).toBe ' ' - expect(editor.lineTextForBufferRow(2)).toBe ' var test' - - editor.setText('function() {\n}') - editor.setCursorBufferPosition([1, 1]) - editor.insertNewlineAbove() - - expect(editor.getCursorBufferPosition()).toEqual [1, 2] - expect(editor.lineTextForBufferRow(0)).toBe 'function() {' - expect(editor.lineTextForBufferRow(1)).toBe ' ' - expect(editor.lineTextForBufferRow(2)).toBe '}' - - describe ".insertNewLine()", -> - describe "when a new line is appended before a closing tag (e.g. by pressing enter before a selection)", -> - it "moves the line down and keeps the indentation level the same when editor.autoIndent is true", -> - editor.update({autoIndent: true}) - editor.setCursorBufferPosition([9, 2]) - editor.insertNewline() - expect(editor.lineTextForBufferRow(10)).toBe ' };' - - describe "when a newline is appended with a trailing closing tag behind the cursor (e.g. by pressing enter in the middel of a line)", -> - it "indents the new line to the correct level when editor.autoIndent is true and using a curly-bracket language", -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - runs -> - editor.update({autoIndent: true}) - editor.setGrammar(atom.grammars.selectGrammar("file.js")) - editor.setText('var test = function () {\n return true;};') - editor.setCursorBufferPosition([1, 14]) - editor.insertNewline() - expect(editor.indentationForBufferRow(1)).toBe 1 - expect(editor.indentationForBufferRow(2)).toBe 0 - - it "indents the new line to the current level when editor.autoIndent is true and no increaseIndentPattern is specified", -> - runs -> - editor.setGrammar(atom.grammars.selectGrammar("file")) - editor.update({autoIndent: true}) - editor.setText(' if true') - editor.setCursorBufferPosition([0, 8]) - editor.insertNewline() - expect(editor.getGrammar()).toBe atom.grammars.nullGrammar - expect(editor.indentationForBufferRow(0)).toBe 1 - expect(editor.indentationForBufferRow(1)).toBe 1 - - it "indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language", -> - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - runs -> - editor.update({autoIndent: true}) - editor.setGrammar(atom.grammars.selectGrammar("file.coffee")) - editor.setText('if true\n return trueelse\n return false') - editor.setCursorBufferPosition([1, 13]) - editor.insertNewline() - expect(editor.indentationForBufferRow(1)).toBe 1 - expect(editor.indentationForBufferRow(2)).toBe 0 - expect(editor.indentationForBufferRow(3)).toBe 1 - - describe "when a newline is appended on a line that matches the decreaseNextIndentPattern", -> - it "indents the new line to the correct level when editor.autoIndent is true", -> - waitsForPromise -> - atom.packages.activatePackage('language-go') - - runs -> - editor.update({autoIndent: true}) - editor.setGrammar(atom.grammars.selectGrammar("file.go")) - editor.setText('fmt.Printf("some%s",\n "thing")') - editor.setCursorBufferPosition([1, 10]) - editor.insertNewline() - expect(editor.indentationForBufferRow(1)).toBe 1 - expect(editor.indentationForBufferRow(2)).toBe 0 - - describe ".backspace()", -> - describe "when there is a single cursor", -> - changeScreenRangeHandler = null - - beforeEach -> - selection = editor.getLastSelection() - changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') - selection.onDidChangeRange changeScreenRangeHandler - - describe "when the cursor is on the middle of the line", -> - it "removes the character before the cursor", -> - editor.setCursorScreenPosition(row: 1, column: 7) - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - - editor.backspace() - - line = buffer.lineForRow(1) - expect(line).toBe " var ort = function(items) {" - expect(editor.getCursorScreenPosition()).toEqual {row: 1, column: 6} - expect(changeScreenRangeHandler).toHaveBeenCalled() - - describe "when the cursor is at the beginning of a line", -> - it "joins it with the line above", -> - originalLine0 = buffer.lineForRow(0) - expect(originalLine0).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - - editor.setCursorScreenPosition(row: 1, column: 0) - editor.backspace() - - line0 = buffer.lineForRow(0) - line1 = buffer.lineForRow(1) - expect(line0).toBe "var quicksort = function () { var sort = function(items) {" - expect(line1).toBe " if (items.length <= 1) return items;" - expect(editor.getCursorScreenPosition()).toEqual [0, originalLine0.length] - - expect(changeScreenRangeHandler).toHaveBeenCalled() - - describe "when the cursor is at the first column of the first line", -> - it "does nothing, but doesn't raise an error", -> - editor.setCursorScreenPosition(row: 0, column: 0) - editor.backspace() - - describe "when the cursor is after a fold", -> - it "deletes the folded range", -> - editor.foldBufferRange([[4, 7], [5, 8]]) - editor.setCursorBufferPosition([5, 8]) - editor.backspace() - - expect(buffer.lineForRow(4)).toBe " whirrent = items.shift();" - expect(editor.isFoldedAtBufferRow(4)).toBe(false) - - describe "when the cursor is in the middle of a line below a fold", -> - it "backspaces as normal", -> - editor.setCursorScreenPosition([4, 0]) - editor.foldCurrentRow() - editor.setCursorScreenPosition([5, 5]) - editor.backspace() - - expect(buffer.lineForRow(7)).toBe " }" - expect(buffer.lineForRow(8)).toBe " eturn sort(left).concat(pivot).concat(sort(right));" - - describe "when the cursor is on a folded screen line", -> - it "deletes the contents of the fold before the cursor", -> - editor.setCursorBufferPosition([3, 0]) - editor.foldCurrentRow() - editor.backspace() - - expect(buffer.lineForRow(1)).toBe " var sort = function(items) var pivot = items.shift(), current, left = [], right = [];" - expect(editor.getCursorScreenPosition()).toEqual [1, 29] - - describe "when there are multiple cursors", -> - describe "when cursors are on the same line", -> - it "removes the characters preceding each cursor", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([3, 38]) - - editor.backspace() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivo = items.shift(), curren, left = [], right = [];" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 12] - expect(cursor2.getBufferPosition()).toEqual [3, 36] - - [selection1, selection2] = editor.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when cursors are on different lines", -> - describe "when the cursors are in the middle of their lines", -> - it "removes the characters preceding each cursor", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([4, 10]) - - editor.backspace() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivo = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " whileitems.length > 0) {" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 12] - expect(cursor2.getBufferPosition()).toEqual [4, 9] - - [selection1, selection2] = editor.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when the cursors are on the first column of their lines", -> - it "removes the newlines preceding each cursor", -> - editor.setCursorScreenPosition([3, 0]) - editor.addCursorAtScreenPosition([6, 0]) - - editor.backspace() - expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(3)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(4)).toBe " current = items.shift(); current < pivot ? left.push(current) : right.push(current);" - expect(editor.lineTextForBufferRow(5)).toBe " }" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [2, 40] - expect(cursor2.getBufferPosition()).toEqual [4, 30] - - describe "when there is a single selection", -> - it "deletes the selection, but not the character before it", -> - editor.setSelectedBufferRange([[0, 5], [0, 9]]) - editor.backspace() - expect(editor.buffer.lineForRow(0)).toBe 'var qsort = function () {' - - describe "when the selection ends on a folded line", -> - it "preserves the fold", -> - editor.setSelectedBufferRange([[3, 0], [4, 0]]) - editor.foldBufferRow(4) - editor.backspace() - - expect(buffer.lineForRow(3)).toBe " while(items.length > 0) {" - expect(editor.isFoldedAtScreenRow(3)).toBe(true) - - describe "when there are multiple selections", -> - it "removes all selected text", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) - editor.backspace() - expect(editor.lineTextForBufferRow(0)).toBe 'var = () {' - - describe ".deleteToPreviousWordBoundary()", -> - describe "when no text is selected", -> - it "deletes to the previous word boundary", -> - editor.setCursorBufferPosition([0, 16]) - editor.addCursorAtBufferPosition([1, 21]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToPreviousWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort =function () {' - expect(buffer.lineForRow(1)).toBe ' var sort = (items) {' - expect(cursor1.getBufferPosition()).toEqual [0, 15] - expect(cursor2.getBufferPosition()).toEqual [1, 13] - - editor.deleteToPreviousWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort function () {' - expect(buffer.lineForRow(1)).toBe ' var sort =(items) {' - expect(cursor1.getBufferPosition()).toEqual [0, 14] - expect(cursor2.getBufferPosition()).toEqual [1, 12] - - describe "when text is selected", -> - it "deletes only selected text", -> - editor.setSelectedBufferRange([[1, 24], [1, 27]]) - editor.deleteToPreviousWordBoundary() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - - describe ".deleteToNextWordBoundary()", -> - describe "when no text is selected", -> - it "deletes to the next word boundary", -> - editor.setCursorBufferPosition([0, 15]) - editor.addCursorAtBufferPosition([1, 24]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToNextWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort =function () {' - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(cursor1.getBufferPosition()).toEqual [0, 15] - expect(cursor2.getBufferPosition()).toEqual [1, 24] - - editor.deleteToNextWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort = () {' - expect(buffer.lineForRow(1)).toBe ' var sort = function(it {' - expect(cursor1.getBufferPosition()).toEqual [0, 15] - expect(cursor2.getBufferPosition()).toEqual [1, 24] - - editor.deleteToNextWordBoundary() - expect(buffer.lineForRow(0)).toBe 'var quicksort =() {' - expect(buffer.lineForRow(1)).toBe ' var sort = function(it{' - expect(cursor1.getBufferPosition()).toEqual [0, 15] - expect(cursor2.getBufferPosition()).toEqual [1, 24] - - describe "when text is selected", -> - it "deletes only selected text", -> - editor.setSelectedBufferRange([[1, 24], [1, 27]]) - editor.deleteToNextWordBoundary() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - - describe ".deleteToBeginningOfWord()", -> - describe "when no text is selected", -> - it "deletes all text between the cursor and the beginning of the word", -> - editor.setCursorBufferPosition([1, 24]) - editor.addCursorAtBufferPosition([3, 5]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(ems) {' - expect(buffer.lineForRow(3)).toBe ' ar pivot = items.shift(), current, left = [], right = [];' - expect(cursor1.getBufferPosition()).toEqual [1, 22] - expect(cursor2.getBufferPosition()).toEqual [3, 4] - - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = functionems) {' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];' - expect(cursor1.getBufferPosition()).toEqual [1, 21] - expect(cursor2.getBufferPosition()).toEqual [2, 39] - - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = ems) {' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];' - expect(cursor1.getBufferPosition()).toEqual [1, 13] - expect(cursor2.getBufferPosition()).toEqual [2, 34] - - editor.setText(' var sort') - editor.setCursorBufferPosition([0, 2]) - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(0)).toBe 'var sort' - - describe "when text is selected", -> - it "deletes only selected text", -> - editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.deleteToBeginningOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - - describe '.deleteToEndOfLine()', -> - describe 'when no text is selected', -> - it 'deletes all text between the cursor and the end of the line', -> - editor.setCursorBufferPosition([1, 24]) - editor.addCursorAtBufferPosition([2, 5]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToEndOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it' - expect(buffer.lineForRow(2)).toBe ' i' - expect(cursor1.getBufferPosition()).toEqual [1, 24] - expect(cursor2.getBufferPosition()).toEqual [2, 5] - - describe 'when at the end of the line', -> - it 'deletes the next newline', -> - editor.setCursorBufferPosition([1, 30]) - editor.deleteToEndOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' - - describe 'when text is selected', -> - it 'deletes only the text in the selection', -> - editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.deleteToEndOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - - describe ".deleteToBeginningOfLine()", -> - describe "when no text is selected", -> - it "deletes all text between the cursor and the beginning of the line", -> - editor.setCursorBufferPosition([1, 24]) - editor.addCursorAtBufferPosition([2, 5]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToBeginningOfLine() - expect(buffer.lineForRow(1)).toBe 'ems) {' - expect(buffer.lineForRow(2)).toBe 'f (items.length <= 1) return items;' - expect(cursor1.getBufferPosition()).toEqual [1, 0] - expect(cursor2.getBufferPosition()).toEqual [2, 0] - - describe "when at the beginning of the line", -> - it "deletes the newline", -> - editor.setCursorBufferPosition([2]) - editor.deleteToBeginningOfLine() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' - - describe "when text is selected", -> - it "still deletes all text to beginning of the line", -> - editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.deleteToBeginningOfLine() - expect(buffer.lineForRow(1)).toBe 'ems) {' - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' - - describe ".delete()", -> - describe "when there is a single cursor", -> - describe "when the cursor is on the middle of a line", -> - it "deletes the character following the cursor", -> - editor.setCursorScreenPosition([1, 6]) - editor.delete() - expect(buffer.lineForRow(1)).toBe ' var ort = function(items) {' - - describe "when the cursor is on the end of a line", -> - it "joins the line with the following line", -> - editor.setCursorScreenPosition([1, buffer.lineForRow(1).length]) - editor.delete() - expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;' - - describe "when the cursor is on the last column of the last line", -> - it "does nothing, but doesn't raise an error", -> - editor.setCursorScreenPosition([12, buffer.lineForRow(12).length]) - editor.delete() - expect(buffer.lineForRow(12)).toBe '};' - - describe "when the cursor is before a fold", -> - it "only deletes the lines inside the fold", -> - editor.foldBufferRange([[3, 6], [4, 8]]) - editor.setCursorScreenPosition([3, 6]) - cursorPositionBefore = editor.getCursorScreenPosition() - - editor.delete() - - expect(buffer.lineForRow(3)).toBe " vae(items.length > 0) {" - expect(buffer.lineForRow(4)).toBe " current = items.shift();" - expect(editor.getCursorScreenPosition()).toEqual cursorPositionBefore - - describe "when the cursor is in the middle a line above a fold", -> - it "deletes as normal", -> - editor.foldBufferRow(4) - editor.setCursorScreenPosition([3, 4]) - cursorPositionBefore = editor.getCursorScreenPosition() - - editor.delete() - - expect(buffer.lineForRow(3)).toBe " ar pivot = items.shift(), current, left = [], right = [];" - expect(editor.isFoldedAtScreenRow(4)).toBe(true) - expect(editor.getCursorScreenPosition()).toEqual [3, 4] - - describe "when the cursor is inside a fold", -> - it "removes the folded content after the cursor", -> - editor.foldBufferRange([[2, 6], [6, 21]]) - editor.setCursorBufferPosition([4, 9]) - - editor.delete() - - expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;' - expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - expect(buffer.lineForRow(4)).toBe ' while ? left.push(current) : right.push(current);' - expect(buffer.lineForRow(5)).toBe ' }' - expect(editor.getCursorBufferPosition()).toEqual [4, 9] - - describe "when there are multiple cursors", -> - describe "when cursors are on the same line", -> - it "removes the characters following each cursor", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([3, 38]) - - editor.delete() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivot= items.shift(), current left = [], right = [];" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 13] - expect(cursor2.getBufferPosition()).toEqual [3, 37] - - [selection1, selection2] = editor.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when cursors are on different lines", -> - describe "when the cursors are in the middle of the lines", -> - it "removes the characters following each cursor", -> - editor.setCursorScreenPosition([3, 13]) - editor.addCursorAtScreenPosition([4, 10]) - - editor.delete() - - expect(editor.lineTextForBufferRow(3)).toBe " var pivot= items.shift(), current, left = [], right = [];" - expect(editor.lineTextForBufferRow(4)).toBe " while(tems.length > 0) {" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [3, 13] - expect(cursor2.getBufferPosition()).toEqual [4, 10] - - [selection1, selection2] = editor.getSelections() - expect(selection1.isEmpty()).toBeTruthy() - expect(selection2.isEmpty()).toBeTruthy() - - describe "when the cursors are at the end of their lines", -> - it "removes the newlines following each cursor", -> - editor.setCursorScreenPosition([0, 29]) - editor.addCursorAtScreenPosition([1, 30]) - - editor.delete() - - expect(editor.lineTextForBufferRow(0)).toBe "var quicksort = function () { var sort = function(items) { if (items.length <= 1) return items;" - - [cursor1, cursor2] = editor.getCursors() - expect(cursor1.getBufferPosition()).toEqual [0, 29] - expect(cursor2.getBufferPosition()).toEqual [0, 59] - - describe "when there is a single selection", -> - it "deletes the selection, but not the character following it", -> - editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) - editor.delete() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe 'if (items.length <= 1) return items;' - expect(editor.getLastSelection().isEmpty()).toBeTruthy() - - describe "when there are multiple selections", -> - describe "when selections are on the same line", -> - it "removes all selected text", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) - editor.delete() - expect(editor.lineTextForBufferRow(0)).toBe 'var = () {' - - describe ".deleteToEndOfWord()", -> - describe "when no text is selected", -> - it "deletes to the end of the word", -> - editor.setCursorBufferPosition([1, 24]) - editor.addCursorAtBufferPosition([2, 5]) - [cursor1, cursor2] = editor.getCursors() - - editor.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - expect(buffer.lineForRow(2)).toBe ' i (items.length <= 1) return items;' - expect(cursor1.getBufferPosition()).toEqual [1, 24] - expect(cursor2.getBufferPosition()).toEqual [2, 5] - - editor.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it {' - expect(buffer.lineForRow(2)).toBe ' iitems.length <= 1) return items;' - expect(cursor1.getBufferPosition()).toEqual [1, 24] - expect(cursor2.getBufferPosition()).toEqual [2, 5] - - describe "when text is selected", -> - it "deletes only selected text", -> - editor.setSelectedBufferRange([[1, 24], [1, 27]]) - editor.deleteToEndOfWord() - expect(buffer.lineForRow(1)).toBe ' var sort = function(it) {' - - describe ".indent()", -> - describe "when the selection is empty", -> - describe "when autoIndent is disabled", -> - describe "if 'softTabs' is true (the default)", -> - it "inserts 'tabLength' spaces into the buffer", -> - tabRegex = new RegExp("^[ ]{#{editor.getTabLength()}}") - expect(buffer.lineForRow(0)).not.toMatch(tabRegex) - editor.indent() - expect(buffer.lineForRow(0)).toMatch(tabRegex) - - it "respects the tab stops when cursor is in the middle of a tab", -> - editor.setTabLength(4) - buffer.insert([12, 2], "\n ") - editor.setCursorBufferPosition [13, 1] - editor.indent() - expect(buffer.lineForRow(13)).toMatch /^\s+$/ - expect(buffer.lineForRow(13).length).toBe 4 - expect(editor.getCursorBufferPosition()).toEqual [13, 4] - - buffer.insert([13, 0], " ") - editor.setCursorBufferPosition [13, 6] - editor.indent() - expect(buffer.lineForRow(13).length).toBe 8 - - describe "if 'softTabs' is false", -> - it "insert a \t into the buffer", -> - editor.setSoftTabs(false) - expect(buffer.lineForRow(0)).not.toMatch(/^\t/) - editor.indent() - expect(buffer.lineForRow(0)).toMatch(/^\t/) - - describe "when autoIndent is enabled", -> - describe "when the cursor's column is less than the suggested level of indentation", -> - describe "when 'softTabs' is true (the default)", -> - it "moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentation", -> - buffer.insert([5, 0], " \n") - editor.setCursorBufferPosition [5, 0] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(5)).toMatch /^\s+$/ - expect(buffer.lineForRow(5).length).toBe 6 - expect(editor.getCursorBufferPosition()).toEqual [5, 6] - - it "respects the tab stops when cursor is in the middle of a tab", -> - editor.setTabLength(4) - buffer.insert([12, 2], "\n ") - editor.setCursorBufferPosition [13, 1] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(13)).toMatch /^\s+$/ - expect(buffer.lineForRow(13).length).toBe 4 - expect(editor.getCursorBufferPosition()).toEqual [13, 4] - - buffer.insert([13, 0], " ") - editor.setCursorBufferPosition [13, 6] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(13).length).toBe 8 - - describe "when 'softTabs' is false", -> - it "moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentation", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - buffer.insert([5, 0], "\t\n") - editor.setCursorBufferPosition [5, 0] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(5)).toMatch /^\t\t\t$/ - expect(editor.getCursorBufferPosition()).toEqual [5, 3] - - describe "when the difference between the suggested level of indentation and the current level of indentation is greater than 0 but less than 1", -> - it "inserts one tab", -> - editor.setSoftTabs(false) - buffer.setText(" \ntest") - editor.setCursorBufferPosition [1, 0] - - editor.indent(autoIndent: true) - expect(buffer.lineForRow(1)).toBe '\ttest' - expect(editor.getCursorBufferPosition()).toEqual [1, 1] - - describe "when the line's indent level is greater than the suggested level of indentation", -> - describe "when 'softTabs' is true (the default)", -> - it "moves the cursor to the end of the leading whitespace and inserts 'tabLength' spaces into the buffer", -> - buffer.insert([7, 0], " \n") - editor.setCursorBufferPosition [7, 2] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(7)).toMatch /^\s+$/ - expect(buffer.lineForRow(7).length).toBe 8 - expect(editor.getCursorBufferPosition()).toEqual [7, 8] - - describe "when 'softTabs' is false", -> - it "moves the cursor to the end of the leading whitespace and inserts \t into the buffer", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - buffer.insert([7, 0], "\t\t\t\n") - editor.setCursorBufferPosition [7, 1] - editor.indent(autoIndent: true) - expect(buffer.lineForRow(7)).toMatch /^\t\t\t\t$/ - expect(editor.getCursorBufferPosition()).toEqual [7, 4] - - describe "when the selection is not empty", -> - it "indents the selected lines", -> - editor.setSelectedBufferRange([[0, 0], [10, 0]]) - selection = editor.getLastSelection() - spyOn(selection, "indentSelectedRows") - editor.indent() - expect(selection.indentSelectedRows).toHaveBeenCalled() - - describe "if editor.softTabs is false", -> - it "inserts a tab character into the buffer", -> - editor.setSoftTabs(false) - expect(buffer.lineForRow(0)).not.toMatch(/^\t/) - editor.indent() - expect(buffer.lineForRow(0)).toMatch(/^\t/) - expect(editor.getCursorBufferPosition()).toEqual [0, 1] - expect(editor.getCursorScreenPosition()).toEqual [0, editor.getTabLength()] - - editor.indent() - expect(buffer.lineForRow(0)).toMatch(/^\t\t/) - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - expect(editor.getCursorScreenPosition()).toEqual [0, editor.getTabLength() * 2] - - describe "clipboard operations", -> - describe ".cutSelectedText()", -> - it "removes the selected text from the buffer and places it on the clipboard", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - editor.cutSelectedText() - expect(buffer.lineForRow(0)).toBe "var = function () {" - expect(buffer.lineForRow(1)).toBe " var = function(items) {" - expect(clipboard.readText()).toBe 'quicksort\nsort' - - describe "when no text is selected", -> - beforeEach -> - editor.setSelectedBufferRanges([ - [[0, 0], [0, 0]], - [[5, 0], [5, 0]], - ]) - - it "cuts the lines on which there are cursors", -> - editor.cutSelectedText() - expect(buffer.getLineCount()).toBe(11) - expect(buffer.lineForRow(1)).toBe(" if (items.length <= 1) return items;") - expect(buffer.lineForRow(4)).toBe(" current < pivot ? left.push(current) : right.push(current);") - expect(atom.clipboard.read()).toEqual """ - var quicksort = function () { - - current = items.shift(); - - """ - - describe "when many selections get added in shuffle order", -> - it "cuts them in order", -> - editor.setSelectedBufferRanges([ - [[2, 8], [2, 13]] - [[0, 4], [0, 13]], - [[1, 6], [1, 10]], - ]) - editor.cutSelectedText() - expect(atom.clipboard.read()).toEqual """ - quicksort - sort - items - """ - - describe ".cutToEndOfLine()", -> - describe "when soft wrap is on", -> - it "cuts up to the end of the line", -> - editor.setSoftWrapped(true) - editor.setDefaultCharWidth(1) - editor.setEditorWidthInChars(25) - editor.setCursorScreenPosition([2, 6]) - editor.cutToEndOfLine() - expect(editor.lineTextForScreenRow(2)).toBe ' var function(items) {' - - describe "when soft wrap is off", -> - describe "when nothing is selected", -> - it "cuts up to the end of the line", -> - editor.setCursorBufferPosition([2, 20]) - editor.addCursorAtBufferPosition([3, 20]) - editor.cutToEndOfLine() - expect(buffer.lineForRow(2)).toBe ' if (items.length' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.clipboard.read()).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];' - - describe "when text is selected", -> - it "only cuts the selected text, not to the end of the line", -> - editor.setSelectedBufferRanges([[[2, 20], [2, 30]], [[3, 20], [3, 20]]]) - - editor.cutToEndOfLine() - - expect(buffer.lineForRow(2)).toBe ' if (items.lengthurn items;' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.clipboard.read()).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];' - - describe ".cutToEndOfBufferLine()", -> - beforeEach -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(10) - - describe "when nothing is selected", -> - it "cuts up to the end of the buffer line", -> - editor.setCursorBufferPosition([2, 20]) - editor.addCursorAtBufferPosition([3, 20]) - - editor.cutToEndOfBufferLine() - - expect(buffer.lineForRow(2)).toBe ' if (items.length' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.clipboard.read()).toBe ' <= 1) return items;\ns.shift(), current, left = [], right = [];' - - describe "when text is selected", -> - it "only cuts the selected text, not to the end of the buffer line", -> - editor.setSelectedBufferRanges([[[2, 20], [2, 30]], [[3, 20], [3, 20]]]) - - editor.cutToEndOfBufferLine() - - expect(buffer.lineForRow(2)).toBe ' if (items.lengthurn items;' - expect(buffer.lineForRow(3)).toBe ' var pivot = item' - expect(atom.clipboard.read()).toBe ' <= 1) ret\ns.shift(), current, left = [], right = [];' - - describe ".copySelectedText()", -> - it "copies selected text onto the clipboard", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]]) - - editor.copySelectedText() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(clipboard.readText()).toBe 'quicksort\nsort\nitems' - expect(atom.clipboard.read()).toEqual """ - quicksort - sort - items - """ - - describe "when no text is selected", -> - beforeEach -> - editor.setSelectedBufferRanges([ - [[1, 5], [1, 5]], - [[5, 8], [5, 8]] - ]) - - it "copies the lines on which there are cursors", -> - editor.copySelectedText() - expect(atom.clipboard.read()).toEqual([ - " var sort = function(items) {\n" - " current = items.shift();\n" - ].join("\n")) - expect(editor.getSelectedBufferRanges()).toEqual([ - [[1, 5], [1, 5]], - [[5, 8], [5, 8]] - ]) - - describe "when many selections get added in shuffle order", -> - it "copies them in order", -> - editor.setSelectedBufferRanges([ - [[2, 8], [2, 13]] - [[0, 4], [0, 13]], - [[1, 6], [1, 10]], - ]) - editor.copySelectedText() - expect(atom.clipboard.read()).toEqual """ - quicksort - sort - items - """ - - describe ".copyOnlySelectedText()", -> - describe "when thee are multiple selections", -> - it "copies selected text onto the clipboard", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]]) - - editor.copyOnlySelectedText() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe " var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(clipboard.readText()).toBe 'quicksort\nsort\nitems' - expect(atom.clipboard.read()).toEqual """ - quicksort - sort - items - """ - - describe "when no text is selected", -> - it "does not copy anything", -> - editor.setCursorBufferPosition([1, 5]) - editor.copyOnlySelectedText() - expect(atom.clipboard.read()).toEqual "initial clipboard content" - - describe ".pasteText()", -> - copyText = (text, {startColumn, textEditor}={}) -> - startColumn ?= 0 - textEditor ?= editor - textEditor.setCursorBufferPosition([0, 0]) - textEditor.insertText(text) - numberOfNewlines = text.match(/\n/g)?.length - endColumn = text.match(/[^\n]*$/)[0]?.length - textEditor.getLastSelection().setBufferRange([[0, startColumn], [numberOfNewlines, endColumn]]) - textEditor.cutSelectedText() - - it "pastes text into the buffer", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - atom.clipboard.write('first') - editor.pasteText() - expect(editor.lineTextForBufferRow(0)).toBe "var first = function () {" - expect(editor.lineTextForBufferRow(1)).toBe " var first = function(items) {" - - it "notifies ::onWillInsertText observers", -> - insertedStrings = [] - editor.onWillInsertText ({text, cancel}) -> - insertedStrings.push(text) - cancel() - - atom.clipboard.write("hello") - editor.pasteText() - - expect(insertedStrings).toEqual ["hello"] - - it "notifies ::onDidInsertText observers", -> - insertedStrings = [] - editor.onDidInsertText ({text, range}) -> - insertedStrings.push(text) - - atom.clipboard.write("hello") - editor.pasteText() - - expect(insertedStrings).toEqual ["hello"] - - describe "when `autoIndentOnPaste` is true", -> - beforeEach -> - editor.update({autoIndentOnPaste: true}) - - describe "when pasting multiple lines before any non-whitespace characters", -> - it "auto-indents the lines spanned by the pasted text, based on the first pasted line", -> - atom.clipboard.write("a(x);\n b(x);\n c(x);\n", indentBasis: 0) - editor.setCursorBufferPosition([5, 0]) - editor.pasteText() - - # Adjust the indentation of the pasted lines while preserving - # their indentation relative to each other. Also preserve the - # indentation of the following line. - expect(editor.lineTextForBufferRow(5)).toBe " a(x);" - expect(editor.lineTextForBufferRow(6)).toBe " b(x);" - expect(editor.lineTextForBufferRow(7)).toBe " c(x);" - expect(editor.lineTextForBufferRow(8)).toBe " current = items.shift();" - - it "auto-indents lines with a mix of hard tabs and spaces without removing spaces", -> - editor.setSoftTabs(false) - expect(editor.indentationForBufferRow(5)).toBe(3) - - atom.clipboard.write("/**\n\t * testing\n\t * indent\n\t **/\n", indentBasis: 1) - editor.setCursorBufferPosition([5, 0]) - editor.pasteText() - - # Do not lose the alignment spaces - expect(editor.lineTextForBufferRow(5)).toBe("\t\t\t/**") - expect(editor.lineTextForBufferRow(6)).toBe("\t\t\t * testing") - expect(editor.lineTextForBufferRow(7)).toBe("\t\t\t * indent") - expect(editor.lineTextForBufferRow(8)).toBe("\t\t\t **/") - - describe "when pasting line(s) above a line that matches the decreaseIndentPattern", -> - it "auto-indents based on the pasted line(s) only", -> - atom.clipboard.write("a(x);\n b(x);\n c(x);\n", indentBasis: 0) - editor.setCursorBufferPosition([7, 0]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(7)).toBe " a(x);" - expect(editor.lineTextForBufferRow(8)).toBe " b(x);" - expect(editor.lineTextForBufferRow(9)).toBe " c(x);" - expect(editor.lineTextForBufferRow(10)).toBe " }" - - describe "when pasting a line of text without line ending", -> - it "does not auto-indent the text", -> - atom.clipboard.write("a(x);", indentBasis: 0) - editor.setCursorBufferPosition([5, 0]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(5)).toBe "a(x); current = items.shift();" - expect(editor.lineTextForBufferRow(6)).toBe " current < pivot ? left.push(current) : right.push(current);" - - describe "when pasting on a line after non-whitespace characters", -> - it "does not auto-indent the affected line", -> - # Before the paste, the indentation is non-standard. - editor.setText """ - if (x) { - y(); - } - """ - - atom.clipboard.write(" z();\n h();") - editor.setCursorBufferPosition([1, Infinity]) - - # The indentation of the non-standard line is unchanged. - editor.pasteText() - expect(editor.lineTextForBufferRow(1)).toBe(" y(); z();") - expect(editor.lineTextForBufferRow(2)).toBe(" h();") - - describe "when `autoIndentOnPaste` is false", -> - beforeEach -> - editor.update({autoIndentOnPaste: false}) - - describe "when the cursor is indented further than the original copied text", -> - it "increases the indentation of the copied lines to match", -> - editor.setSelectedBufferRange([[1, 2], [3, 0]]) - editor.copySelectedText() - - editor.setCursorBufferPosition([5, 6]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(5)).toBe " var sort = function(items) {" - expect(editor.lineTextForBufferRow(6)).toBe " if (items.length <= 1) return items;" - - describe "when the cursor is indented less far than the original copied text", -> - it "decreases the indentation of the copied lines to match", -> - editor.setSelectedBufferRange([[6, 6], [8, 0]]) - editor.copySelectedText() - - editor.setCursorBufferPosition([1, 2]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(1)).toBe " current < pivot ? left.push(current) : right.push(current);" - expect(editor.lineTextForBufferRow(2)).toBe "}" - - describe "when the first copied line has leading whitespace", -> - it "preserves the line's leading whitespace", -> - editor.setSelectedBufferRange([[4, 0], [6, 0]]) - editor.copySelectedText() - - editor.setCursorBufferPosition([0, 0]) - editor.pasteText() - - expect(editor.lineTextForBufferRow(0)).toBe " while(items.length > 0) {" - expect(editor.lineTextForBufferRow(1)).toBe " current = items.shift();" - - describe 'when the clipboard has many selections', -> - beforeEach -> - editor.update({autoIndentOnPaste: false}) - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - editor.copySelectedText() - - it "pastes each selection in order separately into the buffer", -> - editor.setSelectedBufferRanges([ - [[1, 6], [1, 10]] - [[0, 4], [0, 13]], - ]) - - editor.moveRight() - editor.insertText("_") - editor.pasteText() - expect(editor.lineTextForBufferRow(0)).toBe "var quicksort_quicksort = function () {" - expect(editor.lineTextForBufferRow(1)).toBe " var sort_sort = function(items) {" - - describe 'and the selections count does not match', -> - beforeEach -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]]]) - - it "pastes the whole text into the buffer", -> - editor.pasteText() - expect(editor.lineTextForBufferRow(0)).toBe "var quicksort" - expect(editor.lineTextForBufferRow(1)).toBe "sort = function () {" - - describe "when a full line was cut", -> - beforeEach -> - editor.setCursorBufferPosition([2, 13]) - editor.cutSelectedText() - editor.setCursorBufferPosition([2, 13]) - - it "pastes the line above the cursor and retains the cursor's column", -> - editor.pasteText() - expect(editor.lineTextForBufferRow(2)).toBe(" if (items.length <= 1) return items;") - expect(editor.lineTextForBufferRow(3)).toBe(" var pivot = items.shift(), current, left = [], right = [];") - expect(editor.getCursorBufferPosition()).toEqual([3, 13]) - - describe "when a full line was copied", -> - beforeEach -> - editor.setCursorBufferPosition([2, 13]) - editor.copySelectedText() - - describe "when there is a selection", -> - it "overwrites the selection as with any copied text", -> - editor.setSelectedBufferRange([[1, 2], [1, Infinity]]) - editor.pasteText() - expect(editor.lineTextForBufferRow(1)).toBe(" if (items.length <= 1) return items;") - expect(editor.lineTextForBufferRow(2)).toBe("") - expect(editor.lineTextForBufferRow(3)).toBe(" if (items.length <= 1) return items;") - expect(editor.getCursorBufferPosition()).toEqual([2, 0]) - - describe "when there is no selection", -> - it "pastes the line above the cursor and retains the cursor's column", -> - editor.pasteText() - expect(editor.lineTextForBufferRow(2)).toBe(" if (items.length <= 1) return items;") - expect(editor.lineTextForBufferRow(3)).toBe(" if (items.length <= 1) return items;") - expect(editor.getCursorBufferPosition()).toEqual([3, 13]) - - it "respects options that preserve the formatting of the pasted text", -> - editor.update({autoIndentOnPaste: true}) - atom.clipboard.write("a(x);\n b(x);\r\nc(x);\n", indentBasis: 0) - editor.setCursorBufferPosition([5, 0]) - editor.insertText(' ') - editor.pasteText({autoIndent: false, preserveTrailingLineIndentation: true, normalizeLineEndings: false}) - - expect(editor.lineTextForBufferRow(5)).toBe " a(x);" - expect(editor.lineTextForBufferRow(6)).toBe " b(x);" - expect(editor.buffer.lineEndingForRow(6)).toBe "\r\n" - expect(editor.lineTextForBufferRow(7)).toBe "c(x);" - expect(editor.lineTextForBufferRow(8)).toBe " current = items.shift();" - - describe ".indentSelectedRows()", -> - describe "when nothing is selected", -> - describe "when softTabs is enabled", -> - it "indents line and retains selection", -> - editor.setSelectedBufferRange([[0, 3], [0, 3]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 3 + editor.getTabLength()], [0, 3 + editor.getTabLength()]] - - describe "when softTabs is disabled", -> - it "indents line and retains selection", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - editor.setSelectedBufferRange([[0, 3], [0, 3]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 3 + 1], [0, 3 + 1]] - - describe "when one line is selected", -> - describe "when softTabs is enabled", -> - it "indents line and retains selection", -> - editor.setSelectedBufferRange([[0, 4], [0, 14]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "#{editor.getTabText()}var quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 4 + editor.getTabLength()], [0, 14 + editor.getTabLength()]] - - describe "when softTabs is disabled", -> - it "indents line and retains selection", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - editor.setSelectedBufferRange([[0, 4], [0, 14]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" - expect(editor.getSelectedBufferRange()).toEqual [[0, 4 + 1], [0, 14 + 1]] - - describe "when multiple lines are selected", -> - describe "when softTabs is enabled", -> - it "indents selected lines (that are not empty) and retains selection", -> - editor.setSelectedBufferRange([[9, 1], [11, 15]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(9)).toBe " };" - expect(buffer.lineForRow(10)).toBe "" - expect(buffer.lineForRow(11)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.getSelectedBufferRange()).toEqual [[9, 1 + editor.getTabLength()], [11, 15 + editor.getTabLength()]] - - it "does not indent the last row if the selection ends at column 0", -> - editor.setSelectedBufferRange([[9, 1], [11, 0]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(9)).toBe " };" - expect(buffer.lineForRow(10)).toBe "" - expect(buffer.lineForRow(11)).toBe " return sort(Array.apply(this, arguments));" - expect(editor.getSelectedBufferRange()).toEqual [[9, 1 + editor.getTabLength()], [11, 0]] - - describe "when softTabs is disabled", -> - it "indents selected lines (that are not empty) and retains selection", -> - convertToHardTabs(buffer) - editor.setSoftTabs(false) - editor.setSelectedBufferRange([[9, 1], [11, 15]]) - editor.indentSelectedRows() - expect(buffer.lineForRow(9)).toBe "\t\t};" - expect(buffer.lineForRow(10)).toBe "" - expect(buffer.lineForRow(11)).toBe "\t\treturn sort(Array.apply(this, arguments));" - expect(editor.getSelectedBufferRange()).toEqual [[9, 1 + 1], [11, 15 + 1]] - - describe ".outdentSelectedRows()", -> - describe "when nothing is selected", -> - it "outdents line and retains selection", -> - editor.setSelectedBufferRange([[1, 3], [1, 3]]) - editor.outdentSelectedRows() - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(editor.getSelectedBufferRange()).toEqual [[1, 3 - editor.getTabLength()], [1, 3 - editor.getTabLength()]] - - it "outdents when indent is less than a tab length", -> - editor.insertText(' ') - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "outdents a single hard tab when indent is multiple hard tabs and and the session is using soft tabs", -> - editor.insertText('\t\t') - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tvar quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "outdents when a mix of hard tabs and soft tabs are used", -> - editor.insertText('\t ') - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe " var quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - - it "outdents only up to the first non-space non-tab character", -> - editor.insertText(' \tfoo\t ') - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "\tfoo\t var quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "foo\t var quicksort = function () {" - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "foo\t var quicksort = function () {" - - describe "when one line is selected", -> - it "outdents line and retains editor", -> - editor.setSelectedBufferRange([[1, 4], [1, 14]]) - editor.outdentSelectedRows() - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(editor.getSelectedBufferRange()).toEqual [[1, 4 - editor.getTabLength()], [1, 14 - editor.getTabLength()]] - - describe "when multiple lines are selected", -> - it "outdents selected lines and retains editor", -> - editor.setSelectedBufferRange([[0, 1], [3, 15]]) - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [3, 15 - editor.getTabLength()]] - - it "does not outdent the last line of the selection if it ends at column 0", -> - editor.setSelectedBufferRange([[0, 1], [3, 0]]) - editor.outdentSelectedRows() - expect(buffer.lineForRow(0)).toBe "var quicksort = function () {" - expect(buffer.lineForRow(1)).toBe "var sort = function(items) {" - expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;" - expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];" - - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [3, 0]] - - describe ".autoIndentSelectedRows", -> - it "auto-indents the selection", -> - editor.setCursorBufferPosition([2, 0]) - editor.insertText("function() {\ninside=true\n}\n i=1\n") - editor.getLastSelection().setBufferRange([[2, 0], [6, 0]]) - editor.autoIndentSelectedRows() - - expect(editor.lineTextForBufferRow(2)).toBe " function() {" - expect(editor.lineTextForBufferRow(3)).toBe " inside=true" - expect(editor.lineTextForBufferRow(4)).toBe " }" - expect(editor.lineTextForBufferRow(5)).toBe " i=1" - - describe ".undo() and .redo()", -> - it "undoes/redoes the last change", -> - editor.insertText("foo") - editor.undo() - expect(buffer.lineForRow(0)).not.toContain "foo" - - editor.redo() - expect(buffer.lineForRow(0)).toContain "foo" - - it "batches the undo / redo of changes caused by multiple cursors", -> - editor.setCursorScreenPosition([0, 0]) - editor.addCursorAtScreenPosition([1, 0]) - - editor.insertText("foo") - editor.backspace() - - expect(buffer.lineForRow(0)).toContain "fovar" - expect(buffer.lineForRow(1)).toContain "fo " - - editor.undo() - - expect(buffer.lineForRow(0)).toContain "foo" - expect(buffer.lineForRow(1)).toContain "foo" - - editor.redo() - - expect(buffer.lineForRow(0)).not.toContain "foo" - expect(buffer.lineForRow(0)).toContain "fovar" - - it "restores cursors and selections to their states before and after undone and redone changes", -> - editor.setSelectedBufferRanges([ - [[0, 0], [0, 0]], - [[1, 0], [1, 3]], - ]) - editor.insertText("abc") - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 3], [0, 3]], - [[1, 3], [1, 3]] - ] - - editor.setCursorBufferPosition([0, 0]) - editor.setSelectedBufferRanges([ - [[2, 0], [2, 0]], - [[3, 0], [3, 0]], - [[4, 0], [4, 3]], - ]) - editor.insertText("def") - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[2, 3], [2, 3]], - [[3, 3], [3, 3]] - [[4, 3], [4, 3]] - ] - - editor.setCursorBufferPosition([0, 0]) - editor.undo() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[2, 0], [2, 0]], - [[3, 0], [3, 0]], - [[4, 0], [4, 3]], - ] - - editor.undo() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 0], [0, 0]], - [[1, 0], [1, 3]] - ] - - editor.redo() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[0, 3], [0, 3]], - [[1, 3], [1, 3]] - ] - - editor.redo() - - expect(editor.getSelectedBufferRanges()).toEqual [ - [[2, 3], [2, 3]], - [[3, 3], [3, 3]] - [[4, 3], [4, 3]] - ] - - it "restores the selected ranges after undo and redo", -> - editor.setSelectedBufferRanges([[[1, 6], [1, 10]], [[1, 22], [1, 27]]]) - editor.delete() - editor.delete() - - selections = editor.getSelections() - expect(buffer.lineForRow(1)).toBe ' var = function( {' - - expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 17], [1, 17]]] - - editor.undo() - expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] - - editor.undo() - expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 10]], [[1, 22], [1, 27]]] - - editor.redo() - expect(editor.getSelectedBufferRanges()).toEqual [[[1, 6], [1, 6]], [[1, 18], [1, 18]]] - - xit "restores folds after undo and redo", -> - editor.foldBufferRow(1) - editor.setSelectedBufferRange([[1, 0], [10, Infinity]], preserveFolds: true) - expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() - - editor.insertText """ - \ // testing - function foo() { - return 1 + 2; - } - """ - expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() - editor.foldBufferRow(2) - - editor.undo() - expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() - expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() - - editor.redo() - expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() - expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() - - describe "::transact", -> - it "restores the selection when the transaction is undone/redone", -> - buffer.setText('1234') - editor.setSelectedBufferRange([[0, 1], [0, 3]]) - - editor.transact -> - editor.delete() - editor.moveToEndOfLine() - editor.insertText('5') - expect(buffer.getText()).toBe '145' - - editor.undo() - expect(buffer.getText()).toBe '1234' - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [0, 3]] - - editor.redo() - expect(buffer.getText()).toBe '145' - expect(editor.getSelectedBufferRange()).toEqual [[0, 3], [0, 3]] - - describe "when the buffer is changed (via its direct api, rather than via than edit session)", -> - it "moves the cursor so it is in the same relative position of the buffer", -> - expect(editor.getCursorScreenPosition()).toEqual [0, 0] - editor.addCursorAtScreenPosition([0, 5]) - editor.addCursorAtScreenPosition([1, 0]) - [cursor1, cursor2, cursor3] = editor.getCursors() - - buffer.insert([0, 1], 'abc') - - expect(cursor1.getScreenPosition()).toEqual [0, 0] - expect(cursor2.getScreenPosition()).toEqual [0, 8] - expect(cursor3.getScreenPosition()).toEqual [1, 0] - - it "does not destroy cursors or selections when a change encompasses them", -> - cursor = editor.getLastCursor() - cursor.setBufferPosition [3, 3] - editor.buffer.delete([[3, 1], [3, 5]]) - expect(cursor.getBufferPosition()).toEqual [3, 1] - expect(editor.getCursors().indexOf(cursor)).not.toBe -1 - - selection = editor.getLastSelection() - selection.setBufferRange [[3, 5], [3, 10]] - editor.buffer.delete [[3, 3], [3, 8]] - expect(selection.getBufferRange()).toEqual [[3, 3], [3, 5]] - expect(editor.getSelections().indexOf(selection)).not.toBe -1 - - it "merges cursors when the change causes them to overlap", -> - editor.setCursorScreenPosition([0, 0]) - editor.addCursorAtScreenPosition([0, 2]) - editor.addCursorAtScreenPosition([1, 2]) - - [cursor1, cursor2, cursor3] = editor.getCursors() - expect(editor.getCursors().length).toBe 3 - - buffer.delete([[0, 0], [0, 2]]) - - expect(editor.getCursors().length).toBe 2 - expect(editor.getCursors()).toEqual [cursor1, cursor3] - expect(cursor1.getBufferPosition()).toEqual [0, 0] - expect(cursor3.getBufferPosition()).toEqual [1, 2] - - describe ".moveSelectionLeft()", -> - it "moves one active selection on one line one column to the left", -> - editor.setSelectedBufferRange [[0, 4], [0, 13]] - expect(editor.getSelectedText()).toBe 'quicksort' - - editor.moveSelectionLeft() - - expect(editor.getSelectedText()).toBe 'quicksort' - expect(editor.getSelectedBufferRange()).toEqual [[0, 3], [0, 12]] - - it "moves multiple active selections on one line one column to the left", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'function' - - editor.moveSelectionLeft() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'function' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 3], [0, 12]], [[0, 15], [0, 23]]] - - it "moves multiple active selections on multiple lines one column to the left", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'sort' - - editor.moveSelectionLeft() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'sort' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 3], [0, 12]], [[1, 5], [1, 9]]] - - describe "when a selection is at the first column of a line", -> - it "does not change the selection", -> - editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[1, 0], [1, 3]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'var' - expect(selections[1].getText()).toBe ' v' - - editor.moveSelectionLeft() - editor.moveSelectionLeft() - - expect(selections[0].getText()).toBe 'var' - expect(selections[1].getText()).toBe ' v' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]], [[1, 0], [1, 3]]] - - describe "when multiple selections are active on one line", -> - it "does not change the selection", -> - editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[0, 4], [0, 13]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'var' - expect(selections[1].getText()).toBe 'quicksort' - - editor.moveSelectionLeft() - - expect(selections[0].getText()).toBe 'var' - expect(selections[1].getText()).toBe 'quicksort' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]], [[0, 4], [0, 13]]] - - describe ".moveSelectionRight()", -> - it "moves one active selection on one line one column to the right", -> - editor.setSelectedBufferRange [[0, 4], [0, 13]] - expect(editor.getSelectedText()).toBe 'quicksort' - - editor.moveSelectionRight() - - expect(editor.getSelectedText()).toBe 'quicksort' - expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 14]] - - it "moves multiple active selections on one line one column to the right", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'function' - - editor.moveSelectionRight() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'function' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 5], [0, 14]], [[0, 17], [0, 25]]] - - it "moves multiple active selections on multiple lines one column to the right", -> - editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'sort' - - editor.moveSelectionRight() - - expect(selections[0].getText()).toBe 'quicksort' - expect(selections[1].getText()).toBe 'sort' - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 5], [0, 14]], [[1, 7], [1, 11]]] - - describe "when a selection is at the last column of a line", -> - it "does not change the selection", -> - editor.setSelectedBufferRanges([[[2, 34], [2, 40]], [[5, 22], [5, 30]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'items;' - expect(selections[1].getText()).toBe 'shift();' - - editor.moveSelectionRight() - editor.moveSelectionRight() - - expect(selections[0].getText()).toBe 'items;' - expect(selections[1].getText()).toBe 'shift();' - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 34], [2, 40]], [[5, 22], [5, 30]]] - - describe "when multiple selections are active on one line", -> - it "does not change the selection", -> - editor.setSelectedBufferRanges([[[2, 27], [2, 33]], [[2, 34], [2, 40]]]) - selections = editor.getSelections() - - expect(selections[0].getText()).toBe 'return' - expect(selections[1].getText()).toBe 'items;' - - editor.moveSelectionRight() - - expect(selections[0].getText()).toBe 'return' - expect(selections[1].getText()).toBe 'items;' - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 27], [2, 33]], [[2, 34], [2, 40]]] - - describe 'reading text', -> - it '.lineTextForScreenRow(row)', -> - editor.foldBufferRow(4) - expect(editor.lineTextForScreenRow(5)).toEqual ' return sort(left).concat(pivot).concat(sort(right));' - expect(editor.lineTextForScreenRow(9)).toEqual '};' - expect(editor.lineTextForScreenRow(10)).toBeUndefined() - - describe ".deleteLine()", -> - it "deletes the first line when the cursor is there", -> - editor.getLastCursor().moveToTop() - line1 = buffer.lineForRow(1) - count = buffer.getLineCount() - expect(buffer.lineForRow(0)).not.toBe(line1) - editor.deleteLine() - expect(buffer.lineForRow(0)).toBe(line1) - expect(buffer.getLineCount()).toBe(count - 1) - - it "deletes the last line when the cursor is there", -> - count = buffer.getLineCount() - secondToLastLine = buffer.lineForRow(count - 2) - expect(buffer.lineForRow(count - 1)).not.toBe(secondToLastLine) - editor.getLastCursor().moveToBottom() - editor.deleteLine() - newCount = buffer.getLineCount() - expect(buffer.lineForRow(newCount - 1)).toBe(secondToLastLine) - expect(newCount).toBe(count - 1) - - it "deletes whole lines when partial lines are selected", -> - editor.setSelectedBufferRange([[0, 2], [1, 2]]) - line2 = buffer.lineForRow(2) - count = buffer.getLineCount() - expect(buffer.lineForRow(0)).not.toBe(line2) - expect(buffer.lineForRow(1)).not.toBe(line2) - editor.deleteLine() - expect(buffer.lineForRow(0)).toBe(line2) - expect(buffer.getLineCount()).toBe(count - 2) - - it "deletes a line only once when multiple selections are on the same line", -> - line1 = buffer.lineForRow(1) - count = buffer.getLineCount() - editor.setSelectedBufferRanges([ - [[0, 1], [0, 2]], - [[0, 4], [0, 5]] - ]) - expect(buffer.lineForRow(0)).not.toBe(line1) - - editor.deleteLine() - - expect(buffer.lineForRow(0)).toBe(line1) - expect(buffer.getLineCount()).toBe(count - 1) - - it "only deletes first line if only newline is selected on second line", -> - editor.setSelectedBufferRange([[0, 2], [1, 0]]) - line1 = buffer.lineForRow(1) - count = buffer.getLineCount() - expect(buffer.lineForRow(0)).not.toBe(line1) - editor.deleteLine() - expect(buffer.lineForRow(0)).toBe(line1) - expect(buffer.getLineCount()).toBe(count - 1) - - it "deletes the entire region when invoke on a folded region", -> - editor.foldBufferRow(1) - editor.getLastCursor().moveToTop() - editor.getLastCursor().moveDown() - expect(buffer.getLineCount()).toBe(13) - editor.deleteLine() - expect(buffer.getLineCount()).toBe(4) - - it "deletes the entire file from the bottom up", -> - count = buffer.getLineCount() - expect(count).toBeGreaterThan(0) - for [0...count] - editor.getLastCursor().moveToBottom() - editor.deleteLine() - expect(buffer.getLineCount()).toBe(1) - expect(buffer.getText()).toBe('') - - it "deletes the entire file from the top down", -> - count = buffer.getLineCount() - expect(count).toBeGreaterThan(0) - for [0...count] - editor.getLastCursor().moveToTop() - editor.deleteLine() - expect(buffer.getLineCount()).toBe(1) - expect(buffer.getText()).toBe('') - - describe "when soft wrap is enabled", -> - it "deletes the entire line that the cursor is on", -> - editor.setSoftWrapped(true) - editor.setEditorWidthInChars(10) - editor.setCursorBufferPosition([6]) - - line7 = buffer.lineForRow(7) - count = buffer.getLineCount() - expect(buffer.lineForRow(6)).not.toBe(line7) - editor.deleteLine() - expect(buffer.lineForRow(6)).toBe(line7) - expect(buffer.getLineCount()).toBe(count - 1) - - describe "when the line being deleted precedes a fold, and the command is undone", -> - it "restores the line and preserves the fold", -> - editor.setCursorBufferPosition([4]) - editor.foldCurrentRow() - expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() - editor.setCursorBufferPosition([3]) - editor.deleteLine() - expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() - expect(buffer.lineForRow(3)).toBe ' while(items.length > 0) {' - editor.undo() - expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() - expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];' - - describe ".replaceSelectedText(options, fn)", -> - describe "when no text is selected", -> - it "inserts the text returned from the function at the cursor position", -> - editor.replaceSelectedText {}, -> '123' - expect(buffer.lineForRow(0)).toBe '123var quicksort = function () {' - - editor.setCursorBufferPosition([0]) - editor.replaceSelectedText {selectWordIfEmpty: true}, -> 'var' - expect(buffer.lineForRow(0)).toBe 'var quicksort = function () {' - - editor.setCursorBufferPosition([10]) - editor.replaceSelectedText null, -> '' - expect(buffer.lineForRow(10)).toBe '' - - describe "when text is selected", -> - it "replaces the selected text with the text returned from the function", -> - editor.setSelectedBufferRange([[0, 1], [0, 3]]) - editor.replaceSelectedText {}, -> 'ia' - expect(buffer.lineForRow(0)).toBe 'via quicksort = function () {' - - it "replaces the selected text and selects the replacement text", -> - editor.setSelectedBufferRange([[0, 4], [0, 9]]) - editor.replaceSelectedText {}, -> 'whatnot' - expect(buffer.lineForRow(0)).toBe 'var whatnotsort = function () {' - expect(editor.getSelectedBufferRange()).toEqual [[0, 4], [0, 11]] - - describe ".transpose()", -> - it "swaps two characters", -> - editor.buffer.setText("abc") - editor.setCursorScreenPosition([0, 1]) - editor.transpose() - expect(editor.lineTextForBufferRow(0)).toBe 'bac' - - it "reverses a selection", -> - editor.buffer.setText("xabcz") - editor.setSelectedBufferRange([[0, 1], [0, 4]]) - editor.transpose() - expect(editor.lineTextForBufferRow(0)).toBe 'xcbaz' - - describe ".upperCase()", -> - describe "when there is no selection", -> - it "upper cases the current word", -> - editor.buffer.setText("aBc") - editor.setCursorScreenPosition([0, 1]) - editor.upperCase() - expect(editor.lineTextForBufferRow(0)).toBe 'ABC' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 3]] - - describe "when there is a selection", -> - it "upper cases the current selection", -> - editor.buffer.setText("abc") - editor.setSelectedBufferRange([[0, 0], [0, 2]]) - editor.upperCase() - expect(editor.lineTextForBufferRow(0)).toBe 'ABc' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 2]] - - describe ".lowerCase()", -> - describe "when there is no selection", -> - it "lower cases the current word", -> - editor.buffer.setText("aBC") - editor.setCursorScreenPosition([0, 1]) - editor.lowerCase() - expect(editor.lineTextForBufferRow(0)).toBe 'abc' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 3]] - - describe "when there is a selection", -> - it "lower cases the current selection", -> - editor.buffer.setText("ABC") - editor.setSelectedBufferRange([[0, 0], [0, 2]]) - editor.lowerCase() - expect(editor.lineTextForBufferRow(0)).toBe 'abC' - expect(editor.getSelectedBufferRange()).toEqual [[0, 0], [0, 2]] - - describe '.setTabLength(tabLength)', -> - it 'clips atomic soft tabs to the given tab length', -> - expect(editor.getTabLength()).toBe 2 - expect(editor.clipScreenPosition([5, 1], clipDirection: 'forward')).toEqual([5, 2]) - - editor.setTabLength(6) - expect(editor.getTabLength()).toBe 6 - expect(editor.clipScreenPosition([5, 1], clipDirection: 'forward')).toEqual([5, 6]) - - changeHandler = jasmine.createSpy('changeHandler') - editor.onDidChange(changeHandler) - editor.setTabLength(6) - expect(changeHandler).not.toHaveBeenCalled() - - it 'does not change its tab length when the given tab length is null', -> - editor.setTabLength(4) - editor.setTabLength(null) - expect(editor.getTabLength()).toBe(4) - - describe ".indentLevelForLine(line)", -> - it "returns the indent level when the line has only leading whitespace", -> - expect(editor.indentLevelForLine(" hello")).toBe(2) - expect(editor.indentLevelForLine(" hello")).toBe(1.5) - - it "returns the indent level when the line has only leading tabs", -> - expect(editor.indentLevelForLine("\t\thello")).toBe(2) - - it "returns the indent level based on the character starting the line when the leading whitespace contains both spaces and tabs", -> - expect(editor.indentLevelForLine("\t hello")).toBe(2) - expect(editor.indentLevelForLine(" \thello")).toBe(2) - expect(editor.indentLevelForLine(" \t hello")).toBe(2.5) - expect(editor.indentLevelForLine(" \t \thello")).toBe(4) - expect(editor.indentLevelForLine(" \t \thello")).toBe(4) - expect(editor.indentLevelForLine(" \t \t hello")).toBe(4.5) - - describe "when a better-matched grammar is added to syntax", -> - it "switches to the better-matched grammar and re-tokenizes the buffer", -> - editor.destroy() - - jsGrammar = atom.grammars.selectGrammar('a.js') - atom.grammars.removeGrammar(jsGrammar) - - waitsForPromise -> - atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor = o - - runs -> - expect(editor.getGrammar()).toBe atom.grammars.nullGrammar - expect(editor.tokensForScreenRow(0).length).toBe(1) - - atom.grammars.addGrammar(jsGrammar) - expect(editor.getGrammar()).toBe jsGrammar - expect(editor.tokensForScreenRow(0).length).toBeGreaterThan 1 - - describe "editor.autoIndent", -> - describe "when editor.autoIndent is false (default)", -> - describe "when `indent` is triggered", -> - it "does not auto-indent the line", -> - editor.setCursorBufferPosition([1, 30]) - editor.insertText("\n ") - expect(editor.lineTextForBufferRow(2)).toBe " " - - editor.update({autoIndent: false}) - editor.indent() - expect(editor.lineTextForBufferRow(2)).toBe " " - - describe "when editor.autoIndent is true", -> - beforeEach -> - editor.update({autoIndent: true}) - - describe "when `indent` is triggered", -> - it "auto-indents the line", -> - editor.setCursorBufferPosition([1, 30]) - editor.insertText("\n ") - expect(editor.lineTextForBufferRow(2)).toBe " " - - editor.update({autoIndent: true}) - editor.indent() - expect(editor.lineTextForBufferRow(2)).toBe " " - - describe "when a newline is added", -> - describe "when the line preceding the newline adds a new level of indentation", -> - it "indents the newline to one additional level of indentation beyond the preceding line", -> - editor.setCursorBufferPosition([1, Infinity]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - - describe "when the line preceding the newline doesn't add a level of indentation", -> - it "indents the new line to the same level as the preceding line", -> - editor.setCursorBufferPosition([5, 14]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(6)).toBe editor.indentationForBufferRow(5) - - describe "when the line preceding the newline is a comment", -> - it "maintains the indent of the commented line", -> - editor.setCursorBufferPosition([0, 0]) - editor.insertText(' //') - editor.setCursorBufferPosition([0, Infinity]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(1)).toBe 2 - - describe "when the line preceding the newline contains only whitespace", -> - it "bases the new line's indentation on only the preceding line", -> - editor.setCursorBufferPosition([6, Infinity]) - editor.insertText("\n ") - expect(editor.getCursorBufferPosition()).toEqual([7, 2]) - - editor.insertNewline() - expect(editor.lineTextForBufferRow(8)).toBe(" ") - - it "does not indent the line preceding the newline", -> - editor.setCursorBufferPosition([2, 0]) - editor.insertText(' var this-line-should-be-indented-more\n') - expect(editor.indentationForBufferRow(1)).toBe 1 - - editor.update({autoIndent: true}) - editor.setCursorBufferPosition([2, Infinity]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(1)).toBe 1 - expect(editor.indentationForBufferRow(2)).toBe 1 - - describe "when the cursor is before whitespace", -> - it "retains the whitespace following the cursor on the new line", -> - editor.setText(" var sort = function() {}") - editor.setCursorScreenPosition([0, 12]) - editor.insertNewline() - - expect(buffer.lineForRow(0)).toBe ' var sort =' - expect(buffer.lineForRow(1)).toBe ' function() {}' - expect(editor.getCursorScreenPosition()).toEqual [1, 2] - - describe "when inserted text matches a decrease indent pattern", -> - describe "when the preceding line matches an increase indent pattern", -> - it "decreases the indentation to match that of the preceding line", -> - editor.setCursorBufferPosition([1, Infinity]) - editor.insertText('\n') - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - editor.insertText('}') - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) - - describe "when the preceding line doesn't match an increase indent pattern", -> - it "decreases the indentation to be one level below that of the preceding line", -> - editor.setCursorBufferPosition([3, Infinity]) - editor.insertText('\n ') - expect(editor.indentationForBufferRow(4)).toBe editor.indentationForBufferRow(3) - editor.insertText('}') - expect(editor.indentationForBufferRow(4)).toBe editor.indentationForBufferRow(3) - 1 - - it "doesn't break when decreasing the indentation on a row that has no indentation", -> - editor.setCursorBufferPosition([12, Infinity]) - editor.insertText("\n}; # too many closing brackets!") - expect(editor.lineTextForBufferRow(13)).toBe "}; # too many closing brackets!" - - describe "when inserted text does not match a decrease indent pattern", -> - it "does not decrease the indentation", -> - editor.setCursorBufferPosition([12, 0]) - editor.insertText(' ') - expect(editor.lineTextForBufferRow(12)).toBe ' };' - editor.insertText('\t\t') - expect(editor.lineTextForBufferRow(12)).toBe ' \t\t};' - - describe "when the current line does not match a decrease indent pattern", -> - it "leaves the line unchanged", -> - editor.setCursorBufferPosition([2, 4]) - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - editor.insertText('foo') - expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1 - - describe "atomic soft tabs", -> - it "skips tab-length runs of leading whitespace when moving the cursor", -> - editor.update({tabLength: 4, atomicSoftTabs: true}) - - editor.setCursorScreenPosition([2, 3]) - expect(editor.getCursorScreenPosition()).toEqual [2, 4] - - editor.update({atomicSoftTabs: false}) - editor.setCursorScreenPosition([2, 3]) - expect(editor.getCursorScreenPosition()).toEqual [2, 3] - - editor.update({atomicSoftTabs: true}) - editor.setCursorScreenPosition([2, 3]) - expect(editor.getCursorScreenPosition()).toEqual [2, 4] - - describe ".destroy()", -> - it "destroys marker layers associated with the text editor", -> - buffer.retain() - selectionsMarkerLayerId = editor.selectionsMarkerLayer.id - foldsMarkerLayerId = editor.displayLayer.foldsMarkerLayer.id - editor.destroy() - expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined() - expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined() - buffer.release() - - it "notifies ::onDidDestroy observers when the editor is destroyed", -> - destroyObserverCalled = false - editor.onDidDestroy -> destroyObserverCalled = true - - editor.destroy() - expect(destroyObserverCalled).toBe true - - it "does not blow up when query methods are called afterward", -> - editor.destroy() - editor.getGrammar() - editor.getLastCursor() - editor.lineTextForBufferRow(0) - - it "emits the destroy event after destroying the editor's buffer", -> - events = [] - editor.getBuffer().onDidDestroy -> - expect(editor.isDestroyed()).toBe(true) - events.push('buffer-destroyed') - editor.onDidDestroy -> - expect(buffer.isDestroyed()).toBe(true) - events.push('editor-destroyed') - editor.destroy() - expect(events).toEqual(['buffer-destroyed', 'editor-destroyed']) - - describe ".joinLines()", -> - describe "when no text is selected", -> - describe "when the line below isn't empty", -> - it "joins the line below with the current line separated by a space and moves the cursor to the start of line that was moved up", -> - editor.setCursorBufferPosition([0, Infinity]) - editor.insertText(' ') - editor.setCursorBufferPosition([0]) - editor.joinLines() - expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () { var sort = function(items) {' - expect(editor.getCursorBufferPosition()).toEqual [0, 29] - - describe "when the line below is empty", -> - it "deletes the line below and moves the cursor to the end of the line", -> - editor.setCursorBufferPosition([9]) - editor.joinLines() - expect(editor.lineTextForBufferRow(9)).toBe ' };' - expect(editor.lineTextForBufferRow(10)).toBe ' return sort(Array.apply(this, arguments));' - expect(editor.getCursorBufferPosition()).toEqual [9, 4] - - describe "when the cursor is on the last row", -> - it "does nothing", -> - editor.setCursorBufferPosition([Infinity, Infinity]) - editor.joinLines() - expect(editor.lineTextForBufferRow(12)).toBe '};' - - describe "when the line is empty", -> - it "joins the line below with the current line with no added space", -> - editor.setCursorBufferPosition([10]) - editor.joinLines() - expect(editor.lineTextForBufferRow(10)).toBe 'return sort(Array.apply(this, arguments));' - expect(editor.getCursorBufferPosition()).toEqual [10, 0] - - describe "when text is selected", -> - describe "when the selection does not span multiple lines", -> - it "joins the line below with the current line separated by a space and retains the selected text", -> - editor.setSelectedBufferRange([[0, 1], [0, 3]]) - editor.joinLines() - expect(editor.lineTextForBufferRow(0)).toBe 'var quicksort = function () { var sort = function(items) {' - expect(editor.getSelectedBufferRange()).toEqual [[0, 1], [0, 3]] - - describe "when the selection spans multiple lines", -> - it "joins all selected lines separated by a space and retains the selected text", -> - editor.setSelectedBufferRange([[9, 3], [12, 1]]) - editor.joinLines() - expect(editor.lineTextForBufferRow(9)).toBe ' }; return sort(Array.apply(this, arguments)); };' - expect(editor.getSelectedBufferRange()).toEqual [[9, 3], [9, 49]] - - describe ".duplicateLines()", -> - it "for each selection, duplicates all buffer lines intersected by the selection", -> - editor.foldBufferRow(4) - editor.setCursorBufferPosition([2, 5]) - editor.addSelectionForBufferRange([[3, 0], [8, 0]], preserveFolds: true) - - editor.duplicateLines() - - expect(editor.getTextInBufferRange([[2, 0], [13, 5]])).toBe """ - \ if (items.length <= 1) return items; - if (items.length <= 1) return items; - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - """ - expect(editor.getSelectedBufferRanges()).toEqual [[[3, 5], [3, 5]], [[9, 0], [14, 0]]] - - # folds are also duplicated - expect(editor.isFoldedAtScreenRow(5)).toBe(true) - expect(editor.isFoldedAtScreenRow(7)).toBe(true) - expect(editor.lineTextForScreenRow(7)).toBe " while(items.length > 0) {" + editor.displayLayer.foldCharacter - expect(editor.lineTextForScreenRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));" - - it "duplicates all folded lines for empty selections on lines containing folds", -> - editor.foldBufferRow(4) - editor.setCursorBufferPosition([4, 0]) - - editor.duplicateLines() - - expect(editor.getTextInBufferRange([[2, 0], [11, 5]])).toBe """ - \ if (items.length <= 1) return items; - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); - } - """ - expect(editor.getSelectedBufferRange()).toEqual [[8, 0], [8, 0]] - - it "can duplicate the last line of the buffer", -> - editor.setSelectedBufferRange([[11, 0], [12, 2]]) - editor.duplicateLines() - expect(editor.getTextInBufferRange([[11, 0], [14, 2]])).toBe """ - \ return sort(Array.apply(this, arguments)); - }; - return sort(Array.apply(this, arguments)); - }; - """ - expect(editor.getSelectedBufferRange()).toEqual [[13, 0], [14, 2]] - - it "only duplicates lines containing multiple selections once", -> - editor.setText(""" - aaaaaa - bbbbbb - cccccc - dddddd - """) - editor.setSelectedBufferRanges([ - [[0, 1], [0, 2]], - [[0, 3], [0, 4]], - [[2, 1], [2, 2]], - [[2, 3], [3, 1]], - [[3, 3], [3, 4]], - ]) - editor.duplicateLines() - expect(editor.getText()).toBe(""" - aaaaaa - aaaaaa - bbbbbb - cccccc - dddddd - cccccc - dddddd - """) - expect(editor.getSelectedBufferRanges()).toEqual([ - [[1, 1], [1, 2]], - [[1, 3], [1, 4]], - [[5, 1], [5, 2]], - [[5, 3], [6, 1]], - [[6, 3], [6, 4]], - ]) - - describe "when the editor contains surrogate pair characters", -> - it "correctly backspaces over them", -> - editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') - editor.moveToBottom() - editor.backspace() - expect(editor.getText()).toBe '\uD835\uDF97\uD835\uDF97' - editor.backspace() - expect(editor.getText()).toBe '\uD835\uDF97' - editor.backspace() - expect(editor.getText()).toBe '' - - it "correctly deletes over them", -> - editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') - editor.moveToTop() - editor.delete() - expect(editor.getText()).toBe '\uD835\uDF97\uD835\uDF97' - editor.delete() - expect(editor.getText()).toBe '\uD835\uDF97' - editor.delete() - expect(editor.getText()).toBe '' - - it "correctly moves over them", -> - editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97\n') - editor.moveToTop() - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 4] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 6] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 6] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 4] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe "when the editor contains variation sequence character pairs", -> - it "correctly backspaces over them", -> - editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') - editor.moveToBottom() - editor.backspace() - expect(editor.getText()).toBe '\u2714\uFE0E\u2714\uFE0E' - editor.backspace() - expect(editor.getText()).toBe '\u2714\uFE0E' - editor.backspace() - expect(editor.getText()).toBe '' - - it "correctly deletes over them", -> - editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') - editor.moveToTop() - editor.delete() - expect(editor.getText()).toBe '\u2714\uFE0E\u2714\uFE0E' - editor.delete() - expect(editor.getText()).toBe '\u2714\uFE0E' - editor.delete() - expect(editor.getText()).toBe '' - - it "correctly moves over them", -> - editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E\n') - editor.moveToTop() - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 4] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [0, 6] - editor.moveRight() - expect(editor.getCursorBufferPosition()).toEqual [1, 0] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 6] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 4] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 2] - editor.moveLeft() - expect(editor.getCursorBufferPosition()).toEqual [0, 0] - - describe ".setIndentationForBufferRow", -> - describe "when the editor uses soft tabs but the row has hard tabs", -> - it "only replaces whitespace characters", -> - editor.setSoftWrapped(true) - editor.setText("\t1\n\t2") - editor.setCursorBufferPosition([0, 0]) - editor.setIndentationForBufferRow(0, 2) - expect(editor.getText()).toBe(" 1\n\t2") - - describe "when the indentation level is a non-integer", -> - it "does not throw an exception", -> - editor.setSoftWrapped(true) - editor.setText("\t1\n\t2") - editor.setCursorBufferPosition([0, 0]) - editor.setIndentationForBufferRow(0, 2.1) - expect(editor.getText()).toBe(" 1\n\t2") - - describe "when the editor's grammar has an injection selector", -> - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-text') - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - it "includes the grammar's patterns when the selector matches the current scope in other grammars", -> - waitsForPromise -> - atom.packages.activatePackage('language-hyperlink') - - runs -> - grammar = atom.grammars.selectGrammar("text.js") - {line, tags} = grammar.tokenizeLine("var i; // http://github.com") - - tokens = atom.grammars.decodeTokens(line, tags) - expect(tokens[0].value).toBe "var" - expect(tokens[0].scopes).toEqual ["source.js", "storage.type.var.js"] - - expect(tokens[6].value).toBe "http://github.com" - expect(tokens[6].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"] - - describe "when the grammar is added", -> - it "retokenizes existing buffers that contain tokens that match the injection selector", -> - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - editor.setText("// http://github.com") - - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - ] - - waitsForPromise -> - atom.packages.activatePackage('language-hyperlink') - - runs -> - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - {text: 'http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--markup syntax--underline syntax--link syntax--http syntax--hyperlink']} - ] - - describe "when the grammar is updated", -> - it "retokenizes existing buffers that contain tokens that match the injection selector", -> - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - editor.setText("// SELECT * FROM OCTOCATS") - - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - ] - - waitsForPromise -> - atom.packages.activatePackage('package-with-injection-selector') - - runs -> - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - ] - - waitsForPromise -> - atom.packages.activatePackage('language-sql') - - runs -> - tokens = editor.tokensForScreenRow(0) - expect(tokens).toEqual [ - {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, - {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, - {text: 'SELECT', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, - {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, - {text: '*', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--operator syntax--star syntax--sql']}, - {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, - {text: 'FROM', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, - {text: ' OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} - ] - - describe ".normalizeTabsInBufferRange()", -> - it "normalizes tabs depending on the editor's soft tab/tab length settings", -> - editor.setTabLength(1) - editor.setSoftTabs(true) - editor.setText('\t\t\t') - editor.normalizeTabsInBufferRange([[0, 0], [0, 1]]) - expect(editor.getText()).toBe ' \t\t' - - editor.setTabLength(2) - editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) - expect(editor.getText()).toBe ' ' - - editor.setSoftTabs(false) - editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) - expect(editor.getText()).toBe ' ' - - describe ".pageUp/Down()", -> - it "moves the cursor down one page length", -> - editor.update(autoHeight: false) - element = editor.getElement() - jasmine.attachToDOM(element) - element.style.height = element.component.getLineHeight() * 5 + 'px' - element.measureDimensions() - - expect(editor.getCursorBufferPosition().row).toBe 0 - - editor.pageDown() - expect(editor.getCursorBufferPosition().row).toBe 5 - - editor.pageDown() - expect(editor.getCursorBufferPosition().row).toBe 10 - - editor.pageUp() - expect(editor.getCursorBufferPosition().row).toBe 5 - - editor.pageUp() - expect(editor.getCursorBufferPosition().row).toBe 0 - - describe ".selectPageUp/Down()", -> - it "selects one screen height of text up or down", -> - editor.update(autoHeight: false) - element = editor.getElement() - jasmine.attachToDOM(element) - element.style.height = element.component.getLineHeight() * 5 + 'px' - element.measureDimensions() - - expect(editor.getCursorBufferPosition().row).toBe 0 - - editor.selectPageDown() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [5, 0]]] - - editor.selectPageDown() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [10, 0]]] - - editor.selectPageDown() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [12, 2]]] - - editor.moveToBottom() - editor.selectPageUp() - expect(editor.getSelectedBufferRanges()).toEqual [[[7, 0], [12, 2]]] - - editor.selectPageUp() - expect(editor.getSelectedBufferRanges()).toEqual [[[2, 0], [12, 2]]] - - editor.selectPageUp() - expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [12, 2]]] - - describe "::scrollToScreenPosition(position, [options])", -> - it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", -> - scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll") - editor.onDidRequestAutoscroll(scrollSpy) - - editor.scrollToScreenPosition([8, 20]) - editor.scrollToScreenPosition([8, 20], center: true) - editor.scrollToScreenPosition([8, 20], center: false, reversed: true) - - expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {}) - expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: true}) - expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true}) - - describe "scroll past end", -> - it "returns false by default but can be customized", -> - expect(editor.getScrollPastEnd()).toBe(false) - editor.update({scrollPastEnd: true}) - expect(editor.getScrollPastEnd()).toBe(true) - editor.update({scrollPastEnd: false}) - expect(editor.getScrollPastEnd()).toBe(false) - - it "always returns false when autoHeight is on", -> - editor.update({autoHeight: true, scrollPastEnd: true}) - expect(editor.getScrollPastEnd()).toBe(false) - editor.update({autoHeight: false}) - expect(editor.getScrollPastEnd()).toBe(true) - - describe "auto height", -> - it "returns true by default but can be customized", -> - editor = new TextEditor - expect(editor.getAutoHeight()).toBe(true) - editor.update({autoHeight: false}) - expect(editor.getAutoHeight()).toBe(false) - editor.update({autoHeight: true}) - expect(editor.getAutoHeight()).toBe(true) - editor.destroy() - - describe "auto width", -> - it "returns false by default but can be customized", -> - expect(editor.getAutoWidth()).toBe(false) - editor.update({autoWidth: true}) - expect(editor.getAutoWidth()).toBe(true) - editor.update({autoWidth: false}) - expect(editor.getAutoWidth()).toBe(false) - - describe '.get/setPlaceholderText()', -> - it 'can be created with placeholderText', -> - newEditor = new TextEditor({ - mini: true - placeholderText: 'yep' - }) - expect(newEditor.getPlaceholderText()).toBe 'yep' - - it 'models placeholderText and emits an event when changed', -> - editor.onDidChangePlaceholderText handler = jasmine.createSpy() - - expect(editor.getPlaceholderText()).toBeUndefined() - - editor.setPlaceholderText('OK') - expect(handler).toHaveBeenCalledWith 'OK' - expect(editor.getPlaceholderText()).toBe 'OK' - - describe 'gutters', -> - describe 'the TextEditor constructor', -> - it 'creates a line-number gutter', -> - expect(editor.getGutters().length).toBe 1 - lineNumberGutter = editor.gutterWithName('line-number') - expect(lineNumberGutter.name).toBe 'line-number' - expect(lineNumberGutter.priority).toBe 0 - - describe '::addGutter', -> - it 'can add a gutter', -> - expect(editor.getGutters().length).toBe 1 # line-number gutter - options = - name: 'test-gutter' - priority: 1 - gutter = editor.addGutter options - expect(editor.getGutters().length).toBe 2 - expect(editor.getGutters()[1]).toBe gutter - - it "does not allow a custom gutter with the 'line-number' name.", -> - expect(editor.addGutter.bind(editor, {name: 'line-number'})).toThrow() - - describe '::decorateMarker', -> - [marker] = [] - - beforeEach -> - marker = editor.markBufferRange([[1, 0], [1, 0]]) - - it 'reflects an added decoration when one of its custom gutters is decorated.', -> - gutter = editor.addGutter {'name': 'custom-gutter'} - decoration = gutter.decorateMarker marker, {class: 'custom-class'} - gutterDecorations = editor.getDecorations - type: 'gutter' - gutterName: 'custom-gutter' - class: 'custom-class' - expect(gutterDecorations.length).toBe 1 - expect(gutterDecorations[0]).toBe decoration - - it 'reflects an added decoration when its line-number gutter is decorated.', -> - decoration = editor.gutterWithName('line-number').decorateMarker marker, {class: 'test-class'} - gutterDecorations = editor.getDecorations - type: 'line-number' - gutterName: 'line-number' - class: 'test-class' - expect(gutterDecorations.length).toBe 1 - expect(gutterDecorations[0]).toBe decoration - - describe '::observeGutters', -> - [payloads, callback] = [] - - beforeEach -> - payloads = [] - callback = (payload) -> - payloads.push(payload) - - it 'calls the callback immediately with each existing gutter, and with each added gutter after that.', -> - lineNumberGutter = editor.gutterWithName('line-number') - editor.observeGutters(callback) - expect(payloads).toEqual [lineNumberGutter] - gutter1 = editor.addGutter({name: 'test-gutter-1'}) - expect(payloads).toEqual [lineNumberGutter, gutter1] - gutter2 = editor.addGutter({name: 'test-gutter-2'}) - expect(payloads).toEqual [lineNumberGutter, gutter1, gutter2] - - it 'does not call the callback when a gutter is removed.', -> - gutter = editor.addGutter({name: 'test-gutter'}) - editor.observeGutters(callback) - payloads = [] - gutter.destroy() - expect(payloads).toEqual [] - - it 'does not call the callback after the subscription has been disposed.', -> - subscription = editor.observeGutters(callback) - payloads = [] - subscription.dispose() - editor.addGutter({name: 'test-gutter'}) - expect(payloads).toEqual [] - - describe '::onDidAddGutter', -> - [payloads, callback] = [] - - beforeEach -> - payloads = [] - callback = (payload) -> - payloads.push(payload) - - it 'calls the callback with each newly-added gutter, but not with existing gutters.', -> - editor.onDidAddGutter(callback) - expect(payloads).toEqual [] - gutter = editor.addGutter({name: 'test-gutter'}) - expect(payloads).toEqual [gutter] - - it 'does not call the callback after the subscription has been disposed.', -> - subscription = editor.onDidAddGutter(callback) - payloads = [] - subscription.dispose() - editor.addGutter({name: 'test-gutter'}) - expect(payloads).toEqual [] - - describe '::onDidRemoveGutter', -> - [payloads, callback] = [] - - beforeEach -> - payloads = [] - callback = (payload) -> - payloads.push(payload) - - it 'calls the callback when a gutter is removed.', -> - gutter = editor.addGutter({name: 'test-gutter'}) - editor.onDidRemoveGutter(callback) - expect(payloads).toEqual [] - gutter.destroy() - expect(payloads).toEqual ['test-gutter'] - - it 'does not call the callback after the subscription has been disposed.', -> - gutter = editor.addGutter({name: 'test-gutter'}) - subscription = editor.onDidRemoveGutter(callback) - subscription.dispose() - gutter.destroy() - expect(payloads).toEqual [] - - describe "decorations", -> - describe "::decorateMarker", -> - it "includes the decoration in the object returned from ::decorationsStateForScreenRowRange", -> - marker = editor.markBufferRange([[2, 4], [6, 8]]) - decoration = editor.decorateMarker(marker, type: 'highlight', class: 'foo') - expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual { - properties: {type: 'highlight', class: 'foo'} - screenRange: marker.getScreenRange(), - bufferRange: marker.getBufferRange(), - rangeIsReversed: false - } - - it "does not throw errors after the marker's containing layer is destroyed", -> - layer = editor.addMarkerLayer() - marker = layer.markBufferRange([[2, 4], [6, 8]]) - decoration = editor.decorateMarker(marker, type: 'highlight', class: 'foo') - layer.destroy() - editor.decorationsStateForScreenRowRange(0, 5) - - describe "::decorateMarkerLayer", -> - it "based on the markers in the layer, includes multiple decoration objects with the same properties and different ranges in the object returned from ::decorationsStateForScreenRowRange", -> - layer1 = editor.getBuffer().addMarkerLayer() - marker1 = layer1.markRange([[2, 4], [6, 8]]) - marker2 = layer1.markRange([[11, 0], [11, 12]]) - layer2 = editor.getBuffer().addMarkerLayer() - marker3 = layer2.markRange([[8, 0], [9, 0]]) - - layer1Decoration1 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'foo') - layer1Decoration2 = editor.decorateMarkerLayer(layer1, type: 'highlight', class: 'bar') - layer2Decoration = editor.decorateMarkerLayer(layer2, type: 'highlight', class: 'baz') - - decorationState = editor.decorationsStateForScreenRowRange(0, 13) - - expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'foo'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toEqual { - properties: {type: 'highlight', class: 'foo'}, - screenRange: marker2.getRange(), - bufferRange: marker2.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker2.getRange(), - bufferRange: marker2.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual { - properties: {type: 'highlight', class: 'baz'}, - screenRange: marker3.getRange(), - bufferRange: marker3.getRange(), - rangeIsReversed: false - } - - layer1Decoration1.destroy() - - decorationState = editor.decorationsStateForScreenRowRange(0, 12) - expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toBeUndefined() - expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toBeUndefined() - expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker2.getRange(), - bufferRange: marker2.getRange(), - rangeIsReversed: false - } - expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual { - properties: {type: 'highlight', class: 'baz'}, - screenRange: marker3.getRange(), - bufferRange: marker3.getRange(), - rangeIsReversed: false - } - - layer1Decoration2.setPropertiesForMarker(marker1, {type: 'highlight', class: 'quux'}) - decorationState = editor.decorationsStateForScreenRowRange(0, 12) - expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'quux'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - - layer1Decoration2.setPropertiesForMarker(marker1, null) - decorationState = editor.decorationsStateForScreenRowRange(0, 12) - expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual { - properties: {type: 'highlight', class: 'bar'}, - screenRange: marker1.getRange(), - bufferRange: marker1.getRange(), - rangeIsReversed: false - } - - describe "invisibles", -> - beforeEach -> - editor.update({showInvisibles: true}) - - it "substitutes invisible characters according to the given rules", -> - previousLineText = editor.lineTextForScreenRow(0) - editor.update({invisibles: {eol: '?'}}) - expect(editor.lineTextForScreenRow(0)).not.toBe(previousLineText) - expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true) - expect(editor.getInvisibles()).toEqual(eol: '?') - - it "does not use invisibles if showInvisibles is set to false", -> - editor.update({invisibles: {eol: '?'}}) - expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true) - - editor.update({showInvisibles: false}) - expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(false) - - describe "indent guides", -> - it "shows indent guides when `editor.showIndentGuide` is set to true and the editor is not mini", -> - editor.setText(" foo") - editor.setTabLength(2) - - editor.update({showIndentGuide: false}) - expect(editor.tokensForScreenRow(0)).toEqual [ - {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, - {text: 'foo', scopes: ['syntax--source syntax--js']} - ] - - editor.update({showIndentGuide: true}) - expect(editor.tokensForScreenRow(0)).toEqual [ - {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace indent-guide']}, - {text: 'foo', scopes: ['syntax--source syntax--js']} - ] - - editor.setMini(true) - expect(editor.tokensForScreenRow(0)).toEqual [ - {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, - {text: 'foo', scopes: ['syntax--source syntax--js']} - ] - - describe "when the editor is constructed with the grammar option set", -> - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - it "sets the grammar", -> - editor = new TextEditor({grammar: atom.grammars.grammarForScopeName('source.coffee')}) - expect(editor.getGrammar().name).toBe 'CoffeeScript' - - describe "softWrapAtPreferredLineLength", -> - it "soft wraps the editor at the preferred line length unless the editor is narrower or the editor is mini", -> - editor.update({ - editorWidthInChars: 30 - softWrapped: true - softWrapAtPreferredLineLength: true - preferredLineLength: 20 - }) - - expect(editor.lineTextForScreenRow(0)).toBe 'var quicksort = ' - - editor.update({editorWidthInChars: 10}) - expect(editor.lineTextForScreenRow(0)).toBe 'var ' - - editor.update({mini: true}) - expect(editor.lineTextForScreenRow(0)).toBe 'var quicksort = function () {' - - describe "softWrapHangingIndentLength", -> - it "controls how much extra indentation is applied to soft-wrapped lines", -> - editor.setText('123456789') - editor.update({ - editorWidthInChars: 8 - softWrapped: true - softWrapHangingIndentLength: 2 - }) - expect(editor.lineTextForScreenRow(1)).toEqual ' 9' - - editor.update({softWrapHangingIndentLength: 4}) - expect(editor.lineTextForScreenRow(1)).toEqual ' 9' - - describe "::getElement", -> - it "returns an element", -> - expect(editor.getElement() instanceof HTMLElement).toBe(true) - - describe 'setMaxScreenLineLength', -> - it "sets the maximum line length in the editor before soft wrapping is forced", -> - expect(editor.getSoftWrapColumn()).toBe(500) - editor.update({ - maxScreenLineLength: 1500 - }) - expect(editor.getSoftWrapColumn()).toBe(1500) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index d10efa695..b2cc41ab7 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -1,9 +1,6655 @@ +const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers') + const fs = require('fs') +const path = require('path') const temp = require('temp').track() -const {Point, Range} = require('text-buffer') -const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') -const TextBuffer = require('text-buffer') +const dedent = require('dedent') +const clipboard = require('../src/safe-clipboard') const TextEditor = require('../src/text-editor') +const TextBuffer = require('text-buffer') + +describe('TextEditor', () => { + let buffer, editor, lineLengths + + beforeEach(async () => { + editor = await atom.workspace.open('sample.js') + buffer = editor.buffer + editor.update({autoIndent: false}) + lineLengths = buffer.getLines().map(line => line.length) + await atom.packages.activatePackage('language-javascript') + }) + + describe('when the editor is deserialized', () => { + it('restores selections and folds based on markers in the buffer', async () => { + editor.setSelectedBufferRange([[1, 2], [3, 4]]) + editor.addSelectionForBufferRange([[5, 6], [7, 5]], {reversed: true}) + editor.foldBufferRow(4) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + + const buffer2 = await TextBuffer.deserialize(editor.buffer.serialize()) + const editor2 = TextEditor.deserialize(editor.serialize(), { + assert: atom.assert, + textEditors: atom.textEditors, + project: {bufferForIdSync () { return buffer2 }} + }) + + expect(editor2.id).toBe(editor.id) + expect(editor2.getBuffer().getPath()).toBe(editor.getBuffer().getPath()) + expect(editor2.getSelectedBufferRanges()).toEqual([[[1, 2], [3, 4]], [[5, 6], [7, 5]]]) + expect(editor2.getSelections()[1].isReversed()).toBeTruthy() + expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() + editor2.destroy() + }) + + it("restores the editor's layout configuration", async () => { + editor.update({ + softTabs: true, + atomicSoftTabs: false, + tabLength: 12, + softWrapped: true, + softWrapAtPreferredLineLength: true, + softWrapHangingIndentLength: 8, + invisibles: {space: 'S'}, + showInvisibles: true, + editorWidthInChars: 120 + }) + + // Force buffer and display layer to be deserialized as well, rather than + // reusing the same buffer instance + const buffer2 = await TextBuffer.deserialize(editor.buffer.serialize()) + const editor2 = TextEditor.deserialize(editor.serialize(), { + assert: atom.assert, + textEditors: atom.textEditors, + project: {bufferForIdSync () { return buffer2 }} + }) + + expect(editor2.getSoftTabs()).toBe(editor.getSoftTabs()) + expect(editor2.hasAtomicSoftTabs()).toBe(editor.hasAtomicSoftTabs()) + expect(editor2.getTabLength()).toBe(editor.getTabLength()) + expect(editor2.getSoftWrapColumn()).toBe(editor.getSoftWrapColumn()) + expect(editor2.getSoftWrapHangingIndentLength()).toBe(editor.getSoftWrapHangingIndentLength()) + expect(editor2.getInvisibles()).toEqual(editor.getInvisibles()) + expect(editor2.getEditorWidthInChars()).toBe(editor.getEditorWidthInChars()) + expect(editor2.displayLayer.tabLength).toBe(editor2.getTabLength()) + expect(editor2.displayLayer.softWrapColumn).toBe(editor2.getSoftWrapColumn()) + }) + + it('ignores buffers with retired IDs', () => { + const editor2 = TextEditor.deserialize(editor.serialize(), { + assert: atom.assert, + textEditors: atom.textEditors, + project: {bufferForIdSync () { return null }} + }) + + expect(editor2).toBeNull() + }) + }) + + describe('when the editor is constructed with the largeFileMode option set to true', () => { + it("loads the editor but doesn't tokenize", async () => { + editor = await atom.workspace.openTextFile('sample.js', {largeFileMode: true}) + buffer = editor.getBuffer() + expect(editor.lineTextForScreenRow(0)).toBe(buffer.lineForRow(0)) + expect(editor.tokensForScreenRow(0).length).toBe(1) + expect(editor.tokensForScreenRow(1).length).toBe(2) // soft tab + expect(editor.lineTextForScreenRow(12)).toBe(buffer.lineForRow(12)) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + editor.insertText('hey"') + expect(editor.tokensForScreenRow(0).length).toBe(1) + expect(editor.tokensForScreenRow(1).length).toBe(2) + }) + }) + + describe('.copy()', () => { + it('returns a different editor with the same initial state', () => { + expect(editor.getAutoHeight()).toBeFalsy() + expect(editor.getAutoWidth()).toBeFalsy() + expect(editor.getShowCursorOnSelection()).toBeTruthy() + + const element = editor.getElement() + element.setHeight(100) + element.setWidth(100) + jasmine.attachToDOM(element) + + editor.update({showCursorOnSelection: false}) + editor.setSelectedBufferRange([[1, 2], [3, 4]]) + editor.addSelectionForBufferRange([[5, 6], [7, 8]], {reversed: true}) + editor.setScrollTopRow(3) + expect(editor.getScrollTopRow()).toBe(3) + editor.setScrollLeftColumn(4) + expect(editor.getScrollLeftColumn()).toBe(4) + editor.foldBufferRow(4) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + + const editor2 = editor.copy() + const element2 = editor2.getElement() + element2.setHeight(100) + element2.setWidth(100) + jasmine.attachToDOM(element2) + expect(editor2.id).not.toBe(editor.id) + expect(editor2.getSelectedBufferRanges()).toEqual(editor.getSelectedBufferRanges()) + expect(editor2.getSelections()[1].isReversed()).toBeTruthy() + expect(editor2.getScrollTopRow()).toBe(3) + expect(editor2.getScrollLeftColumn()).toBe(4) + expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor2.getAutoWidth()).toBe(false) + expect(editor2.getAutoHeight()).toBe(false) + expect(editor2.getShowCursorOnSelection()).toBeFalsy() + + // editor2 can now diverge from its origin edit session + editor2.getLastSelection().setBufferRange([[2, 1], [4, 3]]) + expect(editor2.getSelectedBufferRanges()).not.toEqual(editor.getSelectedBufferRanges()) + editor2.unfoldBufferRow(4) + expect(editor2.isFoldedAtBufferRow(4)).not.toBe(editor.isFoldedAtBufferRow(4)) + }) + }) + + describe('.update()', () => { + it('updates the editor with the supplied config parameters', () => { + let changeSpy + const { element } = editor // force element initialization + element.setUpdatedSynchronously(false) + editor.update({showInvisibles: true}) + editor.onDidChange(changeSpy = jasmine.createSpy('onDidChange')) + + const returnedPromise = editor.update({ + tabLength: 6, + softTabs: false, + softWrapped: true, + editorWidthInChars: 40, + showInvisibles: false, + mini: false, + lineNumberGutterVisible: false, + scrollPastEnd: true, + autoHeight: false, + maxScreenLineLength: 1000 + }) + + expect(returnedPromise).toBe(element.component.getNextUpdatePromise()) + expect(changeSpy.callCount).toBe(1) + expect(editor.getTabLength()).toBe(6) + expect(editor.getSoftTabs()).toBe(false) + expect(editor.isSoftWrapped()).toBe(true) + expect(editor.getEditorWidthInChars()).toBe(40) + expect(editor.getInvisibles()).toEqual({}) + expect(editor.isMini()).toBe(false) + expect(editor.isLineNumberGutterVisible()).toBe(false) + expect(editor.getScrollPastEnd()).toBe(true) + expect(editor.getAutoHeight()).toBe(false) + }) + }) + + describe('title', () => { + describe('.getTitle()', () => { + it("uses the basename of the buffer's path as its title, or 'untitled' if the path is undefined", () => { + expect(editor.getTitle()).toBe('sample.js') + buffer.setPath(undefined) + expect(editor.getTitle()).toBe('untitled') + }) + }) + + describe('.getLongTitle()', () => { + it('returns file name when there is no opened file with identical name', () => { + expect(editor.getLongTitle()).toBe('sample.js') + buffer.setPath(undefined) + expect(editor.getLongTitle()).toBe('untitled') + }) + + it("returns '' when opened files have identical file names", async () => { + const editor1 = await atom.workspace.open(path.join('sample-theme-1', 'readme')) + const editor2 = await atom.workspace.open(path.join('sample-theme-2', 'readme')) + expect(editor1.getLongTitle()).toBe('readme \u2014 sample-theme-1') + expect(editor2.getLongTitle()).toBe('readme \u2014 sample-theme-2') + }) + + it("returns '' when opened files have identical file names in subdirectories", async () => { + const path1 = path.join('sample-theme-1', 'src', 'js') + const path2 = path.join('sample-theme-2', 'src', 'js') + const editor1 = await atom.workspace.open(path.join(path1, 'main.js')) + const editor2 = await atom.workspace.open(path.join(path2, 'main.js')) + expect(editor1.getLongTitle()).toBe(`main.js \u2014 ${path1}`) + expect(editor2.getLongTitle()).toBe(`main.js \u2014 ${path2}`) + }) + + it("returns '' when opened files have identical file and same parent dir name", async () => { + const editor1 = await atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')) + const editor2 = await atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'plugin', 'main.js')) + expect(editor1.getLongTitle()).toBe('main.js \u2014 js') + expect(editor2.getLongTitle()).toBe(`main.js \u2014 ${path.join('js', 'plugin')}`) + }) + }) + + it('notifies ::onDidChangeTitle observers when the underlying buffer path changes', () => { + const observed = [] + editor.onDidChangeTitle(title => observed.push(title)) + + buffer.setPath('/foo/bar/baz.txt') + buffer.setPath(undefined) + + expect(observed).toEqual(['baz.txt', 'untitled']) + }) + }) + + describe('path', () => { + it('notifies ::onDidChangePath observers when the underlying buffer path changes', () => { + const observed = [] + editor.onDidChangePath(filePath => observed.push(filePath)) + + buffer.setPath(__filename) + buffer.setPath(undefined) + + expect(observed).toEqual([__filename, undefined]) + }) + }) + + describe('encoding', () => { + it('notifies ::onDidChangeEncoding observers when the editor encoding changes', () => { + const observed = [] + editor.onDidChangeEncoding(encoding => observed.push(encoding)) + + editor.setEncoding('utf16le') + editor.setEncoding('utf16le') + editor.setEncoding('utf16be') + editor.setEncoding() + editor.setEncoding() + + expect(observed).toEqual(['utf16le', 'utf16be', 'utf8']) + }) + }) + + describe('cursor', () => { + describe('.getLastCursor()', () => { + it('returns the most recently created cursor', () => { + editor.addCursorAtScreenPosition([1, 0]) + const lastCursor = editor.addCursorAtScreenPosition([2, 0]) + expect(editor.getLastCursor()).toBe(lastCursor) + }) + + it('creates a new cursor at (0, 0) if the last cursor has been destroyed', () => { + editor.getLastCursor().destroy() + expect(editor.getLastCursor().getBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.getCursors()', () => { + it('creates a new cursor at (0, 0) if the last cursor has been destroyed', () => { + editor.getLastCursor().destroy() + expect(editor.getCursors()[0].getBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('when the cursor moves', () => { + it('clears a goal column established by vertical movement', () => { + editor.setText('b') + editor.setCursorBufferPosition([0, 0]) + editor.insertNewline() + editor.moveUp() + editor.insertText('a') + editor.moveDown() + expect(editor.getCursorBufferPosition()).toEqual([1, 1]) + }) + + it('emits an event with the old position, new position, and the cursor that moved', () => { + const cursorCallback = jasmine.createSpy('cursor-changed-position') + const editorCallback = jasmine.createSpy('editor-changed-cursor-position') + + editor.getLastCursor().onDidChangePosition(cursorCallback) + editor.onDidChangeCursorPosition(editorCallback) + + editor.setCursorBufferPosition([2, 4]) + + expect(editorCallback).toHaveBeenCalled() + expect(cursorCallback).toHaveBeenCalled() + const eventObject = editorCallback.mostRecentCall.args[0] + expect(cursorCallback.mostRecentCall.args[0]).toEqual(eventObject) + + expect(eventObject.oldBufferPosition).toEqual([0, 0]) + expect(eventObject.oldScreenPosition).toEqual([0, 0]) + expect(eventObject.newBufferPosition).toEqual([2, 4]) + expect(eventObject.newScreenPosition).toEqual([2, 4]) + expect(eventObject.cursor).toBe(editor.getLastCursor()) + }) + }) + + describe('.setCursorScreenPosition(screenPosition)', () => { + it('clears a goal column established by vertical movement', () => { + // set a goal column by moving down + editor.setCursorScreenPosition({row: 3, column: lineLengths[3]}) + editor.moveDown() + expect(editor.getCursorScreenPosition().column).not.toBe(6) + + // clear the goal column by explicitly setting the cursor position + editor.setCursorScreenPosition([4, 6]) + expect(editor.getCursorScreenPosition().column).toBe(6) + + editor.moveDown() + expect(editor.getCursorScreenPosition().column).toBe(6) + }) + + it('merges multiple cursors', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([0, 1]) + const [cursor1, cursor2] = editor.getCursors() + editor.setCursorScreenPosition([4, 7]) + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursors()).toEqual([cursor1]) + expect(editor.getCursorScreenPosition()).toEqual([4, 7]) + }) + + describe('when soft-wrap is enabled and code is folded', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(50) + editor.foldBufferRowRange(2, 3) + }) + + it('positions the cursor at the buffer position that corresponds to the given screen position', () => { + editor.setCursorScreenPosition([9, 0]) + expect(editor.getCursorBufferPosition()).toEqual([8, 11]) + }) + }) + }) + + describe('.moveUp()', () => { + it('moves the cursor up', () => { + editor.setCursorScreenPosition([2, 2]) + editor.moveUp() + expect(editor.getCursorScreenPosition()).toEqual([1, 2]) + }) + + it('retains the goal column across lines of differing length', () => { + expect(lineLengths[6]).toBeGreaterThan(32) + editor.setCursorScreenPosition({row: 6, column: 32}) + + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[5]) + + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[4]) + + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(32) + }) + + describe('when the cursor is on the first line', () => { + it('moves the cursor to the beginning of the line, but retains the goal column', () => { + editor.setCursorScreenPosition([0, 4]) + editor.moveUp() + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual([1, 4]) + }) + }) + + describe('when there is a selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[4, 9], [5, 10]])) + + it('moves above the selection', () => { + const cursor = editor.getLastCursor() + editor.moveUp() + expect(cursor.getBufferPosition()).toEqual([3, 9]) + }) + }) + + it('merges cursors when they overlap', () => { + editor.addCursorAtScreenPosition([1, 0]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveUp() + expect(editor.getCursors()).toEqual([cursor1]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + }) + + describe('when the cursor was moved down from the beginning of an indented soft-wrapped line', () => { + it('moves to the beginning of the previous line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(50) + + editor.setCursorScreenPosition([3, 0]) + editor.moveDown() + editor.moveDown() + editor.moveUp() + expect(editor.getCursorScreenPosition()).toEqual([4, 4]) + }) + }) + }) + + describe('.moveDown()', () => { + it('moves the cursor down', () => { + editor.setCursorScreenPosition([2, 2]) + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual([3, 2]) + }) + + it('retains the goal column across lines of differing length', () => { + editor.setCursorScreenPosition({row: 3, column: lineLengths[3]}) + + editor.moveDown() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[4]) + + editor.moveDown() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[5]) + + editor.moveDown() + expect(editor.getCursorScreenPosition().column).toBe(lineLengths[3]) + }) + + describe('when the cursor is on the last line', () => { + it('moves the cursor to the end of line, but retains the goal column when moving back up', () => { + const lastLineIndex = buffer.getLines().length - 1 + const lastLine = buffer.lineForRow(lastLineIndex) + expect(lastLine.length).toBeGreaterThan(0) + + editor.setCursorScreenPosition({row: lastLineIndex, column: editor.getTabLength()}) + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual({row: lastLineIndex, column: lastLine.length}) + + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(editor.getTabLength()) + }) + + it('retains a goal column of 0 when moving back up', () => { + const lastLineIndex = buffer.getLines().length - 1 + const lastLine = buffer.lineForRow(lastLineIndex) + expect(lastLine.length).toBeGreaterThan(0) + + editor.setCursorScreenPosition({row: lastLineIndex, column: 0}) + editor.moveDown() + editor.moveUp() + expect(editor.getCursorScreenPosition().column).toBe(0) + }) + }) + + describe('when the cursor is at the beginning of an indented soft-wrapped line', () => { + it("moves to the beginning of the line's continuation on the next screen row", () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(50) + + editor.setCursorScreenPosition([3, 0]) + editor.moveDown() + expect(editor.getCursorScreenPosition()).toEqual([4, 4]) + }) + }) + + describe('when there is a selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[4, 9], [5, 10]])) + + it('moves below the selection', () => { + const cursor = editor.getLastCursor() + editor.moveDown() + expect(cursor.getBufferPosition()).toEqual([6, 10]) + }) + }) + + it('merges cursors when they overlap', () => { + editor.setCursorScreenPosition([12, 2]) + editor.addCursorAtScreenPosition([11, 2]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveDown() + expect(editor.getCursors()).toEqual([cursor1]) + expect(cursor1.getBufferPosition()).toEqual([12, 2]) + }) + }) + + describe('.moveLeft()', () => { + it('moves the cursor by one column to the left', () => { + editor.setCursorScreenPosition([1, 8]) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual([1, 7]) + }) + + it('moves the cursor by n columns to the left', () => { + editor.setCursorScreenPosition([1, 8]) + editor.moveLeft(4) + expect(editor.getCursorScreenPosition()).toEqual([1, 4]) + }) + + it('moves the cursor by two rows up when the columnCount is longer than an entire line', () => { + editor.setCursorScreenPosition([2, 2]) + editor.moveLeft(34) + expect(editor.getCursorScreenPosition()).toEqual([0, 29]) + }) + + it('moves the cursor to the beginning columnCount is longer than the position in the buffer', () => { + editor.setCursorScreenPosition([1, 0]) + editor.moveLeft(100) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + }) + + describe('when the cursor is in the first column', () => { + describe('when there is a previous line', () => { + it('wraps to the end of the previous line', () => { + editor.setCursorScreenPosition({row: 1, column: 0}) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual({row: 0, column: buffer.lineForRow(0).length}) + }) + + it('moves the cursor by one row up and n columns to the left', () => { + editor.setCursorScreenPosition([1, 0]) + editor.moveLeft(4) + expect(editor.getCursorScreenPosition()).toEqual([0, 26]) + }) + }) + + describe('when the next line is empty', () => { + it('wraps to the beginning of the previous line', () => { + editor.setCursorScreenPosition([11, 0]) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual([10, 0]) + }) + }) + + describe('when line is wrapped and follow previous line indentation', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(50) + }) + + it('wraps to the end of the previous line', () => { + editor.setCursorScreenPosition([4, 4]) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual([3, 46]) + }) + }) + + describe('when the cursor is on the first line', () => { + it('remains in the same position (0,0)', () => { + editor.setCursorScreenPosition({row: 0, column: 0}) + editor.moveLeft() + expect(editor.getCursorScreenPosition()).toEqual({row: 0, column: 0}) + }) + + it('remains in the same position (0,0) when columnCount is specified', () => { + editor.setCursorScreenPosition([0, 0]) + editor.moveLeft(4) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + }) + }) + }) + + describe('when softTabs is enabled and the cursor is preceded by leading whitespace', () => { + it('skips tabLength worth of whitespace at a time', () => { + editor.setCursorBufferPosition([5, 6]) + + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([5, 4]) + }) + }) + + describe('when there is a selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[5, 22], [5, 27]])) + + it('moves to the left of the selection', () => { + const cursor = editor.getLastCursor() + editor.moveLeft() + expect(cursor.getBufferPosition()).toEqual([5, 22]) + + editor.moveLeft() + expect(cursor.getBufferPosition()).toEqual([5, 21]) + }) + }) + + it('merges cursors when they overlap', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([0, 1]) + + const [cursor1, cursor2] = editor.getCursors() + editor.moveLeft() + expect(editor.getCursors()).toEqual([cursor1]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.moveRight()', () => { + it('moves the cursor by one column to the right', () => { + editor.setCursorScreenPosition([3, 3]) + editor.moveRight() + expect(editor.getCursorScreenPosition()).toEqual([3, 4]) + }) + + it('moves the cursor by n columns to the right', () => { + editor.setCursorScreenPosition([3, 7]) + editor.moveRight(4) + expect(editor.getCursorScreenPosition()).toEqual([3, 11]) + }) + + it('moves the cursor by two rows down when the columnCount is longer than an entire line', () => { + editor.setCursorScreenPosition([0, 29]) + editor.moveRight(34) + expect(editor.getCursorScreenPosition()).toEqual([2, 2]) + }) + + it('moves the cursor to the end of the buffer when columnCount is longer than the number of characters following the cursor position', () => { + editor.setCursorScreenPosition([11, 5]) + editor.moveRight(100) + expect(editor.getCursorScreenPosition()).toEqual([12, 2]) + }) + + describe('when the cursor is on the last column of a line', () => { + describe('when there is a subsequent line', () => { + it('wraps to the beginning of the next line', () => { + editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) + editor.moveRight() + expect(editor.getCursorScreenPosition()).toEqual([1, 0]) + }) + + it('moves the cursor by one row down and n columns to the right', () => { + editor.setCursorScreenPosition([0, buffer.lineForRow(0).length]) + editor.moveRight(4) + expect(editor.getCursorScreenPosition()).toEqual([1, 3]) + }) + }) + + describe('when the next line is empty', () => { + it('wraps to the beginning of the next line', () => { + editor.setCursorScreenPosition([9, 4]) + editor.moveRight() + expect(editor.getCursorScreenPosition()).toEqual([10, 0]) + }) + }) + + describe('when the cursor is on the last line', () => { + it('remains in the same position', () => { + const lastLineIndex = buffer.getLines().length - 1 + const lastLine = buffer.lineForRow(lastLineIndex) + expect(lastLine.length).toBeGreaterThan(0) + + const lastPosition = {row: lastLineIndex, column: lastLine.length} + editor.setCursorScreenPosition(lastPosition) + editor.moveRight() + + expect(editor.getCursorScreenPosition()).toEqual(lastPosition) + }) + }) + }) + + describe('when there is a selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[5, 22], [5, 27]])) + + it('moves to the left of the selection', () => { + const cursor = editor.getLastCursor() + editor.moveRight() + expect(cursor.getBufferPosition()).toEqual([5, 27]) + + editor.moveRight() + expect(cursor.getBufferPosition()).toEqual([5, 28]) + }) + }) + + it('merges cursors when they overlap', () => { + editor.setCursorScreenPosition([12, 2]) + editor.addCursorAtScreenPosition([12, 1]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveRight() + expect(editor.getCursors()).toEqual([cursor1]) + expect(cursor1.getBufferPosition()).toEqual([12, 2]) + }) + }) + + describe('.moveToTop()', () => { + it('moves the cursor to the top of the buffer', () => { + editor.setCursorScreenPosition([11, 1]) + editor.addCursorAtScreenPosition([12, 0]) + editor.moveToTop() + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.moveToBottom()', () => { + it('moves the cursor to the bottom of the buffer', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([1, 0]) + editor.moveToBottom() + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursorBufferPosition()).toEqual([12, 2]) + }) + }) + + describe('.moveToBeginningOfScreenLine()', () => { + describe('when soft wrap is on', () => { + it('moves cursor to the beginning of the screen line', () => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([1, 2]) + editor.moveToBeginningOfScreenLine() + const cursor = editor.getLastCursor() + expect(cursor.getScreenPosition()).toEqual([1, 0]) + }) + }) + + describe('when soft wrap is off', () => { + it('moves cursor to the beginning of the line', () => { + editor.setCursorScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 7]) + editor.moveToBeginningOfScreenLine() + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + }) + }) + }) + + describe('.moveToEndOfScreenLine()', () => { + describe('when soft wrap is on', () => { + it('moves cursor to the beginning of the screen line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([1, 2]) + editor.moveToEndOfScreenLine() + const cursor = editor.getLastCursor() + expect(cursor.getScreenPosition()).toEqual([1, 9]) + }) + }) + + describe('when soft wrap is off', () => { + it('moves cursor to the end of line', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([1, 0]) + editor.moveToEndOfScreenLine() + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 29]) + expect(cursor2.getBufferPosition()).toEqual([1, 30]) + }) + }) + }) + + describe('.moveToBeginningOfLine()', () => { + it('moves cursor to the beginning of the buffer line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([1, 2]) + editor.moveToBeginningOfLine() + const cursor = editor.getLastCursor() + expect(cursor.getScreenPosition()).toEqual([0, 0]) + }) + }) + + describe('.moveToEndOfLine()', () => { + it('moves cursor to the end of the buffer line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([0, 2]) + editor.moveToEndOfLine() + const cursor = editor.getLastCursor() + expect(cursor.getScreenPosition()).toEqual([4, 4]) + }) + }) + + describe('.moveToFirstCharacterOfLine()', () => { + describe('when soft wrap is on', () => { + it("moves to the first character of the current screen line or the beginning of the screen line if it's already on the first character", () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(10) + editor.setCursorScreenPosition([2, 5]) + editor.addCursorAtScreenPosition([8, 7]) + + editor.moveToFirstCharacterOfLine() + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getScreenPosition()).toEqual([2, 0]) + expect(cursor2.getScreenPosition()).toEqual([8, 2]) + + editor.moveToFirstCharacterOfLine() + expect(cursor1.getScreenPosition()).toEqual([2, 0]) + expect(cursor2.getScreenPosition()).toEqual([8, 2]) + }) + }) + + describe('when soft wrap is off', () => { + it("moves to the first character of the current line or the beginning of the line if it's already on the first character", () => { + editor.setCursorScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 7]) + + editor.moveToFirstCharacterOfLine() + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 2]) + + editor.moveToFirstCharacterOfLine() + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + }) + + it('moves to the beginning of the line if it only contains whitespace ', () => { + editor.setText('first\n \nthird') + editor.setCursorScreenPosition([1, 2]) + editor.moveToFirstCharacterOfLine() + const cursor = editor.getLastCursor() + expect(cursor.getBufferPosition()).toEqual([1, 0]) + }) + + describe('when invisible characters are enabled with soft tabs', () => { + it('moves to the first character of the current line without being confused by the invisible characters', () => { + editor.update({showInvisibles: true}) + editor.setCursorScreenPosition([1, 7]) + editor.moveToFirstCharacterOfLine() + expect(editor.getCursorBufferPosition()).toEqual([1, 2]) + editor.moveToFirstCharacterOfLine() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + }) + }) + + describe('when invisible characters are enabled with hard tabs', () => { + it('moves to the first character of the current line without being confused by the invisible characters', () => { + editor.update({showInvisibles: true}) + buffer.setTextInRange([[1, 0], [1, Infinity]], '\t\t\ta', {normalizeLineEndings: false}) + + editor.setCursorScreenPosition([1, 7]) + editor.moveToFirstCharacterOfLine() + expect(editor.getCursorBufferPosition()).toEqual([1, 3]) + editor.moveToFirstCharacterOfLine() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + }) + }) + }) + }) + + describe('.moveToBeginningOfWord()', () => { + it('moves the cursor to the beginning of the word', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([1, 12]) + editor.addCursorAtBufferPosition([3, 0]) + const [cursor1, cursor2, cursor3] = editor.getCursors() + + editor.moveToBeginningOfWord() + + expect(cursor1.getBufferPosition()).toEqual([0, 4]) + expect(cursor2.getBufferPosition()).toEqual([1, 11]) + expect(cursor3.getBufferPosition()).toEqual([2, 39]) + }) + + it('does not fail at position [0, 0]', () => { + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfWord() + }) + + it('treats lines with only whitespace as a word', () => { + editor.setCursorBufferPosition([11, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('treats lines with only whitespace as a word (CRLF line ending)', () => { + editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition([11, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('works when the current line is blank', () => { + editor.setCursorBufferPosition([10, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual([9, 2]) + }) + + it('works when the current line is blank (CRLF line ending)', () => { + editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition([10, 0]) + editor.moveToBeginningOfWord() + expect(editor.getCursorBufferPosition()).toEqual([9, 2]) + editor.buffer.setText(buffer.getText().replace(/\r\n/g, '\n')) + }) + }) + + describe('.moveToPreviousWordBoundary()', () => { + it('moves the cursor to the previous word boundary', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([2, 0]) + editor.addCursorAtBufferPosition([2, 4]) + editor.addCursorAtBufferPosition([3, 14]) + const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.moveToPreviousWordBoundary() + + expect(cursor1.getBufferPosition()).toEqual([0, 4]) + expect(cursor2.getBufferPosition()).toEqual([1, 30]) + expect(cursor3.getBufferPosition()).toEqual([2, 0]) + expect(cursor4.getBufferPosition()).toEqual([3, 13]) + }) + }) + + describe('.moveToNextWordBoundary()', () => { + it('moves the cursor to the previous word boundary', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([2, 40]) + editor.addCursorAtBufferPosition([3, 0]) + editor.addCursorAtBufferPosition([3, 30]) + const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.moveToNextWordBoundary() + + expect(cursor1.getBufferPosition()).toEqual([0, 13]) + expect(cursor2.getBufferPosition()).toEqual([3, 0]) + expect(cursor3.getBufferPosition()).toEqual([3, 4]) + expect(cursor4.getBufferPosition()).toEqual([3, 31]) + }) + }) + + describe('.moveToEndOfWord()', () => { + it('moves the cursor to the end of the word', () => { + editor.setCursorBufferPosition([0, 6]) + editor.addCursorAtBufferPosition([1, 10]) + editor.addCursorAtBufferPosition([2, 40]) + const [cursor1, cursor2, cursor3] = editor.getCursors() + + editor.moveToEndOfWord() + + expect(cursor1.getBufferPosition()).toEqual([0, 13]) + expect(cursor2.getBufferPosition()).toEqual([1, 12]) + expect(cursor3.getBufferPosition()).toEqual([3, 7]) + }) + + it('does not blow up when there is no next word', () => { + editor.setCursorBufferPosition([Infinity, Infinity]) + const endPosition = editor.getCursorBufferPosition() + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual(endPosition) + }) + + it('treats lines with only whitespace as a word', () => { + editor.setCursorBufferPosition([9, 4]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('treats lines with only whitespace as a word (CRLF line ending)', () => { + editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition([9, 4]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('works when the current line is blank', () => { + editor.setCursorBufferPosition([10, 0]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual([11, 8]) + }) + + it('works when the current line is blank (CRLF line ending)', () => { + editor.buffer.setText(buffer.getText().replace(/\n/g, '\r\n')) + editor.setCursorBufferPosition([10, 0]) + editor.moveToEndOfWord() + expect(editor.getCursorBufferPosition()).toEqual([11, 8]) + }) + }) + + describe('.moveToBeginningOfNextWord()', () => { + it('moves the cursor before the first character of the next word', () => { + editor.setCursorBufferPosition([0, 6]) + editor.addCursorAtBufferPosition([1, 11]) + editor.addCursorAtBufferPosition([2, 0]) + const [cursor1, cursor2, cursor3] = editor.getCursors() + + editor.moveToBeginningOfNextWord() + + expect(cursor1.getBufferPosition()).toEqual([0, 14]) + expect(cursor2.getBufferPosition()).toEqual([1, 13]) + expect(cursor3.getBufferPosition()).toEqual([2, 4]) + + // When the cursor is on whitespace + editor.setText('ab cde- ') + editor.setCursorBufferPosition([0, 2]) + const cursor = editor.getLastCursor() + editor.moveToBeginningOfNextWord() + + expect(cursor.getBufferPosition()).toEqual([0, 3]) + }) + + it('does not blow up when there is no next word', () => { + editor.setCursorBufferPosition([Infinity, Infinity]) + const endPosition = editor.getCursorBufferPosition() + editor.moveToBeginningOfNextWord() + expect(editor.getCursorBufferPosition()).toEqual(endPosition) + }) + + it('treats lines with only whitespace as a word', () => { + editor.setCursorBufferPosition([9, 4]) + editor.moveToBeginningOfNextWord() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + + it('works when the current line is blank', () => { + editor.setCursorBufferPosition([10, 0]) + editor.moveToBeginningOfNextWord() + expect(editor.getCursorBufferPosition()).toEqual([11, 9]) + }) + }) + + describe('.moveToPreviousSubwordBoundary', () => { + it('does not move the cursor when there is no previous subword boundary', () => { + editor.setText('') + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + + it('stops at word and underscore boundaries', () => { + editor.setText('sub_word \n') + editor.setCursorBufferPosition([0, 9]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 8]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + editor.setText(' word\n') + editor.setCursorBufferPosition([0, 3]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('stops at camelCase boundaries', () => { + editor.setText(' getPreviousWord\n') + editor.setCursorBufferPosition([0, 16]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 12]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('skips consecutive non-word characters', () => { + editor.setText('e, => \n') + editor.setCursorBufferPosition([0, 6]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('skips consecutive uppercase characters', () => { + editor.setText(' AAADF \n') + editor.setCursorBufferPosition([0, 7]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.setText('ALPhA\n') + editor.setCursorBufferPosition([0, 4]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + }) + + it('skips consecutive numbers', () => { + editor.setText(' 88 \n') + editor.setCursorBufferPosition([0, 4]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + }) + + it('works with multiple cursors', () => { + editor.setText('curOp\ncursorOptions\n') + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([1, 13]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveToPreviousSubwordBoundary() + + expect(cursor1.getBufferPosition()).toEqual([0, 3]) + expect(cursor2.getBufferPosition()).toEqual([1, 6]) + }) + + it('works with non-English characters', () => { + editor.setText('supåTøåst \n') + editor.setCursorBufferPosition([0, 9]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.setText('supaÖast \n') + editor.setCursorBufferPosition([0, 8]) + editor.moveToPreviousSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + }) + + describe('.moveToNextSubwordBoundary', () => { + it('does not move the cursor when there is no next subword boundary', () => { + editor.setText('') + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + + it('stops at word and underscore boundaries', () => { + editor.setText(' sub_word \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 9]) + + editor.setText('word \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + + it('stops at camelCase boundaries', () => { + editor.setText('getPreviousWord \n') + editor.setCursorBufferPosition([0, 0]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 11]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 15]) + }) + + it('skips consecutive non-word characters', () => { + editor.setText(', => \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + + it('skips consecutive uppercase characters', () => { + editor.setText(' AAADF \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + + editor.setText('ALPhA\n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + }) + + it('skips consecutive numbers', () => { + editor.setText(' 88 \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 3]) + }) + + it('works with multiple cursors', () => { + editor.setText('curOp\ncursorOptions\n') + editor.setCursorBufferPosition([0, 0]) + editor.addCursorAtBufferPosition([1, 0]) + const [cursor1, cursor2] = editor.getCursors() + + editor.moveToNextSubwordBoundary() + expect(cursor1.getBufferPosition()).toEqual([0, 3]) + expect(cursor2.getBufferPosition()).toEqual([1, 6]) + }) + + it('works with non-English characters', () => { + editor.setText('supåTøåst \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + + editor.setText('supaÖast \n') + editor.setCursorBufferPosition([0, 0]) + editor.moveToNextSubwordBoundary() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + }) + }) + + describe('.moveToBeginningOfNextParagraph()', () => { + it('moves the cursor before the first line of the next paragraph', () => { + editor.setCursorBufferPosition([0, 6]) + editor.foldBufferRow(4) + + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + + editor.setText('') + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + + it('moves the cursor before the first line of the next paragraph (CRLF line endings)', () => { + editor.setText(editor.getText().replace(/\n/g, '\r\n')) + + editor.setCursorBufferPosition([0, 6]) + editor.foldBufferRow(4) + + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + + editor.setText('') + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfNextParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.moveToBeginningOfPreviousParagraph()', () => { + it('moves the cursor before the first line of the previous paragraph', () => { + editor.setCursorBufferPosition([10, 0]) + editor.foldBufferRow(4) + + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + editor.setText('') + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + + it('moves the cursor before the first line of the previous paragraph (CRLF line endings)', () => { + editor.setText(editor.getText().replace(/\n/g, '\r\n')) + + editor.setCursorBufferPosition([10, 0]) + editor.foldBufferRow(4) + + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + + editor.setText('') + editor.setCursorBufferPosition([0, 0]) + editor.moveToBeginningOfPreviousParagraph() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.getCurrentParagraphBufferRange()', () => { + it('returns the buffer range of the current paragraph, delimited by blank lines or the beginning / end of the file', () => { + buffer.setText(' ' + dedent` + I am the first paragraph, + bordered by the beginning of + the file + ${' '} + + I am the second paragraph + with blank lines above and below + me. + + I am the last paragraph, + bordered by the end of the file.\ + `) + + // in a paragraph + editor.setCursorBufferPosition([1, 7]) + expect(editor.getCurrentParagraphBufferRange()).toEqual([[0, 0], [2, 8]]) + + editor.setCursorBufferPosition([7, 1]) + expect(editor.getCurrentParagraphBufferRange()).toEqual([[5, 0], [7, 3]]) + + editor.setCursorBufferPosition([9, 10]) + expect(editor.getCurrentParagraphBufferRange()).toEqual([[9, 0], [10, 32]]) + + // between paragraphs + editor.setCursorBufferPosition([3, 1]) + expect(editor.getCurrentParagraphBufferRange()).toBeUndefined() + }) + + it('will limit paragraph range to comments', () => { + editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) + editor.setText(dedent` + var quicksort = function () { + /* Single line comment block */ + var sort = function(items) {}; + + /* + A multiline + comment is here + */ + var sort = function(items) {}; + + // A comment + // + // Multiple comment + // lines + var sort = function(items) {}; + // comment line after fn + + var nosort = function(items) { + item; + } + + };\ + `) + + function paragraphBufferRangeForRow (row) { + editor.setCursorBufferPosition([row, 0]) + return editor.getLastCursor().getCurrentParagraphBufferRange() + } + + expect(paragraphBufferRangeForRow(0)).toEqual([[0, 0], [0, 29]]) + expect(paragraphBufferRangeForRow(1)).toEqual([[1, 0], [1, 33]]) + expect(paragraphBufferRangeForRow(2)).toEqual([[2, 0], [2, 32]]) + expect(paragraphBufferRangeForRow(3)).toBeFalsy() + expect(paragraphBufferRangeForRow(4)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(5)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(6)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(7)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(8)).toEqual([[8, 0], [8, 32]]) + expect(paragraphBufferRangeForRow(9)).toBeFalsy() + expect(paragraphBufferRangeForRow(10)).toEqual([[10, 0], [13, 10]]) + expect(paragraphBufferRangeForRow(11)).toEqual([[10, 0], [13, 10]]) + expect(paragraphBufferRangeForRow(12)).toEqual([[10, 0], [13, 10]]) + expect(paragraphBufferRangeForRow(14)).toEqual([[14, 0], [14, 32]]) + expect(paragraphBufferRangeForRow(15)).toEqual([[15, 0], [15, 26]]) + expect(paragraphBufferRangeForRow(18)).toEqual([[17, 0], [19, 3]]) + }) + }) + + describe('getCursorAtScreenPosition(screenPosition)', () => { + it('returns the cursor at the given screenPosition', () => { + const cursor1 = editor.addCursorAtScreenPosition([0, 2]) + const cursor2 = editor.getCursorAtScreenPosition(cursor1.getScreenPosition()) + expect(cursor2).toBe(cursor1) + }) + }) + + describe('::getCursorScreenPositions()', () => { + it('returns the cursor positions in the order they were added', () => { + editor.foldBufferRow(4) + const cursor1 = editor.addCursorAtBufferPosition([8, 5]) + const cursor2 = editor.addCursorAtBufferPosition([3, 5]) + expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [5, 5], [3, 5]]) + }) + }) + + describe('::getCursorsOrderedByBufferPosition()', () => { + it('returns all cursors ordered by buffer positions', () => { + const originalCursor = editor.getLastCursor() + const cursor1 = editor.addCursorAtBufferPosition([8, 5]) + const cursor2 = editor.addCursorAtBufferPosition([4, 5]) + expect(editor.getCursorsOrderedByBufferPosition()).toEqual([originalCursor, cursor2, cursor1]) + }) + }) + + describe('addCursorAtScreenPosition(screenPosition)', () => { + describe('when a cursor already exists at the position', () => { + it('returns the existing cursor', () => { + const cursor1 = editor.addCursorAtScreenPosition([0, 2]) + const cursor2 = editor.addCursorAtScreenPosition([0, 2]) + expect(cursor2).toBe(cursor1) + }) + }) + }) + + describe('addCursorAtBufferPosition(bufferPosition)', () => { + describe('when a cursor already exists at the position', () => { + it('returns the existing cursor', () => { + const cursor1 = editor.addCursorAtBufferPosition([1, 4]) + const cursor2 = editor.addCursorAtBufferPosition([1, 4]) + expect(cursor2.marker).toBe(cursor1.marker) + }) + }) + }) + + describe('.getCursorScope()', () => { + it('returns the current scope', () => { + const descriptor = editor.getCursorScope() + expect(descriptor.scopes).toContain('source.js') + }) + }) + }) + + describe('selection', () => { + let selection + + beforeEach(() => { + selection = editor.getLastSelection() + }) + + describe('.getLastSelection()', () => { + it('creates a new selection at (0, 0) if the last selection has been destroyed', () => { + editor.getLastSelection().destroy() + expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) + }) + + it("doesn't get stuck in a infinite loop when called from ::onDidAddCursor after the last selection has been destroyed (regression)", () => { + let callCount = 0 + editor.getLastSelection().destroy() + editor.onDidAddCursor(function (cursor) { + callCount++ + editor.getLastSelection() + }) + expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [0, 0]]) + expect(callCount).toBe(1) + }) + }) + + describe('.getSelections()', () => { + it('creates a new selection at (0, 0) if the last selection has been destroyed', () => { + editor.getLastSelection().destroy() + expect(editor.getSelections()[0].getBufferRange()).toEqual([[0, 0], [0, 0]]) + }) + }) + + describe('when the selection range changes', () => { + it('emits an event with the old range, new range, and the selection that moved', () => { + let rangeChangedHandler + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + + editor.onDidChangeSelectionRange(rangeChangedHandler = jasmine.createSpy()) + editor.selectToBufferPosition([6, 2]) + + expect(rangeChangedHandler).toHaveBeenCalled() + const eventObject = rangeChangedHandler.mostRecentCall.args[0] + + expect(eventObject.oldBufferRange).toEqual([[3, 0], [4, 5]]) + expect(eventObject.oldScreenRange).toEqual([[3, 0], [4, 5]]) + expect(eventObject.newBufferRange).toEqual([[3, 0], [6, 2]]) + expect(eventObject.newScreenRange).toEqual([[3, 0], [6, 2]]) + expect(eventObject.selection).toBe(selection) + }) + }) + + describe('.selectUp/Down/Left/Right()', () => { + it("expands each selection to its cursor's new location", () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]) + const [selection1, selection2] = editor.getSelections() + + editor.selectRight() + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 14]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 22]]) + + editor.selectLeft() + editor.selectLeft() + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 20]]) + + editor.selectDown() + expect(selection1.getBufferRange()).toEqual([[0, 9], [1, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [4, 20]]) + + editor.selectUp() + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 20]]) + }) + + it('merges selections when they intersect when moving down', () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[1, 10], [1, 20]], [[2, 15], [3, 25]]]) + const [selection1, selection2, selection3] = editor.getSelections() + + editor.selectDown() + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.getScreenRange()).toEqual([[0, 9], [4, 25]]) + expect(selection1.isReversed()).toBeFalsy() + }) + + it('merges selections when they intersect when moving up', () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[1, 10], [1, 20]]], {reversed: true}) + const [selection1, selection2] = editor.getSelections() + + editor.selectUp() + expect(editor.getSelections().length).toBe(1) + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.getScreenRange()).toEqual([[0, 0], [1, 20]]) + expect(selection1.isReversed()).toBeTruthy() + }) + + it('merges selections when they intersect when moving left', () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[0, 13], [1, 20]]], {reversed: true}) + const [selection1, selection2] = editor.getSelections() + + editor.selectLeft() + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.getScreenRange()).toEqual([[0, 8], [1, 20]]) + expect(selection1.isReversed()).toBeTruthy() + }) + + it('merges selections when they intersect when moving right', () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 14]], [[0, 14], [1, 20]]]) + const [selection1, selection2] = editor.getSelections() + + editor.selectRight() + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.getScreenRange()).toEqual([[0, 9], [1, 21]]) + expect(selection1.isReversed()).toBeFalsy() + }) + + describe('when counts are passed into the selection functions', () => { + it("expands each selection to its cursor's new location", () => { + editor.setSelectedBufferRanges([[[0, 9], [0, 13]], [[3, 16], [3, 21]]]) + const [selection1, selection2] = editor.getSelections() + + editor.selectRight(2) + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 15]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 23]]) + + editor.selectLeft(3) + expect(selection1.getBufferRange()).toEqual([[0, 9], [0, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [3, 20]]) + + editor.selectDown(3) + expect(selection1.getBufferRange()).toEqual([[0, 9], [3, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [6, 20]]) + + editor.selectUp(2) + expect(selection1.getBufferRange()).toEqual([[0, 9], [1, 12]]) + expect(selection2.getBufferRange()).toEqual([[3, 16], [4, 20]]) + }) + }) + }) + + describe('.selectToBufferPosition(bufferPosition)', () => { + it('expands the last selection to the given position', () => { + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtBufferPosition([5, 6]) + editor.selectToBufferPosition([6, 2]) + + const selections = editor.getSelections() + expect(selections.length).toBe(2) + const [selection1, selection2] = selections + expect(selection1.getBufferRange()).toEqual([[3, 0], [4, 5]]) + expect(selection2.getBufferRange()).toEqual([[5, 6], [6, 2]]) + }) + }) + + describe('.selectToScreenPosition(screenPosition)', () => { + it('expands the last selection to the given position', () => { + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtScreenPosition([5, 6]) + editor.selectToScreenPosition([6, 2]) + + const selections = editor.getSelections() + expect(selections.length).toBe(2) + const [selection1, selection2] = selections + expect(selection1.getScreenRange()).toEqual([[3, 0], [4, 5]]) + expect(selection2.getScreenRange()).toEqual([[5, 6], [6, 2]]) + }) + + describe('when selecting with an initial screen range', () => { + it('switches the direction of the selection when selecting to positions before/after the start of the initial range', () => { + editor.setCursorScreenPosition([5, 10]) + editor.selectWordsContainingCursors() + editor.selectToScreenPosition([3, 0]) + expect(editor.getLastSelection().isReversed()).toBe(true) + editor.selectToScreenPosition([9, 0]) + expect(editor.getLastSelection().isReversed()).toBe(false) + }) + }) + }) + + describe('.selectToBeginningOfNextParagraph()', () => { + it('selects from the cursor to first line of the next paragraph', () => { + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtScreenPosition([5, 6]) + editor.selectToScreenPosition([6, 2]) + + editor.selectToBeginningOfNextParagraph() + + const selections = editor.getSelections() + expect(selections.length).toBe(1) + expect(selections[0].getScreenRange()).toEqual([[3, 0], [10, 0]]) + }) + }) + + describe('.selectToBeginningOfPreviousParagraph()', () => { + it('selects from the cursor to the first line of the previous paragraph', () => { + editor.setSelectedBufferRange([[3, 0], [4, 5]]) + editor.addCursorAtScreenPosition([5, 6]) + editor.selectToScreenPosition([6, 2]) + + editor.selectToBeginningOfPreviousParagraph() + + const selections = editor.getSelections() + expect(selections.length).toBe(1) + expect(selections[0].getScreenRange()).toEqual([[0, 0], [5, 6]]) + }) + + it('merges selections if they intersect, maintaining the directionality of the last selection', () => { + editor.setCursorScreenPosition([4, 10]) + editor.selectToScreenPosition([5, 27]) + editor.addCursorAtScreenPosition([3, 10]) + editor.selectToScreenPosition([6, 27]) + + let selections = editor.getSelections() + expect(selections.length).toBe(1) + let [selection1] = selections + expect(selection1.getScreenRange()).toEqual([[3, 10], [6, 27]]) + expect(selection1.isReversed()).toBeFalsy() + + editor.addCursorAtScreenPosition([7, 4]) + editor.selectToScreenPosition([4, 11]) + + selections = editor.getSelections() + expect(selections.length).toBe(1); + [selection1] = selections + expect(selection1.getScreenRange()).toEqual([[3, 10], [7, 4]]) + expect(selection1.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToTop()', () => { + it('selects text from cursor position to the top of the buffer', () => { + editor.setCursorScreenPosition([11, 2]) + editor.addCursorAtScreenPosition([10, 0]) + editor.selectToTop() + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + expect(editor.getLastSelection().getBufferRange()).toEqual([[0, 0], [11, 2]]) + expect(editor.getLastSelection().isReversed()).toBeTruthy() + }) + }) + + describe('.selectToBottom()', () => { + it('selects text from cursor position to the bottom of the buffer', () => { + editor.setCursorScreenPosition([10, 0]) + editor.addCursorAtScreenPosition([9, 3]) + editor.selectToBottom() + expect(editor.getCursors().length).toBe(1) + expect(editor.getCursorBufferPosition()).toEqual([12, 2]) + expect(editor.getLastSelection().getBufferRange()).toEqual([[9, 3], [12, 2]]) + expect(editor.getLastSelection().isReversed()).toBeFalsy() + }) + }) + + describe('.selectAll()', () => { + it('selects the entire buffer', () => { + editor.selectAll() + expect(editor.getLastSelection().getBufferRange()).toEqual(buffer.getRange()) + }) + }) + + describe('.selectToBeginningOfLine()', () => { + it('selects text from cursor position to beginning of line', () => { + editor.setCursorScreenPosition([12, 2]) + editor.addCursorAtScreenPosition([11, 3]) + + editor.selectToBeginningOfLine() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([12, 0]) + expect(cursor2.getBufferPosition()).toEqual([11, 0]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[12, 0], [12, 2]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[11, 0], [11, 3]]) + expect(selection2.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToEndOfLine()', () => { + it('selects text from cursor position to end of line', () => { + editor.setCursorScreenPosition([12, 0]) + editor.addCursorAtScreenPosition([11, 3]) + + editor.selectToEndOfLine() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([12, 2]) + expect(cursor2.getBufferPosition()).toEqual([11, 44]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[12, 0], [12, 2]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[11, 3], [11, 44]]) + expect(selection2.isReversed()).toBeFalsy() + }) + }) + + describe('.selectLinesContainingCursors()', () => { + it('selects to the entire line (including newlines) at given row', () => { + editor.setCursorScreenPosition([1, 2]) + editor.selectLinesContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[1, 0], [2, 0]]) + expect(editor.getSelectedText()).toBe(' var sort = function(items) {\n') + + editor.setCursorScreenPosition([12, 2]) + editor.selectLinesContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[12, 0], [12, 2]]) + + editor.setCursorBufferPosition([0, 2]) + editor.selectLinesContainingCursors() + editor.selectLinesContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [2, 0]]) + }) + + describe('when the selection spans multiple row', () => { + it('selects from the beginning of the first line to the last line', () => { + selection = editor.getLastSelection() + selection.setBufferRange([[1, 10], [3, 20]]) + editor.selectLinesContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[1, 0], [4, 0]]) + }) + }) + }) + + describe('.selectToBeginningOfWord()', () => { + it('selects text from cursor position to beginning of word', () => { + editor.setCursorScreenPosition([0, 13]) + editor.addCursorAtScreenPosition([3, 49]) + + editor.selectToBeginningOfWord() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 4]) + expect(cursor2.getBufferPosition()).toEqual([3, 47]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 4], [0, 13]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[3, 47], [3, 49]]) + expect(selection2.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToEndOfWord()', () => { + it('selects text from cursor position to end of word', () => { + editor.setCursorScreenPosition([0, 4]) + editor.addCursorAtScreenPosition([3, 48]) + + editor.selectToEndOfWord() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 13]) + expect(cursor2.getBufferPosition()).toEqual([3, 50]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 4], [0, 13]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[3, 48], [3, 50]]) + expect(selection2.isReversed()).toBeFalsy() + }) + }) + + describe('.selectToBeginningOfNextWord()', () => { + it('selects text from cursor position to beginning of next word', () => { + editor.setCursorScreenPosition([0, 4]) + editor.addCursorAtScreenPosition([3, 48]) + + editor.selectToBeginningOfNextWord() + + expect(editor.getCursors().length).toBe(2) + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 14]) + expect(cursor2.getBufferPosition()).toEqual([3, 51]) + + expect(editor.getSelections().length).toBe(2) + const [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 4], [0, 14]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[3, 48], [3, 51]]) + expect(selection2.isReversed()).toBeFalsy() + }) + }) + + describe('.selectToPreviousWordBoundary()', () => { + it('select to the previous word boundary', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([2, 0]) + editor.addCursorAtBufferPosition([3, 4]) + editor.addCursorAtBufferPosition([3, 14]) + + editor.selectToPreviousWordBoundary() + + expect(editor.getSelections().length).toBe(4) + const [selection1, selection2, selection3, selection4] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 8], [0, 4]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[2, 0], [1, 30]]) + expect(selection2.isReversed()).toBeTruthy() + expect(selection3.getBufferRange()).toEqual([[3, 4], [3, 0]]) + expect(selection3.isReversed()).toBeTruthy() + expect(selection4.getBufferRange()).toEqual([[3, 14], [3, 13]]) + expect(selection4.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToNextWordBoundary()', () => { + it('select to the next word boundary', () => { + editor.setCursorBufferPosition([0, 8]) + editor.addCursorAtBufferPosition([2, 40]) + editor.addCursorAtBufferPosition([4, 0]) + editor.addCursorAtBufferPosition([3, 30]) + + editor.selectToNextWordBoundary() + + expect(editor.getSelections().length).toBe(4) + const [selection1, selection2, selection3, selection4] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 8], [0, 13]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[2, 40], [3, 0]]) + expect(selection2.isReversed()).toBeFalsy() + expect(selection3.getBufferRange()).toEqual([[4, 0], [4, 4]]) + expect(selection3.isReversed()).toBeFalsy() + expect(selection4.getBufferRange()).toEqual([[3, 30], [3, 31]]) + expect(selection4.isReversed()).toBeFalsy() + }) + }) + + describe('.selectToPreviousSubwordBoundary', () => { + it('selects subwords', () => { + editor.setText('') + editor.insertText('_word\n') + editor.insertText(' getPreviousWord\n') + editor.insertText('e, => \n') + editor.insertText(' 88 \n') + editor.setCursorBufferPosition([0, 5]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 5]) + editor.addCursorAtBufferPosition([3, 3]) + const [selection1, selection2, selection3, selection4] = editor.getSelections() + + editor.selectToPreviousSubwordBoundary() + expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 5]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[1, 4], [1, 7]]) + expect(selection2.isReversed()).toBeTruthy() + expect(selection3.getBufferRange()).toEqual([[2, 3], [2, 5]]) + expect(selection3.isReversed()).toBeTruthy() + expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) + expect(selection4.isReversed()).toBeTruthy() + }) + }) + + describe('.selectToNextSubwordBoundary', () => { + it('selects subwords', () => { + editor.setText('') + editor.insertText('word_\n') + editor.insertText('getPreviousWord\n') + editor.insertText('e, => \n') + editor.insertText(' 88 \n') + editor.setCursorBufferPosition([0, 1]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 2]) + editor.addCursorAtBufferPosition([3, 1]) + const [selection1, selection2, selection3, selection4] = editor.getSelections() + + editor.selectToNextSubwordBoundary() + expect(selection1.getBufferRange()).toEqual([[0, 1], [0, 4]]) + expect(selection1.isReversed()).toBeFalsy() + expect(selection2.getBufferRange()).toEqual([[1, 7], [1, 11]]) + expect(selection2.isReversed()).toBeFalsy() + expect(selection3.getBufferRange()).toEqual([[2, 2], [2, 5]]) + expect(selection3.isReversed()).toBeFalsy() + expect(selection4.getBufferRange()).toEqual([[3, 1], [3, 3]]) + expect(selection4.isReversed()).toBeFalsy() + }) + }) + + describe('.deleteToBeginningOfSubword', () => { + it('deletes subwords', () => { + editor.setText('') + editor.insertText('_word\n') + editor.insertText(' getPreviousWord\n') + editor.insertText('e, => \n') + editor.insertText(' 88 \n') + editor.setCursorBufferPosition([0, 5]) + editor.addCursorAtBufferPosition([1, 7]) + editor.addCursorAtBufferPosition([2, 5]) + editor.addCursorAtBufferPosition([3, 3]) + const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('_') + expect(buffer.lineForRow(1)).toBe(' getviousWord') + expect(buffer.lineForRow(2)).toBe('e, ') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0, 1]) + expect(cursor2.getBufferPosition()).toEqual([1, 4]) + expect(cursor3.getBufferPosition()).toEqual([2, 3]) + expect(cursor4.getBufferPosition()).toEqual([3, 1]) + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe(' viousWord') + expect(buffer.lineForRow(2)).toBe('e ') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 1]) + expect(cursor3.getBufferPosition()).toEqual([2, 1]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) + + editor.deleteToBeginningOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe('viousWord') + expect(buffer.lineForRow(2)).toBe(' ') + expect(buffer.lineForRow(3)).toBe('') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 0]) + expect(cursor4.getBufferPosition()).toEqual([2, 1]) + }) + }) + + describe('.deleteToEndOfSubword', () => { + it('deletes subwords', () => { + editor.setText('') + editor.insertText('word_\n') + editor.insertText('getPreviousWord \n') + editor.insertText('e, => \n') + editor.insertText(' 88 \n') + editor.setCursorBufferPosition([0, 0]) + editor.addCursorAtBufferPosition([1, 0]) + editor.addCursorAtBufferPosition([2, 2]) + editor.addCursorAtBufferPosition([3, 0]) + const [cursor1, cursor2, cursor3, cursor4] = editor.getCursors() + + editor.deleteToEndOfSubword() + expect(buffer.lineForRow(0)).toBe('_') + expect(buffer.lineForRow(1)).toBe('PreviousWord ') + expect(buffer.lineForRow(2)).toBe('e, ') + expect(buffer.lineForRow(3)).toBe('88 ') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 2]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) + + editor.deleteToEndOfSubword() + expect(buffer.lineForRow(0)).toBe('') + expect(buffer.lineForRow(1)).toBe('Word ') + expect(buffer.lineForRow(2)).toBe('e,') + expect(buffer.lineForRow(3)).toBe(' ') + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 0]) + expect(cursor3.getBufferPosition()).toEqual([2, 2]) + expect(cursor4.getBufferPosition()).toEqual([3, 0]) + }) + }) + + describe('.selectWordsContainingCursors()', () => { + describe('when the cursor is inside a word', () => { + it('selects the entire word', () => { + editor.setCursorScreenPosition([0, 8]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedText()).toBe('quicksort') + }) + }) + + describe('when the cursor is between two words', () => { + it('selects the word the cursor is on', () => { + editor.setCursorScreenPosition([0, 4]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedText()).toBe('quicksort') + + editor.setCursorScreenPosition([0, 3]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedText()).toBe('var') + }) + }) + + describe('when the cursor is inside a region of whitespace', () => { + it('selects the whitespace region', () => { + editor.setCursorScreenPosition([5, 2]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[5, 0], [5, 6]]) + + editor.setCursorScreenPosition([5, 0]) + editor.selectWordsContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[5, 0], [5, 6]]) + }) + }) + + describe('when the cursor is at the end of the text', () => { + it('select the previous word', () => { + editor.buffer.append('word') + editor.moveToBottom() + editor.selectWordsContainingCursors() + expect(editor.getSelectedBufferRange()).toEqual([[12, 2], [12, 6]]) + }) + }) + + it("selects words based on the non-word characters configured at the cursor's current scope", () => { + editor.setText("one-one; 'two-two'; three-three") + + editor.setCursorBufferPosition([0, 1]) + editor.addCursorAtBufferPosition([0, 12]) + + const scopeDescriptors = editor.getCursors().map(c => c.getScopeDescriptor()) + expect(scopeDescriptors[0].getScopesArray()).toEqual(['source.js']) + expect(scopeDescriptors[1].getScopesArray()).toEqual(['source.js', 'string.quoted.single.js']) + + editor.setScopedSettingsDelegate({ + getNonWordCharacters (scopes) { + const result = '/\()"\':,.;<>~!@#$%^&*|+=[]{}`?' + if (scopes.some(scope => scope.startsWith('string'))) { + return result + } else { + return result + '-' + } + } + }) + + editor.selectWordsContainingCursors() + + expect(editor.getSelections()[0].getText()).toBe('one') + expect(editor.getSelections()[1].getText()).toBe('two-two') + }) + }) + + describe('.selectToFirstCharacterOfLine()', () => { + it("moves to the first character of the current line or the beginning of the line if it's already on the first character", () => { + editor.setCursorScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 7]) + + editor.selectToFirstCharacterOfLine() + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor2.getBufferPosition()).toEqual([1, 2]) + + expect(editor.getSelections().length).toBe(2) + let [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 0], [0, 5]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[1, 2], [1, 7]]) + expect(selection2.isReversed()).toBeTruthy() + + editor.selectToFirstCharacterOfLine(); + [selection1, selection2] = editor.getSelections() + expect(selection1.getBufferRange()).toEqual([[0, 0], [0, 5]]) + expect(selection1.isReversed()).toBeTruthy() + expect(selection2.getBufferRange()).toEqual([[1, 0], [1, 7]]) + expect(selection2.isReversed()).toBeTruthy() + }) + }) + + describe('.setSelectedBufferRanges(ranges)', () => { + it('clears existing selections and creates selections for each of the given ranges', () => { + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + + editor.setSelectedBufferRanges([[[5, 5], [6, 6]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[5, 5], [6, 6]]]) + }) + + it('merges intersecting selections', () => { + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [5, 5]]]) + }) + + it('does not merge non-empty adjacent selections', () => { + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 3], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [3, 3]], [[3, 3], [5, 5]]]) + }) + + it('recycles existing selection instances', () => { + selection = editor.getLastSelection() + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1).toBe(selection) + expect(selection1.getBufferRange()).toEqual([[2, 2], [3, 3]]) + }) + + describe("when the 'preserveFolds' option is false (the default)", () => { + it("removes folds that contain one or both of the selection's end points", () => { + editor.setSelectedBufferRange([[0, 0], [0, 0]]) + editor.foldBufferRowRange(1, 4) + editor.foldBufferRowRange(2, 3) + editor.foldBufferRowRange(6, 8) + editor.foldBufferRowRange(10, 11) + + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]]) + expect(editor.isFoldedAtScreenRow(1)).toBeFalsy() + expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() + expect(editor.isFoldedAtScreenRow(6)).toBeFalsy() + expect(editor.isFoldedAtScreenRow(10)).toBeTruthy() + + editor.setSelectedBufferRange([[10, 0], [12, 0]]) + expect(editor.isFoldedAtScreenRow(10)).toBeTruthy() + }) + }) + + describe("when the 'preserveFolds' option is true", () => { + it('does not remove folds that contain the selections', () => { + editor.setSelectedBufferRange([[0, 0], [0, 0]]) + editor.foldBufferRowRange(1, 4) + editor.foldBufferRowRange(6, 8) + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], {preserveFolds: true}) + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + }) + }) + }) + + describe('.setSelectedScreenRanges(ranges)', () => { + beforeEach(() => editor.foldBufferRow(4)) + + it('clears existing selections and creates selections for each of the given ranges', () => { + editor.setSelectedScreenRanges([[[3, 4], [3, 7]], [[5, 4], [5, 7]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[3, 4], [3, 7]], [[8, 4], [8, 7]]]) + + editor.setSelectedScreenRanges([[[6, 2], [6, 4]]]) + expect(editor.getSelectedScreenRanges()).toEqual([[[6, 2], [6, 4]]]) + }) + + it('merges intersecting selections and unfolds the fold which contain them', () => { + editor.foldBufferRow(0) + + // Use buffer ranges because only the first line is on screen + editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [5, 5]]]) + }) + + it('recycles existing selection instances', () => { + selection = editor.getLastSelection() + editor.setSelectedScreenRanges([[[2, 2], [3, 4]], [[4, 4], [5, 5]]]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1).toBe(selection) + expect(selection1.getScreenRange()).toEqual([[2, 2], [3, 4]]) + }) + }) + + describe('.selectMarker(marker)', () => { + describe('if the marker is valid', () => { + it("selects the marker's range and returns the selected range", () => { + const marker = editor.markBufferRange([[0, 1], [3, 3]]) + expect(editor.selectMarker(marker)).toEqual([[0, 1], [3, 3]]) + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [3, 3]]) + }) + }) + + describe('if the marker is invalid', () => { + it('does not change the selection and returns a falsy value', () => { + const marker = editor.markBufferRange([[0, 1], [3, 3]]) + marker.destroy() + expect(editor.selectMarker(marker)).toBeFalsy() + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 0]]) + }) + }) + }) + + describe('.addSelectionForBufferRange(bufferRange)', () => { + it('adds a selection for the specified buffer range', () => { + editor.addSelectionForBufferRange([[3, 4], [5, 6]]) + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 0]], [[3, 4], [5, 6]]]) + }) + }) + + describe('.addSelectionBelow()', () => { + describe('when the selection is non-empty', () => { + it('selects the same region of the line below current selections if possible', () => { + editor.setSelectedBufferRange([[3, 16], [3, 21]]) + editor.addSelectionForBufferRange([[3, 25], [3, 34]]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 16], [3, 21]], + [[3, 25], [3, 34]], + [[4, 16], [4, 21]], + [[4, 25], [4, 29]] + ]) + }) + + it('skips lines that are too short to create a non-empty selection', () => { + editor.setSelectedBufferRange([[3, 31], [3, 38]]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 31], [3, 38]], + [[6, 31], [6, 38]] + ]) + }) + + it("honors the original selection's range (goal range) when adding across shorter lines", () => { + editor.setSelectedBufferRange([[3, 22], [3, 38]]) + editor.addSelectionBelow() + editor.addSelectionBelow() + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 22], [3, 38]], + [[4, 22], [4, 29]], + [[5, 22], [5, 30]], + [[6, 22], [6, 38]] + ]) + }) + + it('clears selection goal ranges when the selection changes', () => { + editor.setSelectedBufferRange([[3, 22], [3, 38]]) + editor.addSelectionBelow() + editor.selectLeft() + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 22], [3, 37]], + [[4, 22], [4, 29]], + [[5, 22], [5, 28]] + ]) + + // goal range from previous add selection is honored next time + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 22], [3, 37]], + [[4, 22], [4, 29]], + [[5, 22], [5, 30]], // select to end of line 5 because line 4's goal range was reset by line 3 previously + [[6, 22], [6, 28]] + ]) + }) + + it('can add selections to soft-wrapped line segments', () => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(40) + editor.setDefaultCharWidth(1) + + editor.setSelectedScreenRange([[3, 10], [3, 15]]) + editor.addSelectionBelow() + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 10], [3, 15]], + [[4, 10], [4, 15]] + ]) + }) + + it('takes atomic tokens into account', async () => { + editor = await atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', {autoIndent: false}) + editor.setSelectedBufferRange([[2, 1], [2, 3]]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 1], [2, 3]], + [[3, 1], [3, 2]] + ]) + }) + }) + + describe('when the selection is empty', () => { + describe('when lines are soft-wrapped', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(40) + }) + + it('skips soft-wrap indentation tokens', () => { + editor.setCursorScreenPosition([3, 0]) + editor.addSelectionBelow() + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 0], [3, 0]], + [[4, 4], [4, 4]] + ]) + }) + + it("does not skip them if they're shorter than the current column", () => { + editor.setCursorScreenPosition([3, 37]) + editor.addSelectionBelow() + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[3, 37], [3, 37]], + [[4, 26], [4, 26]] + ]) + }) + }) + + it('does not skip lines that are shorter than the current column', () => { + editor.setCursorBufferPosition([3, 36]) + editor.addSelectionBelow() + editor.addSelectionBelow() + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 36], [3, 36]], + [[4, 29], [4, 29]], + [[5, 30], [5, 30]], + [[6, 36], [6, 36]] + ]) + }) + + it('skips empty lines when the column is non-zero', () => { + editor.setCursorBufferPosition([9, 4]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[9, 4], [9, 4]], + [[11, 4], [11, 4]] + ]) + }) + + it('does not skip empty lines when the column is zero', () => { + editor.setCursorBufferPosition([9, 0]) + editor.addSelectionBelow() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[9, 0], [9, 0]], + [[10, 0], [10, 0]] + ]) + }) + }) + }) + + describe('.addSelectionAbove()', () => { + describe('when the selection is non-empty', () => { + it('selects the same region of the line above current selections if possible', () => { + editor.setSelectedBufferRange([[3, 16], [3, 21]]) + editor.addSelectionForBufferRange([[3, 37], [3, 44]]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 16], [3, 21]], + [[3, 37], [3, 44]], + [[2, 16], [2, 21]], + [[2, 37], [2, 40]] + ]) + }) + + it('skips lines that are too short to create a non-empty selection', () => { + editor.setSelectedBufferRange([[6, 31], [6, 38]]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[6, 31], [6, 38]], + [[3, 31], [3, 38]] + ]) + }) + + it("honors the original selection's range (goal range) when adding across shorter lines", () => { + editor.setSelectedBufferRange([[6, 22], [6, 38]]) + editor.addSelectionAbove() + editor.addSelectionAbove() + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[6, 22], [6, 38]], + [[5, 22], [5, 30]], + [[4, 22], [4, 29]], + [[3, 22], [3, 38]] + ]) + }) + + it('can add selections to soft-wrapped line segments', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(40) + + editor.setSelectedScreenRange([[4, 10], [4, 15]]) + editor.addSelectionAbove() + expect(editor.getSelectedScreenRanges()).toEqual([ + [[4, 10], [4, 15]], + [[3, 10], [3, 15]] + ]) + }) + + it('takes atomic tokens into account', async () => { + editor = await atom.workspace.open('sample-with-tabs-and-leading-comment.coffee', {autoIndent: false}) + editor.setSelectedBufferRange([[3, 1], [3, 2]]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[3, 1], [3, 2]], + [[2, 1], [2, 3]] + ]) + }) + }) + + describe('when the selection is empty', () => { + describe('when lines are soft-wrapped', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(40) + }) + + it('skips soft-wrap indentation tokens', () => { + editor.setCursorScreenPosition([5, 0]) + editor.addSelectionAbove() + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[5, 0], [5, 0]], + [[4, 4], [4, 4]] + ]) + }) + + it("does not skip them if they're shorter than the current column", () => { + editor.setCursorScreenPosition([5, 29]) + editor.addSelectionAbove() + + expect(editor.getSelectedScreenRanges()).toEqual([ + [[5, 29], [5, 29]], + [[4, 26], [4, 26]] + ]) + }) + }) + + it('does not skip lines that are shorter than the current column', () => { + editor.setCursorBufferPosition([6, 36]) + editor.addSelectionAbove() + editor.addSelectionAbove() + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[6, 36], [6, 36]], + [[5, 30], [5, 30]], + [[4, 29], [4, 29]], + [[3, 36], [3, 36]] + ]) + }) + + it('skips empty lines when the column is non-zero', () => { + editor.setCursorBufferPosition([11, 4]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[11, 4], [11, 4]], + [[9, 4], [9, 4]] + ]) + }) + + it('does not skip empty lines when the column is zero', () => { + editor.setCursorBufferPosition([10, 0]) + editor.addSelectionAbove() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[10, 0], [10, 0]], + [[9, 0], [9, 0]] + ]) + }) + }) + }) + + describe('.splitSelectionsIntoLines()', () => { + it('splits all multi-line selections into one selection per line', () => { + editor.setSelectedBufferRange([[0, 3], [2, 4]]) + editor.splitSelectionsIntoLines() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 3], [0, 29]], + [[1, 0], [1, 30]], + [[2, 0], [2, 4]] + ]) + + editor.setSelectedBufferRange([[0, 3], [1, 10]]) + editor.splitSelectionsIntoLines() + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 3], [0, 29]], + [[1, 0], [1, 10]] + ]) + + editor.setSelectedBufferRange([[0, 0], [0, 3]]) + editor.splitSelectionsIntoLines() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 3]]]) + }) + }) + + describe('::consolidateSelections()', () => { + const makeMultipleSelections = () => { + selection.setBufferRange([[3, 16], [3, 21]]) + const selection2 = editor.addSelectionForBufferRange([[3, 25], [3, 34]]) + const selection3 = editor.addSelectionForBufferRange([[8, 4], [8, 10]]) + const selection4 = editor.addSelectionForBufferRange([[1, 6], [1, 10]]) + expect(editor.getSelections()).toEqual([selection, selection2, selection3, selection4]) + return [selection, selection2, selection3, selection4] + } + + it('destroys all selections but the oldest selection and autoscrolls to it, returning true if any selections were destroyed', () => { + const [selection1] = makeMultipleSelections() + + const autoscrollEvents = [] + editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event)) + + expect(editor.consolidateSelections()).toBeTruthy() + expect(editor.getSelections()).toEqual([selection1]) + expect(selection1.isEmpty()).toBeFalsy() + expect(editor.consolidateSelections()).toBeFalsy() + expect(editor.getSelections()).toEqual([selection1]) + + expect(autoscrollEvents).toEqual([ + {screenRange: selection1.getScreenRange(), options: {center: true, reversed: false}} + ]) + }) + }) + + describe('when the cursor is moved while there is a selection', () => { + const makeSelection = () => selection.setBufferRange([[1, 2], [1, 5]]) + + it('clears the selection', () => { + makeSelection() + editor.moveDown() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.moveUp() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.moveLeft() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.moveRight() + expect(selection.isEmpty()).toBeTruthy() + + makeSelection() + editor.setCursorScreenPosition([3, 3]) + expect(selection.isEmpty()).toBeTruthy() + }) + }) + + it('does not share selections between different edit sessions for the same buffer', async () => { + atom.workspace.getActivePane().splitRight() + const editor2 = await atom.workspace.open(editor.getPath()) + + expect(editor2.getText()).toBe(editor.getText()) + editor.setSelectedBufferRanges([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) + editor2.setSelectedBufferRanges([[[8, 7], [6, 5]], [[4, 3], [2, 1]]]) + expect(editor2.getSelectedBufferRanges()).not.toEqual(editor.getSelectedBufferRanges()) + }) + }) + + describe('buffer manipulation', () => { + describe('.moveLineUp', () => { + it('moves the line under the cursor up', () => { + editor.setCursorBufferPosition([1, 0]) + editor.moveLineUp() + expect(editor.getTextInBufferRange([[0, 0], [0, 30]])).toBe(' var sort = function(items) {') + expect(editor.indentationForBufferRow(0)).toBe(1) + expect(editor.indentationForBufferRow(1)).toBe(0) + }) + + it("updates the line's indentation when the the autoIndent setting is true", () => { + editor.update({autoIndent: true}) + editor.setCursorBufferPosition([1, 0]) + editor.moveLineUp() + expect(editor.indentationForBufferRow(0)).toBe(0) + expect(editor.indentationForBufferRow(1)).toBe(0) + }) + + describe('when there is a single selection', () => { + describe('when the selection spans a single line', () => { + describe('when there is no fold in the preceeding row', () => + it('moves the line to the preceding row', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + editor.setSelectedBufferRange([[3, 2], [3, 9]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [2, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + }) + ) + + describe('when the cursor is at the beginning of a fold', () => + it('moves the line to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[4, 2], [4, 9]], {preserveFolds: true}) + expect(editor.getSelectedBufferRange()).toEqual([[4, 2], [4, 9]]) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [3, 9]]) + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + }) + ) + + describe('when the preceding row consists of folded code', () => + it('moves the line above the folded row and perseveres the correct folds', () => { + expect(editor.lineTextForBufferRow(8)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(9)).toBe(' };') + + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRange([[8, 0], [8, 4]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [4, 4]]) + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + ) + }) + + describe('when the selection spans multiple lines', () => { + it('moves the lines spanned by the selection to the preceding row', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.setSelectedBufferRange([[3, 2], [4, 9]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [3, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(4)).toBe(' if (items.length <= 1) return items;') + }) + + describe("when the selection's end intersects a fold", () => + it('moves the lines to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[3, 2], [4, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [3, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(7)).toBe(' if (items.length <= 1) return items;') + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + }) + ) + + describe("when the selection's start intersects a fold", () => + it('moves the lines to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[4, 2], [8, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [7, 9]]) + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(7)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(8)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + }) + ) + }) + + describe('when the selection spans multiple lines, but ends at column 0', () => { + it('does not move the last line of the selection', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.setSelectedBufferRange([[3, 2], [4, 0]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[2, 2], [3, 0]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + }) + }) + + describe('when the preceeding row is a folded row', () => { + it('moves the lines spanned by the selection to the preceeding row, but preserves the folded code', () => { + expect(editor.lineTextForBufferRow(8)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(9)).toBe(' };') + + editor.foldBufferRowRange(4, 7) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRange([[8, 0], [9, 2]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [5, 2]]) + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(5)).toBe(' };') + expect(editor.lineTextForBufferRow(6)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(5)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() + }) + }) + }) + + describe('when there are multiple selections', () => { + describe('when all the selections span different lines', () => { + describe('when there is no folds', () => + it('moves all lines that are spanned by a selection to the preceding row', () => { + editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 2], [0, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]) + expect(editor.lineTextForBufferRow(0)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(1)).toBe('var quicksort = function () {') + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' current = items.shift();') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + }) + ) + + describe('when one selection intersects a fold', () => + it('moves the lines to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRanges([ + [[2, 2], [2, 9]], + [[4, 2], [4, 9]] + ], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[1, 2], [1, 9]], + [[3, 2], [3, 9]] + ]) + + expect(editor.lineTextForBufferRow(1)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + }) + ) + + describe('when there is a fold', () => + it('moves all lines that spanned by a selection to preceding row, preserving all folds', () => { + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRanges([[[8, 0], [8, 3]], [[11, 0], [11, 5]]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[4, 0], [4, 3]], [[10, 0], [10, 5]]]) + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(10)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + ) + }) + + describe('when there are many folds', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample-with-many-folds.js', {autoIndent: false}) + }) + + describe('and many selections intersects folded rows', () => + it('moves and preserves all the folds', () => { + editor.foldBufferRowRange(2, 4) + editor.foldBufferRowRange(7, 9) + + editor.setSelectedBufferRanges([ + [[1, 0], [5, 4]], + [[7, 0], [7, 4]] + ], {preserveFolds: true}) + + editor.moveLineUp() + + expect(editor.lineTextForBufferRow(1)).toEqual('function f3() {') + expect(editor.lineTextForBufferRow(4)).toEqual('6;') + expect(editor.lineTextForBufferRow(5)).toEqual('1;') + expect(editor.lineTextForBufferRow(6)).toEqual('function f8() {') + expect(editor.lineTextForBufferRow(9)).toEqual('7;') + + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() + + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + ) + }) + + describe('when some of the selections span the same lines', () => { + it('moves lines that contain multiple selections correctly', () => { + editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 2], [2, 9]], [[2, 12], [2, 13]]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + }) + + describe('when one of the selections spans line 0', () => { + it("doesn't move any lines, since line 0 can't move", () => { + editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]) + + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]) + expect(buffer.isModified()).toBe(false) + }) + }) + + describe('when one of the selections spans the last line, and it is empty', () => { + it("doesn't move any lines, since the last line can't move", () => { + buffer.append('\n') + editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]]) + + editor.moveLineUp() + + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]]) + }) + }) + }) + }) + + describe('.moveLineDown', () => { + it('moves the line under the cursor down', () => { + editor.setCursorBufferPosition([0, 0]) + editor.moveLineDown() + expect(editor.getTextInBufferRange([[1, 0], [1, 31]])).toBe('var quicksort = function () {') + expect(editor.indentationForBufferRow(0)).toBe(1) + expect(editor.indentationForBufferRow(1)).toBe(0) + }) + + it("updates the line's indentation when the editor.autoIndent setting is true", () => { + editor.update({autoIndent: true}) + editor.setCursorBufferPosition([0, 0]) + editor.moveLineDown() + expect(editor.indentationForBufferRow(0)).toBe(1) + expect(editor.indentationForBufferRow(1)).toBe(2) + }) + + describe('when there is a single selection', () => { + describe('when the selection spans a single line', () => { + describe('when there is no fold in the following row', () => + it('moves the line to the following row', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + editor.setSelectedBufferRange([[2, 2], [2, 9]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [3, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + }) + ) + + describe('when the cursor is at the beginning of a fold', () => + it('moves the line to the following row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[4, 2], [4, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[5, 2], [5, 9]]) + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + ) + + describe('when the following row is a folded row', () => + it('moves the line below the folded row and preserves the fold', () => { + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRange([[3, 0], [3, 4]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[7, 0], [7, 4]]) + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + ) + }) + + describe('when the selection spans multiple lines', () => { + it('moves the lines spanned by the selection to the following row', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.setSelectedBufferRange([[2, 2], [3, 9]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [4, 9]]) + expect(editor.lineTextForBufferRow(2)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + }) + + describe('when the selection spans multiple lines, but ends at column 0', () => { + it('does not move the last line of the selection', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.setSelectedBufferRange([[2, 2], [3, 0]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[3, 2], [4, 0]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + }) + }) + + describe("when the selection's end intersects a fold", () => { + it('moves the lines to the following row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[3, 2], [4, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[4, 2], [5, 9]]) + expect(editor.lineTextForBufferRow(3)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + + expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + }) + + describe("when the selection's start intersects a fold", () => { + it('moves the lines to the following row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRange([[4, 2], [8, 9]], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[5, 2], [9, 9]]) + expect(editor.lineTextForBufferRow(4)).toBe(' };') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(9)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + + expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() + }) + }) + + describe('when the following row is a folded row', () => { + it('moves the lines spanned by the selection to the following row, but preserves the folded code', () => { + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + editor.foldBufferRowRange(4, 7) + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRange([[2, 0], [3, 2]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRange()).toEqual([[6, 0], [7, 2]]) + expect(editor.lineTextForBufferRow(2)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() + expect(editor.lineTextForBufferRow(6)).toBe(' if (items.length <= 1) return items;') + }) + }) + + describe('when the last line of selection does not end with a valid line ending', () => { + it('appends line ending to last line and moves the lines spanned by the selection to the preceeding row', () => { + expect(editor.lineTextForBufferRow(9)).toBe(' };') + expect(editor.lineTextForBufferRow(10)).toBe('') + expect(editor.lineTextForBufferRow(11)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.lineTextForBufferRow(12)).toBe('};') + + editor.setSelectedBufferRange([[10, 0], [12, 2]]) + editor.moveLineUp() + + expect(editor.getSelectedBufferRange()).toEqual([[9, 0], [11, 2]]) + expect(editor.lineTextForBufferRow(9)).toBe('') + expect(editor.lineTextForBufferRow(10)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.lineTextForBufferRow(11)).toBe('};') + expect(editor.lineTextForBufferRow(12)).toBe(' };') + }) + }) + }) + + describe('when there are multiple selections', () => { + describe('when all the selections span different lines', () => { + describe('when there is no folds', () => + it('moves all lines that are spanned by a selection to the following row', () => { + editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([[[6, 2], [6, 9]], [[4, 2], [4, 9]], [[2, 2], [2, 9]]]) + expect(editor.lineTextForBufferRow(1)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(5)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(6)).toBe(' current = items.shift();') + }) + ) + + describe('when there are many folds', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample-with-many-folds.js', {autoIndent: false}) + }) + + describe('and many selections intersects folded rows', () => + it('moves and preserves all the folds', () => { + editor.foldBufferRowRange(2, 4) + editor.foldBufferRowRange(7, 9) + + editor.setSelectedBufferRanges([ + [[2, 0], [2, 4]], + [[6, 0], [10, 4]] + ], {preserveFolds: true}) + + editor.moveLineDown() + + expect(editor.lineTextForBufferRow(2)).toEqual('6;') + expect(editor.lineTextForBufferRow(3)).toEqual('function f3() {') + expect(editor.lineTextForBufferRow(6)).toEqual('12;') + expect(editor.lineTextForBufferRow(7)).toEqual('7;') + expect(editor.lineTextForBufferRow(8)).toEqual('function f8() {') + expect(editor.lineTextForBufferRow(11)).toEqual('11;') + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(10)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(11)).toBeFalsy() + }) + ) + }) + + describe('when there is a fold below one of the selected row', () => + it('moves all lines spanned by a selection to the following row, preserving the fold', () => { + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRanges([[[1, 2], [1, 6]], [[3, 0], [3, 4]], [[8, 0], [8, 3]]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([[[9, 0], [9, 3]], [[7, 0], [7, 4]], [[2, 2], [2, 6]]]) + expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {') + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeFalsy() + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(9)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + }) + ) + + describe('when there is a fold below a group of multiple selections without any lines with no selection in-between', () => + it('moves all the lines below the fold, preserving the fold', () => { + editor.foldBufferRowRange(4, 7) + + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + + editor.setSelectedBufferRanges([[[2, 2], [2, 6]], [[3, 0], [3, 4]]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([[[7, 0], [7, 4]], [[6, 2], [6, 6]]]) + expect(editor.lineTextForBufferRow(2)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(3)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeFalsy() + expect(editor.lineTextForBufferRow(6)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(7)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + ) + }) + + describe('when one selection intersects a fold', () => { + it('moves the lines to the previous row without breaking the fold', () => { + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + + editor.foldBufferRowRange(4, 7) + editor.setSelectedBufferRanges([ + [[2, 2], [2, 9]], + [[4, 2], [4, 9]] + ], {preserveFolds: true}) + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[5, 2], [5, 9]], + [[3, 2], [3, 9]] + ]) + + expect(editor.lineTextForBufferRow(2)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(4)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(9)).toBe(' };') + + expect(editor.isFoldedAtBufferRow(2)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(3)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(4)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(5)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(6)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(7)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(8)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeFalsy() + }) + }) + + describe('when some of the selections span the same lines', () => { + it('moves lines that contain multiple selections correctly', () => { + editor.setSelectedBufferRanges([[[3, 2], [3, 9]], [[3, 12], [3, 13]]]) + editor.moveLineDown() + + expect(editor.getSelectedBufferRanges()).toEqual([[[4, 12], [4, 13]], [[4, 2], [4, 9]]]) + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + }) + }) + + describe('when the selections are above a wrapped line', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(80) + editor.setText(`\ +1 +2 +Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. +3 +4\ +`) + }) + + it('moves the lines past the soft wrapped line', () => { + editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 0]]]) + + editor.moveLineDown() + + expect(editor.lineTextForBufferRow(0)).not.toBe('2') + expect(editor.lineTextForBufferRow(1)).toBe('1') + expect(editor.lineTextForBufferRow(2)).toBe('2') + }) + }) + }) + + describe('when the line is the last buffer row', () => { + it("doesn't move it", () => { + editor.setText('abc\ndef') + editor.setCursorBufferPosition([1, 0]) + editor.moveLineDown() + expect(editor.getText()).toBe('abc\ndef') + }) + }) + }) + + describe('.insertText(text)', () => { + describe('when there is a single selection', () => { + beforeEach(() => editor.setSelectedBufferRange([[1, 0], [1, 2]])) + + it('replaces the selection with the given text', () => { + const range = editor.insertText('xxx') + expect(range).toEqual([ [[1, 0], [1, 3]] ]) + expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {') + }) + }) + + describe('when there are multiple empty selections', () => { + describe('when the cursors are on the same line', () => { + it("inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", () => { + editor.setCursorScreenPosition([1, 2]) + editor.addCursorAtScreenPosition([1, 5]) + + editor.insertText('xxx') + + expect(buffer.lineForRow(1)).toBe(' xxxvarxxx sort = function(items) {') + const [cursor1, cursor2] = editor.getCursors() + + expect(cursor1.getBufferPosition()).toEqual([1, 5]) + expect(cursor2.getBufferPosition()).toEqual([1, 11]) + }) + }) + + describe('when the cursors are on different lines', () => { + it("inserts the given text at the location of each cursor and moves the cursors to the end of each cursor's inserted text", () => { + editor.setCursorScreenPosition([1, 2]) + editor.addCursorAtScreenPosition([2, 4]) + + editor.insertText('xxx') + + expect(buffer.lineForRow(1)).toBe(' xxxvar sort = function(items) {') + expect(buffer.lineForRow(2)).toBe(' xxxif (items.length <= 1) return items;') + const [cursor1, cursor2] = editor.getCursors() + + expect(cursor1.getBufferPosition()).toEqual([1, 5]) + expect(cursor2.getBufferPosition()).toEqual([2, 7]) + }) + }) + }) + + describe('when there are multiple non-empty selections', () => { + describe('when the selections are on the same line', () => { + it('replaces each selection range with the inserted characters', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 22], [0, 24]]]) + editor.insertText('x') + + const [cursor1, cursor2] = editor.getCursors() + const [selection1, selection2] = editor.getSelections() + + expect(cursor1.getScreenPosition()).toEqual([0, 5]) + expect(cursor2.getScreenPosition()).toEqual([0, 15]) + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + + expect(editor.lineTextForBufferRow(0)).toBe('var x = functix () {') + }) + }) + + describe('when the selections are on different lines', () => { + it("replaces each selection with the given text, clears the selections, and places the cursor at the end of each selection's inserted text", () => { + editor.setSelectedBufferRanges([[[1, 0], [1, 2]], [[2, 0], [2, 4]]]) + + editor.insertText('xxx') + + expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {') + expect(buffer.lineForRow(2)).toBe('xxxif (items.length <= 1) return items;') + const [selection1, selection2] = editor.getSelections() + + expect(selection1.isEmpty()).toBeTruthy() + expect(selection1.cursor.getBufferPosition()).toEqual([1, 3]) + expect(selection2.isEmpty()).toBeTruthy() + expect(selection2.cursor.getBufferPosition()).toEqual([2, 3]) + }) + }) + }) + + describe('when there is a selection that ends on a folded line', () => { + it('destroys the selection', () => { + editor.foldBufferRowRange(2, 4) + editor.setSelectedBufferRange([[1, 0], [2, 0]]) + editor.insertText('holy cow') + expect(editor.isFoldedAtScreenRow(2)).toBeFalsy() + }) + }) + + describe('when there are ::onWillInsertText and ::onDidInsertText observers', () => { + beforeEach(() => editor.setSelectedBufferRange([[1, 0], [1, 2]])) + + it('notifies the observers when inserting text', () => { + const willInsertSpy = jasmine.createSpy().andCallFake(() => expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {')) + + const didInsertSpy = jasmine.createSpy().andCallFake(() => expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {')) + + editor.onWillInsertText(willInsertSpy) + editor.onDidInsertText(didInsertSpy) + + expect(editor.insertText('xxx')).toBeTruthy() + expect(buffer.lineForRow(1)).toBe('xxxvar sort = function(items) {') + + expect(willInsertSpy).toHaveBeenCalled() + expect(didInsertSpy).toHaveBeenCalled() + + let options = willInsertSpy.mostRecentCall.args[0] + expect(options.text).toBe('xxx') + expect(options.cancel).toBeDefined() + + options = didInsertSpy.mostRecentCall.args[0] + expect(options.text).toBe('xxx') + }) + + it('cancels text insertion when an ::onWillInsertText observer calls cancel on an event', () => { + const willInsertSpy = jasmine.createSpy().andCallFake(({cancel}) => cancel()) + + const didInsertSpy = jasmine.createSpy() + + editor.onWillInsertText(willInsertSpy) + editor.onDidInsertText(didInsertSpy) + + expect(editor.insertText('xxx')).toBe(false) + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + + expect(willInsertSpy).toHaveBeenCalled() + expect(didInsertSpy).not.toHaveBeenCalled() + }) + }) + + describe("when the undo option is set to 'skip'", () => { + beforeEach(() => editor.setSelectedBufferRange([[1, 2], [1, 2]])) + + it('does not undo the skipped operation', () => { + let range = editor.insertText('x') + range = editor.insertText('y', {undo: 'skip'}) + editor.undo() + expect(buffer.lineForRow(1)).toBe(' yvar sort = function(items) {') + }) + }) + }) + + describe('.insertNewline()', () => { + describe('when there is a single cursor', () => { + describe('when the cursor is at the beginning of a line', () => { + it('inserts an empty line before it', () => { + editor.setCursorScreenPosition({row: 1, column: 0}) + + editor.insertNewline() + + expect(buffer.lineForRow(1)).toBe('') + expect(editor.getCursorScreenPosition()).toEqual({row: 2, column: 0}) + }) + }) + + describe('when the cursor is in the middle of a line', () => { + it('splits the current line to form a new line', () => { + editor.setCursorScreenPosition({row: 1, column: 6}) + const originalLine = buffer.lineForRow(1) + const lineBelowOriginalLine = buffer.lineForRow(2) + + editor.insertNewline() + + expect(buffer.lineForRow(1)).toBe(originalLine.slice(0, 6)) + expect(buffer.lineForRow(2)).toBe(originalLine.slice(6)) + expect(buffer.lineForRow(3)).toBe(lineBelowOriginalLine) + expect(editor.getCursorScreenPosition()).toEqual({row: 2, column: 0}) + }) + }) + + describe('when the cursor is on the end of a line', () => { + it('inserts an empty line after it', () => { + editor.setCursorScreenPosition({row: 1, column: buffer.lineForRow(1).length}) + + editor.insertNewline() + + expect(buffer.lineForRow(2)).toBe('') + expect(editor.getCursorScreenPosition()).toEqual({row: 2, column: 0}) + }) + }) + }) + + describe('when there are multiple cursors', () => { + describe('when the cursors are on the same line', () => { + it('breaks the line at the cursor locations', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([3, 38]) + + editor.insertNewline() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot') + expect(editor.lineTextForBufferRow(4)).toBe(' = items.shift(), current') + expect(editor.lineTextForBufferRow(5)).toBe(', left = [], right = [];') + expect(editor.lineTextForBufferRow(6)).toBe(' while(items.length > 0) {') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([4, 0]) + expect(cursor2.getBufferPosition()).toEqual([5, 0]) + }) + }) + + describe('when the cursors are on different lines', () => { + it('inserts newlines at each cursor location', () => { + editor.setCursorScreenPosition([3, 0]) + editor.addCursorAtScreenPosition([6, 0]) + + editor.insertText('\n') + expect(editor.lineTextForBufferRow(3)).toBe('') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(5)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(6)).toBe(' current = items.shift();') + expect(editor.lineTextForBufferRow(7)).toBe('') + expect(editor.lineTextForBufferRow(8)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(9)).toBe(' }') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([4, 0]) + expect(cursor2.getBufferPosition()).toEqual([8, 0]) + }) + }) + }) + }) + + describe('.insertNewlineBelow()', () => { + describe('when the operation is undone', () => { + it('places the cursor back at the previous location', () => { + editor.setCursorBufferPosition([0, 2]) + editor.insertNewlineBelow() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + editor.undo() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + }) + }) + + it("inserts a newline below the cursor's current line, autoindents it, and moves the cursor to the end of the line", () => { + editor.update({autoIndent: true}) + editor.insertNewlineBelow() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe(' ') + expect(editor.getCursorBufferPosition()).toEqual([1, 2]) + }) + }) + + describe('.insertNewlineAbove()', () => { + describe('when the cursor is on first line', () => { + it('inserts a newline on the first line and moves the cursor to the first line', () => { + editor.setCursorBufferPosition([0]) + editor.insertNewlineAbove() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + expect(editor.lineTextForBufferRow(0)).toBe('') + expect(editor.lineTextForBufferRow(1)).toBe('var quicksort = function () {') + expect(editor.buffer.getLineCount()).toBe(14) + }) + }) + + describe('when the cursor is not on the first line', () => { + it('inserts a newline above the current line and moves the cursor to the inserted line', () => { + editor.setCursorBufferPosition([3, 4]) + editor.insertNewlineAbove() + expect(editor.getCursorBufferPosition()).toEqual([3, 0]) + expect(editor.lineTextForBufferRow(3)).toBe('') + expect(editor.lineTextForBufferRow(4)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.buffer.getLineCount()).toBe(14) + + editor.undo() + expect(editor.getCursorBufferPosition()).toEqual([3, 4]) + }) + }) + + it('indents the new line to the correct level when editor.autoIndent is true', () => { + editor.update({autoIndent: true}) + + editor.setText(' var test') + editor.setCursorBufferPosition([0, 2]) + editor.insertNewlineAbove() + + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + expect(editor.lineTextForBufferRow(0)).toBe(' ') + expect(editor.lineTextForBufferRow(1)).toBe(' var test') + + editor.setText('\n var test') + editor.setCursorBufferPosition([1, 2]) + editor.insertNewlineAbove() + + expect(editor.getCursorBufferPosition()).toEqual([1, 2]) + expect(editor.lineTextForBufferRow(0)).toBe('') + expect(editor.lineTextForBufferRow(1)).toBe(' ') + expect(editor.lineTextForBufferRow(2)).toBe(' var test') + + editor.setText('function() {\n}') + editor.setCursorBufferPosition([1, 1]) + editor.insertNewlineAbove() + + expect(editor.getCursorBufferPosition()).toEqual([1, 2]) + expect(editor.lineTextForBufferRow(0)).toBe('function() {') + expect(editor.lineTextForBufferRow(1)).toBe(' ') + expect(editor.lineTextForBufferRow(2)).toBe('}') + }) + }) + + describe('.insertNewLine()', () => { + describe('when a new line is appended before a closing tag (e.g. by pressing enter before a selection)', () => { + it('moves the line down and keeps the indentation level the same when editor.autoIndent is true', () => { + editor.update({autoIndent: true}) + editor.setCursorBufferPosition([9, 2]) + editor.insertNewline() + expect(editor.lineTextForBufferRow(10)).toBe(' };') + }) + }) + + describe('when a newline is appended with a trailing closing tag behind the cursor (e.g. by pressing enter in the middel of a line)', () => { + it('indents the new line to the correct level when editor.autoIndent is true and using a curly-bracket language', () => { + editor.update({autoIndent: true}) + editor.setGrammar(atom.grammars.selectGrammar('file.js')) + editor.setText('var test = () => {\n return true;};') + editor.setCursorBufferPosition([1, 14]) + editor.insertNewline() + expect(editor.indentationForBufferRow(1)).toBe(1) + expect(editor.indentationForBufferRow(2)).toBe(0) + }) + + it('indents the new line to the current level when editor.autoIndent is true and no increaseIndentPattern is specified', () => { + editor.setGrammar(atom.grammars.selectGrammar('file')) + editor.update({autoIndent: true}) + editor.setText(' if true') + editor.setCursorBufferPosition([0, 8]) + editor.insertNewline() + expect(editor.getGrammar()).toBe(atom.grammars.nullGrammar) + expect(editor.indentationForBufferRow(0)).toBe(1) + expect(editor.indentationForBufferRow(1)).toBe(1) + }) + + it('indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language', async () => { + await atom.packages.activatePackage('language-coffee-script') + editor.update({autoIndent: true}) + editor.setGrammar(atom.grammars.selectGrammar('file.coffee')) + editor.setText('if true\n return trueelse\n return false') + editor.setCursorBufferPosition([1, 13]) + editor.insertNewline() + expect(editor.indentationForBufferRow(1)).toBe(1) + expect(editor.indentationForBufferRow(2)).toBe(0) + expect(editor.indentationForBufferRow(3)).toBe(1) + }) + }) + + describe('when a newline is appended on a line that matches the decreaseNextIndentPattern', () => { + it('indents the new line to the correct level when editor.autoIndent is true', async () => { + await atom.packages.activatePackage('language-go') + editor.update({autoIndent: true}) + editor.setGrammar(atom.grammars.selectGrammar('file.go')) + editor.setText('fmt.Printf("some%s",\n "thing")') + editor.setCursorBufferPosition([1, 10]) + editor.insertNewline() + expect(editor.indentationForBufferRow(1)).toBe(1) + expect(editor.indentationForBufferRow(2)).toBe(0) + }) + }) + }) + + describe('.backspace()', () => { + describe('when there is a single cursor', () => { + let changeScreenRangeHandler = null + + beforeEach(() => { + const selection = editor.getLastSelection() + changeScreenRangeHandler = jasmine.createSpy('changeScreenRangeHandler') + selection.onDidChangeRange(changeScreenRangeHandler) + }) + + describe('when the cursor is on the middle of the line', () => { + it('removes the character before the cursor', () => { + editor.setCursorScreenPosition({row: 1, column: 7}) + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + + editor.backspace() + + const line = buffer.lineForRow(1) + expect(line).toBe(' var ort = function(items) {') + expect(editor.getCursorScreenPosition()).toEqual({row: 1, column: 6}) + expect(changeScreenRangeHandler).toHaveBeenCalled() + }) + }) + + describe('when the cursor is at the beginning of a line', () => { + it('joins it with the line above', () => { + const originalLine0 = buffer.lineForRow(0) + expect(originalLine0).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + + editor.setCursorScreenPosition({row: 1, column: 0}) + editor.backspace() + + const line0 = buffer.lineForRow(0) + const line1 = buffer.lineForRow(1) + expect(line0).toBe('var quicksort = function () { var sort = function(items) {') + expect(line1).toBe(' if (items.length <= 1) return items;') + expect(editor.getCursorScreenPosition()).toEqual([0, originalLine0.length]) + + expect(changeScreenRangeHandler).toHaveBeenCalled() + }) + }) + + describe('when the cursor is at the first column of the first line', () => { + it("does nothing, but doesn't raise an error", () => { + editor.setCursorScreenPosition({row: 0, column: 0}) + editor.backspace() + }) + }) + + describe('when the cursor is after a fold', () => { + it('deletes the folded range', () => { + editor.foldBufferRange([[4, 7], [5, 8]]) + editor.setCursorBufferPosition([5, 8]) + editor.backspace() + + expect(buffer.lineForRow(4)).toBe(' whirrent = items.shift();') + expect(editor.isFoldedAtBufferRow(4)).toBe(false) + }) + }) + + describe('when the cursor is in the middle of a line below a fold', () => { + it('backspaces as normal', () => { + editor.setCursorScreenPosition([4, 0]) + editor.foldCurrentRow() + editor.setCursorScreenPosition([5, 5]) + editor.backspace() + + expect(buffer.lineForRow(7)).toBe(' }') + expect(buffer.lineForRow(8)).toBe(' eturn sort(left).concat(pivot).concat(sort(right));') + }) + }) + + describe('when the cursor is on a folded screen line', () => { + it('deletes the contents of the fold before the cursor', () => { + editor.setCursorBufferPosition([3, 0]) + editor.foldCurrentRow() + editor.backspace() + + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) var pivot = items.shift(), current, left = [], right = [];') + expect(editor.getCursorScreenPosition()).toEqual([1, 29]) + }) + }) + }) + + describe('when there are multiple cursors', () => { + describe('when cursors are on the same line', () => { + it('removes the characters preceding each cursor', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([3, 38]) + + editor.backspace() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivo = items.shift(), curren, left = [], right = [];') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([3, 12]) + expect(cursor2.getBufferPosition()).toEqual([3, 36]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + }) + }) + + describe('when cursors are on different lines', () => { + describe('when the cursors are in the middle of their lines', () => + it('removes the characters preceding each cursor', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([4, 10]) + + editor.backspace() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivo = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' whileitems.length > 0) {') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([3, 12]) + expect(cursor2.getBufferPosition()).toEqual([4, 9]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + }) + ) + + describe('when the cursors are on the first column of their lines', () => + it('removes the newlines preceding each cursor', () => { + editor.setCursorScreenPosition([3, 0]) + editor.addCursorAtScreenPosition([6, 0]) + + editor.backspace() + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items; var pivot = items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(3)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(4)).toBe(' current = items.shift(); current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(5)).toBe(' }') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([2, 40]) + expect(cursor2.getBufferPosition()).toEqual([4, 30]) + }) + ) + }) + }) + + describe('when there is a single selection', () => { + it('deletes the selection, but not the character before it', () => { + editor.setSelectedBufferRange([[0, 5], [0, 9]]) + editor.backspace() + expect(editor.buffer.lineForRow(0)).toBe('var qsort = function () {') + }) + + describe('when the selection ends on a folded line', () => { + it('preserves the fold', () => { + editor.setSelectedBufferRange([[3, 0], [4, 0]]) + editor.foldBufferRow(4) + editor.backspace() + + expect(buffer.lineForRow(3)).toBe(' while(items.length > 0) {') + expect(editor.isFoldedAtScreenRow(3)).toBe(true) + }) + }) + }) + + describe('when there are multiple selections', () => { + it('removes all selected text', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + editor.backspace() + expect(editor.lineTextForBufferRow(0)).toBe('var = () {') + }) + }) + }) + + describe('.deleteToPreviousWordBoundary()', () => { + describe('when no text is selected', () => { + it('deletes to the previous word boundary', () => { + editor.setCursorBufferPosition([0, 16]) + editor.addCursorAtBufferPosition([1, 21]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToPreviousWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort =function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = (items) {') + expect(cursor1.getBufferPosition()).toEqual([0, 15]) + expect(cursor2.getBufferPosition()).toEqual([1, 13]) + + editor.deleteToPreviousWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort function () {') + expect(buffer.lineForRow(1)).toBe(' var sort =(items) {') + expect(cursor1.getBufferPosition()).toEqual([0, 14]) + expect(cursor2.getBufferPosition()).toEqual([1, 12]) + }) + }) + + describe('when text is selected', () => { + it('deletes only selected text', () => { + editor.setSelectedBufferRange([[1, 24], [1, 27]]) + editor.deleteToPreviousWordBoundary() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + }) + }) + }) + + describe('.deleteToNextWordBoundary()', () => { + describe('when no text is selected', () => { + it('deletes to the next word boundary', () => { + editor.setCursorBufferPosition([0, 15]) + editor.addCursorAtBufferPosition([1, 24]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort =function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(cursor1.getBufferPosition()).toEqual([0, 15]) + expect(cursor2.getBufferPosition()).toEqual([1, 24]) + + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort = () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(it {') + expect(cursor1.getBufferPosition()).toEqual([0, 15]) + expect(cursor2.getBufferPosition()).toEqual([1, 24]) + + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(0)).toBe('var quicksort =() {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(it{') + expect(cursor1.getBufferPosition()).toEqual([0, 15]) + expect(cursor2.getBufferPosition()).toEqual([1, 24]) + }) + }) + + describe('when text is selected', () => { + it('deletes only selected text', () => { + editor.setSelectedBufferRange([[1, 24], [1, 27]]) + editor.deleteToNextWordBoundary() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + }) + }) + }) + + describe('.deleteToBeginningOfWord()', () => { + describe('when no text is selected', () => { + it('deletes all text between the cursor and the beginning of the word', () => { + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([3, 5]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(ems) {') + expect(buffer.lineForRow(3)).toBe(' ar pivot = items.shift(), current, left = [], right = [];') + expect(cursor1.getBufferPosition()).toEqual([1, 22]) + expect(cursor2.getBufferPosition()).toEqual([3, 4]) + + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = functionems) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return itemsar pivot = items.shift(), current, left = [], right = [];') + expect(cursor1.getBufferPosition()).toEqual([1, 21]) + expect(cursor2.getBufferPosition()).toEqual([2, 39]) + + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = ems) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return ar pivot = items.shift(), current, left = [], right = [];') + expect(cursor1.getBufferPosition()).toEqual([1, 13]) + expect(cursor2.getBufferPosition()).toEqual([2, 34]) + + editor.setText(' var sort') + editor.setCursorBufferPosition([0, 2]) + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(0)).toBe('var sort') + }) + }) + + describe('when text is selected', () => { + it('deletes only selected text', () => { + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.deleteToBeginningOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(buffer.lineForRow(2)).toBe('if (items.length <= 1) return items;') + }) + }) + }) + + describe('.deleteToEndOfLine()', () => { + describe('when no text is selected', () => { + it('deletes all text between the cursor and the end of the line', () => { + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([2, 5]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToEndOfLine() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it') + expect(buffer.lineForRow(2)).toBe(' i') + expect(cursor1.getBufferPosition()).toEqual([1, 24]) + expect(cursor2.getBufferPosition()).toEqual([2, 5]) + }) + + describe('when at the end of the line', () => { + it('deletes the next newline', () => { + editor.setCursorBufferPosition([1, 30]) + editor.deleteToEndOfLine() + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) { if (items.length <= 1) return items;') + }) + }) + }) + + describe('when text is selected', () => { + it('deletes only the text in the selection', () => { + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.deleteToEndOfLine() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(buffer.lineForRow(2)).toBe('if (items.length <= 1) return items;') + }) + }) + }) + + describe('.deleteToBeginningOfLine()', () => { + describe('when no text is selected', () => { + it('deletes all text between the cursor and the beginning of the line', () => { + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([2, 5]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToBeginningOfLine() + expect(buffer.lineForRow(1)).toBe('ems) {') + expect(buffer.lineForRow(2)).toBe('f (items.length <= 1) return items;') + expect(cursor1.getBufferPosition()).toEqual([1, 0]) + expect(cursor2.getBufferPosition()).toEqual([2, 0]) + }) + + describe('when at the beginning of the line', () => { + it('deletes the newline', () => { + editor.setCursorBufferPosition([2]) + editor.deleteToBeginningOfLine() + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) { if (items.length <= 1) return items;') + }) + }) + }) + + describe('when text is selected', () => { + it('still deletes all text to beginning of the line', () => { + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.deleteToBeginningOfLine() + expect(buffer.lineForRow(1)).toBe('ems) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + }) + }) + }) + + describe('.delete()', () => { + describe('when there is a single cursor', () => { + describe('when the cursor is on the middle of a line', () => { + it('deletes the character following the cursor', () => { + editor.setCursorScreenPosition([1, 6]) + editor.delete() + expect(buffer.lineForRow(1)).toBe(' var ort = function(items) {') + }) + }) + + describe('when the cursor is on the end of a line', () => { + it('joins the line with the following line', () => { + editor.setCursorScreenPosition([1, buffer.lineForRow(1).length]) + editor.delete() + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) { if (items.length <= 1) return items;') + }) + }) + + describe('when the cursor is on the last column of the last line', () => { + it("does nothing, but doesn't raise an error", () => { + editor.setCursorScreenPosition([12, buffer.lineForRow(12).length]) + editor.delete() + expect(buffer.lineForRow(12)).toBe('};') + }) + }) + + describe('when the cursor is before a fold', () => { + it('only deletes the lines inside the fold', () => { + editor.foldBufferRange([[3, 6], [4, 8]]) + editor.setCursorScreenPosition([3, 6]) + const cursorPositionBefore = editor.getCursorScreenPosition() + + editor.delete() + + expect(buffer.lineForRow(3)).toBe(' vae(items.length > 0) {') + expect(buffer.lineForRow(4)).toBe(' current = items.shift();') + expect(editor.getCursorScreenPosition()).toEqual(cursorPositionBefore) + }) + }) + + describe('when the cursor is in the middle a line above a fold', () => { + it('deletes as normal', () => { + editor.foldBufferRow(4) + editor.setCursorScreenPosition([3, 4]) + const cursorPositionBefore = editor.getCursorScreenPosition() + + editor.delete() + + expect(buffer.lineForRow(3)).toBe(' ar pivot = items.shift(), current, left = [], right = [];') + expect(editor.isFoldedAtScreenRow(4)).toBe(true) + expect(editor.getCursorScreenPosition()).toEqual([3, 4]) + }) + }) + + describe('when the cursor is inside a fold', () => { + it('removes the folded content after the cursor', () => { + editor.foldBufferRange([[2, 6], [6, 21]]) + editor.setCursorBufferPosition([4, 9]) + + editor.delete() + + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(buffer.lineForRow(4)).toBe(' while ? left.push(current) : right.push(current);') + expect(buffer.lineForRow(5)).toBe(' }') + expect(editor.getCursorBufferPosition()).toEqual([4, 9]) + }) + }) + }) + + describe('when there are multiple cursors', () => { + describe('when cursors are on the same line', () => { + it('removes the characters following each cursor', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([3, 38]) + + editor.delete() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot= items.shift(), current left = [], right = [];') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([3, 13]) + expect(cursor2.getBufferPosition()).toEqual([3, 37]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + }) + }) + + describe('when cursors are on different lines', () => { + describe('when the cursors are in the middle of the lines', () => + it('removes the characters following each cursor', () => { + editor.setCursorScreenPosition([3, 13]) + editor.addCursorAtScreenPosition([4, 10]) + + editor.delete() + + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot= items.shift(), current, left = [], right = [];') + expect(editor.lineTextForBufferRow(4)).toBe(' while(tems.length > 0) {') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([3, 13]) + expect(cursor2.getBufferPosition()).toEqual([4, 10]) + + const [selection1, selection2] = editor.getSelections() + expect(selection1.isEmpty()).toBeTruthy() + expect(selection2.isEmpty()).toBeTruthy() + }) + ) + + describe('when the cursors are at the end of their lines', () => + it('removes the newlines following each cursor', () => { + editor.setCursorScreenPosition([0, 29]) + editor.addCursorAtScreenPosition([1, 30]) + + editor.delete() + + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () { var sort = function(items) { if (items.length <= 1) return items;') + + const [cursor1, cursor2] = editor.getCursors() + expect(cursor1.getBufferPosition()).toEqual([0, 29]) + expect(cursor2.getBufferPosition()).toEqual([0, 59]) + }) + ) + }) + }) + + describe('when there is a single selection', () => { + it('deletes the selection, but not the character following it', () => { + editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]]) + editor.delete() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(buffer.lineForRow(2)).toBe('if (items.length <= 1) return items;') + expect(editor.getLastSelection().isEmpty()).toBeTruthy() + }) + }) + + describe('when there are multiple selections', () => + describe('when selections are on the same line', () => { + it('removes all selected text', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + editor.delete() + expect(editor.lineTextForBufferRow(0)).toBe('var = () {') + }) + }) + ) + }) + + describe('.deleteToEndOfWord()', () => { + describe('when no text is selected', () => { + it('deletes to the end of the word', () => { + editor.setCursorBufferPosition([1, 24]) + editor.addCursorAtBufferPosition([2, 5]) + const [cursor1, cursor2] = editor.getCursors() + + editor.deleteToEndOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + expect(buffer.lineForRow(2)).toBe(' i (items.length <= 1) return items;') + expect(cursor1.getBufferPosition()).toEqual([1, 24]) + expect(cursor2.getBufferPosition()).toEqual([2, 5]) + + editor.deleteToEndOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it {') + expect(buffer.lineForRow(2)).toBe(' iitems.length <= 1) return items;') + expect(cursor1.getBufferPosition()).toEqual([1, 24]) + expect(cursor2.getBufferPosition()).toEqual([2, 5]) + }) + }) + + describe('when text is selected', () => { + it('deletes only selected text', () => { + editor.setSelectedBufferRange([[1, 24], [1, 27]]) + editor.deleteToEndOfWord() + expect(buffer.lineForRow(1)).toBe(' var sort = function(it) {') + }) + }) + }) + + describe('.indent()', () => { + describe('when the selection is empty', () => { + describe('when autoIndent is disabled', () => { + describe("if 'softTabs' is true (the default)", () => { + it("inserts 'tabLength' spaces into the buffer", () => { + const tabRegex = new RegExp(`^[ ]{${editor.getTabLength()}}`) + expect(buffer.lineForRow(0)).not.toMatch(tabRegex) + editor.indent() + expect(buffer.lineForRow(0)).toMatch(tabRegex) + }) + + it('respects the tab stops when cursor is in the middle of a tab', () => { + editor.setTabLength(4) + buffer.insert([12, 2], '\n ') + editor.setCursorBufferPosition([13, 1]) + editor.indent() + expect(buffer.lineForRow(13)).toMatch(/^\s+$/) + expect(buffer.lineForRow(13).length).toBe(4) + expect(editor.getCursorBufferPosition()).toEqual([13, 4]) + + buffer.insert([13, 0], ' ') + editor.setCursorBufferPosition([13, 6]) + editor.indent() + expect(buffer.lineForRow(13).length).toBe(8) + }) + }) + + describe("if 'softTabs' is false", () => + it('insert a \t into the buffer', () => { + editor.setSoftTabs(false) + expect(buffer.lineForRow(0)).not.toMatch(/^\t/) + editor.indent() + expect(buffer.lineForRow(0)).toMatch(/^\t/) + }) + ) + }) + + describe('when autoIndent is enabled', () => { + describe("when the cursor's column is less than the suggested level of indentation", () => { + describe("when 'softTabs' is true (the default)", () => { + it('moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentation', () => { + buffer.insert([5, 0], ' \n') + editor.setCursorBufferPosition([5, 0]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(5)).toMatch(/^\s+$/) + expect(buffer.lineForRow(5).length).toBe(6) + expect(editor.getCursorBufferPosition()).toEqual([5, 6]) + }) + + it('respects the tab stops when cursor is in the middle of a tab', () => { + editor.setTabLength(4) + buffer.insert([12, 2], '\n ') + editor.setCursorBufferPosition([13, 1]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(13)).toMatch(/^\s+$/) + expect(buffer.lineForRow(13).length).toBe(4) + expect(editor.getCursorBufferPosition()).toEqual([13, 4]) + + buffer.insert([13, 0], ' ') + editor.setCursorBufferPosition([13, 6]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(13).length).toBe(8) + }) + }) + + describe("when 'softTabs' is false", () => { + it('moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentation', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + buffer.insert([5, 0], '\t\n') + editor.setCursorBufferPosition([5, 0]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(5)).toMatch(/^\t\t\t$/) + expect(editor.getCursorBufferPosition()).toEqual([5, 3]) + }) + + describe('when the difference between the suggested level of indentation and the current level of indentation is greater than 0 but less than 1', () => + it('inserts one tab', () => { + editor.setSoftTabs(false) + buffer.setText(' \ntest') + editor.setCursorBufferPosition([1, 0]) + + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(1)).toBe('\ttest') + expect(editor.getCursorBufferPosition()).toEqual([1, 1]) + }) + ) + }) + }) + + describe("when the line's indent level is greater than the suggested level of indentation", () => { + describe("when 'softTabs' is true (the default)", () => + it("moves the cursor to the end of the leading whitespace and inserts 'tabLength' spaces into the buffer", () => { + buffer.insert([7, 0], ' \n') + editor.setCursorBufferPosition([7, 2]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(7)).toMatch(/^\s+$/) + expect(buffer.lineForRow(7).length).toBe(8) + expect(editor.getCursorBufferPosition()).toEqual([7, 8]) + }) + ) + + describe("when 'softTabs' is false", () => + it('moves the cursor to the end of the leading whitespace and inserts \t into the buffer', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + buffer.insert([7, 0], '\t\t\t\n') + editor.setCursorBufferPosition([7, 1]) + editor.indent({autoIndent: true}) + expect(buffer.lineForRow(7)).toMatch(/^\t\t\t\t$/) + expect(editor.getCursorBufferPosition()).toEqual([7, 4]) + }) + ) + }) + }) + }) + + describe('when the selection is not empty', () => { + it('indents the selected lines', () => { + editor.setSelectedBufferRange([[0, 0], [10, 0]]) + const selection = editor.getLastSelection() + spyOn(selection, 'indentSelectedRows') + editor.indent() + expect(selection.indentSelectedRows).toHaveBeenCalled() + }) + }) + + describe('if editor.softTabs is false', () => { + it('inserts a tab character into the buffer', () => { + editor.setSoftTabs(false) + expect(buffer.lineForRow(0)).not.toMatch(/^\t/) + editor.indent() + expect(buffer.lineForRow(0)).toMatch(/^\t/) + expect(editor.getCursorBufferPosition()).toEqual([0, 1]) + expect(editor.getCursorScreenPosition()).toEqual([0, editor.getTabLength()]) + + editor.indent() + expect(buffer.lineForRow(0)).toMatch(/^\t\t/) + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + expect(editor.getCursorScreenPosition()).toEqual([0, editor.getTabLength() * 2]) + }) + }) + }) + + describe('clipboard operations', () => { + describe('.cutSelectedText()', () => { + it('removes the selected text from the buffer and places it on the clipboard', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + editor.cutSelectedText() + expect(buffer.lineForRow(0)).toBe('var = function () {') + expect(buffer.lineForRow(1)).toBe(' var = function(items) {') + expect(clipboard.readText()).toBe('quicksort\nsort') + }) + + describe('when no text is selected', () => { + beforeEach(() => + editor.setSelectedBufferRanges([ + [[0, 0], [0, 0]], + [[5, 0], [5, 0]] + ]) + ) + + it('cuts the lines on which there are cursors', () => { + editor.cutSelectedText() + expect(buffer.getLineCount()).toBe(11) + expect(buffer.lineForRow(1)).toBe(' if (items.length <= 1) return items;') + expect(buffer.lineForRow(4)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(atom.clipboard.read()).toEqual([ + 'var quicksort = function () {', + '', + ' current = items.shift();', + '' + ].join('\n')) + }) + }) + + describe('when many selections get added in shuffle order', () => { + it('cuts them in order', () => { + editor.setSelectedBufferRanges([ + [[2, 8], [2, 13]], + [[0, 4], [0, 13]], + [[1, 6], [1, 10]] + ]) + editor.cutSelectedText() + expect(atom.clipboard.read()).toEqual(`quicksort\nsort\nitems`) + }) + }) + }) + + describe('.cutToEndOfLine()', () => { + describe('when soft wrap is on', () => { + it('cuts up to the end of the line', () => { + editor.setSoftWrapped(true) + editor.setDefaultCharWidth(1) + editor.setEditorWidthInChars(25) + editor.setCursorScreenPosition([2, 6]) + editor.cutToEndOfLine() + expect(editor.lineTextForScreenRow(2)).toBe(' var function(items) {') + }) + }) + + describe('when soft wrap is off', () => { + describe('when nothing is selected', () => + it('cuts up to the end of the line', () => { + editor.setCursorBufferPosition([2, 20]) + editor.addCursorAtBufferPosition([3, 20]) + editor.cutToEndOfLine() + expect(buffer.lineForRow(2)).toBe(' if (items.length') + expect(buffer.lineForRow(3)).toBe(' var pivot = item') + expect(atom.clipboard.read()).toBe(' <= 1) return items;\ns.shift(), current, left = [], right = [];') + }) + ) + + describe('when text is selected', () => + it('only cuts the selected text, not to the end of the line', () => { + editor.setSelectedBufferRanges([[[2, 20], [2, 30]], [[3, 20], [3, 20]]]) + editor.cutToEndOfLine() + expect(buffer.lineForRow(2)).toBe(' if (items.lengthurn items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = item') + expect(atom.clipboard.read()).toBe(' <= 1) ret\ns.shift(), current, left = [], right = [];') + }) + ) + }) + }) + + describe('.cutToEndOfBufferLine()', () => { + beforeEach(() => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(10) + }) + + describe('when nothing is selected', () => { + it('cuts up to the end of the buffer line', () => { + editor.setCursorBufferPosition([2, 20]) + editor.addCursorAtBufferPosition([3, 20]) + editor.cutToEndOfBufferLine() + expect(buffer.lineForRow(2)).toBe(' if (items.length') + expect(buffer.lineForRow(3)).toBe(' var pivot = item') + expect(atom.clipboard.read()).toBe(' <= 1) return items;\ns.shift(), current, left = [], right = [];') + }) + }) + + describe('when text is selected', () => { + it('only cuts the selected text, not to the end of the buffer line', () => { + editor.setSelectedBufferRanges([[[2, 20], [2, 30]], [[3, 20], [3, 20]]]) + editor.cutToEndOfBufferLine() + expect(buffer.lineForRow(2)).toBe(' if (items.lengthurn items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = item') + expect(atom.clipboard.read()).toBe(' <= 1) ret\ns.shift(), current, left = [], right = [];') + }) + }) + }) + + describe('.copySelectedText()', () => { + it('copies selected text onto the clipboard', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]]) + editor.copySelectedText() + + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + expect(clipboard.readText()).toBe('quicksort\nsort\nitems') + expect(atom.clipboard.read()).toEqual('quicksort\nsort\nitems') + }) + + describe('when no text is selected', () => { + beforeEach(() => { + editor.setSelectedBufferRanges([ + [[1, 5], [1, 5]], + [[5, 8], [5, 8]] + ]) + }) + + it('copies the lines on which there are cursors', () => { + editor.copySelectedText() + expect(atom.clipboard.read()).toEqual([ + ' var sort = function(items) {\n', + ' current = items.shift();\n' + ].join('\n')) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[1, 5], [1, 5]], + [[5, 8], [5, 8]] + ]) + }) + }) + + describe('when many selections get added in shuffle order', () => { + it('copies them in order', () => { + editor.setSelectedBufferRanges([ + [[2, 8], [2, 13]], + [[0, 4], [0, 13]], + [[1, 6], [1, 10]] + ]) + editor.copySelectedText() + expect(atom.clipboard.read()).toEqual(`quicksort\nsort\nitems`) + }) + }) + }) + + describe('.copyOnlySelectedText()', () => { + describe('when thee are multiple selections', () => { + it('copies selected text onto the clipboard', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]], [[2, 8], [2, 13]]]) + + editor.copyOnlySelectedText() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + expect(clipboard.readText()).toBe('quicksort\nsort\nitems') + expect(atom.clipboard.read()).toEqual(`quicksort\nsort\nitems`) + }) + }) + + describe('when no text is selected', () => { + it('does not copy anything', () => { + editor.setCursorBufferPosition([1, 5]) + editor.copyOnlySelectedText() + expect(atom.clipboard.read()).toEqual('initial clipboard content') + }) + }) + }) + + describe('.pasteText()', () => { + const copyText = function (text, {startColumn, textEditor} = {}) { + if (startColumn == null) startColumn = 0 + if (textEditor == null) textEditor = editor + textEditor.setCursorBufferPosition([0, 0]) + textEditor.insertText(text) + const numberOfNewlines = text.match(/\n/g).length + const endColumn = text.match(/[^\n]*$/)[0].length + textEditor.getLastSelection().setBufferRange([[0, startColumn], [numberOfNewlines, endColumn]]) + return textEditor.cutSelectedText() + } + + it('pastes text into the buffer', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + atom.clipboard.write('first') + editor.pasteText() + expect(editor.lineTextForBufferRow(0)).toBe('var first = function () {') + expect(editor.lineTextForBufferRow(1)).toBe(' var first = function(items) {') + }) + + it('notifies ::onWillInsertText observers', () => { + const insertedStrings = [] + editor.onWillInsertText(function ({text, cancel}) { + insertedStrings.push(text) + cancel() + }) + + atom.clipboard.write('hello') + editor.pasteText() + + expect(insertedStrings).toEqual(['hello']) + }) + + it('notifies ::onDidInsertText observers', () => { + const insertedStrings = [] + editor.onDidInsertText(({text, range}) => insertedStrings.push(text)) + + atom.clipboard.write('hello') + editor.pasteText() + + expect(insertedStrings).toEqual(['hello']) + }) + + describe('when `autoIndentOnPaste` is true', () => { + beforeEach(() => editor.update({autoIndentOnPaste: true})) + + describe('when pasting multiple lines before any non-whitespace characters', () => { + it('auto-indents the lines spanned by the pasted text, based on the first pasted line', () => { + atom.clipboard.write('a(x);\n b(x);\n c(x);\n', {indentBasis: 0}) + editor.setCursorBufferPosition([5, 0]) + editor.pasteText() + + // Adjust the indentation of the pasted lines while preserving + // their indentation relative to each other. Also preserve the + // indentation of the following line. + expect(editor.lineTextForBufferRow(5)).toBe(' a(x);') + expect(editor.lineTextForBufferRow(6)).toBe(' b(x);') + expect(editor.lineTextForBufferRow(7)).toBe(' c(x);') + expect(editor.lineTextForBufferRow(8)).toBe(' current = items.shift();') + }) + + it('auto-indents lines with a mix of hard tabs and spaces without removing spaces', () => { + editor.setSoftTabs(false) + expect(editor.indentationForBufferRow(5)).toBe(3) + + atom.clipboard.write('/**\n\t * testing\n\t * indent\n\t **/\n', {indentBasis: 1}) + editor.setCursorBufferPosition([5, 0]) + editor.pasteText() + + // Do not lose the alignment spaces + expect(editor.lineTextForBufferRow(5)).toBe('\t\t\t/**') + expect(editor.lineTextForBufferRow(6)).toBe('\t\t\t * testing') + expect(editor.lineTextForBufferRow(7)).toBe('\t\t\t * indent') + expect(editor.lineTextForBufferRow(8)).toBe('\t\t\t **/') + }) + }) + + describe('when pasting line(s) above a line that matches the decreaseIndentPattern', () => + it('auto-indents based on the pasted line(s) only', () => { + atom.clipboard.write('a(x);\n b(x);\n c(x);\n', {indentBasis: 0}) + editor.setCursorBufferPosition([7, 0]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(7)).toBe(' a(x);') + expect(editor.lineTextForBufferRow(8)).toBe(' b(x);') + expect(editor.lineTextForBufferRow(9)).toBe(' c(x);') + expect(editor.lineTextForBufferRow(10)).toBe(' }') + }) + ) + + describe('when pasting a line of text without line ending', () => + it('does not auto-indent the text', () => { + atom.clipboard.write('a(x);', {indentBasis: 0}) + editor.setCursorBufferPosition([5, 0]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(5)).toBe('a(x); current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' current < pivot ? left.push(current) : right.push(current);') + }) + ) + + describe('when pasting on a line after non-whitespace characters', () => + it('does not auto-indent the affected line', () => { + // Before the paste, the indentation is non-standard. + editor.setText(dedent`\ + if (x) { + y(); + }\ + `) + + atom.clipboard.write(' z();\n h();') + editor.setCursorBufferPosition([1, Infinity]) + + // The indentation of the non-standard line is unchanged. + editor.pasteText() + expect(editor.lineTextForBufferRow(1)).toBe(' y(); z();') + expect(editor.lineTextForBufferRow(2)).toBe(' h();') + }) + ) + }) + + describe('when `autoIndentOnPaste` is false', () => { + beforeEach(() => editor.update({autoIndentOnPaste: false})) + + describe('when the cursor is indented further than the original copied text', () => + it('increases the indentation of the copied lines to match', () => { + editor.setSelectedBufferRange([[1, 2], [3, 0]]) + editor.copySelectedText() + + editor.setCursorBufferPosition([5, 6]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(5)).toBe(' var sort = function(items) {') + expect(editor.lineTextForBufferRow(6)).toBe(' if (items.length <= 1) return items;') + }) + ) + + describe('when the cursor is indented less far than the original copied text', () => + it('decreases the indentation of the copied lines to match', () => { + editor.setSelectedBufferRange([[6, 6], [8, 0]]) + editor.copySelectedText() + + editor.setCursorBufferPosition([1, 2]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(1)).toBe(' current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(2)).toBe('}') + }) + ) + + describe('when the first copied line has leading whitespace', () => + it("preserves the line's leading whitespace", () => { + editor.setSelectedBufferRange([[4, 0], [6, 0]]) + editor.copySelectedText() + + editor.setCursorBufferPosition([0, 0]) + editor.pasteText() + + expect(editor.lineTextForBufferRow(0)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(1)).toBe(' current = items.shift();') + }) + ) + }) + + describe('when the clipboard has many selections', () => { + beforeEach(() => { + editor.update({autoIndentOnPaste: false}) + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + editor.copySelectedText() + }) + + it('pastes each selection in order separately into the buffer', () => { + editor.setSelectedBufferRanges([ + [[1, 6], [1, 10]], + [[0, 4], [0, 13]] + ]) + + editor.moveRight() + editor.insertText('_') + editor.pasteText() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort_quicksort = function () {') + expect(editor.lineTextForBufferRow(1)).toBe(' var sort_sort = function(items) {') + }) + + describe('and the selections count does not match', () => { + beforeEach(() => editor.setSelectedBufferRanges([[[0, 4], [0, 13]]])) + + it('pastes the whole text into the buffer', () => { + editor.pasteText() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort') + expect(editor.lineTextForBufferRow(1)).toBe('sort = function () {') + }) + }) + }) + + describe('when a full line was cut', () => { + beforeEach(() => { + editor.setCursorBufferPosition([2, 13]) + editor.cutSelectedText() + editor.setCursorBufferPosition([2, 13]) + }) + + it("pastes the line above the cursor and retains the cursor's column", () => { + editor.pasteText() + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.getCursorBufferPosition()).toEqual([3, 13]) + }) + }) + + describe('when a full line was copied', () => { + beforeEach(() => { + editor.setCursorBufferPosition([2, 13]) + editor.copySelectedText() + }) + + describe('when there is a selection', () => + it('overwrites the selection as with any copied text', () => { + editor.setSelectedBufferRange([[1, 2], [1, Infinity]]) + editor.pasteText() + expect(editor.lineTextForBufferRow(1)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(2)).toBe('') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.getCursorBufferPosition()).toEqual([2, 0]) + }) + ) + + describe('when there is no selection', () => + it("pastes the line above the cursor and retains the cursor's column", () => { + editor.pasteText() + expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.lineTextForBufferRow(3)).toBe(' if (items.length <= 1) return items;') + expect(editor.getCursorBufferPosition()).toEqual([3, 13]) + }) + ) + }) + + it('respects options that preserve the formatting of the pasted text', () => { + editor.update({autoIndentOnPaste: true}) + atom.clipboard.write('a(x);\n b(x);\r\nc(x);\n', {indentBasis: 0}) + editor.setCursorBufferPosition([5, 0]) + editor.insertText(' ') + editor.pasteText({autoIndent: false, preserveTrailingLineIndentation: true, normalizeLineEndings: false}) + + expect(editor.lineTextForBufferRow(5)).toBe(' a(x);') + expect(editor.lineTextForBufferRow(6)).toBe(' b(x);') + expect(editor.buffer.lineEndingForRow(6)).toBe('\r\n') + expect(editor.lineTextForBufferRow(7)).toBe('c(x);') + expect(editor.lineTextForBufferRow(8)).toBe(' current = items.shift();') + }) + }) + }) + + describe('.indentSelectedRows()', () => { + describe('when nothing is selected', () => { + describe('when softTabs is enabled', () => { + it('indents line and retains selection', () => { + editor.setSelectedBufferRange([[0, 3], [0, 3]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe(' var quicksort = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 3 + editor.getTabLength()], [0, 3 + editor.getTabLength()]]) + }) + }) + + describe('when softTabs is disabled', () => { + it('indents line and retains selection', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + editor.setSelectedBufferRange([[0, 3], [0, 3]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe('\tvar quicksort = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 3 + 1], [0, 3 + 1]]) + }) + }) + }) + + describe('when one line is selected', () => { + describe('when softTabs is enabled', () => { + it('indents line and retains selection', () => { + editor.setSelectedBufferRange([[0, 4], [0, 14]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe(`${editor.getTabText()}var quicksort = function () {`) + expect(editor.getSelectedBufferRange()).toEqual([[0, 4 + editor.getTabLength()], [0, 14 + editor.getTabLength()]]) + }) + }) + + describe('when softTabs is disabled', () => { + it('indents line and retains selection', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + editor.setSelectedBufferRange([[0, 4], [0, 14]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(0)).toBe('\tvar quicksort = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 4 + 1], [0, 14 + 1]]) + }) + }) + }) + + describe('when multiple lines are selected', () => { + describe('when softTabs is enabled', () => { + it('indents selected lines (that are not empty) and retains selection', () => { + editor.setSelectedBufferRange([[9, 1], [11, 15]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(9)).toBe(' };') + expect(buffer.lineForRow(10)).toBe('') + expect(buffer.lineForRow(11)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.getSelectedBufferRange()).toEqual([[9, 1 + editor.getTabLength()], [11, 15 + editor.getTabLength()]]) + }) + + it('does not indent the last row if the selection ends at column 0', () => { + editor.setSelectedBufferRange([[9, 1], [11, 0]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(9)).toBe(' };') + expect(buffer.lineForRow(10)).toBe('') + expect(buffer.lineForRow(11)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.getSelectedBufferRange()).toEqual([[9, 1 + editor.getTabLength()], [11, 0]]) + }) + }) + + describe('when softTabs is disabled', () => { + it('indents selected lines (that are not empty) and retains selection', () => { + convertToHardTabs(buffer) + editor.setSoftTabs(false) + editor.setSelectedBufferRange([[9, 1], [11, 15]]) + editor.indentSelectedRows() + expect(buffer.lineForRow(9)).toBe('\t\t};') + expect(buffer.lineForRow(10)).toBe('') + expect(buffer.lineForRow(11)).toBe('\t\treturn sort(Array.apply(this, arguments));') + expect(editor.getSelectedBufferRange()).toEqual([[9, 1 + 1], [11, 15 + 1]]) + }) + }) + }) + }) + + describe('.outdentSelectedRows()', () => { + describe('when nothing is selected', () => { + it('outdents line and retains selection', () => { + editor.setSelectedBufferRange([[1, 3], [1, 3]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(1)).toBe('var sort = function(items) {') + expect(editor.getSelectedBufferRange()).toEqual([[1, 3 - editor.getTabLength()], [1, 3 - editor.getTabLength()]]) + }) + + it('outdents when indent is less than a tab length', () => { + editor.insertText(' ') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + }) + + it('outdents a single hard tab when indent is multiple hard tabs and and the session is using soft tabs', () => { + editor.insertText('\t\t') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('\tvar quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + }) + + it('outdents when a mix of hard tabs and soft tabs are used', () => { + editor.insertText('\t ') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe(' var quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe(' var quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + }) + + it('outdents only up to the first non-space non-tab character', () => { + editor.insertText(' \tfoo\t ') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('\tfoo\t var quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('foo\t var quicksort = function () {') + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('foo\t var quicksort = function () {') + }) + }) + + describe('when one line is selected', () => { + it('outdents line and retains editor', () => { + editor.setSelectedBufferRange([[1, 4], [1, 14]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(1)).toBe('var sort = function(items) {') + expect(editor.getSelectedBufferRange()).toEqual([[1, 4 - editor.getTabLength()], [1, 14 - editor.getTabLength()]]) + }) + }) + + describe('when multiple lines are selected', () => { + it('outdents selected lines and retains editor', () => { + editor.setSelectedBufferRange([[0, 1], [3, 15]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe('var sort = function(items) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [3, 15 - editor.getTabLength()]]) + }) + + it('does not outdent the last line of the selection if it ends at column 0', () => { + editor.setSelectedBufferRange([[0, 1], [3, 0]]) + editor.outdentSelectedRows() + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + expect(buffer.lineForRow(1)).toBe('var sort = function(items) {') + expect(buffer.lineForRow(2)).toBe(' if (items.length <= 1) return items;') + expect(buffer.lineForRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [3, 0]]) + }) + }) + }) + + describe('.autoIndentSelectedRows', () => { + it('auto-indents the selection', () => { + editor.setCursorBufferPosition([2, 0]) + editor.insertText('function() {\ninside=true\n}\n i=1\n') + editor.getLastSelection().setBufferRange([[2, 0], [6, 0]]) + editor.autoIndentSelectedRows() + + expect(editor.lineTextForBufferRow(2)).toBe(' function() {') + expect(editor.lineTextForBufferRow(3)).toBe(' inside=true') + expect(editor.lineTextForBufferRow(4)).toBe(' }') + expect(editor.lineTextForBufferRow(5)).toBe(' i=1') + }) + }) + + describe('.undo() and .redo()', () => { + it('undoes/redoes the last change', () => { + editor.insertText('foo') + editor.undo() + expect(buffer.lineForRow(0)).not.toContain('foo') + + editor.redo() + expect(buffer.lineForRow(0)).toContain('foo') + }) + + it('batches the undo / redo of changes caused by multiple cursors', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([1, 0]) + + editor.insertText('foo') + editor.backspace() + + expect(buffer.lineForRow(0)).toContain('fovar') + expect(buffer.lineForRow(1)).toContain('fo ') + + editor.undo() + + expect(buffer.lineForRow(0)).toContain('foo') + expect(buffer.lineForRow(1)).toContain('foo') + + editor.redo() + + expect(buffer.lineForRow(0)).not.toContain('foo') + expect(buffer.lineForRow(0)).toContain('fovar') + }) + + it('restores cursors and selections to their states before and after undone and redone changes', () => { + editor.setSelectedBufferRanges([ + [[0, 0], [0, 0]], + [[1, 0], [1, 3]] + ]) + editor.insertText('abc') + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 3], [0, 3]], + [[1, 3], [1, 3]] + ]) + + editor.setCursorBufferPosition([0, 0]) + editor.setSelectedBufferRanges([ + [[2, 0], [2, 0]], + [[3, 0], [3, 0]], + [[4, 0], [4, 3]] + ]) + editor.insertText('def') + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 3], [2, 3]], + [[3, 3], [3, 3]], + [[4, 3], [4, 3]] + ]) + + editor.setCursorBufferPosition([0, 0]) + editor.undo() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 0], [2, 0]], + [[3, 0], [3, 0]], + [[4, 0], [4, 3]] + ]) + + editor.undo() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 0], [0, 0]], + [[1, 0], [1, 3]] + ]) + + editor.redo() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[0, 3], [0, 3]], + [[1, 3], [1, 3]] + ]) + + editor.redo() + + expect(editor.getSelectedBufferRanges()).toEqual([ + [[2, 3], [2, 3]], + [[3, 3], [3, 3]], + [[4, 3], [4, 3]] + ]) + }) + + it('restores the selected ranges after undo and redo', () => { + editor.setSelectedBufferRanges([[[1, 6], [1, 10]], [[1, 22], [1, 27]]]) + editor.delete() + editor.delete() + + const selections = editor.getSelections() + expect(buffer.lineForRow(1)).toBe(' var = function( {') + + expect(editor.getSelectedBufferRanges()).toEqual([[[1, 6], [1, 6]], [[1, 17], [1, 17]]]) + + editor.undo() + expect(editor.getSelectedBufferRanges()).toEqual([[[1, 6], [1, 6]], [[1, 18], [1, 18]]]) + + editor.undo() + expect(editor.getSelectedBufferRanges()).toEqual([[[1, 6], [1, 10]], [[1, 22], [1, 27]]]) + + editor.redo() + expect(editor.getSelectedBufferRanges()).toEqual([[[1, 6], [1, 6]], [[1, 18], [1, 18]]]) + }) + + xit('restores folds after undo and redo', () => { + editor.foldBufferRow(1) + editor.setSelectedBufferRange([[1, 0], [10, Infinity]], {preserveFolds: true}) + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + + editor.insertText(dedent`\ + // testing + function foo() { + return 1 + 2; + }\ + `) + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + editor.foldBufferRow(2) + + editor.undo() + expect(editor.isFoldedAtBufferRow(1)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(9)).toBeTruthy() + expect(editor.isFoldedAtBufferRow(10)).toBeFalsy() + + editor.redo() + expect(editor.isFoldedAtBufferRow(1)).toBeFalsy() + expect(editor.isFoldedAtBufferRow(2)).toBeTruthy() + }) + }) + + describe('::transact', () => { + it('restores the selection when the transaction is undone/redone', () => { + buffer.setText('1234') + editor.setSelectedBufferRange([[0, 1], [0, 3]]) + + editor.transact(() => { + editor.delete() + editor.moveToEndOfLine() + editor.insertText('5') + expect(buffer.getText()).toBe('145') + }) + + editor.undo() + expect(buffer.getText()).toBe('1234') + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [0, 3]]) + + editor.redo() + expect(buffer.getText()).toBe('145') + expect(editor.getSelectedBufferRange()).toEqual([[0, 3], [0, 3]]) + }) + }) + + describe('when the buffer is changed (via its direct api, rather than via than edit session)', () => { + it('moves the cursor so it is in the same relative position of the buffer', () => { + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + editor.addCursorAtScreenPosition([0, 5]) + editor.addCursorAtScreenPosition([1, 0]) + const [cursor1, cursor2, cursor3] = editor.getCursors() + + buffer.insert([0, 1], 'abc') + + expect(cursor1.getScreenPosition()).toEqual([0, 0]) + expect(cursor2.getScreenPosition()).toEqual([0, 8]) + expect(cursor3.getScreenPosition()).toEqual([1, 0]) + }) + + it('does not destroy cursors or selections when a change encompasses them', () => { + const cursor = editor.getLastCursor() + cursor.setBufferPosition([3, 3]) + editor.buffer.delete([[3, 1], [3, 5]]) + expect(cursor.getBufferPosition()).toEqual([3, 1]) + expect(editor.getCursors().indexOf(cursor)).not.toBe(-1) + + const selection = editor.getLastSelection() + selection.setBufferRange([[3, 5], [3, 10]]) + editor.buffer.delete([[3, 3], [3, 8]]) + expect(selection.getBufferRange()).toEqual([[3, 3], [3, 5]]) + expect(editor.getSelections().indexOf(selection)).not.toBe(-1) + }) + + it('merges cursors when the change causes them to overlap', () => { + editor.setCursorScreenPosition([0, 0]) + editor.addCursorAtScreenPosition([0, 2]) + editor.addCursorAtScreenPosition([1, 2]) + + const [cursor1, cursor2, cursor3] = editor.getCursors() + expect(editor.getCursors().length).toBe(3) + + buffer.delete([[0, 0], [0, 2]]) + + expect(editor.getCursors().length).toBe(2) + expect(editor.getCursors()).toEqual([cursor1, cursor3]) + expect(cursor1.getBufferPosition()).toEqual([0, 0]) + expect(cursor3.getBufferPosition()).toEqual([1, 2]) + }) + }) + + describe('.moveSelectionLeft()', () => { + it('moves one active selection on one line one column to the left', () => { + editor.setSelectedBufferRange([[0, 4], [0, 13]]) + expect(editor.getSelectedText()).toBe('quicksort') + + editor.moveSelectionLeft() + + expect(editor.getSelectedText()).toBe('quicksort') + expect(editor.getSelectedBufferRange()).toEqual([[0, 3], [0, 12]]) + }) + + it('moves multiple active selections on one line one column to the left', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('function') + + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('function') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 3], [0, 12]], [[0, 15], [0, 23]]]) + }) + + it('moves multiple active selections on multiple lines one column to the left', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('sort') + + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('sort') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 3], [0, 12]], [[1, 5], [1, 9]]]) + }) + + describe('when a selection is at the first column of a line', () => { + it('does not change the selection', () => { + editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[1, 0], [1, 3]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('var') + expect(selections[1].getText()).toBe(' v') + + editor.moveSelectionLeft() + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe('var') + expect(selections[1].getText()).toBe(' v') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 3]], [[1, 0], [1, 3]]]) + }) + + describe('when multiple selections are active on one line', () => { + it('does not change the selection', () => { + editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[0, 4], [0, 13]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('var') + expect(selections[1].getText()).toBe('quicksort') + + editor.moveSelectionLeft() + + expect(selections[0].getText()).toBe('var') + expect(selections[1].getText()).toBe('quicksort') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [0, 3]], [[0, 4], [0, 13]]]) + }) + }) + }) + }) + + describe('.moveSelectionRight()', () => { + it('moves one active selection on one line one column to the right', () => { + editor.setSelectedBufferRange([[0, 4], [0, 13]]) + expect(editor.getSelectedText()).toBe('quicksort') + + editor.moveSelectionRight() + + expect(editor.getSelectedText()).toBe('quicksort') + expect(editor.getSelectedBufferRange()).toEqual([[0, 5], [0, 14]]) + }) + + it('moves multiple active selections on one line one column to the right', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('function') + + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('function') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 5], [0, 14]], [[0, 17], [0, 25]]]) + }) + + it('moves multiple active selections on multiple lines one column to the right', () => { + editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('sort') + + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe('quicksort') + expect(selections[1].getText()).toBe('sort') + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 5], [0, 14]], [[1, 7], [1, 11]]]) + }) + + describe('when a selection is at the last column of a line', () => { + it('does not change the selection', () => { + editor.setSelectedBufferRanges([[[2, 34], [2, 40]], [[5, 22], [5, 30]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('items;') + expect(selections[1].getText()).toBe('shift();') + + editor.moveSelectionRight() + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe('items;') + expect(selections[1].getText()).toBe('shift();') + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 34], [2, 40]], [[5, 22], [5, 30]]]) + }) + + describe('when multiple selections are active on one line', () => { + it('does not change the selection', () => { + editor.setSelectedBufferRanges([[[2, 27], [2, 33]], [[2, 34], [2, 40]]]) + const selections = editor.getSelections() + + expect(selections[0].getText()).toBe('return') + expect(selections[1].getText()).toBe('items;') + + editor.moveSelectionRight() + + expect(selections[0].getText()).toBe('return') + expect(selections[1].getText()).toBe('items;') + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 27], [2, 33]], [[2, 34], [2, 40]]]) + }) + }) + }) + }) + }) + + describe('reading text', () => { + it('.lineTextForScreenRow(row)', () => { + editor.foldBufferRow(4) + expect(editor.lineTextForScreenRow(5)).toEqual(' return sort(left).concat(pivot).concat(sort(right));') + expect(editor.lineTextForScreenRow(9)).toEqual('};') + expect(editor.lineTextForScreenRow(10)).toBeUndefined() + }) + }) + + describe('.deleteLine()', () => { + it('deletes the first line when the cursor is there', () => { + editor.getLastCursor().moveToTop() + const line1 = buffer.lineForRow(1) + const count = buffer.getLineCount() + expect(buffer.lineForRow(0)).not.toBe(line1) + editor.deleteLine() + expect(buffer.lineForRow(0)).toBe(line1) + expect(buffer.getLineCount()).toBe(count - 1) + }) + + it('deletes the last line when the cursor is there', () => { + const count = buffer.getLineCount() + const secondToLastLine = buffer.lineForRow(count - 2) + expect(buffer.lineForRow(count - 1)).not.toBe(secondToLastLine) + editor.getLastCursor().moveToBottom() + editor.deleteLine() + const newCount = buffer.getLineCount() + expect(buffer.lineForRow(newCount - 1)).toBe(secondToLastLine) + expect(newCount).toBe(count - 1) + }) + + it('deletes whole lines when partial lines are selected', () => { + editor.setSelectedBufferRange([[0, 2], [1, 2]]) + const line2 = buffer.lineForRow(2) + const count = buffer.getLineCount() + expect(buffer.lineForRow(0)).not.toBe(line2) + expect(buffer.lineForRow(1)).not.toBe(line2) + editor.deleteLine() + expect(buffer.lineForRow(0)).toBe(line2) + expect(buffer.getLineCount()).toBe(count - 2) + }) + + it('deletes a line only once when multiple selections are on the same line', () => { + const line1 = buffer.lineForRow(1) + const count = buffer.getLineCount() + editor.setSelectedBufferRanges([ + [[0, 1], [0, 2]], + [[0, 4], [0, 5]] + ]) + expect(buffer.lineForRow(0)).not.toBe(line1) + + editor.deleteLine() + + expect(buffer.lineForRow(0)).toBe(line1) + expect(buffer.getLineCount()).toBe(count - 1) + }) + + it('only deletes first line if only newline is selected on second line', () => { + editor.setSelectedBufferRange([[0, 2], [1, 0]]) + const line1 = buffer.lineForRow(1) + const count = buffer.getLineCount() + expect(buffer.lineForRow(0)).not.toBe(line1) + editor.deleteLine() + expect(buffer.lineForRow(0)).toBe(line1) + expect(buffer.getLineCount()).toBe(count - 1) + }) + + it('deletes the entire region when invoke on a folded region', () => { + editor.foldBufferRow(1) + editor.getLastCursor().moveToTop() + editor.getLastCursor().moveDown() + expect(buffer.getLineCount()).toBe(13) + editor.deleteLine() + expect(buffer.getLineCount()).toBe(4) + }) + + it('deletes the entire file from the bottom up', () => { + const count = buffer.getLineCount() + expect(count).toBeGreaterThan(0) + for (let i = 0; i < count; i++) { + editor.getLastCursor().moveToBottom() + editor.deleteLine() + } + expect(buffer.getLineCount()).toBe(1) + expect(buffer.getText()).toBe('') + }) + + it('deletes the entire file from the top down', () => { + const count = buffer.getLineCount() + expect(count).toBeGreaterThan(0) + for (let i = 0; i < count; i++) { + editor.getLastCursor().moveToTop() + editor.deleteLine() + } + expect(buffer.getLineCount()).toBe(1) + expect(buffer.getText()).toBe('') + }) + + describe('when soft wrap is enabled', () => { + it('deletes the entire line that the cursor is on', () => { + editor.setSoftWrapped(true) + editor.setEditorWidthInChars(10) + editor.setCursorBufferPosition([6]) + + const line7 = buffer.lineForRow(7) + const count = buffer.getLineCount() + expect(buffer.lineForRow(6)).not.toBe(line7) + editor.deleteLine() + expect(buffer.lineForRow(6)).toBe(line7) + expect(buffer.getLineCount()).toBe(count - 1) + }) + }) + + describe('when the line being deleted precedes a fold, and the command is undone', () => { + it('restores the line and preserves the fold', () => { + editor.setCursorBufferPosition([4]) + editor.foldCurrentRow() + expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() + editor.setCursorBufferPosition([3]) + editor.deleteLine() + expect(editor.isFoldedAtScreenRow(3)).toBeTruthy() + expect(buffer.lineForRow(3)).toBe(' while(items.length > 0) {') + editor.undo() + expect(editor.isFoldedAtScreenRow(4)).toBeTruthy() + expect(buffer.lineForRow(3)).toBe(' var pivot = items.shift(), current, left = [], right = [];') + }) + }) + }) + + describe('.replaceSelectedText(options, fn)', () => { + describe('when no text is selected', () => { + it('inserts the text returned from the function at the cursor position', () => { + editor.replaceSelectedText({}, () => '123') + expect(buffer.lineForRow(0)).toBe('123var quicksort = function () {') + + editor.setCursorBufferPosition([0]) + editor.replaceSelectedText({selectWordIfEmpty: true}, () => 'var') + expect(buffer.lineForRow(0)).toBe('var quicksort = function () {') + + editor.setCursorBufferPosition([10]) + editor.replaceSelectedText(null, () => '') + expect(buffer.lineForRow(10)).toBe('') + }) + }) + + describe('when text is selected', () => { + it('replaces the selected text with the text returned from the function', () => { + editor.setSelectedBufferRange([[0, 1], [0, 3]]) + editor.replaceSelectedText({}, () => 'ia') + expect(buffer.lineForRow(0)).toBe('via quicksort = function () {') + }) + + it('replaces the selected text and selects the replacement text', () => { + editor.setSelectedBufferRange([[0, 4], [0, 9]]) + editor.replaceSelectedText({}, () => 'whatnot') + expect(buffer.lineForRow(0)).toBe('var whatnotsort = function () {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 4], [0, 11]]) + }) + }) + }) + + describe('.transpose()', () => { + it('swaps two characters', () => { + editor.buffer.setText('abc') + editor.setCursorScreenPosition([0, 1]) + editor.transpose() + expect(editor.lineTextForBufferRow(0)).toBe('bac') + }) + + it('reverses a selection', () => { + editor.buffer.setText('xabcz') + editor.setSelectedBufferRange([[0, 1], [0, 4]]) + editor.transpose() + expect(editor.lineTextForBufferRow(0)).toBe('xcbaz') + }) + }) + + describe('.upperCase()', () => { + describe('when there is no selection', () => { + it('upper cases the current word', () => { + editor.buffer.setText('aBc') + editor.setCursorScreenPosition([0, 1]) + editor.upperCase() + expect(editor.lineTextForBufferRow(0)).toBe('ABC') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 3]]) + }) + }) + + describe('when there is a selection', () => { + it('upper cases the current selection', () => { + editor.buffer.setText('abc') + editor.setSelectedBufferRange([[0, 0], [0, 2]]) + editor.upperCase() + expect(editor.lineTextForBufferRow(0)).toBe('ABc') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 2]]) + }) + }) + }) + + describe('.lowerCase()', () => { + describe('when there is no selection', () => { + it('lower cases the current word', () => { + editor.buffer.setText('aBC') + editor.setCursorScreenPosition([0, 1]) + editor.lowerCase() + expect(editor.lineTextForBufferRow(0)).toBe('abc') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 3]]) + }) + }) + + describe('when there is a selection', () => { + it('lower cases the current selection', () => { + editor.buffer.setText('ABC') + editor.setSelectedBufferRange([[0, 0], [0, 2]]) + editor.lowerCase() + expect(editor.lineTextForBufferRow(0)).toBe('abC') + expect(editor.getSelectedBufferRange()).toEqual([[0, 0], [0, 2]]) + }) + }) + }) + + describe('.setTabLength(tabLength)', () => { + it('clips atomic soft tabs to the given tab length', () => { + expect(editor.getTabLength()).toBe(2) + expect(editor.clipScreenPosition([5, 1], {clipDirection: 'forward'})).toEqual([5, 2]) + + editor.setTabLength(6) + expect(editor.getTabLength()).toBe(6) + expect(editor.clipScreenPosition([5, 1], {clipDirection: 'forward'})).toEqual([5, 6]) + + const changeHandler = jasmine.createSpy('changeHandler') + editor.onDidChange(changeHandler) + editor.setTabLength(6) + expect(changeHandler).not.toHaveBeenCalled() + }) + + it('does not change its tab length when the given tab length is null', () => { + editor.setTabLength(4) + editor.setTabLength(null) + expect(editor.getTabLength()).toBe(4) + }) + }) + + describe('.indentLevelForLine(line)', () => { + it('returns the indent level when the line has only leading whitespace', () => { + expect(editor.indentLevelForLine(' hello')).toBe(2) + expect(editor.indentLevelForLine(' hello')).toBe(1.5) + }) + + it('returns the indent level when the line has only leading tabs', () => expect(editor.indentLevelForLine('\t\thello')).toBe(2)) + + it('returns the indent level based on the character starting the line when the leading whitespace contains both spaces and tabs', () => { + expect(editor.indentLevelForLine('\t hello')).toBe(2) + expect(editor.indentLevelForLine(' \thello')).toBe(2) + expect(editor.indentLevelForLine(' \t hello')).toBe(2.5) + expect(editor.indentLevelForLine(' \t \thello')).toBe(4) + expect(editor.indentLevelForLine(' \t \thello')).toBe(4) + expect(editor.indentLevelForLine(' \t \t hello')).toBe(4.5) + }) + }) + + describe('when a better-matched grammar is added to syntax', () => { + it('switches to the better-matched grammar and re-tokenizes the buffer', async () => { + editor.destroy() + + const jsGrammar = atom.grammars.selectGrammar('a.js') + atom.grammars.removeGrammar(jsGrammar) + + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + + expect(editor.getGrammar()).toBe(atom.grammars.nullGrammar) + expect(editor.tokensForScreenRow(0).length).toBe(1) + + atom.grammars.addGrammar(jsGrammar) + expect(editor.getGrammar()).toBe(jsGrammar) + expect(editor.tokensForScreenRow(0).length).toBeGreaterThan(1) + }) + }) + + describe('editor.autoIndent', () => { + describe('when editor.autoIndent is false (default)', () => { + describe('when `indent` is triggered', () => { + it('does not auto-indent the line', () => { + editor.setCursorBufferPosition([1, 30]) + editor.insertText('\n ') + expect(editor.lineTextForBufferRow(2)).toBe(' ') + + editor.update({autoIndent: false}) + editor.indent() + expect(editor.lineTextForBufferRow(2)).toBe(' ') + }) + }) + }) + + describe('when editor.autoIndent is true', () => { + beforeEach(() => editor.update({autoIndent: true})) + + describe('when `indent` is triggered', () => { + it('auto-indents the line', () => { + editor.setCursorBufferPosition([1, 30]) + editor.insertText('\n ') + expect(editor.lineTextForBufferRow(2)).toBe(' ') + + editor.update({autoIndent: true}) + editor.indent() + expect(editor.lineTextForBufferRow(2)).toBe(' ') + }) + }) + + describe('when a newline is added', () => { + describe('when the line preceding the newline adds a new level of indentation', () => { + it('indents the newline to one additional level of indentation beyond the preceding line', () => { + editor.setCursorBufferPosition([1, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1) + 1) + }) + }) + + describe("when the line preceding the newline doesn't add a level of indentation", () => { + it('indents the new line to the same level as the preceding line', () => { + editor.setCursorBufferPosition([5, 14]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(6)).toBe(editor.indentationForBufferRow(5)) + }) + }) + + describe('when the line preceding the newline is a comment', () => { + it('maintains the indent of the commented line', () => { + editor.setCursorBufferPosition([0, 0]) + editor.insertText(' //') + editor.setCursorBufferPosition([0, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(1)).toBe(2) + }) + }) + + describe('when the line preceding the newline contains only whitespace', () => { + it("bases the new line's indentation on only the preceding line", () => { + editor.setCursorBufferPosition([6, Infinity]) + editor.insertText('\n ') + expect(editor.getCursorBufferPosition()).toEqual([7, 2]) + + editor.insertNewline() + expect(editor.lineTextForBufferRow(8)).toBe(' ') + }) + }) + + it('does not indent the line preceding the newline', () => { + editor.setCursorBufferPosition([2, 0]) + editor.insertText(' var this-line-should-be-indented-more\n') + expect(editor.indentationForBufferRow(1)).toBe(1) + + editor.update({autoIndent: true}) + editor.setCursorBufferPosition([2, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(1)).toBe(1) + expect(editor.indentationForBufferRow(2)).toBe(1) + }) + + describe('when the cursor is before whitespace', () => { + it('retains the whitespace following the cursor on the new line', () => { + editor.setText(' var sort = function() {}') + editor.setCursorScreenPosition([0, 12]) + editor.insertNewline() + + expect(buffer.lineForRow(0)).toBe(' var sort =') + expect(buffer.lineForRow(1)).toBe(' function() {}') + expect(editor.getCursorScreenPosition()).toEqual([1, 2]) + }) + }) + }) + + describe('when inserted text matches a decrease indent pattern', () => { + describe('when the preceding line matches an increase indent pattern', () => { + it('decreases the indentation to match that of the preceding line', () => { + editor.setCursorBufferPosition([1, Infinity]) + editor.insertText('\n') + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1) + 1) + editor.insertText('}') + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1)) + }) + }) + + describe("when the preceding line doesn't match an increase indent pattern", () => { + it('decreases the indentation to be one level below that of the preceding line', () => { + editor.setCursorBufferPosition([3, Infinity]) + editor.insertText('\n ') + expect(editor.indentationForBufferRow(4)).toBe(editor.indentationForBufferRow(3)) + editor.insertText('}') + expect(editor.indentationForBufferRow(4)).toBe(editor.indentationForBufferRow(3) - 1) + }) + + it("doesn't break when decreasing the indentation on a row that has no indentation", () => { + editor.setCursorBufferPosition([12, Infinity]) + editor.insertText('\n}; # too many closing brackets!') + expect(editor.lineTextForBufferRow(13)).toBe('}; # too many closing brackets!') + }) + }) + }) + + describe('when inserted text does not match a decrease indent pattern', () => { + it('does not decrease the indentation', () => { + editor.setCursorBufferPosition([12, 0]) + editor.insertText(' ') + expect(editor.lineTextForBufferRow(12)).toBe(' };') + editor.insertText('\t\t') + expect(editor.lineTextForBufferRow(12)).toBe(' \t\t};') + }) + }) + + describe('when the current line does not match a decrease indent pattern', () => { + it('leaves the line unchanged', () => { + editor.setCursorBufferPosition([2, 4]) + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1) + 1) + editor.insertText('foo') + expect(editor.indentationForBufferRow(2)).toBe(editor.indentationForBufferRow(1) + 1) + }) + }) + }) + }) + + describe('atomic soft tabs', () => { + it('skips tab-length runs of leading whitespace when moving the cursor', () => { + editor.update({tabLength: 4, atomicSoftTabs: true}) + + editor.setCursorScreenPosition([2, 3]) + expect(editor.getCursorScreenPosition()).toEqual([2, 4]) + + editor.update({atomicSoftTabs: false}) + editor.setCursorScreenPosition([2, 3]) + expect(editor.getCursorScreenPosition()).toEqual([2, 3]) + + editor.update({atomicSoftTabs: true}) + editor.setCursorScreenPosition([2, 3]) + expect(editor.getCursorScreenPosition()).toEqual([2, 4]) + }) + }) + + describe('.destroy()', () => { + it('destroys marker layers associated with the text editor', () => { + buffer.retain() + const selectionsMarkerLayerId = editor.selectionsMarkerLayer.id + const foldsMarkerLayerId = editor.displayLayer.foldsMarkerLayer.id + editor.destroy() + expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined() + expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined() + buffer.release() + }) + + it('notifies ::onDidDestroy observers when the editor is destroyed', () => { + let destroyObserverCalled = false + editor.onDidDestroy(() => destroyObserverCalled = true) + + editor.destroy() + expect(destroyObserverCalled).toBe(true) + }) + + it('does not blow up when query methods are called afterward', () => { + editor.destroy() + editor.getGrammar() + editor.getLastCursor() + editor.lineTextForBufferRow(0) + }) + + it("emits the destroy event after destroying the editor's buffer", () => { + const events = [] + editor.getBuffer().onDidDestroy(() => { + expect(editor.isDestroyed()).toBe(true) + events.push('buffer-destroyed') + }) + editor.onDidDestroy(() => { + expect(buffer.isDestroyed()).toBe(true) + events.push('editor-destroyed') + }) + editor.destroy() + expect(events).toEqual(['buffer-destroyed', 'editor-destroyed']) + }) + }) + + describe('.joinLines()', () => { + describe('when no text is selected', () => { + describe("when the line below isn't empty", () => { + it('joins the line below with the current line separated by a space and moves the cursor to the start of line that was moved up', () => { + editor.setCursorBufferPosition([0, Infinity]) + editor.insertText(' ') + editor.setCursorBufferPosition([0]) + editor.joinLines() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () { var sort = function(items) {') + expect(editor.getCursorBufferPosition()).toEqual([0, 29]) + }) + }) + + describe('when the line below is empty', () => { + it('deletes the line below and moves the cursor to the end of the line', () => { + editor.setCursorBufferPosition([9]) + editor.joinLines() + expect(editor.lineTextForBufferRow(9)).toBe(' };') + expect(editor.lineTextForBufferRow(10)).toBe(' return sort(Array.apply(this, arguments));') + expect(editor.getCursorBufferPosition()).toEqual([9, 4]) + }) + }) + + describe('when the cursor is on the last row', () => { + it('does nothing', () => { + editor.setCursorBufferPosition([Infinity, Infinity]) + editor.joinLines() + expect(editor.lineTextForBufferRow(12)).toBe('};') + }) + }) + + describe('when the line is empty', () => { + it('joins the line below with the current line with no added space', () => { + editor.setCursorBufferPosition([10]) + editor.joinLines() + expect(editor.lineTextForBufferRow(10)).toBe('return sort(Array.apply(this, arguments));') + expect(editor.getCursorBufferPosition()).toEqual([10, 0]) + }) + }) + }) + + describe('when text is selected', () => { + describe('when the selection does not span multiple lines', () => { + it('joins the line below with the current line separated by a space and retains the selected text', () => { + editor.setSelectedBufferRange([[0, 1], [0, 3]]) + editor.joinLines() + expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () { var sort = function(items) {') + expect(editor.getSelectedBufferRange()).toEqual([[0, 1], [0, 3]]) + }) + }) + + describe('when the selection spans multiple lines', () => { + it('joins all selected lines separated by a space and retains the selected text', () => { + editor.setSelectedBufferRange([[9, 3], [12, 1]]) + editor.joinLines() + expect(editor.lineTextForBufferRow(9)).toBe(' }; return sort(Array.apply(this, arguments)); };') + expect(editor.getSelectedBufferRange()).toEqual([[9, 3], [9, 49]]) + }) + }) + }) + }) + + describe('.duplicateLines()', () => { + it('for each selection, duplicates all buffer lines intersected by the selection', () => { + editor.foldBufferRow(4) + editor.setCursorBufferPosition([2, 5]) + editor.addSelectionForBufferRange([[3, 0], [8, 0]], {preserveFolds: true}) + + editor.duplicateLines() + + expect(editor.getTextInBufferRange([[2, 0], [13, 5]])).toBe(`\ +\ if (items.length <= 1) return items; + if (items.length <= 1) return items; + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + } + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + }\ +` + ) + expect(editor.getSelectedBufferRanges()).toEqual([[[3, 5], [3, 5]], [[9, 0], [14, 0]]]) + + // folds are also duplicated + expect(editor.isFoldedAtScreenRow(5)).toBe(true) + expect(editor.isFoldedAtScreenRow(7)).toBe(true) + expect(editor.lineTextForScreenRow(7)).toBe(` while(items.length > 0) {${editor.displayLayer.foldCharacter}`) + expect(editor.lineTextForScreenRow(8)).toBe(' return sort(left).concat(pivot).concat(sort(right));') + }) + + it('duplicates all folded lines for empty selections on lines containing folds', () => { + editor.foldBufferRow(4) + editor.setCursorBufferPosition([4, 0]) + + editor.duplicateLines() + + expect(editor.getTextInBufferRange([[2, 0], [11, 5]])).toBe(`\ +\ if (items.length <= 1) return items; + var pivot = items.shift(), current, left = [], right = []; + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + } + while(items.length > 0) { + current = items.shift(); + current < pivot ? left.push(current) : right.push(current); + }\ +` + ) + expect(editor.getSelectedBufferRange()).toEqual([[8, 0], [8, 0]]) + }) + + it('can duplicate the last line of the buffer', () => { + editor.setSelectedBufferRange([[11, 0], [12, 2]]) + editor.duplicateLines() + expect(editor.getTextInBufferRange([[11, 0], [14, 2]])).toBe(`\ +\ return sort(Array.apply(this, arguments)); +}; + return sort(Array.apply(this, arguments)); +};\ +` + ) + expect(editor.getSelectedBufferRange()).toEqual([[13, 0], [14, 2]]) + }) + + it('only duplicates lines containing multiple selections once', () => { + editor.setText(`\ +aaaaaa +bbbbbb +cccccc +dddddd\ +`) + editor.setSelectedBufferRanges([ + [[0, 1], [0, 2]], + [[0, 3], [0, 4]], + [[2, 1], [2, 2]], + [[2, 3], [3, 1]], + [[3, 3], [3, 4]] + ]) + editor.duplicateLines() + expect(editor.getText()).toBe(`\ +aaaaaa +aaaaaa +bbbbbb +cccccc +dddddd +cccccc +dddddd\ +`) + expect(editor.getSelectedBufferRanges()).toEqual([ + [[1, 1], [1, 2]], + [[1, 3], [1, 4]], + [[5, 1], [5, 2]], + [[5, 3], [6, 1]], + [[6, 3], [6, 4]] + ]) + }) + }) + + describe('when the editor contains surrogate pair characters', () => { + it('correctly backspaces over them', () => { + editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') + editor.moveToBottom() + editor.backspace() + expect(editor.getText()).toBe('\uD835\uDF97\uD835\uDF97') + editor.backspace() + expect(editor.getText()).toBe('\uD835\uDF97') + editor.backspace() + expect(editor.getText()).toBe('') + }) + + it('correctly deletes over them', () => { + editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') + editor.moveToTop() + editor.delete() + expect(editor.getText()).toBe('\uD835\uDF97\uD835\uDF97') + editor.delete() + expect(editor.getText()).toBe('\uD835\uDF97') + editor.delete() + expect(editor.getText()).toBe('') + }) + + it('correctly moves over them', () => { + editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97\n') + editor.moveToTop() + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('when the editor contains variation sequence character pairs', () => { + it('correctly backspaces over them', () => { + editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') + editor.moveToBottom() + editor.backspace() + expect(editor.getText()).toBe('\u2714\uFE0E\u2714\uFE0E') + editor.backspace() + expect(editor.getText()).toBe('\u2714\uFE0E') + editor.backspace() + expect(editor.getText()).toBe('') + }) + + it('correctly deletes over them', () => { + editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E') + editor.moveToTop() + editor.delete() + expect(editor.getText()).toBe('\u2714\uFE0E\u2714\uFE0E') + editor.delete() + expect(editor.getText()).toBe('\u2714\uFE0E') + editor.delete() + expect(editor.getText()).toBe('') + }) + + it('correctly moves over them', () => { + editor.setText('\u2714\uFE0E\u2714\uFE0E\u2714\uFE0E\n') + editor.moveToTop() + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + editor.moveRight() + expect(editor.getCursorBufferPosition()).toEqual([1, 0]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 6]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 4]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 2]) + editor.moveLeft() + expect(editor.getCursorBufferPosition()).toEqual([0, 0]) + }) + }) + + describe('.setIndentationForBufferRow', () => { + describe('when the editor uses soft tabs but the row has hard tabs', () => { + it('only replaces whitespace characters', () => { + editor.setSoftWrapped(true) + editor.setText('\t1\n\t2') + editor.setCursorBufferPosition([0, 0]) + editor.setIndentationForBufferRow(0, 2) + expect(editor.getText()).toBe(' 1\n\t2') + }) + }) + + describe('when the indentation level is a non-integer', () => { + it('does not throw an exception', () => { + editor.setSoftWrapped(true) + editor.setText('\t1\n\t2') + editor.setCursorBufferPosition([0, 0]) + editor.setIndentationForBufferRow(0, 2.1) + expect(editor.getText()).toBe(' 1\n\t2') + }) + }) + }) + + describe("when the editor's grammar has an injection selector", () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-text') + await atom.packages.activatePackage('language-javascript') + }) + + it("includes the grammar's patterns when the selector matches the current scope in other grammars", async () => { + await atom.packages.activatePackage('language-hyperlink') + + const grammar = atom.grammars.selectGrammar('text.js') + const {line, tags} = grammar.tokenizeLine('var i; // http://github.com') + + const tokens = atom.grammars.decodeTokens(line, tags) + expect(tokens[0].value).toBe('var') + expect(tokens[0].scopes).toEqual(['source.js', 'storage.type.var.js']) + expect(tokens[6].value).toBe('http://github.com') + expect(tokens[6].scopes).toEqual(['source.js', 'comment.line.double-slash.js', 'markup.underline.link.http.hyperlink']) + }) + + describe('when the grammar is added', () => { + it('retokenizes existing buffers that contain tokens that match the injection selector', async () => { + editor = await atom.workspace.open('sample.js') + editor.setText('// http://github.com') + let tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + ]) + + await atom.packages.activatePackage('language-hyperlink') + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: 'http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--markup syntax--underline syntax--link syntax--http syntax--hyperlink']} + ]) + }) + + describe('when the grammar is updated', () => { + it('retokenizes existing buffers that contain tokens that match the injection selector', async () => { + editor = await atom.workspace.open('sample.js') + editor.setText('// SELECT * FROM OCTOCATS') + let tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + ]) + + await atom.packages.activatePackage('package-with-injection-selector') + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + ]) + + await atom.packages.activatePackage('language-sql') + tokens = editor.tokensForScreenRow(0) + expect(tokens).toEqual([ + {text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: 'SELECT', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: '*', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--operator syntax--star syntax--sql']}, + {text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}, + {text: 'FROM', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']}, + {text: ' OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']} + ]) + }) + }) + }) + }) + + describe('.normalizeTabsInBufferRange()', () => { + it("normalizes tabs depending on the editor's soft tab/tab length settings", () => { + editor.setTabLength(1) + editor.setSoftTabs(true) + editor.setText('\t\t\t') + editor.normalizeTabsInBufferRange([[0, 0], [0, 1]]) + expect(editor.getText()).toBe(' \t\t') + + editor.setTabLength(2) + editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) + expect(editor.getText()).toBe(' ') + + editor.setSoftTabs(false) + editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]]) + expect(editor.getText()).toBe(' ') + }) + }) + + describe('.pageUp/Down()', () => { + it('moves the cursor down one page length', () => { + editor.update({autoHeight: false}) + const element = editor.getElement() + jasmine.attachToDOM(element) + element.style.height = (element.component.getLineHeight() * 5) + 'px' + element.measureDimensions() + + expect(editor.getCursorBufferPosition().row).toBe(0) + + editor.pageDown() + expect(editor.getCursorBufferPosition().row).toBe(5) + + editor.pageDown() + expect(editor.getCursorBufferPosition().row).toBe(10) + + editor.pageUp() + expect(editor.getCursorBufferPosition().row).toBe(5) + + editor.pageUp() + expect(editor.getCursorBufferPosition().row).toBe(0) + }) + }) + + describe('.selectPageUp/Down()', () => { + it('selects one screen height of text up or down', () => { + editor.update({autoHeight: false}) + const element = editor.getElement() + jasmine.attachToDOM(element) + element.style.height = (element.component.getLineHeight() * 5) + 'px' + element.measureDimensions() + + expect(editor.getCursorBufferPosition().row).toBe(0) + + editor.selectPageDown() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [5, 0]]]) + + editor.selectPageDown() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [10, 0]]]) + + editor.selectPageDown() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [12, 2]]]) + + editor.moveToBottom() + editor.selectPageUp() + expect(editor.getSelectedBufferRanges()).toEqual([[[7, 0], [12, 2]]]) + + editor.selectPageUp() + expect(editor.getSelectedBufferRanges()).toEqual([[[2, 0], [12, 2]]]) + + editor.selectPageUp() + expect(editor.getSelectedBufferRanges()).toEqual([[[0, 0], [12, 2]]]) + }) + }) + + describe('::scrollToScreenPosition(position, [options])', () => { + it('triggers ::onDidRequestAutoscroll with the logical coordinates along with the options', () => { + const scrollSpy = jasmine.createSpy('::onDidRequestAutoscroll') + editor.onDidRequestAutoscroll(scrollSpy) + + editor.scrollToScreenPosition([8, 20]) + editor.scrollToScreenPosition([8, 20], {center: true}) + editor.scrollToScreenPosition([8, 20], {center: false, reversed: true}) + + expect(scrollSpy).toHaveBeenCalledWith({screenRange: [[8, 20], [8, 20]], options: {}}) + expect(scrollSpy).toHaveBeenCalledWith({screenRange: [[8, 20], [8, 20]], options: {center: true}}) + expect(scrollSpy).toHaveBeenCalledWith({screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true}}) + }) + }) + + describe('scroll past end', () => { + it('returns false by default but can be customized', () => { + expect(editor.getScrollPastEnd()).toBe(false) + editor.update({scrollPastEnd: true}) + expect(editor.getScrollPastEnd()).toBe(true) + editor.update({scrollPastEnd: false}) + expect(editor.getScrollPastEnd()).toBe(false) + }) + + it('always returns false when autoHeight is on', () => { + editor.update({autoHeight: true, scrollPastEnd: true}) + expect(editor.getScrollPastEnd()).toBe(false) + editor.update({autoHeight: false}) + expect(editor.getScrollPastEnd()).toBe(true) + }) + }) + + describe('auto height', () => { + it('returns true by default but can be customized', () => { + editor = new TextEditor() + expect(editor.getAutoHeight()).toBe(true) + editor.update({autoHeight: false}) + expect(editor.getAutoHeight()).toBe(false) + editor.update({autoHeight: true}) + expect(editor.getAutoHeight()).toBe(true) + editor.destroy() + }) + }) + + describe('auto width', () => { + it('returns false by default but can be customized', () => { + expect(editor.getAutoWidth()).toBe(false) + editor.update({autoWidth: true}) + expect(editor.getAutoWidth()).toBe(true) + editor.update({autoWidth: false}) + expect(editor.getAutoWidth()).toBe(false) + }) + }) + + describe('.get/setPlaceholderText()', () => { + it('can be created with placeholderText', () => { + const newEditor = new TextEditor({ + mini: true, + placeholderText: 'yep' + }) + expect(newEditor.getPlaceholderText()).toBe('yep') + }) + + it('models placeholderText and emits an event when changed', () => { + let handler + editor.onDidChangePlaceholderText(handler = jasmine.createSpy()) + + expect(editor.getPlaceholderText()).toBeUndefined() + + editor.setPlaceholderText('OK') + expect(handler).toHaveBeenCalledWith('OK') + expect(editor.getPlaceholderText()).toBe('OK') + }) + }) + + describe('gutters', () => { + describe('the TextEditor constructor', () => { + it('creates a line-number gutter', () => { + expect(editor.getGutters().length).toBe(1) + const lineNumberGutter = editor.gutterWithName('line-number') + expect(lineNumberGutter.name).toBe('line-number') + expect(lineNumberGutter.priority).toBe(0) + }) + }) + + describe('::addGutter', () => { + it('can add a gutter', () => { + expect(editor.getGutters().length).toBe(1) // line-number gutter + const options = { + name: 'test-gutter', + priority: 1 + } + const gutter = editor.addGutter(options) + expect(editor.getGutters().length).toBe(2) + expect(editor.getGutters()[1]).toBe(gutter) + }) + + it("does not allow a custom gutter with the 'line-number' name.", () => expect(editor.addGutter.bind(editor, {name: 'line-number'})).toThrow()) + }) + + describe('::decorateMarker', () => { + let marker + + beforeEach(() => marker = editor.markBufferRange([[1, 0], [1, 0]])) + + it('reflects an added decoration when one of its custom gutters is decorated.', () => { + const gutter = editor.addGutter({'name': 'custom-gutter'}) + const decoration = gutter.decorateMarker(marker, {class: 'custom-class'}) + const gutterDecorations = editor.getDecorations({ + type: 'gutter', + gutterName: 'custom-gutter', + class: 'custom-class' + }) + expect(gutterDecorations.length).toBe(1) + expect(gutterDecorations[0]).toBe(decoration) + }) + + it('reflects an added decoration when its line-number gutter is decorated.', () => { + const decoration = editor.gutterWithName('line-number').decorateMarker(marker, {class: 'test-class'}) + const gutterDecorations = editor.getDecorations({ + type: 'line-number', + gutterName: 'line-number', + class: 'test-class' + }) + expect(gutterDecorations.length).toBe(1) + expect(gutterDecorations[0]).toBe(decoration) + }) + }) + + describe('::observeGutters', () => { + let payloads, callback + + beforeEach(() => { + payloads = [] + callback = payload => payloads.push(payload) + }) + + it('calls the callback immediately with each existing gutter, and with each added gutter after that.', () => { + const lineNumberGutter = editor.gutterWithName('line-number') + editor.observeGutters(callback) + expect(payloads).toEqual([lineNumberGutter]) + const gutter1 = editor.addGutter({name: 'test-gutter-1'}) + expect(payloads).toEqual([lineNumberGutter, gutter1]) + const gutter2 = editor.addGutter({name: 'test-gutter-2'}) + expect(payloads).toEqual([lineNumberGutter, gutter1, gutter2]) + }) + + it('does not call the callback when a gutter is removed.', () => { + const gutter = editor.addGutter({name: 'test-gutter'}) + editor.observeGutters(callback) + payloads = [] + gutter.destroy() + expect(payloads).toEqual([]) + }) + + it('does not call the callback after the subscription has been disposed.', () => { + const subscription = editor.observeGutters(callback) + payloads = [] + subscription.dispose() + editor.addGutter({name: 'test-gutter'}) + expect(payloads).toEqual([]) + }) + }) + + describe('::onDidAddGutter', () => { + let payloads, callback + + beforeEach(() => { + payloads = [] + callback = payload => payloads.push(payload) + }) + + it('calls the callback with each newly-added gutter, but not with existing gutters.', () => { + editor.onDidAddGutter(callback) + expect(payloads).toEqual([]) + const gutter = editor.addGutter({name: 'test-gutter'}) + expect(payloads).toEqual([gutter]) + }) + + it('does not call the callback after the subscription has been disposed.', () => { + const subscription = editor.onDidAddGutter(callback) + payloads = [] + subscription.dispose() + editor.addGutter({name: 'test-gutter'}) + expect(payloads).toEqual([]) + }) + }) + + describe('::onDidRemoveGutter', () => { + let payloads, callback + + beforeEach(() => { + payloads = [] + callback = payload => payloads.push(payload) + }) + + it('calls the callback when a gutter is removed.', () => { + const gutter = editor.addGutter({name: 'test-gutter'}) + editor.onDidRemoveGutter(callback) + expect(payloads).toEqual([]) + gutter.destroy() + expect(payloads).toEqual(['test-gutter']) + }) + + it('does not call the callback after the subscription has been disposed.', () => { + const gutter = editor.addGutter({name: 'test-gutter'}) + const subscription = editor.onDidRemoveGutter(callback) + subscription.dispose() + gutter.destroy() + expect(payloads).toEqual([]) + }) + }) + }) + + describe('decorations', () => { + describe('::decorateMarker', () => { + it('includes the decoration in the object returned from ::decorationsStateForScreenRowRange', () => { + const marker = editor.markBufferRange([[2, 4], [6, 8]]) + const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'foo'}) + expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual({ + properties: {type: 'highlight', class: 'foo'}, + screenRange: marker.getScreenRange(), + bufferRange: marker.getBufferRange(), + rangeIsReversed: false + }) + }) + + it("does not throw errors after the marker's containing layer is destroyed", () => { + const layer = editor.addMarkerLayer() + const marker = layer.markBufferRange([[2, 4], [6, 8]]) + const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'foo'}) + layer.destroy() + editor.decorationsStateForScreenRowRange(0, 5) + }) + }) + + describe('::decorateMarkerLayer', () => { + it('based on the markers in the layer, includes multiple decoration objects with the same properties and different ranges in the object returned from ::decorationsStateForScreenRowRange', () => { + const layer1 = editor.getBuffer().addMarkerLayer() + const marker1 = layer1.markRange([[2, 4], [6, 8]]) + const marker2 = layer1.markRange([[11, 0], [11, 12]]) + const layer2 = editor.getBuffer().addMarkerLayer() + const marker3 = layer2.markRange([[8, 0], [9, 0]]) + + const layer1Decoration1 = editor.decorateMarkerLayer(layer1, {type: 'highlight', class: 'foo'}) + const layer1Decoration2 = editor.decorateMarkerLayer(layer1, {type: 'highlight', class: 'bar'}) + const layer2Decoration = editor.decorateMarkerLayer(layer2, {type: 'highlight', class: 'baz'}) + + let decorationState = editor.decorationsStateForScreenRowRange(0, 13) + + expect(decorationState[`${layer1Decoration1.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'foo'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer1Decoration1.id}-${marker2.id}`]).toEqual({ + properties: {type: 'highlight', class: 'foo'}, + screenRange: marker2.getRange(), + bufferRange: marker2.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer1Decoration2.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer1Decoration2.id}-${marker2.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker2.getRange(), + bufferRange: marker2.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer2Decoration.id}-${marker3.id}`]).toEqual({ + properties: {type: 'highlight', class: 'baz'}, + screenRange: marker3.getRange(), + bufferRange: marker3.getRange(), + rangeIsReversed: false + }) + + layer1Decoration1.destroy() + + decorationState = editor.decorationsStateForScreenRowRange(0, 12) + expect(decorationState[`${layer1Decoration1.id}-${marker1.id}`]).toBeUndefined() + expect(decorationState[`${layer1Decoration1.id}-${marker2.id}`]).toBeUndefined() + expect(decorationState[`${layer1Decoration2.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer1Decoration2.id}-${marker2.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker2.getRange(), + bufferRange: marker2.getRange(), + rangeIsReversed: false + }) + expect(decorationState[`${layer2Decoration.id}-${marker3.id}`]).toEqual({ + properties: {type: 'highlight', class: 'baz'}, + screenRange: marker3.getRange(), + bufferRange: marker3.getRange(), + rangeIsReversed: false + }) + + layer1Decoration2.setPropertiesForMarker(marker1, {type: 'highlight', class: 'quux'}) + decorationState = editor.decorationsStateForScreenRowRange(0, 12) + expect(decorationState[`${layer1Decoration2.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'quux'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + + layer1Decoration2.setPropertiesForMarker(marker1, null) + decorationState = editor.decorationsStateForScreenRowRange(0, 12) + expect(decorationState[`${layer1Decoration2.id}-${marker1.id}`]).toEqual({ + properties: {type: 'highlight', class: 'bar'}, + screenRange: marker1.getRange(), + bufferRange: marker1.getRange(), + rangeIsReversed: false + }) + }) + }) + }) + + describe('invisibles', () => { + beforeEach(() => { + editor.update({showInvisibles: true}) + }) + + it('substitutes invisible characters according to the given rules', () => { + const previousLineText = editor.lineTextForScreenRow(0) + editor.update({invisibles: {eol: '?'}}) + expect(editor.lineTextForScreenRow(0)).not.toBe(previousLineText) + expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true) + expect(editor.getInvisibles()).toEqual({eol: '?'}) + }) + + it('does not use invisibles if showInvisibles is set to false', () => { + editor.update({invisibles: {eol: '?'}}) + expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(true) + + editor.update({showInvisibles: false}) + expect(editor.lineTextForScreenRow(0).endsWith('?')).toBe(false) + }) + }) + + describe('indent guides', () => { + it('shows indent guides when `editor.showIndentGuide` is set to true and the editor is not mini', () => { + editor.setText(' foo') + editor.setTabLength(2) + + editor.update({showIndentGuide: false}) + expect(editor.tokensForScreenRow(0)).toEqual([ + {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, + {text: 'foo', scopes: ['syntax--source syntax--js']} + ]) + + editor.update({showIndentGuide: true}) + expect(editor.tokensForScreenRow(0)).toEqual([ + {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace indent-guide']}, + {text: 'foo', scopes: ['syntax--source syntax--js']} + ]) + + editor.setMini(true) + expect(editor.tokensForScreenRow(0)).toEqual([ + {text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']}, + {text: 'foo', scopes: ['syntax--source syntax--js']} + ]) + }) + }) + + describe('when the editor is constructed with the grammar option set', () => { + beforeEach(async () => { + await atom.packages.activatePackage('language-coffee-script') + }) + + it('sets the grammar', () => { + editor = new TextEditor({grammar: atom.grammars.grammarForScopeName('source.coffee')}) + expect(editor.getGrammar().name).toBe('CoffeeScript') + }) + }) + + describe('softWrapAtPreferredLineLength', () => { + it('soft wraps the editor at the preferred line length unless the editor is narrower or the editor is mini', () => { + editor.update({ + editorWidthInChars: 30, + softWrapped: true, + softWrapAtPreferredLineLength: true, + preferredLineLength: 20 + }) + + expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = ') + + editor.update({editorWidthInChars: 10}) + expect(editor.lineTextForScreenRow(0)).toBe('var ') + + editor.update({mini: true}) + expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {') + }) + }) + + describe('softWrapHangingIndentLength', () => { + it('controls how much extra indentation is applied to soft-wrapped lines', () => { + editor.setText('123456789') + editor.update({ + editorWidthInChars: 8, + softWrapped: true, + softWrapHangingIndentLength: 2 + }) + expect(editor.lineTextForScreenRow(1)).toEqual(' 9') + + editor.update({softWrapHangingIndentLength: 4}) + expect(editor.lineTextForScreenRow(1)).toEqual(' 9') + }) + }) + + describe('::getElement', () => { + it('returns an element', () => expect(editor.getElement() instanceof HTMLElement).toBe(true)) + }) + + describe('setMaxScreenLineLength', () => { + it('sets the maximum line length in the editor before soft wrapping is forced', () => { + expect(editor.getSoftWrapColumn()).toBe(500) + editor.update({ + maxScreenLineLength: 1500 + }) + expect(editor.getSoftWrapColumn()).toBe(1500) + }) + }) +}) describe('TextEditor', () => { let editor @@ -539,3 +7185,7 @@ describe('TextEditor', () => { }) }) }) + +function convertToHardTabs (buffer) { + buffer.setText(buffer.getText().replace(/[ ]{2}/g, '\t')) +} From af82dff75bfebe79b056ddae744f99d4fa499f38 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 16:15:32 -0700 Subject: [PATCH 3/6] Fix error in .getLongTitle when editors have no path --- 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 4d7d94de0..033cea5d6 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -1059,11 +1059,11 @@ class TextEditor { let myPathSegments const openEditorPathSegmentsWithSameFilename = [] for (const textEditor of atom.workspace.getTextEditors()) { - const pathSegments = fs.tildify(textEditor.getDirectoryPath()).split(path.sep) if (textEditor.getFileName() === fileName) { + const pathSegments = fs.tildify(textEditor.getDirectoryPath()).split(path.sep) openEditorPathSegmentsWithSameFilename.push(pathSegments) + if (textEditor === this) myPathSegments = pathSegments } - if (textEditor === this) myPathSegments = pathSegments } if (openEditorPathSegmentsWithSameFilename.length === 1) return fileName From 96e6b3a2ce467193b6671bff3b532ff501a1ce0c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 16:51:01 -0700 Subject: [PATCH 4/6] Fix error in .getLongTitle when editor isn't in the workspace --- spec/text-editor-spec.js | 9 +++++++++ src/text-editor.js | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index b2cc41ab7..cece5d753 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -218,6 +218,15 @@ describe('TextEditor', () => { expect(editor1.getLongTitle()).toBe('main.js \u2014 js') expect(editor2.getLongTitle()).toBe(`main.js \u2014 ${path.join('js', 'plugin')}`) }) + + it('returns the filename when the editor is not in the workspace', async () => { + editor.onDidDestroy(() => { + expect(editor.getLongTitle()).toBe('sample.js') + }) + + await atom.workspace.getActivePane().close() + expect(editor.isDestroyed()).toBe(true) + }) }) it('notifies ::onDidChangeTitle observers when the underlying buffer path changes', () => { diff --git a/src/text-editor.js b/src/text-editor.js index 033cea5d6..a0b9d19a0 100644 --- a/src/text-editor.js +++ b/src/text-editor.js @@ -1066,7 +1066,7 @@ class TextEditor { } } - if (openEditorPathSegmentsWithSameFilename.length === 1) return fileName + if (!myPathSegments || openEditorPathSegmentsWithSameFilename.length === 1) return fileName let commonPathSegmentCount for (let i = 0, {length} = myPathSegments; i < length; i++) { From 7f48c140ba61c9cf72d1848514ab610b16643413 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 17:37:29 -0700 Subject: [PATCH 5/6] :arrow_up: tabs for spec fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 72250311d..584ea0af6 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "status-bar": "1.8.14", "styleguide": "0.49.8", "symbols-view": "0.118.1", - "tabs": "0.109.0", + "tabs": "0.109.1", "timecop": "0.36.0", "tree-view": "0.221.0", "update-package-dependencies": "0.12.0", From 9540d3f33ebb248c61e6ed92edb402024e35f510 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Nov 2017 21:42:04 -0700 Subject: [PATCH 6/6] :arrow_up: whitespace, snippets --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 584ea0af6..1dc4406aa 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "open-on-github": "1.2.1", "package-generator": "1.1.1", "settings-view": "0.252.2", - "snippets": "1.1.8", + "snippets": "1.1.9", "spell-check": "0.72.3", "status-bar": "1.8.14", "styleguide": "0.49.8", @@ -134,7 +134,7 @@ "tree-view": "0.221.0", "update-package-dependencies": "0.12.0", "welcome": "0.36.5", - "whitespace": "0.37.4", + "whitespace": "0.37.5", "wrap-guide": "0.40.2", "language-c": "0.58.1", "language-clojure": "0.22.4",