diff options
Diffstat (limited to 'atom/packages/vim-mode/lib/operators/general-operators.coffee')
| -rw-r--r-- | atom/packages/vim-mode/lib/operators/general-operators.coffee | 255 |
1 files changed, 255 insertions, 0 deletions
diff --git a/atom/packages/vim-mode/lib/operators/general-operators.coffee b/atom/packages/vim-mode/lib/operators/general-operators.coffee new file mode 100644 index 0000000..dd99256 --- /dev/null +++ b/atom/packages/vim-mode/lib/operators/general-operators.coffee @@ -0,0 +1,255 @@ +_ = require 'underscore-plus' +{Point, Range} = require 'atom' +{ViewModel} = require '../view-models/view-model' +Utils = require '../utils' +settings = require '../settings' + +class OperatorError + constructor: (@message) -> + @name = 'Operator Error' + +class Operator + vimState: null + motion: null + complete: null + selectOptions: null + + # selectOptions - The options object to pass through to the motion when + # selecting. + constructor: (@editor, @vimState, {@selectOptions}={}) -> + @complete = false + + # Public: Determines when the command can be executed. + # + # Returns true if ready to execute and false otherwise. + isComplete: -> @complete + + # Public: Determines if this command should be recorded in the command + # history for repeats. + # + # Returns true if this command should be recorded. + isRecordable: -> true + + # Public: Marks this as ready to execute and saves the motion. + # + # motion - The motion used to select what to operate on. + # + # Returns nothing. + compose: (motion) -> + if not motion.select + throw new OperatorError('Must compose with a motion') + + @motion = motion + @complete = true + + canComposeWith: (operation) -> operation.select? + + # Public: Preps text and sets the text register + # + # Returns nothing + setTextRegister: (register, text) -> + if @motion?.isLinewise?() + type = 'linewise' + if text[-1..] isnt '\n' + text += '\n' + else + type = Utils.copyType(text) + @vimState.setRegister(register, {text, type}) unless text is '' + +# Public: Generic class for an operator that requires extra input +class OperatorWithInput extends Operator + constructor: (@editor, @vimState) -> + @editor = @editor + @complete = false + + canComposeWith: (operation) -> operation.characters? or operation.select? + + compose: (operation) -> + if operation.select? + @motion = operation + if operation.characters? + @input = operation + @complete = true + +# +# It deletes everything selected by the following motion. +# +class Delete extends Operator + register: null + allowEOL: null + + # allowEOL - Determines whether the cursor should be allowed to rest on the + # end of line character or not. + constructor: (@editor, @vimState, {@allowEOL, @selectOptions}={}) -> + @complete = false + @selectOptions ?= {} + @selectOptions.requireEOL ?= true + @register = settings.defaultRegister() + + # Public: Deletes the text selected by the given motion. + # + # count - The number of times to execute. + # + # Returns nothing. + execute: (count) -> + if _.contains(@motion.select(count, @selectOptions), true) + @setTextRegister(@register, @editor.getSelectedText()) + for selection in @editor.getSelections() + selection.deleteSelectedText() + for cursor in @editor.getCursors() + if @motion.isLinewise?() + cursor.skipLeadingWhitespace() + else + cursor.moveLeft() if cursor.isAtEndOfLine() and not cursor.isAtBeginningOfLine() + + @vimState.activateCommandMode() + +# +# It toggles the case of everything selected by the following motion +# +class ToggleCase extends Operator + constructor: (@editor, @vimState, {@complete, @selectOptions}={}) -> + + execute: (count=1) -> + if @motion? + if _.contains(@motion.select(count, @selectOptions), true) + @editor.replaceSelectedText {}, (text) -> + text.split('').map((char) -> + lower = char.toLowerCase() + if char is lower + char.toUpperCase() + else + lower + ).join('') + else + @editor.transact => + for cursor in @editor.getCursors() + point = cursor.getBufferPosition() + lineLength = @editor.lineTextForBufferRow(point.row).length + cursorCount = Math.min(count, lineLength - point.column) + + _.times cursorCount, => + point = cursor.getBufferPosition() + range = Range.fromPointWithDelta(point, 0, 1) + char = @editor.getTextInBufferRange(range) + + if char is char.toLowerCase() + @editor.setTextInBufferRange(range, char.toUpperCase()) + else + @editor.setTextInBufferRange(range, char.toLowerCase()) + + cursor.moveRight() unless point.column >= lineLength - 1 + + @vimState.activateCommandMode() + +# +# In visual mode or after `g` with a motion, it makes the selection uppercase +# +class UpperCase extends Operator + constructor: (@editor, @vimState, {@selectOptions}={}) -> + @complete = false + + execute: (count=1) -> + if _.contains(@motion.select(count, @selectOptions), true) + @editor.replaceSelectedText {}, (text) -> + text.toUpperCase() + + @vimState.activateCommandMode() + +# +# In visual mode or after `g` with a motion, it makes the selection lowercase +# +class LowerCase extends Operator + constructor: (@editor, @vimState, {@selectOptions}={}) -> + @complete = false + + execute: (count=1) -> + if _.contains(@motion.select(count, @selectOptions), true) + @editor.replaceSelectedText {}, (text) -> + text.toLowerCase() + + @vimState.activateCommandMode() + +# +# It copies everything selected by the following motion. +# +class Yank extends Operator + register: null + + constructor: (@editor, @vimState, {@allowEOL, @selectOptions}={}) -> + @register = settings.defaultRegister() + + # Public: Copies the text selected by the given motion. + # + # count - The number of times to execute. + # + # Returns nothing. + execute: (count) -> + originalPositions = @editor.getCursorBufferPositions() + if _.contains(@motion.select(count), true) + text = @editor.getSelectedText() + startPositions = _.pluck(@editor.getSelectedBufferRanges(), "start") + newPositions = for originalPosition, i in originalPositions + if startPositions[i] and (@vimState.mode is 'visual' or not @motion.isLinewise?()) + Point.min(startPositions[i], originalPositions[i]) + else + originalPosition + else + text = '' + newPositions = originalPositions + + @setTextRegister(@register, text) + + @editor.setSelectedBufferRanges(newPositions.map (p) -> new Range(p, p)) + @vimState.activateCommandMode() + +# +# It combines the current line with the following line. +# +class Join extends Operator + constructor: (@editor, @vimState, {@selectOptions}={}) -> @complete = true + + # Public: Combines the current with the following lines + # + # count - The number of times to execute. + # + # Returns nothing. + execute: (count=1) -> + @editor.transact => + _.times count, => + @editor.joinLines() + @vimState.activateCommandMode() + +# +# Repeat the last operation +# +class Repeat extends Operator + constructor: (@editor, @vimState, {@selectOptions}={}) -> @complete = true + + isRecordable: -> false + + execute: (count=1) -> + @editor.transact => + _.times count, => + cmd = @vimState.history[0] + cmd?.execute() +# +# It creates a mark at the current cursor position +# +class Mark extends OperatorWithInput + constructor: (@editor, @vimState, {@selectOptions}={}) -> + super(@editor, @vimState) + @viewModel = new ViewModel(this, class: 'mark', singleChar: true, hidden: true) + + # Public: Creates the mark in the specified mark register (from user input) + # at the current position + # + # Returns nothing. + execute: -> + @vimState.setMark(@input.characters, @editor.getCursorBufferPosition()) + @vimState.activateCommandMode() + +module.exports = { + Operator, OperatorWithInput, OperatorError, Delete, ToggleCase, + UpperCase, LowerCase, Yank, Join, Repeat, Mark +} |