aboutsummaryrefslogtreecommitdiff
path: root/atom/packages/vim-mode/lib
diff options
context:
space:
mode:
authorBen Beltran <ben@nsovocal.com>2015-07-10 11:12:25 -0500
committerBen Beltran <ben@nsovocal.com>2015-07-10 11:12:25 -0500
commit24c7594d62d8d7fbbcdb64b11ce4adc5d8e6991a (patch)
treeded312222bb108923da1820ba40b04d710d20e5b /atom/packages/vim-mode/lib
parenteb786e82d170e2abc351a432ade616d6ecdeeb6b (diff)
Adds atom
Diffstat (limited to 'atom/packages/vim-mode/lib')
-rw-r--r--atom/packages/vim-mode/lib/global-vim-state.coffee5
-rw-r--r--atom/packages/vim-mode/lib/motions/find-motion.coffee54
-rw-r--r--atom/packages/vim-mode/lib/motions/general-motions.coffee466
-rw-r--r--atom/packages/vim-mode/lib/motions/index.coffee14
-rw-r--r--atom/packages/vim-mode/lib/motions/move-to-mark-motion.coffee25
-rw-r--r--atom/packages/vim-mode/lib/motions/search-motion.coffee210
-rw-r--r--atom/packages/vim-mode/lib/operators/general-operators.coffee255
-rw-r--r--atom/packages/vim-mode/lib/operators/increase-operators.coffee57
-rw-r--r--atom/packages/vim-mode/lib/operators/indent-operators.coffee28
-rw-r--r--atom/packages/vim-mode/lib/operators/index.coffee14
-rw-r--r--atom/packages/vim-mode/lib/operators/input.coffee231
-rw-r--r--atom/packages/vim-mode/lib/operators/put-operator.coffee73
-rw-r--r--atom/packages/vim-mode/lib/operators/replace-operator.coffee50
-rw-r--r--atom/packages/vim-mode/lib/prefixes.coffee68
-rw-r--r--atom/packages/vim-mode/lib/scroll.coffee88
-rw-r--r--atom/packages/vim-mode/lib/settings.coffee28
-rw-r--r--atom/packages/vim-mode/lib/status-bar-manager.coffee37
-rw-r--r--atom/packages/vim-mode/lib/text-objects.coffee164
-rw-r--r--atom/packages/vim-mode/lib/utils.coffee14
-rw-r--r--atom/packages/vim-mode/lib/view-models/search-view-model.coffee42
-rw-r--r--atom/packages/vim-mode/lib/view-models/view-model.coffee24
-rw-r--r--atom/packages/vim-mode/lib/view-models/vim-command-mode-input-element.coffee64
-rw-r--r--atom/packages/vim-mode/lib/vim-mode.coffee51
-rw-r--r--atom/packages/vim-mode/lib/vim-state.coffee592
24 files changed, 2654 insertions, 0 deletions
diff --git a/atom/packages/vim-mode/lib/global-vim-state.coffee b/atom/packages/vim-mode/lib/global-vim-state.coffee
new file mode 100644
index 0000000..25da977
--- /dev/null
+++ b/atom/packages/vim-mode/lib/global-vim-state.coffee
@@ -0,0 +1,5 @@
+module.exports =
+class GlobalVimState
+ registers: {}
+ searchHistory: []
+ currentSearch: {}
diff --git a/atom/packages/vim-mode/lib/motions/find-motion.coffee b/atom/packages/vim-mode/lib/motions/find-motion.coffee
new file mode 100644
index 0000000..58685d7
--- /dev/null
+++ b/atom/packages/vim-mode/lib/motions/find-motion.coffee
@@ -0,0 +1,54 @@
+{MotionWithInput} = require './general-motions'
+{ViewModel} = require '../view-models/view-model'
+{Point, Range} = require 'atom'
+
+class Find extends MotionWithInput
+ constructor: (@editor, @vimState) ->
+ super(@editor, @vimState)
+ @vimState.currentFind = this
+ @viewModel = new ViewModel(this, class: 'find', singleChar: true, hidden: true)
+ @backwards = false
+ @repeatReversed = false
+ @offset = 0
+ @repeated = false
+
+ match: (cursor, count) ->
+ currentPosition = cursor.getBufferPosition()
+ line = @editor.lineTextForBufferRow(currentPosition.row)
+ if @backwards
+ index = currentPosition.column
+ for i in [0..count-1]
+ return if index <= 0 # we can't move backwards any further, quick return
+ index = line.lastIndexOf(@input.characters, index-1-(@offset*@repeated))
+ if index >= 0
+ new Point(currentPosition.row, index + @offset)
+ else
+ index = currentPosition.column
+ for i in [0..count-1]
+ index = line.indexOf(@input.characters, index+1+(@offset*@repeated))
+ return if index < 0 # no match found
+ if index >= 0
+ new Point(currentPosition.row, index - @offset)
+
+ reverse: ->
+ @backwards = not @backwards
+ this
+
+ moveCursor: (cursor, count=1) ->
+ if (match = @match(cursor, count))?
+ cursor.setBufferPosition(match)
+
+ repeat: (opts={}) ->
+ opts.reverse = !!opts.reverse
+ @repeated = true
+ if opts.reverse isnt @repeatReversed
+ @reverse()
+ @repeatReversed = opts.reverse
+ this
+
+class Till extends Find
+ constructor: (@editor, @vimState) ->
+ super(@editor, @vimState)
+ @offset = 1
+
+module.exports = {Find, Till}
diff --git a/atom/packages/vim-mode/lib/motions/general-motions.coffee b/atom/packages/vim-mode/lib/motions/general-motions.coffee
new file mode 100644
index 0000000..47a3cc7
--- /dev/null
+++ b/atom/packages/vim-mode/lib/motions/general-motions.coffee
@@ -0,0 +1,466 @@
+_ = require 'underscore-plus'
+{Point, Range} = require 'atom'
+settings = require '../settings'
+
+WholeWordRegex = /\S+/
+WholeWordOrEmptyLineRegex = /^\s*$|\S+/
+AllWhitespace = /^\s$/
+
+class MotionError
+ constructor: (@message) ->
+ @name = 'Motion Error'
+
+class Motion
+ operatesInclusively: true
+ operatesLinewise: false
+
+ constructor: (@editor, @vimState) ->
+
+ select: (count, options) ->
+ value = for selection in @editor.getSelections()
+ if @isLinewise()
+ @moveSelectionLinewise(selection, count, options)
+ else if @isInclusive()
+ @moveSelectionInclusively(selection, count, options)
+ else
+ @moveSelection(selection, count, options)
+ not selection.isEmpty()
+
+ @editor.mergeCursors()
+ @editor.mergeIntersectingSelections()
+ value
+
+ execute: (count) ->
+ for cursor in @editor.getCursors()
+ @moveCursor(cursor, count)
+ @editor.mergeCursors()
+
+ moveSelectionLinewise: (selection, count, options) ->
+ selection.modifySelection =>
+ [oldStartRow, oldEndRow] = selection.getBufferRowRange()
+
+ wasEmpty = selection.isEmpty()
+ wasReversed = selection.isReversed()
+ unless wasEmpty or wasReversed
+ selection.cursor.moveLeft()
+
+ @moveCursor(selection.cursor, count, options)
+
+ isEmpty = selection.isEmpty()
+ isReversed = selection.isReversed()
+ unless isEmpty or isReversed
+ selection.cursor.moveRight()
+
+ [newStartRow, newEndRow] = selection.getBufferRowRange()
+
+ if isReversed and not wasReversed
+ newEndRow = Math.max(newEndRow, oldStartRow)
+ if wasReversed and not isReversed
+ newStartRow = Math.min(newStartRow, oldEndRow)
+
+ selection.setBufferRange([[newStartRow, 0], [newEndRow + 1, 0]])
+
+ moveSelectionInclusively: (selection, count, options) ->
+ selection.modifySelection =>
+ range = selection.getBufferRange()
+ [oldStart, oldEnd] = [range.start, range.end]
+
+ wasEmpty = selection.isEmpty()
+ wasReversed = selection.isReversed()
+ unless wasEmpty or wasReversed
+ selection.cursor.moveLeft()
+
+ @moveCursor(selection.cursor, count, options)
+
+ isEmpty = selection.isEmpty()
+ isReversed = selection.isReversed()
+ unless isEmpty or isReversed
+ selection.cursor.moveRight()
+
+ range = selection.getBufferRange()
+ [newStart, newEnd] = [range.start, range.end]
+
+ if (isReversed or isEmpty) and not (wasReversed or wasEmpty)
+ selection.setBufferRange([newStart, [newEnd.row, oldStart.column + 1]])
+ if wasReversed and not wasEmpty and not isReversed
+ selection.setBufferRange([[newStart.row, oldEnd.column - 1], newEnd])
+
+ moveSelection: (selection, count, options) ->
+ selection.modifySelection => @moveCursor(selection.cursor, count, options)
+
+ ensureCursorIsWithinLine: (cursor) ->
+ return if @vimState.mode is 'visual' or not cursor.selection.isEmpty()
+ {goalColumn} = cursor
+ {row, column} = cursor.getBufferPosition()
+ lastColumn = cursor.getCurrentLineBufferRange().end.column
+ if column >= lastColumn - 1
+ cursor.setBufferPosition([row, Math.max(lastColumn - 1, 0)])
+ cursor.goalColumn ?= goalColumn
+
+ isComplete: -> true
+
+ isRecordable: -> false
+
+ isLinewise: ->
+ if @vimState?.mode is 'visual'
+ @vimState?.submode is 'linewise'
+ else
+ @operatesLinewise
+
+ isInclusive: ->
+ @vimState.mode is 'visual' or @operatesInclusively
+
+class CurrentSelection extends Motion
+ constructor: (@editor, @vimState) ->
+ super(@editor, @vimState)
+ @selection = @editor.getSelectedBufferRanges()
+
+ execute: (count=1) ->
+ _.times(count, -> true)
+
+ select: (count=1) ->
+ @editor.setSelectedBufferRanges(@selection)
+ _.times(count, -> true)
+
+# Public: Generic class for motions that require extra input
+class MotionWithInput extends Motion
+ constructor: (@editor, @vimState) ->
+ super(@editor, @vimState)
+ @complete = false
+
+ isComplete: -> @complete
+
+ canComposeWith: (operation) -> return operation.characters?
+
+ compose: (input) ->
+ if not input.characters
+ throw new MotionError('Must compose with an Input')
+ @input = input
+ @complete = true
+
+class MoveLeft extends Motion
+ operatesInclusively: false
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, =>
+ cursor.moveLeft() if not cursor.isAtBeginningOfLine() or settings.wrapLeftRightMotion()
+ @ensureCursorIsWithinLine(cursor)
+
+class MoveRight extends Motion
+ operatesInclusively: false
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, =>
+ wrapToNextLine = settings.wrapLeftRightMotion()
+
+ # when the motion is combined with an operator, we will only wrap to the next line
+ # if we are already at the end of the line (after the last character)
+ wrapToNextLine = false if @vimState.mode is 'operator-pending' and not cursor.isAtEndOfLine()
+
+ cursor.moveRight() unless cursor.isAtEndOfLine()
+ cursor.moveRight() if wrapToNextLine and cursor.isAtEndOfLine()
+ @ensureCursorIsWithinLine(cursor)
+
+class MoveUp extends Motion
+ operatesLinewise: true
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, =>
+ unless cursor.getScreenRow() is 0
+ cursor.moveUp()
+ @ensureCursorIsWithinLine(cursor)
+
+class MoveDown extends Motion
+ operatesLinewise: true
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, =>
+ unless cursor.getScreenRow() is @editor.getLastScreenRow()
+ cursor.moveDown()
+ @ensureCursorIsWithinLine(cursor)
+
+class MoveToPreviousWord extends Motion
+ operatesInclusively: false
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, ->
+ cursor.moveToBeginningOfWord()
+
+class MoveToPreviousWholeWord extends Motion
+ operatesInclusively: false
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, =>
+ cursor.moveToBeginningOfWord()
+ while not @isWholeWord(cursor) and not @isBeginningOfFile(cursor)
+ cursor.moveToBeginningOfWord()
+
+ isWholeWord: (cursor) ->
+ char = cursor.getCurrentWordPrefix().slice(-1)
+ AllWhitespace.test(char)
+
+ isBeginningOfFile: (cursor) ->
+ cur = cursor.getBufferPosition()
+ not cur.row and not cur.column
+
+class MoveToNextWord extends Motion
+ wordRegex: null
+ operatesInclusively: false
+
+ moveCursor: (cursor, count=1, options) ->
+ _.times count, =>
+ current = cursor.getBufferPosition()
+
+ next = if options?.excludeWhitespace
+ cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex)
+ else
+ cursor.getBeginningOfNextWordBufferPosition(wordRegex: @wordRegex)
+
+ return if @isEndOfFile(cursor)
+
+ if cursor.isAtEndOfLine()
+ cursor.moveDown()
+ cursor.moveToBeginningOfLine()
+ cursor.skipLeadingWhitespace()
+ else if current.row is next.row and current.column is next.column
+ cursor.moveToEndOfWord()
+ else
+ cursor.setBufferPosition(next)
+
+ isEndOfFile: (cursor) ->
+ cur = cursor.getBufferPosition()
+ eof = @editor.getEofBufferPosition()
+ cur.row is eof.row and cur.column is eof.column
+
+class MoveToNextWholeWord extends MoveToNextWord
+ wordRegex: WholeWordOrEmptyLineRegex
+
+class MoveToEndOfWord extends Motion
+ wordRegex: null
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, =>
+ current = cursor.getBufferPosition()
+
+ next = cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex)
+ next.column-- if next.column > 0
+
+ if next.isEqual(current)
+ cursor.moveRight()
+ if cursor.isAtEndOfLine()
+ cursor.moveDown()
+ cursor.moveToBeginningOfLine()
+
+ next = cursor.getEndOfCurrentWordBufferPosition(wordRegex: @wordRegex)
+ next.column-- if next.column > 0
+
+ cursor.setBufferPosition(next)
+
+class MoveToEndOfWholeWord extends MoveToEndOfWord
+ wordRegex: WholeWordRegex
+
+class MoveToNextParagraph extends Motion
+ operatesInclusively: false
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, ->
+ cursor.moveToBeginningOfNextParagraph()
+
+class MoveToPreviousParagraph extends Motion
+ moveCursor: (cursor, count=1) ->
+ _.times count, ->
+ cursor.moveToBeginningOfPreviousParagraph()
+
+class MoveToLine extends Motion
+ operatesLinewise: true
+
+ getDestinationRow: (count) ->
+ if count? then count - 1 else (@editor.getLineCount() - 1)
+
+class MoveToAbsoluteLine extends MoveToLine
+ moveCursor: (cursor, count) ->
+ cursor.setBufferPosition([@getDestinationRow(count), Infinity])
+ cursor.moveToFirstCharacterOfLine()
+ cursor.moveToEndOfLine() if cursor.getBufferColumn() is 0
+
+class MoveToRelativeLine extends MoveToLine
+ operatesLinewise: true
+
+ moveCursor: (cursor, count=1) ->
+ {row, column} = cursor.getBufferPosition()
+ cursor.setBufferPosition([row + (count - 1), 0])
+
+class MoveToScreenLine extends MoveToLine
+ constructor: (@editorElement, @vimState, @scrolloff) ->
+ @scrolloff = 2 # atom default
+ super(@editorElement.getModel(), @vimState)
+
+ moveCursor: (cursor, count=1) ->
+ {row, column} = cursor.getBufferPosition()
+ cursor.setScreenPosition([@getDestinationRow(count), 0])
+
+class MoveToBeginningOfLine extends Motion
+ operatesInclusively: false
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, ->
+ cursor.moveToBeginningOfLine()
+
+class MoveToFirstCharacterOfLine extends Motion
+ operatesInclusively: false
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, ->
+ cursor.moveToBeginningOfLine()
+ cursor.moveToFirstCharacterOfLine()
+
+class MoveToFirstCharacterOfLineAndDown extends Motion
+ operatesLinewise: true
+ operatesInclusively: true
+
+ moveCursor: (cursor, count=0) ->
+ _.times count-1, ->
+ cursor.moveDown()
+ cursor.moveToBeginningOfLine()
+ cursor.moveToFirstCharacterOfLine()
+
+class MoveToLastCharacterOfLine extends Motion
+ operatesInclusively: false
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, =>
+ cursor.moveToEndOfLine()
+ cursor.goalColumn = Infinity
+ @ensureCursorIsWithinLine(cursor)
+
+class MoveToLastNonblankCharacterOfLineAndDown extends Motion
+ operatesInclusively: true
+
+ # moves cursor to the last non-whitespace character on the line
+ # similar to skipLeadingWhitespace() in atom's cursor.coffee
+ skipTrailingWhitespace: (cursor) ->
+ position = cursor.getBufferPosition()
+ scanRange = cursor.getCurrentLineBufferRange()
+ startOfTrailingWhitespace = [scanRange.end.row, scanRange.end.column - 1]
+ @editor.scanInBufferRange /[ \t]+$/, scanRange, ({range}) ->
+ startOfTrailingWhitespace = range.start
+ startOfTrailingWhitespace.column -= 1
+ cursor.setBufferPosition(startOfTrailingWhitespace)
+
+ moveCursor: (cursor, count=1) ->
+ _.times count-1, ->
+ cursor.moveDown()
+ @skipTrailingWhitespace(cursor)
+
+class MoveToFirstCharacterOfLineUp extends Motion
+ operatesLinewise: true
+ operatesInclusively: true
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, ->
+ cursor.moveUp()
+ cursor.moveToBeginningOfLine()
+ cursor.moveToFirstCharacterOfLine()
+
+class MoveToFirstCharacterOfLineDown extends Motion
+ operatesLinewise: true
+
+ moveCursor: (cursor, count=1) ->
+ _.times count, ->
+ cursor.moveDown()
+ cursor.moveToBeginningOfLine()
+ cursor.moveToFirstCharacterOfLine()
+
+class MoveToStartOfFile extends MoveToLine
+ moveCursor: (cursor, count=1) ->
+ {row, column} = @editor.getCursorBufferPosition()
+ cursor.setBufferPosition([@getDestinationRow(count), 0])
+ unless @isLinewise()
+ cursor.moveToFirstCharacterOfLine()
+
+class MoveToTopOfScreen extends MoveToScreenLine
+ getDestinationRow: (count=0) ->
+ firstScreenRow = @editorElement.getFirstVisibleScreenRow()
+ if firstScreenRow > 0
+ offset = Math.max(count - 1, @scrolloff)
+ else
+ offset = if count > 0 then count - 1 else count
+ firstScreenRow + offset
+
+class MoveToBottomOfScreen extends MoveToScreenLine
+ getDestinationRow: (count=0) ->
+ lastScreenRow = @editorElement.getLastVisibleScreenRow()
+ lastRow = @editor.getBuffer().getLastRow()
+ if lastScreenRow isnt lastRow
+ offset = Math.max(count - 1, @scrolloff)
+ else
+ offset = if count > 0 then count - 1 else count
+ lastScreenRow - offset
+
+class MoveToMiddleOfScreen extends MoveToScreenLine
+ getDestinationRow: (count) ->
+ firstScreenRow = @editorElement.getFirstVisibleScreenRow()
+ lastScreenRow = @editorElement.getLastVisibleScreenRow()
+ height = lastScreenRow - firstScreenRow
+ Math.floor(firstScreenRow + (height / 2))
+
+class ScrollKeepingCursor extends MoveToLine
+ previousFirstScreenRow: 0
+ currentFirstScreenRow: 0
+
+ constructor: (@editorElement, @vimState) ->
+ super(@editorElement.getModel(), @vimState)
+
+ select: (count, options) ->
+ finalDestination = @scrollScreen(count)
+ super(count, options)
+ @editor.setScrollTop(finalDestination)
+
+ execute: (count) ->
+ finalDestination = @scrollScreen(count)
+ super(count)
+ @editor.setScrollTop(finalDestination)
+
+ moveCursor: (cursor, count=1) ->
+ cursor.setScreenPosition([@getDestinationRow(count), 0])
+
+ getDestinationRow: (count) ->
+ {row, column} = @editor.getCursorScreenPosition()
+ @currentFirstScreenRow - @previousFirstScreenRow + row
+
+ scrollScreen: (count = 1) ->
+ @previousFirstScreenRow = @editorElement.getFirstVisibleScreenRow()
+ destination = @scrollDestination(count)
+ @editor.setScrollTop(destination)
+ @currentFirstScreenRow = @editorElement.getFirstVisibleScreenRow()
+ destination
+
+class ScrollHalfUpKeepCursor extends ScrollKeepingCursor
+ scrollDestination: (count) ->
+ half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels())
+ @editor.getScrollTop() - count * half
+
+class ScrollFullUpKeepCursor extends ScrollKeepingCursor
+ scrollDestination: (count) ->
+ @editor.getScrollTop() - (count * @editor.getHeight())
+
+class ScrollHalfDownKeepCursor extends ScrollKeepingCursor
+ scrollDestination: (count) ->
+ half = (Math.floor(@editor.getRowsPerPage() / 2) * @editor.getLineHeightInPixels())
+ @editor.getScrollTop() + count * half
+
+class ScrollFullDownKeepCursor extends ScrollKeepingCursor
+ scrollDestination: (count) ->
+ @editor.getScrollTop() + (count * @editor.getHeight())
+
+module.exports = {
+ Motion, MotionWithInput, CurrentSelection, MoveLeft, MoveRight, MoveUp, MoveDown,
+ MoveToPreviousWord, MoveToPreviousWholeWord, MoveToNextWord, MoveToNextWholeWord,
+ MoveToEndOfWord, MoveToNextParagraph, MoveToPreviousParagraph, MoveToAbsoluteLine, MoveToRelativeLine, MoveToBeginningOfLine,
+ MoveToFirstCharacterOfLineUp, MoveToFirstCharacterOfLineDown,
+ MoveToFirstCharacterOfLine, MoveToFirstCharacterOfLineAndDown, MoveToLastCharacterOfLine,
+ MoveToLastNonblankCharacterOfLineAndDown, MoveToStartOfFile,
+ MoveToTopOfScreen, MoveToBottomOfScreen, MoveToMiddleOfScreen, MoveToEndOfWholeWord, MotionError,
+ ScrollHalfUpKeepCursor, ScrollFullUpKeepCursor,
+ ScrollHalfDownKeepCursor, ScrollFullDownKeepCursor
+}
diff --git a/atom/packages/vim-mode/lib/motions/index.coffee b/atom/packages/vim-mode/lib/motions/index.coffee
new file mode 100644
index 0000000..0fd420f
--- /dev/null
+++ b/atom/packages/vim-mode/lib/motions/index.coffee
@@ -0,0 +1,14 @@
+Motions = require './general-motions'
+{Search, SearchCurrentWord, BracketMatchingMotion, RepeatSearch} = require './search-motion'
+MoveToMark = require './move-to-mark-motion'
+{Find, Till} = require './find-motion'
+
+Motions.Search = Search
+Motions.SearchCurrentWord = SearchCurrentWord
+Motions.BracketMatchingMotion = BracketMatchingMotion
+Motions.RepeatSearch = RepeatSearch
+Motions.MoveToMark = MoveToMark
+Motions.Find = Find
+Motions.Till = Till
+
+module.exports = Motions
diff --git a/atom/packages/vim-mode/lib/motions/move-to-mark-motion.coffee b/atom/packages/vim-mode/lib/motions/move-to-mark-motion.coffee
new file mode 100644
index 0000000..0187620
--- /dev/null
+++ b/atom/packages/vim-mode/lib/motions/move-to-mark-motion.coffee
@@ -0,0 +1,25 @@
+{MotionWithInput, MoveToFirstCharacterOfLine} = require './general-motions'
+{ViewModel} = require '../view-models/view-model'
+{Point, Range} = require 'atom'
+
+module.exports =
+class MoveToMark extends MotionWithInput
+ operatesInclusively: false
+
+ constructor: (@editor, @vimState, @linewise=true) ->
+ super(@editor, @vimState)
+ @operatesLinewise = @linewise
+ @viewModel = new ViewModel(this, class: 'move-to-mark', singleChar: true, hidden: true)
+
+ isLinewise: -> @linewise
+
+ moveCursor: (cursor, count=1) ->
+ markPosition = @vimState.getMark(@input.characters)
+
+ if @input.characters is '`' # double '`' pressed
+ markPosition ?= [0, 0] # if markPosition not set, go to the beginning of the file
+ @vimState.setMark('`', cursor.getBufferPosition())
+
+ cursor.setBufferPosition(markPosition) if markPosition?
+ if @linewise
+ cursor.moveToFirstCharacterOfLine()
diff --git a/atom/packages/vim-mode/lib/motions/search-motion.coffee b/atom/packages/vim-mode/lib/motions/search-motion.coffee
new file mode 100644
index 0000000..11761ad
--- /dev/null
+++ b/atom/packages/vim-mode/lib/motions/search-motion.coffee
@@ -0,0 +1,210 @@
+_ = require 'underscore-plus'
+{MotionWithInput} = require './general-motions'
+SearchViewModel = require '../view-models/search-view-model'
+{Input} = require '../view-models/view-model'
+{Point, Range} = require 'atom'
+settings = require '../settings'
+
+class SearchBase extends MotionWithInput
+ operatesInclusively: false
+
+ constructor: (@editor, @vimState, options = {}) ->
+ super(@editor, @vimState)
+ @reverse = @initiallyReversed = false
+ @updateCurrentSearch() unless options.dontUpdateCurrentSearch
+
+ reversed: =>
+ @initiallyReversed = @reverse = true
+ @updateCurrentSearch()
+ this
+
+ moveCursor: (cursor, count=1) ->
+ ranges = @scan(cursor)
+ if ranges.length > 0
+ range = ranges[(count - 1) % ranges.length]
+ cursor.setBufferPosition(range.start)
+ else
+ atom.beep()
+
+ scan: (cursor) ->
+ currentPosition = cursor.getBufferPosition()
+
+ [rangesBefore, rangesAfter] = [[], []]
+ @editor.scan @getSearchTerm(@input.characters), ({range}) =>
+ isBefore = if @reverse
+ range.start.compare(currentPosition) < 0
+ else
+ range.start.compare(currentPosition) <= 0
+
+ if isBefore
+ rangesBefore.push(range)
+ else
+ rangesAfter.push(range)
+
+ if @reverse
+ rangesAfter.concat(rangesBefore).reverse()
+ else
+ rangesAfter.concat(rangesBefore)
+
+ getSearchTerm: (term) ->
+ modifiers = {'g': true}
+
+ if not term.match('[A-Z]') and settings.useSmartcaseForSearch()
+ modifiers['i'] = true
+
+ if term.indexOf('\\c') >= 0
+ term = term.replace('\\c', '')
+ modifiers['i'] = true
+
+ modFlags = Object.keys(modifiers).join('')
+
+ try
+ new RegExp(term, modFlags)
+ catch
+ new RegExp(_.escapeRegExp(term), modFlags)
+
+ updateCurrentSearch: ->
+ @vimState.globalVimState.currentSearch.reverse = @reverse
+ @vimState.globalVimState.currentSearch.initiallyReversed = @initiallyReversed
+
+ replicateCurrentSearch: ->
+ @reverse = @vimState.globalVimState.currentSearch.reverse
+ @initiallyReversed = @vimState.globalVimState.currentSearch.initiallyReversed
+
+class Search extends SearchBase
+ constructor: (@editor, @vimState) ->
+ super(@editor, @vimState)
+ @viewModel = new SearchViewModel(this)
+
+class SearchCurrentWord extends SearchBase
+ @keywordRegex: null
+
+ constructor: (@editor, @vimState) ->
+ super(@editor, @vimState)
+
+ # FIXME: This must depend on the current language
+ defaultIsKeyword = "[@a-zA-Z0-9_\-]+"
+ userIsKeyword = atom.config.get('vim-mode.iskeyword')
+ @keywordRegex = new RegExp(userIsKeyword or defaultIsKeyword)
+
+ searchString = @getCurrentWordMatch()
+ @input = new Input(searchString)
+ @vimState.pushSearchHistory(searchString) unless searchString is @vimState.getSearchHistoryItem()
+
+ getCurrentWord: ->
+ cursor = @editor.getLastCursor()
+ wordStart = cursor.getBeginningOfCurrentWordBufferPosition(wordRegex: @keywordRegex, allowPrevious: false)
+ wordEnd = cursor.getEndOfCurrentWordBufferPosition (wordRegex: @keywordRegex, allowNext: false)
+ cursorPosition = cursor.getBufferPosition()
+
+ if wordEnd.column is cursorPosition.column
+ # either we don't have a current word, or it ends on cursor, i.e. precedes it, so look for the next one
+ wordEnd = cursor.getEndOfCurrentWordBufferPosition (wordRegex: @keywordRegex, allowNext: true)
+ return "" if wordEnd.row isnt cursorPosition.row # don't look beyond the current line
+
+ cursor.setBufferPosition wordEnd
+ wordStart = cursor.getBeginningOfCurrentWordBufferPosition(wordRegex: @keywordRegex, allowPrevious: false)
+
+ cursor.setBufferPosition wordStart
+
+ @editor.getTextInBufferRange([wordStart, wordEnd])
+
+ cursorIsOnEOF: (cursor) ->
+ pos = cursor.getNextWordBoundaryBufferPosition(wordRegex: @keywordRegex)
+ eofPos = @editor.getEofBufferPosition()
+ pos.row is eofPos.row and pos.column is eofPos.column
+
+ getCurrentWordMatch: ->
+ characters = @getCurrentWord()
+ if characters.length > 0
+ if /\W/.test(characters) then "#{characters}\\b" else "\\b#{characters}\\b"
+ else
+ characters
+
+ isComplete: -> true
+
+ execute: (count=1) ->
+ super(count) if @input.characters.length > 0
+
+OpenBrackets = ['(', '{', '[']
+CloseBrackets = [')', '}', ']']
+AnyBracket = new RegExp(OpenBrackets.concat(CloseBrackets).map(_.escapeRegExp).join("|"))
+
+class BracketMatchingMotion extends SearchBase
+ operatesInclusively: true
+
+ isComplete: -> true
+
+ searchForMatch: (startPosition, reverse, inCharacter, outCharacter) ->
+ depth = 0
+ point = startPosition.copy()
+ lineLength = @editor.lineTextForBufferRow(point.row).length
+ eofPosition = @editor.getEofBufferPosition().translate([0, 1])
+ increment = if reverse then -1 else 1
+
+ loop
+ character = @characterAt(point)
+ depth++ if character is inCharacter
+ depth-- if character is outCharacter
+
+ return point if depth is 0
+
+ point.column += increment
+
+ return null if depth < 0
+ return null if point.isEqual([0, -1])
+ return null if point.isEqual(eofPosition)
+
+ if point.column < 0
+ point.row--
+ lineLength = @editor.lineTextForBufferRow(point.row).length
+ point.column = lineLength - 1
+ else if point.column >= lineLength
+ point.row++
+ lineLength = @editor.lineTextForBufferRow(point.row).length
+ point.column = 0
+
+ characterAt: (position) ->
+ @editor.getTextInBufferRange([position, position.translate([0, 1])])
+
+ getSearchData: (position) ->
+ character = @characterAt(position)
+ if (index = OpenBrackets.indexOf(character)) >= 0
+ [character, CloseBrackets[index], false]
+ else if (index = CloseBrackets.indexOf(character)) >= 0
+ [character, OpenBrackets[index], true]
+ else
+ []
+
+ moveCursor: (cursor) ->
+ startPosition = cursor.getBufferPosition()
+
+ [inCharacter, outCharacter, reverse] = @getSearchData(startPosition)
+
+ unless inCharacter?
+ restOfLine = [startPosition, [startPosition.row, Infinity]]
+ @editor.scanInBufferRange AnyBracket, restOfLine, ({range, stop}) ->
+ startPosition = range.start
+ stop()
+
+ [inCharacter, outCharacter, reverse] = @getSearchData(startPosition)
+
+ return unless inCharacter?
+
+ if matchPosition = @searchForMatch(startPosition, reverse, inCharacter, outCharacter)
+ cursor.setBufferPosition(matchPosition)
+
+class RepeatSearch extends SearchBase
+ constructor: (@editor, @vimState) ->
+ super(@editor, @vimState, dontUpdateCurrentSearch: true)
+ @input = new Input(@vimState.getSearchHistoryItem(0) ? "")
+ @replicateCurrentSearch()
+
+ isComplete: -> true
+
+ reversed: ->
+ @reverse = not @initiallyReversed
+ this
+
+
+module.exports = {Search, SearchCurrentWord, BracketMatchingMotion, RepeatSearch}
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()
diff --git a/atom/packages/vim-mode/lib/prefixes.coffee b/atom/packages/vim-mode/lib/prefixes.coffee
new file mode 100644
index 0000000..b29bc92
--- /dev/null
+++ b/atom/packages/vim-mode/lib/prefixes.coffee
@@ -0,0 +1,68 @@
+class Prefix
+ complete: null
+ composedObject: null
+
+ isComplete: -> @complete
+
+ isRecordable: -> @composedObject.isRecordable()
+
+ # Public: Marks this as complete upon receiving an object to compose with.
+ #
+ # composedObject - The next motion or operator.
+ #
+ # Returns nothing.
+ compose: (@composedObject) ->
+ @complete = true
+
+ # Public: Executes the composed operator or motion.
+ #
+ # Returns nothing.
+ execute: ->
+ @composedObject.execute?(@count)
+
+ # Public: Selects using the composed motion.
+ #
+ # Returns an array of booleans representing whether each selections' success.
+ select: ->
+ @composedObject.select?(@count)
+
+ isLinewise: ->
+ @composedObject.isLinewise()
+
+#
+# Used to track the number of times either a motion or operator should
+# be repeated.
+#
+class Repeat extends Prefix
+ count: null
+
+ # count - The initial digit of the repeat sequence.
+ constructor: (@count) -> @complete = false
+
+ # Public: Adds an additional digit to this repeat sequence.
+ #
+ # digit - A single digit, 0-9.
+ #
+ # Returns nothing.
+ addDigit: (digit) ->
+ @count = @count * 10 + digit
+
+#
+# Used to track which register the following operator should operate on.
+#
+class Register extends Prefix
+ name: null
+
+ # name - The single character name of the desired register
+ constructor: (@name) -> @complete = false
+
+ # Public: Marks as complete and sets the operator's register if it accepts it.
+ #
+ # composedOperator - The operator this register pertains to.
+ #
+ # Returns nothing.
+ compose: (composedObject) ->
+ super(composedObject)
+ composedObject.register = @name if composedObject.register?
+
+module.exports = {Repeat, Register}
diff --git a/atom/packages/vim-mode/lib/scroll.coffee b/atom/packages/vim-mode/lib/scroll.coffee
new file mode 100644
index 0000000..1bc1258
--- /dev/null
+++ b/atom/packages/vim-mode/lib/scroll.coffee
@@ -0,0 +1,88 @@
+class Scroll
+ isComplete: -> true
+ isRecordable: -> false
+ constructor: (@editorElement) ->
+ @scrolloff = 2 # atom default
+ @editor = @editorElement.getModel()
+ @rows =
+ first: @editorElement.getFirstVisibleScreenRow()
+ last: @editorElement.getLastVisibleScreenRow()
+ final: @editor.getLastScreenRow()
+
+class ScrollDown extends Scroll
+ execute: (count=1) ->
+ @keepCursorOnScreen(count)
+ @scrollUp(count)
+
+ keepCursorOnScreen: (count) ->
+ {row, column} = @editor.getCursorScreenPosition()
+ firstScreenRow = @rows.first + @scrolloff + 1
+ if row - count <= firstScreenRow
+ @editor.setCursorScreenPosition([firstScreenRow + count, column])
+
+ scrollUp: (count) ->
+ lastScreenRow = @rows.last - @scrolloff
+ @editor.scrollToScreenPosition([lastScreenRow + count, 0])
+
+class ScrollUp extends Scroll
+ execute: (count=1) ->
+ @keepCursorOnScreen(count)
+ @scrollDown(count)
+
+ keepCursorOnScreen: (count) ->
+ {row, column} = @editor.getCursorScreenPosition()
+ lastScreenRow = @rows.last - @scrolloff - 1
+ if row + count >= lastScreenRow
+ @editor.setCursorScreenPosition([lastScreenRow - count, column])
+
+ scrollDown: (count) ->
+ firstScreenRow = @rows.first + @scrolloff
+ @editor.scrollToScreenPosition([firstScreenRow - count, 0])
+
+class ScrollCursor extends Scroll
+ constructor: (@editorElement, @opts={}) ->
+ super
+ cursor = @editor.getCursorScreenPosition()
+ @pixel = @editorElement.pixelPositionForScreenPosition(cursor).top
+
+class ScrollCursorToTop extends ScrollCursor
+ execute: ->
+ @moveToFirstNonBlank() unless @opts.leaveCursor
+ @scrollUp()
+
+ scrollUp: ->
+ return if @rows.last is @rows.final
+ @pixel -= (@editor.getLineHeightInPixels() * @scrolloff)
+ @editor.setScrollTop(@pixel)
+
+ moveToFirstNonBlank: ->
+ @editor.moveToFirstCharacterOfLine()
+
+class ScrollCursorToMiddle extends ScrollCursor
+ execute: ->
+ @moveToFirstNonBlank() unless @opts.leaveCursor
+ @scrollMiddle()
+
+ scrollMiddle: ->
+ @pixel -= (@editor.getHeight() / 2)
+ @editor.setScrollTop(@pixel)
+
+ moveToFirstNonBlank: ->
+ @editor.moveToFirstCharacterOfLine()
+
+class ScrollCursorToBottom extends ScrollCursor
+ execute: ->
+ @moveToFirstNonBlank() unless @opts.leaveCursor
+ @scrollDown()
+
+ scrollDown: ->
+ return if @rows.first is 0
+ offset = (@editor.getLineHeightInPixels() * (@scrolloff + 1))
+ @pixel -= (@editor.getHeight() - offset)
+ @editor.setScrollTop(@pixel)
+
+ moveToFirstNonBlank: ->
+ @editor.moveToFirstCharacterOfLine()
+
+module.exports = {ScrollDown, ScrollUp, ScrollCursorToTop, ScrollCursorToMiddle,
+ ScrollCursorToBottom}
diff --git a/atom/packages/vim-mode/lib/settings.coffee b/atom/packages/vim-mode/lib/settings.coffee
new file mode 100644
index 0000000..30ff35b
--- /dev/null
+++ b/atom/packages/vim-mode/lib/settings.coffee
@@ -0,0 +1,28 @@
+
+settings =
+ config:
+ startInInsertMode:
+ type: 'boolean'
+ default: false
+ useSmartcaseForSearch:
+ type: 'boolean'
+ default: false
+ wrapLeftRightMotion:
+ type: 'boolean'
+ default: false
+ useClipboardAsDefaultRegister:
+ type: 'boolean'
+ default: false
+ numberRegex:
+ type: 'string'
+ default: '-?[0-9]+'
+ description: 'Use this to control how Ctrl-A/Ctrl-X finds numbers; use "(?:\\B-)?[0-9]+" to treat numbers as positive if the minus is preceded by a character, e.g. in "identifier-1".'
+
+Object.keys(settings.config).forEach (k) ->
+ settings[k] = ->
+ atom.config.get('vim-mode.'+k)
+
+settings.defaultRegister = ->
+ if settings.useClipboardAsDefaultRegister() then '*' else '"'
+
+module.exports = settings
diff --git a/atom/packages/vim-mode/lib/status-bar-manager.coffee b/atom/packages/vim-mode/lib/status-bar-manager.coffee
new file mode 100644
index 0000000..82e35e1
--- /dev/null
+++ b/atom/packages/vim-mode/lib/status-bar-manager.coffee
@@ -0,0 +1,37 @@
+{Disposable, CompositeDisposable} = require 'event-kit'
+
+ContentsByMode =
+ 'insert': ["status-bar-vim-mode-insert", "Insert"]
+ 'insert.replace': ["status-bar-vim-mode-insert", "Replace"]
+ 'command': ["status-bar-vim-mode-command", "Command"]
+ 'visual': ["status-bar-vim-mode-visual", "Visual"]
+ 'visual.characterwise': ["status-bar-vim-mode-visual", "Visual"]
+ 'visual.linewise': ["status-bar-vim-mode-visual", "Visual Line"]
+ 'visual.blockwise': ["status-bar-vim-mode-visual", "Visual Block"]
+
+module.exports =
+class StatusBarManager
+ constructor: ->
+ @element = document.createElement("div")
+ @element.id = "status-bar-vim-mode"
+
+ @container = document.createElement("div")
+ @container.className = "inline-block"
+ @container.appendChild(@element)
+
+ initialize: (@statusBar) ->
+
+ update: (currentMode, currentSubmode) ->
+ currentMode = currentMode + "." + currentSubmode if currentSubmode?
+ if newContents = ContentsByMode[currentMode]
+ [klass, text] = newContents
+ @element.className = klass
+ @element.textContent = text
+
+ # Private
+
+ attach: ->
+ @tile = @statusBar.addRightTile(item: @container, priority: 20)
+
+ detach: ->
+ @tile.destroy()
diff --git a/atom/packages/vim-mode/lib/text-objects.coffee b/atom/packages/vim-mode/lib/text-objects.coffee
new file mode 100644
index 0000000..6d9741b
--- /dev/null
+++ b/atom/packages/vim-mode/lib/text-objects.coffee
@@ -0,0 +1,164 @@
+{Range} = require 'atom'
+AllWhitespace = /^\s$/
+
+class TextObject
+ constructor: (@editor, @state) ->
+
+ isComplete: -> true
+ isRecordable: -> false
+
+class SelectInsideWord extends TextObject
+ select: ->
+ @editor.selectWordsContainingCursors()
+ [true]
+
+# SelectInsideQuotes and the next class defined (SelectInsideBrackets) are
+# almost-but-not-quite-repeated code. They are different because of the depth
+# checks in the bracket matcher.
+
+class SelectInsideQuotes extends TextObject
+ constructor: (@editor, @char, @includeQuotes) ->
+
+ findOpeningQuote: (pos) ->
+ start = pos.copy()
+ pos = pos.copy()
+ while pos.row >= 0
+ line = @editor.lineTextForBufferRow(pos.row)
+ pos.column = line.length - 1 if pos.column is -1
+ while pos.column >= 0
+ if line[pos.column] is @char
+ if pos.column is 0 or line[pos.column - 1] isnt '\\'
+ if @isStartQuote(pos)
+ return pos
+ else
+ return @lookForwardOnLine(start)
+ -- pos.column
+ pos.column = -1
+ -- pos.row
+ @lookForwardOnLine(start)
+
+ isStartQuote: (end) ->
+ line = @editor.lineTextForBufferRow(end.row)
+ numQuotes = line.substring(0, end.column + 1).replace( "'#{@char}", '').split(@char).length - 1
+ numQuotes % 2
+
+ lookForwardOnLine: (pos) ->
+ line = @editor.lineTextForBufferRow(pos.row)
+
+ index = line.substring(pos.column).indexOf(@char)
+ if index >= 0
+ pos.column += index
+ return pos
+ null
+
+ findClosingQuote: (start) ->
+ end = start.copy()
+ escaping = false
+
+ while end.row < @editor.getLineCount()
+ endLine = @editor.lineTextForBufferRow(end.row)
+ while end.column < endLine.length
+ if endLine[end.column] is '\\'
+ ++ end.column
+ else if endLine[end.column] is @char
+ -- start.column if @includeQuotes
+ ++ end.column if @includeQuotes
+ return end
+ ++ end.column
+ end.column = 0
+ ++ end.row
+ return
+
+ select: ->
+ for selection in @editor.getSelections()
+ start = @findOpeningQuote(selection.cursor.getBufferPosition())
+ if start?
+ ++ start.column # skip the opening quote
+ end = @findClosingQuote(start)
+ if end?
+ selection.setBufferRange([start, end])
+ not selection.isEmpty()
+
+# SelectInsideBrackets and the previous class defined (SelectInsideQuotes) are
+# almost-but-not-quite-repeated code. They are different because of the depth
+# checks in the bracket matcher.
+
+class SelectInsideBrackets extends TextObject
+ constructor: (@editor, @beginChar, @endChar, @includeBrackets) ->
+
+ findOpeningBracket: (pos) ->
+ pos = pos.copy()
+ depth = 0
+ while pos.row >= 0
+ line = @editor.lineTextForBufferRow(pos.row)
+ pos.column = line.length - 1 if pos.column is -1
+ while pos.column >= 0
+ switch line[pos.column]
+ when @endChar then ++ depth
+ when @beginChar
+ return pos if -- depth < 0
+ -- pos.column
+ pos.column = -1
+ -- pos.row
+
+ findClosingBracket: (start) ->
+ end = start.copy()
+ depth = 0
+ while end.row < @editor.getLineCount()
+ endLine = @editor.lineTextForBufferRow(end.row)
+ while end.column < endLine.length
+ switch endLine[end.column]
+ when @beginChar then ++ depth
+ when @endChar
+ if -- depth < 0
+ -- start.column if @includeBrackets
+ ++ end.column if @includeBrackets
+ return end
+ ++ end.column
+ end.column = 0
+ ++ end.row
+ return
+
+ select: ->
+ for selection in @editor.getSelections()
+ start = @findOpeningBracket(selection.cursor.getBufferPosition())
+ if start?
+ ++ start.column # skip the opening quote
+ end = @findClosingBracket(start)
+ if end?
+ selection.setBufferRange([start, end])
+ not selection.isEmpty()
+
+class SelectAWord extends TextObject
+ select: ->
+ for selection in @editor.getSelections()
+ selection.selectWord()
+ loop
+ endPoint = selection.getBufferRange().end
+ char = @editor.getTextInRange(Range.fromPointWithDelta(endPoint, 0, 1))
+ break unless AllWhitespace.test(char)
+ selection.selectRight()
+ true
+
+class SelectInsideParagraph extends TextObject
+ constructor: (@editor, @inclusive) ->
+ select: ->
+ for selection in @editor.getSelections()
+ range = selection.cursor.getCurrentParagraphBufferRange()
+ if range?
+ selection.setBufferRange(range)
+ selection.selectToBeginningOfNextParagraph()
+ true
+
+class SelectAParagraph extends TextObject
+ constructor: (@editor, @inclusive) ->
+ select: ->
+ for selection in @editor.getSelections()
+ range = selection.cursor.getCurrentParagraphBufferRange()
+ if range?
+ selection.setBufferRange(range)
+ selection.selectToBeginningOfNextParagraph()
+ selection.selectDown()
+ true
+
+module.exports = {TextObject, SelectInsideWord, SelectInsideQuotes, SelectInsideBrackets, SelectAWord, SelectInsideParagraph, SelectAParagraph}
diff --git a/atom/packages/vim-mode/lib/utils.coffee b/atom/packages/vim-mode/lib/utils.coffee
new file mode 100644
index 0000000..362b65d
--- /dev/null
+++ b/atom/packages/vim-mode/lib/utils.coffee
@@ -0,0 +1,14 @@
+module.exports =
+ # Public: Determines if a string should be considered linewise or character
+ #
+ # text - The string to consider
+ #
+ # Returns 'linewise' if the string ends with a line return and 'character'
+ # otherwise.
+ copyType: (text) ->
+ if text.lastIndexOf("\n") is text.length - 1
+ 'linewise'
+ else if text.lastIndexOf("\r") is text.length - 1
+ 'linewise'
+ else
+ 'character'
diff --git a/atom/packages/vim-mode/lib/view-models/search-view-model.coffee b/atom/packages/vim-mode/lib/view-models/search-view-model.coffee
new file mode 100644
index 0000000..8787210
--- /dev/null
+++ b/atom/packages/vim-mode/lib/view-models/search-view-model.coffee
@@ -0,0 +1,42 @@
+{ViewModel} = require './view-model'
+
+module.exports =
+class SearchViewModel extends ViewModel
+ constructor: (@searchMotion) ->
+ super(@searchMotion, class: 'search')
+ @historyIndex = -1
+
+ atom.commands.add(@view.editorElement, 'core:move-up', @increaseHistorySearch)
+ atom.commands.add(@view.editorElement, 'core:move-down', @decreaseHistorySearch)
+
+ restoreHistory: (index) ->
+ @view.editorElement.getModel().setText(@history(index))
+
+ history: (index) ->
+ @vimState.getSearchHistoryItem(index)
+
+ increaseHistorySearch: =>
+ if @history(@historyIndex + 1)?
+ @historyIndex += 1
+ @restoreHistory(@historyIndex)
+
+ decreaseHistorySearch: =>
+ if @historyIndex <= 0
+ # get us back to a clean slate
+ @historyIndex = -1
+ @view.editorElement.getModel().setText('')
+ else
+ @historyIndex -= 1
+ @restoreHistory(@historyIndex)
+
+ confirm: (view) =>
+ repeatChar = if @searchMotion.initiallyReversed then '?' else '/'
+ if @view.value is '' or @view.value is repeatChar
+ lastSearch = @history(0)
+ if lastSearch?
+ @view.value = lastSearch
+ else
+ @view.value = ''
+ atom.beep()
+ super(view)
+ @vimState.pushSearchHistory(@view.value)
diff --git a/atom/packages/vim-mode/lib/view-models/view-model.coffee b/atom/packages/vim-mode/lib/view-models/view-model.coffee
new file mode 100644
index 0000000..9cc63e1
--- /dev/null
+++ b/atom/packages/vim-mode/lib/view-models/view-model.coffee
@@ -0,0 +1,24 @@
+VimCommandModeInputElement = require './vim-command-mode-input-element'
+
+class ViewModel
+ constructor: (@operation, opts={}) ->
+ {@editor, @vimState} = @operation
+ @view = new VimCommandModeInputElement().initialize(this, opts)
+ @editor.commandModeInputView = @view
+ @vimState.onDidFailToCompose => @view.remove()
+
+ confirm: (view) ->
+ @vimState.pushOperations(new Input(@view.value))
+
+ cancel: (view) ->
+ if @vimState.isOperatorPending()
+ @vimState.pushOperations(new Input(''))
+
+class Input
+ constructor: (@characters) ->
+ isComplete: -> true
+ isRecordable: -> true
+
+module.exports = {
+ ViewModel, Input
+}
diff --git a/atom/packages/vim-mode/lib/view-models/vim-command-mode-input-element.coffee b/atom/packages/vim-mode/lib/view-models/vim-command-mode-input-element.coffee
new file mode 100644
index 0000000..4fd0c52
--- /dev/null
+++ b/atom/packages/vim-mode/lib/view-models/vim-command-mode-input-element.coffee
@@ -0,0 +1,64 @@
+class VimCommandModeInputElement extends HTMLDivElement
+ createdCallback: ->
+ @className = "command-mode-input"
+
+ @editorContainer = document.createElement("div")
+ @editorContainer.className = "editor-container"
+
+ @appendChild(@editorContainer)
+
+ initialize: (@viewModel, opts = {}) ->
+ if opts.class?
+ @editorContainer.classList.add(opts.class)
+
+ if opts.hidden
+ @editorContainer.style.height = "0px"
+
+ @editorElement = document.createElement "atom-text-editor"
+ @editorElement.classList.add('editor')
+ @editorElement.getModel().setMini(true)
+ @editorElement.setAttribute('mini', '')
+ @editorContainer.appendChild(@editorElement)
+
+ @singleChar = opts.singleChar
+ @defaultText = opts.defaultText ? ''
+
+ @panel = atom.workspace.addBottomPanel(item: this, priority: 100)
+
+ @focus()
+ @handleEvents()
+
+ this
+
+ handleEvents: ->
+ if @singleChar?
+ @editorElement.getModel().getBuffer().onDidChange (e) =>
+ @confirm() if e.newText
+ else
+ atom.commands.add(@editorElement, 'editor:newline', @confirm.bind(this))
+
+ atom.commands.add(@editorElement, 'core:confirm', @confirm.bind(this))
+ atom.commands.add(@editorElement, 'core:cancel', @cancel.bind(this))
+ atom.commands.add(@editorElement, 'blur', @cancel.bind(this))
+
+ confirm: ->
+ @value = @editorElement.getModel().getText() or @defaultText
+ @viewModel.confirm(this)
+ @removePanel()
+
+ focus: ->
+ @editorElement.focus()
+
+ cancel: (e) ->
+ @viewModel.cancel(this)
+ @removePanel()
+
+ removePanel: ->
+ atom.workspace.getActivePane().activate()
+ @panel.destroy()
+
+module.exports =
+document.registerElement("vim-command-mode-input"
+ extends: "div",
+ prototype: VimCommandModeInputElement.prototype
+)
diff --git a/atom/packages/vim-mode/lib/vim-mode.coffee b/atom/packages/vim-mode/lib/vim-mode.coffee
new file mode 100644
index 0000000..660fba0
--- /dev/null
+++ b/atom/packages/vim-mode/lib/vim-mode.coffee
@@ -0,0 +1,51 @@
+{Disposable, CompositeDisposable} = require 'event-kit'
+StatusBarManager = require './status-bar-manager'
+GlobalVimState = require './global-vim-state'
+VimState = require './vim-state'
+settings = require './settings'
+
+module.exports =
+ config: settings.config
+
+ activate: (state) ->
+ @disposables = new CompositeDisposable
+ @globalVimState = new GlobalVimState
+ @statusBarManager = new StatusBarManager
+
+ @vimStates = new Set
+ @vimStatesByEditor = new WeakMap
+
+ @disposables.add atom.workspace.observeTextEditors (editor) =>
+ return if editor.isMini() or @vimStatesByEditor.get(editor)
+
+ vimState = new VimState(
+ atom.views.getView(editor),
+ @statusBarManager,
+ @globalVimState
+ )
+
+ @vimStates.add(vimState)
+ @vimStatesByEditor.set(editor, vimState)
+ vimState.onDidDestroy => @vimStates.delete(vimState)
+
+ @disposables.add new Disposable =>
+ @vimStates.forEach (vimState) -> vimState.destroy()
+
+ deactivate: ->
+ @disposables.dispose()
+
+ getGlobalState: ->
+ @globalVimState
+
+ getEditorState: (editor) ->
+ @vimStatesByEditor.get(editor)
+
+ consumeStatusBar: (statusBar) ->
+ @statusBarManager.initialize(statusBar)
+ @statusBarManager.attach()
+ @disposables.add new Disposable =>
+ @statusBarManager.detach()
+
+ provideVimMode: ->
+ getGlobalState: @getGlobalState.bind(this)
+ getEditorState: @getEditorState.bind(this)
diff --git a/atom/packages/vim-mode/lib/vim-state.coffee b/atom/packages/vim-mode/lib/vim-state.coffee
new file mode 100644
index 0000000..a45551c
--- /dev/null
+++ b/atom/packages/vim-mode/lib/vim-state.coffee
@@ -0,0 +1,592 @@
+_ = require 'underscore-plus'
+{Point, Range} = require 'atom'
+{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
+settings = require './settings'
+
+Operators = require './operators/index'
+Prefixes = require './prefixes'
+Motions = require './motions/index'
+
+TextObjects = require './text-objects'
+Utils = require './utils'
+Scroll = require './scroll'
+
+module.exports =
+class VimState
+ editor: null
+ opStack: null
+ mode: null
+ submode: null
+ destroyed: false
+
+ constructor: (@editorElement, @statusBarManager, @globalVimState) ->
+ @emitter = new Emitter
+ @subscriptions = new CompositeDisposable
+ @editor = @editorElement.getModel()
+ @opStack = []
+ @history = []
+ @marks = {}
+ @subscriptions.add @editor.onDidDestroy => @destroy()
+
+ @subscriptions.add @editor.onDidChangeSelectionRange _.debounce(=>
+ return unless @editor?
+ if @editor.getSelections().every((selection) -> selection.isEmpty())
+ @activateCommandMode() if @mode is 'visual'
+ else
+ @activateVisualMode('characterwise') if @mode is 'command'
+ , 100)
+
+ @editorElement.classList.add("vim-mode")
+ @setupCommandMode()
+ if settings.startInInsertMode()
+ @activateInsertMode()
+ else
+ @activateCommandMode()
+
+ destroy: ->
+ unless @destroyed
+ @destroyed = true
+ @emitter.emit 'did-destroy'
+ @subscriptions.dispose()
+ if @editor.isAlive()
+ @deactivateInsertMode()
+ @editorElement.component?.setInputEnabled(true)
+ @editorElement.classList.remove("vim-mode")
+ @editorElement.classList.remove("command-mode")
+ @editor = null
+ @editorElement = null
+
+ # Private: Creates the plugin's bindings
+ #
+ # Returns nothing.
+ setupCommandMode: ->
+ @registerCommands
+ 'activate-command-mode': => @activateCommandMode()
+ 'activate-linewise-visual-mode': => @activateVisualMode('linewise')
+ 'activate-characterwise-visual-mode': => @activateVisualMode('characterwise')
+ 'activate-blockwise-visual-mode': => @activateVisualMode('blockwise')
+ 'reset-command-mode': => @resetCommandMode()
+ 'repeat-prefix': (e) => @repeatPrefix(e)
+ 'reverse-selections': (e) => @reverseSelections(e)
+ 'undo': (e) => @undo(e)
+
+ @registerOperationCommands
+ 'activate-insert-mode': => new Operators.Insert(@editor, this)
+ 'substitute': => new Operators.Substitute(@editor, this)
+ 'substitute-line': => new Operators.SubstituteLine(@editor, this)
+ 'insert-after': => new Operators.InsertAfter(@editor, this)
+ 'insert-after-end-of-line': => new Operators.InsertAfterEndOfLine(@editor, this)
+ 'insert-at-beginning-of-line': => new Operators.InsertAtBeginningOfLine(@editor, this)
+ 'insert-above-with-newline': => new Operators.InsertAboveWithNewline(@editor, this)
+ 'insert-below-with-newline': => new Operators.InsertBelowWithNewline(@editor, this)
+ 'delete': => @linewiseAliasedOperator(Operators.Delete)
+ 'change': => @linewiseAliasedOperator(Operators.Change)
+ 'change-to-last-character-of-line': => [new Operators.Change(@editor, this), new Motions.MoveToLastCharacterOfLine(@editor, this)]
+ 'delete-right': => [new Operators.Delete(@editor, this), new Motions.MoveRight(@editor, this)]
+ 'delete-left': => [new Operators.Delete(@editor, this), new Motions.MoveLeft(@editor, this)]
+ 'delete-to-last-character-of-line': => [new Operators.Delete(@editor, this), new Motions.MoveToLastCharacterOfLine(@editor, this)]
+ 'toggle-case': => new Operators.ToggleCase(@editor, this)
+ 'upper-case': => new Operators.UpperCase(@editor, this)
+ 'lower-case': => new Operators.LowerCase(@editor, this)
+ 'toggle-case-now': => new Operators.ToggleCase(@editor, this, complete: true)
+ 'yank': => @linewiseAliasedOperator(Operators.Yank)
+ 'yank-line': => [new Operators.Yank(@editor, this), new Motions.MoveToRelativeLine(@editor, this)]
+ 'put-before': => new Operators.Put(@editor, this, location: 'before')
+ 'put-after': => new Operators.Put(@editor, this, location: 'after')
+ 'join': => new Operators.Join(@editor, this)
+ 'indent': => @linewiseAliasedOperator(Operators.Indent)
+ 'outdent': => @linewiseAliasedOperator(Operators.Outdent)
+ 'auto-indent': => @linewiseAliasedOperator(Operators.Autoindent)
+ 'increase': => new Operators.Increase(@editor, this)
+ 'decrease': => new Operators.Decrease(@editor, this)
+ 'move-left': => new Motions.MoveLeft(@editor, this)
+ 'move-up': => new Motions.MoveUp(@editor, this)
+ 'move-down': => new Motions.MoveDown(@editor, this)
+ 'move-right': => new Motions.MoveRight(@editor, this)
+ 'move-to-next-word': => new Motions.MoveToNextWord(@editor, this)
+ 'move-to-next-whole-word': => new Motions.MoveToNextWholeWord(@editor, this)
+ 'move-to-end-of-word': => new Motions.MoveToEndOfWord(@editor, this)
+ 'move-to-end-of-whole-word': => new Motions.MoveToEndOfWholeWord(@editor, this)
+ 'move-to-previous-word': => new Motions.MoveToPreviousWord(@editor, this)
+ 'move-to-previous-whole-word': => new Motions.MoveToPreviousWholeWord(@editor, this)
+ 'move-to-next-paragraph': => new Motions.MoveToNextParagraph(@editor, this)
+ 'move-to-previous-paragraph': => new Motions.MoveToPreviousParagraph(@editor, this)
+ 'move-to-first-character-of-line': => new Motions.MoveToFirstCharacterOfLine(@editor, this)
+ 'move-to-first-character-of-line-and-down': => new Motions.MoveToFirstCharacterOfLineAndDown(@editor, this)
+ 'move-to-last-character-of-line': => new Motions.MoveToLastCharacterOfLine(@editor, this)
+ 'move-to-last-nonblank-character-of-line-and-down': => new Motions.MoveToLastNonblankCharacterOfLineAndDown(@editor, this)
+ 'move-to-beginning-of-line': (e) => @moveOrRepeat(e)
+ 'move-to-first-character-of-line-up': => new Motions.MoveToFirstCharacterOfLineUp(@editor, this)
+ 'move-to-first-character-of-line-down': => new Motions.MoveToFirstCharacterOfLineDown(@editor, this)
+ 'move-to-start-of-file': => new Motions.MoveToStartOfFile(@editor, this)
+ 'move-to-line': => new Motions.MoveToAbsoluteLine(@editor, this)
+ 'move-to-top-of-screen': => new Motions.MoveToTopOfScreen(@editorElement, this)
+ 'move-to-bottom-of-screen': => new Motions.MoveToBottomOfScreen(@editorElement, this)
+ 'move-to-middle-of-screen': => new Motions.MoveToMiddleOfScreen(@editorElement, this)
+ 'scroll-down': => new Scroll.ScrollDown(@editorElement)
+ 'scroll-up': => new Scroll.ScrollUp(@editorElement)
+ 'scroll-cursor-to-top': => new Scroll.ScrollCursorToTop(@editorElement)
+ 'scroll-cursor-to-top-leave': => new Scroll.ScrollCursorToTop(@editorElement, {leaveCursor: true})
+ 'scroll-cursor-to-middle': => new Scroll.ScrollCursorToMiddle(@editorElement)
+ 'scroll-cursor-to-middle-leave': => new Scroll.ScrollCursorToMiddle(@editorElement, {leaveCursor: true})
+ 'scroll-cursor-to-bottom': => new Scroll.ScrollCursorToBottom(@editorElement)
+ 'scroll-cursor-to-bottom-leave': => new Scroll.ScrollCursorToBottom(@editorElement, {leaveCursor: true})
+ 'scroll-half-screen-up': => new Motions.ScrollHalfUpKeepCursor(@editorElement, this)
+ 'scroll-full-screen-up': => new Motions.ScrollFullUpKeepCursor(@editorElement, this)
+ 'scroll-half-screen-down': => new Motions.ScrollHalfDownKeepCursor(@editorElement, this)
+ 'scroll-full-screen-down': => new Motions.ScrollFullDownKeepCursor(@editorElement, this)
+ 'select-inside-word': => new TextObjects.SelectInsideWord(@editor)
+ 'select-inside-double-quotes': => new TextObjects.SelectInsideQuotes(@editor, '"', false)
+ 'select-inside-single-quotes': => new TextObjects.SelectInsideQuotes(@editor, '\'', false)
+ 'select-inside-back-ticks': => new TextObjects.SelectInsideQuotes(@editor, '`', false)
+ 'select-inside-curly-brackets': => new TextObjects.SelectInsideBrackets(@editor, '{', '}', false)
+ 'select-inside-angle-brackets': => new TextObjects.SelectInsideBrackets(@editor, '<', '>', false)
+ 'select-inside-tags': => new TextObjects.SelectInsideBrackets(@editor, '>', '<', false)
+ 'select-inside-square-brackets': => new TextObjects.SelectInsideBrackets(@editor, '[', ']', false)
+ 'select-inside-parentheses': => new TextObjects.SelectInsideBrackets(@editor, '(', ')', false)
+ 'select-inside-paragraph': => new TextObjects.SelectInsideParagraph(@editor, false)
+ 'select-a-word': => new TextObjects.SelectAWord(@editor)
+ 'select-around-double-quotes': => new TextObjects.SelectInsideQuotes(@editor, '"', true)
+ 'select-around-single-quotes': => new TextObjects.SelectInsideQuotes(@editor, '\'', true)
+ 'select-around-back-ticks': => new TextObjects.SelectInsideQuotes(@editor, '`', true)
+ 'select-around-curly-brackets': => new TextObjects.SelectInsideBrackets(@editor, '{', '}', true)
+ 'select-around-angle-brackets': => new TextObjects.SelectInsideBrackets(@editor, '<', '>', true)
+ 'select-around-square-brackets': => new TextObjects.SelectInsideBrackets(@editor, '[', ']', true)
+ 'select-around-parentheses': => new TextObjects.SelectInsideBrackets(@editor, '(', ')', true)
+ 'select-around-paragraph': => new TextObjects.SelectAParagraph(@editor, true)
+ 'register-prefix': (e) => @registerPrefix(e)
+ 'repeat': (e) => new Operators.Repeat(@editor, this)
+ 'repeat-search': (e) => new Motions.RepeatSearch(@editor, this)
+ 'repeat-search-backwards': (e) => new Motions.RepeatSearch(@editor, this).reversed()
+ 'move-to-mark': (e) => new Motions.MoveToMark(@editor, this)
+ 'move-to-mark-literal': (e) => new Motions.MoveToMark(@editor, this, false)
+ 'mark': (e) => new Operators.Mark(@editor, this)
+ 'find': (e) => new Motions.Find(@editor, this)
+ 'find-backwards': (e) => new Motions.Find(@editor, this).reverse()
+ 'till': (e) => new Motions.Till(@editor, this)
+ 'till-backwards': (e) => new Motions.Till(@editor, this).reverse()
+ 'repeat-find': (e) => @currentFind.repeat() if @currentFind?
+ 'repeat-find-reverse': (e) => @currentFind.repeat(reverse: true) if @currentFind?
+ 'replace': (e) => new Operators.Replace(@editor, this)
+ 'search': (e) => new Motions.Search(@editor, this)
+ 'reverse-search': (e) => (new Motions.Search(@editor, this)).reversed()
+ 'search-current-word': (e) => new Motions.SearchCurrentWord(@editor, this)
+ 'bracket-matching-motion': (e) => new Motions.BracketMatchingMotion(@editor, this)
+ 'reverse-search-current-word': (e) => (new Motions.SearchCurrentWord(@editor, this)).reversed()
+
+ # Private: Register multiple command handlers via an {Object} that maps
+ # command names to command handler functions.
+ #
+ # Prefixes the given command names with 'vim-mode:' to reduce redundancy in
+ # the provided object.
+ registerCommands: (commands) ->
+ for commandName, fn of commands
+ do (fn) =>
+ @subscriptions.add(atom.commands.add(@editorElement, "vim-mode:#{commandName}", fn))
+
+ # Private: Register multiple Operators via an {Object} that
+ # maps command names to functions that return operations to push.
+ #
+ # Prefixes the given command names with 'vim-mode:' to reduce redundancy in
+ # the given object.
+ registerOperationCommands: (operationCommands) ->
+ commands = {}
+ for commandName, operationFn of operationCommands
+ do (operationFn) =>
+ commands[commandName] = (event) => @pushOperations(operationFn(event))
+ @registerCommands(commands)
+
+ # Private: Push the given operations onto the operation stack, then process
+ # it.
+ pushOperations: (operations) ->
+ return unless operations?
+ operations = [operations] unless _.isArray(operations)
+
+ for operation in operations
+ # Motions in visual mode perform their selections.
+ if @mode is 'visual' and (operation instanceof Motions.Motion or operation instanceof TextObjects.TextObject)
+ operation.execute = operation.select
+
+ # if we have started an operation that responds to canComposeWith check if it can compose
+ # with the operation we're going to push onto the stack
+ if (topOp = @topOperation())? and topOp.canComposeWith? and not topOp.canComposeWith(operation)
+ @resetCommandMode()
+ @emitter.emit('failed-to-compose')
+ break
+
+ @opStack.push(operation)
+
+ # If we've received an operator in visual mode, mark the current
+ # selection as the motion to operate on.
+ if @mode is 'visual' and operation instanceof Operators.Operator
+ @opStack.push(new Motions.CurrentSelection(@editor, this))
+
+ @processOpStack()
+
+ onDidFailToCompose: (fn) ->
+ @emitter.on('failed-to-compose', fn)
+
+ onDidDestroy: (fn) ->
+ @emitter.on('did-destroy', fn)
+
+ # Private: Removes all operations from the stack.
+ #
+ # Returns nothing.
+ clearOpStack: ->
+ @opStack = []
+
+ undo: ->
+ @editor.undo()
+ @activateCommandMode()
+
+ # Private: Processes the command if the last operation is complete.
+ #
+ # Returns nothing.
+ processOpStack: ->
+ unless @opStack.length > 0
+ return
+
+ unless @topOperation().isComplete()
+ if @mode is 'command' and @topOperation() instanceof Operators.Operator
+ @activateOperatorPendingMode()
+ return
+
+ poppedOperation = @opStack.pop()
+ if @opStack.length
+ try
+ @topOperation().compose(poppedOperation)
+ @processOpStack()
+ catch e
+ if (e instanceof Operators.OperatorError) or (e instanceof Motions.MotionError)
+ @resetCommandMode()
+ else
+ throw e
+ else
+ @history.unshift(poppedOperation) if poppedOperation.isRecordable()
+ poppedOperation.execute()
+
+ # Private: Fetches the last operation.
+ #
+ # Returns the last operation.
+ topOperation: ->
+ _.last @opStack
+
+ # Private: Fetches the value of a given register.
+ #
+ # name - The name of the register to fetch.
+ #
+ # Returns the value of the given register or undefined if it hasn't
+ # been set.
+ getRegister: (name) ->
+ if name in ['*', '+']
+ text = atom.clipboard.read()
+ type = Utils.copyType(text)
+ {text, type}
+ else if name is '%'
+ text = @editor.getURI()
+ type = Utils.copyType(text)
+ {text, type}
+ else if name is "_" # Blackhole always returns nothing
+ text = ''
+ type = Utils.copyType(text)
+ {text, type}
+ else
+ @globalVimState.registers[name.toLowerCase()]
+
+ # Private: Fetches the value of a given mark.
+ #
+ # name - The name of the mark to fetch.
+ #
+ # Returns the value of the given mark or undefined if it hasn't
+ # been set.
+ getMark: (name) ->
+ if @marks[name]
+ @marks[name].getBufferRange().start
+ else
+ undefined
+
+ # Private: Sets the value of a given register.
+ #
+ # name - The name of the register to fetch.
+ # value - The value to set the register to.
+ #
+ # Returns nothing.
+ setRegister: (name, value) ->
+ if name in ['*', '+']
+ atom.clipboard.write(value.text)
+ else if name is '_'
+ # Blackhole register, nothing to do
+ else if /^[A-Z]$/.test(name)
+ @appendRegister(name.toLowerCase(), value)
+ else
+ @globalVimState.registers[name] = value
+
+
+ # Private: append a value into a given register
+ # like setRegister, but appends the value
+ appendRegister: (name, value) ->
+ register = @globalVimState.registers[name] ?=
+ type: 'character'
+ text: ""
+ if register.type is 'linewise' and value.type isnt 'linewise'
+ register.text += value.text + '\n'
+ else if register.type isnt 'linewise' and value.type is 'linewise'
+ register.text += '\n' + value.text
+ register.type = 'linewise'
+ else
+ register.text += value.text
+
+ # Private: Sets the value of a given mark.
+ #
+ # name - The name of the mark to fetch.
+ # pos {Point} - The value to set the mark to.
+ #
+ # Returns nothing.
+ setMark: (name, pos) ->
+ # check to make sure name is in [a-z] or is `
+ if (charCode = name.charCodeAt(0)) >= 96 and charCode <= 122
+ marker = @editor.markBufferRange(new Range(pos, pos), {invalidate: 'never', persistent: false})
+ @marks[name] = marker
+
+ # Public: Append a search to the search history.
+ #
+ # Motions.Search - The confirmed search motion to append
+ #
+ # Returns nothing
+ pushSearchHistory: (search) ->
+ @globalVimState.searchHistory.unshift search
+
+ # Public: Get the search history item at the given index.
+ #
+ # index - the index of the search history item
+ #
+ # Returns a search motion
+ getSearchHistoryItem: (index = 0) ->
+ @globalVimState.searchHistory[index]
+
+ ##############################################################################
+ # Mode Switching
+ ##############################################################################
+
+ # Private: Used to enable command mode.
+ #
+ # Returns nothing.
+ activateCommandMode: ->
+ @deactivateInsertMode()
+ @deactivateVisualMode()
+
+ @mode = 'command'
+ @submode = null
+
+ @changeModeClass('command-mode')
+
+ @clearOpStack()
+ selection.clear(autoscroll: false) for selection in @editor.getSelections()
+ for cursor in @editor.getCursors()
+ if cursor.isAtEndOfLine() and not cursor.isAtBeginningOfLine()
+ cursor.moveLeft()
+
+ @updateStatusBar()
+
+ # Private: Used to enable insert mode.
+ #
+ # Returns nothing.
+ activateInsertMode: ->
+ @mode = 'insert'
+ @editorElement.component.setInputEnabled(true)
+ @setInsertionCheckpoint()
+ @submode = null
+ @changeModeClass('insert-mode')
+ @updateStatusBar()
+
+ setInsertionCheckpoint: ->
+ @insertionCheckpoint = @editor.createCheckpoint() unless @insertionCheckpoint?
+
+ deactivateInsertMode: ->
+ return unless @mode in [null, 'insert']
+ @editorElement.component.setInputEnabled(false)
+ @editor.groupChangesSinceCheckpoint(@insertionCheckpoint)
+ changes = getChangesSinceCheckpoint(@editor.buffer, @insertionCheckpoint)
+ item = @inputOperator(@history[0])
+ @insertionCheckpoint = null
+ if item?
+ item.confirmChanges(changes)
+ for cursor in @editor.getCursors()
+ cursor.moveLeft() unless cursor.isAtBeginningOfLine()
+
+ deactivateVisualMode: ->
+ return unless @mode is 'visual'
+ for selection in @editor.getSelections()
+ selection.cursor.moveLeft() unless (selection.isEmpty() or selection.isReversed())
+
+ # Private: Get the input operator that needs to be told about about the
+ # typed undo transaction in a recently completed operation, if there
+ # is one.
+ inputOperator: (item) ->
+ return item unless item?
+ return item if item.inputOperator?()
+ return item.composedObject if item.composedObject?.inputOperator?()
+
+ # Private: Used to enable visual mode.
+ #
+ # type - One of 'characterwise', 'linewise' or 'blockwise'
+ #
+ # Returns nothing.
+ activateVisualMode: (type) ->
+ # Already in 'visual', this means one of following command is
+ # executed within `vim-mode.visual-mode`
+ # * activate-blockwise-visual-mode
+ # * activate-characterwise-visual-mode
+ # * activate-linewise-visual-mode
+ if @mode is 'visual'
+ if @submode is type
+ @activateCommandMode()
+ return
+
+ @submode = type
+ if @submode is 'linewise'
+ for selection in @editor.getSelections()
+ # Keep original range as marker's property to get back
+ # to characterwise.
+ # Since selectLine lost original cursor column.
+ originalRange = selection.getBufferRange()
+ selection.marker.setProperties({originalRange})
+ [start, end] = selection.getBufferRowRange()
+ selection.selectLine(row) for row in [start..end]
+
+ else if @submode in ['characterwise', 'blockwise']
+ # Currently, 'blockwise' is not yet implemented.
+ # So treat it as characterwise.
+ # Recover original range.
+ for selection in @editor.getSelections()
+ {originalRange} = selection.marker.getProperties()
+ if originalRange
+ [startRow, endRow] = selection.getBufferRowRange()
+ originalRange.start.row = startRow
+ originalRange.end.row = endRow
+ selection.setBufferRange(originalRange)
+ else
+ @deactivateInsertMode()
+ @mode = 'visual'
+ @submode = type
+ @changeModeClass('visual-mode')
+
+ if @submode is 'linewise'
+ @editor.selectLinesContainingCursors()
+ else if @editor.getSelectedText() is ''
+ @editor.selectRight()
+
+ @updateStatusBar()
+
+ # Private: Used to re-enable visual mode
+ resetVisualMode: ->
+ @activateVisualMode(@submode)
+
+ # Private: Used to enable operator-pending mode.
+ activateOperatorPendingMode: ->
+ @deactivateInsertMode()
+ @mode = 'operator-pending'
+ @submodule = null
+ @changeModeClass('operator-pending-mode')
+
+ @updateStatusBar()
+
+ changeModeClass: (targetMode) ->
+ for mode in ['command-mode', 'insert-mode', 'visual-mode', 'operator-pending-mode']
+ if mode is targetMode
+ @editorElement.classList.add(mode)
+ else
+ @editorElement.classList.remove(mode)
+
+ # Private: Resets the command mode back to it's initial state.
+ #
+ # Returns nothing.
+ resetCommandMode: ->
+ @clearOpStack()
+ @editor.clearSelections()
+ @activateCommandMode()
+
+ # Private: A generic way to create a Register prefix based on the event.
+ #
+ # e - The event that triggered the Register prefix.
+ #
+ # Returns nothing.
+ registerPrefix: (e) ->
+ keyboardEvent = e.originalEvent?.originalEvent ? e.originalEvent
+ name = atom.keymaps.keystrokeForKeyboardEvent(keyboardEvent)
+ if name.lastIndexOf('shift-', 0) is 0
+ name = name.slice(6)
+ new Prefixes.Register(name)
+
+ # Private: A generic way to create a Number prefix based on the event.
+ #
+ # e - The event that triggered the Number prefix.
+ #
+ # Returns nothing.
+ repeatPrefix: (e) ->
+ keyboardEvent = e.originalEvent?.originalEvent ? e.originalEvent
+ num = parseInt(atom.keymaps.keystrokeForKeyboardEvent(keyboardEvent))
+ if @topOperation() instanceof Prefixes.Repeat
+ @topOperation().addDigit(num)
+ else
+ if num is 0
+ e.abortKeyBinding()
+ else
+ @pushOperations(new Prefixes.Repeat(num))
+
+ reverseSelections: ->
+ for selection in @editor.getSelections()
+ reversed = not selection.isReversed()
+ selection.setBufferRange(selection.getBufferRange(), {reversed})
+
+ # Private: Figure out whether or not we are in a repeat sequence or we just
+ # want to move to the beginning of the line. If we are within a repeat
+ # sequence, we pass control over to @repeatPrefix.
+ #
+ # e - The triggered event.
+ #
+ # Returns new motion or nothing.
+ moveOrRepeat: (e) ->
+ if @topOperation() instanceof Prefixes.Repeat
+ @repeatPrefix(e)
+ null
+ else
+ new Motions.MoveToBeginningOfLine(@editor, this)
+
+ # Private: A generic way to handle Operators that can be repeated for
+ # their linewise form.
+ #
+ # constructor - The constructor of the operator.
+ #
+ # Returns nothing.
+ linewiseAliasedOperator: (constructor) ->
+ if @isOperatorPending(constructor)
+ new Motions.MoveToRelativeLine(@editor, this)
+ else
+ new constructor(@editor, this)
+
+ # Private: Check if there is a pending operation of a certain type, or
+ # if there is any pending operation, if no type given.
+ #
+ # constructor - The constructor of the object type you're looking for.
+ #
+ isOperatorPending: (constructor) ->
+ if constructor?
+ for op in @opStack
+ return op if op instanceof constructor
+ false
+ else
+ @opStack.length > 0
+
+ updateStatusBar: ->
+ @statusBarManager.update(@mode, @submode)
+
+# This uses private APIs and may break if TextBuffer is refactored.
+# Package authors - copy and paste this code at your own risk.
+getChangesSinceCheckpoint = (buffer, checkpoint) ->
+ {history} = buffer
+
+ if (index = history.getCheckpointIndex(checkpoint))?
+ history.undoStack.slice(index)
+ else
+ []