diff options
| author | Ben Beltran <ben@nsovocal.com> | 2015-07-10 11:12:25 -0500 |
|---|---|---|
| committer | Ben Beltran <ben@nsovocal.com> | 2015-07-10 11:12:25 -0500 |
| commit | 24c7594d62d8d7fbbcdb64b11ce4adc5d8e6991a (patch) | |
| tree | ded312222bb108923da1820ba40b04d710d20e5b /atom/packages/vim-mode/lib/operators | |
| parent | eb786e82d170e2abc351a432ade616d6ecdeeb6b (diff) | |
Adds atom
Diffstat (limited to 'atom/packages/vim-mode/lib/operators')
7 files changed, 708 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 +} diff --git a/atom/packages/vim-mode/lib/operators/increase-operators.coffee b/atom/packages/vim-mode/lib/operators/increase-operators.coffee new file mode 100644 index 0000000..e62f781 --- /dev/null +++ b/atom/packages/vim-mode/lib/operators/increase-operators.coffee @@ -0,0 +1,57 @@ +{Operator} = require './general-operators' +{Range} = require 'atom' +settings = require '../settings' + +# +# It increases or decreases the next number on the line +# +class Increase extends Operator + step: 1 + + constructor: -> + super + @complete = true + @numberRegex = new RegExp(settings.numberRegex()) + + execute: (count=1) -> + @editor.transact => + increased = false + for cursor in @editor.getCursors() + if @increaseNumber(count, cursor) then increased = true + atom.beep() unless increased + + increaseNumber: (count, cursor) -> + # find position of current number, adapted from from SearchCurrentWord + cursorPosition = cursor.getBufferPosition() + numEnd = cursor.getEndOfCurrentWordBufferPosition(wordRegex: @numberRegex, allowNext: false) + + if numEnd.column is cursorPosition.column + # either we don't have a current number, or it ends on cursor, i.e. precedes it, so look for the next one + numEnd = cursor.getEndOfCurrentWordBufferPosition(wordRegex: @numberRegex, allowNext: true) + return if numEnd.row isnt cursorPosition.row # don't look beyond the current line + return if numEnd.column is cursorPosition.column # no number after cursor + + cursor.setBufferPosition numEnd + numStart = cursor.getBeginningOfCurrentWordBufferPosition(wordRegex: @numberRegex, allowPrevious: false) + + range = new Range(numStart, numEnd) + + # parse number, increase/decrease + number = parseInt(@editor.getTextInBufferRange(range), 10) + if isNaN(number) + cursor.setBufferPosition(cursorPosition) + return + + number += @step*count + + # replace current number with new + newValue = String(number) + @editor.setTextInBufferRange(range, newValue, normalizeLineEndings: false) + + cursor.setBufferPosition(row: numStart.row, column: numStart.column-1+newValue.length) + return true + +class Decrease extends Increase + step: -1 + +module.exports = {Increase, Decrease} diff --git a/atom/packages/vim-mode/lib/operators/indent-operators.coffee b/atom/packages/vim-mode/lib/operators/indent-operators.coffee new file mode 100644 index 0000000..0ad37e0 --- /dev/null +++ b/atom/packages/vim-mode/lib/operators/indent-operators.coffee @@ -0,0 +1,28 @@ +{Operator} = require './general-operators' + +class AdjustIndentation extends Operator + execute: (count=1) -> + mode = @vimState.mode + @motion.select(count) + {start} = @editor.getSelectedBufferRange() + + @indent() + + if mode isnt 'visual' + @editor.setCursorBufferPosition([start.row, 0]) + @editor.moveToFirstCharacterOfLine() + @vimState.activateCommandMode() + +class Indent extends AdjustIndentation + indent: -> + @editor.indentSelectedRows() + +class Outdent extends AdjustIndentation + indent: -> + @editor.outdentSelectedRows() + +class Autoindent extends AdjustIndentation + indent: -> + @editor.autoIndentSelectedRows() + +module.exports = {Indent, Outdent, Autoindent} diff --git a/atom/packages/vim-mode/lib/operators/index.coffee b/atom/packages/vim-mode/lib/operators/index.coffee new file mode 100644 index 0000000..575def3 --- /dev/null +++ b/atom/packages/vim-mode/lib/operators/index.coffee @@ -0,0 +1,14 @@ +_ = require 'underscore-plus' +IndentOperators = require './indent-operators' +IncreaseOperators = require './increase-operators' +Put = require './put-operator' +InputOperators = require './input' +Replace = require './replace-operator' +Operators = require './general-operators' + +Operators.Put = Put +Operators.Replace = Replace +_.extend(Operators, IndentOperators) +_.extend(Operators, IncreaseOperators) +_.extend(Operators, InputOperators) +module.exports = Operators diff --git a/atom/packages/vim-mode/lib/operators/input.coffee b/atom/packages/vim-mode/lib/operators/input.coffee new file mode 100644 index 0000000..3821045 --- /dev/null +++ b/atom/packages/vim-mode/lib/operators/input.coffee @@ -0,0 +1,231 @@ +{Operator, Delete} = require './general-operators' +_ = require 'underscore-plus' +settings = require '../settings' + +# The operation for text entered in input mode. Broadly speaking, input +# operators manage an undo transaction and set a @typingCompleted variable when +# it's done. When the input operation is completed, the typingCompleted variable +# tells the operation to repeat itself instead of enter insert mode. +class Insert extends Operator + standalone: true + + isComplete: -> @standalone or super + + confirmChanges: (changes) -> + bundler = new TransactionBundler(changes, @editor) + @typedText = bundler.buildInsertText() + + execute: -> + if @typingCompleted + return unless @typedText? and @typedText.length > 0 + @editor.insertText(@typedText, normalizeLineEndings: true) + for cursor in @editor.getCursors() + cursor.moveLeft() unless cursor.isAtBeginningOfLine() + else + @vimState.activateInsertMode() + @typingCompleted = true + return + + inputOperator: -> true + +class InsertAfter extends Insert + execute: -> + @editor.moveRight() unless @editor.getLastCursor().isAtEndOfLine() + super + +class InsertAfterEndOfLine extends Insert + execute: -> + @editor.moveToEndOfLine() + super + +class InsertAtBeginningOfLine extends Insert + execute: -> + @editor.moveToBeginningOfLine() + @editor.moveToFirstCharacterOfLine() + super + +class InsertAboveWithNewline extends Insert + execute: (count=1) -> + @vimState.setInsertionCheckpoint() unless @typingCompleted + @editor.insertNewlineAbove() + @editor.getLastCursor().skipLeadingWhitespace() + + if @typingCompleted + # We'll have captured the inserted newline, but we want to do that + # over again by hand, or differing indentations will be wrong. + @typedText = @typedText.trimLeft() + return super + + @vimState.activateInsertMode() + @typingCompleted = true + +class InsertBelowWithNewline extends Insert + execute: (count=1) -> + @vimState.setInsertionCheckpoint() unless @typingCompleted + @editor.insertNewlineBelow() + @editor.getLastCursor().skipLeadingWhitespace() + + if @typingCompleted + # We'll have captured the inserted newline, but we want to do that + # over again by hand, or differing indentations will be wrong. + @typedText = @typedText.trimLeft() + return super + + @vimState.activateInsertMode() + @typingCompleted = true + +# +# Delete the following motion and enter insert mode to replace it. +# +class Change extends Insert + standalone: false + register: null + + constructor: (@editor, @vimState, {@selectOptions}={}) -> + @register = settings.defaultRegister() + + # Public: Changes the text selected by the given motion. + # + # count - The number of times to execute. + # + # Returns nothing. + execute: (count) -> + # If we've typed, we're being repeated. If we're being repeated, + # undo transactions are already handled. + @vimState.setInsertionCheckpoint() unless @typingCompleted + + if _.contains(@motion.select(count, excludeWhitespace: true), true) + @setTextRegister(@register, @editor.getSelectedText()) + if @motion.isLinewise?() + @editor.insertNewline() + @editor.moveLeft() + else + for selection in @editor.getSelections() + selection.deleteSelectedText() + + return super if @typingCompleted + + @vimState.activateInsertMode() + @typingCompleted = true + +class Substitute extends Insert + register: null + + constructor: (@editor, @vimState, {@selectOptions}={}) -> + @register = settings.defaultRegister() + + execute: (count=1) -> + @vimState.setInsertionCheckpoint() unless @typingCompleted + _.times count, => + @editor.selectRight() + @setTextRegister(@register, @editor.getSelectedText()) + @editor.delete() + + if @typingCompleted + @typedText = @typedText.trimLeft() + return super + + @vimState.activateInsertMode() + @typingCompleted = true + +class SubstituteLine extends Insert + register: null + + constructor: (@editor, @vimState, {@selectOptions}={}) -> + @register = settings.defaultRegister() + + execute: (count=1) -> + @vimState.setInsertionCheckpoint() unless @typingCompleted + @editor.moveToBeginningOfLine() + _.times count, => + @editor.selectToEndOfLine() + @editor.selectRight() + @setTextRegister(@register, @editor.getSelectedText()) + @editor.delete() + @editor.insertNewlineAbove() + @editor.getLastCursor().skipLeadingWhitespace() + + if @typingCompleted + @typedText = @typedText.trimLeft() + return super + + @vimState.activateInsertMode() + @typingCompleted = true + +# Takes a transaction and turns it into a string of what was typed. +# This class is an implementation detail of Insert +class TransactionBundler + constructor: (@changes, @editor) -> + @start = null + @end = null + + buildInsertText: -> + @addChange(change) for change in @changes + if @start? + @editor.getTextInBufferRange [@start, @end] + else + "" + + addChange: (change) -> + return unless change.newRange? + if @isRemovingFromPrevious(change) + @subtractRange change.oldRange + if @isAddingWithinPrevious(change) + @addRange change.newRange + + isAddingWithinPrevious: (change) -> + return false unless @isAdding(change) + + return true if @start is null + + @start.isLessThanOrEqual(change.newRange.start) and + @end.isGreaterThanOrEqual(change.newRange.start) + + isRemovingFromPrevious: (change) -> + return false unless @isRemoving(change) and @start? + + @start.isLessThanOrEqual(change.oldRange.start) and + @end.isGreaterThanOrEqual(change.oldRange.end) + + isAdding: (change) -> + change.newText.length > 0 + + isRemoving: (change) -> + change.oldText.length > 0 + + addRange: (range) -> + if @start is null + {@start, @end} = range + return + + rows = range.end.row - range.start.row + + if (range.start.row is @end.row) + cols = range.end.column - range.start.column + else + cols = 0 + + @end = @end.translate [rows, cols] + + subtractRange: (range) -> + rows = range.end.row - range.start.row + + if (range.end.row is @end.row) + cols = range.end.column - range.start.column + else + cols = 0 + + @end = @end.translate [-rows, -cols] + + +module.exports = { + Insert, + InsertAfter, + InsertAfterEndOfLine, + InsertAtBeginningOfLine, + InsertAboveWithNewline, + InsertBelowWithNewline, + Change, + Substitute, + SubstituteLine +} diff --git a/atom/packages/vim-mode/lib/operators/put-operator.coffee b/atom/packages/vim-mode/lib/operators/put-operator.coffee new file mode 100644 index 0000000..63151f1 --- /dev/null +++ b/atom/packages/vim-mode/lib/operators/put-operator.coffee @@ -0,0 +1,73 @@ +_ = require 'underscore-plus' +{Operator} = require './general-operators' +settings = require '../settings' + +module.exports = +# +# It pastes everything contained within the specifed register +# +class Put extends Operator + register: null + + constructor: (@editor, @vimState, {@location, @selectOptions}={}) -> + @location ?= 'after' + @complete = true + @register = settings.defaultRegister() + + # Public: Pastes the text in the given register. + # + # count - The number of times to execute. + # + # Returns nothing. + execute: (count=1) -> + {text, type} = @vimState.getRegister(@register) or {} + return unless text + + textToInsert = _.times(count, -> text).join('') + + selection = @editor.getSelectedBufferRange() + if selection.isEmpty() + # Clean up some corner cases on the last line of the file + if type is 'linewise' + textToInsert = textToInsert.replace(/\n$/, '') + if @location is 'after' and @onLastRow() + textToInsert = "\n#{textToInsert}" + else + textToInsert = "#{textToInsert}\n" + + if @location is 'after' + if type is 'linewise' + if @onLastRow() + @editor.moveToEndOfLine() + + originalPosition = @editor.getCursorScreenPosition() + originalPosition.row += 1 + else + @editor.moveDown() + else + unless @onLastColumn() + @editor.moveRight() + + if type is 'linewise' and not originalPosition? + @editor.moveToBeginningOfLine() + originalPosition = @editor.getCursorScreenPosition() + + @editor.insertText(textToInsert) + + if originalPosition? + @editor.setCursorScreenPosition(originalPosition) + @editor.moveToFirstCharacterOfLine() + + @vimState.activateCommandMode() + if type isnt 'linewise' + @editor.moveLeft() + + # Private: Helper to determine if the editor is currently on the last row. + # + # Returns true on the last row and false otherwise. + onLastRow: -> + {row, column} = @editor.getCursorBufferPosition() + row is @editor.getBuffer().getLastRow() + + onLastColumn: -> + @editor.getLastCursor().isAtEndOfLine() diff --git a/atom/packages/vim-mode/lib/operators/replace-operator.coffee b/atom/packages/vim-mode/lib/operators/replace-operator.coffee new file mode 100644 index 0000000..52157d0 --- /dev/null +++ b/atom/packages/vim-mode/lib/operators/replace-operator.coffee @@ -0,0 +1,50 @@ +_ = require 'underscore-plus' +{OperatorWithInput} = require './general-operators' +{ViewModel} = require '../view-models/view-model' +{Range} = require 'atom' + +module.exports = +class Replace extends OperatorWithInput + constructor: (@editor, @vimState, {@selectOptions}={}) -> + super(@editor, @vimState) + @viewModel = new ViewModel(this, class: 'replace', hidden: true, singleChar: true, defaultText: '\n') + + execute: (count=1) -> + if @input.characters is "" + # replace canceled + + if @vimState.mode is "visual" + @vimState.resetVisualMode() + else + @vimState.activateCommandMode() + + return + + @editor.transact => + if @motion? + if _.contains(@motion.select(), true) + @editor.replaceSelectedText null, (text) => + text.replace(/./g, @input.characters) + for selection in @editor.getSelections() + point = selection.getBufferRange().start + selection.setBufferRange(Range.fromPointWithDelta(point, 0, 0)) + else + for cursor in @editor.getCursors() + pos = cursor.getBufferPosition() + currentRowLength = @editor.lineTextForBufferRow(pos.row).length + continue unless currentRowLength - pos.column >= count + + _.times count, => + point = cursor.getBufferPosition() + @editor.setTextInBufferRange(Range.fromPointWithDelta(point, 0, 1), @input.characters) + cursor.moveRight() + cursor.setBufferPosition(pos) + + # Special case: when replaced with a newline move to the start of the + # next row. + if @input.characters is "\n" + _.times count, => + @editor.moveDown() + @editor.moveToFirstCharacterOfLine() + + @vimState.activateCommandMode() |